node-red-contrib-knx-ultimate 5.2.5 → 6.0.0

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 (77) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/nodes/commonFunctions.js +40 -3
  3. package/nodes/knxUltimate-config.js +379 -335
  4. package/nodes/knxUltimate.html +8 -9
  5. package/nodes/knxUltimate.js +5 -5
  6. package/nodes/knxUltimateAI.js +1 -1
  7. package/nodes/knxUltimateHueLight.html +3 -3
  8. package/nodes/knxUltimateHueLight.js +1 -1
  9. package/nodes/knxUltimateMatterBridge.html +321 -247
  10. package/nodes/knxUltimateMatterBridge.js +48 -1
  11. package/nodes/knxUltimateMatterControllerDevice.html +2119 -0
  12. package/nodes/knxUltimateMatterControllerDevice.js +1701 -0
  13. package/nodes/locales/de/knxUltimateMatterBridge.html +6 -2
  14. package/nodes/locales/de/knxUltimateMatterBridge.json +14 -1
  15. package/nodes/locales/de/knxUltimateMatterControllerDevice.html +27 -0
  16. package/nodes/locales/de/knxUltimateMatterControllerDevice.json +97 -0
  17. package/nodes/locales/de/matter-config.json +5 -1
  18. package/nodes/locales/de/matterbridge-config.json +1 -0
  19. package/nodes/locales/en/knxUltimateMatterBridge.html +6 -2
  20. package/nodes/locales/en/knxUltimateMatterBridge.json +14 -1
  21. package/nodes/locales/en/knxUltimateMatterControllerDevice.html +27 -0
  22. package/nodes/locales/en/knxUltimateMatterControllerDevice.json +97 -0
  23. package/nodes/locales/en/matter-config.json +6 -1
  24. package/nodes/locales/en/matterbridge-config.json +1 -0
  25. package/nodes/locales/es/knxUltimateMatterBridge.html +6 -2
  26. package/nodes/locales/es/knxUltimateMatterBridge.json +14 -1
  27. package/nodes/locales/es/knxUltimateMatterControllerDevice.html +27 -0
  28. package/nodes/locales/es/knxUltimateMatterControllerDevice.json +97 -0
  29. package/nodes/locales/es/matter-config.json +5 -1
  30. package/nodes/locales/es/matterbridge-config.json +1 -0
  31. package/nodes/locales/fr/knxUltimateMatterBridge.html +6 -2
  32. package/nodes/locales/fr/knxUltimateMatterBridge.json +14 -1
  33. package/nodes/locales/fr/knxUltimateMatterControllerDevice.html +27 -0
  34. package/nodes/locales/fr/knxUltimateMatterControllerDevice.json +89 -0
  35. package/nodes/locales/fr/matter-config.json +5 -1
  36. package/nodes/locales/fr/matterbridge-config.json +1 -0
  37. package/nodes/locales/it/knxUltimateMatterBridge.html +6 -2
  38. package/nodes/locales/it/knxUltimateMatterBridge.json +14 -1
  39. package/nodes/locales/it/knxUltimateMatterControllerDevice.html +27 -0
  40. package/nodes/locales/it/knxUltimateMatterControllerDevice.json +97 -0
  41. package/nodes/locales/it/matter-config.json +6 -1
  42. package/nodes/locales/it/matterbridge-config.json +1 -0
  43. package/nodes/locales/zh-CN/knxUltimateMatterBridge.html +6 -2
  44. package/nodes/locales/zh-CN/knxUltimateMatterBridge.json +14 -1
  45. package/nodes/locales/zh-CN/knxUltimateMatterControllerDevice.html +27 -0
  46. package/nodes/locales/zh-CN/knxUltimateMatterControllerDevice.json +89 -0
  47. package/nodes/locales/zh-CN/matter-config.json +5 -1
  48. package/nodes/locales/zh-CN/matterbridge-config.json +1 -0
  49. package/nodes/matter-config.html +58 -6
  50. package/nodes/matter-config.js +7 -2
  51. package/nodes/matterbridge-config.html +1 -0
  52. package/nodes/utils/lightEngines/canonicalLight.js +75 -0
  53. package/nodes/utils/lightEngines/hueLightEngine.js +51 -0
  54. package/nodes/utils/lightEngines/index.js +25 -0
  55. package/nodes/utils/lightEngines/knxLightCommand.js +79 -0
  56. package/nodes/utils/lightEngines/lightEngine.js +46 -0
  57. package/nodes/utils/lightEngines/matterHueShim.js +59 -0
  58. package/nodes/utils/lightEngines/matterLightEngine.js +117 -0
  59. package/nodes/utils/matterEngine.mjs +185 -14
  60. package/nodes/utils/matterKnxConverter.js +1 -1
  61. package/package.json +11 -3
  62. package/resources/KNXSendSnippets.js +53 -53
  63. package/resources/htmlUtils.js +6 -4
  64. package/nodes/knxUltimateMatterDevice.html +0 -632
  65. package/nodes/knxUltimateMatterDevice.js +0 -308
  66. package/nodes/locales/de/knxUltimateMatterDevice.html +0 -56
  67. package/nodes/locales/de/knxUltimateMatterDevice.json +0 -109
  68. package/nodes/locales/en/knxUltimateMatterDevice.html +0 -56
  69. package/nodes/locales/en/knxUltimateMatterDevice.json +0 -109
  70. package/nodes/locales/es/knxUltimateMatterDevice.html +0 -56
  71. package/nodes/locales/es/knxUltimateMatterDevice.json +0 -109
  72. package/nodes/locales/fr/knxUltimateMatterDevice.html +0 -56
  73. package/nodes/locales/fr/knxUltimateMatterDevice.json +0 -109
  74. package/nodes/locales/it/knxUltimateMatterDevice.html +0 -56
  75. package/nodes/locales/it/knxUltimateMatterDevice.json +0 -109
  76. package/nodes/locales/zh-CN/knxUltimateMatterDevice.html +0 -56
  77. package/nodes/locales/zh-CN/knxUltimateMatterDevice.json +0 -109
@@ -0,0 +1,1701 @@
1
+ /* eslint-disable no-param-reassign */
2
+ /* eslint-disable camelcase */
3
+ /* eslint-disable max-len */
4
+ /* eslint-disable no-lonely-if */
5
+ /* eslint-disable no-case-declarations */
6
+ /* eslint-disable no-unused-vars */
7
+ const cloneDeep = require('lodash/cloneDeep')
8
+ const dptlib = require('knxultimate').dptlib
9
+ const hueColorConverter = require('./utils/colorManipulators/hueColorConverter')
10
+ const { createLightEngine } = require('./utils/lightEngines')
11
+ const { hueStateToCanonical, matterEventToHuePatch } = require('./utils/lightEngines/matterHueShim')
12
+
13
+ module.exports = function (RED) {
14
+ function knxUltimateMatterControllerDevice (config) {
15
+ RED.nodes.createNode(this, config)
16
+ const node = this
17
+ node.serverKNX = RED.nodes.getNode(config.server) || undefined
18
+ node.serverMatter = RED.nodes.getNode(config.serverMatter) || undefined
19
+ node.matterNodeId = config.matterNodeId !== undefined ? String(config.matterNodeId) : ''
20
+ node.matterEndpointId = config.matterEndpointId !== undefined ? Number(config.matterEndpointId) : 1
21
+ node.matterDeviceName = config.matterDeviceName !== undefined ? String(config.matterDeviceName) : ''
22
+
23
+ // Matter engine (adapter). The manager is resolved lazily at each write, because
24
+ // matter-config creates its matterManager asynchronously after the deploy.
25
+ node.lightEngine = createLightEngine('matter', {
26
+ deviceId: node.matterNodeId,
27
+ nodeId: node.matterNodeId,
28
+ endpointId: node.matterEndpointId
29
+ })
30
+
31
+ // Facade impersonating the hue-config node. The entire light logic below was born
32
+ // in the Hue Light node and is kept UNCHANGED: it speaks Hue-native state objects,
33
+ // and this boundary translates them to the Matter engine (strategy/adapter).
34
+ node.serverHue = {
35
+ get linkStatus () { return node.serverMatter ? node.serverMatter.linkStatus : 'disconnected' },
36
+ hueManager: {
37
+ writeHueQueueAdd: (_lightID, _state, _operation) => {
38
+ try {
39
+ if (!node.lightEngine) return
40
+ const patch = hueStateToCanonical(_state)
41
+ if (!patch) return
42
+ node.lightEngine.manager = node.serverMatter ? node.serverMatter.matterManager : undefined
43
+ node.lightEngine.applyState(patch)
44
+ } catch (error) {
45
+ RED.log.error('knxUltimateMatterControllerDevice: writeHueQueueAdd shim: ' + (error && error.message))
46
+ }
47
+ },
48
+ deleteHueQueue: () => { }
49
+ },
50
+ addClient: (_node) => { try { if (node.serverMatter) node.serverMatter.addClient(_node) } catch (error) { /* empty */ } },
51
+ removeClient: (_node) => { if (node.serverMatter) { try { node.serverMatter.removeClient(_node) } catch (error) { /* empty */ } } },
52
+ getAllLightsBelongingToTheGroup: async () => [],
53
+ getHueResourceSnapshot: async () => node.currentHUEDevice,
54
+ getAverageColorsXYBrightnessAndTemperature: async () => undefined
55
+ }
56
+
57
+ // Convert for backward compatibility
58
+ if (config.nameLightKelvinDIM === undefined) {
59
+ config.nameLightKelvinDIM = config.nameLightHSV
60
+ config.GALightKelvinDIM = config.GALightHSV
61
+ config.dptLightKelvinDIM = config.dptLightHSV
62
+
63
+ config.nameLightKelvinPercentage = config.nameLightHSVPercentage
64
+ config.GALightKelvinPercentage = config.GALightHSVPercentage
65
+ config.dptLightKelvinPercentage = config.dptLightHSVPercentage
66
+
67
+ config.nameLightKelvinPercentageState = config.nameLightHSVState
68
+ config.GALightKelvinPercentageState = config.GALightHSVState
69
+ config.dptLightKelvinPercentageState = config.dptLightHSVState
70
+ }
71
+
72
+ node.name = config.name || node.matterDeviceName || 'Control Matter from KNX'
73
+ node.topic = node.name
74
+ node.outputtopic = node.name
75
+ node.dpt = ''
76
+ node.notifyreadrequest = true
77
+ node.notifyreadrequestalsorespondtobus = 'false'
78
+ node.notifyreadrequestalsorespondtobusdefaultvalueifnotinitialized = ''
79
+ node.notifyresponse = false
80
+ node.notifywrite = true
81
+ node.initialread = true
82
+ node.listenallga = true // Don't remove
83
+ node.outputtype = 'write'
84
+ node.outputRBE = 'false' // Apply or not RBE to the output (Messages coming from flow)
85
+ node.inputRBE = 'false' // Apply or not RBE to the input (Messages coming from BUS)
86
+ node.currentPayload = '' // Current value for the RBE input and for the .previouspayload msg
87
+ node.passthrough = 'no'
88
+ node.formatmultiplyvalue = 1
89
+ node.formatnegativevalue = 'leave'
90
+ node.formatdecimalsvalue = 2
91
+ node.knxUltimateAcceptedGAs = [
92
+ config.GALightSwitch,
93
+ config.GALightState,
94
+ config.GALightDIM,
95
+ config.GALightColor,
96
+ config.GALightColorState,
97
+ config.GALightHSV_H_DIM,
98
+ config.GALightHSV_H_State,
99
+ config.GALightHSV_S_DIM,
100
+ config.GALightHSV_S_State,
101
+ config.GALightKelvinDIM,
102
+ config.GALightKelvinPercentage,
103
+ config.GALightKelvinPercentageState,
104
+ config.GALightKelvin,
105
+ config.GALightKelvinState,
106
+ config.GALightBrightness,
107
+ config.GALightBrightnessState,
108
+ config.GALightBlink,
109
+ config.GALightColorCycle,
110
+ config.GADaylightSensor
111
+ ].map((ga) => String(ga || '').trim()).filter((ga) => ga !== '')
112
+ const isConfiguredKNXGA = (ga) => node.knxUltimateAcceptedGAs.includes(String(ga || '').trim())
113
+ // Synthetic "current device" in Hue shape, kept in sync from the Matter attribute
114
+ // reports. It lets the whole Hue-born logic below run untouched on a Matter light.
115
+ node.currentHUEDevice = {
116
+ id: node.matterNodeId,
117
+ type: 'light',
118
+ on: { on: false },
119
+ dimming: { brightness: 100, min_dim_level: 0 },
120
+ color: { xy: { x: 0.3127, y: 0.329 }, gamut: undefined, gamut_type: undefined },
121
+ color_temperature: { mirek: 333, mirek_valid: true, mirek_schema: { mirek_minimum: 153, mirek_maximum: 500 } }
122
+ }
123
+ node.HUEDeviceWhileDaytime = null// This retains the HUE device status while daytime, to be restored after nighttime elapsed.
124
+ node.HUELightsBelongingToGroupWhileDaytime = null // Array contains all light belonging to the grouped_light (if grouped_light is selected)
125
+ node.DayTime = true
126
+ node.isGrouped_light = false // No grouped lights on Matter
127
+ node.hueDevice = node.matterNodeId
128
+ node.initializingAtStart = (config.readStatusAtStartup === undefined || config.readStatusAtStartup === 'yes')
129
+ config.specifySwitchOnBrightness = (config.specifySwitchOnBrightness === undefined || config.specifySwitchOnBrightness === '') ? 'temperature' : config.specifySwitchOnBrightness
130
+ config.specifySwitchOnBrightnessNightTime = (config.specifySwitchOnBrightnessNightTime === undefined || config.specifySwitchOnBrightnessNightTime === '') ? 'no' : config.specifySwitchOnBrightnessNightTime
131
+ config.colorAtSwitchOnDayTime = (config.colorAtSwitchOnDayTime === '' || config.colorAtSwitchOnDayTime === undefined) ? '{ "kelvin":3000, "brightness":100 }' : config.colorAtSwitchOnDayTime
132
+ config.colorAtSwitchOnNightTime = (config.colorAtSwitchOnNightTime === '' || config.colorAtSwitchOnNightTime === undefined) ? '{ "kelvin":2700, "brightness":20 }' : config.colorAtSwitchOnNightTime
133
+ config.colorAtSwitchOnDayTime = config.colorAtSwitchOnDayTime.replace('geen', 'green')
134
+ config.colorAtSwitchOnNightTime = config.colorAtSwitchOnNightTime.replace('geen', 'green')
135
+ config.dimSpeed = (config.dimSpeed === undefined || config.dimSpeed === '') ? 5000 : Number(config.dimSpeed)
136
+ config.HSVDimSpeed = (config.HSVDimSpeed === undefined || config.HSVDimSpeed === '') ? 5000 : Number(config.HSVDimSpeed)
137
+ config.invertDimTunableWhiteDirection = config.invertDimTunableWhiteDirection !== undefined
138
+ config.restoreDayMode = config.restoreDayMode === undefined ? 'no' : config.restoreDayMode // no or setDayByFastSwitchLightSingle or setDayByFastSwitchLightALL
139
+ config.updateLocalStateFromKNXWrite = config.updateLocalStateFromKNXWrite === true || config.updateLocalStateFromKNXWrite === 'true' // Starting from v 4.1.31
140
+ node.timerCheckForFastLightSwitch = null
141
+ config.invertDayNight = config.invertDayNight === undefined ? false : config.invertDayNight
142
+ node.HSVObject = null // { h, s, v };// Store the current light calculated HSV
143
+
144
+ // Transform HEX in RGB and stringified json in json oblects.
145
+ if (config.colorAtSwitchOnDayTime.indexOf('#') !== -1) {
146
+ // Transform to rgb.
147
+ try {
148
+ config.colorAtSwitchOnDayTime = hueColorConverter.ColorConverter.hexRgb(config.colorAtSwitchOnDayTime.replace('#', ''))
149
+ } catch (error) {
150
+ config.colorAtSwitchOnDayTime = { kelvin: 3000, brightness: 100 }
151
+ }
152
+ } else {
153
+ try {
154
+ config.colorAtSwitchOnDayTime = JSON.parse(config.colorAtSwitchOnDayTime)
155
+ } catch (error) {
156
+ RED.log.error(`knxUltimateMatterControllerDevice: config.colorAtSwitchOnDayTime = JSON.parse(config.colorAtSwitchOnDayTime): ${error.message} : ${error.stack || ''} `)
157
+ config.colorAtSwitchOnDayTime = ''
158
+ }
159
+ }
160
+ // Same thing, but with night color
161
+ if (config.colorAtSwitchOnNightTime.indexOf('#') !== -1) {
162
+ // Transform to rgb.
163
+ try {
164
+ config.colorAtSwitchOnNightTime = hueColorConverter.ColorConverter.hexRgb(config.colorAtSwitchOnNightTime.replace('#', ''))
165
+ } catch (error) {
166
+ config.colorAtSwitchOnNightTime = { kelvin: 2700, brightness: 20 }
167
+ }
168
+ } else {
169
+ try {
170
+ config.colorAtSwitchOnNightTime = JSON.parse(config.colorAtSwitchOnNightTime)
171
+ } catch (error) {
172
+ RED.log.error(`knxUltimateMatterControllerDevice: config.colorAtSwitchOnDayTime = JSON.parse(config.colorAtSwitchOnNightTime): ${error.message} : ${error.stack || ''} `)
173
+ config.colorAtSwitchOnNightTime = ''
174
+ }
175
+ }
176
+
177
+ const formatTs = (date) => {
178
+ const d = date instanceof Date ? date : new Date(date)
179
+ const provider = node.serverKNX
180
+ if (provider && typeof provider.formatStatusTimestamp === 'function') return provider.formatStatusTimestamp(d)
181
+ return `${d.getDate()}, ${d.toLocaleTimeString()}`
182
+ }
183
+
184
+ node.syncCurrentHUEDeviceFromKNXState = function syncCurrentHUEDeviceFromKNXState (_state) { // Starting from v 4.1.31
185
+ if (config.updateLocalStateFromKNXWrite !== true) return // Starting from v 4.1.31
186
+ if (_state === undefined || _state === null || typeof _state !== 'object') return // Starting from v 4.1.31
187
+ if (node.currentHUEDevice === undefined || node.currentHUEDevice === null) return // Starting from v 4.1.31
188
+ if (_state.on !== undefined && typeof _state.on.on === 'boolean') { // Starting from v 4.1.31
189
+ if (node.currentHUEDevice.on === undefined || node.currentHUEDevice.on === null) node.currentHUEDevice.on = {} // Starting from v 4.1.31
190
+ node.currentHUEDevice.on.on = _state.on.on // Starting from v 4.1.31
191
+ } // Starting from v 4.1.31
192
+ if (_state.dimming !== undefined && _state.dimming !== null && _state.dimming.brightness !== undefined) { // Starting from v 4.1.31
193
+ if (node.currentHUEDevice.dimming === undefined || node.currentHUEDevice.dimming === null) node.currentHUEDevice.dimming = {} // Starting from v 4.1.31
194
+ node.currentHUEDevice.dimming.brightness = _state.dimming.brightness // Starting from v 4.1.31
195
+ } // Starting from v 4.1.31
196
+ if (_state.color !== undefined && _state.color !== null && _state.color.xy !== undefined) { // Starting from v 4.1.31
197
+ if (node.currentHUEDevice.color === undefined || node.currentHUEDevice.color === null) node.currentHUEDevice.color = {} // Starting from v 4.1.31
198
+ node.currentHUEDevice.color.xy = cloneDeep(_state.color.xy) // Starting from v 4.1.31
199
+ } // Starting from v 4.1.31
200
+ if (_state.color_temperature !== undefined && _state.color_temperature !== null && _state.color_temperature.mirek !== undefined) { // Starting from v 4.1.31
201
+ if (node.currentHUEDevice.color_temperature === undefined || node.currentHUEDevice.color_temperature === null) node.currentHUEDevice.color_temperature = {} // Starting from v 4.1.31
202
+ node.currentHUEDevice.color_temperature.mirek = _state.color_temperature.mirek // Starting from v 4.1.31
203
+ } // Starting from v 4.1.31
204
+ } // Starting from v 4.1.31
205
+
206
+ // Used to call the status update from the config node.
207
+ node.setNodeStatus = ({
208
+ fill, shape, text, payload
209
+ }) => {
210
+ try {
211
+ if (node.currentHUEDevice?.on?.on === true) { fill = 'blue'; shape = 'dot' } else { fill = 'blue'; shape = 'ring' };
212
+ if (payload === undefined) payload = ''
213
+ const dDate = new Date()
214
+ payload = typeof payload === 'object' ? JSON.stringify(payload) : payload.toString()
215
+ node.sKNXNodeStatusText = `|KNX: ${text} ${payload} (${formatTs(dDate)})`
216
+ node.status({ fill, shape, text: (node.sHUENodeStatusText || '') + ' ' + (node.sKNXNodeStatusText || '') })
217
+ } catch (error) { }
218
+ }
219
+ // Used to call the status update from the HUE config node.
220
+ node.setNodeStatusHue = ({ fill, shape, text, payload }) => {
221
+ try {
222
+ if (node.currentHUEDevice?.on?.on === true) { fill = 'blue'; shape = 'dot' } else { fill = 'blue'; shape = 'ring' };
223
+ if (payload === undefined) payload = ''
224
+ const dDate = new Date()
225
+ payload = typeof payload === 'object' ? JSON.stringify(payload) : payload.toString()
226
+ node.sHUENodeStatusText = `|HUE: ${text} ${payload} (${formatTs(dDate)})`
227
+ node.status({ fill, shape, text: node.sHUENodeStatusText + ' ' + (node.sKNXNodeStatusText || '') })
228
+ } catch (error) { }
229
+ }
230
+
231
+ node.writeHueState = function writeHueState (_state) {
232
+ const defaultOperation = node.isGrouped_light === false ? 'setLight' : 'setGroupedLight'
233
+ const isGroupedLightOff = node.isGrouped_light === true && node.currentHUEDevice?.on?.on === false
234
+ const stateKeys = _state && typeof _state === 'object' ? Object.keys(_state) : []
235
+ const presetKeys = ['dimming', 'color', 'color_temperature', 'gradient']
236
+ const actionableKeys = ['dimming', ...presetKeys]
237
+ const hasActionablePayload = stateKeys.some((key) => actionableKeys.includes(key))
238
+ const mustPresetGroupedLightChildren = isGroupedLightOff &&
239
+ stateKeys.some((key) => presetKeys.includes(key))
240
+
241
+ if (!mustPresetGroupedLightChildren) {
242
+ node.serverHue.hueManager.writeHueQueueAdd(node.hueDevice, _state, defaultOperation)
243
+ return
244
+ }
245
+
246
+ (async () => {
247
+ try {
248
+ const groupLights = await node.serverHue.getAllLightsBelongingToTheGroup(node.hueDevice, false)
249
+ RED.log.debug(`knxUltimateMatterControllerDevice: preset grouped_light children before group on. Group=${node.hueDevice} lights=${Array.isArray(groupLights) ? groupLights.length : 0}`)
250
+ let hasWrittenAtLeastOneLight = false
251
+ for (let index = 0; index < groupLights.length; index++) {
252
+ const light = groupLights[index]
253
+ if (!light?.id) continue
254
+ const lightState = {}
255
+ let hasActionableStateForLight = false
256
+
257
+ if (_state.dimming !== undefined && light.dimming !== undefined) {
258
+ lightState.dimming = cloneDeep(_state.dimming)
259
+ hasActionableStateForLight = true
260
+ }
261
+ if (_state.color !== undefined && light.color !== undefined) {
262
+ lightState.color = cloneDeep(_state.color)
263
+ hasActionableStateForLight = true
264
+ }
265
+ if (_state.color_temperature !== undefined && light.color_temperature !== undefined) {
266
+ lightState.color_temperature = cloneDeep(_state.color_temperature)
267
+ hasActionableStateForLight = true
268
+ }
269
+ if (_state.gradient !== undefined && light.gradient !== undefined) {
270
+ lightState.gradient = cloneDeep(_state.gradient)
271
+ hasActionableStateForLight = true
272
+ }
273
+
274
+ if (!hasActionableStateForLight) continue
275
+ node.serverHue.hueManager.writeHueQueueAdd(light.id, lightState, 'setLight')
276
+ hasWrittenAtLeastOneLight = true
277
+ }
278
+
279
+ if (!hasWrittenAtLeastOneLight || !hasActionablePayload) {
280
+ node.serverHue.hueManager.writeHueQueueAdd(node.hueDevice, _state, defaultOperation)
281
+ return
282
+ }
283
+
284
+ if (_state?.on?.on !== true) return
285
+
286
+ const groupedLightState = { on: cloneDeep(_state.on) }
287
+ if (_state.dynamics !== undefined) groupedLightState.dynamics = cloneDeep(_state.dynamics)
288
+ RED.log.debug(`knxUltimateMatterControllerDevice: turning on grouped_light after children preset. Group=${node.hueDevice}`)
289
+ node.serverHue.hueManager.writeHueQueueAdd(node.hueDevice, groupedLightState, defaultOperation)
290
+ } catch (error) {
291
+ RED.log.debug(`knxUltimateMatterControllerDevice: node.writeHueState fallback to grouped_light write: ${error.message}`)
292
+ node.serverHue.hueManager.writeHueQueueAdd(node.hueDevice, _state, defaultOperation)
293
+ }
294
+ })()
295
+ }
296
+
297
+ node.deleteHueStateQueue = function deleteHueStateQueue () {
298
+ node.serverHue.hueManager.deleteHueQueue(node.hueDevice)
299
+ if (node.isGrouped_light !== true) return;
300
+
301
+ (async () => {
302
+ try {
303
+ const groupLights = await node.serverHue.getAllLightsBelongingToTheGroup(node.hueDevice, false)
304
+ for (let index = 0; index < groupLights.length; index++) {
305
+ const light = groupLights[index]
306
+ if (!light?.id) continue
307
+ node.serverHue.hueManager.deleteHueQueue(light.id)
308
+ }
309
+ } catch (error) {
310
+ RED.log.debug(`knxUltimateMatterControllerDevice: node.deleteHueStateQueue: ${error.message}`)
311
+ }
312
+ })()
313
+ }
314
+
315
+ function getRandomIntInclusive (min, max) {
316
+ min = Math.ceil(min)
317
+ max = Math.floor(max)
318
+ return Math.floor(Math.random() * (max - min + 1) + min) // The maximum is inclusive and the minimum is inclusive
319
+ }
320
+
321
+ // This function is called by the hue-config.js
322
+ node.handleSend = (msg) => {
323
+ if (!msg || !msg.knx || !isConfiguredKNXGA(msg.knx.destination)) return
324
+ if (node.currentHUEDevice === undefined && node.serverHue.linkStatus === 'connected') {
325
+ node.setNodeStatusHue({
326
+ fill: 'yellow',
327
+ shape: 'ring',
328
+ text: 'Initializing. Please wait.',
329
+ payload: ''
330
+ })
331
+ return
332
+ }
333
+ if (msg.knx.event !== 'GroupValue_Read' && node.currentHUEDevice !== undefined) {
334
+ let state = {}
335
+ try {
336
+ switch (msg.knx.destination) {
337
+ case config.GALightSwitch:
338
+ msg.payload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptLightSwitch))
339
+
340
+ // 15/05/2024 Supergiovane: check the Override to Day option
341
+ // config.restoreDayMode can be: no or setDayByFastSwitchLightSingle or setDayByFastSwitchLightALL
342
+ // ----------------------------------------------------------
343
+ if (config.restoreDayMode === 'setDayByFastSwitchLightSingle' || config.restoreDayMode === 'setDayByFastSwitchLightALL') {
344
+ if (node.DayTime === false) {
345
+ if (msg.payload === true) {
346
+ if (node.timerCheckForFastLightSwitch === null) {
347
+ node.timerCheckForFastLightSwitch = setTimeout(() => {
348
+ node.DayTime = false
349
+ RED.log.debug('knxUltimateMatterControllerDevice: node.timerCheckForFastLightSwitch: set daytime to false after node.timerCheckForFastLightSwitch elapsed')
350
+ node.timerCheckForFastLightSwitch = null
351
+ }, 10000) // 10 seconds
352
+ } else {
353
+ if (config.restoreDayMode === 'setDayByFastSwitchLightALL') {
354
+ // Turn off the Day/Night group address
355
+ if (config.GADaylightSensor !== undefined && config.GADaylightSensor !== '') {
356
+ if (node.timerCheckForFastLightSwitch !== null) { clearTimeout(node.timerCheckForFastLightSwitch); node.timerCheckForFastLightSwitch = null }
357
+ RED.log.debug(`knxUltimateMatterControllerDevice: node.timerCheckForFastLightSwitch: set daytime the group address ${config.GADaylightSensor}`)
358
+ node.serverKNX.sendKNXTelegramToKNXEngine({
359
+ grpaddr: config.GADaylightSensor,
360
+ payload: config.invertDayNight === false,
361
+ dpt: config.dptDaylightSensor,
362
+ outputtype: 'write',
363
+ nodecallerid: node.id
364
+ })
365
+ }
366
+ }
367
+ node.DayTime = true
368
+ RED.log.debug('knxUltimateMatterControllerDevice: node.timerCheckForFastLightSwitch: set daytime to true')
369
+ }
370
+ }
371
+ }
372
+ }
373
+ // ----------------------------------------------------------
374
+
375
+ if (msg.payload === true) {
376
+ // From HUE Api core concepts:
377
+ // If you try and control multiple conflicting parameters at once e.g. {"color": {"xy": {"x":0.5,"y":0.5}}, "color_temperature": {"mirek": 250}}
378
+ // the lights can only physically do one, for this we apply the rule that xy beats ct. Simple.
379
+ // color_temperature.mirek: color temperature in mirek is null when the light color is not in the ct spectrum
380
+ if ((node.DayTime === true && config.specifySwitchOnBrightness === 'no') && ((node.isGrouped_light === false && node.HUEDeviceWhileDaytime !== null) || (node.isGrouped_light === true && node.HUELightsBelongingToGroupWhileDaytime !== null))) {
381
+ if (node.isGrouped_light === false && node.HUEDeviceWhileDaytime !== null) {
382
+ // The DayNight has switched into day, so restore the previous light status
383
+ state = { on: { on: true }, dimming: node.HUEDeviceWhileDaytime.dimming, color: node.HUEDeviceWhileDaytime.color, color_temperature: node.HUEDeviceWhileDaytime.color_temperature }
384
+ if (node.HUEDeviceWhileDaytime.color_temperature !== undefined && node.HUEDeviceWhileDaytime.color_temperature.mirek === null) delete state.color_temperature // Otherwise the lamp will not turn on due to an error. color_temperature.mirek: color temperature in mirek is null when the light color is not in the ct spectrum
385
+ node.writeHueState(state)
386
+ node.setNodeStatusHue({
387
+ fill: 'green',
388
+ shape: 'dot',
389
+ text: 'KNX->Matter',
390
+ payload: 'Restore light status'
391
+ })
392
+ node.HUEDeviceWhileDaytime = null // Nullize the object.
393
+ } else if (node.isGrouped_light === true && node.HUELightsBelongingToGroupWhileDaytime !== null) {
394
+ // The DayNight has switched into day, so restore the previous light state, belonging to the group
395
+ let bAtLeastOneIsOn = false
396
+ for (let index = 0; index < node.HUELightsBelongingToGroupWhileDaytime.length; index++) { // Ensure, at least 1 lamp was on, otherwise turn all lamps on
397
+ const element = node.HUELightsBelongingToGroupWhileDaytime[index].light[0]
398
+ if (element.on.on === true) {
399
+ bAtLeastOneIsOn = true
400
+ break
401
+ }
402
+ }
403
+ for (let index = 0; index < node.HUELightsBelongingToGroupWhileDaytime.length; index++) {
404
+ const element = node.HUELightsBelongingToGroupWhileDaytime[index].light[0]
405
+ if (bAtLeastOneIsOn === true) {
406
+ state = { on: element.on, dimming: element.dimming, color: element.color, color_temperature: element.color_temperature }
407
+ } else {
408
+ // Failsafe all on
409
+ state = { on: { on: true }, dimming: element.dimming, color: element.color, color_temperature: element.color_temperature }
410
+ }
411
+ if (element.color_temperature !== undefined && element.color_temperature.mirek === null) delete state.color_temperature // Otherwise the lamp will not turn on due to an error. color_temperature.mirek: color temperature in mirek is null when the light color is not in the ct spectrum
412
+ node.serverHue.hueManager.writeHueQueueAdd(element.id, state, 'setLight')
413
+ }
414
+ node.setNodeStatusHue({
415
+ fill: 'green',
416
+ shape: 'dot',
417
+ text: 'KNX->Matter',
418
+ payload: "Resuming all group's light"
419
+ })
420
+ node.HUELightsBelongingToGroupWhileDaytime = null // Nullize the object.
421
+ return
422
+ }
423
+ } else {
424
+ let colorChoosen
425
+ let temperatureChoosen
426
+ let brightnessChoosen
427
+ // The light must support the temperature (in this case, colorAtSwitchOnNightTime is an object {kelvin:xx, brightness:yy})
428
+ if (node.currentHUEDevice.color_temperature !== undefined) {
429
+ if (node.DayTime === true && config.specifySwitchOnBrightness === 'temperature') {
430
+ temperatureChoosen = config.colorAtSwitchOnDayTime.kelvin
431
+ } else if (node.DayTime === false && config.enableDayNightLighting === 'temperature') {
432
+ temperatureChoosen = config.colorAtSwitchOnNightTime.kelvin
433
+ }
434
+ }
435
+ if (node.currentHUEDevice.dimming !== undefined) {
436
+ // Check wether the user selected specific brightness at switch on (in this case, colorAtSwitchOnNightTime is an object {kelvin:xx, brightness:yy})
437
+ if (node.DayTime === true && config.specifySwitchOnBrightness === 'temperature') {
438
+ brightnessChoosen = config.colorAtSwitchOnDayTime.brightness
439
+ } else if (node.DayTime === false && config.enableDayNightLighting === 'temperature') {
440
+ brightnessChoosen = config.colorAtSwitchOnNightTime.brightness
441
+ }
442
+ }
443
+ if (node.currentHUEDevice.color !== undefined) {
444
+ // Check wether the user selected specific color at switch on (in this case, colorAtSwitchOnDayTime is a text with HTML web color)
445
+ if (node.DayTime === true && config.specifySwitchOnBrightness === 'yes') {
446
+ colorChoosen = config.colorAtSwitchOnDayTime
447
+ } else if (node.DayTime === false && config.enableDayNightLighting === 'yes') {
448
+ colorChoosen = config.colorAtSwitchOnNightTime
449
+ }
450
+ }
451
+ // Create the HUE command
452
+ if (colorChoosen !== undefined) {
453
+ // Now we have a jColorChoosen. Proceed illuminating the light
454
+ let gamut = null
455
+ if (node.currentHUEDevice.color.gamut !== undefined) {
456
+ gamut = node.currentHUEDevice.color.gamut
457
+ }
458
+ const dretXY = hueColorConverter.ColorConverter.calculateXYFromRGB(colorChoosen.red, colorChoosen.green, colorChoosen.blue, gamut)
459
+ const dbright = hueColorConverter.ColorConverter.getBrightnessFromRGBOrHex(colorChoosen.red, colorChoosen.green, colorChoosen.blue)
460
+ node.currentHUEDevice.dimming.brightness = Math.round(dbright, 0)
461
+ if (node.currentHUEDevice.color !== undefined) node.currentHUEDevice.color.xy = dretXY // 26/03/2024
462
+ node.updateKNXBrightnessState(node.currentHUEDevice.dimming.brightness)
463
+ state = dbright > 0 ? { on: { on: true }, dimming: { brightness: dbright }, color: { xy: dretXY } } : { on: { on: false } }
464
+ // state = { on: { on: true }, dimming: { brightness: dbright }, color: { xy: dretXY } };
465
+ } else if (temperatureChoosen !== undefined) {
466
+ // Kelvin
467
+ const mirek = hueColorConverter.ColorConverter.kelvinToMirek(temperatureChoosen)
468
+ node.currentHUEDevice.color_temperature.mirek = mirek
469
+ node.currentHUEDevice.dimming.brightness = brightnessChoosen
470
+ node.updateKNXBrightnessState(node.currentHUEDevice.dimming.brightness)
471
+ // Kelvin temp
472
+ state = brightnessChoosen > 0 ? { on: { on: true }, dimming: { brightness: brightnessChoosen }, color_temperature: { mirek } } : { on: { on: false } }
473
+ // state = { on: { on: true }, dimming: { brightness: brightnessChoosen }, color_temperature: { mirek: mirek } };
474
+ } else if (brightnessChoosen !== undefined) {
475
+ state = brightnessChoosen > 0 ? { on: { on: true }, dimming: { brightness: brightnessChoosen } } : { on: { on: false } }
476
+ // state = { on: { on: true }, dimming: { brightness: brightnessChoosen } };
477
+ } else {
478
+ state = { on: { on: true } }
479
+ }
480
+ }
481
+ } else {
482
+ // Stop color cycle
483
+ if (node.timerColorCycle !== undefined) clearInterval(node.timerColorCycle)
484
+ // Stop Blinking
485
+ if (node.timerBlink !== undefined) clearInterval(node.timerBlink)
486
+ state = { on: { on: false } }
487
+ }
488
+
489
+ node.syncCurrentHUEDeviceFromKNXState(state) // Starting from v 4.1.31
490
+ node.writeHueState(state)
491
+ node.setNodeStatusHue({
492
+ fill: 'green',
493
+ shape: 'dot',
494
+ text: 'KNX->Matter',
495
+ payload: state
496
+ })
497
+ break
498
+ case config.GALightDIM:
499
+ // { decr_incr: 1, data: 1 } : Start increasing until { decr_incr: 0, data: 0 } is received.
500
+ // { decr_incr: 0, data: 1 } : Start decreasing until { decr_incr: 0, data: 0 } is received.
501
+ msg.payload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptLightDIM))
502
+ node.hueDimming(msg.payload.decr_incr, msg.payload.data, config.dimSpeed)
503
+ node.setNodeStatusHue({
504
+ fill: 'green', shape: 'dot', text: 'KNX->Matter', payload: JSON.stringify(msg.payload)
505
+ })
506
+ break
507
+ case config.GALightHSV_H_DIM:
508
+ // { decr_incr: 1, data: 1 } : Start increasing until { decr_incr: 0, data: 0 } is received.
509
+ // { decr_incr: 0, data: 1 } : Start decreasing until { decr_incr: 0, data: 0 } is received.
510
+ msg.payload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptLightDIM))
511
+ node.hueDimmingHSV_H(msg.payload.decr_incr, msg.payload.data, config.HSVDimSpeed)
512
+ node.setNodeStatusHue({
513
+ fill: 'green', shape: 'dot', text: 'KNX->Matter', payload: JSON.stringify(msg.payload)
514
+ })
515
+ break
516
+ case config.GALightHSV_S_DIM:
517
+ // { decr_incr: 1, data: 1 } : Start increasing until { decr_incr: 0, data: 0 } is received.
518
+ // { decr_incr: 0, data: 1 } : Start decreasing until { decr_incr: 0, data: 0 } is received.
519
+ msg.payload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptLightDIM))
520
+ node.hueDimmingHSV_S(msg.payload.decr_incr, msg.payload.data, config.HSVDimSpeed)
521
+ node.setNodeStatusHue({
522
+ fill: 'green', shape: 'dot', text: 'KNX->Matter', payload: JSON.stringify(msg.payload)
523
+ })
524
+ break
525
+ case config.GALightKelvin:
526
+ let retMirek
527
+ let kelvinValue = 0
528
+ msg.payload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptLightKelvin))
529
+ if (config.dptLightKelvin === '7.600') {
530
+ if (msg.payload > 6535) msg.payload = 6535
531
+ if (msg.payload < 2000) msg.payload = 2000
532
+ kelvinValue = msg.payload// hueColorConverter.ColorConverter.scale(msg.payload, [0, 65535], [2000, 6535]);
533
+ retMirek = hueColorConverter.ColorConverter.kelvinToMirek(kelvinValue)
534
+ } else if (config.dptLightKelvin === '9.002') {
535
+ // Relative temperature in Kelvin. Use HUE scale.
536
+ if (msg.payload > 6535) msg.payload = 6535
537
+ if (msg.payload < 2000) msg.payload = 2000
538
+ retMirek = hueColorConverter.ColorConverter.kelvinToMirek(msg.payload)
539
+ }
540
+ state = { color_temperature: { mirek: retMirek } }
541
+ node.syncCurrentHUEDeviceFromKNXState(state) // Starting from v 4.1.31
542
+ node.writeHueState(state)
543
+ node.setNodeStatusHue({
544
+ fill: 'green',
545
+ shape: 'dot',
546
+ text: 'KNX->Matter',
547
+ payload: kelvinValue
548
+ })
549
+ break
550
+ case config.GADaylightSensor:
551
+ node.DayTime = Boolean(dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptDaylightSensor)))
552
+ if (config.invertDayNight === true) node.DayTime = !node.DayTime
553
+ if (config.specifySwitchOnBrightness === 'no') {
554
+ // This retains the HUE device status while daytime, to be restored after nighttime elapsed. https://github.com/Supergiovane/node-red-contrib-knx-ultimate/issues/298
555
+ if (node.DayTime === false) {
556
+ if (node.isGrouped_light === false) node.HUEDeviceWhileDaytime = cloneDeep(node.currentHUEDevice) // DayTime has switched to false: save the currentHUEDevice into the HUEDeviceWhileDaytime
557
+ if (node.isGrouped_light === true) {
558
+ (async () => {
559
+ try {
560
+ const retLights = await node.serverHue.getAllLightsBelongingToTheGroup(node.hueDevice, false)
561
+ node.HUELightsBelongingToGroupWhileDaytime = cloneDeep(retLights) // DayTime has switched to false: save the lights belonging to the group into the HUELightsBelongingToGroupWhileDaytime array
562
+ } catch (error) { /* empty */ }
563
+ })()
564
+ }
565
+ }
566
+ } else {
567
+ node.HUEDeviceWhileDaytime = null
568
+ node.HUELightsBelongingToGroupWhileDaytime = null
569
+ }
570
+ node.setNodeStatusHue({
571
+ fill: 'green',
572
+ shape: 'dot',
573
+ text: 'KNX->Matter Daytime',
574
+ payload: node.DayTime
575
+ })
576
+
577
+ break
578
+ case config.GALightKelvinDIM:
579
+ if (config.dptLightKelvinDIM === '3.007') {
580
+ // MDT smartbutton will dim the color temperature
581
+ // { decr_incr: 1, data: 1 } : Start increasing until { decr_incr: 0, data: 0 } is received.
582
+ // { decr_incr: 0, data: 1 } : Start decreasing until { decr_incr: 0, data: 0 } is received.
583
+ msg.payload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptLightKelvinDIM))
584
+ node.hueDimmingTunableWhite(msg.payload.decr_incr, msg.payload.data, 5000)
585
+ node.setNodeStatusHue({
586
+ fill: 'green', shape: 'dot', text: 'KNX->Matter', payload: JSON.stringify(msg.payload)
587
+ })
588
+ }
589
+ break
590
+ case config.GALightKelvinPercentage:
591
+ if (config.dptLightKelvinPercentage === '5.001') {
592
+ // 0-100% tunable white
593
+ msg.payload = 100 - dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptLightKelvinPercentage))
594
+ // msg.payload = msg.payload <= 0 ? 1 : msg.payload
595
+ const retMirek = hueColorConverter.ColorConverter.scale(msg.payload, [0, 100], [153, 500])
596
+ msg.payload = retMirek
597
+ state = { color_temperature: { mirek: msg.payload } }
598
+ node.syncCurrentHUEDeviceFromKNXState(state) // Starting from v 4.1.31
599
+ node.writeHueState(state)
600
+ node.setNodeStatusHue({
601
+ fill: 'green',
602
+ shape: 'dot',
603
+ text: 'KNX->Matter',
604
+ payload: state
605
+ })
606
+ }
607
+ break
608
+ case config.GALightBrightness:
609
+ msg.payload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptLightBrightness))
610
+ if (typeof msg.payload !== 'number' || Number.isNaN(msg.payload)) throw new Error('Invalid KNX brightness payload')
611
+ state = { dimming: { brightness: msg.payload } }
612
+ if (msg.payload === 0) {
613
+ state.on = { on: false }
614
+ } else if (node.currentHUEDevice !== undefined && node.currentHUEDevice.on?.on === true) {
615
+ state.on = { on: true }
616
+ }
617
+ node.syncCurrentHUEDeviceFromKNXState(state) // Starting from v 4.1.31
618
+ node.writeHueState(state)
619
+ node.setNodeStatusHue({
620
+ fill: 'green',
621
+ shape: 'dot',
622
+ text: 'KNX->Matter',
623
+ payload: state
624
+ })
625
+ break
626
+ case config.GALightColor:
627
+ msg.payload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptLightColor))
628
+ let gamut = null
629
+ if (
630
+ node.currentHUEDevice !== undefined &&
631
+ node.currentHUEDevice.color !== undefined &&
632
+ node.currentHUEDevice.color.gamut !== undefined
633
+ ) {
634
+ gamut = node.currentHUEDevice.color.gamut
635
+ }
636
+ const retXY = hueColorConverter.ColorConverter.calculateXYFromRGB(msg.payload.red, msg.payload.green, msg.payload.blue, gamut)
637
+ const bright = hueColorConverter.ColorConverter.getBrightnessFromRGBOrHex(msg.payload.red, msg.payload.green, msg.payload.blue)
638
+ state = { dimming: { brightness: bright }, color: { xy: retXY } }
639
+ if (node.currentHUEDevice === undefined) {
640
+ // Grouped light
641
+ state.on = { on: bright > 0 }
642
+ } else {
643
+ // Light
644
+ if (node.currentHUEDevice.on.on === false && bright > 0) state.on = { on: true }
645
+ if (node.currentHUEDevice.on.on === true && bright === 0) state = { on: { on: false }, dimming: { brightness: bright } }
646
+ }
647
+ node.syncCurrentHUEDeviceFromKNXState(state) // Starting from v 4.1.31
648
+ node.writeHueState(state)
649
+ node.setNodeStatusHue({
650
+ fill: 'green',
651
+ shape: 'dot',
652
+ text: 'KNX->Matter',
653
+ payload: state
654
+ })
655
+ break
656
+ case config.GALightBlink:
657
+ const gaVal = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptLightBlink))
658
+ if (gaVal) {
659
+ node.timerBlink = setInterval(() => {
660
+ if (node.blinkValue === undefined) node.blinkValue = true
661
+ node.blinkValue = !node.blinkValue
662
+ msg.payload = node.blinkValue
663
+ // state = msg.payload === true ? { on: { on: true } } : { on: { on: false } }
664
+ state = msg.payload === true
665
+ ? { on: { on: true }, dimming: { brightness: 100 }, dynamics: { duration: 0 } }
666
+ : { on: { on: false }, dimming: { brightness: 0 }, dynamics: { duration: 0 } }
667
+ node.writeHueState(state)
668
+ }, 1500)
669
+ } else {
670
+ if (node.timerBlink !== undefined) clearInterval(node.timerBlink)
671
+ node.writeHueState({ on: { on: false } })
672
+ }
673
+ node.setNodeStatusHue({
674
+ fill: 'green',
675
+ shape: 'dot',
676
+ text: 'KNX->Matter',
677
+ payload: gaVal
678
+ })
679
+ break
680
+ case config.GALightColorCycle:
681
+ {
682
+ if (node.timerColorCycle !== undefined) clearInterval(node.timerColorCycle)
683
+ const gaValColorCycle = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptLightColorCycle))
684
+ if (gaValColorCycle === true) {
685
+ node.writeHueState({ on: { on: true } })
686
+ node.timerColorCycle = setInterval(() => {
687
+ try {
688
+ const red = getRandomIntInclusive(0, 255)
689
+ const green = getRandomIntInclusive(0, 255)
690
+ const blue = getRandomIntInclusive(0, 255)
691
+ let gamut = null
692
+ if (
693
+ node.currentHUEDevice !== undefined &&
694
+ node.currentHUEDevice.color !== undefined &&
695
+ node.currentHUEDevice.color.gamut !== undefined
696
+ ) {
697
+ gamut = node.currentHUEDevice.color.gamut
698
+ }
699
+ const retXY = hueColorConverter.ColorConverter.calculateXYFromRGB(red, green, blue, gamut)
700
+ const bright = hueColorConverter.ColorConverter.getBrightnessFromRGBOrHex(red, green, blue)
701
+ state = bright > 0 ? { on: { on: true }, dimming: { brightness: bright }, color: { xy: retXY } } : { on: { on: false } }
702
+ node.writeHueState(state)
703
+ } catch (error) { }
704
+ }, 10000)
705
+ } else {
706
+ if (node.timerColorCycle !== undefined) clearInterval(node.timerColorCycle)
707
+ node.writeHueState({ on: { on: false } })
708
+ }
709
+ node.setNodeStatusHue({
710
+ fill: 'green',
711
+ shape: 'dot',
712
+ text: 'KNX->Matter',
713
+ payload: gaValColorCycle
714
+ })
715
+ }
716
+ break
717
+ default:
718
+ break
719
+ }
720
+ } catch (error) {
721
+ node.status({
722
+ fill: 'red',
723
+ shape: 'dot',
724
+ text: `KNX->Matter errorRead ${error.message} (${formatTs(new Date())})`
725
+ })
726
+ RED.log.error(`knxUltimateMatterControllerDevice: node.handleSend: if (msg.knx.event !== "GroupValue_Read"): ${error.message} : ${error.stack || ''} `)
727
+ }
728
+ }
729
+
730
+ // I must respond to query requests (read request) sent from the KNX BUS
731
+ try {
732
+ if (msg.knx.event === 'GroupValue_Read' && node.currentHUEDevice !== undefined) {
733
+ const zeroBrightnessWhenOff = (config.updateKNXBrightnessStatusOnHUEOnOff === undefined || config.updateKNXBrightnessStatusOnHUEOnOff === 'onhueoff')
734
+ let ret
735
+ switch (msg.knx.destination) {
736
+ case config.GALightState:
737
+ ret = node.currentHUEDevice.on.on
738
+ if (ret !== undefined) node.updateKNXLightState(ret, 'response')
739
+ break
740
+ case config.GALightColorState:
741
+ ret = node.currentHUEDevice.color.xy
742
+ if (ret !== undefined) node.updateKNXLightColorState(node.currentHUEDevice.color, 'response')
743
+ break
744
+ case config.GALightKelvinPercentageState:
745
+ // The kelvin level belongs to the group defice, so i don't need to get the first light in the collection (if the device is a grouped_light)
746
+ ret = node.currentHUEDevice.color_temperature.mirek
747
+ if (ret !== undefined) node.updateKNXLightKelvinPercentageState(ret, 'response')
748
+ // if (node.isGrouped_light === false) {
749
+ // ret = node.currentHUEDevice.color_temperature.mirek;
750
+ // if (ret !== undefined) node.updateKNXLightKelvinPercentageState(ret, "response");
751
+ // } else {
752
+ // (async () => {
753
+ // try {
754
+ // // Find the first light in the collection, having the color_temperature capabilities
755
+ // const devices = await node.serverHue.getAllLightsBelongingToTheGroup(node.hueDevice, false);
756
+ // for (let index = 0; index < devices.length; index++) {
757
+ // const element = devices[index];
758
+ // if (element.light[0].color_temperature !== undefined) {
759
+ // ret = element.light[0].color_temperature.mirek;
760
+ // break;
761
+ // }
762
+ // }
763
+ // if (ret !== undefined) node.updateKNXLightKelvinPercentageState(ret, "response");
764
+ // } catch (error) { /* empty */ }
765
+ // })();
766
+ // }
767
+ break
768
+ case config.GALightBrightnessState:
769
+ // Hue keeps the last brightness even when the light is OFF. If the user enabled "brightness status -> 0 on HUE off",
770
+ // keep returning 0 while OFF to avoid the KNX status jumping back to the last/start value after some time/read-requests.
771
+ if (zeroBrightnessWhenOff === true && node.currentHUEDevice?.on?.on === false) {
772
+ ret = 0
773
+ } else {
774
+ ret = node.currentHUEDevice.dimming.brightness
775
+ }
776
+ if (ret !== undefined) node.updateKNXBrightnessState(ret, 'response')
777
+ break
778
+ case config.GALightKelvinState:
779
+ // The kelvin level belongs to the group defice, so i don't need to get the first light in the collection (if the device is a grouped_light)
780
+ ret = node.currentHUEDevice.color_temperature.mirek
781
+ if (ret !== undefined) node.updateKNXLightKelvinState(ret, 'response')
782
+ // if (node.isGrouped_light === false) {
783
+ // ret = node.currentHUEDevice.color_temperature.mirek;
784
+ // if (ret !== undefined) node.updateKNXLightKelvinState(ret, "response");
785
+ // } else {
786
+ // (async () => {
787
+ // try {
788
+ // // Find the first light in the collection, having the color_temperature capabilities
789
+ // const devices = await node.serverHue.getAllLightsBelongingToTheGroup(node.hueDevice, false);
790
+ // for (let index = 0; index < devices.length; index++) {
791
+ // const element = devices[index];
792
+ // if (element.light[0].color_temperature !== undefined) {
793
+ // ret = element.light[0].color_temperature.mirek;
794
+ // break;
795
+ // }
796
+ // }
797
+ // if (ret !== undefined) node.updateKNXLightKelvinState(ret, "response");
798
+ // } catch (error) { /* empty */ }
799
+ // })();
800
+ // }
801
+ break
802
+ default:
803
+ break
804
+ }
805
+ }
806
+ } catch (error) {
807
+ node.status({
808
+ fill: 'red',
809
+ shape: 'dot',
810
+ text: `KNX->Matter error :-( ${error.message} (${formatTs(new Date())})`
811
+ })
812
+ RED.log.error(`knxUltimateMatterControllerDevice: node.handleSend: if (msg.knx.event === "GroupValue_Read" && node.currentHUEDevice !== undefined): ${error.message} : ${error.stack || ''} `)
813
+ }
814
+ }
815
+
816
+ // Start dimming
817
+ // ***********************************************************
818
+ node.hueDimming = function hueDimming (_KNXaction, _KNXbrightness_Direction, _dimSpeedInMillisecs = undefined) {
819
+ // 31/10/2023 after many attempts to use dimming_delta function of the HueApeV2, loosing days of my life, without a decent success, will use the standard dimming calculations
820
+ // i decide to go to the "step brightness" way.
821
+ try {
822
+ let hueTelegram = {}
823
+ let numStep = 10 // Steps from 0 to 100 by 10
824
+ const extendedConf = {}
825
+
826
+ if (_KNXbrightness_Direction === 0 && _KNXaction === 0) {
827
+ // STOP DIM
828
+ if (node.timerStepDim !== undefined) clearInterval(node.timerStepDim)
829
+ node.brightnessStep = null
830
+ node.deleteHueStateQueue() // Clear dimming queue.
831
+ return
832
+ }
833
+
834
+ // 26/03/2024 set the extended configuration as well, because the light was off (so i've been unable to set it up elsewhere)
835
+ if (node.currentHUEDevice.on !== undefined && node.currentHUEDevice.on.on === false) {
836
+ // if (node.currentHUEDevice.color !== undefined) extendedConf.color = { xy: node.currentHUEDevice.color.xy }; // DO NOT ADD THE COLOR, BECAUSE THE COLOR HAS ALSO THE BRIGHTNESS, SO ALL THE DATA NEEDED TO BE SWITCHED ON CORRECLY
837
+ if (node.currentHUEDevice.color_temperature !== undefined) extendedConf.color_temperature = { mirek: node.currentHUEDevice.color_temperature.mirek }
838
+ }
839
+
840
+ // If i'm dimming up while the light is off, start the dim with the initial brightness set to zero
841
+ if (_KNXbrightness_Direction > 0 && _KNXaction === 1 && node.currentHUEDevice.on !== undefined && node.currentHUEDevice.on.on === false) {
842
+ node.brightnessStep = null
843
+ node.currentHUEDevice.dimming.brightness = 0
844
+ }
845
+
846
+ // Set the actual brightness to start with
847
+ if (node.brightnessStep === null || node.brightnessStep === undefined) node.brightnessStep = node.currentHUEDevice.dimming.brightness !== undefined ? node.currentHUEDevice.dimming.brightness : 50
848
+ node.brightnessStep = Math.ceil(Number(node.brightnessStep))
849
+
850
+ // We have also minDimLevelLight and maxDimLevelLight to take care of.
851
+ let minDimLevelLight
852
+ if (config.minDimLevelLight === undefined) {
853
+ minDimLevelLight = 10
854
+ } else if (config.minDimLevelLight === 'useHueLightLevel') {
855
+ minDimLevelLight = node.currentHUEDevice.dimming.min_dim_level === undefined ? 10 : node.currentHUEDevice.dimming.min_dim_level
856
+ } else {
857
+ minDimLevelLight = Number(config.minDimLevelLight)
858
+ }
859
+ const maxDimLevelLight = config.maxDimLevelLight === undefined ? 100 : Number(config.maxDimLevelLight)
860
+
861
+ // Set the speed
862
+ _dimSpeedInMillisecs /= numStep
863
+ numStep = Math.round((maxDimLevelLight - minDimLevelLight) / numStep, 0)
864
+
865
+ if (_KNXbrightness_Direction > 0 && _KNXaction === 1) {
866
+ // DIM UP
867
+ if (node.timerStepDim !== undefined) clearInterval(node.timerStepDim)
868
+ node.timerStepDim = setInterval(() => {
869
+ node.updateKNXBrightnessState(node.brightnessStep) // Unnecessary, but necessary to set the KNX Status in real time.
870
+ node.brightnessStep += numStep
871
+ if (node.brightnessStep > maxDimLevelLight) node.brightnessStep = maxDimLevelLight
872
+ hueTelegram = { dimming: { brightness: node.brightnessStep }, dynamics: { duration: _dimSpeedInMillisecs + 500 } } // + is to avoid ladder effect
873
+ // Switch on the light if off
874
+ if (node.currentHUEDevice.on !== undefined && node.currentHUEDevice.on.on === false) {
875
+ hueTelegram.on = { on: true }
876
+ Object.assign(hueTelegram, extendedConf) // 26/03/2024 add extended conf
877
+ }
878
+ // console.log(hueTelegram)
879
+ node.writeHueState(hueTelegram)
880
+ if (node.brightnessStep >= maxDimLevelLight) clearInterval(node.timerStepDim)
881
+ }, _dimSpeedInMillisecs)
882
+ }
883
+ if (_KNXbrightness_Direction > 0 && _KNXaction === 0) {
884
+ if (node.currentHUEDevice.on.on === false) return // Don't dim down, if the light is already off.
885
+ // DIM DOWN
886
+ if (node.timerStepDim !== undefined) clearInterval(node.timerStepDim)
887
+ node.timerStepDim = setInterval(() => {
888
+ node.updateKNXBrightnessState(node.brightnessStep) // Unnecessary, but necessary to set the KNX Status in real time.
889
+ node.brightnessStep -= numStep
890
+ if (node.brightnessStep < minDimLevelLight) node.brightnessStep = minDimLevelLight
891
+ hueTelegram = { dimming: { brightness: node.brightnessStep }, dynamics: { duration: _dimSpeedInMillisecs + 500 } }// + 100 is to avoid ladder effect
892
+ // Switch off the light if on
893
+ if (node.currentHUEDevice.on !== undefined && node.currentHUEDevice.on.on === true && node.brightnessStep === 0) {
894
+ hueTelegram.on = { on: false }
895
+ }
896
+ node.writeHueState(hueTelegram)
897
+ if (node.brightnessStep <= minDimLevelLight) clearInterval(node.timerStepDim)
898
+ }, _dimSpeedInMillisecs)
899
+ }
900
+ } catch (error) { }
901
+ }
902
+ // ***********************************************************
903
+
904
+ // Start dimming tunable white
905
+ // mirek: required(integer minimum: 153, maximum: 500)
906
+ // ***********************************************************
907
+ node.hueDimmingTunableWhite = function hueDimmingTunableWhite (_KNXaction, _KNXbrightness_DirectionTunableWhite, _dimSpeedInMillisecsTunableWhite = undefined) {
908
+ // 23/23/2023 after many attempts to use dimming_delta function of the HueApeV2, loosing days of my life, without a decent success, will use the standard dimming calculations
909
+ // i decide to go to the "step brightness" way.
910
+ try {
911
+ if (_KNXbrightness_DirectionTunableWhite === 0 && _KNXaction === 0) {
912
+ // STOP DIM
913
+ if (node.timerStepDimTunableWhite !== undefined) clearInterval(node.timerStepDimTunableWhite)
914
+ node.brightnessStepTunableWhite = null
915
+ node.deleteHueStateQueue() // Clear dimming queue.
916
+ return
917
+ }
918
+
919
+ let numStepTunableWhite = 10 // Steps from 153 to 500
920
+ if (config.invertDimTunableWhiteDirection === true) {
921
+ if (_KNXaction === 1) { _KNXaction = 0 } else { _KNXaction = 1 }
922
+ }
923
+
924
+ // Set the actual brightness to start with
925
+ if (node.brightnessStepTunableWhite === null || node.brightnessStepTunableWhite === undefined) node.brightnessStepTunableWhite = node.currentHUEDevice.color_temperature.mirek || 372
926
+ node.brightnessStepTunableWhite = Math.ceil(Number(node.brightnessStepTunableWhite))
927
+
928
+ // Set the speed
929
+ _dimSpeedInMillisecsTunableWhite = Math.ceil(_dimSpeedInMillisecsTunableWhite / numStepTunableWhite)
930
+
931
+ const minDimLevelLightTunableWhite = 153
932
+ const maxDimLevelLightTunableWhite = 500
933
+ // numStepTunableWhite = hueColorConverter.ColorConverter.scale(numStepTunableWhite, [0, 100], [minDimLevelLightTunableWhite, maxDimLevelLightTunableWhite]);
934
+ // numStepTunableWhite = hueColorConverter.ColorConverter.scale(numStepTunableWhite, [node.brightnessStepTunableWhite, maxDimLevelLightTunableWhite], [node.brightnessStepTunableWhite, maxDimLevelLightTunableWhite]);
935
+ numStepTunableWhite = Math.round((maxDimLevelLightTunableWhite - minDimLevelLightTunableWhite) / numStepTunableWhite, 0)
936
+
937
+ if (_KNXbrightness_DirectionTunableWhite > 0 && _KNXaction === 1) {
938
+ // DIM UP
939
+ if (node.timerStepDimTunableWhite !== undefined) clearInterval(node.timerStepDimTunableWhite)
940
+ node.timerStepDimTunableWhite = setInterval(() => {
941
+ node.updateKNXLightKelvinPercentageState(node.brightnessStepTunableWhite) // Unnecessary, but necessary to set the KNX Status in real time.
942
+ node.brightnessStepTunableWhite += numStepTunableWhite // *2 to speed up the things
943
+ if (node.brightnessStepTunableWhite > maxDimLevelLightTunableWhite) node.brightnessStepTunableWhite = maxDimLevelLightTunableWhite
944
+ const hueTelegram = { color_temperature: { mirek: node.brightnessStepTunableWhite }, dynamics: { duration: _dimSpeedInMillisecsTunableWhite } }
945
+ // Switch on the light if off
946
+ if (node.currentHUEDevice.on !== undefined && node.currentHUEDevice.on.on === false) {
947
+ hueTelegram.on = { on: true }
948
+ }
949
+ node.writeHueState(hueTelegram)
950
+ if (node.brightnessStepTunableWhite >= maxDimLevelLightTunableWhite) clearInterval(node.timerStepDimTunableWhite)
951
+ }, _dimSpeedInMillisecsTunableWhite)
952
+ }
953
+ if (_KNXbrightness_DirectionTunableWhite > 0 && _KNXaction === 0) {
954
+ if (node.currentHUEDevice.on.on === false) return // Don't dim down, if the light is already off.
955
+ // DIM DOWN
956
+ if (node.timerStepDimTunableWhite !== undefined) clearInterval(node.timerStepDimTunableWhite)
957
+ node.timerStepDimTunableWhite = setInterval(() => {
958
+ node.updateKNXLightKelvinPercentageState(node.brightnessStepTunableWhite) // Unnecessary, but necessary to set the KNX Status in real time.
959
+ node.brightnessStepTunableWhite -= numStepTunableWhite // *2 to speed up the things
960
+ if (node.brightnessStepTunableWhite < minDimLevelLightTunableWhite) node.brightnessStepTunableWhite = minDimLevelLightTunableWhite
961
+ const hueTelegram = { color_temperature: { mirek: node.brightnessStepTunableWhite }, dynamics: { duration: _dimSpeedInMillisecsTunableWhite } }
962
+ // Switch off the light if on
963
+ if (node.currentHUEDevice.on !== undefined && node.currentHUEDevice.on.on === true && node.brightnessStepTunableWhite === 0) {
964
+ hueTelegram.on = { on: false }
965
+ }
966
+ node.writeHueState(hueTelegram)
967
+ if (node.brightnessStepTunableWhite <= minDimLevelLightTunableWhite) clearInterval(node.timerStepDimTunableWhite)
968
+ }, _dimSpeedInMillisecsTunableWhite)
969
+ }
970
+ } catch (error) { }
971
+ }
972
+ // ***********************************************************
973
+
974
+ /**
975
+ * Starts dimming / stop dimming of the HUE
976
+ * @param {number} _KNXaction 1 for start, 0 for stop
977
+ * @param {number} _KNXbrightness_DirectionHSV_H 1 for up, 0 for down
978
+ * @param {number} _dimSpeedInMillisecsHSV Speed time in milliseconds
979
+ * @returns {}
980
+ */
981
+ node.hueDimmingHSV_H = function hueDimmingHSV_H (_KNXaction, _KNXbrightness_DirectionHSV_H, _dimSpeedInMillisecsHSV = undefined) {
982
+ // After many attempts to use dimming_delta function of the HueApiV2, loosing days of my life, without a decent success, will use the standard dimming calculations
983
+ // i decide to go to the "step brightness" way.
984
+ try {
985
+ if (_KNXbrightness_DirectionHSV_H === 0 && _KNXaction === 0) {
986
+ // STOP DIM
987
+ if (node.timerStepDimHSV_H !== undefined) clearInterval(node.timerStepDimHSV_H)
988
+ node.deleteHueStateQueue() // Clear dimming queue.
989
+ return
990
+ }
991
+
992
+ let xyBrightnessToHsv
993
+ if (node.currentHUEDevice.color !== undefined && node.currentHUEDevice.color.xy !== undefined) {
994
+ // Get the XY + brightness from the lamp and transform it into HSV
995
+ // xyBrightnessToHsv = hueColorConverter.ColorConverter.xyBrightnessToHsv(node.currentHUEDevice.color.xy.x, node.currentHUEDevice.color.xy.y, node.currentHUEDevice.dimming.brightness);
996
+ xyBrightnessToHsv = hueColorConverter.ColorConverter.xyBrightnessToHsv(hueColorConverter.ColorConverter.scale(node.currentHUEDevice.color.xy.x, [0, 1], [0, 100]), hueColorConverter.ColorConverter.scale(node.currentHUEDevice.color.xy.y, [0, 1], [0, 100]), node.currentHUEDevice.dimming.brightness)
997
+ } else {
998
+ return // Something wrong here.
999
+ }
1000
+ let numStepHSV_H = 10 // Steps from 0 to 100
1001
+
1002
+ // Set the actual brightness to start with
1003
+ if (node.brightnessStepHSV_H === null || node.brightnessStepHSV_H === undefined) node.brightnessStepHSV_H = node.currentHUEDevice.dimming.brightness
1004
+ node.brightnessStepHSV_H = Math.ceil(Number(node.brightnessStepHSV_H))
1005
+
1006
+ const minDimLevelLightHSV_H = 1
1007
+ const maxDimLevelLightHSV_H = 100
1008
+ numStepHSV_H = Math.round((maxDimLevelLightHSV_H - minDimLevelLightHSV_H) / numStepHSV_H, 0)
1009
+ // Set the speed
1010
+ _dimSpeedInMillisecsHSV = Math.ceil(_dimSpeedInMillisecsHSV / (maxDimLevelLightHSV_H / numStepHSV_H))
1011
+
1012
+ if (_KNXbrightness_DirectionHSV_H > 0 && _KNXaction === 1) {
1013
+ // DIM UP
1014
+ if (node.timerStepDimHSV_H !== undefined) clearInterval(node.timerStepDimHSV_H)
1015
+ node.timerStepDimHSV_H = setInterval(() => {
1016
+ node.deleteHueStateQueue() // Clear dimming queue.
1017
+ node.brightnessStepHSV_H += numStepHSV_H
1018
+ if (node.brightnessStepHSV_H > maxDimLevelLightHSV_H) node.brightnessStepHSV_H = maxDimLevelLightHSV_H
1019
+ // node.updateKNXLightHSV_H_State(node.brightnessStepHSV_H); // Unnecessary, but necessary to set the KNX Status in real time.
1020
+ xyBrightnessToHsv.h = node.brightnessStepHSV_H
1021
+ const hsvToXYBri = hueColorConverter.ColorConverter.hsvToxyBrightness(xyBrightnessToHsv, node.currentHUEDevice.color.gamut)
1022
+ const hueTelegram = { color: { xy: { x: hsvToXYBri.x, y: hsvToXYBri.y } }, dynamics: { duration: _dimSpeedInMillisecsHSV + 500 } } // Add some ms, to prevent the "ladder" step effect.
1023
+ // Switch on the light if off
1024
+ if (node.currentHUEDevice.on !== undefined && node.currentHUEDevice.on.on === false) {
1025
+ hueTelegram.on = { on: true }
1026
+ }
1027
+ node.currentHUEDevice.color.xy = hsvToXYBri// Unnecessary, but necessary to set the KNX Status in real time.
1028
+ node.updateKNXLightColorState(node.currentHUEDevice.color)
1029
+ node.writeHueState(hueTelegram)
1030
+ if (node.brightnessStepHSV_H >= maxDimLevelLightHSV_H) {
1031
+ if (node.timerStepDimHSV_H !== undefined) clearInterval(node.timerStepDimHSV_H)
1032
+ }
1033
+ }, _dimSpeedInMillisecsHSV)
1034
+ }
1035
+ if (_KNXbrightness_DirectionHSV_H > 0 && _KNXaction === 0) {
1036
+ if (node.currentHUEDevice.on.on === false) return // Don't dim down, if the light is already off.
1037
+ // DIM DOWN
1038
+ if (node.timerStepDimHSV_H !== undefined) clearInterval(node.timerStepDimHSV_H)
1039
+ node.timerStepDimHSV_H = setInterval(() => {
1040
+ node.deleteHueStateQueue() // Clear dimming queue.
1041
+ node.brightnessStepHSV_H -= numStepHSV_H // *2 to speed up the things
1042
+ if (node.brightnessStepHSV_H < minDimLevelLightHSV_H) node.brightnessStepHSV_H = minDimLevelLightHSV_H
1043
+ // node.updateKNXLightHSV_H_State(node.brightnessStepHSV_H); // Unnecessary, but necessary to set the KNX Status in real time.
1044
+ // Produco l'RGB
1045
+ xyBrightnessToHsv.h = node.brightnessStepHSV_H
1046
+ const hsvToXYBri = hueColorConverter.ColorConverter.hsvToxyBrightness(xyBrightnessToHsv, node.currentHUEDevice.color.gamut)
1047
+ const hueTelegram = { color: { xy: { x: hsvToXYBri.x, y: hsvToXYBri.y } }, dynamics: { duration: _dimSpeedInMillisecsHSV + 500 } }
1048
+ // Switch off the light if on
1049
+ if (node.currentHUEDevice.on !== undefined && node.currentHUEDevice.on.on === true && node.brightnessStepHSV_H === 0) {
1050
+ hueTelegram.on = { on: false }
1051
+ }
1052
+ node.currentHUEDevice.color.xy = hsvToXYBri// Unnecessary, but necessary to set the KNX Status in real time.
1053
+ node.updateKNXLightColorState(node.currentHUEDevice.color)
1054
+ node.writeHueState(hueTelegram)
1055
+ if (node.brightnessStepHSV_H <= minDimLevelLightHSV_H) {
1056
+ if (node.timerStepDimHSV_H !== undefined) clearInterval(node.timerStepDimHSV_H)
1057
+ }
1058
+ }, _dimSpeedInMillisecsHSV)
1059
+ }
1060
+ } catch (error) { /* empty */ }
1061
+ }
1062
+ /**
1063
+ * Starts dimming / stop dimming of the Saturation
1064
+ * @param {number} _KNXaction 1 for start, 0 for stop
1065
+ * @param {number} _KNXbrightness_DirectionHSV_S 1 for up, 0 for down
1066
+ * @param {number} _dimSpeedInMillisecsHSV Speed time in milliseconds
1067
+ * @returns {}
1068
+ */
1069
+ node.hueDimmingHSV_S = function hueDimmingHSV_S (_KNXaction, _KNXbrightness_DirectionHSV_S, _dimSpeedInMillisecsHSV = undefined) {
1070
+ // After many attempts to use dimming_delta function of the HueApiV2, loosing days of my life, without a decent success, will use the standard dimming calculations
1071
+ // i decide to go to the "step brightness" way.
1072
+ try {
1073
+ if (_KNXbrightness_DirectionHSV_S === 0 && _KNXaction === 0) {
1074
+ // STOP DIM
1075
+ if (node.timerStepDimHSV_S !== undefined) clearInterval(node.timerStepDimHSV_S)
1076
+ node.deleteHueStateQueue() // Clear dimming queue.
1077
+ return
1078
+ }
1079
+
1080
+ let xyBrightnessToHsv
1081
+ if (node.currentHUEDevice.color !== undefined && node.currentHUEDevice.color.xy !== undefined) {
1082
+ // Get the XY + brightness from the lamp and transform it into HSV
1083
+ xyBrightnessToHsv = hueColorConverter.ColorConverter.xyBrightnessToHsv(hueColorConverter.ColorConverter.scale(node.currentHUEDevice.color.xy.x, [0, 1], [0, 100]), hueColorConverter.ColorConverter.scale(node.currentHUEDevice.color.xy.y, [0, 1], [0, 100]), node.currentHUEDevice.dimming.brightness)
1084
+ } else {
1085
+ return // Something wrong here.
1086
+ }
1087
+ let numStepHSV_S = 10 // Steps from 0 to 100
1088
+
1089
+ // Set the actual brightness to start with
1090
+ if (node.brightnessStepHSV_S === null || node.brightnessStepHSV_S === undefined) node.brightnessStepHSV_S = node.currentHUEDevice.dimming.brightness
1091
+ node.brightnessStepHSV_S = Math.ceil(Number(node.brightnessStepHSV_S))
1092
+
1093
+ const minDimLevelLightHSV_S = 1
1094
+ const maxDimLevelLightHSV_S = 100
1095
+ numStepHSV_S = Math.round((maxDimLevelLightHSV_S - minDimLevelLightHSV_S) / numStepHSV_S, 0)
1096
+ // Set the speed
1097
+ _dimSpeedInMillisecsHSV = Math.ceil(_dimSpeedInMillisecsHSV / (maxDimLevelLightHSV_S / numStepHSV_S))
1098
+
1099
+ if (_KNXbrightness_DirectionHSV_S > 0 && _KNXaction === 1) {
1100
+ // DIM UP
1101
+ if (node.timerStepDimHSV_S !== undefined) clearInterval(node.timerStepDimHSV_S)
1102
+ node.timerStepDimHSV_S = setInterval(() => {
1103
+ node.deleteHueStateQueue() // Clear dimming queue.
1104
+ node.brightnessStepHSV_S += numStepHSV_S
1105
+ if (node.brightnessStepHSV_S > maxDimLevelLightHSV_S) node.brightnessStepHSV_S = maxDimLevelLightHSV_S
1106
+ // node.updateKNXLightHSV_S_State(node.brightnessStepHSV_S); // Unnecessary, but necessary to set the KNX Status in real time.
1107
+ xyBrightnessToHsv.s = node.brightnessStepHSV_S
1108
+ const hsvToXYBri = hueColorConverter.ColorConverter.hsvToxyBrightness(xyBrightnessToHsv, node.currentHUEDevice.color.gamut)
1109
+ const hueTelegram = { color: { xy: { x: hsvToXYBri.x, y: hsvToXYBri.y } }, dynamics: { duration: _dimSpeedInMillisecsHSV + 500 } } // Add some ms, to prevent the "ladder" step effect.
1110
+ // Switch on the light if off
1111
+ if (node.currentHUEDevice.on !== undefined && node.currentHUEDevice.on.on === false) {
1112
+ hueTelegram.on = { on: true }
1113
+ }
1114
+ node.currentHUEDevice.color.xy = cloneDeep(hsvToXYBri)// Unnecessary, but necessary to set the KNX Status in real time.
1115
+ node.updateKNXLightColorState(node.currentHUEDevice.color)
1116
+ node.writeHueState(hueTelegram)
1117
+ if (node.brightnessStepHSV_S >= maxDimLevelLightHSV_S) {
1118
+ if (node.timerStepDimHSV_S !== undefined) clearInterval(node.timerStepDimHSV_S)
1119
+ }
1120
+ }, _dimSpeedInMillisecsHSV)
1121
+ }
1122
+ if (_KNXbrightness_DirectionHSV_S > 0 && _KNXaction === 0) {
1123
+ if (node.currentHUEDevice.on.on === false) return // Don't dim down, if the light is already off.
1124
+ // DIM DOWN
1125
+ if (node.timerStepDimHSV_S !== undefined) clearInterval(node.timerStepDimHSV_S)
1126
+ node.timerStepDimHSV_S = setInterval(() => {
1127
+ node.deleteHueStateQueue() // Clear dimming queue.
1128
+ node.brightnessStepHSV_S -= numStepHSV_S // *2 to speed up the things
1129
+ if (node.brightnessStepHSV_S < minDimLevelLightHSV_S) node.brightnessStepHSV_S = minDimLevelLightHSV_S
1130
+ // node.updateKNXLightHSV_S_State(node.brightnessStepHSV_S); // Unnecessary, but necessary to set the KNX Status in real time.
1131
+ // Produco l'RGB
1132
+ xyBrightnessToHsv.s = node.brightnessStepHSV_S
1133
+ const hsvToXYBri = hueColorConverter.ColorConverter.hsvToxyBrightness(xyBrightnessToHsv, node.currentHUEDevice.color.gamut)
1134
+ const hueTelegram = { color: { xy: { x: hsvToXYBri.x, y: hsvToXYBri.y } }, dynamics: { duration: _dimSpeedInMillisecsHSV + 500 } }
1135
+ // Switch off the light if on
1136
+ if (node.currentHUEDevice.on !== undefined && node.currentHUEDevice.on.on === true && node.brightnessStepHSV_S === 0) {
1137
+ hueTelegram.on = { on: false }
1138
+ }
1139
+ node.currentHUEDevice.color.xy = cloneDeep(hsvToXYBri)// Unnecessary, but necessary to set the KNX Status in real time.
1140
+ node.updateKNXLightColorState(node.currentHUEDevice.color)
1141
+ node.writeHueState(hueTelegram)
1142
+ if (node.brightnessStepHSV_S <= minDimLevelLightHSV_S) {
1143
+ if (node.timerStepDimHSV_S !== undefined) clearInterval(node.timerStepDimHSV_S)
1144
+ }
1145
+ }, _dimSpeedInMillisecsHSV)
1146
+ }
1147
+ } catch (error) { /* empty */ }
1148
+ }
1149
+ // ***********************************************************
1150
+
1151
+ node.handleSendHUE = async (_event) => {
1152
+ if (_event === undefined) return
1153
+ if (_event.type !== 'grouped_light' && _event.type !== 'light') return
1154
+
1155
+ // !!!! >>> if the node is a grouped_light, the only values required, thus present, by HUE apis are "on" and "dimming".
1156
+ // !!!! >>> For all others properties like for example color and tunable white, i must get the data from one of the child lights.
1157
+
1158
+ // (async () => {
1159
+ try {
1160
+ if (node.currentHUEDevice === undefined || node.serverHue === null || node.serverHue === undefined) {
1161
+ node.setNodeStatusHue({
1162
+ fill: 'red',
1163
+ shape: 'ring',
1164
+ text: "Rejected HUE light settings. I'm still not ready...",
1165
+ payload: ''
1166
+ })
1167
+ return
1168
+ }
1169
+
1170
+ const receivedHUEObject = cloneDeep(_event)
1171
+
1172
+ // #region "CALCULATE COLOR AND COLOR TEMPERATURE OF THE LIGHTS BELONGING TO THE GROUPED_LIGHT"
1173
+ // Check wether the incoming _event belongs to a light belonging to the group_light
1174
+ if (node.isGrouped_light === true && receivedHUEObject.type === 'light') {
1175
+ // Handling of not by HUE handled Color Temperature and ColorXY
1176
+ let modifiedLight
1177
+ const groupChilds = []
1178
+ let AverageColorsXYBrightnessAndTemperature // Average color xy and color temp if the node is a grouped light.
1179
+ // If the current node is a grouped lights, i must check wether the receivedHUEObject (containing a light) belongs to the node lights collection.
1180
+ // Find all the lights in the collection, having the color_temperature capabilities, belonging to the group.
1181
+ const devices = await node.serverHue.getAllLightsBelongingToTheGroup(node.hueDevice, false)
1182
+ if (devices.length === 0) {
1183
+ devices.push(receivedHUEObject)
1184
+ }
1185
+
1186
+ for (let index = 0; index < devices.length; index++) {
1187
+ const element = devices[index]
1188
+ if (receivedHUEObject.id === element.id) {
1189
+ modifiedLight = element
1190
+ // The dimming is not necessary, beacause the HUE API already sends a group_light event with the average brightness //if (receivedHUEObject.dimming !== undefined) modifiedLight.dimming = { brightness: receivedHUEObject.dimming.brightness };
1191
+ if (receivedHUEObject.color !== undefined && receivedHUEObject.color.xy !== undefined) modifiedLight.color = receivedHUEObject.color
1192
+ if (receivedHUEObject.color_temperature !== undefined) modifiedLight.color_temperature = receivedHUEObject.color_temperature
1193
+ groupChilds.push(modifiedLight)
1194
+ } else {
1195
+ // Simply append the light
1196
+ if ((element.color_temperature !== undefined && element.color_temperature.mirek !== undefined) ||
1197
+ (element.color !== undefined && element.color.xy !== undefined)) {
1198
+ groupChilds.push(element)
1199
+ }
1200
+ }
1201
+ }
1202
+
1203
+ // Use the arithmetic average of color xy, brightness and color temperature "averageColorXYandKelvinOfGroupedLights"
1204
+ if (groupChilds !== undefined) AverageColorsXYBrightnessAndTemperature = await node.serverHue.getAverageColorsXYBrightnessAndTemperature(groupChilds)
1205
+
1206
+ // Set the new values based on average calculated above
1207
+ // CHECK FIRST THE COLOR_TEMPERATURE, because it can be undefined, because the current selected color
1208
+ // is out of the mirek range, so it cannot be represented with the colore temperature.
1209
+ if (receivedHUEObject.color_temperature !== undefined && AverageColorsXYBrightnessAndTemperature.mirek !== undefined && receivedHUEObject.color_temperature.mirel_valid === true) {
1210
+ receivedHUEObject.color_temperature.mirek = AverageColorsXYBrightnessAndTemperature.mirek
1211
+ node.currentHUEDevice.color_temperature = { mirek: AverageColorsXYBrightnessAndTemperature.mirek }
1212
+ node.updateKNXLightKelvinPercentageState(receivedHUEObject.color_temperature.mirek)
1213
+ node.updateKNXLightKelvinState(receivedHUEObject.color_temperature.mirek)
1214
+ } else if (receivedHUEObject.color !== undefined && receivedHUEObject.color.xy !== undefined && AverageColorsXYBrightnessAndTemperature.x !== undefined) {
1215
+ receivedHUEObject.color.xy.x = AverageColorsXYBrightnessAndTemperature.x
1216
+ receivedHUEObject.color.xy.y = AverageColorsXYBrightnessAndTemperature.y
1217
+ node.currentHUEDevice.color = { xy: { x: AverageColorsXYBrightnessAndTemperature.x, y: AverageColorsXYBrightnessAndTemperature.y } }
1218
+ node.updateKNXLightColorState(receivedHUEObject.color)
1219
+ }
1220
+ // The dimming is not necessary, beacause the HUE API already sends a group_light event with the average brightness
1221
+ // if (receivedHUEObject.dimming !== undefined && AverageColorsXYBrightnessAndTemperature.brightness !== undefined) {
1222
+ // receivedHUEObject.dimming = { brightness: AverageColorsXYBrightnessAndTemperature.brightness };
1223
+ // }
1224
+ // --------------------------------------------------------------------------------------------------
1225
+ if (modifiedLight !== undefined && modifiedLight.metadata !== undefined && modifiedLight.metadata.name !== undefined) {
1226
+ receivedHUEObject.lightName = modifiedLight.metadata.name
1227
+ }
1228
+ node.send(receivedHUEObject)
1229
+ return
1230
+ }
1231
+ // #endregion
1232
+
1233
+ if (receivedHUEObject.id === node.hueDevice) {
1234
+ // Output the msg to the flow
1235
+ node.send(receivedHUEObject)
1236
+
1237
+ // // DEBUG testing enable/disable HTML UI Tabs
1238
+ // delete _event.dimming;
1239
+ // delete _event.color;
1240
+ // delete _event.color_temperature;
1241
+ // delete _event.color_temperature_delta;
1242
+
1243
+ if (receivedHUEObject.on !== undefined) {
1244
+ node.updateKNXLightState(receivedHUEObject.on.on)
1245
+ // In case of switch off, set the dim to zero
1246
+ if (receivedHUEObject.on.on === false && (config.updateKNXBrightnessStatusOnHUEOnOff === undefined || config.updateKNXBrightnessStatusOnHUEOnOff === 'onhueoff')) {
1247
+ node.updateKNXBrightnessState(0)
1248
+ if (receivedHUEObject.dimming !== undefined) delete receivedHUEObject.dimming // Remove event.dimming, because has beem handled by this function and i don't want the function below to take care of it.
1249
+ } else if (receivedHUEObject.on.on === true && node.currentHUEDevice.on.on === false) {
1250
+ // Turn on: update the dimming KNX Status value as well, but only if we have a reliable value.
1251
+ // For grouped_light, the bridge exposes an aggregated brightness value via /resource/grouped_light/{id}.
1252
+ let brightVal
1253
+ if (receivedHUEObject.dimming !== undefined && receivedHUEObject.dimming.brightness !== undefined) {
1254
+ brightVal = receivedHUEObject.dimming.brightness
1255
+ } else if (node.isGrouped_light === true && typeof node.serverHue?.getHueResourceSnapshot === 'function' && config.GALightBrightnessState) {
1256
+ try {
1257
+ const snapshot = await node.serverHue.getHueResourceSnapshot(node.hueDevice, { forceRefresh: true })
1258
+ if (snapshot?.dimming?.brightness !== undefined) brightVal = snapshot.dimming.brightness
1259
+ } catch (error) { /* empty */ }
1260
+ } else if (node.currentHUEDevice.dimming !== undefined && node.currentHUEDevice.dimming.brightness !== undefined) {
1261
+ brightVal = node.currentHUEDevice.dimming.brightness
1262
+ }
1263
+ if (brightVal !== undefined) node.updateKNXBrightnessState(brightVal)
1264
+ }
1265
+ node.currentHUEDevice.on.on = receivedHUEObject.on.on
1266
+ }
1267
+
1268
+ if (receivedHUEObject.color !== undefined && receivedHUEObject.color.xy !== undefined) { // fixed https://github.com/Supergiovane/node-red-contrib-knx-ultimate/issues/287
1269
+ node.updateKNXLightColorState(receivedHUEObject.color)
1270
+ node.currentHUEDevice.color = receivedHUEObject.color
1271
+ }
1272
+
1273
+ if (receivedHUEObject.dimming !== undefined && receivedHUEObject.dimming.brightness !== undefined) {
1274
+ const zeroBrightnessWhenOff = (config.updateKNXBrightnessStatusOnHUEOnOff === undefined || config.updateKNXBrightnessStatusOnHUEOnOff === 'onhueoff')
1275
+ // Once upon n a time, the light transmit the brightness value of 0.39.
1276
+ // To avoid wrongly turn light state on, exit
1277
+ if (receivedHUEObject.dimming.brightness < 1) receivedHUEObject.dimming.brightness = 0
1278
+ // If the light is OFF and the user wants KNX brightness status to stay 0 while OFF,
1279
+ // don't propagate Hue's cached brightness (usually the last/boot brightness) back to KNX.
1280
+ if (zeroBrightnessWhenOff === true && node.currentHUEDevice?.on?.on === false && (receivedHUEObject.on === undefined || receivedHUEObject.on.on === false)) {
1281
+ if (node.currentHUEDevice.dimming !== undefined) node.currentHUEDevice.dimming.brightness = receivedHUEObject.dimming.brightness
1282
+ } else if (node.currentHUEDevice.on !== undefined && node.currentHUEDevice.on.on === false && receivedHUEObject.dimming.brightness === 0) {
1283
+ // Do nothing, because the light is off and the dimming also is 0
1284
+ } else {
1285
+ if (node.currentHUEDevice.on !== undefined && node.currentHUEDevice.on.on === false && (receivedHUEObject.on === undefined || (receivedHUEObject.on !== undefined && receivedHUEObject.on.on === true))) node.updateKNXLightState(receivedHUEObject.dimming.brightness > 0)
1286
+ node.updateKNXBrightnessState(receivedHUEObject.dimming.brightness)
1287
+ // If the brightness reaches zero, the hue lamp "on" property must be set to zero as well
1288
+ if (receivedHUEObject.dimming.brightness === 0 && node.currentHUEDevice.on !== undefined && node.currentHUEDevice.on.on === true) {
1289
+ node.writeHueState({ on: { on: false } })
1290
+ node.currentHUEDevice.on.on = false
1291
+ }
1292
+ node.currentHUEDevice.dimming.brightness = receivedHUEObject.dimming.brightness
1293
+ }
1294
+ }
1295
+
1296
+ if (receivedHUEObject.color_temperature !== undefined && receivedHUEObject.color_temperature.mirek !== undefined) {
1297
+ node.updateKNXLightKelvinPercentageState(receivedHUEObject.color_temperature.mirek)
1298
+ node.updateKNXLightKelvinState(receivedHUEObject.color_temperature.mirek)
1299
+ node.currentHUEDevice.color_temperature.mirek = receivedHUEObject.color_temperature.mirek
1300
+ }
1301
+ }
1302
+ } catch (error) {
1303
+ node.status({
1304
+ fill: 'red',
1305
+ shape: 'dot',
1306
+ text: `Matter->KNX error ${node.id} ${error.message}. Seee Log`
1307
+ })
1308
+ RED.log.error(`knxUltimateMatterControllerDevice: node.handleSendHUE = (_event): ${error.stack}`)
1309
+ }
1310
+ // })();
1311
+ }
1312
+
1313
+ // Leave the name after "function", to avoid <anonymous function> in the stack trace, in caso of errors.
1314
+ // ---- Matter -> KNX feedback (replaces the Hue event stream) -----------------
1315
+ // Applies a feedback patch from the shim: updates the synthetic currentHUEDevice
1316
+ // and reuses the very same updateKNX* writers of the Hue-born logic below.
1317
+ node.applyMatterFeedback = function applyMatterFeedback (patch, _outputtype = 'write') {
1318
+ if (!patch || node.currentHUEDevice === undefined) return
1319
+ const dev = node.currentHUEDevice
1320
+ const zeroBrightnessWhenOff = (config.updateKNXBrightnessStatusOnHUEOnOff === undefined || config.updateKNXBrightnessStatusOnHUEOnOff === 'onhueoff')
1321
+ if (patch.on !== undefined) {
1322
+ dev.on.on = !!patch.on
1323
+ node.updateKNXLightState(dev.on.on, _outputtype)
1324
+ if (zeroBrightnessWhenOff) node.updateKNXBrightnessState(dev.on.on ? dev.dimming.brightness : 0, _outputtype)
1325
+ }
1326
+ if (patch.brightnessPct !== undefined) {
1327
+ dev.dimming.brightness = Number(patch.brightnessPct)
1328
+ if (dev.on.on === true || !zeroBrightnessWhenOff) node.updateKNXBrightnessState(dev.dimming.brightness, _outputtype)
1329
+ }
1330
+ if (patch.mirek !== undefined) {
1331
+ dev.color_temperature.mirek = patch.mirek
1332
+ node.updateKNXLightKelvinState(patch.mirek, _outputtype)
1333
+ node.updateKNXLightKelvinPercentageState(patch.mirek, _outputtype)
1334
+ }
1335
+ if (patch.xyX !== undefined) dev.color.xy.x = patch.xyX
1336
+ if (patch.xyY !== undefined) dev.color.xy.y = patch.xyY
1337
+ if (patch.xyX !== undefined || patch.xyY !== undefined) node.updateKNXLightColorState({ xy: { x: dev.color.xy.x, y: dev.color.xy.y } }, _outputtype)
1338
+ }
1339
+
1340
+ // Called by matter-config on every attribute report of the subscription.
1341
+ node.handleSendMatter = (_event) => {
1342
+ try {
1343
+ if (_event === undefined || _event.attributeName === undefined) return
1344
+ if (String(_event.nodeId) !== String(node.matterNodeId)) return
1345
+ if (Number(_event.endpointId) !== Number(node.matterEndpointId)) return
1346
+ const patch = matterEventToHuePatch(_event)
1347
+ if (patch) node.applyMatterFeedback(patch)
1348
+ } catch (error) {
1349
+ RED.log.error(`knxUltimateMatterControllerDevice: node.handleSendMatter: ${error.message}`)
1350
+ }
1351
+ }
1352
+ node.handleMatterClusterEvent = () => { }
1353
+ node.handleMatterNodeInitialized = () => { }
1354
+
1355
+ node.updateKNXBrightnessState = function updateKNXBrightnessState (_value, _outputtype = 'write') {
1356
+ if (config.GALightBrightnessState !== undefined && config.GALightBrightnessState !== '') {
1357
+ const knxMsgPayload = {}
1358
+ knxMsgPayload.topic = config.GALightBrightnessState
1359
+ knxMsgPayload.dpt = config.dptLightBrightnessState
1360
+ knxMsgPayload.payload = _value
1361
+ // Send to KNX bus
1362
+ if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
1363
+ if (node.serverKNX !== null && node.serverKNX !== undefined) {
1364
+ node.serverKNX.sendKNXTelegramToKNXEngine({
1365
+ grpaddr: knxMsgPayload.topic,
1366
+ payload: knxMsgPayload.payload,
1367
+ dpt: knxMsgPayload.dpt,
1368
+ outputtype: _outputtype,
1369
+ nodecallerid: node.id
1370
+ })
1371
+ }
1372
+ }
1373
+ node.setNodeStatusHue({
1374
+ fill: 'blue',
1375
+ shape: 'ring',
1376
+ text: 'Matter->KNX Bright',
1377
+ payload: knxMsgPayload.payload
1378
+ })
1379
+ }
1380
+ }
1381
+
1382
+ node.updateKNXLightState = function updateKNXLightState (_value, _outputtype = 'write') {
1383
+ try {
1384
+ const knxMsgPayload = {}
1385
+ knxMsgPayload.topic = config.GALightState
1386
+ knxMsgPayload.dpt = config.dptLightState
1387
+ knxMsgPayload.payload = _value
1388
+ if (config.GALightState !== undefined && config.GALightState !== '') {
1389
+ // Check not to have already sent the value
1390
+ // Send to KNX bus
1391
+ if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
1392
+ if (node.serverKNX !== null && node.serverKNX !== undefined) {
1393
+ node.serverKNX.sendKNXTelegramToKNXEngine({
1394
+ grpaddr: knxMsgPayload.topic,
1395
+ payload: knxMsgPayload.payload,
1396
+ dpt: knxMsgPayload.dpt,
1397
+ outputtype: _outputtype,
1398
+ nodecallerid: node.id
1399
+ })
1400
+ }
1401
+ }
1402
+ node.setNodeStatusHue({
1403
+ fill: 'blue',
1404
+ shape: 'ring',
1405
+ text: 'Matter->KNX On/Off',
1406
+ payload: knxMsgPayload.payload
1407
+ })
1408
+ }
1409
+ } catch (error) {
1410
+ /* empty */
1411
+ }
1412
+ }
1413
+
1414
+ node.updateKNXLightKelvinPercentageState = function updateKNXLightKelvinPercentageState (_value, _outputtype = 'write') {
1415
+ if (config.GALightKelvinPercentageState !== undefined && config.GALightKelvinPercentageState !== '') {
1416
+ const knxMsgPayload = {}
1417
+ knxMsgPayload.topic = config.GALightKelvinPercentageState
1418
+ knxMsgPayload.dpt = config.dptLightKelvinPercentageState
1419
+ if (config.dptLightKelvinPercentageState === '5.001') {
1420
+ const retPercent = hueColorConverter.ColorConverter.scale(_value, [153, 500], [0, 100])
1421
+ knxMsgPayload.payload = 100 - retPercent
1422
+ }
1423
+ // Send to KNX bus
1424
+ if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
1425
+ if (node.serverKNX !== null && node.serverKNX !== undefined) {
1426
+ node.serverKNX.sendKNXTelegramToKNXEngine({
1427
+ grpaddr: knxMsgPayload.topic,
1428
+ payload: knxMsgPayload.payload,
1429
+ dpt: knxMsgPayload.dpt,
1430
+ outputtype: _outputtype,
1431
+ nodecallerid: node.id
1432
+ })
1433
+ }
1434
+ }
1435
+ node.setNodeStatusHue({
1436
+ fill: 'blue',
1437
+ shape: 'ring',
1438
+ text: 'Matter->KNX Tunable White',
1439
+ payload: knxMsgPayload.payload
1440
+ })
1441
+ }
1442
+ }
1443
+
1444
+ // /**
1445
+ // * Update the internal HSV variable status and send the valuge in % to the KNX BUS
1446
+ // * @param {number} _value 0-100%
1447
+ // * @param {string} _outputtype "write" is the default KNX command
1448
+ // * * @returns {}
1449
+ // */
1450
+ // node.updateKNXLightHSV_H_State = function updateKNXLightHSV_H_State(_value, _outputtype = "write") {
1451
+ // if (config.GALightHSV_H_State !== undefined && config.GALightHSV_H_State !== "") {
1452
+ // const knxMsgPayload = {};
1453
+ // knxMsgPayload.topic = config.GALightHSV_H_State;
1454
+ // knxMsgPayload.dpt = config.dptLightHSV_H_State;
1455
+ // if (config.dptLightHSV_H_State === "5.001") {
1456
+ // knxMsgPayload.payload = 100 - _value;
1457
+ // }
1458
+ // // Send to KNX bus
1459
+ // if (knxMsgPayload.topic !== "" && knxMsgPayload.topic !== undefined) {
1460
+ // if (node.serverKNX !== null && node.serverKNX !== undefined) {
1461
+ // node.serverKNX.sendKNXTelegramToKNXEngine({
1462
+ // grpaddr: knxMsgPayload.topic,
1463
+ // payload: knxMsgPayload.payload,
1464
+ // dpt: knxMsgPayload.dpt,
1465
+ // outputtype: _outputtype || "write",
1466
+ // nodecallerid: node.id,
1467
+ // });
1468
+ // }
1469
+ // }
1470
+ // node.setNodeStatusHue({
1471
+ // fill: "blue",
1472
+ // shape: "ring",
1473
+ // text: "Matter->KNX HSV (H)",
1474
+ // payload: knxMsgPayload.payload,
1475
+ // });
1476
+ // }
1477
+ // };
1478
+
1479
+ // /**
1480
+ // * Update the internal HSV variable status and send the valuge in % to the KNX BUS
1481
+ // * @param {number} _value 0-100%
1482
+ // * @param {string} _outputtype "write" is the default KNX command
1483
+ // * @returns {}
1484
+ // */
1485
+ // node.updateKNXLightHSV_S_State = function updateKNXLightHSV_S_State(_value, _outputtype = "write") {
1486
+ // if (config.GALightHSV_S_State !== undefined && config.GALightHSV_S_State !== "") {
1487
+ // const knxMsgPayload = {};
1488
+ // knxMsgPayload.topic = config.GALightHSV_S_State;
1489
+ // knxMsgPayload.dpt = config.dptLightHSV_S_State;
1490
+ // if (config.dptLightHSV_S_State === "5.001") {
1491
+ // knxMsgPayload.payload = _value;
1492
+ // }
1493
+ // // Send to KNX bus
1494
+ // if (knxMsgPayload.topic !== "" && knxMsgPayload.topic !== undefined) {
1495
+ // if (node.serverKNX !== null && node.serverKNX !== undefined) {
1496
+ // node.serverKNX.sendKNXTelegramToKNXEngine({
1497
+ // grpaddr: knxMsgPayload.topic,
1498
+ // payload: knxMsgPayload.payload,
1499
+ // dpt: knxMsgPayload.dpt,
1500
+ // outputtype: _outputtype || "write",
1501
+ // nodecallerid: node.id,
1502
+ // });
1503
+ // }
1504
+ // }
1505
+ // node.setNodeStatusHue({
1506
+ // fill: "blue",
1507
+ // shape: "ring",
1508
+ // text: "Matter->KNX HSV (H)",
1509
+ // payload: knxMsgPayload.payload,
1510
+ // });
1511
+ // }
1512
+ // };
1513
+
1514
+ /**
1515
+ * Update the KNC colors and HSV states group addresses
1516
+ * @param {object} _value {xy:{x,y}} in 0-1 scale
1517
+ * @param {string} _outputtype "write" is the default KNX command
1518
+ * @returns {}
1519
+ */
1520
+ node.updateKNXLightColorState = function updateKNXLightColorState (_value, _outputtype = 'write') {
1521
+ if (config.GALightColorState !== undefined && config.GALightColorState !== '') {
1522
+ if (_value.xy === undefined || _value.xy.x === undefined) return
1523
+ const knxMsgPayload = {}
1524
+ knxMsgPayload.topic = config.GALightColorState
1525
+ knxMsgPayload.dpt = config.dptLightColorState
1526
+ try {
1527
+ knxMsgPayload.payload = hueColorConverter.ColorConverter.xyBriToRgb(
1528
+ _value.xy.x,
1529
+ _value.xy.y,
1530
+ node.currentHUEDevice !== undefined && node.currentHUEDevice.dimming !== undefined && node.currentHUEDevice.dimming.brightness === undefined ? node.currentHUEDevice.dimming.brightness : 100
1531
+ )
1532
+ knxMsgPayload.payload = { red: knxMsgPayload.payload.r, green: knxMsgPayload.payload.g, blue: knxMsgPayload.payload.b }
1533
+ // Send to KNX bus
1534
+ if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
1535
+ if (node.serverKNX !== null && node.serverKNX !== undefined) {
1536
+ node.serverKNX.sendKNXTelegramToKNXEngine({
1537
+ grpaddr: knxMsgPayload.topic,
1538
+ payload: knxMsgPayload.payload,
1539
+ dpt: knxMsgPayload.dpt,
1540
+ outputtype: _outputtype,
1541
+ nodecallerid: node.id
1542
+ })
1543
+ }
1544
+ }
1545
+ node.setNodeStatusHue({
1546
+ fill: 'blue',
1547
+ shape: 'ring',
1548
+ text: 'Matter->KNX Color',
1549
+ payload: knxMsgPayload.payload
1550
+ })
1551
+ } catch (error) { }
1552
+ }
1553
+ if (config.GALightHSV_S_State !== undefined && config.GALightHSV_S_State !== '') {
1554
+ const knxMsgPayload = {}
1555
+ knxMsgPayload.topic = config.GALightHSV_S_State
1556
+ knxMsgPayload.dpt = config.dptLightHSV_S_State
1557
+ if (config.dptLightHSV_S_State === '5.001') {
1558
+ knxMsgPayload.payload = hueColorConverter.ColorConverter.xyBrightnessToHsv(_value.xy.x, _value.xy.y, node.currentHUEDevice !== undefined && node.currentHUEDevice.dimming !== undefined && node.currentHUEDevice.dimming.brightness === undefined ? node.currentHUEDevice.dimming.brightness : 100, false)
1559
+ knxMsgPayload.payload = knxMsgPayload.payload.s
1560
+ }
1561
+ // Send to KNX bus
1562
+ if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
1563
+ if (node.serverKNX !== null && node.serverKNX !== undefined) {
1564
+ node.serverKNX.sendKNXTelegramToKNXEngine({
1565
+ grpaddr: knxMsgPayload.topic,
1566
+ payload: knxMsgPayload.payload,
1567
+ dpt: knxMsgPayload.dpt,
1568
+ outputtype: _outputtype || 'write',
1569
+ nodecallerid: node.id
1570
+ })
1571
+ }
1572
+ }
1573
+ node.setNodeStatusHue({
1574
+ fill: 'blue',
1575
+ shape: 'ring',
1576
+ text: 'Matter->KNX HSV (S)',
1577
+ payload: knxMsgPayload.payload
1578
+ })
1579
+ }
1580
+ if (config.GALightHSV_H_State !== undefined && config.GALightHSV_H_State !== '') {
1581
+ const knxMsgPayload = {}
1582
+ knxMsgPayload.topic = config.GALightHSV_H_State
1583
+ knxMsgPayload.dpt = config.dptLightHSV_H_State
1584
+ if (config.dptLightHSV_H_State === '5.001') {
1585
+ knxMsgPayload.payload = hueColorConverter.ColorConverter.xyBrightnessToHsv(_value.xy.x, _value.xy.y, node.currentHUEDevice !== undefined && node.currentHUEDevice.dimming !== undefined && node.currentHUEDevice.dimming.brightness === undefined ? node.currentHUEDevice.dimming.brightness : 100, false)
1586
+ knxMsgPayload.payload = 100 - knxMsgPayload.payload.h
1587
+ }
1588
+ // Send to KNX bus
1589
+ if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
1590
+ if (node.serverKNX !== null && node.serverKNX !== undefined) {
1591
+ node.serverKNX.sendKNXTelegramToKNXEngine({
1592
+ grpaddr: knxMsgPayload.topic,
1593
+ payload: knxMsgPayload.payload,
1594
+ dpt: knxMsgPayload.dpt,
1595
+ outputtype: _outputtype || 'write',
1596
+ nodecallerid: node.id
1597
+ })
1598
+ }
1599
+ }
1600
+ node.setNodeStatusHue({
1601
+ fill: 'blue',
1602
+ shape: 'ring',
1603
+ text: 'Matter->KNX HSV (H)',
1604
+ payload: knxMsgPayload.payload
1605
+ })
1606
+ }
1607
+ }
1608
+
1609
+ node.updateKNXLightKelvinState = function updateKNXLightKelvinState (_value, _outputtype = 'write') {
1610
+ if (config.GALightKelvinState !== undefined && config.GALightKelvinState !== '') {
1611
+ const knxMsgPayload = {}
1612
+ const kelvinValue = 0
1613
+ knxMsgPayload.topic = config.GALightKelvinState
1614
+ knxMsgPayload.dpt = config.dptLightKelvinState
1615
+ if (config.dptLightKelvinState === '7.600') {
1616
+ knxMsgPayload.payload = hueColorConverter.ColorConverter.mirekToKelvin(_value)
1617
+ // knxMsgPayload.payload = hueColorConverter.ColorConverter.scale(kelvinValue, [2000, 6535], [0, 65535]);
1618
+ } else if (config.dptLightKelvinState === '9.002') {
1619
+ knxMsgPayload.payload = hueColorConverter.ColorConverter.mirekToKelvin(_value)
1620
+ }
1621
+ // Send to KNX bus
1622
+ if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
1623
+ if (node.serverKNX !== null && node.serverKNX !== undefined) {
1624
+ node.serverKNX.sendKNXTelegramToKNXEngine({
1625
+ grpaddr: knxMsgPayload.topic,
1626
+ payload: knxMsgPayload.payload,
1627
+ dpt: knxMsgPayload.dpt,
1628
+ outputtype: _outputtype,
1629
+ nodecallerid: node.id
1630
+ })
1631
+ }
1632
+
1633
+ node.setNodeStatusHue({
1634
+ fill: 'blue',
1635
+ shape: 'ring',
1636
+ text: 'Matter->KNX Kelvin',
1637
+ payload: knxMsgPayload.payload
1638
+ })
1639
+ }
1640
+ }
1641
+ }
1642
+
1643
+ // On each deploy, unsubscribe+resubscribe
1644
+ if (node.serverKNX) {
1645
+ node.serverKNX.removeClient(node)
1646
+ node.serverKNX.addClient(node)
1647
+ } else {
1648
+ node.setNodeStatus({
1649
+ fill: 'yellow',
1650
+ shape: 'ring',
1651
+ text: 'No KNX gateway selected',
1652
+ payload: ''
1653
+ })
1654
+ }
1655
+ if (node.serverHue) {
1656
+ try {
1657
+ node.serverHue.removeClient(node)
1658
+ node.serverHue.addClient(node)
1659
+ } catch (error) {
1660
+ RED.log.error('knxUltimateMatterControllerDevice: if (node.serverKNX): ' + error.message)
1661
+ }
1662
+ }
1663
+
1664
+ node.on('input', (msg, send, done) => {
1665
+ try {
1666
+ const state = RED.util.cloneMessage(msg)
1667
+ node.writeHueState(state)
1668
+ node.setNodeStatusHue({
1669
+ fill: 'green',
1670
+ shape: 'dot',
1671
+ text: '->Matter',
1672
+ payload: 'Flow msg.'
1673
+ })
1674
+ } catch (error) {
1675
+ node.setNodeStatusHue({
1676
+ fill: 'red',
1677
+ shape: 'dot',
1678
+ text: '->Matter',
1679
+ payload: error.message
1680
+ })
1681
+ }
1682
+ // Once finished, call 'done'.
1683
+ // This call is wrapped in a check that 'done' exists
1684
+ // so the node will work in earlier versions of Node-RED (<1.0)
1685
+ if (done) {
1686
+ done()
1687
+ }
1688
+ })
1689
+
1690
+ node.on('close', (done) => {
1691
+ if (node.serverKNX) {
1692
+ node.serverKNX.removeClient(node)
1693
+ }
1694
+ if (node.serverHue) {
1695
+ node.serverHue.removeClient(node)
1696
+ }
1697
+ done()
1698
+ })
1699
+ }
1700
+ RED.nodes.registerType('knxUltimateMatterControllerDevice', knxUltimateMatterControllerDevice)
1701
+ }