node-red-contrib-knx-ultimate 6.0.4 → 6.0.6

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +1 -1
  3. package/nodes/commonFunctions.js +33 -0
  4. package/nodes/icons/node-matter-bridge-icon.svg +6 -0
  5. package/nodes/icons/node-matter-controller-icon.svg +6 -0
  6. package/nodes/knxUltimate-config.js +5 -1
  7. package/nodes/knxUltimateMatterBridge.html +39 -21
  8. package/nodes/knxUltimateMatterBridge.js +70 -15
  9. package/nodes/knxUltimateMatterControllerDevice.html +2 -7
  10. package/nodes/locales/de/knxUltimateMatterBridge.html +4 -2
  11. package/nodes/locales/de/knxUltimateMatterBridge.json +13 -10
  12. package/nodes/locales/de/matter-config.html +4 -0
  13. package/nodes/locales/de/matter-config.json +7 -2
  14. package/nodes/locales/de/matterbridge-config.html +4 -0
  15. package/nodes/locales/de/matterbridge-config.json +4 -1
  16. package/nodes/locales/en/knxUltimateMatterBridge.html +4 -2
  17. package/nodes/locales/en/knxUltimateMatterBridge.json +13 -10
  18. package/nodes/locales/en/matter-config.html +4 -0
  19. package/nodes/locales/en/matter-config.json +7 -2
  20. package/nodes/locales/en/matterbridge-config.html +4 -0
  21. package/nodes/locales/en/matterbridge-config.json +4 -1
  22. package/nodes/locales/es/knxUltimateMatterBridge.html +4 -2
  23. package/nodes/locales/es/knxUltimateMatterBridge.json +13 -10
  24. package/nodes/locales/es/matter-config.html +4 -0
  25. package/nodes/locales/es/matter-config.json +7 -2
  26. package/nodes/locales/es/matterbridge-config.html +4 -0
  27. package/nodes/locales/es/matterbridge-config.json +4 -1
  28. package/nodes/locales/fr/knxUltimateMatterBridge.html +4 -2
  29. package/nodes/locales/fr/knxUltimateMatterBridge.json +13 -10
  30. package/nodes/locales/fr/matter-config.html +4 -0
  31. package/nodes/locales/fr/matter-config.json +7 -2
  32. package/nodes/locales/fr/matterbridge-config.html +4 -0
  33. package/nodes/locales/fr/matterbridge-config.json +4 -1
  34. package/nodes/locales/it/knxUltimateMatterBridge.html +4 -2
  35. package/nodes/locales/it/knxUltimateMatterBridge.json +13 -10
  36. package/nodes/locales/it/matter-config.html +4 -0
  37. package/nodes/locales/it/matter-config.json +7 -2
  38. package/nodes/locales/it/matterbridge-config.html +4 -0
  39. package/nodes/locales/it/matterbridge-config.json +4 -1
  40. package/nodes/locales/zh-CN/knxUltimateMatterBridge.html +4 -2
  41. package/nodes/locales/zh-CN/knxUltimateMatterBridge.json +13 -10
  42. package/nodes/locales/zh-CN/matter-config.html +4 -0
  43. package/nodes/locales/zh-CN/matter-config.json +7 -2
  44. package/nodes/locales/zh-CN/matterbridge-config.html +4 -0
  45. package/nodes/locales/zh-CN/matterbridge-config.json +4 -1
  46. package/nodes/matter-config.html +59 -18
  47. package/nodes/matter-config.js +33 -0
  48. package/nodes/matterbridge-config.html +73 -3
  49. package/nodes/matterbridge-config.js +38 -0
  50. package/nodes/utils/matterBridgeDeviceFactory.mjs +244 -130
  51. package/nodes/utils/matterBridgeEngine.mjs +27 -22
  52. package/nodes/utils/matterEngine.mjs +13 -7
  53. package/nodes/utils/matterEnvironmentLock.mjs +32 -0
  54. package/nodes/utils/matterStorageBackup.js +100 -0
  55. package/package.json +1 -1
  56. package/resources/htmlUtils.js +11 -0
package/CHANGELOG.md CHANGED
@@ -6,6 +6,22 @@
6
6
 
7
7
  # CHANGELOG
8
8
 
9
+ **Version 6.0.6** - July 2026<br/>
10
+
11
+ - **Matter nodes — cleaner editors**: long pairing, storage, cache and endpoint-structure explanations were removed from the Matter node forms and consolidated in the localized HTML help, keeping the editors focused on fields, actions and live operational status.<br/>
12
+ - **Matter nodes — distinct canvas icons**: Matter Controller and Matter Bridge nodes now use separate compact icons, with `mat` above a large right/left direction arrow, making the two integration directions immediately distinguishable in the palette, workspace and configuration selectors.<br/>
13
+ - **Matter Controller/Bridge — protocol version and portable storage backups**: the Node-RED startup version line now logs the implemented Matter protocol revision (Matter 1.5.1, separately from the matter.js package version). Both Matter configuration editors can export and import a per-instance JSON backup containing the complete persistent Matter storage required to preserve fabrics, private credentials, sessions and commissioned/paired nodes. Import atomically replaces only the selected instance and restarts its engine; backup files contain secrets and must be protected like passwords.<br/>
14
+ - **Homepage — KNX integration engines, MQTT and Matter overview**: the integration-logo heading and Supported technologies section now identify the KNX integration engines and include dedicated MQTT and Matter cards in all six languages (EN/IT/DE/FR/ES/zh-CN), highlighting MQTT/Home Assistant discovery and the bidirectional Matter Controller/Bridge integration for controlling Matter devices and exposing KNX objects to Matter. The feature-request/enterprise-support solicitation, standalone changelog promotion and donation section have been removed.<br/>
15
+ - **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/>
16
+ - **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/>
17
+ - **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/>
18
+ - **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/>
19
+ - **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/>
20
+ - **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/>
21
+ - **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/>
22
+ - **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/>
23
+ - **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/>
24
+
9
25
  **Version 6.0.4** - July 2026<br/>
10
26
 
11
27
  - **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/>
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## The most popular KNX node for Node-RED
8
8
 
9
- KNX Ultimate is the most advanced KNX integration for Node-RED, providing secure KNX/IP communication, routing, ETS project import, Philips Hue, Matter, MQTT, diagnostics, virtual devices, and powerful automation nodes. Build professional, reliable, and scalable smart home and building automation projects with minimal effort.
9
+ KNX Ultimate is the most advanced KNX integration for Node-RED, providing secure KNX/IP communication, routing, ETS project import, Philips Hue, Matter Controller and Matter Bridge (control matter device via KNX and expose KNX GA via Matter), MQTT, diagnostics, virtual devices, and powerful automation nodes. Build professional, reliable, and scalable smart home and building automation projects with minimal effort.
10
10
 
11
11
  <br/>
12
12
  <br/>
@@ -1043,6 +1043,36 @@ module.exports = (RED) => {
1043
1043
  }
1044
1044
  })
1045
1045
 
1046
+ const exportMatterStorage = async (req, res, idKey) => {
1047
+ try {
1048
+ const configNode = RED.nodes.getNode(req.query[idKey])
1049
+ if (!configNode || typeof configNode.exportMatterStorage !== 'function') throw new Error('PLEASE DEPLOY FIRST: then try again.')
1050
+ const backup = await configNode.exportMatterStorage()
1051
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-')
1052
+ res.setHeader('Content-Type', 'application/json; charset=utf-8')
1053
+ res.setHeader('Content-Disposition', `attachment; filename="knx-ultimate-matter-${backup.kind}-${stamp}.json"`)
1054
+ res.send(JSON.stringify(backup, null, 2))
1055
+ } catch (error) {
1056
+ RED.log.error(`Err Matter storage export: ${error.message}`)
1057
+ if (!res.headersSent) res.status(500).json({ error: error.message })
1058
+ }
1059
+ }
1060
+
1061
+ const importMatterStorage = async (req, res, idKey) => {
1062
+ try {
1063
+ const configNode = RED.nodes.getNode(req.query[idKey])
1064
+ if (!configNode || typeof configNode.importMatterStorage !== 'function') throw new Error('PLEASE DEPLOY FIRST: then try again.')
1065
+ await configNode.importMatterStorage(req.body)
1066
+ res.json({ status: 'ok' })
1067
+ } catch (error) {
1068
+ RED.log.error(`Err Matter storage import: ${error.message}`)
1069
+ res.status(400).json({ error: error.message })
1070
+ }
1071
+ }
1072
+
1073
+ RED.httpAdmin.get('/KNXUltimateMatterStorageExport', normalizeAuthFromAccessTokenQuery, RED.auth.needsPermission('matter-config.read'), (req, res) => exportMatterStorage(req, res, 'serverId'))
1074
+ RED.httpAdmin.post('/KNXUltimateMatterStorageImport', RED.auth.needsPermission('matter-config.write'), (req, res) => importMatterStorage(req, res, 'serverId'))
1075
+
1046
1076
  // MATTER BRIDGE: returns the pairing info (QR code, manual code, commissioned fabrics) of a matterbridge-config node
1047
1077
  RED.httpAdmin.get('/KNXUltimateMatterBridgeInfo', RED.auth.needsPermission('matterbridge-config.read'), (req, res) => {
1048
1078
  try {
@@ -1074,6 +1104,9 @@ module.exports = (RED) => {
1074
1104
  }
1075
1105
  })
1076
1106
 
1107
+ RED.httpAdmin.get('/KNXUltimateMatterBridgeStorageExport', normalizeAuthFromAccessTokenQuery, RED.auth.needsPermission('matterbridge-config.read'), (req, res) => exportMatterStorage(req, res, 'configId'))
1108
+ RED.httpAdmin.post('/KNXUltimateMatterBridgeStorageImport', RED.auth.needsPermission('matterbridge-config.write'), (req, res) => importMatterStorage(req, res, 'configId'))
1109
+
1077
1110
  RED.httpAdmin.get('/knxUltimateDpts', (req, res) => {
1078
1111
  try {
1079
1112
  const dpts = Object.entries(dptlib.dpts).filter(onlyDptKeys).map(extractBaseNo).sort(sortBy('base'))
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg width="40" height="60" viewBox="0 0 40 60" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Matter Bridge">
3
+ <text x="20" y="21" fill="#ffffff" text-anchor="middle" font-family="Arial, sans-serif" font-size="20" font-weight="700" textLength="38" lengthAdjust="spacingAndGlyphs">mat</text>
4
+ <path d="M 37 34 H 12 V 29 L 2 38 L 12 47 V 42 H 25"
5
+ fill="none" stroke="#ffffff" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>
6
+ </svg>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg width="40" height="60" viewBox="0 0 40 60" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Matter Controller">
3
+ <text x="20" y="21" fill="#ffffff" text-anchor="middle" font-family="Arial, sans-serif" font-size="20" font-weight="700" textLength="38" lengthAdjust="spacingAndGlyphs">mat</text>
4
+ <path d="M 3 34 H 28 V 29 L 38 38 L 28 47 V 42 H 15"
5
+ fill="none" stroke="#ffffff" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>
6
+ </svg>
@@ -29,6 +29,10 @@ let NODE_VERSION = 'unknown'
29
29
  try { NODE_VERSION = require('../package.json').version } catch (e) { /* empty */ }
30
30
  let KNX_ENGINE_VERSION = 'unknown'
31
31
  try { KNX_ENGINE_VERSION = require('knxultimate/package.json').version } catch (e) { /* empty */ }
32
+ let MATTER_PACKAGE_VERSION = 'unknown'
33
+ let Specification = { REVISION: 'unknown' }
34
+ try { MATTER_PACKAGE_VERSION = require('@matter/main').version } catch (e) { /* empty */ }
35
+ try { Specification = require('@matter/model').Specification } catch (e) { /* empty */ }
32
36
 
33
37
  // DATAPONT MANIPULATION HELPERS
34
38
  // ####################
@@ -193,7 +197,7 @@ const findAutoEthernetInterface = (targetIP) => {
193
197
  module.exports = (RED) => {
194
198
  // Log node and KNXUltimate engine versions once, at Node-RED startup.
195
199
  try {
196
- RED.log.info(`KNXUltimate: node-red-contrib-knx-ultimate v${NODE_VERSION} (KNXUltimate engine v${KNX_ENGINE_VERSION})`)
200
+ RED.log.info(`KNXUltimate: node-red-contrib-knx-ultimate v${NODE_VERSION} (KNXUltimate engine v${KNX_ENGINE_VERSION}, Matter protocol ${Specification.REVISION}, matter.js v${MATTER_PACKAGE_VERSION})`)
197
201
  } catch (e) { /* empty */ }
198
202
 
199
203
  function knxUltimateConfigNode (config) {
@@ -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' },
@@ -96,7 +100,7 @@
96
100
  defaults: gaDefaults,
97
101
  inputs: 0,
98
102
  outputs: 0,
99
- icon: 'node-matter-icon.svg',
103
+ icon: 'node-matter-bridge-icon.svg',
100
104
  label() {
101
105
  return this.name || 'Expose KNX to Matter (BETA)';
102
106
  },
@@ -147,6 +151,7 @@
147
151
  const $fields = $('#mb-ga-fields').css({ display: 'flex', flexWrap: 'wrap', alignItems: 'flex-end', gap: '10px' });
148
152
  const $invertRow = $('#mb-invert-row');
149
153
  const $advancedRows = $('#mb-compatibility-options');
154
+ const $commandAdvancedRows = $('.mb-command-advanced-row');
150
155
  const $dimmerAdvancedRows = $('.mb-dimmer-advanced-row');
151
156
  const $coverAdvancedRows = $('.mb-cover-advanced-row');
152
157
  const gaInputs = {};
@@ -220,7 +225,11 @@
220
225
  $invertRow.toggle(selected === 'windowcovering');
221
226
  const hasLevel = selected === 'dimmablelight' || selected === 'rgblight' || selected === 'colortemperaturelight';
222
227
  const isCover = selected === 'windowcovering';
223
- $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);
224
233
  $dimmerAdvancedRows.toggle(hasLevel);
225
234
  $coverAdvancedRows.toggle(isCover);
226
235
  }
@@ -232,7 +241,9 @@
232
241
  node._matterGaInputs = gaInputs;
233
242
 
234
243
  $('#node-input-invertPosition').prop('checked', node.invertPosition === true);
235
- $('#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);
236
247
  $('#node-input-turnOnBehavior').val(node.turnOnBehavior || 'ignoreLevelAfterOn');
237
248
  $('#node-input-ignoreLevelAfterOnMs').val(node.ignoreLevelAfterOnMs === undefined ? 800 : node.ignoreLevelAfterOnMs);
238
249
  $('#node-input-coverUpdateMode').val(node.coverUpdateMode || 'optimistic');
@@ -246,7 +257,7 @@
246
257
  const selected = $('#node-input-deviceType').val();
247
258
  node.deviceType = selected;
248
259
  node.invertPosition = $('#node-input-invertPosition').is(':checked');
249
- node.coverExposeAsDimmableLight = selected === 'windowcovering' && $('#node-input-coverExposeAsDimmableLight').is(':checked');
260
+ node.coverSwapOpenClose = $('#node-input-coverSwapOpenClose').is(':checked');
250
261
  const active = TYPE_FIELDS[selected] || [];
251
262
  const gaInputs = node._matterGaInputs || {};
252
263
  // Clear every GA/DPT, then write only the ones of the selected type, reading
@@ -264,6 +275,11 @@
264
275
  }
265
276
  });
266
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)));
267
283
  this.turnOnBehavior = $('#node-input-turnOnBehavior').val() || 'ignoreLevelAfterOn';
268
284
  this.ignoreLevelAfterOnMs = Number($('#node-input-ignoreLevelAfterOnMs').val()) || 0;
269
285
  this.coverUpdateMode = $('#node-input-coverUpdateMode').val() || 'optimistic';
@@ -321,11 +337,6 @@
321
337
  </ul>
322
338
 
323
339
  <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
340
  <div id="mb-ga-fields"></div>
330
341
 
331
342
  <div class="form-row" id="mb-invert-row" style="display:none;">
@@ -339,10 +350,11 @@
339
350
 
340
351
  <div id="mb-tab-advanced" style="padding-top:12px;">
341
352
  <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>
353
+ <div class="form-row mb-command-advanced-row" style="display:none; margin-top:8px;">
354
+ <label for="node-input-commandDedupMs" style="width:260px;">
355
+ <i class="fa fa-clone"></i> <span data-i18n="knxUltimateMatterBridge.advanced.alexa_dedup_ms"></span>
356
+ </label>
357
+ <input type="number" id="node-input-commandDedupMs" min="0" step="100" style="width:120px;">
346
358
  </div>
347
359
 
348
360
  <div class="form-row mb-dimmer-advanced-row" style="display:none; margin-top:8px;">
@@ -363,12 +375,17 @@
363
375
  </div>
364
376
 
365
377
  <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;">
378
+ <label for="node-input-coverSwapOpenClose" style="width:260px;">
379
+ <i class="fa fa-exchange"></i> <span data-i18n="knxUltimateMatterBridge.advanced.cover_swap_open_close"></span>
380
+ </label>
381
+ <input type="checkbox" id="node-input-coverSwapOpenClose" style="width:auto; vertical-align:middle;">
382
+ </div>
383
+
384
+ <div class="form-row mb-cover-advanced-row" style="display:none;">
385
+ <label for="node-input-coverSliderDebounceMs" style="width:260px;">
386
+ <i class="fa fa-clock-o"></i> <span data-i18n="knxUltimateMatterBridge.advanced.cover_slider_debounce"></span>
370
387
  </label>
371
- <div class="form-tips" style="margin-top:5px;" data-i18n="knxUltimateMatterBridge.advanced.cover_expose_as_light_help"></div>
388
+ <input type="number" id="node-input-coverSliderDebounceMs" min="0" max="5000" step="50" style="width:120px;">
372
389
  </div>
373
390
 
374
391
  <div class="form-row mb-cover-advanced-row" style="display:none; margin-top:8px;">
@@ -430,8 +447,9 @@ Point it to a **Matter Bridge** configuration node (the actual bridge, paired on
430
447
  The advanced compatibility options are intentionally type-specific:
431
448
 
432
449
  - dimmable devices can ignore the brightness command that some controllers send immediately after an On command
450
+ - covers can swap Open/Close (including the percentage direction) for controllers such as Alexa that interpret them in reverse
451
+ - 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
433
452
  - 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
435
453
 
436
454
  The **KNX gateway is optional**: without it, enable the node PINs and drive the device from the flow.
437
455
 
@@ -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)
@@ -125,7 +125,7 @@
125
125
  },
126
126
  inputs: 0,
127
127
  outputs: 0,
128
- icon: "node-matter-icon.svg",
128
+ icon: "node-matter-controller-icon.svg",
129
129
  label: function () {
130
130
  return this.name || this.matterDeviceName || "Control Matter from KNX (BETA)";
131
131
  },
@@ -679,7 +679,6 @@
679
679
  const $tabs = $("#tabs");
680
680
  const $pinSectionRow = $("#node-input-enableNodePINS").closest('.form-row');
681
681
  const $pinSelect = $("#node-input-enableNodePINS");
682
- const $pinInfoRow = $pinSectionRow.next('.form-tips');
683
682
  let currentMatterCapabilities = parseMatterCapabilities($matterCapabilitiesInput.val() || node.matterDeviceCapabilities);
684
683
  $tabs.addClass('hue-vertical-tabs');
685
684
  $tabs.tabs(); // Tabs gestione KNX
@@ -984,9 +983,6 @@
984
983
  if ($pinSectionRow.length) {
985
984
  $pinSectionRow.show();
986
985
  }
987
- if ($pinInfoRow.length) {
988
- $pinInfoRow.show();
989
- }
990
986
  };
991
987
  updateTabsVisibility();
992
988
 
@@ -1899,7 +1895,6 @@
1899
1895
  </label>
1900
1896
  <input type="checkbox" id="node-input-updateLocalStateFromKNXWrite" style="display:inline-block; width:auto; vertical-align:top;" />
1901
1897
  </div>
1902
- <div class="form-tips" style="margin-left:260px;" data-i18n="knxUltimateMatterControllerDevice.update_local_state_from_knx_write_hint"></div><br/>
1903
1898
  <div id ="divBehaviourBrightness">
1904
1899
  <div class="form-row">
1905
1900
  <label for="node-input-specifySwitchOnBrightness" style="width:260px;">
@@ -2116,4 +2111,4 @@ _Non-Hue basic effects_
2116
2111
  The Dimming function works in **KNX mode `start` and`stop` ** . To start dimming, send only one "start" KNX telegram. To stop dimming, send a "stop" KNX telegram. Please**remember that** , when you set your wall swiches properties.
2117
2112
 
2118
2113
  <br/>
2119
- </script>
2114
+ </script>
@@ -9,6 +9,8 @@ Jeder Expose KNX to Matter Node stellt **ein KNX-Gerät als Matter-Gerät** bere
9
9
 
10
10
  Das ist die Gegenrichtung zum *Matter Device*-Node: Dort steuert KNX ein Matter-Gerät, hier steuern Matter-Controller KNX.
11
11
 
12
+ Das Ändern des Gerätetyps nach der Kopplung der Bridge verändert die Matter-Endpunktstruktur. Controller können den alten Endpunkt als nicht erreichbar behalten; dann die Bridge zurücksetzen/neu koppeln oder ein neues bereitgestelltes Gerät erstellen.
13
+
12
14
  ## Konfiguration
13
15
 
14
16
  |Feld|Beschreibung|
@@ -28,7 +30,7 @@ Das ist die Gegenrichtung zum *Matter Device*-Node: Dort steuert KNX ein Matter-
28
30
  | 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
31
  | Tunable-White-Licht | + Farbtemperatur Befehls-/Status-GA in Kelvin (DPT 7.600) |
30
32
  | 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) |
33
+ | 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
34
  | Ventilator / Lüftung | Geschwindigkeit % Befehls-/Status-GA (DPT 5.001) |
33
35
  | Sensoren (Temperatur, Feuchte, Licht, Präsenz, Kontakt) | Je eine Status-GA |
34
36
  | Rauch-/CO-Melder | Rauchalarm Status-GA + optionale CO-Alarm Status-GA (DPT 1.005): kritische Benachrichtigungen auf dem Telefon |
@@ -41,7 +43,7 @@ Das ist die Gegenrichtung zum *Matter Device*-Node: Dort steuert KNX ein Matter-
41
43
 
42
44
  ## Erweiterte Kompatibilität
43
45
 
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.
46
+ 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
47
 
46
48
  ## Node-PINs
47
49
 
@@ -17,24 +17,25 @@
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
+ "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
- "cover_status_timeout": "Timeout für Positionsstatus (ms)"
33
- },
34
- "tabs": {
35
- "mappings": "KNX-Zuordnungen",
36
- "advanced": "Erweiterte Optionen"
37
- },
33
+ "cover_status_timeout": "Timeout für Positionsstatus (ms)"
34
+ },
35
+ "tabs": {
36
+ "mappings": "KNX-Zuordnungen",
37
+ "advanced": "Erweiterte Optionen"
38
+ },
38
39
  "functions": {
39
40
  "fn_onoff_cmd": "Ein/Aus Befehls-GA",
40
41
  "fn_onoff_status": "Ein/Aus Status-GA",
@@ -48,6 +49,8 @@
48
49
  "fn_currenttemp": "GA aktuelle Temperatur",
49
50
  "fn_setpoint_cmd": "Sollwert Befehls-GA",
50
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",
51
54
  "fn_rgb_cmd": "RGB-Farbe Befehls-GA (DPT 232.600)",
52
55
  "fn_rgb_status": "RGB-Farbe Status-GA (DPT 232.600)",
53
56
  "fn_ct_cmd": "Farbtemperatur Befehls-GA (Kelvin)",
@@ -15,10 +15,14 @@ Der Controller kommuniziert mit den Geräten über das **IP-Netzwerk** (WLAN, Et
15
15
 
16
16
  Wenn das Gerät fabrikneu ist und nur Bluetooth-Kommissionierung unterstützt, kopple es zuerst mit der Hersteller-App oder einem anderen Matter-Controller (Alexa, Google Home, Apple Home) und nutze dann dessen Funktion **"mit weiterem Hub teilen"**, um einen neuen Kopplungscode für KNX-Ultimate zu erzeugen. So tritt das Gerät mehreren Fabrics gleichzeitig bei.
17
17
 
18
+ Bevorzuge den QR-Payload (`MT:...`): Er enthält den vollständigen Diskriminator. Der manuelle Code enthält nur den kurzen Diskriminator und kann bei mehreren identischen Modellen im Kopplungsmodus das falsche Gerät auswählen. Immer nur ein Gerät gleichzeitig koppeln.
19
+
18
20
  ## Speicherung
19
21
 
20
22
  Die Fabric-Zugangsdaten und die gekoppelten Geräte werden im Ordner `knxultimatestorage/matter` im Node-RED-Benutzerverzeichnis gespeichert. Das Löschen dieses Ordners entfernt alle Kopplungen.
21
23
 
24
+ Mit **Exportieren** wird eine vollständige Sicherung dieser Controller-Instanz heruntergeladen. Sie enthält Fabric, private Zugangsdaten, Sitzungen und Daten gekoppelter Geräte. **Die Datei wie ein Passwort schützen.** Der Import ersetzt den Matter-Speicher dieser Instanz und startet den Controller kurz neu. Eine Controller-Sicherung kann nicht in eine Bridge importiert werden.
25
+
22
26
  ## Ein Gerät entfernen
23
27
 
24
28
  Nutze den Papierkorb-Button in der Liste der gekoppelten Geräte. Der Controller versucht, das Gerät sauber zu dekommissionieren; ist es nicht erreichbar, wird es trotzdem aus der Fabric entfernt (danach kann ein Werksreset des Geräts nötig sein).
@@ -2,7 +2,6 @@
2
2
  "matter-config": {
3
3
  "properties": {
4
4
  "title": "Matter Controller",
5
- "intro": "Dieser Node ist ein Matter-Controller (mit eigener Fabric). Kopple hier deine Matter-Geräte und verwende dann den \"Matter Device\"-Node, um sie auf KNX-Gruppenadressen abzubilden. Zuerst deployen, dann hierher zurückkehren, um Geräte zu koppeln.",
6
5
  "node-config-input-name": "Name",
7
6
  "fabric_label": "Fabric-Bezeichnung",
8
7
  "pairing_title": "Neues Gerät koppeln",
@@ -24,7 +23,13 @@
24
23
  "pairing_ok": "Gerät erfolgreich gekoppelt!",
25
24
  "rename_button": "Umbenennen",
26
25
  "rename_prompt": "Neuer Gerätename",
27
- "rename_ok": "Gerät umbenannt."
26
+ "rename_ok": "Gerät umbenannt.",
27
+ "storage_title": "Sicherung von Fabric und Inbetriebnahme",
28
+ "storage_export": "Exportieren",
29
+ "storage_import": "Importieren",
30
+ "storage_warning": "Die Sicherung enthält Fabric-Zugangsdaten, private Schlüssel und Daten gekoppelter Geräte. Wie ein Passwort schützen. Der Import ersetzt den Matter-Speicher dieses Controllers und startet ihn kurz neu.",
31
+ "storage_import_confirm": "Matter-Fabric und gekoppelte Geräte dieses Controllers durch die gewählte Sicherung ersetzen?",
32
+ "storage_import_ok": "Matter-Sicherung importiert. Der Controller wurde neu gestartet."
28
33
  }
29
34
  }
30
35
  }
@@ -21,12 +21,16 @@ Dieser Konfigurations-Node ist die **Matter-Bridge selbst**: Er betreibt den Mat
21
21
  2. Das Kopplungs-Panel zeigt den **QR-Code** und den **manuellen Code**: scannen oder in Alexa / Google Home / Apple Home eingeben ("Matter-Gerät hinzufügen").
22
22
  3. Mehrere Controller können mit derselben Bridge gekoppelt werden (Matter Multi-Fabric).
23
23
 
24
+ Um nach dem Ausblenden des QR-Codes einen weiteren Controller hinzuzufügen, den Kopplungsmodus über einen bereits gekoppelten Controller öffnen und dann im neuen Controller ein Matter-Gerät hinzufügen. **Kopplung zurücksetzen** nur verwenden, um alle vorhandenen Controller zu entfernen und neu zu beginnen.
25
+
24
26
  Der Button **Kopplung zurücksetzen** entfernt alle gekoppelten Controller und startet die Kopplungs-Ankündigung neu.
25
27
 
26
28
  ## Identität und Speicherung
27
29
 
28
30
  Die Bridge-Identität ist an diesen Konfigurations-Node gebunden und wird in `knxultimatestorage/matter` im Node-RED-Benutzerverzeichnis gespeichert: Re-Deploys (auch mit geändertem Port oder Namen) erfordern **KEINE** neue Kopplung. Nur das Löschen dieses Konfigurations-Nodes und das Anlegen eines neuen ändert die Identität — in dem Fall die alte Bridge aus der Matter-App entfernen und neu koppeln.
29
31
 
32
+ Mit **Exportieren** wird eine vollständige Sicherung dieser Bridge-Instanz heruntergeladen, einschließlich Fabrics, privater Zugangsdaten, Sitzungen und Kopplungsdaten. **Die Datei wie ein Passwort schützen.** Der Import ersetzt den Speicher dieser Instanz und startet die Bridge kurz neu. Eine Bridge-Sicherung kann nicht in einen Controller importiert werden.
33
+
30
34
  ## Hinweise
31
35
 
32
36
  - Der Node-RED-Host muss **IPv6 link-local** aktiviert haben (Standard-Matter-Anforderung) und von den Controllern im lokalen Netzwerk erreichbar sein.
@@ -16,7 +16,10 @@
16
16
  "deploy_first": "Bitte zuerst diesen Konfigurations-Node deployen und dann auf Aktualisieren klicken.",
17
17
  "reset_button": "Kopplung zurücksetzen",
18
18
  "reset_confirm": "ALLE gekoppelten Controller entfernen und die Kopplung neu starten? Die Geräte verschwinden aus Alexa/Google/Apple.",
19
- "reset_ok": "Kopplung zurückgesetzt. Die Bridge wartet wieder auf Kopplung."
19
+ "reset_ok": "Kopplung zurückgesetzt. Die Bridge wartet wieder auf Kopplung.",
20
+ "storage_title": "Sicherung von Fabric und Kopplung", "storage_export": "Exportieren", "storage_import": "Importieren",
21
+ "storage_warning": "Die Sicherung enthält Fabric-Zugangsdaten, private Schlüssel und Kopplungsdaten. Wie ein Passwort schützen. Der Import ersetzt den Matter-Speicher dieser Bridge und startet sie kurz neu.",
22
+ "storage_import_confirm": "Matter-Fabrics und gekoppelte Controller dieser Bridge durch die gewählte Sicherung ersetzen?", "storage_import_ok": "Matter-Sicherung importiert. Die Bridge wurde neu gestartet."
20
23
  }
21
24
  }
22
25
  }