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.
- package/CHANGELOG.md +13 -0
- package/nodes/commonFunctions.js +40 -3
- package/nodes/knxUltimate-config.js +379 -335
- package/nodes/knxUltimate.html +8 -9
- package/nodes/knxUltimate.js +5 -5
- package/nodes/knxUltimateAI.js +1 -1
- package/nodes/knxUltimateHueLight.html +3 -3
- package/nodes/knxUltimateHueLight.js +1 -1
- package/nodes/knxUltimateMatterBridge.html +321 -247
- package/nodes/knxUltimateMatterBridge.js +52 -1
- package/nodes/knxUltimateMatterControllerDevice.html +2119 -0
- package/nodes/knxUltimateMatterControllerDevice.js +1701 -0
- package/nodes/locales/de/knxUltimateMatterBridge.html +6 -2
- package/nodes/locales/de/knxUltimateMatterBridge.json +14 -1
- package/nodes/locales/de/knxUltimateMatterControllerDevice.html +27 -0
- package/nodes/locales/de/knxUltimateMatterControllerDevice.json +97 -0
- package/nodes/locales/de/matter-config.json +5 -1
- package/nodes/locales/de/matterbridge-config.json +1 -0
- package/nodes/locales/en/knxUltimateMatterBridge.html +6 -2
- package/nodes/locales/en/knxUltimateMatterBridge.json +14 -1
- package/nodes/locales/en/knxUltimateMatterControllerDevice.html +27 -0
- package/nodes/locales/en/knxUltimateMatterControllerDevice.json +97 -0
- package/nodes/locales/en/matter-config.json +6 -1
- package/nodes/locales/en/matterbridge-config.json +1 -0
- package/nodes/locales/es/knxUltimateMatterBridge.html +6 -2
- package/nodes/locales/es/knxUltimateMatterBridge.json +14 -1
- package/nodes/locales/es/knxUltimateMatterControllerDevice.html +27 -0
- package/nodes/locales/es/knxUltimateMatterControllerDevice.json +97 -0
- package/nodes/locales/es/matter-config.json +5 -1
- package/nodes/locales/es/matterbridge-config.json +1 -0
- package/nodes/locales/fr/knxUltimateMatterBridge.html +6 -2
- package/nodes/locales/fr/knxUltimateMatterBridge.json +14 -1
- package/nodes/locales/fr/knxUltimateMatterControllerDevice.html +27 -0
- package/nodes/locales/fr/knxUltimateMatterControllerDevice.json +89 -0
- package/nodes/locales/fr/matter-config.json +5 -1
- package/nodes/locales/fr/matterbridge-config.json +1 -0
- package/nodes/locales/it/knxUltimateMatterBridge.html +6 -2
- package/nodes/locales/it/knxUltimateMatterBridge.json +14 -1
- package/nodes/locales/it/knxUltimateMatterControllerDevice.html +27 -0
- package/nodes/locales/it/knxUltimateMatterControllerDevice.json +97 -0
- package/nodes/locales/it/matter-config.json +6 -1
- package/nodes/locales/it/matterbridge-config.json +1 -0
- package/nodes/locales/zh-CN/knxUltimateMatterBridge.html +6 -2
- package/nodes/locales/zh-CN/knxUltimateMatterBridge.json +14 -1
- package/nodes/locales/zh-CN/knxUltimateMatterControllerDevice.html +27 -0
- package/nodes/locales/zh-CN/knxUltimateMatterControllerDevice.json +89 -0
- package/nodes/locales/zh-CN/matter-config.json +5 -1
- package/nodes/locales/zh-CN/matterbridge-config.json +1 -0
- package/nodes/matter-config.html +58 -6
- package/nodes/matter-config.js +7 -2
- package/nodes/matterbridge-config.html +1 -0
- package/nodes/utils/lightEngines/canonicalLight.js +75 -0
- package/nodes/utils/lightEngines/hueLightEngine.js +51 -0
- package/nodes/utils/lightEngines/index.js +25 -0
- package/nodes/utils/lightEngines/knxLightCommand.js +79 -0
- package/nodes/utils/lightEngines/lightEngine.js +46 -0
- package/nodes/utils/lightEngines/matterHueShim.js +59 -0
- package/nodes/utils/lightEngines/matterLightEngine.js +117 -0
- package/nodes/utils/matterEngine.mjs +185 -14
- package/nodes/utils/matterKnxConverter.js +1 -1
- package/package.json +11 -3
- package/resources/KNXSendSnippets.js +53 -53
- package/resources/htmlUtils.js +6 -4
- package/nodes/knxUltimateMatterDevice.html +0 -632
- package/nodes/knxUltimateMatterDevice.js +0 -308
- package/nodes/locales/de/knxUltimateMatterDevice.html +0 -56
- package/nodes/locales/de/knxUltimateMatterDevice.json +0 -109
- package/nodes/locales/en/knxUltimateMatterDevice.html +0 -56
- package/nodes/locales/en/knxUltimateMatterDevice.json +0 -109
- package/nodes/locales/es/knxUltimateMatterDevice.html +0 -56
- package/nodes/locales/es/knxUltimateMatterDevice.json +0 -109
- package/nodes/locales/fr/knxUltimateMatterDevice.html +0 -56
- package/nodes/locales/fr/knxUltimateMatterDevice.json +0 -109
- package/nodes/locales/it/knxUltimateMatterDevice.html +0 -56
- package/nodes/locales/it/knxUltimateMatterDevice.json +0 -109
- package/nodes/locales/zh-CN/knxUltimateMatterDevice.html +0 -56
- 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)
|