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
@@ -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
  |字段|说明|
@@ -28,7 +30,7 @@
28
30
  | RGB 彩色灯 | + RGB 颜色命令/状态 GA (DPT 232.600)。Matter 颜色(hue/saturation 或 XY,来自 App 的色轮)与 KNX RGB 三元组相互转换 |
29
31
  | 色温灯 | + 色温命令/状态 GA,开尔文 (DPT 7.600) |
30
32
  | 窗帘/卷帘 | 上/下 (DPT 1.008)、停止 (DPT 1.017)、位置 % 命令/状态 (DPT 5.001)、可选位置反转 |
31
- | 温控器(采暖) | 当前温度 GA、设定点命令/状态 GA (DPT 9.001) |
33
+ | 温控器(采暖和/或制冷) | 当前温度 GA、设定点命令/状态 GA (DPT 9.001)。如再配置制冷设定点命令/状态 GA,则同时提供 Cool 模式(双设定点) |
32
34
  | 风扇/新风 | 速度 % 命令/状态 GA (DPT 5.001) |
33
35
  | 传感器(温度、湿度、光照、人体、门磁) | 各一个状态 GA |
34
36
  | 烟雾/CO 报警器 | 烟雾报警状态 GA + 可选 CO 报警状态 GA (DPT 1.005):手机上的紧急通知 |
@@ -41,7 +43,7 @@
41
43
 
42
44
  ## 高级兼容性
43
45
 
44
- 这些选项只会在适用于所选类型时显示。调光设备可以忽略某些控制器在 `On` 后立即发送的亮度命令。窗帘可以在命令后乐观更新 Matter 位置,并在 KNX 状态 GA 报告真实位置时再修正。
46
+ 这些选项只会在适用于所选类型时显示。调光设备可以忽略某些控制器在 `On` 后立即发送的亮度命令。对于窗帘,**交换打开 / 关闭**会同时反转 KNX 二进制命令和百分比方向。**窗帘滑块防抖**会在写入 KNX 前合并快速的中间目标:`0` 使用自适应窗口(首个命令 400 ms,后续命令 150 ms);`1`–`5000` 使用固定窗口。窗帘还可先乐观更新 Matter 位置,再由 KNX 状态 GA 修正。
45
47
 
46
48
  ## 节点引脚
47
49
 
@@ -17,24 +17,25 @@
17
17
  "name_placeholder": "例如 厨房灯",
18
18
  "invert": "反转位置 %"
19
19
  },
20
- "advanced": {
20
+ "advanced": {
21
21
  "title": "高级兼容性",
22
22
  "help": " 仅当 Matter 控制器或 KNX 执行器需要特殊处理时使用这些选项。",
23
+ "alexa_dedup_ms": "Alexa 去重窗口 (毫秒, 0 = 关闭)",
23
24
  "turn_on_behavior": "开灯行为",
24
25
  "turn_on_ignore_level": "忽略 On 后立即发送的亮度",
25
26
  "turn_on_forward_all": "转发每个亮度命令",
26
- "ignore_level_ms": "On 后忽略亮度时间 (ms)",
27
- "cover_expose_as_light": "将窗帘显示为可调光灯(Alexa 兼容方案)",
28
- "cover_expose_as_light_help": "仅当 Alexa 无法设置窗帘百分比时使用。亮度表示窗帘开启程度;KNX 组地址保持不变。",
27
+ "ignore_level_ms": "On 后忽略亮度时间 (ms)",
28
+ "cover_swap_open_close": "交换窗帘打开 / 关闭",
29
+ "cover_slider_debounce": "窗帘滑块防抖(毫秒,0 = 自适应)",
29
30
  "cover_update_mode": "窗帘状态更新",
30
31
  "cover_optimistic": "乐观更新 Matter 位置",
31
32
  "cover_wait_status": "仅等待 KNX 状态",
32
- "cover_status_timeout": "窗帘状态超时 (ms)"
33
- },
34
- "tabs": {
35
- "mappings": "KNX 映射",
36
- "advanced": "高级选项"
37
- },
33
+ "cover_status_timeout": "窗帘状态超时 (ms)"
34
+ },
35
+ "tabs": {
36
+ "mappings": "KNX 映射",
37
+ "advanced": "高级选项"
38
+ },
38
39
  "functions": {
39
40
  "fn_onoff_cmd": "开/关 命令 GA",
40
41
  "fn_onoff_status": "开/关 状态 GA",
@@ -48,6 +49,8 @@
48
49
  "fn_currenttemp": "当前温度 GA",
49
50
  "fn_setpoint_cmd": "设定点 命令 GA",
50
51
  "fn_setpoint_status": "设定点 状态 GA",
52
+ "fn_coolingsetpoint_cmd": "制冷设定点 命令 GA",
53
+ "fn_coolingsetpoint_status": "制冷设定点 状态 GA",
51
54
  "fn_rgb_cmd": "RGB 颜色命令 GA (DPT 232.600)",
52
55
  "fn_rgb_status": "RGB 颜色状态 GA (DPT 232.600)",
53
56
  "fn_ct_cmd": "色温命令 GA(开尔文)",
@@ -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) => {