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 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-advanced-details');
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 class="form-tips" style="display:block; width:100%; max-width:none; box-sizing:border-box; margin-bottom:8px; border-left:4px solid #d9822b;">
312
- <i class="fa fa-triangle-exclamation"></i>
313
- <span data-i18n="knxUltimateMatterBridge.type_change_warning"></span>
314
- </div>
315
-
316
- <div id="mb-ga-fields"></div>
317
-
318
- <div class="form-row" id="mb-invert-row" style="display:none;">
319
- <label for="node-input-invertPosition" style="width:auto;">
320
- <input type="checkbox" id="node-input-invertPosition" style="width:auto; display:inline-block; vertical-align:middle;">
321
- <span data-i18n="knxUltimateMatterBridge.fields.invert"></span>
322
- </label>
323
- </div>
324
-
325
- <details id="mb-advanced-details" style="margin:10px 0 8px 0; display:none;">
326
- <summary style="cursor:pointer;">
327
- <i class="fa fa-sliders"></i>
328
- <b><span data-i18n="knxUltimateMatterBridge.advanced.title"></span></b>
329
- <span data-i18n="knxUltimateMatterBridge.advanced.help"></span>
330
- </summary>
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 class="form-row mb-cover-advanced-row" style="display:none;">
360
- <label for="node-input-coverStatusTimeoutMs" style="width:260px;">
361
- <i class="fa fa-clock-o"></i> <span data-i18n="knxUltimateMatterBridge.advanced.cover_status_timeout"></span>
362
- </label>
363
- <input type="number" id="node-input-coverStatusTimeoutMs" min="0" step="500" style="width:120px;">
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
- "advanced": {
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
- "ignore_level_ms": "Helligkeit nach Ein ignorieren (ms)",
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
- "cover_status_timeout": "Timeout für Positionsstatus (ms)"
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
- "advanced": {
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
- "ignore_level_ms": "Ignore brightness after On (ms)",
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
- "cover_status_timeout": "Cover status timeout (ms)"
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
- "advanced": {
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
- "ignore_level_ms": "Ignorar brillo tras On (ms)",
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
- "cover_status_timeout": "Timeout estado persiana (ms)"
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
- "advanced": {
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
- "ignore_level_ms": "Ignorer la luminosité après On (ms)",
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
- "cover_status_timeout": "Timeout état volet (ms)"
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
- "advanced": {
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
- "ignore_level_ms": "Ignora luminosità dopo On (ms)",
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
- "cover_status_timeout": "Timeout stato tapparella (ms)"
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
- "advanced": {
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
- "ignore_level_ms": "On 后忽略亮度时间 (ms)",
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
- "cover_status_timeout": "窗帘状态超时 (ms)"
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 || (newDef.invertPosition === true) !== (oldDef.invertPosition === true))
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.2",
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/",