node-red-contrib-knx-ultimate 6.0.5 → 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 (45) hide show
  1. package/CHANGELOG.md +5 -1
  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 +1 -6
  8. package/nodes/knxUltimateMatterControllerDevice.html +2 -7
  9. package/nodes/locales/de/knxUltimateMatterBridge.html +2 -0
  10. package/nodes/locales/de/matter-config.html +4 -0
  11. package/nodes/locales/de/matter-config.json +7 -2
  12. package/nodes/locales/de/matterbridge-config.html +4 -0
  13. package/nodes/locales/de/matterbridge-config.json +4 -1
  14. package/nodes/locales/en/knxUltimateMatterBridge.html +2 -0
  15. package/nodes/locales/en/matter-config.html +4 -0
  16. package/nodes/locales/en/matter-config.json +7 -2
  17. package/nodes/locales/en/matterbridge-config.html +4 -0
  18. package/nodes/locales/en/matterbridge-config.json +4 -1
  19. package/nodes/locales/es/knxUltimateMatterBridge.html +2 -0
  20. package/nodes/locales/es/matter-config.html +4 -0
  21. package/nodes/locales/es/matter-config.json +7 -2
  22. package/nodes/locales/es/matterbridge-config.html +4 -0
  23. package/nodes/locales/es/matterbridge-config.json +4 -1
  24. package/nodes/locales/fr/knxUltimateMatterBridge.html +2 -0
  25. package/nodes/locales/fr/matter-config.html +4 -0
  26. package/nodes/locales/fr/matter-config.json +7 -2
  27. package/nodes/locales/fr/matterbridge-config.html +4 -0
  28. package/nodes/locales/fr/matterbridge-config.json +4 -1
  29. package/nodes/locales/it/knxUltimateMatterBridge.html +2 -0
  30. package/nodes/locales/it/matter-config.html +4 -0
  31. package/nodes/locales/it/matter-config.json +7 -2
  32. package/nodes/locales/it/matterbridge-config.html +4 -0
  33. package/nodes/locales/it/matterbridge-config.json +4 -1
  34. package/nodes/locales/zh-CN/knxUltimateMatterBridge.html +2 -0
  35. package/nodes/locales/zh-CN/matter-config.html +4 -0
  36. package/nodes/locales/zh-CN/matter-config.json +7 -2
  37. package/nodes/locales/zh-CN/matterbridge-config.html +4 -0
  38. package/nodes/locales/zh-CN/matterbridge-config.json +4 -1
  39. package/nodes/matter-config.html +59 -18
  40. package/nodes/matter-config.js +33 -0
  41. package/nodes/matterbridge-config.html +73 -3
  42. package/nodes/matterbridge-config.js +38 -0
  43. package/nodes/utils/matterStorageBackup.js +100 -0
  44. package/package.json +1 -1
  45. package/resources/htmlUtils.js +11 -0
package/CHANGELOG.md CHANGED
@@ -6,8 +6,12 @@
6
6
 
7
7
  # CHANGELOG
8
8
 
9
- **Version 6.0.5** - July 2026<br/>
9
+ **Version 6.0.6** - July 2026<br/>
10
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/>
11
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/>
12
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/>
13
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/>
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) {
@@ -100,7 +100,7 @@
100
100
  defaults: gaDefaults,
101
101
  inputs: 0,
102
102
  outputs: 0,
103
- icon: 'node-matter-icon.svg',
103
+ icon: 'node-matter-bridge-icon.svg',
104
104
  label() {
105
105
  return this.name || 'Expose KNX to Matter (BETA)';
106
106
  },
@@ -337,11 +337,6 @@
337
337
  </ul>
338
338
 
339
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
340
  <div id="mb-ga-fields"></div>
346
341
 
347
342
  <div class="form-row" id="mb-invert-row" style="display:none;">
@@ -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|
@@ -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
  }
@@ -9,6 +9,8 @@ Each Expose KNX to Matter node exposes **one KNX device as a Matter device**: th
9
9
 
10
10
  This is the opposite direction of the *Matter Device* node: there KNX controls a Matter device, here the Matter controllers control KNX.
11
11
 
12
+ Changing the device type after the bridge has been paired changes the Matter endpoint structure. Controllers may keep the old endpoint as unreachable; in that case reset/re-pair the bridge or create a new exposed device.
13
+
12
14
  ## Configuration
13
15
 
14
16
  |Field|Description|
@@ -15,10 +15,14 @@ The controller talks to the devices over the **IP network** (WiFi, Ethernet, or
15
15
 
16
16
  If the device is brand new and only supports Bluetooth commissioning, first pair it with its vendor app or another Matter controller (Alexa, Google Home, Apple Home), then use that controller's **"share / pair with another hub"** function to generate a new pairing code for KNX-Ultimate. This way the device joins multiple fabrics at once.
17
17
 
18
+ Prefer the QR payload (`MT:...`): it contains the full discriminator. A manual code contains only the short discriminator and may select the wrong device when several identical models are in commissioning mode. Pair one device at a time.
19
+
18
20
  ## Storage
19
21
 
20
22
  The fabric credentials and the paired devices are stored in the `knxultimatestorage/matter` folder inside your Node-RED user directory. Deleting that folder unpairs everything.
21
23
 
24
+ Use **Export** to download a complete backup of this controller instance. It includes the fabric, private credentials, sessions and commissioned-device data. **Protect the file like a password.** Import replaces this instance's current Matter storage and briefly restarts the controller. A controller backup cannot be imported into a bridge.
25
+
22
26
  ## Removing a device
23
27
 
24
28
  Use the trash button in the commissioned devices list. The controller tries to decommission the device properly; if it is unreachable, it is removed from the fabric anyway (a factory reset of the device may then be needed).
@@ -2,7 +2,6 @@
2
2
  "matter-config": {
3
3
  "properties": {
4
4
  "title": "Matter Controller",
5
- "intro": "This node is a Matter controller (its own fabric). Pair your Matter devices here, then use the \"Matter Device\" node to map them to KNX group addresses. Deploy the node first, then come back here to pair devices.",
6
5
  "node-config-input-name": "Name",
7
6
  "fabric_label": "Fabric label",
8
7
  "pairing_title": "Pair a new device",
@@ -25,7 +24,13 @@
25
24
  "pairing_hint": "Prefer the QR code (MT:…): it carries the full device discriminator. The manual code stores only a short discriminator and can't reliably tell identical devices apart (e.g. several Shelly of the same model), which may pair the wrong one or report it as already commissioned. Pair one device at a time; if it's already paired elsewhere, factory-reset it first.",
26
25
  "rename_button": "Rename",
27
26
  "rename_prompt": "New device name",
28
- "rename_ok": "Device renamed."
27
+ "rename_ok": "Device renamed.",
28
+ "storage_title": "Fabric and commissioning backup",
29
+ "storage_export": "Export",
30
+ "storage_import": "Import",
31
+ "storage_warning": "The backup contains fabric credentials, private keys and commissioned-device data. Keep it secret like a password. Import replaces this controller's current Matter storage and briefly restarts it.",
32
+ "storage_import_confirm": "Replace this controller's Matter fabric and commissioned devices with the selected backup?",
33
+ "storage_import_ok": "Matter backup imported. The controller has restarted."
29
34
  }
30
35
  }
31
36
  }
@@ -21,12 +21,16 @@ This configuration node is the **Matter bridge itself**: it runs the Matter serv
21
21
  2. The pairing panel shows the **QR code** and the **manual pairing code**: scan or type it in Alexa / Google Home / Apple Home ("add Matter device").
22
22
  3. Multiple controllers can be paired with the same bridge (Matter multi-fabric).
23
23
 
24
+ To add another controller after the QR code is hidden, open pairing mode from an already paired controller, then add a Matter device in the new controller. Use **Reset pairing** only to remove every existing controller and start over.
25
+
24
26
  The **Reset pairing** button removes all paired controllers and restarts the pairing advertising.
25
27
 
26
28
  ## Identity and storage
27
29
 
28
30
  The bridge identity is tied to this configuration node and stored in `knxultimatestorage/matter` inside the Node-RED user directory: re-deploys (even changing port or name) do **NOT** require a new pairing. Only deleting this configuration node and creating a new one changes the identity — in that case remove the old bridge from the Matter app and pair again.
29
31
 
32
+ Use **Export** to download a complete backup of this bridge instance, including fabrics, private credentials, sessions and pairing data. **Protect the file like a password.** Import replaces this instance's storage and briefly restarts the bridge. A bridge backup cannot be imported into a controller.
33
+
30
34
  ## Notes
31
35
 
32
36
  - The Node-RED host must have **IPv6 link-local** enabled (standard Matter requirement) and be reachable from the controllers on the local network.
@@ -16,7 +16,10 @@
16
16
  "deploy_first": "Deploy this configuration node first, then click refresh.",
17
17
  "reset_button": "Reset pairing",
18
18
  "reset_confirm": "Remove ALL paired controllers and restart pairing? The devices will disappear from Alexa/Google/Apple.",
19
- "reset_ok": "Pairing reset. The bridge is now advertising for pairing again."
19
+ "reset_ok": "Pairing reset. The bridge is now advertising for pairing again.",
20
+ "storage_title": "Fabric and commissioning backup", "storage_export": "Export", "storage_import": "Import",
21
+ "storage_warning": "The backup contains fabric credentials, private keys and pairing data. Keep it secret like a password. Import replaces this bridge's current Matter storage and briefly restarts it.",
22
+ "storage_import_confirm": "Replace this bridge's Matter fabrics and paired controllers with the selected backup?", "storage_import_ok": "Matter backup imported. The bridge has restarted."
20
23
  }
21
24
  }
22
25
  }
@@ -9,6 +9,8 @@ Cada nodo Expose KNX to Matter expone **un dispositivo KNX como dispositivo Matt
9
9
 
10
10
  Es la dirección opuesta al nodo *Matter Device*: allí KNX controla un dispositivo Matter, aquí los controladores Matter controlan KNX.
11
11
 
12
+ Cambiar el tipo de dispositivo después de emparejar el bridge modifica la estructura del endpoint Matter. Los controladores pueden conservar el endpoint antiguo como no alcanzable; en ese caso, restablece/vuelve a emparejar el bridge o crea un nuevo dispositivo expuesto.
13
+
12
14
  ## Configuración
13
15
 
14
16
  |Campo|Descripción|
@@ -15,10 +15,14 @@ El controlador se comunica con los dispositivos a través de la **red IP** (WiFi
15
15
 
16
16
  Si el dispositivo es nuevo de fábrica y solo admite emparejamiento por Bluetooth, emparéjalo primero con la app del fabricante o con otro controlador Matter (Alexa, Google Home, Apple Home) y usa después su función **"compartir / emparejar con otro hub"** para generar un nuevo código para KNX-Ultimate. Así el dispositivo se une a varias fabrics a la vez.
17
17
 
18
+ Prefiere el payload QR (`MT:...`): contiene el discriminador completo. El código manual solo contiene el discriminador corto y puede seleccionar el dispositivo equivocado si hay varios modelos idénticos en modo de emparejamiento. Empareja un dispositivo cada vez.
19
+
18
20
  ## Almacenamiento
19
21
 
20
22
  Las credenciales de la fabric y los dispositivos emparejados se guardan en la carpeta `knxultimatestorage/matter` dentro del directorio de usuario de Node-RED. Borrar esa carpeta elimina todos los emparejamientos.
21
23
 
24
+ Usa **Exportar** para descargar una copia completa de esta instancia del controlador. Incluye la fabric, credenciales privadas, sesiones y datos de dispositivos vinculados. **Protege el archivo como una contraseña.** La importación reemplaza el almacenamiento Matter de esta instancia y reinicia brevemente el controlador. Una copia de controlador no se puede importar en un bridge.
25
+
22
26
  ## Eliminar un dispositivo
23
27
 
24
28
  Usa el botón de la papelera en la lista de dispositivos emparejados. El controlador intenta retirar el dispositivo correctamente; si no está accesible, se elimina igualmente de la fabric (puede ser necesario un reset de fábrica del dispositivo).
@@ -2,7 +2,6 @@
2
2
  "matter-config": {
3
3
  "properties": {
4
4
  "title": "Controlador Matter",
5
- "intro": "Este nodo es un controlador Matter (con su propia fabric). Empareja aquí tus dispositivos Matter y usa el nodo \"Matter Device\" para mapearlos a direcciones de grupo KNX. Haz deploy primero y vuelve aquí para emparejar dispositivos.",
6
5
  "node-config-input-name": "Nombre",
7
6
  "fabric_label": "Etiqueta de fabric",
8
7
  "pairing_title": "Emparejar un nuevo dispositivo",
@@ -24,7 +23,13 @@
24
23
  "pairing_ok": "¡Dispositivo emparejado correctamente!",
25
24
  "rename_button": "Renombrar",
26
25
  "rename_prompt": "Nuevo nombre del dispositivo",
27
- "rename_ok": "Dispositivo renombrado."
26
+ "rename_ok": "Dispositivo renombrado.",
27
+ "storage_title": "Copia de fabric y vinculaciones",
28
+ "storage_export": "Exportar",
29
+ "storage_import": "Importar",
30
+ "storage_warning": "La copia contiene credenciales de fabric, claves privadas y datos de dispositivos vinculados. Protégela como una contraseña. La importación reemplaza el almacenamiento Matter de este controlador y lo reinicia brevemente.",
31
+ "storage_import_confirm": "¿Reemplazar la fabric Matter y los dispositivos vinculados de este controlador con la copia seleccionada?",
32
+ "storage_import_ok": "Copia Matter importada. El controlador se ha reiniciado."
28
33
  }
29
34
  }
30
35
  }
@@ -21,12 +21,16 @@ Este nodo de configuración es el **bridge Matter en sí**: ejecuta el servidor
21
21
  2. El panel de emparejamiento muestra el **código QR** y el **código manual**: escanéalo o escríbelo en Alexa / Google Home / Apple Home ("añadir dispositivo Matter").
22
22
  3. Se pueden emparejar varios controladores con el mismo bridge (multi-fabric Matter).
23
23
 
24
+ Para añadir otro controlador cuando el código QR está oculto, abre el modo de emparejamiento desde un controlador ya vinculado y añade un dispositivo Matter en el nuevo controlador. Usa **Restablecer emparejamiento** solo para eliminar todos los controladores existentes y empezar de nuevo.
25
+
24
26
  El botón **Restablecer emparejamiento** elimina todos los controladores emparejados y reinicia el anuncio de emparejamiento.
25
27
 
26
28
  ## Identidad y almacenamiento
27
29
 
28
30
  La identidad del bridge está ligada a este nodo de configuración y se guarda en `knxultimatestorage/matter` dentro del directorio de usuario de Node-RED: los re-deploys (incluso cambiando puerto o nombre) **NO** requieren un nuevo emparejamiento. Solo borrar este nodo de configuración y crear uno nuevo cambia la identidad — en ese caso elimina el bridge antiguo de la app Matter y vuelve a emparejar.
29
31
 
32
+ Usa **Exportar** para descargar una copia completa de esta instancia del bridge, con fabrics, credenciales privadas, sesiones y datos de vinculación. **Protege el archivo como una contraseña.** La importación reemplaza el almacenamiento de esta instancia y reinicia brevemente el bridge. Una copia de bridge no se puede importar en un controlador.
33
+
30
34
  ## Notas
31
35
 
32
36
  - El host de Node-RED debe tener **IPv6 link-local** habilitado (requisito estándar de Matter) y ser accesible desde los controladores en la red local.
@@ -16,7 +16,10 @@
16
16
  "deploy_first": "Haz deploy de este nodo de configuración primero y pulsa actualizar.",
17
17
  "reset_button": "Restablecer emparejamiento",
18
18
  "reset_confirm": "¿Eliminar TODOS los controladores emparejados y reiniciar el emparejamiento? Los dispositivos desaparecerán de Alexa/Google/Apple.",
19
- "reset_ok": "Emparejamiento restablecido. El bridge vuelve a estar en espera de emparejamiento."
19
+ "reset_ok": "Emparejamiento restablecido. El bridge vuelve a estar en espera de emparejamiento.",
20
+ "storage_title": "Copia de fabric y vinculaciones", "storage_export": "Exportar", "storage_import": "Importar",
21
+ "storage_warning": "La copia contiene credenciales de fabric, claves privadas y datos de vinculación. Protégela como una contraseña. La importación reemplaza el almacenamiento Matter de este bridge y lo reinicia brevemente.",
22
+ "storage_import_confirm": "¿Reemplazar las fabrics Matter y los controladores vinculados de este bridge con la copia seleccionada?", "storage_import_ok": "Copia Matter importada. El bridge se ha reiniciado."
20
23
  }
21
24
  }
22
25
  }
@@ -9,6 +9,8 @@ Chaque nœud Expose KNX to Matter expose **un appareil KNX comme appareil Matter
9
9
 
10
10
  C'est la direction opposée du nœud *Matter Device* : là-bas KNX contrôle un appareil Matter, ici les contrôleurs Matter contrôlent KNX.
11
11
 
12
+ Changer le type d'appareil après l'appairage du bridge modifie la structure de l'endpoint Matter. Les contrôleurs peuvent conserver l'ancien endpoint comme injoignable ; dans ce cas, réinitialisez/réappairez le bridge ou créez un nouvel appareil exposé.
13
+
12
14
  ## Configuration
13
15
 
14
16
  |Champ|Description|
@@ -15,10 +15,14 @@ Le contrôleur communique avec les appareils via le **réseau IP** (WiFi, Ethern
15
15
 
16
16
  Si l'appareil est neuf et ne prend en charge que l'appairage Bluetooth, appairez-le d'abord avec l'app du fabricant ou un autre contrôleur Matter (Alexa, Google Home, Apple Home), puis utilisez sa fonction **« partager / appairer avec un autre hub »** pour générer un nouveau code pour KNX-Ultimate. L'appareil rejoint ainsi plusieurs fabrics à la fois.
17
17
 
18
+ Préférez le payload QR (`MT:...`) : il contient le discriminateur complet. Le code manuel ne contient que le discriminateur court et peut sélectionner le mauvais appareil si plusieurs modèles identiques sont en mode appairage. Appairez un appareil à la fois.
19
+
18
20
  ## Stockage
19
21
 
20
22
  Les identifiants de la fabric et les appareils appairés sont enregistrés dans le dossier `knxultimatestorage/matter` du répertoire utilisateur de Node-RED. Supprimer ce dossier efface tous les appairages.
21
23
 
24
+ Utilisez **Exporter** pour télécharger une sauvegarde complète de cette instance de contrôleur. Elle contient la fabric, les identifiants privés, les sessions et les données des appareils associés. **Protégez le fichier comme un mot de passe.** L'import remplace le stockage Matter de cette instance et redémarre brièvement le contrôleur. Une sauvegarde de contrôleur ne peut pas être importée dans un bridge.
25
+
22
26
  ## Supprimer un appareil
23
27
 
24
28
  Utilisez le bouton corbeille dans la liste des appareils appairés. Le contrôleur essaie de retirer l'appareil proprement ; s'il est injoignable, il est quand même supprimé de la fabric (une réinitialisation d'usine de l'appareil peut alors être nécessaire).
@@ -2,7 +2,6 @@
2
2
  "matter-config": {
3
3
  "properties": {
4
4
  "title": "Contrôleur Matter",
5
- "intro": "Ce nœud est un contrôleur Matter (avec sa propre fabric). Appairez ici vos appareils Matter, puis utilisez le nœud \"Matter Device\" pour les mapper sur des adresses de groupe KNX. Déployez d'abord le nœud, puis revenez ici pour appairer les appareils.",
6
5
  "node-config-input-name": "Nom",
7
6
  "fabric_label": "Étiquette de fabric",
8
7
  "pairing_title": "Appairer un nouvel appareil",
@@ -24,7 +23,13 @@
24
23
  "pairing_ok": "Appareil appairé avec succès !",
25
24
  "rename_button": "Renommer",
26
25
  "rename_prompt": "Nouveau nom de l'appareil",
27
- "rename_ok": "Appareil renommé."
26
+ "rename_ok": "Appareil renommé.",
27
+ "storage_title": "Sauvegarde de la fabric et des associations",
28
+ "storage_export": "Exporter",
29
+ "storage_import": "Importer",
30
+ "storage_warning": "La sauvegarde contient les identifiants de fabric, les clés privées et les données des appareils associés. Protégez-la comme un mot de passe. L'import remplace le stockage Matter de ce contrôleur et le redémarre brièvement.",
31
+ "storage_import_confirm": "Remplacer la fabric Matter et les appareils associés de ce contrôleur par la sauvegarde sélectionnée ?",
32
+ "storage_import_ok": "Sauvegarde Matter importée. Le contrôleur a redémarré."
28
33
  }
29
34
  }
30
35
  }
@@ -21,12 +21,16 @@ Ce nœud de configuration est le **bridge Matter lui-même** : il exécute le se
21
21
  2. Le panneau d'appairage montre le **QR code** et le **code manuel** : scannez ou saisissez-le dans Alexa / Google Home / Apple Home (« ajouter un appareil Matter »).
22
22
  3. Plusieurs contrôleurs peuvent être appairés au même bridge (multi-fabric Matter).
23
23
 
24
+ Pour ajouter un contrôleur lorsque le QR code est masqué, ouvrez le mode d'appairage depuis un contrôleur déjà associé, puis ajoutez un appareil Matter dans le nouveau contrôleur. Utilisez **Réinitialiser l'appairage** uniquement pour supprimer tous les contrôleurs et recommencer.
25
+
24
26
  Le bouton **Réinitialiser l'appairage** supprime tous les contrôleurs appairés et relance l'annonce d'appairage.
25
27
 
26
28
  ## Identité et stockage
27
29
 
28
30
  L'identité du bridge est liée à ce nœud de configuration et enregistrée dans `knxultimatestorage/matter` du répertoire utilisateur de Node-RED : les re-deploys (même en changeant port ou nom) ne nécessitent **PAS** de nouvel appairage. Seule la suppression de ce nœud de configuration et la création d'un nouveau changent l'identité — dans ce cas supprimez l'ancien bridge de l'app Matter et appairez de nouveau.
29
31
 
32
+ Utilisez **Exporter** pour télécharger une sauvegarde complète de cette instance du bridge, avec fabrics, identifiants privés, sessions et données d'association. **Protégez le fichier comme un mot de passe.** L'import remplace le stockage de cette instance et redémarre brièvement le bridge. Une sauvegarde de bridge ne peut pas être importée dans un contrôleur.
33
+
30
34
  ## Notes
31
35
 
32
36
  - L'hôte Node-RED doit avoir **IPv6 link-local** activé (exigence standard de Matter) et être joignable par les contrôleurs sur le réseau local.
@@ -16,7 +16,10 @@
16
16
  "deploy_first": "Déployez d'abord ce nœud de configuration, puis cliquez sur actualiser.",
17
17
  "reset_button": "Réinitialiser l'appairage",
18
18
  "reset_confirm": "Supprimer TOUS les contrôleurs appairés et relancer l'appairage ? Les appareils disparaîtront d'Alexa/Google/Apple.",
19
- "reset_ok": "Appairage réinitialisé. Le bridge attend de nouveau un appairage."
19
+ "reset_ok": "Appairage réinitialisé. Le bridge attend de nouveau un appairage.",
20
+ "storage_title": "Sauvegarde de la fabric et des associations", "storage_export": "Exporter", "storage_import": "Importer",
21
+ "storage_warning": "La sauvegarde contient les identifiants de fabric, les clés privées et les données d'association. Protégez-la comme un mot de passe. L'import remplace le stockage Matter de ce bridge et le redémarre brièvement.",
22
+ "storage_import_confirm": "Remplacer les fabrics Matter et les contrôleurs associés de ce bridge par la sauvegarde sélectionnée ?", "storage_import_ok": "Sauvegarde Matter importée. Le bridge a redémarré."
20
23
  }
21
24
  }
22
25
  }
@@ -9,6 +9,8 @@ Ogni nodo Expose KNX to Matter espone **un dispositivo KNX come dispositivo Matt
9
9
 
10
10
  È la direzione opposta del nodo *Matter Device*: lì KNX controlla un dispositivo Matter, qui i controller Matter controllano KNX.
11
11
 
12
+ Cambiare il tipo dispositivo dopo l'abbinamento del bridge modifica la struttura dell'endpoint Matter. I controller possono conservare il vecchio endpoint come irraggiungibile; in tal caso resetta/riabbina il bridge oppure crea un nuovo dispositivo esposto.
13
+
12
14
  ## Configurazione
13
15
 
14
16
  |Campo|Descrizione|
@@ -15,10 +15,14 @@ Il controller comunica con i dispositivi tramite la **rete IP** (WiFi, Ethernet,
15
15
 
16
16
  Se il dispositivo è nuovo di fabbrica e supporta solo il commissioning Bluetooth, associalo prima con l'app del produttore o con un altro controller Matter (Alexa, Google Home, Apple Home), poi usa la funzione **"condividi / abbina con altro hub"** di quel controller per generare un nuovo codice di abbinamento per KNX-Ultimate. In questo modo il dispositivo entra in più fabric contemporaneamente.
17
17
 
18
+ Preferisci il payload QR (`MT:...`): contiene il discriminatore completo. Il codice manuale contiene solo quello corto e può selezionare il dispositivo sbagliato quando più modelli identici sono in modalità abbinamento. Associa un dispositivo alla volta.
19
+
18
20
  ## Archiviazione
19
21
 
20
22
  Le credenziali della fabric e i dispositivi associati sono salvati nella cartella `knxultimatestorage/matter` dentro la directory utente di Node-RED. Cancellando quella cartella si perdono tutte le associazioni.
21
23
 
24
+ Usa **Esporta** per scaricare un backup completo di questa istanza controller. Include fabric, credenziali private, sessioni e dati dei dispositivi associati. **Proteggi il file come una password.** L'importazione sostituisce lo storage Matter di questa istanza e riavvia brevemente il controller. Un backup controller non può essere importato in un bridge.
25
+
22
26
  ## Rimuovere un dispositivo
23
27
 
24
28
  Usa il pulsante cestino nella lista dei dispositivi associati. Il controller prova a decommissionare correttamente il dispositivo; se non è raggiungibile, viene comunque rimosso dalla fabric (potrebbe poi servire un reset di fabbrica del dispositivo).
@@ -2,7 +2,6 @@
2
2
  "matter-config": {
3
3
  "properties": {
4
4
  "title": "Controller Matter",
5
- "intro": "Questo nodo è un controller Matter (con una propria fabric). Associa qui i tuoi dispositivi Matter, poi usa il nodo \"Matter Device\" per mapparli sugli indirizzi di gruppo KNX. Fai prima il deploy del nodo, poi torna qui per associare i dispositivi.",
6
5
  "node-config-input-name": "Nome",
7
6
  "fabric_label": "Etichetta fabric",
8
7
  "pairing_title": "Associa un nuovo dispositivo",
@@ -25,7 +24,13 @@
25
24
  "pairing_hint": "Preferisci il QR code (MT:…): contiene il discriminatore completo del dispositivo. Il codice manuale contiene solo un discriminatore corto e non distingue in modo affidabile dispositivi identici (es. più Shelly dello stesso modello), rischiando di associare quello sbagliato o segnalarlo come già registrato. Registra un dispositivo alla volta; se è già associato altrove, fai prima un factory reset.",
26
25
  "rename_button": "Rinomina",
27
26
  "rename_prompt": "Nuovo nome dispositivo",
28
- "rename_ok": "Dispositivo rinominato."
27
+ "rename_ok": "Dispositivo rinominato.",
28
+ "storage_title": "Backup fabric e associazioni",
29
+ "storage_export": "Esporta",
30
+ "storage_import": "Importa",
31
+ "storage_warning": "Il backup contiene credenziali fabric, chiavi private e dati dei dispositivi associati. Proteggilo come una password. L'importazione sostituisce lo storage Matter di questo controller e lo riavvia brevemente.",
32
+ "storage_import_confirm": "Sostituire la fabric Matter e i dispositivi associati di questo controller con il backup selezionato?",
33
+ "storage_import_ok": "Backup Matter importato. Il controller è stato riavviato."
29
34
  }
30
35
  }
31
36
  }
@@ -21,12 +21,16 @@ Questo nodo di configurazione è il **bridge Matter vero e proprio**: esegue il
21
21
  2. Il pannello di abbinamento mostra il **QR code** e il **codice manuale**: scansiona o digita in Alexa / Google Home / Apple Home ("aggiungi dispositivo Matter").
22
22
  3. Più controller possono essere abbinati allo stesso bridge (multi-fabric Matter).
23
23
 
24
+ Per aggiungere un altro controller quando il QR code è nascosto, apri la modalità di abbinamento da un controller già associato, poi aggiungi un dispositivo Matter nel nuovo controller. Usa **Reset abbinamento** solo per rimuovere tutti i controller esistenti e ripartire.
25
+
24
26
  Il pulsante **Reset abbinamento** rimuove tutti i controller abbinati e riavvia l'advertising di abbinamento.
25
27
 
26
28
  ## Identità e archiviazione
27
29
 
28
30
  L'identità del bridge è legata a questo nodo di configurazione ed è salvata in `knxultimatestorage/matter` nella directory utente di Node-RED: i re-deploy (anche cambiando porta o nome) **NON** richiedono un nuovo abbinamento. Solo eliminando questo nodo di configurazione e creandone uno nuovo cambia l'identità — in quel caso rimuovi il vecchio bridge dall'app Matter e riabbina.
29
31
 
32
+ Usa **Esporta** per scaricare un backup completo di questa istanza bridge, incluse fabric, credenziali private, sessioni e dati di abbinamento. **Proteggi il file come una password.** L'importazione sostituisce lo storage dell'istanza e riavvia brevemente il bridge. Un backup bridge non può essere importato in un controller.
33
+
30
34
  ## Note
31
35
 
32
36
  - L'host Node-RED deve avere **IPv6 link-local** attivo (requisito standard Matter) ed essere raggiungibile dai controller sulla rete locale.
@@ -16,7 +16,10 @@
16
16
  "deploy_first": "Fai prima il deploy di questo nodo di configurazione, poi clicca aggiorna.",
17
17
  "reset_button": "Reset abbinamento",
18
18
  "reset_confirm": "Rimuovere TUTTI i controller abbinati e riavviare l'abbinamento? I dispositivi spariranno da Alexa/Google/Apple.",
19
- "reset_ok": "Abbinamento resettato. Il bridge è di nuovo in attesa di abbinamento."
19
+ "reset_ok": "Abbinamento resettato. Il bridge è di nuovo in attesa di abbinamento.",
20
+ "storage_title": "Backup fabric e associazioni", "storage_export": "Esporta", "storage_import": "Importa",
21
+ "storage_warning": "Il backup contiene credenziali fabric, chiavi private e dati di abbinamento. Proteggilo come una password. L'importazione sostituisce lo storage Matter di questo bridge e lo riavvia brevemente.",
22
+ "storage_import_confirm": "Sostituire le fabric Matter e i controller abbinati di questo bridge con il backup selezionato?", "storage_import_ok": "Backup Matter importato. Il bridge è stato riavviato."
20
23
  }
21
24
  }
22
25
  }
@@ -9,6 +9,8 @@
9
9
 
10
10
  这与 *Matter Device* 节点方向相反:那里是 KNX 控制 Matter 设备,这里是 Matter 控制器控制 KNX。
11
11
 
12
+ 桥接器配对后更改设备类型会改变 Matter 端点结构。控制器可能会将旧端点保留为不可达;此时请重置并重新配对桥接器,或创建新的公开设备。
13
+
12
14
  ## 配置
13
15
 
14
16
  |字段|说明|
@@ -15,10 +15,14 @@
15
15
 
16
16
  如果设备是全新的且仅支持蓝牙配对,请先用厂商 App 或其他 Matter 控制器(Alexa、Google Home、Apple Home)配对,然后使用其**"与其他中枢共享/配对"**功能为 KNX-Ultimate 生成新的配对码。这样设备可以同时加入多个 fabric。
17
17
 
18
+ 优先使用二维码 payload(`MT:...`),因为它包含完整的鉴别码。手动配对码只包含短鉴别码;当多个相同型号同时处于配对模式时,可能会选错设备。请一次只配对一个设备。
19
+
18
20
  ## 存储
19
21
 
20
22
  Fabric 凭据和已配对设备保存在 Node-RED 用户目录下的 `knxultimatestorage/matter` 文件夹中。删除该文件夹将移除所有配对。
21
23
 
24
+ 使用 **导出** 下载此控制器实例的完整备份,其中包含 Fabric、私有凭据、会话和已配网设备数据。**请像密码一样保护此文件。** 导入会替换此实例当前的 Matter 存储并短暂重启控制器。控制器备份不能导入网桥。
25
+
22
26
  ## 移除设备
23
27
 
24
28
  使用已配对设备列表中的垃圾桶按钮。控制器会尝试正确地解除设备调试;如果设备无法访问,仍会从 fabric 中移除(之后可能需要对设备进行恢复出厂设置)。
@@ -2,7 +2,6 @@
2
2
  "matter-config": {
3
3
  "properties": {
4
4
  "title": "Matter 控制器",
5
- "intro": "此节点是一个 Matter 控制器(拥有自己的 fabric)。在此配对您的 Matter 设备,然后使用 \"Matter Device\" 节点将其映射到 KNX 组地址。请先部署该节点,然后回到这里配对设备。",
6
5
  "node-config-input-name": "名称",
7
6
  "fabric_label": "Fabric 标签",
8
7
  "pairing_title": "配对新设备",
@@ -24,7 +23,13 @@
24
23
  "pairing_ok": "设备配对成功!",
25
24
  "rename_button": "重命名",
26
25
  "rename_prompt": "新的设备名称",
27
- "rename_ok": "设备已重命名。"
26
+ "rename_ok": "设备已重命名。",
27
+ "storage_title": "Fabric 和配网备份",
28
+ "storage_export": "导出",
29
+ "storage_import": "导入",
30
+ "storage_warning": "备份包含 Fabric 凭据、私钥和已配网设备数据。请像密码一样妥善保管。导入会替换此控制器的 Matter 存储并短暂重启。",
31
+ "storage_import_confirm": "要用所选备份替换此控制器的 Matter Fabric 和已配网设备吗?",
32
+ "storage_import_ok": "Matter 备份已导入,控制器已重启。"
28
33
  }
29
34
  }
30
35
  }
@@ -21,12 +21,16 @@
21
21
  2. 配对面板显示**二维码**和**手动配对码**:在 Alexa / Google Home / Apple Home 中扫描或输入("添加 Matter 设备")。
22
22
  3. 多个控制器可以与同一个桥接器配对(Matter 多 fabric)。
23
23
 
24
+ 二维码隐藏后如需添加其他控制器,请从一个已配对的控制器开启配对模式,然后在新控制器中添加 Matter 设备。仅在需要移除所有现有控制器并重新开始时使用**重置配对**。
25
+
24
26
  **重置配对**按钮会移除所有已配对的控制器并重新开始配对广播。
25
27
 
26
28
  ## 身份与存储
27
29
 
28
30
  桥接器身份与此配置节点绑定,保存在 Node-RED 用户目录的 `knxultimatestorage/matter` 中:重新部署(即使更改端口或名称)**不**需要重新配对。只有删除此配置节点并新建一个才会改变身份——此时请从 Matter App 中移除旧桥接器并重新配对。
29
31
 
32
+ 使用 **导出** 下载此网桥实例的完整备份,其中包含 Fabric、私有凭据、会话和配对数据。**请像密码一样保护此文件。** 导入会替换此实例的存储并短暂重启网桥。网桥备份不能导入控制器。
33
+
30
34
  ## 说明
31
35
 
32
36
  - Node-RED 主机必须启用 **IPv6 link-local**(Matter 标准要求),并且控制器可在本地网络访问到它。
@@ -16,7 +16,10 @@
16
16
  "deploy_first": "请先部署此配置节点,然后点击刷新。",
17
17
  "reset_button": "重置配对",
18
18
  "reset_confirm": "移除所有已配对的控制器并重新开始配对?设备将从 Alexa/Google/Apple 中消失。",
19
- "reset_ok": "配对已重置。桥接器重新进入配对模式。"
19
+ "reset_ok": "配对已重置。桥接器重新进入配对模式。",
20
+ "storage_title": "Fabric 和配网备份", "storage_export": "导出", "storage_import": "导入",
21
+ "storage_warning": "备份包含 Fabric 凭据、私钥和配对数据。请像密码一样妥善保管。导入会替换此网桥的 Matter 存储并短暂重启。",
22
+ "storage_import_confirm": "要用所选备份替换此网桥的 Matter Fabric 和已配对控制器吗?", "storage_import_ok": "Matter 备份已导入,网桥已重启。"
20
23
  }
21
24
  }
22
25
  }
@@ -3,6 +3,7 @@
3
3
  <script type="text/javascript">
4
4
  RED.nodes.registerType('matter-config', {
5
5
  category: 'config',
6
+ icon: 'node-matter-controller-icon.svg',
6
7
  defaults: {
7
8
  name: { value: "Matter Controller" },
8
9
  fabricLabel: { value: "KNX-Ultimate" }
@@ -20,6 +21,23 @@
20
21
  var $devicesBody = $("#matter-devices-body");
21
22
  var $refreshDevices = $("#matterRefreshDevices");
22
23
  var $pairSpinner = $("#matterPairSpinner");
24
+ var $storageExport = $('#matter-storage-export');
25
+ var $storageImport = $('#matter-storage-import');
26
+ var $storageFile = $('#matter-storage-file');
27
+
28
+ function storageExportUrl() {
29
+ var adminRoot = (RED.settings && typeof RED.settings.httpAdminRoot === 'string') ? RED.settings.httpAdminRoot : '/';
30
+ adminRoot = String(adminRoot || '/').trim();
31
+ adminRoot = adminRoot === '/' ? '' : '/' + adminRoot.replace(/^\/+|\/+$/g, '');
32
+ var params = new URLSearchParams();
33
+ params.set('serverId', node.id);
34
+ params.set('_', String(Date.now()));
35
+ try {
36
+ var tokens = (RED.settings && typeof RED.settings.get === 'function') ? RED.settings.get('auth-tokens') : null;
37
+ if (tokens && typeof tokens.access_token === 'string' && tokens.access_token.trim() !== '') params.set('access_token', tokens.access_token.trim());
38
+ } catch (error) { }
39
+ return adminRoot + '/KNXUltimateMatterStorageExport?' + params.toString();
40
+ }
23
41
 
24
42
  function notifyPairingError(msg) {
25
43
  var text = RED._("node-red-contrib-knx-ultimate/matter-config:matter-config.properties.pairing_failed") + ': ' + (msg || '');
@@ -148,6 +166,32 @@
148
166
  });
149
167
 
150
168
  loadDevices();
169
+
170
+ $storageExport.on('click', function () {
171
+ window.location = storageExportUrl();
172
+ });
173
+ $storageImport.on('click', function () { $storageFile.trigger('click'); });
174
+ $storageFile.on('change', function () {
175
+ var file = this.files && this.files[0];
176
+ this.value = '';
177
+ if (!file || !window.confirm(RED._('node-red-contrib-knx-ultimate/matter-config:matter-config.properties.storage_import_confirm'))) return;
178
+ var reader = new FileReader();
179
+ reader.onload = function () {
180
+ var backup;
181
+ try { backup = JSON.parse(reader.result); } catch (error) { RED.notify(error.message, { type: 'error' }); return; }
182
+ $storageImport.prop('disabled', true);
183
+ $.ajax({
184
+ url: 'KNXUltimateMatterStorageImport?serverId=' + encodeURIComponent(node.id),
185
+ method: 'POST', contentType: 'application/json', data: JSON.stringify(backup)
186
+ }).done(function () {
187
+ RED.notify(RED._('node-red-contrib-knx-ultimate/matter-config:matter-config.properties.storage_import_ok'), { type: 'success' });
188
+ loadDevices();
189
+ }).fail(function (xhr) {
190
+ RED.notify((xhr.responseJSON && xhr.responseJSON.error) || xhr.statusText, { type: 'error', fixed: true });
191
+ }).always(function () { $storageImport.prop('disabled', false); });
192
+ };
193
+ reader.readAsText(file);
194
+ });
151
195
  },
152
196
  oneditsave: function () {
153
197
  // Return to the info tab
@@ -167,7 +211,6 @@
167
211
  </div>
168
212
 
169
213
  <div class="form-row">
170
- <b><span data-i18n="matter-config.properties.title"></span></b>
171
214
  <p align="center" style="margin:8px 0;">
172
215
  <svg width="70" height="77" viewBox="0 8 40 44" xmlns="http://www.w3.org/2000/svg" aria-label="Matter" style="vertical-align:middle;">
173
216
  <g fill="currentColor" stroke="none">
@@ -187,9 +230,8 @@
187
230
  </g>
188
231
  </svg>
189
232
  <br/>
190
- <span style="font-size:18px; font-weight:600; letter-spacing:1px;">matter</span>
233
+ <span style="font-size:18px; font-weight:600; letter-spacing:1px;" data-i18n="matter-config.properties.title"></span>
191
234
  </p>
192
- <p><span data-i18n="matter-config.properties.intro"></span></p>
193
235
  </div>
194
236
 
195
237
  <div class="form-row">
@@ -210,9 +252,17 @@
210
252
 
211
253
  <hr/>
212
254
 
255
+ <div class="form-row">
256
+ <b><i class="fa fa-database"></i>&nbsp;<span data-i18n="matter-config.properties.storage_title"></span></b>
257
+ <button type="button" id="matter-storage-export" class="red-ui-button" style="margin-left:8px;"><i class="fa fa-download"></i>&nbsp;<span data-i18n="matter-config.properties.storage_export"></span></button>
258
+ <button type="button" id="matter-storage-import" class="red-ui-button"><i class="fa fa-upload"></i>&nbsp;<span data-i18n="matter-config.properties.storage_import"></span></button>
259
+ <input type="file" id="matter-storage-file" accept="application/json,.json" style="display:none;">
260
+ </div>
261
+
262
+ <hr/>
263
+
213
264
  <div class="form-row">
214
265
  <b><i class="fa fa-qrcode"></i>&nbsp;<span data-i18n="matter-config.properties.pairing_title"></span></b>
215
- <p><span data-i18n="matter-config.properties.pairing_help"></span></p>
216
266
  </div>
217
267
 
218
268
  <div class="form-row">
@@ -236,15 +286,6 @@
236
286
  </label>
237
287
  <input type="text" id="matter-pairing-name" placeholder="Kitchen plug" style="width:250px;">
238
288
  </div>
239
- <div class="form-tips" style="margin: 4px 0 8px; font-size:12px;">
240
- <i class="fa fa-circle-info" style="color:#1b7d33; margin-right:4px;"></i>
241
- <span data-i18n="matter-config.properties.pairing_hint">Prefer the QR code (MT:…): it carries the full device discriminator. The manual code
242
- stores only a short discriminator and cannot reliably tell identical devices apart
243
- (e.g. several Shelly of the same model), which can pair the wrong one or report it as
244
- already commissioned. Pair one device at a time; if a device is already paired
245
- elsewhere, factory-reset it first.</span>
246
- </div>
247
-
248
289
  <hr/>
249
290
 
250
291
  <div class="form-row">
@@ -270,9 +311,9 @@
270
311
  </div>
271
312
 
272
313
  <br/>
273
- <br/>
274
- <br/>
275
- <br/>
276
- <br/>
277
- <br/>
314
+ <br/>
315
+ <br/>
316
+ <br/>
317
+ <br/>
318
+ <br/>
278
319
  </script>
@@ -1,5 +1,8 @@
1
1
  /* eslint-disable max-len */
2
2
  const path = require('path')
3
+ const { exportMatterStorage, importMatterStorage } = require('./utils/matterStorageBackup')
4
+ const matterPackageVersion = require('@matter/main').version
5
+ const { Specification } = require('@matter/model')
3
6
 
4
7
  // 10/09/2024 Setup the color logger
5
8
  const loggerSetup = (options) => {
@@ -18,6 +21,7 @@ module.exports = (RED) => {
18
21
  node.sysLogger = null
19
22
  node.matterManager = null
20
23
  node.timerMatterConfigCheckState = null
24
+ node.storageOperation = null
21
25
  try {
22
26
  node.sysLogger = loggerSetup({ loglevel: node.loglevel, setPrefix: 'matter-config.js' })
23
27
  } catch (error) { console.log(error.stack) }
@@ -190,6 +194,35 @@ module.exports = (RED) => {
190
194
  if (node.matterManager === null) throw new Error('Matter controller not started')
191
195
  return node.matterManager.renameNode(_nodeId, _label)
192
196
  }
197
+
198
+ const withStoppedController = async (operation) => {
199
+ if (node.storageOperation !== null) throw new Error('Another Matter storage operation is already running')
200
+ node.storageOperation = (async () => {
201
+ if (node.matterManager !== null) await node.matterManager.close()
202
+ node.matterManager = null
203
+ try {
204
+ return await operation()
205
+ } finally {
206
+ await node.initMatterConnection()
207
+ }
208
+ })()
209
+ try { return await node.storageOperation } finally { node.storageOperation = null }
210
+ }
211
+
212
+ node.exportMatterStorage = () => withStoppedController(() => exportMatterStorage({
213
+ storagePath: node.matterStoragePath,
214
+ instanceId: node.matterInstanceId,
215
+ kind: 'controller',
216
+ protocolVersion: Specification.REVISION,
217
+ packageVersion: matterPackageVersion
218
+ }))
219
+
220
+ node.importMatterStorage = (backup) => withStoppedController(() => importMatterStorage({
221
+ storagePath: node.matterStoragePath,
222
+ instanceId: node.matterInstanceId,
223
+ kind: 'controller',
224
+ backup
225
+ }))
193
226
  // END functions called from the nodes -------------------------------------------------------------
194
227
 
195
228
  node.addClient = (_Node) => {
@@ -3,6 +3,7 @@
3
3
  <script type="text/javascript">
4
4
  RED.nodes.registerType('matterbridge-config', {
5
5
  category: 'config',
6
+ icon: 'node-matter-bridge-icon.svg',
6
7
  defaults: {
7
8
  name: { value: '' },
8
9
  bridgeDeviceName: { value: '' },
@@ -15,6 +16,21 @@
15
16
  var $pairingBody = $('#mbc-pairing-body');
16
17
  var $refreshPairing = $('#mbc-refresh-pairing');
17
18
  var $resetButton = $('#mbc-factory-reset');
19
+ var $storageFile = $('#mbc-storage-file');
20
+
21
+ function storageExportUrl() {
22
+ var adminRoot = (RED.settings && typeof RED.settings.httpAdminRoot === 'string') ? RED.settings.httpAdminRoot : '/';
23
+ adminRoot = String(adminRoot || '/').trim();
24
+ adminRoot = adminRoot === '/' ? '' : '/' + adminRoot.replace(/^\/+|\/+$/g, '');
25
+ var params = new URLSearchParams();
26
+ params.set('configId', node.id);
27
+ params.set('_', String(Date.now()));
28
+ try {
29
+ var tokens = (RED.settings && typeof RED.settings.get === 'function') ? RED.settings.get('auth-tokens') : null;
30
+ if (tokens && typeof tokens.access_token === 'string' && tokens.access_token.trim() !== '') params.set('access_token', tokens.access_token.trim());
31
+ } catch (error) { }
32
+ return adminRoot + '/KNXUltimateMatterBridgeStorageExport?' + params.toString();
33
+ }
18
34
 
19
35
  var t = function (key) {
20
36
  return RED._('node-red-contrib-knx-ultimate/matterbridge-config:matterbridge-config.properties.' + key);
@@ -37,7 +53,6 @@
37
53
  info.fabrics.forEach(function (f) {
38
54
  $pairingBody.append($('<div style="margin-left:10px;"></div>').text('• ' + (f.label || 'Controller') + ' (vendor ' + f.vendorId + ')'));
39
55
  });
40
- $pairingBody.append($('<p style="max-width:760px; margin:14px auto 0; color:#777;"></p>').text(t('commissioned_hint')));
41
56
  $resetButton.show();
42
57
  return;
43
58
  }
@@ -69,6 +84,26 @@
69
84
  loadPairingInfo();
70
85
  }).always(function () { $resetButton.prop('disabled', false); });
71
86
  });
87
+ $('#mbc-storage-export').on('click', function () {
88
+ window.location = storageExportUrl();
89
+ });
90
+ $('#mbc-storage-import').on('click', function () { $storageFile.trigger('click'); });
91
+ $storageFile.on('change', function () {
92
+ var file = this.files && this.files[0];
93
+ this.value = '';
94
+ if (!file || !window.confirm(t('storage_import_confirm'))) return;
95
+ var reader = new FileReader();
96
+ reader.onload = function () {
97
+ var backup;
98
+ try { backup = JSON.parse(reader.result); } catch (error) { RED.notify(error.message, { type: 'error' }); return; }
99
+ $('#mbc-storage-import').prop('disabled', true);
100
+ $.ajax({ url: 'KNXUltimateMatterBridgeStorageImport?configId=' + encodeURIComponent(node.id), method: 'POST', contentType: 'application/json', data: JSON.stringify(backup) })
101
+ .done(function () { RED.notify(t('storage_import_ok'), { type: 'success' }); loadPairingInfo(); })
102
+ .fail(function (xhr) { RED.notify((xhr.responseJSON && xhr.responseJSON.error) || xhr.statusText, { type: 'error', fixed: true }); })
103
+ .always(function () { $('#mbc-storage-import').prop('disabled', false); });
104
+ };
105
+ reader.readAsText(file);
106
+ });
72
107
 
73
108
  loadPairingInfo();
74
109
  },
@@ -87,8 +122,27 @@
87
122
  </div>
88
123
 
89
124
  <div class="form-row">
90
- <b><span data-i18n="matterbridge-config.properties.title"></span></b>
91
- <p><span data-i18n="matterbridge-config.properties.intro"></span></p>
125
+ <p align="center" style="margin:8px 0;">
126
+ <svg width="70" height="77" viewBox="0 8 40 44" xmlns="http://www.w3.org/2000/svg" aria-label="Matter" style="vertical-align:middle;">
127
+ <g fill="currentColor" stroke="none">
128
+ <circle cx="20" cy="30" r="4.5"/>
129
+ <g>
130
+ <rect x="18.4" y="9" width="3.2" height="11"/>
131
+ <path d="M 14.5 18.5 L 25.5 18.5 L 20 25.5 Z"/>
132
+ </g>
133
+ <g transform="rotate(120 20 30)">
134
+ <rect x="18.4" y="9" width="3.2" height="11"/>
135
+ <path d="M 14.5 18.5 L 25.5 18.5 L 20 25.5 Z"/>
136
+ </g>
137
+ <g transform="rotate(240 20 30)">
138
+ <rect x="18.4" y="9" width="3.2" height="11"/>
139
+ <path d="M 14.5 18.5 L 25.5 18.5 L 20 25.5 Z"/>
140
+ </g>
141
+ </g>
142
+ </svg>
143
+ <br/>
144
+ <span style="font-size:18px; font-weight:600; letter-spacing:1px;" data-i18n="matterbridge-config.properties.title"></span>
145
+ </p>
92
146
  </div>
93
147
 
94
148
  <div class="form-row">
@@ -109,6 +163,15 @@
109
163
 
110
164
  <hr/>
111
165
 
166
+ <div class="form-row">
167
+ <b><i class="fa fa-database"></i>&nbsp;<span data-i18n="matterbridge-config.properties.storage_title"></span></b>
168
+ <button type="button" id="mbc-storage-export" class="red-ui-button" style="margin-left:8px;"><i class="fa fa-download"></i>&nbsp;<span data-i18n="matterbridge-config.properties.storage_export"></span></button>
169
+ <button type="button" id="mbc-storage-import" class="red-ui-button"><i class="fa fa-upload"></i>&nbsp;<span data-i18n="matterbridge-config.properties.storage_import"></span></button>
170
+ <input type="file" id="mbc-storage-file" accept="application/json,.json" style="display:none;">
171
+ </div>
172
+
173
+ <hr/>
174
+
112
175
  <div class="form-row">
113
176
  <b><i class="fa fa-qrcode"></i>&nbsp;<span data-i18n="matterbridge-config.properties.pairing_title"></span></b>
114
177
  <button type="button" id="mbc-refresh-pairing" class="red-ui-button" style="margin-left:6px; color:#1b7d33; border-color:#1b7d33;">
@@ -119,4 +182,11 @@
119
182
  </button>
120
183
  </div>
121
184
  <div class="form-row" id="mbc-pairing-body" style="text-align:center;"></div>
185
+
186
+ <br/>
187
+ <br/>
188
+ <br/>
189
+ <br/>
190
+ <br/>
191
+ <br/>
122
192
  </script>
@@ -1,5 +1,8 @@
1
1
  /* eslint-disable max-len */
2
2
  const path = require('path')
3
+ const { exportMatterStorage, importMatterStorage } = require('./utils/matterStorageBackup')
4
+ const matterPackageVersion = require('@matter/main').version
5
+ const { Specification } = require('@matter/model')
3
6
 
4
7
  // 10/09/2024 Setup the color logger
5
8
  const loggerSetup = (options) => {
@@ -39,6 +42,7 @@ module.exports = (RED) => {
39
42
  let startupTimer = null
40
43
  let reconcileTimer = null
41
44
  let closing = false
45
+ let storageOperation = null
42
46
 
43
47
  Object.defineProperty(node, 'linkStatus', {
44
48
  get: function () {
@@ -193,6 +197,40 @@ module.exports = (RED) => {
193
197
  await node.matterBridge.factoryReset()
194
198
  notifyClientsStatus()
195
199
  }
200
+
201
+ const withStoppedBridge = async (operation) => {
202
+ if (storageOperation !== null) throw new Error('Another Matter storage operation is already running')
203
+ storageOperation = (async () => {
204
+ const engine = node.matterBridge
205
+ if (engine !== null) {
206
+ engine.removeAllListeners()
207
+ await engine.close()
208
+ runningBridges.delete(node.id)
209
+ node.matterBridge = null
210
+ }
211
+ try {
212
+ return await operation()
213
+ } finally {
214
+ await ensureEngineStarted()
215
+ }
216
+ })()
217
+ try { return await storageOperation } finally { storageOperation = null }
218
+ }
219
+
220
+ node.exportMatterStorage = () => withStoppedBridge(() => exportMatterStorage({
221
+ storagePath: node.matterStoragePath,
222
+ instanceId: node.matterInstanceId,
223
+ kind: 'bridge',
224
+ protocolVersion: Specification.REVISION,
225
+ packageVersion: matterPackageVersion
226
+ }))
227
+
228
+ node.importMatterStorage = (backup) => withStoppedBridge(() => importMatterStorage({
229
+ storagePath: node.matterStoragePath,
230
+ instanceId: node.matterInstanceId,
231
+ kind: 'bridge',
232
+ backup
233
+ }))
196
234
  // END functions called from the device nodes -------------------------------------
197
235
 
198
236
  node.on('close', (removed, done) => {
@@ -0,0 +1,100 @@
1
+ const fs = require('fs/promises')
2
+ const path = require('path')
3
+
4
+ const BACKUP_FORMAT = 'knx-ultimate-matter-storage'
5
+ const BACKUP_VERSION = 1
6
+ const MAX_BACKUP_BYTES = 64 * 1024 * 1024
7
+
8
+ const instancePath = (storagePath, instanceId) => path.join(storagePath, instanceId)
9
+
10
+ const listFiles = async (root, current = root) => {
11
+ const entries = await fs.readdir(current, { withFileTypes: true })
12
+ const files = []
13
+ for (const entry of entries) {
14
+ if (entry.isSymbolicLink()) throw new Error('Matter storage contains an unsupported symbolic link')
15
+ const absolute = path.join(current, entry.name)
16
+ if (entry.isDirectory()) files.push(...await listFiles(root, absolute))
17
+ else if (entry.isFile()) files.push(path.relative(root, absolute).split(path.sep).join('/'))
18
+ }
19
+ return files.sort()
20
+ }
21
+
22
+ const exportMatterStorage = async ({ storagePath, instanceId, kind, protocolVersion, packageVersion }) => {
23
+ const root = instancePath(storagePath, instanceId)
24
+ let names
25
+ try {
26
+ names = await listFiles(root)
27
+ } catch (error) {
28
+ if (error.code === 'ENOENT') names = []
29
+ else throw error
30
+ }
31
+ const files = []
32
+ let totalBytes = 0
33
+ for (const name of names) {
34
+ const content = await fs.readFile(path.join(root, ...name.split('/')))
35
+ totalBytes += content.length
36
+ if (totalBytes > MAX_BACKUP_BYTES) throw new Error('Matter backup exceeds the 64 MB safety limit')
37
+ files.push({ path: name, content: content.toString('base64') })
38
+ }
39
+ return {
40
+ format: BACKUP_FORMAT,
41
+ version: BACKUP_VERSION,
42
+ kind,
43
+ createdAt: new Date().toISOString(),
44
+ sourceInstanceId: instanceId,
45
+ matterProtocolVersion: protocolVersion,
46
+ matterPackageVersion: packageVersion,
47
+ files
48
+ }
49
+ }
50
+
51
+ const validateRelativePath = (name) => {
52
+ if (typeof name !== 'string' || name === '' || name.includes('\\')) return false
53
+ const normalized = path.posix.normalize(name)
54
+ return normalized === name && !path.posix.isAbsolute(name) && name !== '..' && !name.startsWith('../')
55
+ }
56
+
57
+ const importMatterStorage = async ({ storagePath, instanceId, kind, backup }) => {
58
+ if (!backup || backup.format !== BACKUP_FORMAT || backup.version !== BACKUP_VERSION) throw new Error('Unsupported Matter backup file')
59
+ if (backup.kind !== kind) throw new Error(`This is a Matter ${backup.kind || 'unknown'} backup, not a ${kind} backup`)
60
+ if (!Array.isArray(backup.files)) throw new Error('Invalid Matter backup: files are missing')
61
+
62
+ const decoded = []
63
+ const seen = new Set()
64
+ let totalBytes = 0
65
+ for (const file of backup.files) {
66
+ if (!file || !validateRelativePath(file.path) || seen.has(file.path) || typeof file.content !== 'string') throw new Error('Invalid Matter backup file entry')
67
+ const content = Buffer.from(file.content, 'base64')
68
+ totalBytes += content.length
69
+ if (totalBytes > MAX_BACKUP_BYTES) throw new Error('Matter backup exceeds the 64 MB safety limit')
70
+ seen.add(file.path)
71
+ decoded.push({ path: file.path, content })
72
+ }
73
+
74
+ await fs.mkdir(storagePath, { recursive: true })
75
+ const target = instancePath(storagePath, instanceId)
76
+ const temporary = `${target}.import-${process.pid}-${Date.now()}`
77
+ const previous = `${target}.previous-${process.pid}-${Date.now()}`
78
+ await fs.mkdir(temporary, { recursive: true })
79
+ try {
80
+ for (const file of decoded) {
81
+ const destination = path.join(temporary, ...file.path.split('/'))
82
+ await fs.mkdir(path.dirname(destination), { recursive: true })
83
+ await fs.writeFile(destination, file.content, { mode: 0o600 })
84
+ }
85
+ let hadTarget = true
86
+ try { await fs.rename(target, previous) } catch (error) { if (error.code === 'ENOENT') hadTarget = false; else throw error }
87
+ try {
88
+ await fs.rename(temporary, target)
89
+ } catch (error) {
90
+ if (hadTarget) await fs.rename(previous, target)
91
+ throw error
92
+ }
93
+ if (hadTarget) await fs.rm(previous, { recursive: true, force: true })
94
+ } catch (error) {
95
+ await fs.rm(temporary, { recursive: true, force: true })
96
+ throw error
97
+ }
98
+ }
99
+
100
+ module.exports = { exportMatterStorage, importMatterStorage, BACKUP_FORMAT, BACKUP_VERSION, MAX_BACKUP_BYTES }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "engines": {
4
4
  "node": ">=20.18.1"
5
5
  },
6
- "version": "6.0.5",
6
+ "version": "6.0.6",
7
7
  "description": "Control your KNX and KNX Secure intallation via Node-Red! A bunch of KNX nodes, with integrated Philips HUE control, ETS group address importer, KNX AI for diagnosticsand KNX routing between interfaces. Easy to use and highly configurable.",
8
8
  "files": [
9
9
  "nodes/",
@@ -1,6 +1,17 @@
1
1
  /* global window, document, MutationObserver, $, jQuery */
2
2
  /* eslint-disable camelcase */
3
3
 
4
+ // Keep informational tips aligned with the full width of our Node-RED editor forms.
5
+ // Node-RED applies a max-width to .form-tips by default, which otherwise leaves
6
+ // inconsistent empty space in wider edit dialogs.
7
+ ;(function ensureKnxUltimateEditorStyles () {
8
+ if (document.getElementById('knx-ultimate-editor-styles')) return
9
+ const style = document.createElement('style')
10
+ style.id = 'knx-ultimate-editor-styles'
11
+ style.textContent = '.form-tips { width: 100% !important; max-width: none !important; box-sizing: border-box !important; background-color: #e8f5e9 !important; }'
12
+ document.head.appendChild(style)
13
+ })()
14
+
4
15
  // 31/03/2020 Search Helper
5
16
  function htmlUtilsfullCSVSearch (sourceText, searchString) {
6
17
  let aSearchWords = []