node-red-contrib-knx-ultimate 6.0.3 → 6.0.5

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,22 @@
6
6
 
7
7
  # CHANGELOG
8
8
 
9
+ **Version 6.0.5** - July 2026<br/>
10
+
11
+ - **Expose KNX to Matter (BETA) — dedup for duplicate Alexa light commands**: same Alexa quirk as the covers below, on lights: a single "turn on" / "set to 50%" voice request could arrive as up to two *identical* cluster commands (two `On`, or `MoveToLevelWithOnOff` + `MoveToLevel` with the same level), making the node emit duplicate raw msgs on `OnOffLightDevice`/`DimmableLightDevice` ([discussion #516](https://github.com/Supergiovane/node-red-contrib-knx-ultimate/discussions/516)). A short leading-edge dedup now forwards the first command immediately (no added latency — lights react instantly) and drops only a command identical to the one just emitted for the same function within the window; any change of value (a real On→Off, or a different brightness) always passes through at once, so genuine commands are never swallowed. The window is exposed in the node's **Advanced** tab as *Alexa duplicate-command window* (default 1.2s, **0 = disabled/raw**), the same approach the Home Assistant Matter Hub adopted for this Alexa behavior. The complementary *ignore brightness after On* option (already present) covers Alexa's other habit of sending a brightness right after On.<br/>
12
+ - **Expose KNX to Matter (BETA) — Home Assistant Matter Hub-style cover compatibility**: `WindowCovering` now exposes **Swap Open / Close** and **Cover slider debounce** in the Advanced tab. Swap reverses the binary KNX Up/Down command and percentage direction symmetrically for Alexa installations that interpret them backwards. Debounce collapses rapid `GoToLiftPercentage` targets into the last KNX command; `0` uses the adaptive Home Assistant Matter Hub windows (400ms first command / 150ms subsequent commands), while `1`–`5000` selects a fixed per-cover window. `StopMotion` remains immediate and cancels every pending movement. Help HTML and Matter Bridge docs were updated in EN/IT/DE/FR/ES/zh-CN.<br/>
13
+ - **Expose KNX to Matter (BETA) — cover "arrived" safety net**: in *Wait for KNX status* mode, if no KNX status telegram confirms the exact requested position within a generous window (45s), the node now falls back to reflecting the requested position to Matter anyway. Without this, a real actuator that never reports back the *exact* requested percentage (positioning tolerance/drift) could leave the Matter device reporting "still moving" forever, since matter.js only clears `OperationalStatus` on an exact match.<br/>
14
+ - **Expose KNX to Matter (BETA) — raw color commands**: RGB and tunable-white lights now capture color commands at the same "raw" command boundary already used for On/Off, dimming and covers - a repeated color command (e.g. the same color sent twice) is forwarded on the node's optional output PIN every time, not just on the first. Only possible for ColorControl because it is command-based (`MoveToHue`, `MoveToSaturation`, `MoveToHueAndSaturation`, `MoveToColor`, `MoveToColorTemperature`); Thermostat setpoints and Fan speed remain attribute-write only in the Matter spec, so they still report only on actual value changes - there is no lower-level hook matter.js exposes for those.<br/>
15
+ - **Expose KNX to Matter (BETA) — fixed WindowCovering percentage commands**: `GoToLiftPercentage` commands (e.g. "Alexa, set the cover to 50%") were being misread as plain Open/Close commands, discarding the requested percentage. matter.js always resolves the command's `direction` parameter to a concrete Open/Close before calling our handler — it is not a usable signal — while `targetPercent100ths` is always populated. The node now classifies purely on the target position (0/10000 → Up/Down GA, anything else → Position GA), fixing intermediate cover positions from any Matter controller. Thanks to [@till69](https://github.com/till69) for tracing this down in [discussion #516](https://github.com/Supergiovane/node-red-contrib-knx-ultimate/discussions/516).<br/>
16
+ - **Expose KNX to Matter (BETA) — removed the "Expose as dimmable light" Alexa workaround**: now that the actual cause of Alexa's broken cover percentages is fixed (see above), the v6.0.3 workaround that exposed covers as dimmable lights is no longer needed and has been removed, including its editor checkbox. **Breaking for the small number of BETA users who enabled it**: existing flows with the option turned on simply revert to a normal Matter Window Covering device on next deploy - you may need to remove/re-add the accessory in your controller's app.<br/>
17
+ - **Expose KNX to Matter (BETA) — heating/cooling thermostats**: the bridged Thermostat can now expose a **Cooling setpoint** in addition to the existing heating one. Add a *Cooling setpoint command/status GA* on the device to automatically upgrade the Matter endpoint to a dual-mode (Heat/Cool) thermostat; leaving it empty keeps the previous heating-only behavior unchanged. Mode (Heat/Cool) switches from the controller are accepted but not yet mirrored back to KNX.<br/>
18
+ - **Expose KNX to Matter (BETA) — stable per-device identity**: bridged devices now also set the optional `UniqueID` of `BridgedDeviceBasicInformation` (matching the existing serial number), improving cross-reset device identity for controllers that rely on it (e.g. Apple Home) alongside the endpoint number.<br/>
19
+ - **Matter Bridge/Controller — safer shared storage initialization**: the `storage.path` write on the process-wide matter.js `Environment.default`, followed by the `ServerNode`/`CommissioningController` creation, is now serialized through a small FIFO lock (`matterEnvironmentLock.mjs`) shared by both engines, removing a theoretical race between a concurrently starting Matter Bridge and Matter Controller.<br/>
20
+
21
+ **Version 6.0.4** - July 2026<br/>
22
+
23
+ - **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/>
24
+
9
25
  **Version 6.0.3** - July 2026<br/>
10
26
 
11
27
  - **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/>
@@ -50,7 +50,9 @@
50
50
  thermostat: [
51
51
  { key: 'CurrentTemp', labelKey: 'fn_currenttemp', dptPrefix: '9.', fallbackDpt: '9.001' },
52
52
  { key: 'Setpoint', labelKey: 'fn_setpoint_cmd', dptPrefix: '9.', fallbackDpt: '9.001' },
53
- { key: 'SetpointStatus', labelKey: 'fn_setpoint_status', dptPrefix: '9.', fallbackDpt: '9.001' }
53
+ { key: 'SetpointStatus', labelKey: 'fn_setpoint_status', dptPrefix: '9.', fallbackDpt: '9.001' },
54
+ { key: 'CoolingSetpoint', labelKey: 'fn_coolingsetpoint_cmd', dptPrefix: '9.', fallbackDpt: '9.001' },
55
+ { key: 'CoolingSetpointStatus', labelKey: 'fn_coolingsetpoint_status', dptPrefix: '9.', fallbackDpt: '9.001' }
54
56
  ],
55
57
  smokecoalarm: [
56
58
  { key: 'Smoke', labelKey: 'fn_smoke_ga', dptPrefix: '1.', fallbackDpt: '1.005' },
@@ -75,7 +77,9 @@
75
77
  name: { value: '' },
76
78
  deviceType: { value: 'onofflight' },
77
79
  invertPosition: { value: false },
78
- coverExposeAsDimmableLight: { value: false },
80
+ coverSwapOpenClose: { value: false },
81
+ coverSliderDebounceMs: { value: 0 },
82
+ commandDedupMs: { value: 1200 },
79
83
  turnOnBehavior: { value: 'ignoreLevelAfterOn' },
80
84
  ignoreLevelAfterOnMs: { value: 800 },
81
85
  coverUpdateMode: { value: 'optimistic' },
@@ -105,6 +109,9 @@
105
109
  try { RED.sidebar.show('help'); } catch (error) { /* empty */ }
106
110
  const node = this;
107
111
 
112
+ // Use the same jQuery UI tab structure as the main KNXUltimate node.
113
+ $('#mb-tabs').tabs();
114
+
108
115
  // Resolves the KNX gateway at query time: the selected one, or - as a
109
116
  // fallback - the first knxUltimate-config available, so the GA list works
110
117
  // even before the user picks the gateway on this device node.
@@ -143,7 +150,8 @@
143
150
  // on one or more rows (same pattern used across the rest of the KNX-Ultimate nodes).
144
151
  const $fields = $('#mb-ga-fields').css({ display: 'flex', flexWrap: 'wrap', alignItems: 'flex-end', gap: '10px' });
145
152
  const $invertRow = $('#mb-invert-row');
146
- const $advancedRows = $('#mb-advanced-details');
153
+ const $advancedRows = $('#mb-compatibility-options');
154
+ const $commandAdvancedRows = $('.mb-command-advanced-row');
147
155
  const $dimmerAdvancedRows = $('.mb-dimmer-advanced-row');
148
156
  const $coverAdvancedRows = $('.mb-cover-advanced-row');
149
157
  const gaInputs = {};
@@ -217,7 +225,11 @@
217
225
  $invertRow.toggle(selected === 'windowcovering');
218
226
  const hasLevel = selected === 'dimmablelight' || selected === 'rgblight' || selected === 'colortemperaturelight';
219
227
  const isCover = selected === 'windowcovering';
220
- $advancedRows.toggle(hasLevel || isCover);
228
+ // The Alexa duplicate-command dedup applies to any device that emits On/Off or
229
+ // level commands (plain on/off lights and plugs included), not just dimmers.
230
+ const hasCommand = selected === 'onofflight' || selected === 'onoffplug' || hasLevel;
231
+ $advancedRows.toggle(hasCommand || isCover);
232
+ $commandAdvancedRows.toggle(hasCommand);
221
233
  $dimmerAdvancedRows.toggle(hasLevel);
222
234
  $coverAdvancedRows.toggle(isCover);
223
235
  }
@@ -229,7 +241,9 @@
229
241
  node._matterGaInputs = gaInputs;
230
242
 
231
243
  $('#node-input-invertPosition').prop('checked', node.invertPosition === true);
232
- $('#node-input-coverExposeAsDimmableLight').prop('checked', node.coverExposeAsDimmableLight === true);
244
+ $('#node-input-coverSwapOpenClose').prop('checked', node.coverSwapOpenClose === true);
245
+ $('#node-input-coverSliderDebounceMs').val(node.coverSliderDebounceMs === undefined ? 0 : node.coverSliderDebounceMs);
246
+ $('#node-input-commandDedupMs').val(node.commandDedupMs === undefined ? 1200 : node.commandDedupMs);
233
247
  $('#node-input-turnOnBehavior').val(node.turnOnBehavior || 'ignoreLevelAfterOn');
234
248
  $('#node-input-ignoreLevelAfterOnMs').val(node.ignoreLevelAfterOnMs === undefined ? 800 : node.ignoreLevelAfterOnMs);
235
249
  $('#node-input-coverUpdateMode').val(node.coverUpdateMode || 'optimistic');
@@ -243,7 +257,7 @@
243
257
  const selected = $('#node-input-deviceType').val();
244
258
  node.deviceType = selected;
245
259
  node.invertPosition = $('#node-input-invertPosition').is(':checked');
246
- node.coverExposeAsDimmableLight = selected === 'windowcovering' && $('#node-input-coverExposeAsDimmableLight').is(':checked');
260
+ node.coverSwapOpenClose = $('#node-input-coverSwapOpenClose').is(':checked');
247
261
  const active = TYPE_FIELDS[selected] || [];
248
262
  const gaInputs = node._matterGaInputs || {};
249
263
  // Clear every GA/DPT, then write only the ones of the selected type, reading
@@ -261,6 +275,11 @@
261
275
  }
262
276
  });
263
277
  delete node._matterGaInputs;
278
+ // Keep an explicit 0 (dedup disabled); treat only an empty/invalid field as the default.
279
+ const dedupRaw = $('#node-input-commandDedupMs').val();
280
+ this.commandDedupMs = (dedupRaw === '' || dedupRaw === null || isNaN(Number(dedupRaw))) ? 1200 : Math.max(0, Number(dedupRaw));
281
+ const coverDebounceRaw = $('#node-input-coverSliderDebounceMs').val();
282
+ this.coverSliderDebounceMs = (coverDebounceRaw === '' || coverDebounceRaw === null || isNaN(Number(coverDebounceRaw))) ? 0 : Math.min(5000, Math.max(0, Number(coverDebounceRaw)));
264
283
  this.turnOnBehavior = $('#node-input-turnOnBehavior').val() || 'ignoreLevelAfterOn';
265
284
  this.ignoreLevelAfterOnMs = Number($('#node-input-ignoreLevelAfterOnMs').val()) || 0;
266
285
  this.coverUpdateMode = $('#node-input-coverUpdateMode').val() || 'optimistic';
@@ -311,90 +330,107 @@
311
330
  <select id="node-input-deviceType" style="width:260px;"></select>
312
331
  </div>
313
332
 
314
- <div class="form-tips" style="display:block; width:100%; max-width:none; box-sizing:border-box; margin-bottom:8px; border-left:4px solid #d9822b;">
315
- <i class="fa fa-triangle-exclamation"></i>
316
- <span data-i18n="knxUltimateMatterBridge.type_change_warning"></span>
317
- </div>
318
-
319
- <div id="mb-ga-fields"></div>
320
-
321
- <div class="form-row" id="mb-invert-row" style="display:none;">
322
- <label for="node-input-invertPosition" style="width:auto;">
323
- <input type="checkbox" id="node-input-invertPosition" style="width:auto; display:inline-block; vertical-align:middle;">
324
- <span data-i18n="knxUltimateMatterBridge.fields.invert"></span>
325
- </label>
326
- </div>
327
-
328
- <details id="mb-advanced-details" style="margin:10px 0 8px 0; display:none;">
329
- <summary style="cursor:pointer;">
330
- <i class="fa fa-sliders"></i>
331
- <b><span data-i18n="knxUltimateMatterBridge.advanced.title"></span></b>
332
- <span data-i18n="knxUltimateMatterBridge.advanced.help"></span>
333
- </summary>
334
-
335
- <div class="form-row mb-dimmer-advanced-row" style="display:none; margin-top:8px;">
336
- <label for="node-input-turnOnBehavior" style="width:260px;">
337
- <i class="fa fa-lightbulb-o"></i> <span data-i18n="knxUltimateMatterBridge.advanced.turn_on_behavior"></span>
338
- </label>
339
- <select id="node-input-turnOnBehavior" style="width:280px;">
340
- <option value="ignoreLevelAfterOn" data-i18n="knxUltimateMatterBridge.advanced.turn_on_ignore_level"></option>
341
- <option value="forwardAll" data-i18n="knxUltimateMatterBridge.advanced.turn_on_forward_all"></option>
342
- </select>
343
- </div>
344
-
345
- <div class="form-row mb-dimmer-advanced-row" style="display:none;">
346
- <label for="node-input-ignoreLevelAfterOnMs" style="width:260px;">
347
- <i class="fa fa-clock-o"></i> <span data-i18n="knxUltimateMatterBridge.advanced.ignore_level_ms"></span>
348
- </label>
349
- <input type="number" id="node-input-ignoreLevelAfterOnMs" min="0" step="100" style="width:120px;">
350
- </div>
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>
333
+ <div id="mb-tabs" style="margin-top:12px;">
334
+ <ul>
335
+ <li><a href="#mb-tab-mappings"><i class="fa fa-exchange"></i> <span data-i18n="knxUltimateMatterBridge.tabs.mappings"></span></a></li>
336
+ <li><a href="#mb-tab-advanced"><i class="fa fa-sliders"></i> <span data-i18n="knxUltimateMatterBridge.tabs.advanced"></span></a></li>
337
+ </ul>
338
+
339
+ <div id="mb-tab-mappings" style="padding-top:12px;">
340
+ <div class="form-tips" style="display:block; width:100%; max-width:none; box-sizing:border-box; margin-bottom:12px; border-left:4px solid #d9822b;">
341
+ <i class="fa fa-triangle-exclamation"></i>
342
+ <span data-i18n="knxUltimateMatterBridge.type_change_warning"></span>
343
+ </div>
344
+
345
+ <div id="mb-ga-fields"></div>
346
+
347
+ <div class="form-row" id="mb-invert-row" style="display:none;">
348
+ <label for="node-input-invertPosition" style="width:auto;">
349
+ <i class="fa fa-exchange"></i>
350
+ <span data-i18n="knxUltimateMatterBridge.fields.invert"></span>
351
+ <input type="checkbox" id="node-input-invertPosition" style="width:auto; display:inline-block; vertical-align:middle; margin-left:6px;">
352
+ </label>
353
+ </div>
358
354
  </div>
359
355
 
360
- <div class="form-row mb-cover-advanced-row" style="display:none; margin-top:8px;">
361
- <label for="node-input-coverUpdateMode" style="width:260px;">
362
- <i class="fa fa-window-maximize"></i> <span data-i18n="knxUltimateMatterBridge.advanced.cover_update_mode"></span>
363
- </label>
364
- <select id="node-input-coverUpdateMode" style="width:280px;">
365
- <option value="optimistic" data-i18n="knxUltimateMatterBridge.advanced.cover_optimistic"></option>
366
- <option value="waitStatus" data-i18n="knxUltimateMatterBridge.advanced.cover_wait_status"></option>
367
- </select>
356
+ <div id="mb-tab-advanced" style="padding-top:12px;">
357
+ <div id="mb-compatibility-options" style="display:none;">
358
+ <div class="form-row mb-command-advanced-row" style="display:none; margin-top:8px;">
359
+ <label for="node-input-commandDedupMs" style="width:260px;">
360
+ <i class="fa fa-clone"></i> <span data-i18n="knxUltimateMatterBridge.advanced.alexa_dedup_ms"></span>
361
+ </label>
362
+ <input type="number" id="node-input-commandDedupMs" min="0" step="100" style="width:120px;">
363
+ </div>
364
+
365
+ <div class="form-row mb-dimmer-advanced-row" style="display:none; margin-top:8px;">
366
+ <label for="node-input-turnOnBehavior" style="width:260px;">
367
+ <i class="fa fa-lightbulb-o"></i> <span data-i18n="knxUltimateMatterBridge.advanced.turn_on_behavior"></span>
368
+ </label>
369
+ <select id="node-input-turnOnBehavior" style="width:280px;">
370
+ <option value="ignoreLevelAfterOn" data-i18n="knxUltimateMatterBridge.advanced.turn_on_ignore_level"></option>
371
+ <option value="forwardAll" data-i18n="knxUltimateMatterBridge.advanced.turn_on_forward_all"></option>
372
+ </select>
373
+ </div>
374
+
375
+ <div class="form-row mb-dimmer-advanced-row" style="display:none;">
376
+ <label for="node-input-ignoreLevelAfterOnMs" style="width:260px;">
377
+ <i class="fa fa-clock-o"></i> <span data-i18n="knxUltimateMatterBridge.advanced.ignore_level_ms"></span>
378
+ </label>
379
+ <input type="number" id="node-input-ignoreLevelAfterOnMs" min="0" step="100" style="width:120px;">
380
+ </div>
381
+
382
+ <div class="form-row mb-cover-advanced-row" style="display:none; margin-top:8px;">
383
+ <label for="node-input-coverSwapOpenClose" style="width:260px;">
384
+ <i class="fa fa-exchange"></i> <span data-i18n="knxUltimateMatterBridge.advanced.cover_swap_open_close"></span>
385
+ </label>
386
+ <input type="checkbox" id="node-input-coverSwapOpenClose" style="width:auto; vertical-align:middle;">
387
+ </div>
388
+
389
+ <div class="form-row mb-cover-advanced-row" style="display:none;">
390
+ <label for="node-input-coverSliderDebounceMs" style="width:260px;">
391
+ <i class="fa fa-clock-o"></i> <span data-i18n="knxUltimateMatterBridge.advanced.cover_slider_debounce"></span>
392
+ </label>
393
+ <input type="number" id="node-input-coverSliderDebounceMs" min="0" max="5000" step="50" style="width:120px;">
394
+ </div>
395
+
396
+ <div class="form-row mb-cover-advanced-row" style="display:none; margin-top:8px;">
397
+ <label for="node-input-coverUpdateMode" style="width:260px;">
398
+ <i class="fa fa-window-maximize"></i> <span data-i18n="knxUltimateMatterBridge.advanced.cover_update_mode"></span>
399
+ </label>
400
+ <select id="node-input-coverUpdateMode" style="width:280px;">
401
+ <option value="optimistic" data-i18n="knxUltimateMatterBridge.advanced.cover_optimistic"></option>
402
+ <option value="waitStatus" data-i18n="knxUltimateMatterBridge.advanced.cover_wait_status"></option>
403
+ </select>
404
+ </div>
405
+
406
+ <div class="form-row mb-cover-advanced-row" style="display:none;">
407
+ <label for="node-input-coverStatusTimeoutMs" style="width:260px;">
408
+ <i class="fa fa-clock-o"></i> <span data-i18n="knxUltimateMatterBridge.advanced.cover_status_timeout"></span>
409
+ </label>
410
+ <input type="number" id="node-input-coverStatusTimeoutMs" min="0" step="500" style="width:120px;">
411
+ </div>
412
+ </div>
413
+
414
+ <div class="form-row">
415
+ <label for="node-input-readStatusAtStartup" style="width:260px;">
416
+ <i class="fa fa-question-circle"></i> <span data-i18n="knxUltimateMatterBridge.read_status_startup"></span>
417
+ </label>
418
+ <select id="node-input-readStatusAtStartup" style="width:120px;">
419
+ <option value="yes" data-i18n="knxUltimateMatterBridge.opt_yes"></option>
420
+ <option value="no" data-i18n="knxUltimateMatterBridge.opt_no"></option>
421
+ </select>
422
+ </div>
423
+
424
+ <div class="form-row">
425
+ <label for="node-input-enableNodePINS" style="width:260px;">
426
+ <i class="fa fa-code"></i> <span data-i18n="knxUltimateMatterBridge.node_pins"></span>
427
+ </label>
428
+ <select id="node-input-enableNodePINS" style="width:220px;">
429
+ <option value="no" data-i18n="knxUltimateMatterBridge.node_pins_hide"></option>
430
+ <option value="yes" data-i18n="knxUltimateMatterBridge.node_pins_show"></option>
431
+ </select>
432
+ </div>
368
433
  </div>
369
-
370
- <div class="form-row mb-cover-advanced-row" style="display:none;">
371
- <label for="node-input-coverStatusTimeoutMs" style="width:260px;">
372
- <i class="fa fa-clock-o"></i> <span data-i18n="knxUltimateMatterBridge.advanced.cover_status_timeout"></span>
373
- </label>
374
- <input type="number" id="node-input-coverStatusTimeoutMs" min="0" step="500" style="width:120px;">
375
- </div>
376
- </details>
377
-
378
- <hr/>
379
-
380
- <div class="form-row">
381
- <label for="node-input-readStatusAtStartup" style="width:260px;">
382
- <i class="fa fa-question-circle"></i> <span data-i18n="knxUltimateMatterBridge.read_status_startup"></span>
383
- </label>
384
- <select id="node-input-readStatusAtStartup" style="width:120px;">
385
- <option value="yes" data-i18n="knxUltimateMatterBridge.opt_yes"></option>
386
- <option value="no" data-i18n="knxUltimateMatterBridge.opt_no"></option>
387
- </select>
388
- </div>
389
-
390
- <div class="form-row">
391
- <label for="node-input-enableNodePINS" style="width:260px;">
392
- <i class="fa fa-code"></i> <span data-i18n="knxUltimateMatterBridge.node_pins"></span>
393
- </label>
394
- <select id="node-input-enableNodePINS" style="width:220px;">
395
- <option value="no" data-i18n="knxUltimateMatterBridge.node_pins_hide"></option>
396
- <option value="yes" data-i18n="knxUltimateMatterBridge.node_pins_show"></option>
397
- </select>
398
434
  </div>
399
435
 
400
436
  <br/>
@@ -416,8 +452,9 @@ Point it to a **Matter Bridge** configuration node (the actual bridge, paired on
416
452
  The advanced compatibility options are intentionally type-specific:
417
453
 
418
454
  - dimmable devices can ignore the brightness command that some controllers send immediately after an On command
455
+ - covers can swap Open/Close (including the percentage direction) for controllers such as Alexa that interpret them in reverse
456
+ - cover slider debounce coalesces rapid intermediate targets before writing to KNX: `0` uses adaptive 400/150 ms windows, while `1`–`5000` forces a fixed window
419
457
  - 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
421
458
 
422
459
  The **KNX gateway is optional**: without it, enable the node PINs and drive the device from the flow.
423
460
 
@@ -51,7 +51,9 @@ module.exports = function (RED) {
51
51
  thermostat: [
52
52
  { fn: 'currenttemp', direction: 'status', ga: 'gaCurrentTemp', dpt: 'dptCurrentTemp', fallbackDpt: '9.001' },
53
53
  { fn: 'setpoint', direction: 'command', ga: 'gaSetpoint', dpt: 'dptSetpoint', fallbackDpt: '9.001' },
54
- { fn: 'setpoint', direction: 'status', ga: 'gaSetpointStatus', dpt: 'dptSetpointStatus', fallbackDpt: '9.001' }
54
+ { fn: 'setpoint', direction: 'status', ga: 'gaSetpointStatus', dpt: 'dptSetpointStatus', fallbackDpt: '9.001' },
55
+ { fn: 'coolingsetpoint', direction: 'command', ga: 'gaCoolingSetpoint', dpt: 'dptCoolingSetpoint', fallbackDpt: '9.001' },
56
+ { fn: 'coolingsetpoint', direction: 'status', ga: 'gaCoolingSetpointStatus', dpt: 'dptCoolingSetpointStatus', fallbackDpt: '9.001' }
55
57
  ],
56
58
  smokecoalarm: [
57
59
  { fn: 'smoke', direction: 'status', ga: 'gaSmoke', dpt: 'dptSmoke', fallbackDpt: '1.005' },
@@ -77,10 +79,17 @@ module.exports = function (RED) {
77
79
  // The Matter device id is the (stable) Node-RED node id, so the endpoint survives re-deploys.
78
80
  node.matterDeviceId = node.id
79
81
  node.invertPosition = config.invertPosition === true || config.invertPosition === 'true'
80
- node.coverExposeAsDimmableLight = config.coverExposeAsDimmableLight === true || config.coverExposeAsDimmableLight === 'true'
82
+ node.coverSwapOpenClose = config.coverSwapOpenClose === true || config.coverSwapOpenClose === 'true'
83
+ node.coverSliderDebounceMs = Number(config.coverSliderDebounceMs)
84
+ if (!Number.isFinite(node.coverSliderDebounceMs) || node.coverSliderDebounceMs < 0) node.coverSliderDebounceMs = 0
85
+ node.coverSliderDebounceMs = Math.min(5000, node.coverSliderDebounceMs)
81
86
  node.turnOnBehavior = config.turnOnBehavior || 'ignoreLevelAfterOn'
82
87
  node.ignoreLevelAfterOnMs = Number(config.ignoreLevelAfterOnMs)
83
88
  if (!Number.isFinite(node.ignoreLevelAfterOnMs) || node.ignoreLevelAfterOnMs < 0) node.ignoreLevelAfterOnMs = 800
89
+ // Alexa duplicate-command dedup window (ms). 0 disables it (raw forwarding);
90
+ // an empty/invalid value falls back to the 1200ms default. See the device factory.
91
+ node.commandDedupMs = Number(config.commandDedupMs)
92
+ if (!Number.isFinite(node.commandDedupMs) || node.commandDedupMs < 0) node.commandDedupMs = 1200
84
93
  node.coverUpdateMode = config.coverUpdateMode || 'optimistic'
85
94
  node.coverStatusTimeoutMs = Number(config.coverStatusTimeoutMs)
86
95
  if (!Number.isFinite(node.coverStatusTimeoutMs) || node.coverStatusTimeoutMs < 0) node.coverStatusTimeoutMs = 3000
@@ -110,6 +119,15 @@ module.exports = function (RED) {
110
119
  let initialReadTimer = null
111
120
  let suppressLevelCommandsUntil = 0
112
121
  let coverStatusTimer = null
122
+ let coverArrivalTimer = null
123
+ // Generous safety net, independent from the (short, configurable) coverStatusTimeoutMs
124
+ // warning below: real KNX cover actuators are not guaranteed to report back EXACTLY the
125
+ // requested percent100ths (positioning tolerance, drift...), and matter.js's
126
+ // WindowCoveringServer only clears OperationalStatus (Opening/Closing) back to Stopped
127
+ // once currentPosition === targetPosition exactly. Without this, a controller could show
128
+ // the cover as "still moving" forever after a movement that in reality completed close
129
+ // enough. Not (yet) user-configurable - ask if you want it exposed in the editor.
130
+ const COVER_ARRIVAL_FALLBACK_MS = Number(config.coverArrivalFallbackMs) > 0 ? Number(config.coverArrivalFallbackMs) : 45000
113
131
 
114
132
  const formatTs = (date) => {
115
133
  const d = date instanceof Date ? date : new Date(date)
@@ -157,6 +175,13 @@ module.exports = function (RED) {
157
175
  }
158
176
  }
159
177
 
178
+ const clearCoverArrivalTimer = () => {
179
+ if (coverArrivalTimer !== null) {
180
+ clearTimeout(coverArrivalTimer)
181
+ coverArrivalTimer = null
182
+ }
183
+ }
184
+
160
185
  // Precompute the routing tables for this single device:
161
186
  // statusRoutes: GA -> [{ fn, dpt }] (KNX -> Matter)
162
187
  // commandRoutes: fn -> { ga, dpt } (Matter -> KNX)
@@ -181,20 +206,42 @@ module.exports = function (RED) {
181
206
  return false
182
207
  }
183
208
 
184
- const applyOptimisticCoverPosition = (command) => {
185
- if (node.deviceType !== 'windowcovering' || node.coverUpdateMode !== 'optimistic' || !node.serverMatterBridge) return
186
- let optimisticPosition
187
- if (command.fn === 'position') optimisticPosition = command.value
188
- if (command.fn === 'updown') optimisticPosition = command.value === true ? 100 : 0
189
- if (optimisticPosition === undefined) return
190
- node.serverMatterBridge.setDeviceState(node.matterDeviceId, 'position', optimisticPosition)
209
+ // Matter-side feedback for a movement command just sent to KNX.
210
+ // - 'optimistic' (unchanged): reflect the requested position to Matter immediately; real
211
+ // KNX status, if configured, corrects it afterwards via handleSend below.
212
+ // - 'waitStatus': wait for the real KNX status GA. If it doesn't arrive within
213
+ // coverStatusTimeoutMs, just warn in the node status (as before). If NOTHING arrives
214
+ // within the much longer COVER_ARRIVAL_FALLBACK_MS either, fall back to reflecting the
215
+ // requested position anyway, so a real actuator that never reports back the exact
216
+ // target percent100ths doesn't leave the Matter device stuck "moving" forever.
217
+ const applyCoverPositionFeedback = (command) => {
218
+ if (node.deviceType !== 'windowcovering' || !node.serverMatterBridge) return
219
+ let requestedPosition
220
+ if (command.fn === 'position') requestedPosition = command.value
221
+ if (command.fn === 'updown') requestedPosition = command.value === true ? 100 : 0
222
+ if (requestedPosition === undefined) return
223
+
191
224
  clearCoverStatusTimer()
192
- if (node.coverStatusTimeoutMs > 0 && hasStatusRouteFor('position')) {
225
+ clearCoverArrivalTimer()
226
+
227
+ if (node.coverUpdateMode === 'optimistic') {
228
+ node.serverMatterBridge.setDeviceState(node.matterDeviceId, 'position', requestedPosition)
229
+ return
230
+ }
231
+
232
+ if (!hasStatusRouteFor('position')) return // no status GA at all: nothing to wait for or fall back from; the flow (if any) owns feedback
233
+
234
+ if (node.coverStatusTimeoutMs > 0) {
193
235
  coverStatusTimer = setTimeout(() => {
194
236
  coverStatusTimer = null
195
- node.setNodeStatus({ fill: 'yellow', shape: 'ring', text: 'Waiting for KNX cover status', payload: optimisticPosition })
237
+ node.setNodeStatus({ fill: 'yellow', shape: 'ring', text: 'Waiting for KNX cover status', payload: requestedPosition })
196
238
  }, node.coverStatusTimeoutMs)
197
239
  }
240
+ coverArrivalTimer = setTimeout(() => {
241
+ coverArrivalTimer = null
242
+ node.serverMatterBridge.setDeviceState(node.matterDeviceId, 'position', requestedPosition)
243
+ node.setNodeStatus({ fill: 'yellow', shape: 'ring', text: 'No exact KNX confirmation in time, assuming arrived at', payload: requestedPosition })
244
+ }, COVER_ARRIVAL_FALLBACK_MS)
198
245
  }
199
246
  node.knxUltimateAcceptedGAs = Array.from(statusRoutes.keys())
200
247
 
@@ -204,8 +251,15 @@ module.exports = function (RED) {
204
251
  type: node.deviceType,
205
252
  name: node.name,
206
253
  invertPosition: node.invertPosition === true,
207
- coverExposeAsDimmableLight: node.coverExposeAsDimmableLight === true,
208
- coverUpdateMode: node.coverUpdateMode
254
+ coverSwapOpenClose: node.coverSwapOpenClose === true,
255
+ coverSliderDebounceMs: node.coverSliderDebounceMs,
256
+ commandDedupMs: node.commandDedupMs,
257
+ coverUpdateMode: node.coverUpdateMode,
258
+ // Thermostat only: whether a heating/cooling setpoint GA (command or status) was
259
+ // configured, so the bridge engine can build a Heating-only, Cooling-only or dual
260
+ // ThermostatServer without a dedicated "mode" dropdown in the editor.
261
+ hasHeatingSetpoint: devConfigValue('gaSetpoint') !== '' || devConfigValue('gaSetpointStatus') !== '',
262
+ hasCoolingSetpoint: devConfigValue('gaCoolingSetpoint') !== '' || devConfigValue('gaCoolingSetpointStatus') !== ''
209
263
  })
210
264
 
211
265
  // Reflects the bridge (config node) status on this device node.
@@ -248,7 +302,7 @@ module.exports = function (RED) {
248
302
  }
249
303
  // Confirm the requested position to Matter even when this is a flow-only
250
304
  // device and no KNX command GA is configured.
251
- applyOptimisticCoverPosition(command)
305
+ applyCoverPositionFeedback(command)
252
306
  const route = commandRoutes.get(command.fn)
253
307
  if (route === undefined) {
254
308
  if (node.enableNodePINS) return // Flow-only device: the flow already got the command
@@ -286,7 +340,7 @@ module.exports = function (RED) {
286
340
  const value = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(route.dpt))
287
341
  if (value === undefined || value === null) return
288
342
  node.serverMatterBridge.setDeviceState(node.matterDeviceId, route.fn, value)
289
- if (node.deviceType === 'windowcovering' && route.fn === 'position') clearCoverStatusTimer()
343
+ if (node.deviceType === 'windowcovering' && route.fn === 'position') { clearCoverStatusTimer(); clearCoverArrivalTimer() }
290
344
  node.setNodeStatus({ fill: 'blue', shape: 'dot', text: `KNX->Matter ${route.fn}`, payload: value })
291
345
  } catch (error) {
292
346
  node.setNodeStatus({ fill: 'red', shape: 'dot', text: `KNX->Matter error ${error.message}` })
@@ -358,6 +412,7 @@ module.exports = function (RED) {
358
412
  try {
359
413
  if (initialReadTimer !== null) clearTimeout(initialReadTimer)
360
414
  clearCoverStatusTimer()
415
+ clearCoverArrivalTimer()
361
416
  } catch (error) { /* empty */ }
362
417
  try {
363
418
  if (node.serverKNX) node.serverKNX.removeClient(node)
@@ -28,7 +28,7 @@ Das ist die Gegenrichtung zum *Matter Device*-Node: Dort steuert KNX ein Matter-
28
28
  | RGB-Farblicht | + RGB-Farbe Befehls-/Status-GA (DPT 232.600). Die Matter-Farbe (Hue/Saturation oder XY, vom Farbrad der App) wird von/zur KNX-RGB-Triplette konvertiert |
29
29
  | Tunable-White-Licht | + Farbtemperatur Befehls-/Status-GA in Kelvin (DPT 7.600) |
30
30
  | Rollladen / Jalousie | Auf/Ab (DPT 1.008), Stopp (DPT 1.017), Position % Befehl/Status (DPT 5.001), optionale Positionsumkehr |
31
- | Thermostat (Heizung) | GA aktuelle Temperatur, Sollwert Befehls-/Status-GA (DPT 9.001) |
31
+ | Thermostat (Heizung und/oder Kühlung) | GA aktuelle Temperatur, Sollwert Befehls-/Status-GA (DPT 9.001). Mit zusätzlichem Kühl-Sollwert Befehls-/Status-GA wird auch der Kühlmodus (Doppel-Sollwert) bereitgestellt |
32
32
  | Ventilator / Lüftung | Geschwindigkeit % Befehls-/Status-GA (DPT 5.001) |
33
33
  | Sensoren (Temperatur, Feuchte, Licht, Präsenz, Kontakt) | Je eine Status-GA |
34
34
  | Rauch-/CO-Melder | Rauchalarm Status-GA + optionale CO-Alarm Status-GA (DPT 1.005): kritische Benachrichtigungen auf dem Telefon |
@@ -41,7 +41,7 @@ Das ist die Gegenrichtung zum *Matter Device*-Node: Dort steuert KNX ein Matter-
41
41
 
42
42
  ## Erweiterte Kompatibilität
43
43
 
44
- Diese Optionen werden nur angezeigt, wenn sie zum gewählten Typ passen. Dimmbare Geräte können den Helligkeitsbefehl ignorieren, den manche Controller direkt nach `Ein` senden. Rollläden/Jalousien können die Matter-Position nach einem Befehl optimistisch aktualisieren und sie korrigieren, sobald die KNX-Status-GA die echte Position meldet.
44
+ Diese Optionen werden nur angezeigt, wenn sie zum gewählten Typ passen. Dimmbare Geräte können den Helligkeitsbefehl ignorieren, den manche Controller direkt nach `Ein` senden. Bei Abdeckungen kehrt **Öffnen / Schließen tauschen** sowohl den binären KNX-Befehl als auch die Prozentrichtung um. **Abdeckungsregler-Entprellung** fasst schnelle Zwischenziele vor dem KNX-Schreiben zusammen: `0` nutzt adaptive Fenster (400 ms für den ersten, 150 ms für weitere Befehle); `1`–`5000` erzwingt ein festes Fenster. Die Matter-Position kann außerdem optimistisch aktualisiert und später durch die KNX-Status-GA korrigiert werden.
45
45
 
46
46
  ## Node-PINs
47
47
 
@@ -20,17 +20,22 @@
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
+ "alexa_dedup_ms": "Alexa-Duplikat-Fenster (ms, 0 = aus)",
23
24
  "turn_on_behavior": "Einschaltverhalten",
24
25
  "turn_on_ignore_level": "Helligkeit direkt nach Ein ignorieren",
25
26
  "turn_on_forward_all": "Jeden Helligkeitsbefehl weiterleiten",
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
+ "ignore_level_ms": "Helligkeit nach Ein ignorieren (ms)",
28
+ "cover_swap_open_close": "Abdeckung Öffnen / Schließen tauschen",
29
+ "cover_slider_debounce": "Abdeckungsregler-Entprellung (ms, 0 = adaptiv)",
29
30
  "cover_update_mode": "Jalousie-/Rollladenstatus",
30
31
  "cover_optimistic": "Matter-Position optimistisch aktualisieren",
31
32
  "cover_wait_status": "Nur auf KNX-Status warten",
32
33
  "cover_status_timeout": "Timeout für Positionsstatus (ms)"
33
34
  },
35
+ "tabs": {
36
+ "mappings": "KNX-Zuordnungen",
37
+ "advanced": "Erweiterte Optionen"
38
+ },
34
39
  "functions": {
35
40
  "fn_onoff_cmd": "Ein/Aus Befehls-GA",
36
41
  "fn_onoff_status": "Ein/Aus Status-GA",
@@ -44,6 +49,8 @@
44
49
  "fn_currenttemp": "GA aktuelle Temperatur",
45
50
  "fn_setpoint_cmd": "Sollwert Befehls-GA",
46
51
  "fn_setpoint_status": "Sollwert Status-GA",
52
+ "fn_coolingsetpoint_cmd": "Kühl-Sollwert Befehls-GA",
53
+ "fn_coolingsetpoint_status": "Kühl-Sollwert Status-GA",
47
54
  "fn_rgb_cmd": "RGB-Farbe Befehls-GA (DPT 232.600)",
48
55
  "fn_rgb_status": "RGB-Farbe Status-GA (DPT 232.600)",
49
56
  "fn_ct_cmd": "Farbtemperatur Befehls-GA (Kelvin)",
@@ -28,7 +28,7 @@ This is the opposite direction of the *Matter Device* node: there KNX controls a
28
28
  | RGB color light | + RGB color command/status GA (DPT 232.600). The Matter color (hue/saturation or XY, from the app color wheel) is converted to/from the KNX RGB triplet |
29
29
  | Tunable white light | + Color temperature command/status GA in Kelvin (DPT 7.600) |
30
30
  | Cover / Shutter | Up/Down (DPT 1.008), Stop (DPT 1.017), Position % command/status (DPT 5.001), optional position inversion |
31
- | Thermostat (heating) | Current temperature GA, Setpoint command/status GA (DPT 9.001) |
31
+ | Thermostat (heating and/or cooling) | Current temperature GA, Setpoint command/status GA (DPT 9.001). Optionally add a Cooling setpoint command/status GA to also expose a Cool mode (dual-setpoint) |
32
32
  | Fan / Ventilation | Fan speed % command/status GA (DPT 5.001) |
33
33
  | Sensors (temperature, humidity, light, occupancy, contact) | One status GA each |
34
34
  | Smoke/CO alarm | Smoke alarm status GA + optional CO alarm status GA (DPT 1.005): critical notifications on the phone |
@@ -41,7 +41,7 @@ This is the opposite direction of the *Matter Device* node: there KNX controls a
41
41
 
42
42
  ## Advanced compatibility
43
43
 
44
- These options are hidden unless they apply to the selected type. Dimmable devices can ignore the brightness command that some controllers send immediately after `On`. Covers can optimistically update the Matter position after a command, then correct it when the KNX status GA reports the real position.
44
+ These options are hidden unless they apply to the selected type. Dimmable devices can ignore the brightness command that some controllers send immediately after `On`. For covers, **Swap Open / Close** reverses both the binary KNX command and percentage direction. **Cover slider debounce** coalesces rapid intermediate targets before writing to KNX: `0` uses adaptive windows (400 ms for the first command, 150 ms for subsequent commands); `1`–`5000` forces one fixed window. Covers can also optimistically update the Matter position after a command, then correct it when the KNX status GA reports the real position.
45
45
 
46
46
  ## Node PINs
47
47
 
@@ -20,17 +20,22 @@
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
+ "alexa_dedup_ms": "Alexa duplicate-command window (ms, 0 = off)",
23
24
  "turn_on_behavior": "Turn-on behavior",
24
25
  "turn_on_ignore_level": "Ignore brightness sent immediately after On",
25
26
  "turn_on_forward_all": "Forward every brightness command",
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
+ "ignore_level_ms": "Ignore brightness after On (ms)",
28
+ "cover_swap_open_close": "Swap cover Open / Close",
29
+ "cover_slider_debounce": "Cover slider debounce (ms, 0 = adaptive)",
29
30
  "cover_update_mode": "Cover state update",
30
31
  "cover_optimistic": "Optimistically update Matter position",
31
32
  "cover_wait_status": "Wait for KNX status only",
32
33
  "cover_status_timeout": "Cover status timeout (ms)"
33
34
  },
35
+ "tabs": {
36
+ "mappings": "KNX mappings",
37
+ "advanced": "Advanced options"
38
+ },
34
39
  "functions": {
35
40
  "fn_onoff_cmd": "On/Off command GA",
36
41
  "fn_onoff_status": "On/Off status GA",
@@ -44,6 +49,8 @@
44
49
  "fn_currenttemp": "Current temperature GA",
45
50
  "fn_setpoint_cmd": "Setpoint command GA",
46
51
  "fn_setpoint_status": "Setpoint status GA",
52
+ "fn_coolingsetpoint_cmd": "Cooling setpoint command GA",
53
+ "fn_coolingsetpoint_status": "Cooling setpoint status GA",
47
54
  "fn_rgb_cmd": "RGB color command GA (DPT 232.600)",
48
55
  "fn_rgb_status": "RGB color status GA (DPT 232.600)",
49
56
  "fn_ct_cmd": "Color temperature command GA (Kelvin)",
@@ -28,7 +28,7 @@ Es la dirección opuesta al nodo *Matter Device*: allí KNX controla un disposit
28
28
  | Luz RGB (color) | + GA comando/estado color RGB (DPT 232.600). El color Matter (hue/saturation o XY, de la rueda de color de la app) se convierte desde/hacia la terna RGB KNX |
29
29
  | Luz blanco dinámico | + GA comando/estado temperatura de color en Kelvin (DPT 7.600) |
30
30
  | Persiana / Cortina | Subir/Bajar (DPT 1.008), Stop (DPT 1.017), posición % comando/estado (DPT 5.001), inversión de posición opcional |
31
- | Termostato (calefacción) | GA temperatura actual, GA comando/estado consigna (DPT 9.001) |
31
+ | Termostato (calefacción y/o refrigeración) | GA temperatura actual, GA comando/estado consigna (DPT 9.001). Añadiendo también la GA comando/estado consigna de refrigeración se expone también el modo Cool (doble consigna) |
32
32
  | Ventilador / VMC | GA comando/estado velocidad % (DPT 5.001) |
33
33
  | Sensores (temperatura, humedad, luz, presencia, contacto) | Una GA de estado cada uno |
34
34
  | Alarma humo/CO | GA estado alarma de humo + GA estado alarma CO opcional (DPT 1.005): notificaciones críticas en el teléfono |
@@ -41,7 +41,7 @@ Es la dirección opuesta al nodo *Matter Device*: allí KNX controla un disposit
41
41
 
42
42
  ## Compatibilidad avanzada
43
43
 
44
- Estas opciones solo aparecen cuando tienen sentido para el tipo seleccionado. Los dispositivos regulables pueden ignorar el comando de brillo que algunos controladores envían justo después de `On`. Las persianas pueden actualizar de forma optimista la posición Matter tras un comando y corregirla cuando la GA de estado KNX informa la posición real.
44
+ Estas opciones solo aparecen cuando tienen sentido para el tipo seleccionado. Los dispositivos regulables pueden ignorar el brillo enviado justo después de `On`. Para persianas, **Intercambiar Abrir / Cerrar** invierte el comando KNX binario y la dirección porcentual. **Antirrebote del deslizador** agrupa objetivos intermedios rápidos antes de escribir en KNX: `0` usa ventanas adaptativas (400 ms para el primer comando y 150 ms para los siguientes); `1`–`5000` fuerza una ventana fija. La posición Matter también puede actualizarse de forma optimista y corregirse después mediante la GA de estado KNX.
45
45
 
46
46
  ## PINes del nodo
47
47