node-red-contrib-knx-ultimate 6.0.2 → 6.0.4
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 +8 -0
- package/nodes/knxUltimateMatterBridge.html +101 -75
- package/nodes/knxUltimateMatterBridge.js +2 -0
- package/nodes/locales/de/knxUltimateMatterBridge.json +10 -4
- package/nodes/locales/en/knxUltimateMatterBridge.json +10 -4
- package/nodes/locales/es/knxUltimateMatterBridge.json +10 -4
- package/nodes/locales/fr/knxUltimateMatterBridge.json +10 -4
- package/nodes/locales/it/knxUltimateMatterBridge.json +10 -4
- package/nodes/locales/zh-CN/knxUltimateMatterBridge.json +10 -4
- package/nodes/utils/matterBridgeDeviceFactory.mjs +52 -1
- package/nodes/utils/matterBridgeEngine.mjs +3 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
# CHANGELOG
|
|
8
8
|
|
|
9
|
+
**Version 6.0.4** - July 2026<br/>
|
|
10
|
+
|
|
11
|
+
- **Expose KNX to Matter (BETA) — clearer editor layout**: reorganized the device editor into dedicated **KNX mappings** and **Advanced options** tabs, following the established KNXUltimate node UI. Type-specific compatibility settings are easier to find, and checkbox labels now include icons with the checkbox positioned after the text. All saved fields and runtime behavior remain unchanged.<br/>
|
|
12
|
+
|
|
13
|
+
**Version 6.0.3** - July 2026<br/>
|
|
14
|
+
|
|
15
|
+
- **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/>
|
|
16
|
+
|
|
9
17
|
**Version 6.0.2** - July 2026<br/>
|
|
10
18
|
|
|
11
19
|
- **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/>
|
|
@@ -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' },
|
|
@@ -104,6 +105,9 @@
|
|
|
104
105
|
try { RED.sidebar.show('help'); } catch (error) { /* empty */ }
|
|
105
106
|
const node = this;
|
|
106
107
|
|
|
108
|
+
// Use the same jQuery UI tab structure as the main KNXUltimate node.
|
|
109
|
+
$('#mb-tabs').tabs();
|
|
110
|
+
|
|
107
111
|
// Resolves the KNX gateway at query time: the selected one, or - as a
|
|
108
112
|
// fallback - the first knxUltimate-config available, so the GA list works
|
|
109
113
|
// even before the user picks the gateway on this device node.
|
|
@@ -142,7 +146,7 @@
|
|
|
142
146
|
// on one or more rows (same pattern used across the rest of the KNX-Ultimate nodes).
|
|
143
147
|
const $fields = $('#mb-ga-fields').css({ display: 'flex', flexWrap: 'wrap', alignItems: 'flex-end', gap: '10px' });
|
|
144
148
|
const $invertRow = $('#mb-invert-row');
|
|
145
|
-
const $advancedRows = $('#mb-
|
|
149
|
+
const $advancedRows = $('#mb-compatibility-options');
|
|
146
150
|
const $dimmerAdvancedRows = $('.mb-dimmer-advanced-row');
|
|
147
151
|
const $coverAdvancedRows = $('.mb-cover-advanced-row');
|
|
148
152
|
const gaInputs = {};
|
|
@@ -228,6 +232,7 @@
|
|
|
228
232
|
node._matterGaInputs = gaInputs;
|
|
229
233
|
|
|
230
234
|
$('#node-input-invertPosition').prop('checked', node.invertPosition === true);
|
|
235
|
+
$('#node-input-coverExposeAsDimmableLight').prop('checked', node.coverExposeAsDimmableLight === true);
|
|
231
236
|
$('#node-input-turnOnBehavior').val(node.turnOnBehavior || 'ignoreLevelAfterOn');
|
|
232
237
|
$('#node-input-ignoreLevelAfterOnMs').val(node.ignoreLevelAfterOnMs === undefined ? 800 : node.ignoreLevelAfterOnMs);
|
|
233
238
|
$('#node-input-coverUpdateMode').val(node.coverUpdateMode || 'optimistic');
|
|
@@ -241,6 +246,7 @@
|
|
|
241
246
|
const selected = $('#node-input-deviceType').val();
|
|
242
247
|
node.deviceType = selected;
|
|
243
248
|
node.invertPosition = $('#node-input-invertPosition').is(':checked');
|
|
249
|
+
node.coverExposeAsDimmableLight = selected === 'windowcovering' && $('#node-input-coverExposeAsDimmableLight').is(':checked');
|
|
244
250
|
const active = TYPE_FIELDS[selected] || [];
|
|
245
251
|
const gaInputs = node._matterGaInputs || {};
|
|
246
252
|
// Clear every GA/DPT, then write only the ones of the selected type, reading
|
|
@@ -308,82 +314,101 @@
|
|
|
308
314
|
<select id="node-input-deviceType" style="width:260px;"></select>
|
|
309
315
|
</div>
|
|
310
316
|
|
|
311
|
-
<div
|
|
312
|
-
<
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
<div class="form-row mb-dimmer-advanced-row" style="display:none; margin-top:8px;">
|
|
333
|
-
<label for="node-input-turnOnBehavior" style="width:260px;">
|
|
334
|
-
<i class="fa fa-lightbulb-o"></i> <span data-i18n="knxUltimateMatterBridge.advanced.turn_on_behavior"></span>
|
|
335
|
-
</label>
|
|
336
|
-
<select id="node-input-turnOnBehavior" style="width:280px;">
|
|
337
|
-
<option value="ignoreLevelAfterOn" data-i18n="knxUltimateMatterBridge.advanced.turn_on_ignore_level"></option>
|
|
338
|
-
<option value="forwardAll" data-i18n="knxUltimateMatterBridge.advanced.turn_on_forward_all"></option>
|
|
339
|
-
</select>
|
|
340
|
-
</div>
|
|
341
|
-
|
|
342
|
-
<div class="form-row mb-dimmer-advanced-row" style="display:none;">
|
|
343
|
-
<label for="node-input-ignoreLevelAfterOnMs" style="width:260px;">
|
|
344
|
-
<i class="fa fa-clock-o"></i> <span data-i18n="knxUltimateMatterBridge.advanced.ignore_level_ms"></span>
|
|
345
|
-
</label>
|
|
346
|
-
<input type="number" id="node-input-ignoreLevelAfterOnMs" min="0" step="100" style="width:120px;">
|
|
347
|
-
</div>
|
|
348
|
-
|
|
349
|
-
<div class="form-row mb-cover-advanced-row" style="display:none; margin-top:8px;">
|
|
350
|
-
<label for="node-input-coverUpdateMode" style="width:260px;">
|
|
351
|
-
<i class="fa fa-window-maximize"></i> <span data-i18n="knxUltimateMatterBridge.advanced.cover_update_mode"></span>
|
|
352
|
-
</label>
|
|
353
|
-
<select id="node-input-coverUpdateMode" style="width:280px;">
|
|
354
|
-
<option value="optimistic" data-i18n="knxUltimateMatterBridge.advanced.cover_optimistic"></option>
|
|
355
|
-
<option value="waitStatus" data-i18n="knxUltimateMatterBridge.advanced.cover_wait_status"></option>
|
|
356
|
-
</select>
|
|
317
|
+
<div id="mb-tabs" style="margin-top:12px;">
|
|
318
|
+
<ul>
|
|
319
|
+
<li><a href="#mb-tab-mappings"><i class="fa fa-exchange"></i> <span data-i18n="knxUltimateMatterBridge.tabs.mappings"></span></a></li>
|
|
320
|
+
<li><a href="#mb-tab-advanced"><i class="fa fa-sliders"></i> <span data-i18n="knxUltimateMatterBridge.tabs.advanced"></span></a></li>
|
|
321
|
+
</ul>
|
|
322
|
+
|
|
323
|
+
<div id="mb-tab-mappings" style="padding-top:12px;">
|
|
324
|
+
<div class="form-tips" style="display:block; width:100%; max-width:none; box-sizing:border-box; margin-bottom:12px; border-left:4px solid #d9822b;">
|
|
325
|
+
<i class="fa fa-triangle-exclamation"></i>
|
|
326
|
+
<span data-i18n="knxUltimateMatterBridge.type_change_warning"></span>
|
|
327
|
+
</div>
|
|
328
|
+
|
|
329
|
+
<div id="mb-ga-fields"></div>
|
|
330
|
+
|
|
331
|
+
<div class="form-row" id="mb-invert-row" style="display:none;">
|
|
332
|
+
<label for="node-input-invertPosition" style="width:auto;">
|
|
333
|
+
<i class="fa fa-exchange"></i>
|
|
334
|
+
<span data-i18n="knxUltimateMatterBridge.fields.invert"></span>
|
|
335
|
+
<input type="checkbox" id="node-input-invertPosition" style="width:auto; display:inline-block; vertical-align:middle; margin-left:6px;">
|
|
336
|
+
</label>
|
|
337
|
+
</div>
|
|
357
338
|
</div>
|
|
358
339
|
|
|
359
|
-
<div
|
|
360
|
-
<
|
|
361
|
-
<
|
|
362
|
-
|
|
363
|
-
|
|
340
|
+
<div id="mb-tab-advanced" style="padding-top:12px;">
|
|
341
|
+
<div id="mb-compatibility-options" style="display:none;">
|
|
342
|
+
<div class="form-tips" style="display:block; width:100%; max-width:none; box-sizing:border-box; margin-bottom:12px; border-left:4px solid #4b8cc4;">
|
|
343
|
+
<i class="fa fa-info-circle"></i>
|
|
344
|
+
<b data-i18n="knxUltimateMatterBridge.advanced.title"></b>
|
|
345
|
+
<span data-i18n="knxUltimateMatterBridge.advanced.help"></span>
|
|
346
|
+
</div>
|
|
347
|
+
|
|
348
|
+
<div class="form-row mb-dimmer-advanced-row" style="display:none; margin-top:8px;">
|
|
349
|
+
<label for="node-input-turnOnBehavior" style="width:260px;">
|
|
350
|
+
<i class="fa fa-lightbulb-o"></i> <span data-i18n="knxUltimateMatterBridge.advanced.turn_on_behavior"></span>
|
|
351
|
+
</label>
|
|
352
|
+
<select id="node-input-turnOnBehavior" style="width:280px;">
|
|
353
|
+
<option value="ignoreLevelAfterOn" data-i18n="knxUltimateMatterBridge.advanced.turn_on_ignore_level"></option>
|
|
354
|
+
<option value="forwardAll" data-i18n="knxUltimateMatterBridge.advanced.turn_on_forward_all"></option>
|
|
355
|
+
</select>
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
<div class="form-row mb-dimmer-advanced-row" style="display:none;">
|
|
359
|
+
<label for="node-input-ignoreLevelAfterOnMs" style="width:260px;">
|
|
360
|
+
<i class="fa fa-clock-o"></i> <span data-i18n="knxUltimateMatterBridge.advanced.ignore_level_ms"></span>
|
|
361
|
+
</label>
|
|
362
|
+
<input type="number" id="node-input-ignoreLevelAfterOnMs" min="0" step="100" style="width:120px;">
|
|
363
|
+
</div>
|
|
364
|
+
|
|
365
|
+
<div class="form-row mb-cover-advanced-row" style="display:none; margin-top:8px;">
|
|
366
|
+
<label for="node-input-coverExposeAsDimmableLight" style="width:auto;">
|
|
367
|
+
<i class="fa fa-lightbulb-o"></i>
|
|
368
|
+
<span data-i18n="knxUltimateMatterBridge.advanced.cover_expose_as_light"></span>
|
|
369
|
+
<input type="checkbox" id="node-input-coverExposeAsDimmableLight" style="width:auto; display:inline-block; vertical-align:middle; margin-left:6px;">
|
|
370
|
+
</label>
|
|
371
|
+
<div class="form-tips" style="margin-top:5px;" data-i18n="knxUltimateMatterBridge.advanced.cover_expose_as_light_help"></div>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
<div class="form-row mb-cover-advanced-row" style="display:none; margin-top:8px;">
|
|
375
|
+
<label for="node-input-coverUpdateMode" style="width:260px;">
|
|
376
|
+
<i class="fa fa-window-maximize"></i> <span data-i18n="knxUltimateMatterBridge.advanced.cover_update_mode"></span>
|
|
377
|
+
</label>
|
|
378
|
+
<select id="node-input-coverUpdateMode" style="width:280px;">
|
|
379
|
+
<option value="optimistic" data-i18n="knxUltimateMatterBridge.advanced.cover_optimistic"></option>
|
|
380
|
+
<option value="waitStatus" data-i18n="knxUltimateMatterBridge.advanced.cover_wait_status"></option>
|
|
381
|
+
</select>
|
|
382
|
+
</div>
|
|
383
|
+
|
|
384
|
+
<div class="form-row mb-cover-advanced-row" style="display:none;">
|
|
385
|
+
<label for="node-input-coverStatusTimeoutMs" style="width:260px;">
|
|
386
|
+
<i class="fa fa-clock-o"></i> <span data-i18n="knxUltimateMatterBridge.advanced.cover_status_timeout"></span>
|
|
387
|
+
</label>
|
|
388
|
+
<input type="number" id="node-input-coverStatusTimeoutMs" min="0" step="500" style="width:120px;">
|
|
389
|
+
</div>
|
|
390
|
+
</div>
|
|
391
|
+
|
|
392
|
+
<div class="form-row">
|
|
393
|
+
<label for="node-input-readStatusAtStartup" style="width:260px;">
|
|
394
|
+
<i class="fa fa-question-circle"></i> <span data-i18n="knxUltimateMatterBridge.read_status_startup"></span>
|
|
395
|
+
</label>
|
|
396
|
+
<select id="node-input-readStatusAtStartup" style="width:120px;">
|
|
397
|
+
<option value="yes" data-i18n="knxUltimateMatterBridge.opt_yes"></option>
|
|
398
|
+
<option value="no" data-i18n="knxUltimateMatterBridge.opt_no"></option>
|
|
399
|
+
</select>
|
|
400
|
+
</div>
|
|
401
|
+
|
|
402
|
+
<div class="form-row">
|
|
403
|
+
<label for="node-input-enableNodePINS" style="width:260px;">
|
|
404
|
+
<i class="fa fa-code"></i> <span data-i18n="knxUltimateMatterBridge.node_pins"></span>
|
|
405
|
+
</label>
|
|
406
|
+
<select id="node-input-enableNodePINS" style="width:220px;">
|
|
407
|
+
<option value="no" data-i18n="knxUltimateMatterBridge.node_pins_hide"></option>
|
|
408
|
+
<option value="yes" data-i18n="knxUltimateMatterBridge.node_pins_show"></option>
|
|
409
|
+
</select>
|
|
410
|
+
</div>
|
|
364
411
|
</div>
|
|
365
|
-
</details>
|
|
366
|
-
|
|
367
|
-
<hr/>
|
|
368
|
-
|
|
369
|
-
<div class="form-row">
|
|
370
|
-
<label for="node-input-readStatusAtStartup" style="width:260px;">
|
|
371
|
-
<i class="fa fa-question-circle"></i> <span data-i18n="knxUltimateMatterBridge.read_status_startup"></span>
|
|
372
|
-
</label>
|
|
373
|
-
<select id="node-input-readStatusAtStartup" style="width:120px;">
|
|
374
|
-
<option value="yes" data-i18n="knxUltimateMatterBridge.opt_yes"></option>
|
|
375
|
-
<option value="no" data-i18n="knxUltimateMatterBridge.opt_no"></option>
|
|
376
|
-
</select>
|
|
377
|
-
</div>
|
|
378
|
-
|
|
379
|
-
<div class="form-row">
|
|
380
|
-
<label for="node-input-enableNodePINS" style="width:260px;">
|
|
381
|
-
<i class="fa fa-code"></i> <span data-i18n="knxUltimateMatterBridge.node_pins"></span>
|
|
382
|
-
</label>
|
|
383
|
-
<select id="node-input-enableNodePINS" style="width:220px;">
|
|
384
|
-
<option value="no" data-i18n="knxUltimateMatterBridge.node_pins_hide"></option>
|
|
385
|
-
<option value="yes" data-i18n="knxUltimateMatterBridge.node_pins_show"></option>
|
|
386
|
-
</select>
|
|
387
412
|
</div>
|
|
388
413
|
|
|
389
414
|
<br/>
|
|
@@ -406,6 +431,7 @@ The advanced compatibility options are intentionally type-specific:
|
|
|
406
431
|
|
|
407
432
|
- dimmable devices can ignore the brightness command that some controllers send immediately after an On command
|
|
408
433
|
- covers can optimistically update the Matter position after a command, then correct it when the KNX status GA reports the real position
|
|
434
|
+
- 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
435
|
|
|
410
436
|
The **KNX gateway is optional**: without it, enable the node PINs and drive the device from the flow.
|
|
411
437
|
|
|
@@ -413,4 +439,4 @@ If you enable the **node PINs**:
|
|
|
413
439
|
|
|
414
440
|
- input: `msg.payload = { function: "onoff", value: true }` updates the Matter state without the KNX bus
|
|
415
441
|
- output: every command from a Matter controller is forwarded as `msg` (topic = device name, payload = value, `msg.matter` = raw command)
|
|
416
|
-
</script>
|
|
442
|
+
</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
|
|
|
@@ -17,18 +17,24 @@
|
|
|
17
17
|
"name_placeholder": "z.B. Küchenlicht",
|
|
18
18
|
"invert": "Position % invertieren"
|
|
19
19
|
},
|
|
20
|
-
|
|
20
|
+
"advanced": {
|
|
21
21
|
"title": "Erweiterte Kompatibilität",
|
|
22
22
|
"help": " Verwende diese Optionen nur, wenn ein Matter-Controller oder ein KNX-Aktor eine besondere Behandlung benötigt.",
|
|
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",
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
"cover_status_timeout": "Timeout für Positionsstatus (ms)"
|
|
33
|
+
},
|
|
34
|
+
"tabs": {
|
|
35
|
+
"mappings": "KNX-Zuordnungen",
|
|
36
|
+
"advanced": "Erweiterte Optionen"
|
|
37
|
+
},
|
|
32
38
|
"functions": {
|
|
33
39
|
"fn_onoff_cmd": "Ein/Aus Befehls-GA",
|
|
34
40
|
"fn_onoff_status": "Ein/Aus Status-GA",
|
|
@@ -17,18 +17,24 @@
|
|
|
17
17
|
"name_placeholder": "e.g. Kitchen light",
|
|
18
18
|
"invert": "Invert position %"
|
|
19
19
|
},
|
|
20
|
-
|
|
20
|
+
"advanced": {
|
|
21
21
|
"title": "Advanced compatibility",
|
|
22
22
|
"help": " Use these options only when a Matter controller or a KNX actuator needs special handling.",
|
|
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",
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
"cover_status_timeout": "Cover status timeout (ms)"
|
|
33
|
+
},
|
|
34
|
+
"tabs": {
|
|
35
|
+
"mappings": "KNX mappings",
|
|
36
|
+
"advanced": "Advanced options"
|
|
37
|
+
},
|
|
32
38
|
"functions": {
|
|
33
39
|
"fn_onoff_cmd": "On/Off command GA",
|
|
34
40
|
"fn_onoff_status": "On/Off status GA",
|
|
@@ -17,18 +17,24 @@
|
|
|
17
17
|
"name_placeholder": "p.ej. Luz cocina",
|
|
18
18
|
"invert": "Invertir posición %"
|
|
19
19
|
},
|
|
20
|
-
|
|
20
|
+
"advanced": {
|
|
21
21
|
"title": "Compatibilidad avanzada",
|
|
22
22
|
"help": " Usa estas opciones solo cuando un controlador Matter o un actuador KNX necesite un tratamiento especial.",
|
|
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",
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
"cover_status_timeout": "Timeout estado persiana (ms)"
|
|
33
|
+
},
|
|
34
|
+
"tabs": {
|
|
35
|
+
"mappings": "Asignaciones KNX",
|
|
36
|
+
"advanced": "Opciones avanzadas"
|
|
37
|
+
},
|
|
32
38
|
"functions": {
|
|
33
39
|
"fn_onoff_cmd": "GA comando On/Off",
|
|
34
40
|
"fn_onoff_status": "GA estado On/Off",
|
|
@@ -17,18 +17,24 @@
|
|
|
17
17
|
"name_placeholder": "ex. Lumière cuisine",
|
|
18
18
|
"invert": "Inverser la position %"
|
|
19
19
|
},
|
|
20
|
-
|
|
20
|
+
"advanced": {
|
|
21
21
|
"title": "Compatibilité avancée",
|
|
22
22
|
"help": " Utilisez ces options seulement lorsqu'un contrôleur Matter ou un actionneur KNX demande un traitement spécial.",
|
|
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",
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
"cover_status_timeout": "Timeout état volet (ms)"
|
|
33
|
+
},
|
|
34
|
+
"tabs": {
|
|
35
|
+
"mappings": "Mappages KNX",
|
|
36
|
+
"advanced": "Options avancées"
|
|
37
|
+
},
|
|
32
38
|
"functions": {
|
|
33
39
|
"fn_onoff_cmd": "GA commande On/Off",
|
|
34
40
|
"fn_onoff_status": "GA état On/Off",
|
|
@@ -17,18 +17,24 @@
|
|
|
17
17
|
"name_placeholder": "es. Luce cucina",
|
|
18
18
|
"invert": "Inverti posizione %"
|
|
19
19
|
},
|
|
20
|
-
|
|
20
|
+
"advanced": {
|
|
21
21
|
"title": "Compatibilità avanzata",
|
|
22
22
|
"help": " Usa queste opzioni solo quando un controller Matter o un attuatore KNX richiede una gestione speciale.",
|
|
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",
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
"cover_status_timeout": "Timeout stato tapparella (ms)"
|
|
33
|
+
},
|
|
34
|
+
"tabs": {
|
|
35
|
+
"mappings": "Mappature KNX",
|
|
36
|
+
"advanced": "Opzioni avanzate"
|
|
37
|
+
},
|
|
32
38
|
"functions": {
|
|
33
39
|
"fn_onoff_cmd": "GA comando On/Off",
|
|
34
40
|
"fn_onoff_status": "GA stato On/Off",
|
|
@@ -17,18 +17,24 @@
|
|
|
17
17
|
"name_placeholder": "例如 厨房灯",
|
|
18
18
|
"invert": "反转位置 %"
|
|
19
19
|
},
|
|
20
|
-
|
|
20
|
+
"advanced": {
|
|
21
21
|
"title": "高级兼容性",
|
|
22
22
|
"help": " 仅当 Matter 控制器或 KNX 执行器需要特殊处理时使用这些选项。",
|
|
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 状态",
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
"cover_status_timeout": "窗帘状态超时 (ms)"
|
|
33
|
+
},
|
|
34
|
+
"tabs": {
|
|
35
|
+
"mappings": "KNX 映射",
|
|
36
|
+
"advanced": "高级选项"
|
|
37
|
+
},
|
|
32
38
|
"functions": {
|
|
33
39
|
"fn_onoff_cmd": "开/关 命令 GA",
|
|
34
40
|
"fn_onoff_status": "开/关 状态 GA",
|
|
@@ -84,6 +84,15 @@ function knxValueToMatterPatch (def, fn, value) {
|
|
|
84
84
|
let percent = Number(value)
|
|
85
85
|
if (Number.isNaN(percent)) return undefined
|
|
86
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
|
+
}
|
|
87
96
|
return { windowCovering: { currentPositionLiftPercent100ths: clamp(Math.round(percent * 100), 0, 10000) } }
|
|
88
97
|
}
|
|
89
98
|
case 'rgb': {
|
|
@@ -292,7 +301,7 @@ function createBridgedEndpoint (def, serialPrefix, onCommand) {
|
|
|
292
301
|
// Although Matter permits an unknown (null) position, some controllers (including
|
|
293
302
|
// Alexa) then expose only Open/Close instead of percentage positioning. Start with
|
|
294
303
|
// a valid position; KNX or flow feedback replaces it as soon as it is available.
|
|
295
|
-
if (def.type === 'windowcovering') {
|
|
304
|
+
if (def.type === 'windowcovering' && def.coverExposeAsDimmableLight !== true) {
|
|
296
305
|
initialState.windowCovering = {
|
|
297
306
|
currentPositionLiftPercent100ths: 0,
|
|
298
307
|
targetPositionLiftPercent100ths: 0
|
|
@@ -331,6 +340,48 @@ function createBridgedEndpoint (def, serialPrefix, onCommand) {
|
|
|
331
340
|
}
|
|
332
341
|
}
|
|
333
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
|
+
|
|
334
385
|
if (def.type === 'windowcovering') {
|
|
335
386
|
// The WindowCoveringServer requires the movement logic: we forward it to the KNX bus.
|
|
336
387
|
// Position feedback comes back from the KNX status GA, so we do NOT call the default
|
|
@@ -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.4",
|
|
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/",
|