node-red-contrib-knx-ultimate 6.0.1 → 6.0.3
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 +9 -0
- package/nodes/knxUltimateMatterBridge.html +13 -1
- package/nodes/knxUltimateMatterBridge.js +9 -0
- package/nodes/locales/de/knxUltimateMatterBridge.json +3 -1
- package/nodes/locales/en/knxUltimateMatterBridge.json +3 -1
- package/nodes/locales/es/knxUltimateMatterBridge.json +3 -1
- package/nodes/locales/fr/knxUltimateMatterBridge.json +3 -1
- package/nodes/locales/it/knxUltimateMatterBridge.json +3 -1
- package/nodes/locales/zh-CN/knxUltimateMatterBridge.json +3 -1
- package/nodes/utils/matterBridgeDeviceFactory.mjs +116 -26
- package/nodes/utils/matterBridgeEngine.mjs +3 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,15 @@
|
|
|
6
6
|
|
|
7
7
|
# CHANGELOG
|
|
8
8
|
|
|
9
|
+
**Version 6.0.3** - July 2026<br/>
|
|
10
|
+
|
|
11
|
+
- **Expose KNX to Matter (BETA) — Alexa cover percentage workaround**: covers/shutters can optionally be exposed to Matter controllers as dimmable lights when Alexa does not send percentage commands to a native Window Covering endpoint. Brightness represents openness (`100%` open, `0%` closed), while On/Off opens or closes the cover. Commands and status remain mapped through the existing KNX cover position GA/DPT, including position inversion and loop-protected KNX-to-Matter feedback. The standard Matter Window Covering endpoint remains the default.<br/>
|
|
12
|
+
|
|
13
|
+
**Version 6.0.2** - July 2026<br/>
|
|
14
|
+
|
|
15
|
+
- **Expose KNX to Matter (BETA) — covers/shutters with Alexa**: initialize position-aware covers with a valid numeric Matter position instead of an unknown (`null`) value. This helps controllers such as Alexa expose percentage positioning and send `GoToLiftPercentage` instead of degrading to Open/Close only. Added targeted command diagnostics to the Node-RED log and output message for interoperability testing.<br/>
|
|
16
|
+
- **Expose KNX to Matter (BETA) — raw flow output**: On/Off and absolute dimming commands are now captured at the Matter command boundary, so the optional output PIN forwards repeated commands even when the Matter state is already unchanged. Matter validation still runs before KNX routing, and KNX-to-Matter status updates remain loop-protected.<br/>
|
|
17
|
+
|
|
9
18
|
**Version 6.0.1** - July 2026<br/>
|
|
10
19
|
|
|
11
20
|
- **Expose KNX to Matter (BETA) — flow-only covers/shutters**: fixed intermediate position commands reaching the Node-RED output but not being optimistically confirmed to Matter when no KNX command group address was configured. Voice assistants such as Alexa no longer report that the device is not responding after a percentage-position command.<br/>
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
name: { value: '' },
|
|
76
76
|
deviceType: { value: 'onofflight' },
|
|
77
77
|
invertPosition: { value: false },
|
|
78
|
+
coverExposeAsDimmableLight: { value: false },
|
|
78
79
|
turnOnBehavior: { value: 'ignoreLevelAfterOn' },
|
|
79
80
|
ignoreLevelAfterOnMs: { value: 800 },
|
|
80
81
|
coverUpdateMode: { value: 'optimistic' },
|
|
@@ -228,6 +229,7 @@
|
|
|
228
229
|
node._matterGaInputs = gaInputs;
|
|
229
230
|
|
|
230
231
|
$('#node-input-invertPosition').prop('checked', node.invertPosition === true);
|
|
232
|
+
$('#node-input-coverExposeAsDimmableLight').prop('checked', node.coverExposeAsDimmableLight === true);
|
|
231
233
|
$('#node-input-turnOnBehavior').val(node.turnOnBehavior || 'ignoreLevelAfterOn');
|
|
232
234
|
$('#node-input-ignoreLevelAfterOnMs').val(node.ignoreLevelAfterOnMs === undefined ? 800 : node.ignoreLevelAfterOnMs);
|
|
233
235
|
$('#node-input-coverUpdateMode').val(node.coverUpdateMode || 'optimistic');
|
|
@@ -241,6 +243,7 @@
|
|
|
241
243
|
const selected = $('#node-input-deviceType').val();
|
|
242
244
|
node.deviceType = selected;
|
|
243
245
|
node.invertPosition = $('#node-input-invertPosition').is(':checked');
|
|
246
|
+
node.coverExposeAsDimmableLight = selected === 'windowcovering' && $('#node-input-coverExposeAsDimmableLight').is(':checked');
|
|
244
247
|
const active = TYPE_FIELDS[selected] || [];
|
|
245
248
|
const gaInputs = node._matterGaInputs || {};
|
|
246
249
|
// Clear every GA/DPT, then write only the ones of the selected type, reading
|
|
@@ -346,6 +349,14 @@
|
|
|
346
349
|
<input type="number" id="node-input-ignoreLevelAfterOnMs" min="0" step="100" style="width:120px;">
|
|
347
350
|
</div>
|
|
348
351
|
|
|
352
|
+
<div class="form-row mb-cover-advanced-row" style="display:none; margin-top:8px;">
|
|
353
|
+
<label for="node-input-coverExposeAsDimmableLight" style="width:auto;">
|
|
354
|
+
<input type="checkbox" id="node-input-coverExposeAsDimmableLight" style="width:auto; display:inline-block; vertical-align:middle;">
|
|
355
|
+
<span data-i18n="knxUltimateMatterBridge.advanced.cover_expose_as_light"></span>
|
|
356
|
+
</label>
|
|
357
|
+
<div class="form-tips" style="margin-top:5px;" data-i18n="knxUltimateMatterBridge.advanced.cover_expose_as_light_help"></div>
|
|
358
|
+
</div>
|
|
359
|
+
|
|
349
360
|
<div class="form-row mb-cover-advanced-row" style="display:none; margin-top:8px;">
|
|
350
361
|
<label for="node-input-coverUpdateMode" style="width:260px;">
|
|
351
362
|
<i class="fa fa-window-maximize"></i> <span data-i18n="knxUltimateMatterBridge.advanced.cover_update_mode"></span>
|
|
@@ -406,6 +417,7 @@ The advanced compatibility options are intentionally type-specific:
|
|
|
406
417
|
|
|
407
418
|
- dimmable devices can ignore the brightness command that some controllers send immediately after an On command
|
|
408
419
|
- covers can optimistically update the Matter position after a command, then correct it when the KNX status GA reports the real position
|
|
420
|
+
- if Alexa cannot set a cover percentage, a cover can be exposed as a dimmable light: brightness means openness, while the configured KNX cover GAs and DPTs remain unchanged
|
|
409
421
|
|
|
410
422
|
The **KNX gateway is optional**: without it, enable the node PINs and drive the device from the flow.
|
|
411
423
|
|
|
@@ -413,4 +425,4 @@ If you enable the **node PINs**:
|
|
|
413
425
|
|
|
414
426
|
- input: `msg.payload = { function: "onoff", value: true }` updates the Matter state without the KNX bus
|
|
415
427
|
- output: every command from a Matter controller is forwarded as `msg` (topic = device name, payload = value, `msg.matter` = raw command)
|
|
416
|
-
</script>
|
|
428
|
+
</script>
|
|
@@ -77,6 +77,7 @@ 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.coverExposeAsDimmableLight = config.coverExposeAsDimmableLight === true || config.coverExposeAsDimmableLight === 'true'
|
|
80
81
|
node.turnOnBehavior = config.turnOnBehavior || 'ignoreLevelAfterOn'
|
|
81
82
|
node.ignoreLevelAfterOnMs = Number(config.ignoreLevelAfterOnMs)
|
|
82
83
|
if (!Number.isFinite(node.ignoreLevelAfterOnMs) || node.ignoreLevelAfterOnMs < 0) node.ignoreLevelAfterOnMs = 800
|
|
@@ -203,6 +204,7 @@ module.exports = function (RED) {
|
|
|
203
204
|
type: node.deviceType,
|
|
204
205
|
name: node.name,
|
|
205
206
|
invertPosition: node.invertPosition === true,
|
|
207
|
+
coverExposeAsDimmableLight: node.coverExposeAsDimmableLight === true,
|
|
206
208
|
coverUpdateMode: node.coverUpdateMode
|
|
207
209
|
})
|
|
208
210
|
|
|
@@ -227,6 +229,13 @@ module.exports = function (RED) {
|
|
|
227
229
|
// Matter -> KNX: a controller (Alexa...) sent a command to this device.
|
|
228
230
|
node.handleMatterCommand = (command) => {
|
|
229
231
|
try {
|
|
232
|
+
if (node.deviceType === 'windowcovering' && command.matterDiagnostic && typeof node.log === 'function') {
|
|
233
|
+
node.log(`Matter WindowCovering command: ${JSON.stringify({
|
|
234
|
+
fn: command.fn,
|
|
235
|
+
value: command.value,
|
|
236
|
+
...command.matterDiagnostic
|
|
237
|
+
})}`)
|
|
238
|
+
}
|
|
230
239
|
if (node.enableNodePINS) {
|
|
231
240
|
try {
|
|
232
241
|
node.send({
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
"turn_on_behavior": "Einschaltverhalten",
|
|
24
24
|
"turn_on_ignore_level": "Helligkeit direkt nach Ein ignorieren",
|
|
25
25
|
"turn_on_forward_all": "Jeden Helligkeitsbefehl weiterleiten",
|
|
26
|
-
|
|
26
|
+
"ignore_level_ms": "Helligkeit nach Ein ignorieren (ms)",
|
|
27
|
+
"cover_expose_as_light": "Rollladen als dimmbares Licht anzeigen (Alexa-Workaround)",
|
|
28
|
+
"cover_expose_as_light_help": "Nur verwenden, wenn Alexa die Rollladenposition nicht einstellen kann. Die Helligkeit entspricht der Öffnung; die KNX-Gruppenadressen bleiben unverändert.",
|
|
27
29
|
"cover_update_mode": "Jalousie-/Rollladenstatus",
|
|
28
30
|
"cover_optimistic": "Matter-Position optimistisch aktualisieren",
|
|
29
31
|
"cover_wait_status": "Nur auf KNX-Status warten",
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
"turn_on_behavior": "Turn-on behavior",
|
|
24
24
|
"turn_on_ignore_level": "Ignore brightness sent immediately after On",
|
|
25
25
|
"turn_on_forward_all": "Forward every brightness command",
|
|
26
|
-
|
|
26
|
+
"ignore_level_ms": "Ignore brightness after On (ms)",
|
|
27
|
+
"cover_expose_as_light": "Expose cover as a dimmable light (Alexa workaround)",
|
|
28
|
+
"cover_expose_as_light_help": "Use only if Alexa cannot set the cover percentage. Brightness represents how open the cover is; KNX group addresses remain unchanged.",
|
|
27
29
|
"cover_update_mode": "Cover state update",
|
|
28
30
|
"cover_optimistic": "Optimistically update Matter position",
|
|
29
31
|
"cover_wait_status": "Wait for KNX status only",
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
"turn_on_behavior": "Comportamiento al encender",
|
|
24
24
|
"turn_on_ignore_level": "Ignorar brillo enviado justo después de On",
|
|
25
25
|
"turn_on_forward_all": "Reenviar cada comando de brillo",
|
|
26
|
-
|
|
26
|
+
"ignore_level_ms": "Ignorar brillo tras On (ms)",
|
|
27
|
+
"cover_expose_as_light": "Exponer persiana como luz regulable (solución para Alexa)",
|
|
28
|
+
"cover_expose_as_light_help": "Úsalo solo si Alexa no puede ajustar el porcentaje de la persiana. El brillo representa cuánto está abierta; las direcciones de grupo KNX no cambian.",
|
|
27
29
|
"cover_update_mode": "Actualización de estado de persiana",
|
|
28
30
|
"cover_optimistic": "Actualizar la posición Matter de forma optimista",
|
|
29
31
|
"cover_wait_status": "Esperar solo el estado KNX",
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
"turn_on_behavior": "Comportement à l'allumage",
|
|
24
24
|
"turn_on_ignore_level": "Ignorer la luminosité envoyée juste après On",
|
|
25
25
|
"turn_on_forward_all": "Transmettre chaque commande de luminosité",
|
|
26
|
-
|
|
26
|
+
"ignore_level_ms": "Ignorer la luminosité après On (ms)",
|
|
27
|
+
"cover_expose_as_light": "Exposer le volet comme lumière variable (contournement Alexa)",
|
|
28
|
+
"cover_expose_as_light_help": "À utiliser seulement si Alexa ne règle pas le pourcentage du volet. La luminosité représente son ouverture ; les adresses de groupe KNX restent inchangées.",
|
|
27
29
|
"cover_update_mode": "Mise à jour état volet",
|
|
28
30
|
"cover_optimistic": "Mettre à jour la position Matter de façon optimiste",
|
|
29
31
|
"cover_wait_status": "Attendre seulement l'état KNX",
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
"turn_on_behavior": "Comportamento all'accensione",
|
|
24
24
|
"turn_on_ignore_level": "Ignora luminosità inviata subito dopo On",
|
|
25
25
|
"turn_on_forward_all": "Inoltra ogni comando luminosità",
|
|
26
|
-
|
|
26
|
+
"ignore_level_ms": "Ignora luminosità dopo On (ms)",
|
|
27
|
+
"cover_expose_as_light": "Esponi tapparella come luce dimmerabile (workaround Alexa)",
|
|
28
|
+
"cover_expose_as_light_help": "Usa questa opzione solo se Alexa non imposta la percentuale della tapparella. La luminosità indica quanto è aperta; gli indirizzi di gruppo KNX non cambiano.",
|
|
27
29
|
"cover_update_mode": "Aggiornamento stato tapparella",
|
|
28
30
|
"cover_optimistic": "Aggiorna ottimisticamente la posizione Matter",
|
|
29
31
|
"cover_wait_status": "Attendi solo lo stato KNX",
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
"turn_on_behavior": "开灯行为",
|
|
24
24
|
"turn_on_ignore_level": "忽略 On 后立即发送的亮度",
|
|
25
25
|
"turn_on_forward_all": "转发每个亮度命令",
|
|
26
|
-
|
|
26
|
+
"ignore_level_ms": "On 后忽略亮度时间 (ms)",
|
|
27
|
+
"cover_expose_as_light": "将窗帘显示为可调光灯(Alexa 兼容方案)",
|
|
28
|
+
"cover_expose_as_light_help": "仅当 Alexa 无法设置窗帘百分比时使用。亮度表示窗帘开启程度;KNX 组地址保持不变。",
|
|
27
29
|
"cover_update_mode": "窗帘状态更新",
|
|
28
30
|
"cover_optimistic": "乐观更新 Matter 位置",
|
|
29
31
|
"cover_wait_status": "仅等待 KNX 状态",
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// Each KNX "virtual device" definition becomes a Matter endpoint under the aggregator.
|
|
4
4
|
import { Endpoint } from '@matter/main'
|
|
5
5
|
import { BridgedDeviceBasicInformationServer } from '@matter/main/behaviors/bridged-device-basic-information'
|
|
6
|
+
import { OnOffServer } from '@matter/main/behaviors/on-off'
|
|
7
|
+
import { LevelControlServer } from '@matter/main/behaviors/level-control'
|
|
6
8
|
import { OnOffLightDevice } from '@matter/main/devices/on-off-light'
|
|
7
9
|
import { DimmableLightDevice } from '@matter/main/devices/dimmable-light'
|
|
8
10
|
import { OnOffPlugInUnitDevice } from '@matter/main/devices/on-off-plug-in-unit'
|
|
@@ -82,6 +84,15 @@ function knxValueToMatterPatch (def, fn, value) {
|
|
|
82
84
|
let percent = Number(value)
|
|
83
85
|
if (Number.isNaN(percent)) return undefined
|
|
84
86
|
if (def.invertPosition === true) percent = 100 - percent
|
|
87
|
+
if (def.type === 'windowcovering' && def.coverExposeAsDimmableLight === true) {
|
|
88
|
+
// Alexa compatibility mode: light brightness represents how open the cover is.
|
|
89
|
+
// Keep the KNX convention at this boundary and invert only the Matter view.
|
|
90
|
+
const openness = clamp(100 - percent, 0, 100)
|
|
91
|
+
return {
|
|
92
|
+
onOff: { onOff: openness > 0 },
|
|
93
|
+
levelControl: { currentLevel: clamp(Math.round(openness * 254 / 100), 1, 254) }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
85
96
|
return { windowCovering: { currentPositionLiftPercent100ths: clamp(Math.round(percent * 100), 0, 10000) } }
|
|
86
97
|
}
|
|
87
98
|
case 'rgb': {
|
|
@@ -287,9 +298,90 @@ function createBridgedEndpoint (def, serialPrefix, onCommand) {
|
|
|
287
298
|
// Non nullable attributes need a sane initial value
|
|
288
299
|
if (def.type === 'contactsensor') initialState.booleanState = { stateValue: false }
|
|
289
300
|
if (def.type === 'occupancysensor') initialState.occupancySensing = { occupancy: { occupied: false } }
|
|
301
|
+
// Although Matter permits an unknown (null) position, some controllers (including
|
|
302
|
+
// Alexa) then expose only Open/Close instead of percentage positioning. Start with
|
|
303
|
+
// a valid position; KNX or flow feedback replaces it as soon as it is available.
|
|
304
|
+
if (def.type === 'windowcovering' && def.coverExposeAsDimmableLight !== true) {
|
|
305
|
+
initialState.windowCovering = {
|
|
306
|
+
currentPositionLiftPercent100ths: 0,
|
|
307
|
+
targetPositionLiftPercent100ths: 0
|
|
308
|
+
}
|
|
309
|
+
}
|
|
290
310
|
|
|
291
311
|
let endpoint
|
|
292
312
|
|
|
313
|
+
// Emit controller commands at the command boundary instead of observing attribute
|
|
314
|
+
// changes. This makes the node output truly raw: repeated commands (for example
|
|
315
|
+
// Off while already off) are still forwarded exactly once.
|
|
316
|
+
const RawOnOffBase = def.type === 'onoffplug' ? OnOffServer : OnOffServer.with('Lighting')
|
|
317
|
+
class RawOnOffServer extends RawOnOffBase {
|
|
318
|
+
async on () {
|
|
319
|
+
await super.on()
|
|
320
|
+
onCommand({ deviceId: def.id, fn: 'onoff', value: true, matterCommand: { cluster: 'OnOff', command: 'on' } })
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async off () {
|
|
324
|
+
await super.off()
|
|
325
|
+
onCommand({ deviceId: def.id, fn: 'onoff', value: false, matterCommand: { cluster: 'OnOff', command: 'off' } })
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
class RawLevelControlServer extends LevelControlServer.with('Lighting', 'OnOff') {
|
|
330
|
+
async moveToLevel (request) {
|
|
331
|
+
await super.moveToLevel(request)
|
|
332
|
+
const value = clamp(Math.round(Number(request.level) * 100 / 254), 0, 100)
|
|
333
|
+
onCommand({ deviceId: def.id, fn: 'level', value, matterCommand: { cluster: 'LevelControl', command: 'moveToLevel', request } })
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async moveToLevelWithOnOff (request) {
|
|
337
|
+
await super.moveToLevelWithOnOff(request)
|
|
338
|
+
const value = clamp(Math.round(Number(request.level) * 100 / 254), 0, 100)
|
|
339
|
+
onCommand({ deviceId: def.id, fn: 'level', value, matterCommand: { cluster: 'LevelControl', command: 'moveToLevelWithOnOff', request } })
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (def.type === 'windowcovering' && def.coverExposeAsDimmableLight === true) {
|
|
344
|
+
const invert = def.invertPosition === true
|
|
345
|
+
const emitPosition = (matterPosition, command, request) => {
|
|
346
|
+
let position = clamp(Math.round(matterPosition), 0, 100)
|
|
347
|
+
if (invert) position = 100 - position
|
|
348
|
+
onCommand({
|
|
349
|
+
deviceId: def.id,
|
|
350
|
+
fn: 'position',
|
|
351
|
+
value: position,
|
|
352
|
+
matterCommand: { cluster: command === 'on' || command === 'off' ? 'OnOff' : 'LevelControl', command, request },
|
|
353
|
+
matterDiagnostic: { handler: 'coverAsDimmableLight', command }
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
class CoverAsLightOnOffServer extends OnOffServer.with('Lighting') {
|
|
358
|
+
async on () {
|
|
359
|
+
await super.on()
|
|
360
|
+
emitPosition(0, 'on')
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async off () {
|
|
364
|
+
await super.off()
|
|
365
|
+
emitPosition(100, 'off')
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
class CoverAsLightLevelServer extends LevelControlServer.with('Lighting', 'OnOff') {
|
|
370
|
+
async moveToLevel (request) {
|
|
371
|
+
await super.moveToLevel(request)
|
|
372
|
+
emitPosition(100 - clamp(Number(request.level) * 100 / 254, 0, 100), 'moveToLevel', request)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async moveToLevelWithOnOff (request) {
|
|
376
|
+
await super.moveToLevelWithOnOff(request)
|
|
377
|
+
emitPosition(100 - clamp(Number(request.level) * 100 / 254, 0, 100), 'moveToLevelWithOnOff', request)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
endpoint = new Endpoint(DimmableLightDevice.with(CoverAsLightOnOffServer, CoverAsLightLevelServer, BridgedDeviceBasicInformationServer), initialState)
|
|
382
|
+
return endpoint
|
|
383
|
+
}
|
|
384
|
+
|
|
293
385
|
if (def.type === 'windowcovering') {
|
|
294
386
|
// The WindowCoveringServer requires the movement logic: we forward it to the KNX bus.
|
|
295
387
|
// Position feedback comes back from the KNX status GA, so we do NOT call the default
|
|
@@ -299,20 +391,35 @@ function createBridgedEndpoint (def, serialPrefix, onCommand) {
|
|
|
299
391
|
async handleMovement (type, reversed, direction, targetPercent100ths) {
|
|
300
392
|
try {
|
|
301
393
|
if (type !== MovementType.Lift) return
|
|
394
|
+
const matterDiagnostic = {
|
|
395
|
+
handler: 'handleMovement',
|
|
396
|
+
movementType: type,
|
|
397
|
+
reversed: reversed === true,
|
|
398
|
+
direction,
|
|
399
|
+
directionName: direction === MovementDirection.DefinedByPosition
|
|
400
|
+
? 'DefinedByPosition'
|
|
401
|
+
: direction === MovementDirection.Open ? 'Open' : direction === MovementDirection.Close ? 'Close' : 'Unknown',
|
|
402
|
+
targetPercent100ths: targetPercent100ths ?? null
|
|
403
|
+
}
|
|
302
404
|
if (direction === MovementDirection.DefinedByPosition && targetPercent100ths !== undefined && targetPercent100ths !== null) {
|
|
303
405
|
let percent = Math.round(Number(targetPercent100ths) / 100)
|
|
304
406
|
if (invert) percent = 100 - percent
|
|
305
|
-
onCommand({ deviceId: def.id, fn: 'position', value: clamp(percent, 0, 100) })
|
|
407
|
+
onCommand({ deviceId: def.id, fn: 'position', value: clamp(percent, 0, 100), matterDiagnostic })
|
|
306
408
|
} else if (direction === MovementDirection.Open || direction === MovementDirection.Close) {
|
|
307
409
|
// KNX DPT 1.008: 0 = up/open, 1 = down/close
|
|
308
|
-
onCommand({ deviceId: def.id, fn: 'updown', value: direction === MovementDirection.Close })
|
|
410
|
+
onCommand({ deviceId: def.id, fn: 'updown', value: direction === MovementDirection.Close, matterDiagnostic })
|
|
309
411
|
}
|
|
310
412
|
} catch (error) { /* empty */ }
|
|
311
413
|
}
|
|
312
414
|
|
|
313
415
|
async handleStopMovement () {
|
|
314
416
|
try {
|
|
315
|
-
onCommand({
|
|
417
|
+
onCommand({
|
|
418
|
+
deviceId: def.id,
|
|
419
|
+
fn: 'stop',
|
|
420
|
+
value: true,
|
|
421
|
+
matterDiagnostic: { handler: 'handleStopMovement' }
|
|
422
|
+
})
|
|
316
423
|
} catch (error) { /* empty */ }
|
|
317
424
|
return super.handleStopMovement()
|
|
318
425
|
}
|
|
@@ -353,7 +460,7 @@ function createBridgedEndpoint (def, serialPrefix, onCommand) {
|
|
|
353
460
|
currentX: Math.round(0.31 * 65536),
|
|
354
461
|
currentY: Math.round(0.33 * 65536)
|
|
355
462
|
}
|
|
356
|
-
endpoint = new Endpoint(ExtendedColorLightDevice.with(ColorControlServer.with('HueSaturation', 'Xy'), BridgedDeviceBasicInformationServer), initialState)
|
|
463
|
+
endpoint = new Endpoint(ExtendedColorLightDevice.with(RawOnOffServer, RawLevelControlServer, ColorControlServer.with('HueSaturation', 'Xy'), BridgedDeviceBasicInformationServer), initialState)
|
|
357
464
|
attachColorTracker(endpoint, def, onCommand)
|
|
358
465
|
} else if (def.type === 'colortemperaturelight') {
|
|
359
466
|
initialState.colorControl = {
|
|
@@ -365,7 +472,7 @@ function createBridgedEndpoint (def, serialPrefix, onCommand) {
|
|
|
365
472
|
startUpColorTemperatureMireds: 250,
|
|
366
473
|
coupleColorTempToLevelMinMireds: 153
|
|
367
474
|
}
|
|
368
|
-
endpoint = new Endpoint(ColorTemperatureLightDevice.with(ColorControlServer.with('ColorTemperature'), BridgedDeviceBasicInformationServer), initialState)
|
|
475
|
+
endpoint = new Endpoint(ColorTemperatureLightDevice.with(RawOnOffServer, RawLevelControlServer, ColorControlServer.with('ColorTemperature'), BridgedDeviceBasicInformationServer), initialState)
|
|
369
476
|
endpoint.events.colorControl.colorTemperatureMireds$Changed.on((value, oldValue, context) => {
|
|
370
477
|
try {
|
|
371
478
|
if (context?.offline === true) return
|
|
@@ -482,27 +589,10 @@ function createBridgedEndpoint (def, serialPrefix, onCommand) {
|
|
|
482
589
|
} catch (error) { /* empty */ }
|
|
483
590
|
})
|
|
484
591
|
} else {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
// for state changes we apply ourselves via endpoint.set(), which must NOT loop back to KNX).
|
|
490
|
-
if (typeInfo.commandFunctions.includes('onoff')) {
|
|
491
|
-
endpoint.events.onOff.onOff$Changed.on((value, oldValue, context) => {
|
|
492
|
-
try {
|
|
493
|
-
if (context?.offline === true) return
|
|
494
|
-
onCommand({ deviceId: def.id, fn: 'onoff', value: value === true })
|
|
495
|
-
} catch (error) { /* empty */ }
|
|
496
|
-
})
|
|
497
|
-
}
|
|
498
|
-
if (typeInfo.commandFunctions.includes('level')) {
|
|
499
|
-
endpoint.events.levelControl.currentLevel$Changed.on((value, oldValue, context) => {
|
|
500
|
-
try {
|
|
501
|
-
if (context?.offline === true) return
|
|
502
|
-
if (value === null || value === undefined) return
|
|
503
|
-
onCommand({ deviceId: def.id, fn: 'level', value: clamp(Math.round(Number(value) * 100 / 254), 0, 100) })
|
|
504
|
-
} catch (error) { /* empty */ }
|
|
505
|
-
})
|
|
592
|
+
let device = typeInfo.device
|
|
593
|
+
if (typeInfo.commandFunctions.includes('onoff')) device = device.with(RawOnOffServer)
|
|
594
|
+
if (typeInfo.commandFunctions.includes('level')) device = device.with(RawLevelControlServer)
|
|
595
|
+
endpoint = new Endpoint(device.with(BridgedDeviceBasicInformationServer), initialState)
|
|
506
596
|
}
|
|
507
597
|
|
|
508
598
|
return endpoint
|
|
@@ -169,7 +169,9 @@ class classMatterBridge extends EventEmitter {
|
|
|
169
169
|
const newDef = newById.get(id)
|
|
170
170
|
const oldDef = this.deviceDefs.get(id)
|
|
171
171
|
const needsRecreate = newDef !== undefined && oldDef !== undefined &&
|
|
172
|
-
(newDef.type !== oldDef.type ||
|
|
172
|
+
(newDef.type !== oldDef.type ||
|
|
173
|
+
(newDef.invertPosition === true) !== (oldDef.invertPosition === true) ||
|
|
174
|
+
(newDef.coverExposeAsDimmableLight === true) !== (oldDef.coverExposeAsDimmableLight === true))
|
|
173
175
|
if (newDef === undefined || needsRecreate) {
|
|
174
176
|
await this.removeDeviceEndpoint(id)
|
|
175
177
|
}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"engines": {
|
|
4
4
|
"node": ">=20.18.1"
|
|
5
5
|
},
|
|
6
|
-
"version": "6.0.
|
|
6
|
+
"version": "6.0.3",
|
|
7
7
|
"description": "Control your KNX and KNX Secure intallation via Node-Red! A bunch of KNX nodes, with integrated Philips HUE control, ETS group address importer, KNX AI for diagnosticsand KNX routing between interfaces. Easy to use and highly configurable.",
|
|
8
8
|
"files": [
|
|
9
9
|
"nodes/",
|