node-red-contrib-knx-ultimate 6.2.2 → 6.2.3-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/nodes/knxUltimateAI.js +7 -1
  3. package/nodes/knxUltimateHueAreaMotion.html +16 -4
  4. package/nodes/knxUltimateHueBattery.html +16 -4
  5. package/nodes/knxUltimateHueButton.html +16 -4
  6. package/nodes/knxUltimateHueCameraMotion.html +16 -4
  7. package/nodes/knxUltimateHueContactSensor.html +16 -4
  8. package/nodes/knxUltimateHueController.html +1259 -0
  9. package/nodes/knxUltimateHueController.js +28 -0
  10. package/nodes/knxUltimateHueHumiditySensor.html +16 -4
  11. package/nodes/knxUltimateHueLight.html +16 -4
  12. package/nodes/knxUltimateHueLightSensor.html +16 -4
  13. package/nodes/knxUltimateHueMotion.html +16 -4
  14. package/nodes/knxUltimateHuePlug.html +16 -4
  15. package/nodes/knxUltimateHueScene.html +19 -7
  16. package/nodes/knxUltimateHueTapDial.html +16 -4
  17. package/nodes/knxUltimateHueTemperatureSensor.html +16 -4
  18. package/nodes/knxUltimateHueZigbeeConnectivity.html +16 -4
  19. package/nodes/knxUltimateHuedevice_software_update.html +16 -4
  20. package/nodes/locales/de/knxUltimateHueAreaMotion.html +1 -1
  21. package/nodes/locales/de/knxUltimateHueBattery.html +1 -0
  22. package/nodes/locales/de/knxUltimateHueButton.html +1 -0
  23. package/nodes/locales/de/knxUltimateHueCameraMotion.html +1 -0
  24. package/nodes/locales/de/knxUltimateHueContactSensor.html +1 -0
  25. package/nodes/locales/de/knxUltimateHueController.html +668 -0
  26. package/nodes/locales/de/knxUltimateHueController.json +47 -0
  27. package/nodes/locales/de/knxUltimateHueHumiditySensor.html +1 -0
  28. package/nodes/locales/de/knxUltimateHueLight.html +1 -0
  29. package/nodes/locales/de/knxUltimateHueLightSensor.html +1 -0
  30. package/nodes/locales/de/knxUltimateHueMotion.html +1 -0
  31. package/nodes/locales/de/knxUltimateHuePlug.html +1 -0
  32. package/nodes/locales/de/knxUltimateHueScene.html +1 -0
  33. package/nodes/locales/de/knxUltimateHueTapDial.html +1 -0
  34. package/nodes/locales/de/knxUltimateHueTemperatureSensor.html +1 -0
  35. package/nodes/locales/de/knxUltimateHueZigbeeConnectivity.html +1 -0
  36. package/nodes/locales/de/knxUltimateHuedevice_software_update.html +1 -0
  37. package/nodes/locales/en/knxUltimateHueAreaMotion.html +1 -1
  38. package/nodes/locales/en/knxUltimateHueBattery.html +1 -0
  39. package/nodes/locales/en/knxUltimateHueButton.html +1 -0
  40. package/nodes/locales/en/knxUltimateHueCameraMotion.html +1 -0
  41. package/nodes/locales/en/knxUltimateHueContactSensor.html +1 -0
  42. package/nodes/locales/en/knxUltimateHueController.html +671 -0
  43. package/nodes/locales/en/knxUltimateHueController.json +47 -0
  44. package/nodes/locales/en/knxUltimateHueHumiditySensor.html +1 -0
  45. package/nodes/locales/en/knxUltimateHueLight.html +1 -0
  46. package/nodes/locales/en/knxUltimateHueLightSensor.html +1 -0
  47. package/nodes/locales/en/knxUltimateHueMotion.html +1 -0
  48. package/nodes/locales/en/knxUltimateHuePlug.html +1 -0
  49. package/nodes/locales/en/knxUltimateHueScene.html +1 -0
  50. package/nodes/locales/en/knxUltimateHueTapDial.html +1 -0
  51. package/nodes/locales/en/knxUltimateHueTemperatureSensor.html +1 -0
  52. package/nodes/locales/en/knxUltimateHueZigbeeConnectivity.html +1 -0
  53. package/nodes/locales/en/knxUltimateHuedevice_software_update.html +1 -0
  54. package/nodes/locales/es/knxUltimateHueAreaMotion.html +1 -1
  55. package/nodes/locales/es/knxUltimateHueBattery.html +1 -0
  56. package/nodes/locales/es/knxUltimateHueButton.html +1 -0
  57. package/nodes/locales/es/knxUltimateHueCameraMotion.html +1 -0
  58. package/nodes/locales/es/knxUltimateHueContactSensor.html +1 -0
  59. package/nodes/locales/es/knxUltimateHueController.html +671 -0
  60. package/nodes/locales/es/knxUltimateHueController.json +47 -0
  61. package/nodes/locales/es/knxUltimateHueHumiditySensor.html +1 -0
  62. package/nodes/locales/es/knxUltimateHueLight.html +1 -0
  63. package/nodes/locales/es/knxUltimateHueLightSensor.html +1 -0
  64. package/nodes/locales/es/knxUltimateHueMotion.html +1 -0
  65. package/nodes/locales/es/knxUltimateHuePlug.html +1 -0
  66. package/nodes/locales/es/knxUltimateHueScene.html +1 -0
  67. package/nodes/locales/es/knxUltimateHueTapDial.html +1 -0
  68. package/nodes/locales/es/knxUltimateHueTemperatureSensor.html +1 -0
  69. package/nodes/locales/es/knxUltimateHueZigbeeConnectivity.html +1 -0
  70. package/nodes/locales/es/knxUltimateHuedevice_software_update.html +1 -0
  71. package/nodes/locales/fr/knxUltimateHueAreaMotion.html +1 -1
  72. package/nodes/locales/fr/knxUltimateHueBattery.html +1 -0
  73. package/nodes/locales/fr/knxUltimateHueButton.html +1 -0
  74. package/nodes/locales/fr/knxUltimateHueCameraMotion.html +1 -0
  75. package/nodes/locales/fr/knxUltimateHueContactSensor.html +1 -0
  76. package/nodes/locales/fr/knxUltimateHueController.html +671 -0
  77. package/nodes/locales/fr/knxUltimateHueController.json +47 -0
  78. package/nodes/locales/fr/knxUltimateHueHumiditySensor.html +1 -0
  79. package/nodes/locales/fr/knxUltimateHueLight.html +1 -0
  80. package/nodes/locales/fr/knxUltimateHueLightSensor.html +1 -0
  81. package/nodes/locales/fr/knxUltimateHueMotion.html +1 -0
  82. package/nodes/locales/fr/knxUltimateHuePlug.html +1 -0
  83. package/nodes/locales/fr/knxUltimateHueScene.html +1 -0
  84. package/nodes/locales/fr/knxUltimateHueTapDial.html +1 -0
  85. package/nodes/locales/fr/knxUltimateHueTemperatureSensor.html +1 -0
  86. package/nodes/locales/fr/knxUltimateHueZigbeeConnectivity.html +1 -0
  87. package/nodes/locales/fr/knxUltimateHuedevice_software_update.html +1 -0
  88. package/nodes/locales/it/knxUltimateHueAreaMotion.html +1 -1
  89. package/nodes/locales/it/knxUltimateHueBattery.html +1 -0
  90. package/nodes/locales/it/knxUltimateHueButton.html +1 -0
  91. package/nodes/locales/it/knxUltimateHueCameraMotion.html +1 -0
  92. package/nodes/locales/it/knxUltimateHueContactSensor.html +1 -0
  93. package/nodes/locales/it/knxUltimateHueController.html +671 -0
  94. package/nodes/locales/it/knxUltimateHueController.json +47 -0
  95. package/nodes/locales/it/knxUltimateHueHumiditySensor.html +1 -0
  96. package/nodes/locales/it/knxUltimateHueLight.html +1 -0
  97. package/nodes/locales/it/knxUltimateHueLightSensor.html +1 -0
  98. package/nodes/locales/it/knxUltimateHueMotion.html +1 -0
  99. package/nodes/locales/it/knxUltimateHuePlug.html +1 -0
  100. package/nodes/locales/it/knxUltimateHueScene.html +1 -0
  101. package/nodes/locales/it/knxUltimateHueTapDial.html +1 -0
  102. package/nodes/locales/it/knxUltimateHueTemperatureSensor.html +1 -0
  103. package/nodes/locales/it/knxUltimateHueZigbeeConnectivity.html +1 -0
  104. package/nodes/locales/it/knxUltimateHuedevice_software_update.html +1 -0
  105. package/nodes/locales/zh-CN/knxUltimateHueAreaMotion.html +1 -1
  106. package/nodes/locales/zh-CN/knxUltimateHueBattery.html +1 -0
  107. package/nodes/locales/zh-CN/knxUltimateHueButton.html +1 -0
  108. package/nodes/locales/zh-CN/knxUltimateHueCameraMotion.html +1 -0
  109. package/nodes/locales/zh-CN/knxUltimateHueContactSensor.html +1 -0
  110. package/nodes/locales/zh-CN/knxUltimateHueController.html +673 -0
  111. package/nodes/locales/zh-CN/knxUltimateHueController.json +47 -0
  112. package/nodes/locales/zh-CN/knxUltimateHueHumiditySensor.html +1 -0
  113. package/nodes/locales/zh-CN/knxUltimateHueLight.html +1 -0
  114. package/nodes/locales/zh-CN/knxUltimateHueLightSensor.html +1 -0
  115. package/nodes/locales/zh-CN/knxUltimateHueMotion.html +1 -0
  116. package/nodes/locales/zh-CN/knxUltimateHuePlug.html +1 -0
  117. package/nodes/locales/zh-CN/knxUltimateHueScene.html +1 -0
  118. package/nodes/locales/zh-CN/knxUltimateHueTapDial.html +1 -0
  119. package/nodes/locales/zh-CN/knxUltimateHueTemperatureSensor.html +1 -0
  120. package/nodes/locales/zh-CN/knxUltimateHueZigbeeConnectivity.html +1 -0
  121. package/nodes/locales/zh-CN/knxUltimateHuedevice_software_update.html +1 -0
  122. package/nodes/utils/hueControllerProfileAdapter.js +104 -0
  123. package/nodes/utils/hueControllerProfiles/runtime/area_motion.js +221 -0
  124. package/nodes/utils/hueControllerProfiles/runtime/button.js +295 -0
  125. package/nodes/utils/hueControllerProfiles/runtime/camera_motion.js +221 -0
  126. package/nodes/utils/hueControllerProfiles/runtime/contact.js +191 -0
  127. package/nodes/utils/hueControllerProfiles/runtime/device_power.js +185 -0
  128. package/nodes/utils/hueControllerProfiles/runtime/device_software_update.js +185 -0
  129. package/nodes/utils/hueControllerProfiles/runtime/humidity.js +204 -0
  130. package/nodes/utils/hueControllerProfiles/runtime/light.js +1583 -0
  131. package/nodes/utils/hueControllerProfiles/runtime/light_level.js +180 -0
  132. package/nodes/utils/hueControllerProfiles/runtime/motion.js +185 -0
  133. package/nodes/utils/hueControllerProfiles/runtime/plug.js +344 -0
  134. package/nodes/utils/hueControllerProfiles/runtime/relative_rotary.js +271 -0
  135. package/nodes/utils/hueControllerProfiles/runtime/scene.js +352 -0
  136. package/nodes/utils/hueControllerProfiles/runtime/temperature.js +187 -0
  137. package/nodes/utils/hueControllerProfiles/runtime/zigbee_connectivity.js +185 -0
  138. package/package.json +5 -2
  139. package/resources/hueControllerMigration.js +338 -0
  140. package/resources/hueControllerMigrationDialog.js +235 -0
  141. package/resources/hueControllerProfiles.js +9290 -0
@@ -0,0 +1,295 @@
1
+ // Canonical private runtime profile for HUE Controller: button.
2
+ // This module is shipped with the Controller but is intentionally absent from
3
+ // package.json, so Node-RED never registers it as a separate palette node.
4
+ module.exports = function (RED) {
5
+ const dptlib = require('knxultimate').dptlib
6
+
7
+ function knxUltimateHueButton (config) {
8
+ RED.nodes.createNode(this, config)
9
+ const node = this
10
+ node.serverKNX = RED.nodes.getNode(config.server) || undefined
11
+ node.serverHue = RED.nodes.getNode(config.serverHue) || undefined
12
+ node.topic = node.name
13
+ node.name = config.name === undefined ? 'Hue' : config.name
14
+ node.dpt = ''
15
+ node.notifyreadrequest = false
16
+ node.notifyreadrequestalsorespondtobus = 'false'
17
+ node.notifyreadrequestalsorespondtobusdefaultvalueifnotinitialized = ''
18
+ node.notifyresponse = false
19
+ node.notifywrite = true
20
+ node.initialread = true
21
+ node.listenallga = true // Don't remove
22
+ node.outputtype = 'write'
23
+ node.outputRBE = 'false' // Apply or not RBE to the output (Messages coming from flow)
24
+ node.inputRBE = 'false' // Apply or not RBE to the input (Messages coming from BUS)
25
+ node.currentPayload = '' // Current value for the RBE input and for the .previouspayload msg
26
+ node.passthrough = 'no'
27
+ node.formatmultiplyvalue = 1
28
+ node.formatnegativevalue = 'leave'
29
+ node.formatdecimalsvalue = 2
30
+ node.short_releaseValue = false
31
+ node.isTimerDimStopRunning = false
32
+ node.hueDevice = config.hueDevice
33
+ node.initializingAtStart = false
34
+
35
+ // When toggle status is disabled, uses these values
36
+ node.switchSend = config.switchSend === undefined ? 'true' : config.switchSend
37
+ node.switchSend = node.switchSend === 'true' // The typedvalue in the html returns a string, so i convert it to bool
38
+ node.dimSend = config.dimSend === undefined ? 'up' : config.dimSend
39
+ if (node.dimSend === 'up') node.dimSend = { decr_incr: 1, data: 3 }
40
+ if (node.dimSend === 'down') node.dimSend = { decr_incr: 0, data: 3 }
41
+ if (node.dimSend === 'stop') node.dimSend = { decr_incr: 0, data: 0 }
42
+
43
+ const pushStatus = (status) => {
44
+ if (!status) return
45
+ const provider = node.serverKNX
46
+ if (provider && typeof provider.applyStatusUpdate === 'function') {
47
+ provider.applyStatusUpdate(node, status)
48
+ } else {
49
+ node.status(status)
50
+ }
51
+ }
52
+
53
+ const updateStatus = (status) => {
54
+ if (!status) return
55
+ pushStatus(status)
56
+ }
57
+
58
+ const formatTs = (date) => {
59
+ const d = date instanceof Date ? date : new Date(date)
60
+ const provider = node.serverKNX
61
+ if (provider && typeof provider.formatStatusTimestamp === 'function') return provider.formatStatusTimestamp(d)
62
+ return `${d.getDate()}, ${d.toLocaleTimeString()}`
63
+ }
64
+
65
+ const safeSendToKNX = (telegram, context = 'write') => {
66
+ try {
67
+ if (!node.serverKNX || typeof node.serverKNX.sendKNXTelegramToKNXEngine !== 'function') {
68
+ const now = new Date()
69
+ updateStatus({ fill: 'red', shape: 'dot', text: `KNX server missing (${context}) (${formatTs(now)})` })
70
+ return
71
+ }
72
+ node.serverKNX.sendKNXTelegramToKNXEngine({ ...telegram, nodecallerid: node.id })
73
+ } catch (error) {
74
+ updateStatus({ fill: 'red', shape: 'dot', text: `KNX send error ${error.message}` })
75
+ }
76
+ }
77
+
78
+ // Used to call the status update from the config node.
79
+ node.setNodeStatus = ({
80
+ fill, shape, text, payload
81
+ }) => {
82
+ try {
83
+ if (payload === undefined) payload = ''
84
+ const dDate = new Date()
85
+ payload = typeof payload === 'object' ? JSON.stringify(payload) : payload.toString()
86
+ node.sKNXNodeStatusText = `|KNX: ${text} ${payload} (${formatTs(dDate)})`
87
+ updateStatus({ fill, shape, text: (node.sHUENodeStatusText || '') + ' ' + (node.sKNXNodeStatusText || '') })
88
+ } catch (error) { }
89
+ }
90
+ // Used to call the status update from the HUE config node.
91
+ node.setNodeStatusHue = ({ fill, shape, text, payload }) => {
92
+ try {
93
+ if (payload === undefined) payload = ''
94
+ const dDate = new Date()
95
+ payload = typeof payload === 'object' ? JSON.stringify(payload) : payload.toString()
96
+ node.sHUENodeStatusText = `|HUE: ${text} ${payload} (${formatTs(dDate)})`
97
+ updateStatus({ fill, shape, text: node.sHUENodeStatusText + ' ' + (node.sKNXNodeStatusText || '') })
98
+ } catch (error) { }
99
+ }
100
+
101
+ // This function is called by the knx-ultimate config node, to output a msg.payload.
102
+ node.handleSend = (msg) => {
103
+ try {
104
+ switch (msg.knx.destination) {
105
+ case config.GAshort_releaseStatus:
106
+ msg.payload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptshort_release))
107
+ node.short_releaseValue = msg.payload
108
+ node.setNodeStatusHue({
109
+ fill: 'green', shape: 'dot', text: 'KNX->HUE Short Release Status', payload: msg.payload
110
+ })
111
+ break
112
+ case config.GArepeatStatus:
113
+ msg.payload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptrepeat))
114
+ node.toggleGArepeat = msg.payload.decr_incr === 1
115
+ node.setNodeStatusHue({
116
+ fill: 'green', shape: 'dot', text: 'KNX->HUE Repeat Status', payload: msg.payload
117
+ })
118
+ break
119
+ default:
120
+ break
121
+ }
122
+ } catch (error) {
123
+ node.setNodeStatusHue({
124
+ fill: 'red', shape: 'dot', text: `KNX->HUE error ${error.message}`, payload: ''
125
+ })
126
+ }
127
+ }
128
+
129
+ node.handleSendHUE = (_event) => {
130
+ try {
131
+ if (_event.id === config.hueDevice) {
132
+ const buttonEvent = _event?.button?.button_report?.event || _event?.button?.last_event
133
+ if (!_event.hasOwnProperty('button') || buttonEvent === undefined) return
134
+
135
+ const knxMsgPayload = {}
136
+ let flowMsgPayload = true
137
+ let didUpdateStatus = false
138
+ // Handling events with toggles
139
+ // KNX Dimming reminder tips
140
+ // { decr_incr: 1, data: 1 } : Start increasing until { decr_incr: 0, data: 0 } is received.
141
+ // { decr_incr: 0, data: 1 } : Start decreasing until { decr_incr: 0, data: 0 } is received.
142
+ switch (buttonEvent) {
143
+ case 'initial_press':
144
+ if (node.initial_pressValue === undefined) node.initial_pressValue = false
145
+ node.initial_pressValue = config.toggleValues ? !node.initial_pressValue : node.switchSend
146
+ flowMsgPayload = node.initial_pressValue
147
+ break
148
+ case 'long_release':
149
+ flowMsgPayload = node.long_pressValue
150
+ // if the dimmer was running, send the STOP telegram to the KNX bus wires, using the GArepeat Group address and dpt.
151
+ if (node.isTimerDimStopRunning) {
152
+ knxMsgPayload.topic = config.GArepeat
153
+ knxMsgPayload.dpt = config.dptrepeat
154
+ node.stopDIM(knxMsgPayload)
155
+ }
156
+ break
157
+ case 'double_short_release':
158
+ if (node.double_short_releaseValue === undefined) node.double_short_releaseValue = false
159
+ node.double_short_releaseValue = config.toggleValues ? !node.double_short_releaseValue : node.switchSend
160
+ flowMsgPayload = node.double_short_releaseValue
161
+ break
162
+ case 'long_press':
163
+ if (node.long_pressValue === undefined) node.long_pressValue = false
164
+ node.long_pressValue = config.toggleValues ? !node.long_pressValue : node.dimSend
165
+ flowMsgPayload = node.long_pressValue
166
+ break
167
+ case 'short_release':
168
+ node.short_releaseValue = config.toggleValues ? !node.short_releaseValue : node.switchSend
169
+ flowMsgPayload = node.short_releaseValue
170
+ if (config.GAshort_release !== undefined && config.GAshort_release !== '') {
171
+ knxMsgPayload.topic = config.GAshort_release
172
+ knxMsgPayload.dpt = config.dptshort_release
173
+ knxMsgPayload.payload = node.short_releaseValue
174
+ // Send to KNX bus
175
+ if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
176
+ safeSendToKNX({
177
+ grpaddr: knxMsgPayload.topic, payload: knxMsgPayload.payload, dpt: knxMsgPayload.dpt, outputtype: 'write'
178
+ }, 'write')
179
+ }
180
+ if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
181
+ node.setNodeStatusHue({
182
+ fill: 'blue', shape: 'dot', text: `HUE->KNX ${buttonEvent}`, payload: knxMsgPayload.payload
183
+ })
184
+ didUpdateStatus = true
185
+ }
186
+ }
187
+ break
188
+ case 'repeat':
189
+ flowMsgPayload = node.long_pressValue
190
+ if (config.GArepeat !== undefined && config.GArepeat !== '') {
191
+ if (node.isTimerDimStopRunning === false) {
192
+ // Set KNX Dim up/down start
193
+ knxMsgPayload.topic = config.GArepeat
194
+ knxMsgPayload.dpt = config.dptrepeat
195
+ if (typeof (node.long_pressValue) === 'object') {
196
+ knxMsgPayload.payload = node.long_pressValue // Send fixed value when toggleValues is false
197
+ } else {
198
+ knxMsgPayload.payload = node.long_pressValue ? { decr_incr: 0, data: 3 } : { decr_incr: 1, data: 3 } // If the light is turned on, the initial DIM direction must be down, otherwise, up
199
+ }
200
+ // Send to KNX bus
201
+ if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
202
+ safeSendToKNX({
203
+ grpaddr: knxMsgPayload.topic, payload: knxMsgPayload.payload, dpt: knxMsgPayload.dpt, outputtype: 'write'
204
+ }, 'write')
205
+ }
206
+ if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
207
+ node.setNodeStatusHue({
208
+ fill: 'blue', shape: 'dot', text: 'HUE->KNX START DIM', payload: ''
209
+ })
210
+ didUpdateStatus = true
211
+ }
212
+ }
213
+ node.startDimStopper(knxMsgPayload)
214
+ }
215
+ break
216
+ default:
217
+ break
218
+ }
219
+
220
+ if (!didUpdateStatus) {
221
+ node.setNodeStatusHue({
222
+ fill: 'blue',
223
+ shape: 'dot',
224
+ text: `HUE ${buttonEvent}`,
225
+ payload: flowMsgPayload
226
+ })
227
+ }
228
+
229
+ // Setup the output msg
230
+ const flowMsg = {}
231
+ flowMsg.name = node.name
232
+ flowMsg.event = buttonEvent
233
+ if (_event.button?.button_report?.updated) flowMsg.updated = _event.button.button_report.updated
234
+ flowMsg.rawEvent = _event
235
+ flowMsg.payload = flowMsgPayload
236
+ node.send(flowMsg)
237
+ }
238
+ } catch (error) {
239
+ node.setNodeStatusHue({
240
+ fill: 'red', shape: 'dot', text: `HUE->KNX error ${error.message}`, payload: ''
241
+ })
242
+ }
243
+ }
244
+
245
+ // Timer to stop the dimming sequence
246
+ node.startDimStopper = function (knxMsgPayload) {
247
+ if (node.timerDimStop !== undefined) clearTimeout(node.timerDimStop)
248
+ node.isTimerDimStopRunning = true
249
+ node.timerDimStop = setTimeout(() => {
250
+ node.stopDIM(knxMsgPayload)
251
+ }, 2000)
252
+ }
253
+
254
+ node.stopDIM = function (knxMsgPayload) {
255
+ // KNX Stop DIM
256
+ if (node.timerDimStop !== undefined) clearTimeout(node.timerDimStop)
257
+ node.isTimerDimStopRunning = false
258
+ knxMsgPayload.payload = { decr_incr: 0, data: 0 } // Payload for the output msg
259
+ // Send to KNX bus
260
+ if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
261
+ safeSendToKNX({
262
+ grpaddr: knxMsgPayload.topic, payload: knxMsgPayload.payload, dpt: knxMsgPayload.dpt, outputtype: 'write'
263
+ }, 'write')
264
+ node.setNodeStatusHue({
265
+ fill: 'grey', shape: 'ring', text: 'HUE->KNX STOP DIM', payload: knxMsgPayload.payload
266
+ })
267
+ }
268
+ }
269
+
270
+ // On each deploy, unsubscribe+resubscribe
271
+ if (node.serverKNX) {
272
+ node.serverKNX.removeClient(node)
273
+ node.serverKNX.addClient(node)
274
+ }
275
+ if (node.serverHue) {
276
+ node.serverHue.removeClient(node)
277
+ node.serverHue.addClient(node)
278
+ }
279
+
280
+ node.on('input', (msg) => {
281
+
282
+ })
283
+
284
+ node.on('close', (done) => {
285
+ if (node.serverKNX) {
286
+ node.serverKNX.removeClient(node)
287
+ }
288
+ if (node.serverHue) {
289
+ node.serverHue.removeClient(node)
290
+ }
291
+ done()
292
+ })
293
+ }
294
+ RED.nodes.registerType('knxUltimateHueButton', knxUltimateHueButton)
295
+ }
@@ -0,0 +1,221 @@
1
+ // Canonical private runtime profile for HUE Controller: camera_motion.
2
+ // This module is shipped with the Controller but is intentionally absent from
3
+ // package.json, so Node-RED never registers it as a separate palette node.
4
+ module.exports = function (RED) {
5
+ function knxUltimateHueCameraMotion (config) {
6
+ RED.nodes.createNode(this, config)
7
+ const node = this
8
+ node.serverKNX = RED.nodes.getNode(config.server) || undefined
9
+ node.serverHue = RED.nodes.getNode(config.serverHue) || undefined
10
+ node.topic = node.name
11
+ node.name = config.name === undefined ? 'Hue' : config.name
12
+ node.dpt = ''
13
+ node.notifyreadrequest = true
14
+ node.notifyreadrequestalsorespondtobus = 'false'
15
+ node.notifyreadrequestalsorespondtobusdefaultvalueifnotinitialized = ''
16
+ node.notifyresponse = false
17
+ node.notifywrite = true
18
+ node.initialread = true
19
+ node.listenallga = true // Don't remove
20
+ node.outputtype = 'write'
21
+ node.outputRBE = 'false'
22
+ node.inputRBE = 'false'
23
+ node.currentPayload = ''
24
+ node.passthrough = 'no'
25
+ node.formatmultiplyvalue = 1
26
+ node.formatnegativevalue = 'leave'
27
+ node.formatdecimalsvalue = 2
28
+ node.hueDevice = config.hueDevice
29
+ node.dptCameraMotion = config.dptCameraMotion
30
+ node.motionDPT = (config.dptCameraMotion && config.dptCameraMotion !== '') ? config.dptCameraMotion : '1.001'
31
+ node.initializingAtStart = (config.readStatusAtStartup === undefined || config.readStatusAtStartup === 'yes')
32
+ node.currentDeviceValue = false
33
+ node.enableNodePINS = (config.enableNodePINS === undefined || config.enableNodePINS === 'yes')
34
+ node.outputs = node.enableNodePINS ? 1 : 0
35
+
36
+ if (!node.serverKNX && node.enableNodePINS !== 'yes') {
37
+ node.enableNodePINS = 'yes'
38
+ node.outputs = 1
39
+ }
40
+
41
+ const pushStatus = (status) => {
42
+ if (!status) return
43
+ const provider = node.serverKNX
44
+ if (provider && typeof provider.applyStatusUpdate === 'function') {
45
+ provider.applyStatusUpdate(node, status)
46
+ } else {
47
+ node.status(status)
48
+ }
49
+ }
50
+
51
+ const updateStatus = (status) => {
52
+ if (!status) return
53
+ pushStatus(status)
54
+ }
55
+
56
+ const formatTs = (date) => {
57
+ const d = date instanceof Date ? date : new Date(date)
58
+ const provider = node.serverKNX
59
+ if (provider && typeof provider.formatStatusTimestamp === 'function') return provider.formatStatusTimestamp(d)
60
+ return `${d.getDate()}, ${d.toLocaleTimeString()}`
61
+ }
62
+
63
+ const safeSendToKNX = (telegram, context = 'write') => {
64
+ try {
65
+ if (!node.serverKNX || typeof node.serverKNX.sendKNXTelegramToKNXEngine !== 'function') {
66
+ const now = new Date()
67
+ updateStatus({ fill: 'red', shape: 'dot', text: `KNX server missing (${context}) (${formatTs(now)})` })
68
+ return
69
+ }
70
+ node.serverKNX.sendKNXTelegramToKNXEngine({ ...telegram, nodecallerid: node.id })
71
+ } catch (error) {
72
+ updateStatus({ fill: 'red', shape: 'dot', text: `KNX send error ${error.message}` })
73
+ }
74
+ }
75
+
76
+ node.setNodeStatus = ({ fill, shape, text, payload }) => {
77
+ try {
78
+ if (payload === undefined) payload = ''
79
+ const dDate = new Date()
80
+ payload = typeof payload === 'object' ? JSON.stringify(payload) : payload.toString()
81
+ node.sKNXNodeStatusText = `|KNX: ${text} ${payload} (${formatTs(dDate)})`
82
+ pushStatus({ fill, shape, text: (node.sHUENodeStatusText || '') + ' ' + (node.sKNXNodeStatusText || '') })
83
+ } catch (error) { /* empty */ }
84
+ }
85
+
86
+ node.setNodeStatusHue = ({ fill, shape, text, payload }) => {
87
+ try {
88
+ if (payload === undefined) payload = ''
89
+ const dDate = new Date()
90
+ payload = typeof payload === 'object' ? JSON.stringify(payload) : payload.toString()
91
+ node.sHUENodeStatusText = `|HUE: ${text} ${payload} (${formatTs(dDate)})`
92
+ pushStatus({ fill, shape, text: node.sHUENodeStatusText + ' ' + (node.sKNXNodeStatusText || '') })
93
+ } catch (error) { /* empty */ }
94
+ }
95
+
96
+ const mapMotionValue = (_event) => {
97
+ const motion = _event?.motion
98
+ if (!motion) return undefined
99
+
100
+ const readBoolean = (candidate) => {
101
+ if (typeof candidate === 'boolean') return candidate
102
+ if (candidate && typeof candidate.value === 'boolean') return candidate.value
103
+ return undefined
104
+ }
105
+
106
+ let value = readBoolean(motion.motion)
107
+ if (value === undefined && motion.motion_report) {
108
+ value = readBoolean(motion.motion_report.motion)
109
+ }
110
+ if (value === undefined && motion.motion_report && typeof motion.motion_report.motion === 'boolean') {
111
+ value = motion.motion_report.motion
112
+ }
113
+ if (value === undefined && typeof motion.motion_valid === 'boolean') {
114
+ value = motion.motion_valid
115
+ }
116
+ if (value === undefined) {
117
+ value = readBoolean(motion.motion_valid)
118
+ }
119
+ return value
120
+ }
121
+
122
+ node.handleSend = (msg) => {
123
+ if (!msg || !msg.knx) return
124
+ if (msg.knx.event === 'GroupValue_Read') {
125
+ switch (msg.knx.destination) {
126
+ case config.GAcameraMotion:
127
+ node.sendResponseToKNX(node.currentDeviceValue)
128
+ break
129
+ default:
130
+ break
131
+ }
132
+ }
133
+ }
134
+
135
+ node.handleSendHUE = (_event) => {
136
+ try {
137
+ if (_event.id !== config.hueDevice) return
138
+ const motionValue = mapMotionValue(_event)
139
+ if (motionValue === undefined) return
140
+
141
+ const knxMsgPayload = {
142
+ topic: config.GAcameraMotion,
143
+ dpt: node.motionDPT,
144
+ payload: motionValue === true || motionValue === 1 || motionValue === '1' ? true : Boolean(motionValue),
145
+ name: node.name,
146
+ event: 'motion',
147
+ rawEvent: _event
148
+ }
149
+
150
+ if (knxMsgPayload.topic) {
151
+ safeSendToKNX({
152
+ grpaddr: knxMsgPayload.topic,
153
+ payload: knxMsgPayload.payload,
154
+ dpt: knxMsgPayload.dpt,
155
+ outputtype: 'write'
156
+ }, 'write')
157
+ }
158
+ node.currentDeviceValue = knxMsgPayload.payload
159
+
160
+ updateStatus({
161
+ fill: 'green',
162
+ shape: 'dot',
163
+ text: `HUE->KNX ${knxMsgPayload.payload} (${formatTs(new Date())})`
164
+ })
165
+
166
+ if (node.enableNodePINS) {
167
+ node.send(knxMsgPayload)
168
+ }
169
+ node.setNodeStatusHue({
170
+ fill: 'blue',
171
+ shape: 'ring',
172
+ text: 'HUE->KNX',
173
+ payload: knxMsgPayload.payload
174
+ })
175
+ } catch (error) {
176
+ updateStatus({
177
+ fill: 'red',
178
+ shape: 'dot',
179
+ text: `HUE->KNX error ${error.message} (${formatTs(new Date())})`
180
+ })
181
+ }
182
+ }
183
+
184
+ node.sendResponseToKNX = (_level) => {
185
+ if (!node.serverKNX) return
186
+ const knxMsgPayload = {
187
+ topic: config.GAcameraMotion,
188
+ dpt: node.motionDPT,
189
+ payload: !!(_level === true || _level === 1 || _level === '1')
190
+ }
191
+ if (knxMsgPayload.topic) {
192
+ safeSendToKNX({
193
+ grpaddr: knxMsgPayload.topic,
194
+ payload: knxMsgPayload.payload,
195
+ dpt: knxMsgPayload.dpt,
196
+ outputtype: 'response'
197
+ }, 'response')
198
+ }
199
+ }
200
+
201
+ if (node.serverKNX) {
202
+ node.serverKNX.removeClient(node)
203
+ node.serverKNX.addClient(node)
204
+ }
205
+ if (node.serverHue) {
206
+ node.serverHue.removeClient(node)
207
+ node.serverHue.addClient(node)
208
+ }
209
+
210
+ node.on('close', (done) => {
211
+ if (node.serverKNX) {
212
+ node.serverKNX.removeClient(node)
213
+ }
214
+ if (node.serverHue) {
215
+ node.serverHue.removeClient(node)
216
+ }
217
+ done()
218
+ })
219
+ }
220
+ RED.nodes.registerType('knxUltimateHueCameraMotion', knxUltimateHueCameraMotion)
221
+ }