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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +13 -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 +52 -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
@@ -77,6 +77,12 @@ module.exports = function (RED) {
77
77
  // The Matter device id is the (stable) Node-RED node id, so the endpoint survives re-deploys.
78
78
  node.matterDeviceId = node.id
79
79
  node.invertPosition = config.invertPosition === true || config.invertPosition === 'true'
80
+ node.turnOnBehavior = config.turnOnBehavior || 'ignoreLevelAfterOn'
81
+ node.ignoreLevelAfterOnMs = Number(config.ignoreLevelAfterOnMs)
82
+ if (!Number.isFinite(node.ignoreLevelAfterOnMs) || node.ignoreLevelAfterOnMs < 0) node.ignoreLevelAfterOnMs = 800
83
+ node.coverUpdateMode = config.coverUpdateMode || 'optimistic'
84
+ node.coverStatusTimeoutMs = Number(config.coverStatusTimeoutMs)
85
+ if (!Number.isFinite(node.coverStatusTimeoutMs) || node.coverStatusTimeoutMs < 0) node.coverStatusTimeoutMs = 3000
80
86
  node.readStatusAtStartup = config.readStatusAtStartup === undefined || config.readStatusAtStartup === 'yes'
81
87
  node.enableNodePINS = config.enableNodePINS === 'yes'
82
88
  node.inputs = node.enableNodePINS ? 1 : 0
@@ -101,6 +107,8 @@ module.exports = function (RED) {
101
107
  node.formatdecimalsvalue = 2
102
108
 
103
109
  let initialReadTimer = null
110
+ let suppressLevelCommandsUntil = 0
111
+ let coverStatusTimer = null
104
112
 
105
113
  const formatTs = (date) => {
106
114
  const d = date instanceof Date ? date : new Date(date)
@@ -141,6 +149,13 @@ module.exports = function (RED) {
141
149
  }
142
150
  }
143
151
 
152
+ const clearCoverStatusTimer = () => {
153
+ if (coverStatusTimer !== null) {
154
+ clearTimeout(coverStatusTimer)
155
+ coverStatusTimer = null
156
+ }
157
+ }
158
+
144
159
  // Precompute the routing tables for this single device:
145
160
  // statusRoutes: GA -> [{ fn, dpt }] (KNX -> Matter)
146
161
  // commandRoutes: fn -> { ga, dpt } (Matter -> KNX)
@@ -158,13 +173,37 @@ module.exports = function (RED) {
158
173
  commandRoutes.set(fnDef.fn, { ga, dpt })
159
174
  }
160
175
  })
176
+ const hasStatusRouteFor = (fn) => {
177
+ for (const routes of statusRoutes.values()) {
178
+ if (routes.some((route) => route.fn === fn)) return true
179
+ }
180
+ return false
181
+ }
182
+
183
+ const applyOptimisticCoverPosition = (command) => {
184
+ if (node.deviceType !== 'windowcovering' || node.coverUpdateMode !== 'optimistic' || !node.serverMatterBridge) return
185
+ let optimisticPosition
186
+ if (command.fn === 'position') optimisticPosition = command.value
187
+ if (command.fn === 'updown') optimisticPosition = command.value === true ? 100 : 0
188
+ if (optimisticPosition === undefined) return
189
+ node.serverMatterBridge.setDeviceState(node.matterDeviceId, 'position', optimisticPosition)
190
+ clearCoverStatusTimer()
191
+ if (node.coverStatusTimeoutMs > 0 && hasStatusRouteFor('position')) {
192
+ coverStatusTimer = setTimeout(() => {
193
+ coverStatusTimer = null
194
+ node.setNodeStatus({ fill: 'yellow', shape: 'ring', text: 'Waiting for KNX cover status', payload: optimisticPosition })
195
+ }, node.coverStatusTimeoutMs)
196
+ }
197
+ }
198
+ node.knxUltimateAcceptedGAs = Array.from(statusRoutes.keys())
161
199
 
162
200
  // The device definition consumed by the bridge engine (via the config node).
163
201
  node.getMatterDef = () => ({
164
202
  id: node.matterDeviceId,
165
203
  type: node.deviceType,
166
204
  name: node.name,
167
- invertPosition: node.invertPosition === true
205
+ invertPosition: node.invertPosition === true,
206
+ coverUpdateMode: node.coverUpdateMode
168
207
  })
169
208
 
170
209
  // Reflects the bridge (config node) status on this device node.
@@ -198,6 +237,9 @@ module.exports = function (RED) {
198
237
  })
199
238
  } catch (error) { /* empty */ }
200
239
  }
240
+ // Confirm the requested position to Matter even when this is a flow-only
241
+ // device and no KNX command GA is configured.
242
+ applyOptimisticCoverPosition(command)
201
243
  const route = commandRoutes.get(command.fn)
202
244
  if (route === undefined) {
203
245
  if (node.enableNodePINS) return // Flow-only device: the flow already got the command
@@ -209,6 +251,13 @@ module.exports = function (RED) {
209
251
  node.setNodeStatus({ fill: 'yellow', shape: 'ring', text: `No KNX gateway: enable the node PINs to handle ${command.fn}` })
210
252
  return
211
253
  }
254
+ if (command.fn === 'onoff' && command.value === true && node.turnOnBehavior === 'ignoreLevelAfterOn') {
255
+ suppressLevelCommandsUntil = Date.now() + node.ignoreLevelAfterOnMs
256
+ }
257
+ if (command.fn === 'level' && node.turnOnBehavior === 'ignoreLevelAfterOn' && Date.now() < suppressLevelCommandsUntil) {
258
+ node.setNodeStatus({ fill: 'yellow', shape: 'ring', text: 'Ignored Matter level after On', payload: command.value })
259
+ return
260
+ }
212
261
  safeSendToKNX({ grpaddr: route.ga, payload: command.value, dpt: route.dpt, outputtype: 'write' }, 'write')
213
262
  node.setNodeStatus({ fill: 'green', shape: 'dot', text: `Matter->KNX ${command.fn}`, payload: command.value })
214
263
  } catch (error) {
@@ -228,6 +277,7 @@ module.exports = function (RED) {
228
277
  const value = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(route.dpt))
229
278
  if (value === undefined || value === null) return
230
279
  node.serverMatterBridge.setDeviceState(node.matterDeviceId, route.fn, value)
280
+ if (node.deviceType === 'windowcovering' && route.fn === 'position') clearCoverStatusTimer()
231
281
  node.setNodeStatus({ fill: 'blue', shape: 'dot', text: `KNX->Matter ${route.fn}`, payload: value })
232
282
  } catch (error) {
233
283
  node.setNodeStatus({ fill: 'red', shape: 'dot', text: `KNX->Matter error ${error.message}` })
@@ -298,6 +348,7 @@ module.exports = function (RED) {
298
348
  node.on('close', (done) => {
299
349
  try {
300
350
  if (initialReadTimer !== null) clearTimeout(initialReadTimer)
351
+ clearCoverStatusTimer()
301
352
  } catch (error) { /* empty */ }
302
353
  try {
303
354
  if (node.serverKNX) node.serverKNX.removeClient(node)