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,344 @@
1
+ // Canonical private runtime profile for HUE Controller: plug.
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
+ const cloneDeep = require('lodash/cloneDeep')
5
+ const dptlib = require('knxultimate').dptlib
6
+
7
+ module.exports = function (RED) {
8
+ function knxUltimateHuePlug (config) {
9
+ RED.nodes.createNode(this, config)
10
+ const node = this
11
+ node.serverKNX = RED.nodes.getNode(config.server) || undefined
12
+ node.serverHue = RED.nodes.getNode(config.serverHue) || undefined
13
+
14
+ node.topic = node.name
15
+ node.name = config.name === undefined || config.name === '' ? 'Hue Plug' : config.name
16
+ node.dpt = ''
17
+ node.notifyreadrequest = false
18
+ node.notifyreadrequestalsorespondtobus = 'false'
19
+ node.notifyreadrequestalsorespondtobusdefaultvalueifnotinitialized = ''
20
+ node.notifyresponse = false
21
+ node.notifywrite = true
22
+ node.initialread = true
23
+ node.listenallga = true // Don't remove
24
+ node.outputtype = 'write'
25
+ node.outputRBE = 'false'
26
+ node.inputRBE = 'false'
27
+ node.currentPayload = ''
28
+ node.passthrough = 'no'
29
+ node.formatmultiplyvalue = 1
30
+ node.formatnegativevalue = 'leave'
31
+ node.formatdecimalsvalue = 2
32
+
33
+ const hueDeviceParts = (config.hueDevice || '').split('#')
34
+ node.hueDevice = hueDeviceParts[0] || ''
35
+ node.hueDeviceType = hueDeviceParts[1] || 'plug'
36
+
37
+ node.initializingAtStart = config.readStatusAtStartup === undefined || config.readStatusAtStartup === 'yes'
38
+ node.enableNodePINS = config.enableNodePINS === 'yes'
39
+
40
+ node.inputs = node.enableNodePINS ? 1 : 0
41
+ node.outputs = node.enableNodePINS ? 1 : 0
42
+
43
+ node.currentHUEDevice = null
44
+ const pendingKnxMessages = []
45
+ const MAX_PENDING_KNX_MESSAGES = 5
46
+ const PENDING_KNX_TTL_MS = 10000
47
+ let pendingHueDeviceSnapshotPromise = null
48
+
49
+ const prunePendingKnxMessages = (now = Date.now()) => {
50
+ if (!pendingKnxMessages.length) return now
51
+ while (pendingKnxMessages.length > 0 && (now - pendingKnxMessages[0].enqueuedAt) > PENDING_KNX_TTL_MS) {
52
+ pendingKnxMessages.shift()
53
+ }
54
+ return now
55
+ }
56
+
57
+ const enqueuePendingKnxMessage = (msg) => {
58
+ const now = prunePendingKnxMessages()
59
+ const snapshot = {
60
+ msg: (() => {
61
+ try {
62
+ return cloneDeep(msg)
63
+ } catch (error) {
64
+ return msg
65
+ }
66
+ })(),
67
+ enqueuedAt: now
68
+ }
69
+ if (pendingKnxMessages.length >= MAX_PENDING_KNX_MESSAGES) {
70
+ pendingKnxMessages.shift()
71
+ }
72
+ pendingKnxMessages.push(snapshot)
73
+ }
74
+
75
+ const ensureCurrentHueDevice = async ({ forceRefresh = false } = {}) => {
76
+ if (!node.serverHue || typeof node.serverHue.getHueResourceSnapshot !== 'function') return undefined
77
+ if (!node.hueDevice) return undefined
78
+ if (pendingHueDeviceSnapshotPromise) {
79
+ try {
80
+ return await pendingHueDeviceSnapshotPromise
81
+ } catch (error) {
82
+ return undefined
83
+ }
84
+ }
85
+ pendingHueDeviceSnapshotPromise = (async () => {
86
+ try {
87
+ const resource = await node.serverHue.getHueResourceSnapshot(node.hueDevice, { forceRefresh })
88
+ if (!resource) return undefined
89
+ node.currentHUEDevice = cloneDeep(resource)
90
+ try {
91
+ node.handleSendHUE(node.currentHUEDevice)
92
+ } catch (error) {
93
+ RED.log.debug(`knxUltimateHuePlug: ensureCurrentHueDevice handleSendHUE error ${error.message}`)
94
+ }
95
+ const now = prunePendingKnxMessages()
96
+ const queued = pendingKnxMessages.splice(0).filter((entry) => (now - entry.enqueuedAt) <= PENDING_KNX_TTL_MS)
97
+ queued.forEach(({ msg }) => {
98
+ try {
99
+ node.handleSend(msg)
100
+ } catch (error) {
101
+ RED.log.warn(`knxUltimateHuePlug: replay queued KNX command error ${error.message}`)
102
+ }
103
+ })
104
+ return node.currentHUEDevice
105
+ } catch (error) {
106
+ RED.log.warn(`knxUltimateHuePlug: ensureCurrentHueDevice error ${error.message}`)
107
+ return undefined
108
+ } finally {
109
+ pendingHueDeviceSnapshotPromise = null
110
+ }
111
+ })()
112
+ return pendingHueDeviceSnapshotPromise
113
+ }
114
+
115
+ const pushStatus = (status) => {
116
+ if (!status) return
117
+ const provider = node.serverKNX
118
+ if (provider && typeof provider.applyStatusUpdate === 'function') {
119
+ provider.applyStatusUpdate(node, status)
120
+ } else {
121
+ node.status(status)
122
+ }
123
+ }
124
+
125
+ const updateStatus = (status) => {
126
+ if (!status) return
127
+ pushStatus(status)
128
+ }
129
+
130
+ const formatTs = (date) => {
131
+ const d = date instanceof Date ? date : new Date(date)
132
+ const provider = node.serverKNX
133
+ if (provider && typeof provider.formatStatusTimestamp === 'function') return provider.formatStatusTimestamp(d)
134
+ return `${d.getDate()}, ${d.toLocaleTimeString()}`
135
+ }
136
+
137
+ const safeSendToKNX = (telegram, context = 'write') => {
138
+ try {
139
+ if (!node.serverKNX || typeof node.serverKNX.sendKNXTelegramToKNXEngine !== 'function') {
140
+ const now = new Date()
141
+ updateStatus({ fill: 'red', shape: 'dot', text: `KNX server missing (${context}) (${formatTs(now)})` })
142
+ return
143
+ }
144
+ node.serverKNX.sendKNXTelegramToKNXEngine({ ...telegram, nodecallerid: node.id })
145
+ } catch (error) {
146
+ updateStatus({ fill: 'red', shape: 'dot', text: `KNX send error ${error.message}` })
147
+ }
148
+ }
149
+
150
+ node.setNodeStatus = ({
151
+ fill, shape, text, payload
152
+ }) => {
153
+ try {
154
+ if (payload === undefined) payload = ''
155
+ const dDate = new Date()
156
+ payload = typeof payload === 'object' ? JSON.stringify(payload) : payload.toString()
157
+ node.sKNXNodeStatusText = `|KNX: ${text} ${payload} (${formatTs(dDate)})`
158
+ updateStatus({ fill, shape, text: (node.sHUENodeStatusText || '') + ' ' + (node.sKNXNodeStatusText || '') })
159
+ } catch (error) { /* empty */ }
160
+ }
161
+
162
+ node.setNodeStatusHue = ({ fill, shape, text, payload }) => {
163
+ try {
164
+ if (payload === undefined) payload = ''
165
+ const dDate = new Date()
166
+ payload = typeof payload === 'object' ? JSON.stringify(payload) : payload.toString()
167
+ node.sHUENodeStatusText = `|HUE: ${text} ${payload} (${formatTs(dDate)})`
168
+ updateStatus({ fill, shape, text: node.sHUENodeStatusText + ' ' + (node.sKNXNodeStatusText || '') })
169
+ } catch (error) { /* empty */ }
170
+ }
171
+
172
+ node.updateKNXPlugState = function updateKNXPlugState (_value, _outputtype = 'write') {
173
+ if (!config.GAPlugState) return
174
+ const knxMsgPayload = {}
175
+ knxMsgPayload.topic = config.GAPlugState
176
+ knxMsgPayload.dpt = config.dptPlugState
177
+ knxMsgPayload.payload = Boolean(_value)
178
+ if (knxMsgPayload.topic) {
179
+ safeSendToKNX({
180
+ grpaddr: knxMsgPayload.topic,
181
+ payload: knxMsgPayload.payload,
182
+ dpt: knxMsgPayload.dpt,
183
+ outputtype: _outputtype
184
+ }, _outputtype)
185
+ }
186
+ node.setNodeStatusHue({
187
+ fill: 'blue',
188
+ shape: knxMsgPayload.payload ? 'dot' : 'ring',
189
+ text: 'HUE->KNX On/Off',
190
+ payload: knxMsgPayload.payload
191
+ })
192
+ }
193
+
194
+ node.updateKNXPlugPowerState = function updateKNXPlugPowerState (_value, _outputtype = 'write') {
195
+ if (!config.GAPlugPowerState) return
196
+ const knxMsgPayload = {}
197
+ knxMsgPayload.topic = config.GAPlugPowerState
198
+ knxMsgPayload.dpt = config.dptPlugPowerState
199
+ knxMsgPayload.payload = Boolean(_value)
200
+ if (knxMsgPayload.topic) {
201
+ safeSendToKNX({
202
+ grpaddr: knxMsgPayload.topic,
203
+ payload: knxMsgPayload.payload,
204
+ dpt: knxMsgPayload.dpt,
205
+ outputtype: _outputtype
206
+ }, _outputtype)
207
+ }
208
+ node.setNodeStatusHue({
209
+ fill: 'blue',
210
+ shape: knxMsgPayload.payload ? 'dot' : 'ring',
211
+ text: 'HUE->KNX Power',
212
+ payload: knxMsgPayload.payload
213
+ })
214
+ }
215
+
216
+ node.handleSend = (msg) => {
217
+ if (node.hueDevice === '') {
218
+ node.setNodeStatusHue({ fill: 'red', shape: 'ring', text: 'Missing HUE plug selection', payload: '' })
219
+ return
220
+ }
221
+ if (!node.currentHUEDevice) {
222
+ if (node.serverHue && node.serverHue.linkStatus === 'connected') {
223
+ node.setNodeStatusHue({ fill: 'yellow', shape: 'ring', text: 'Syncing with HUE bridge', payload: '' })
224
+ enqueuePendingKnxMessage(msg)
225
+ ensureCurrentHueDevice({ forceRefresh: true })
226
+ } else {
227
+ node.setNodeStatusHue({ fill: 'red', shape: 'ring', text: 'HUE bridge unavailable', payload: '' })
228
+ }
229
+ return
230
+ }
231
+ try {
232
+ if (msg.knx.event !== 'GroupValue_Read') {
233
+ switch (msg.knx.destination) {
234
+ case config.GAPlugSwitch: {
235
+ const value = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptPlugSwitch))
236
+ const desiredState = value === true || value === 1
237
+ node.serverHue?.hueManager.writeHueQueueAdd(node.hueDevice, { on: { on: desiredState } }, 'setPlug', node.hueDeviceType || 'plug')
238
+ node.setNodeStatusHue({
239
+ fill: 'green',
240
+ shape: 'dot',
241
+ text: 'KNX->HUE On/Off',
242
+ payload: desiredState
243
+ })
244
+ if (!node.currentHUEDevice) node.currentHUEDevice = {}
245
+ if (!node.currentHUEDevice.on) node.currentHUEDevice.on = {}
246
+ node.currentHUEDevice.on.on = desiredState
247
+ break
248
+ }
249
+ default:
250
+ break
251
+ }
252
+ } else {
253
+ switch (msg.knx.destination) {
254
+ case config.GAPlugState:
255
+ if (node.currentHUEDevice?.on?.on !== undefined) node.updateKNXPlugState(node.currentHUEDevice.on.on, 'response')
256
+ break
257
+ case config.GAPlugPowerState:
258
+ if (node.currentHUEDevice?.power_state?.power_state !== undefined) {
259
+ const powerState = node.currentHUEDevice.power_state.power_state === 'on'
260
+ node.updateKNXPlugPowerState(powerState, 'response')
261
+ }
262
+ break
263
+ default:
264
+ break
265
+ }
266
+ }
267
+ } catch (error) {
268
+ node.setNodeStatusHue({ fill: 'red', shape: 'dot', text: `KNX->HUE error ${error.message}`, payload: '' })
269
+ }
270
+ }
271
+
272
+ node.handleSendHUE = (_event) => {
273
+ try {
274
+ if (_event === undefined) return
275
+ const eventType = (_event.type || '').toLowerCase()
276
+ if (!['plug', 'smartplug', 'smart_plug', 'light'].includes(eventType)) return
277
+ if (_event.id !== node.hueDevice) return
278
+
279
+ node.currentHUEDevice = cloneDeep(_event)
280
+ const onState = _event.on?.on === true
281
+ node.updateKNXPlugState(onState)
282
+ if (_event.power_state && _event.power_state.power_state !== undefined) {
283
+ node.updateKNXPlugPowerState(_event.power_state.power_state === 'on')
284
+ }
285
+ node.setNodeStatusHue({
286
+ fill: onState ? 'green' : 'blue',
287
+ shape: onState ? 'dot' : 'ring',
288
+ text: 'HUE plug',
289
+ payload: onState
290
+ })
291
+ if (node.enableNodePINS) {
292
+ const flowMsg = {
293
+ payload: onState,
294
+ on: _event.on,
295
+ power_state: _event.power_state,
296
+ rawEvent: _event
297
+ }
298
+ node.send(flowMsg)
299
+ }
300
+ } catch (error) {
301
+ node.setNodeStatusHue({ fill: 'red', shape: 'dot', text: `HUE->KNX error ${error.message}`, payload: '' })
302
+ }
303
+ }
304
+
305
+ if (node.serverKNX) {
306
+ node.serverKNX.removeClient(node)
307
+ node.serverKNX.addClient(node)
308
+ }
309
+ if (node.serverHue) {
310
+ try {
311
+ node.serverHue.removeClient(node)
312
+ node.serverHue.addClient(node)
313
+ if (typeof node.serverHue.getHueResourceSnapshot === 'function') {
314
+ ensureCurrentHueDevice({ forceRefresh: false })
315
+ }
316
+ } catch (error) {
317
+ RED.log.error(`knxUltimateHuePlug: register client error ${error.message}`)
318
+ }
319
+ }
320
+
321
+ node.on('input', (msg, send, done) => {
322
+ if (!node.enableNodePINS) {
323
+ if (done) done()
324
+ return
325
+ }
326
+ try {
327
+ const state = RED.util.cloneMessage(msg)
328
+ node.serverHue?.hueManager.writeHueQueueAdd(node.hueDevice, state, 'setPlug', node.hueDeviceType || 'plug')
329
+ node.setNodeStatusHue({ fill: 'green', shape: 'dot', text: 'Flow->HUE', payload: state })
330
+ if (done) done()
331
+ } catch (error) {
332
+ node.setNodeStatusHue({ fill: 'red', shape: 'dot', text: `Flow error ${error.message}`, payload: '' })
333
+ if (done) done(error)
334
+ }
335
+ })
336
+
337
+ node.on('close', () => {
338
+ if (node.serverKNX) node.serverKNX.removeClient(node)
339
+ if (node.serverHue) node.serverHue.removeClient(node)
340
+ })
341
+ }
342
+
343
+ RED.nodes.registerType('knxUltimateHuePlug', knxUltimateHuePlug)
344
+ }
@@ -0,0 +1,271 @@
1
+ // Canonical private runtime profile for HUE Controller: relative_rotary.
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
+ /* eslint-disable max-len */
5
+ module.exports = function (RED) {
6
+ function knxUltimateHueTapDial (config) {
7
+ RED.nodes.createNode(this, config)
8
+ const node = this
9
+ node.serverKNX = RED.nodes.getNode(config.server) || undefined
10
+ node.serverHue = RED.nodes.getNode(config.serverHue) || undefined
11
+ node.topic = node.name
12
+ node.name = config.name === undefined ? 'Hue' : config.name
13
+ node.dpt = ''
14
+ node.notifyreadrequest = false
15
+ node.notifyreadrequestalsorespondtobus = 'false'
16
+ node.notifyreadrequestalsorespondtobusdefaultvalueifnotinitialized = ''
17
+ node.notifyresponse = false
18
+ node.notifywrite = true
19
+ node.initialread = true
20
+ node.listenallga = true // Don't remove
21
+ node.outputtype = 'write'
22
+ node.outputRBE = 'false' // Apply or not RBE to the output (Messages coming from flow)
23
+ node.inputRBE = 'false' // Apply or not RBE to the input (Messages coming from BUS)
24
+ node.currentPayload = '' // Current value for the RBE input and for the .previouspayload msg
25
+ node.passthrough = 'no'
26
+ node.formatmultiplyvalue = 1
27
+ node.formatnegativevalue = 'leave'
28
+ node.formatdecimalsvalue = 2
29
+ node.brightnessState = 0
30
+ node.isTimerDimStopRunning = false
31
+ node.hueDevice = config.hueDevice
32
+ const pinsSetting = (config.enableNodePINS === undefined || config.enableNodePINS === 'yes' || config.enableNodePINS === true)
33
+ node.enableNodePINS = pinsSetting ? 'yes' : 'no'
34
+ node.outputs = pinsSetting ? 1 : 0
35
+ if (!node.serverKNX && node.outputs === 0) {
36
+ node.enableNodePINS = 'yes'
37
+ node.outputs = 1
38
+ }
39
+ node.initializingAtStart = false
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
+ // Used to call the status update from the config node.
77
+ node.setNodeStatus = ({
78
+ fill, shape, text, payload
79
+ }) => {
80
+ try {
81
+ if (payload === undefined) payload = ''
82
+ const dDate = new Date()
83
+ payload = typeof payload === 'object' ? JSON.stringify(payload) : payload.toString()
84
+ node.sKNXNodeStatusText = `|KNX: ${text} ${payload} (${formatTs(dDate)})`
85
+ pushStatus({ fill, shape, text: (node.sHUENodeStatusText || '') + ' ' + (node.sKNXNodeStatusText || '') })
86
+ } catch (error) { }
87
+ }
88
+ // Used to call the status update from the HUE config node.
89
+ node.setNodeStatusHue = ({ fill, shape, text, payload }) => {
90
+ try {
91
+ if (payload === undefined) payload = ''
92
+ const dDate = new Date()
93
+ payload = typeof payload === 'object' ? JSON.stringify(payload) : payload.toString()
94
+ node.sHUENodeStatusText = `|HUE: ${text} ${payload} (${formatTs(dDate)})`
95
+ pushStatus({ fill, shape, text: node.sHUENodeStatusText + ' ' + (node.sKNXNodeStatusText || '') })
96
+ } catch (error) { }
97
+ }
98
+
99
+ // This function is called by the knx-ultimate config node, to output a msg.payload.
100
+ node.handleSend = (msg) => {
101
+ }
102
+
103
+ node.handleSendHUE = (_event) => {
104
+ try {
105
+ if (_event.id === config.hueDevice) {
106
+ if (!_event.hasOwnProperty('relative_rotary') ||
107
+ !_event.relative_rotary.hasOwnProperty('last_event') ||
108
+ _event.relative_rotary.last_event === undefined ||
109
+ !_event.relative_rotary.last_event.hasOwnProperty('rotation') ||
110
+ !_event.relative_rotary.last_event.rotation.direction === undefined ||
111
+ _event.relative_rotary.last_event.action === undefined) return
112
+
113
+ const knxMsgPayload = {}
114
+ knxMsgPayload.topic = config.GArepeat
115
+ knxMsgPayload.dpt = config.dptrepeat
116
+
117
+ if (_event.relative_rotary.last_event.rotation.direction === 'clock_wise') {
118
+ if (knxMsgPayload.dpt.startsWith('3.007')) {
119
+ if (node.isTimerDimStopRunning === false) {
120
+ // Set KNX Dim up/down start
121
+ knxMsgPayload.payload = { decr_incr: 1, data: 5 } // Send to KNX bus
122
+ if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
123
+ safeSendToKNX({
124
+ grpaddr: knxMsgPayload.topic,
125
+ payload: knxMsgPayload.payload,
126
+ dpt: knxMsgPayload.dpt,
127
+ outputtype: 'write'
128
+ }, 'write')
129
+ }
130
+ if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) updateStatus({ fill: 'green', shape: 'dot', text: 'HUE->KNX start Dim' + ` (${formatTs(new Date())})` })
131
+ }
132
+ node.startDimStopper(knxMsgPayload)
133
+ } else if (knxMsgPayload.dpt.startsWith('5.001')) {
134
+ // 0 - maximum: 32767
135
+ node.brightnessState < 100 ? node.brightnessState += 5 : node.brightnessState = 100
136
+ knxMsgPayload.payload = node.brightnessState
137
+ if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
138
+ safeSendToKNX({
139
+ grpaddr: knxMsgPayload.topic,
140
+ payload: knxMsgPayload.payload,
141
+ dpt: knxMsgPayload.dpt,
142
+ outputtype: 'write'
143
+ }, 'write')
144
+ }
145
+ } else if (knxMsgPayload.dpt.startsWith('232.600')) {
146
+ if (_event.relative_rotary.last_event.action === 'start') {
147
+ // Random color
148
+ function getRandomIntInclusive (min, max) {
149
+ min = Math.ceil(min)
150
+ max = Math.floor(max)
151
+ return Math.floor(Math.random() * (max - min + 1) + min) // The maximum is inclusive and the minimum is inclusive
152
+ }
153
+ knxMsgPayload.payload = { red: getRandomIntInclusive(0, 255), green: getRandomIntInclusive(0, 255), blue: getRandomIntInclusive(0, 255) }
154
+ // Send to KNX bus
155
+ if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
156
+ safeSendToKNX({
157
+ grpaddr: knxMsgPayload.topic,
158
+ payload: knxMsgPayload.payload,
159
+ dpt: knxMsgPayload.dpt,
160
+ outputtype: 'write'
161
+ }, 'write')
162
+ }
163
+ if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) updateStatus({ fill: 'green', shape: 'dot', text: 'HUE->KNX Change color clockwise' + ` (${formatTs(new Date())})` })
164
+ }
165
+ }
166
+ } else if (_event.relative_rotary.last_event.rotation.direction === 'counter_clock_wise') {
167
+ if (knxMsgPayload.dpt.startsWith('3.007')) {
168
+ if (node.isTimerDimStopRunning === false) {
169
+ // Set KNX Dim up/down start
170
+ knxMsgPayload.payload = { decr_incr: 0, data: 5 } // Send to KNX bus
171
+ if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
172
+ safeSendToKNX({
173
+ grpaddr: knxMsgPayload.topic,
174
+ payload: knxMsgPayload.payload,
175
+ dpt: knxMsgPayload.dpt,
176
+ outputtype: 'write'
177
+ }, 'write')
178
+ }
179
+ if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) updateStatus({ fill: 'green', shape: 'dot', text: 'HUE->KNX start Dim' + ` (${formatTs(new Date())})` })
180
+ }
181
+ node.startDimStopper(knxMsgPayload)
182
+ } else if (knxMsgPayload.dpt.startsWith('5.001')) {
183
+ node.brightnessState > 0 ? node.brightnessState -= 5 : node.brightnessState = 0
184
+ knxMsgPayload.payload = node.brightnessState
185
+ if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
186
+ safeSendToKNX({
187
+ grpaddr: knxMsgPayload.topic,
188
+ payload: knxMsgPayload.payload,
189
+ dpt: knxMsgPayload.dpt,
190
+ outputtype: 'write'
191
+ }, 'write')
192
+ }
193
+ } else if (knxMsgPayload.dpt.startsWith('232.600')) {
194
+ if (_event.relative_rotary.last_event.action === 'start') {
195
+ // Set white color
196
+ knxMsgPayload.payload = { red: 255, green: 255, blue: 255 }
197
+ // Send to KNX bus
198
+ if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
199
+ safeSendToKNX({
200
+ grpaddr: knxMsgPayload.topic,
201
+ payload: knxMsgPayload.payload,
202
+ dpt: knxMsgPayload.dpt,
203
+ outputtype: 'write'
204
+ }, 'write')
205
+ }
206
+ if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) updateStatus({ fill: 'green', shape: 'dot', text: 'HUE->KNX Change color counterclockwise' + ` (${formatTs(new Date())})` })
207
+ }
208
+ }
209
+ }
210
+
211
+ // Setup the output msg
212
+ knxMsgPayload.name = node.name
213
+ knxMsgPayload.event = `rotation ${_event.relative_rotary.last_event.rotation.direction}`
214
+ knxMsgPayload.payload = _event
215
+ node.send(knxMsgPayload)
216
+ node.setNodeStatusHue({
217
+ fill: 'blue', shape: 'ring', text: 'HUE->KNX', payload: knxMsgPayload.payload
218
+ })
219
+ }
220
+ } catch (error) {
221
+ updateStatus({ fill: 'red', shape: 'dot', text: `HUE->KNX error ${error.message} (${formatTs(new Date())})` })
222
+ }
223
+ }
224
+
225
+ // Timer to stop the dimming sequence
226
+ node.startDimStopper = function (knxMsgPayload) {
227
+ if (node.timerDimStop !== undefined) clearTimeout(node.timerDimStop)
228
+ node.isTimerDimStopRunning = true
229
+ node.timerDimStop = setTimeout(() => {
230
+ // KNX Stop DIM
231
+ knxMsgPayload.payload = { decr_incr: 0, data: 0 } // Payload for the output msg
232
+ // Send to KNX bus
233
+ if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
234
+ safeSendToKNX({
235
+ grpaddr: knxMsgPayload.topic,
236
+ payload: knxMsgPayload.payload,
237
+ dpt: knxMsgPayload.dpt,
238
+ outputtype: 'write'
239
+ }, 'write')
240
+ }
241
+ if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) updateStatus({ fill: 'green', shape: 'dot', text: 'HUE->KNX Stop DIM' + ` (${formatTs(new Date())})` })
242
+ node.isTimerDimStopRunning = false
243
+ }, 500)
244
+ }
245
+
246
+ // On each deploy, unsubscribe+resubscribe
247
+ if (node.serverKNX) {
248
+ node.serverKNX.removeClient(node)
249
+ node.serverKNX.addClient(node)
250
+ }
251
+ if (node.serverHue) {
252
+ node.serverHue.removeClient(node)
253
+ node.serverHue.addClient(node)
254
+ }
255
+
256
+ node.on('input', (msg) => {
257
+
258
+ })
259
+
260
+ node.on('close', (done) => {
261
+ if (node.serverKNX) {
262
+ node.serverKNX.removeClient(node)
263
+ }
264
+ if (node.serverHue) {
265
+ node.serverHue.removeClient(node)
266
+ }
267
+ done()
268
+ })
269
+ }
270
+ RED.nodes.registerType('knxUltimateHueTapDial', knxUltimateHueTapDial)
271
+ }