matterbridge 1.2.12 → 1.2.14

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 (36) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +9 -4
  3. package/dist/BridgedDeviceBasicInformationCluster.d.ts +223 -0
  4. package/dist/BridgedDeviceBasicInformationCluster.d.ts.map +1 -0
  5. package/dist/BridgedDeviceBasicInformationCluster.js +176 -0
  6. package/dist/BridgedDeviceBasicInformationCluster.js.map +1 -0
  7. package/dist/defaultConfigSchema.d.ts +28 -0
  8. package/dist/defaultConfigSchema.d.ts.map +1 -0
  9. package/dist/defaultConfigSchema.js +217 -0
  10. package/dist/defaultConfigSchema.js.map +1 -0
  11. package/dist/matterbridge.d.ts +22 -0
  12. package/dist/matterbridge.d.ts.map +1 -1
  13. package/dist/matterbridge.js +284 -70
  14. package/dist/matterbridge.js.map +1 -1
  15. package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -1
  16. package/dist/matterbridgeAccessoryPlatform.js.map +1 -1
  17. package/dist/matterbridgeDevice.d.ts +60 -15
  18. package/dist/matterbridgeDevice.d.ts.map +1 -1
  19. package/dist/matterbridgeDevice.js +7 -3
  20. package/dist/matterbridgeDevice.js.map +1 -1
  21. package/dist/matterbridgePlatform.d.ts +1 -0
  22. package/dist/matterbridgePlatform.d.ts.map +1 -1
  23. package/dist/matterbridgePlatform.js +1 -0
  24. package/dist/matterbridgePlatform.js.map +1 -1
  25. package/frontend/build/asset-manifest.json +6 -6
  26. package/frontend/build/index.html +1 -1
  27. package/frontend/build/static/css/main.979e07d2.css +2 -0
  28. package/frontend/build/static/css/main.979e07d2.css.map +1 -0
  29. package/frontend/build/static/js/main.ed3f81ca.js +3 -0
  30. package/frontend/build/static/js/main.ed3f81ca.js.map +1 -0
  31. package/package.json +14 -8
  32. package/frontend/build/static/css/main.1880392b.css +0 -2
  33. package/frontend/build/static/css/main.1880392b.css.map +0 -1
  34. package/frontend/build/static/js/main.0c70c26b.js +0 -3
  35. package/frontend/build/static/js/main.0c70c26b.js.map +0 -1
  36. /package/frontend/build/static/js/{main.0c70c26b.js.LICENSE.txt → main.ed3f81ca.js.LICENSE.txt} +0 -0
@@ -32,8 +32,9 @@ import express from 'express';
32
32
  import os from 'os';
33
33
  import path from 'path';
34
34
  import WebSocket, { WebSocketServer } from 'ws';
35
+ import { BridgedDeviceBasicInformation, BridgedDeviceBasicInformationCluster } from './BridgedDeviceBasicInformationCluster.js';
35
36
  import { CommissioningController, CommissioningServer, MatterServer } from '@project-chip/matter-node.js';
36
- import { BasicInformationCluster, BooleanStateCluster, BridgedDeviceBasicInformation, BridgedDeviceBasicInformationCluster, ClusterServer, FixedLabelCluster, GeneralCommissioning, PowerSourceCluster, ThreadNetworkDiagnosticsCluster, getClusterNameById, } from '@project-chip/matter-node.js/cluster';
37
+ import { BasicInformationCluster, BooleanStateCluster, ClusterServer, FixedLabelCluster, GeneralCommissioning, GeneralDiagnostics, GeneralDiagnosticsCluster, PowerSourceCluster, ThreadNetworkDiagnosticsCluster, getClusterNameById, } from '@project-chip/matter-node.js/cluster';
37
38
  import { DeviceTypeId, VendorId } from '@project-chip/matter-node.js/datatype';
38
39
  import { Aggregator, DeviceTypes, NodeStateInformation } from '@project-chip/matter-node.js/device';
39
40
  import { Format, Level, Logger } from '@project-chip/matter-node.js/log';
@@ -41,6 +42,7 @@ import { ManualPairingCodeCodec, QrCodeSchema } from '@project-chip/matter-node.
41
42
  import { StorageBackendDisk, StorageBackendJsonFile, StorageManager } from '@project-chip/matter-node.js/storage';
42
43
  import { requireMinNodeVersion, getParameter, getIntParameter, hasParameter } from '@project-chip/matter-node.js/util';
43
44
  import { CryptoNode } from '@project-chip/matter-node.js/crypto';
45
+ import { somfytahoma_config, somfytahoma_schema, zigbee2mqtt_config, zigbee2mqtt_schema } from './defaultConfigSchema.js';
44
46
  const plg = '\u001B[38;5;33m';
45
47
  const dev = '\u001B[38;5;79m';
46
48
  const typ = '\u001B[38;5;207m';
@@ -49,6 +51,7 @@ const typ = '\u001B[38;5;207m';
49
51
  */
50
52
  export class Matterbridge extends EventEmitter {
51
53
  systemInformation = {
54
+ macAddress: '',
52
55
  ipv4Address: '',
53
56
  ipv6Address: '',
54
57
  nodeVersion: '',
@@ -156,7 +159,7 @@ export class Matterbridge extends EventEmitter {
156
159
  addedDevices: 0,
157
160
  };
158
161
  this.registeredPlugins.push(plugin);
159
- await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
162
+ await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
160
163
  // Log system info and create .matterbridge directory
161
164
  await this.logNodeAndSystemInfo();
162
165
  this.matterbridgeDirectory = dataPath;
@@ -498,7 +501,7 @@ export class Matterbridge extends EventEmitter {
498
501
  const plugin = { path: packageJsonPath, type: '', name: packageJson.name, version: packageJson.version, description: packageJson.description, author: packageJson.author, enabled: true };
499
502
  if (await this.loadPlugin(plugin)) {
500
503
  this.registeredPlugins.push(plugin);
501
- await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
504
+ await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
502
505
  this.log.info(`Plugin ${plg}${packageJsonPath}${nf} type ${plugin.type} added to matterbridge`);
503
506
  }
504
507
  else {
@@ -512,7 +515,7 @@ export class Matterbridge extends EventEmitter {
512
515
  else if (mode === 'remove') {
513
516
  if (this.registeredPlugins.find((registeredPlugin) => registeredPlugin.name === packageJson.name)) {
514
517
  this.registeredPlugins.splice(this.registeredPlugins.findIndex((registeredPlugin) => registeredPlugin.name === packageJson.name), 1);
515
- await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
518
+ await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
516
519
  this.log.info(`Plugin ${plg}${packageJsonPath}${nf} removed from matterbridge`);
517
520
  }
518
521
  else {
@@ -527,7 +530,7 @@ export class Matterbridge extends EventEmitter {
527
530
  plugin.started = undefined;
528
531
  plugin.configured = undefined;
529
532
  plugin.connected = undefined;
530
- await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
533
+ await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
531
534
  this.log.info(`Plugin ${plg}${packageJsonPath}${nf} enabled`);
532
535
  }
533
536
  else {
@@ -542,7 +545,7 @@ export class Matterbridge extends EventEmitter {
542
545
  plugin.started = undefined;
543
546
  plugin.configured = undefined;
544
547
  plugin.connected = undefined;
545
- await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
548
+ await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
546
549
  this.log.info(`Plugin ${plg}${packageJsonPath}${nf} disabled`);
547
550
  }
548
551
  else {
@@ -564,7 +567,7 @@ export class Matterbridge extends EventEmitter {
564
567
  if (!context)
565
568
  this.log.error(`Plugin ${plg}${plugin.name}${er} context not found`);
566
569
  await context?.clearAll();
567
- await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
570
+ await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
568
571
  this.log.info(`Reset commissionig for plugin ${plg}${plugin.name}${nf} done! Remove the device from the controller.`);
569
572
  }
570
573
  else {
@@ -655,7 +658,7 @@ export class Matterbridge extends EventEmitter {
655
658
  if (plugin.platform) {
656
659
  try {
657
660
  await plugin.platform.onShutdown('Matterbridge is closing: ' + message);
658
- await this.savePluginConfig(plugin);
661
+ // await this.savePluginConfig(plugin);
659
662
  }
660
663
  catch (error) {
661
664
  this.log.error(`Plugin ${plg}${plugin.name}${er} shutting down error: ${error}`);
@@ -991,7 +994,8 @@ export class Matterbridge extends EventEmitter {
991
994
  }
992
995
  }
993
996
  catch (error) {
994
- this.log.error('Storage initialize() error!');
997
+ this.log.error('Storage initialize() error! The file .matterbridge/matterbridge.json may be corrupted.');
998
+ this.log.error('Please delete it and rename matterbridge.backup.json to matterbridge.json and try to restart Matterbridge.');
995
999
  await this.cleanup('Storage initialize() error!');
996
1000
  }
997
1001
  }
@@ -1122,6 +1126,97 @@ export class Matterbridge extends EventEmitter {
1122
1126
  }, 60 * 1000);
1123
1127
  }, 1000);
1124
1128
  }
1129
+ /**
1130
+ * Loads the schema for a plugin.
1131
+ * If the schema file exists, it reads the file and returns the parsed JSON data.
1132
+ * If the schema file does not exist, it creates a new file with default configuration and returns it.
1133
+ * If any error occurs during file access or creation, it logs an error and return an empty schema.
1134
+ *
1135
+ * @param plugin - The plugin for which to load the schema.
1136
+ * @returns A promise that resolves to the loaded or created schema.
1137
+ */
1138
+ async loadPluginSchema(plugin) {
1139
+ const schemaFile = path.join(this.matterbridgeDirectory, `${plugin.name}.schema.json`);
1140
+ try {
1141
+ await fs.access(schemaFile);
1142
+ const data = await fs.readFile(schemaFile, 'utf8');
1143
+ const schema = JSON.parse(data);
1144
+ schema.title = plugin.description;
1145
+ schema.description = plugin.name + ' v. ' + plugin.version + ' by ' + plugin.author;
1146
+ this.log.debug(`Schema file found: ${schemaFile}.\nSchema:${rs}\n`, schema);
1147
+ return schema;
1148
+ }
1149
+ catch (err) {
1150
+ if (err instanceof Error) {
1151
+ const nodeErr = err;
1152
+ if (nodeErr.code === 'ENOENT') {
1153
+ let schema;
1154
+ if (plugin.name === 'matterbridge-zigbee2mqtt')
1155
+ schema = zigbee2mqtt_schema;
1156
+ else if (plugin.name === 'matterbridge-somfy-tahoma')
1157
+ schema = somfytahoma_schema;
1158
+ else
1159
+ schema = {
1160
+ title: plugin.description,
1161
+ description: plugin.name + ' v. ' + plugin.version + ' by ' + plugin.author,
1162
+ type: 'object',
1163
+ properties: {
1164
+ name: {
1165
+ description: 'Plugin name',
1166
+ type: 'string',
1167
+ readOnly: true,
1168
+ },
1169
+ type: {
1170
+ description: 'Plugin type',
1171
+ type: 'string',
1172
+ readOnly: true,
1173
+ },
1174
+ unregisterOnShutdown: {
1175
+ description: 'Unregister all devices on shutdown (development only)',
1176
+ type: 'boolean',
1177
+ },
1178
+ },
1179
+ };
1180
+ try {
1181
+ await this.writeFile(schemaFile, JSON.stringify(schema, null, 2));
1182
+ this.log.debug(`Created schema file: ${schemaFile}.\nSchema:${rs}\n`, schema);
1183
+ return schema;
1184
+ }
1185
+ catch (err) {
1186
+ this.log.error(`Error creating schema file ${schemaFile}: ${err}`);
1187
+ return schema;
1188
+ }
1189
+ }
1190
+ else {
1191
+ this.log.error(`Error accessing schema file ${schemaFile}: ${err}`);
1192
+ return {};
1193
+ }
1194
+ }
1195
+ this.log.error(`Error loading schema file ${schemaFile}: ${err}`);
1196
+ return {};
1197
+ }
1198
+ }
1199
+ /**
1200
+ * Saves the plugin configuration to a JSON file.
1201
+ * @param plugin - The registered plugin.
1202
+ * @param config - The platform configuration.
1203
+ * @returns A promise that resolves when the configuration is saved successfully, or rejects with an error.
1204
+ */
1205
+ async savePluginConfigFromJson(plugin, config) {
1206
+ if (!config.name || !config.type || config.name !== plugin.name) {
1207
+ this.log.error(`Error saving plugin ${plg}${plugin.name}${er} config`);
1208
+ return;
1209
+ }
1210
+ const configFile = path.join(this.matterbridgeDirectory, `${plugin.name}.config.json`);
1211
+ try {
1212
+ await this.writeFile(configFile, JSON.stringify(config, null, 2));
1213
+ this.log.debug(`Saved config file: ${configFile}.\nConfig:${rs}\n`, config);
1214
+ }
1215
+ catch (err) {
1216
+ this.log.error(`Error saving plugin ${plg}${plugin.name}${er} config: ${err}`);
1217
+ return;
1218
+ }
1219
+ }
1125
1220
  /**
1126
1221
  * Loads the configuration for a plugin.
1127
1222
  * If the configuration file exists, it reads the file and returns the parsed JSON data.
@@ -1147,22 +1242,30 @@ export class Matterbridge extends EventEmitter {
1147
1242
  if (err instanceof Error) {
1148
1243
  const nodeErr = err;
1149
1244
  if (nodeErr.code === 'ENOENT') {
1245
+ let config;
1246
+ if (plugin.name === 'matterbridge-zigbee2mqtt')
1247
+ config = zigbee2mqtt_config;
1248
+ else if (plugin.name === 'matterbridge-somfy-tahoma')
1249
+ config = somfytahoma_config;
1250
+ else
1251
+ config = { name: plugin.name, type: plugin.type, unregisterOnShutdown: false };
1150
1252
  try {
1151
- await this.writeFile(configFile, JSON.stringify({ name: plugin.name, type: plugin.type }, null, 2));
1152
- this.log.debug(`Created config file: ${configFile}.\nConfig:${rs}\n`, { name: plugin.name, type: plugin.type });
1153
- return { name: plugin.name, type: plugin.type };
1253
+ await this.writeFile(configFile, JSON.stringify(config, null, 2));
1254
+ this.log.debug(`Created config file: ${configFile}.\nConfig:${rs}\n`, config);
1255
+ return config;
1154
1256
  }
1155
1257
  catch (err) {
1156
1258
  this.log.error(`Error creating config file ${configFile}: ${err}`);
1157
- return Promise.reject(err);
1259
+ return config;
1158
1260
  }
1159
1261
  }
1160
1262
  else {
1161
1263
  this.log.error(`Error accessing config file ${configFile}: ${err}`);
1162
- return Promise.reject(err);
1264
+ return {};
1163
1265
  }
1164
1266
  }
1165
- return Promise.reject(err);
1267
+ this.log.error(`Error loading config file ${configFile}: ${err}`);
1268
+ return {};
1166
1269
  }
1167
1270
  }
1168
1271
  /**
@@ -1266,6 +1369,7 @@ export class Matterbridge extends EventEmitter {
1266
1369
  .then(() => {
1267
1370
  plugin.configured = true;
1268
1371
  this.log.info(`Configured plugin ${plg}${plugin.name}${db} type ${typ}${plugin.type}${db}`);
1372
+ this.savePluginConfig(plugin);
1269
1373
  return Promise.resolve();
1270
1374
  })
1271
1375
  .catch((err) => {
@@ -1297,7 +1401,7 @@ export class Matterbridge extends EventEmitter {
1297
1401
  this.log.error(`Plugin ${plg}${plugin.name}${er} already loaded`);
1298
1402
  return Promise.resolve(plugin.platform);
1299
1403
  }
1300
- this.log.info(`Loading plugin ${plg}${plugin.name}${db} type ${typ}${plugin.type}${db}`);
1404
+ this.log.info(`Loading plugin ${plg}${plugin.name}${nf} type ${typ}${plugin.type}${nf}`);
1301
1405
  try {
1302
1406
  // Load the package.json of the plugin
1303
1407
  const packageJson = JSON.parse(await fs.readFile(plugin.path, 'utf8'));
@@ -1316,6 +1420,7 @@ export class Matterbridge extends EventEmitter {
1316
1420
  const platform = pluginInstance.default(this, log, config);
1317
1421
  platform.name = packageJson.name;
1318
1422
  platform.config = config;
1423
+ platform.version = packageJson.version;
1319
1424
  plugin.name = packageJson.name;
1320
1425
  plugin.description = packageJson.description;
1321
1426
  plugin.version = packageJson.version;
@@ -1325,8 +1430,20 @@ export class Matterbridge extends EventEmitter {
1325
1430
  plugin.loaded = true;
1326
1431
  plugin.registeredDevices = 0;
1327
1432
  plugin.addedDevices = 0;
1328
- await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
1329
- this.log.info(`Loaded plugin ${plg}${plugin.name}${db} type ${typ}${platform.type}${db} (entrypoint ${UNDERLINE}${pluginEntry}${UNDERLINEOFF})`);
1433
+ await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
1434
+ this.getLatestVersion(plugin.name)
1435
+ .then(async (latestVersion) => {
1436
+ plugin.latestVersion = latestVersion;
1437
+ await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
1438
+ if (plugin.version !== latestVersion)
1439
+ this.log.warn(`The plugin ${plg}${plugin.name}${wr} is out of date. Current version: ${plugin.version}, Latest version: ${latestVersion}`);
1440
+ else
1441
+ this.log.info(`The plugin ${plg}${plugin.name}${nf} is up to date. Current version: ${plugin.version}, Latest version: ${latestVersion}`);
1442
+ })
1443
+ .catch((error) => {
1444
+ this.log.error(`Error getting ${plugin.name} latest version: ${error}`);
1445
+ });
1446
+ this.log.info(`Loaded plugin ${plg}${plugin.name}${nf} type ${typ}${platform.type} ${db}(entrypoint ${UNDERLINE}${pluginEntry}${UNDERLINEOFF})`);
1330
1447
  if (start)
1331
1448
  this.startPlugin(plugin, message); // No await do it asyncronously
1332
1449
  return Promise.resolve(platform);
@@ -1555,8 +1672,6 @@ export class Matterbridge extends EventEmitter {
1555
1672
  this.log.error(`Node storage context undefined for ${plg}Matterbridge${er}`);
1556
1673
  return;
1557
1674
  }
1558
- await this.matterbridgeContext.set('softwareVersion', 1);
1559
- await this.matterbridgeContext.set('softwareVersionString', this.matterbridgeVersion);
1560
1675
  this.log.debug(`Creating commissioning server for ${plg}Matterbridge${db}`);
1561
1676
  this.commissioningServer = await this.createCommisioningServer(this.matterbridgeContext, 'Matterbridge');
1562
1677
  this.log.debug(`Creating matter aggregator for ${plg}Matterbridge${db}`);
@@ -1568,6 +1683,7 @@ export class Matterbridge extends EventEmitter {
1568
1683
  await this.startMatterServer();
1569
1684
  this.log.info('Matter server started');
1570
1685
  await this.showCommissioningQRCode(this.commissioningServer, this.matterbridgeContext, this.nodeContext, 'Matterbridge');
1686
+ // logEndpoint(this.commissioningServer.getRootEndpoint());
1571
1687
  //if (hasParameter('advertise')) await this.commissioningServer.advertise();
1572
1688
  setTimeout(() => {
1573
1689
  this.log.info(`Setting reachability to true for ${plg}Matterbridge${db}`);
@@ -1606,8 +1722,7 @@ export class Matterbridge extends EventEmitter {
1606
1722
  continue;
1607
1723
  if (!plugin.storageContext)
1608
1724
  plugin.storageContext = await this.importCommissioningServerContext(plugin.name, registeredDevice.device);
1609
- await plugin.storageContext.set('softwareVersion', 1);
1610
- await plugin.storageContext.set('softwareVersionString', this.matterbridgeVersion);
1725
+ this.log.debug(`Creating commissioning server for ${plg}${plugin.name}${db}`);
1611
1726
  if (!plugin.commissioningServer)
1612
1727
  plugin.commissioningServer = await this.createCommisioningServer(plugin.storageContext, plugin.name);
1613
1728
  this.log.debug(`Adding device ${dev}${registeredDevice.device.name}${db} to commissioning server for plugin ${plg}${plugin.name}${db}`);
@@ -1619,10 +1734,9 @@ export class Matterbridge extends EventEmitter {
1619
1734
  await this.matterServer?.addCommissioningServer(plugin.commissioningServer, { uniqueStorageKey: plugin.name });
1620
1735
  }
1621
1736
  if (plugin.type === 'DynamicPlatform') {
1622
- // eslint-disable-next-line max-len
1737
+ this.log.debug(`Creating commissioning server context for ${plg}${plugin.name}${db}`);
1623
1738
  plugin.storageContext = await this.createCommissioningServerContext(plugin.name, 'Matterbridge', DeviceTypes.AGGREGATOR.code, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge Dynamic Platform');
1624
- await plugin.storageContext.set('softwareVersion', 1);
1625
- await plugin.storageContext.set('softwareVersionString', this.matterbridgeVersion);
1739
+ this.log.debug(`Creating commissioning server for ${plg}${plugin.name}${db}`);
1626
1740
  plugin.commissioningServer = await this.createCommisioningServer(plugin.storageContext, plugin.name);
1627
1741
  this.log.debug(`Creating aggregator for plugin ${plg}${plugin.name}${db}`);
1628
1742
  plugin.aggregator = await this.createMatterAggregator(plugin.storageContext); // Generate serialNumber and uniqueId
@@ -1711,10 +1825,35 @@ export class Matterbridge extends EventEmitter {
1711
1825
  this.log.debug(`Importing matter commissioning server storage context from device for ${plg}${pluginName}${db}`);
1712
1826
  const basic = device.getClusterServer(BasicInformationCluster);
1713
1827
  if (!basic) {
1714
- throw new Error('importCommissioningServerContext error: cannot find the BasicInformationCluster');
1828
+ this.log.error('importCommissioningServerContext error: cannot find the BasicInformationCluster');
1829
+ process.exit(1);
1830
+ }
1831
+ if (!this.storageManager) {
1832
+ this.log.error('importCommissioningServerContext error: no storage manager initialized');
1833
+ process.exit(1);
1715
1834
  }
1716
- //const random = 'CS' + CryptoNode.getRandomData(8).toHex();
1717
- return await this.createCommissioningServerContext(pluginName, basic.getNodeLabelAttribute(), DeviceTypeId(device.deviceType), basic.getVendorIdAttribute(), basic.getVendorNameAttribute(), basic.getProductIdAttribute(), basic.getProductNameAttribute(), basic.attributes.serialNumber?.getLocal(), basic.attributes.uniqueId?.getLocal(), basic.getSoftwareVersionAttribute(), basic.getSoftwareVersionStringAttribute(), basic.getHardwareVersionAttribute(), basic.getHardwareVersionStringAttribute());
1835
+ this.log.debug(`Importing commissioning server storage context for ${plg}${pluginName}${db}`);
1836
+ const storageContext = this.storageManager.createContext(pluginName);
1837
+ await storageContext.set('deviceName', basic.getNodeLabelAttribute());
1838
+ await storageContext.set('deviceType', DeviceTypeId(device.deviceType));
1839
+ await storageContext.set('vendorId', basic.getVendorIdAttribute());
1840
+ await storageContext.set('vendorName', basic.getVendorNameAttribute());
1841
+ await storageContext.set('productId', basic.getProductIdAttribute());
1842
+ await storageContext.set('productName', basic.getProductNameAttribute());
1843
+ await storageContext.set('nodeLabel', basic.getNodeLabelAttribute());
1844
+ await storageContext.set('productLabel', basic.getNodeLabelAttribute());
1845
+ await storageContext.set('serialNumber', basic.attributes.serialNumber?.getLocal());
1846
+ await storageContext.set('uniqueId', basic.attributes.uniqueId?.getLocal());
1847
+ await storageContext.set('softwareVersion', basic.getSoftwareVersionAttribute());
1848
+ await storageContext.set('softwareVersionString', basic.getSoftwareVersionStringAttribute());
1849
+ await storageContext.set('hardwareVersion', basic.getHardwareVersionAttribute());
1850
+ await storageContext.set('hardwareVersionString', basic.getHardwareVersionStringAttribute());
1851
+ this.log.debug(`Imported commissioning server storage context for ${plg}${pluginName}${db}`);
1852
+ this.log.debug(`- deviceName: ${await storageContext.get('deviceName')} deviceType: ${await storageContext.get('deviceType')}(0x${(await storageContext.get('deviceType'))?.toString(16).padStart(4, '0')})`);
1853
+ this.log.debug(`- serialNumber: ${await storageContext.get('serialNumber')} uniqueId: ${await storageContext.get('uniqueId')}`);
1854
+ this.log.debug(`- softwareVersion: ${await storageContext.get('softwareVersion')} softwareVersionString: ${await storageContext.get('softwareVersionString')}`);
1855
+ this.log.debug(`- hardwareVersion: ${await storageContext.get('hardwareVersion')} hardwareVersionString: ${await storageContext.get('hardwareVersionString')}`);
1856
+ return storageContext;
1718
1857
  }
1719
1858
  /**
1720
1859
  * Creates a commissioning server storage context.
@@ -1734,7 +1873,7 @@ export class Matterbridge extends EventEmitter {
1734
1873
  * @param hardwareVersionString - The hardware version string of the device (optional).
1735
1874
  * @returns The storage context for the commissioning server.
1736
1875
  */
1737
- async createCommissioningServerContext(pluginName, deviceName, deviceType, vendorId, vendorName, productId, productName, serialNumber, uniqueId, softwareVersion, softwareVersionString, hardwareVersion, hardwareVersionString) {
1876
+ async createCommissioningServerContext(pluginName, deviceName, deviceType, vendorId, vendorName, productId, productName) {
1738
1877
  if (!this.storageManager) {
1739
1878
  this.log.error('No storage manager initialized');
1740
1879
  process.exit(1);
@@ -1752,11 +1891,13 @@ export class Matterbridge extends EventEmitter {
1752
1891
  await storageContext.set('productLabel', productName.slice(0, 32));
1753
1892
  await storageContext.set('serialNumber', await storageContext.get('serialNumber', random));
1754
1893
  await storageContext.set('uniqueId', await storageContext.get('uniqueId', random));
1755
- await storageContext.set('softwareVersion', softwareVersion ?? 1);
1756
- await storageContext.set('softwareVersionString', softwareVersionString ?? '1.0.0');
1757
- await storageContext.set('hardwareVersion', hardwareVersion ?? 1);
1758
- await storageContext.set('hardwareVersionString', hardwareVersionString ?? '1.0.0');
1894
+ await storageContext.set('softwareVersion', this.matterbridgeVersion && this.matterbridgeVersion.includes('.') ? parseInt(this.matterbridgeVersion.split('.')[0], 10) : 1);
1895
+ await storageContext.set('softwareVersionString', this.matterbridgeVersion ?? '1.0.0');
1896
+ await storageContext.set('hardwareVersion', this.systemInformation.osRelease && this.systemInformation.osRelease.includes('.') ? parseInt(this.systemInformation.osRelease.split('.')[0], 10) : 1);
1897
+ await storageContext.set('hardwareVersionString', this.systemInformation.osRelease ?? '1.0.0');
1759
1898
  this.log.debug(`Created commissioning server storage context for ${plg}${pluginName}${db}`);
1899
+ this.log.debug(`- deviceName: ${await storageContext.get('deviceName')} deviceType: ${await storageContext.get('deviceType')}(0x${(await storageContext.get('deviceType'))?.toString(16).padStart(4, '0')})`);
1900
+ this.log.debug(`- serialNumber: ${await storageContext.get('serialNumber')} uniqueId: ${await storageContext.get('uniqueId')}`);
1760
1901
  this.log.debug(`- softwareVersion: ${await storageContext.get('softwareVersion')} softwareVersionString: ${await storageContext.get('softwareVersionString')}`);
1761
1902
  this.log.debug(`- hardwareVersion: ${await storageContext.get('hardwareVersion')} hardwareVersionString: ${await storageContext.get('hardwareVersionString')}`);
1762
1903
  return storageContext;
@@ -1773,14 +1914,14 @@ export class Matterbridge extends EventEmitter {
1773
1914
  if (!commissioningServer || !storageContext || !pluginName)
1774
1915
  return;
1775
1916
  if (!commissioningServer.isCommissioned()) {
1776
- this.log.info(`***The commissioning server on port ${commissioningServer.getPort()} for ${plg}${pluginName}${nf} is not commissioned. Pair it scanning the QR code ...`);
1777
1917
  const { qrPairingCode, manualPairingCode } = commissioningServer.getPairingCode();
1778
1918
  await storageContext.set('qrPairingCode', qrPairingCode);
1779
1919
  await storageContext.set('manualPairingCode', manualPairingCode);
1780
1920
  await nodeContext.set('qrPairingCode', qrPairingCode);
1781
1921
  await nodeContext.set('manualPairingCode', manualPairingCode);
1782
1922
  const QrCode = new QrCodeSchema();
1783
- this.log.info(`Pairing code:\n\n${QrCode.encode(qrPairingCode)}\n${plg}${pluginName}${nf}\n\nqrPairingCode: ${qrPairingCode}\n\nManual pairing code: ${manualPairingCode}\n`);
1923
+ this.log.info(`***The commissioning server on port ${commissioningServer.getPort()} for ${plg}${pluginName}${nf} is not commissioned. Pair it scanning the QR code:\n\n` +
1924
+ `${QrCode.encode(qrPairingCode)}\n${plg}${pluginName}${nf}\n\nqrPairingCode: ${qrPairingCode}\n\nManual pairing code: ${manualPairingCode}\n`);
1784
1925
  if (pluginName !== 'Matterbridge') {
1785
1926
  const plugin = this.findPlugin(pluginName);
1786
1927
  if (plugin) {
@@ -1789,7 +1930,7 @@ export class Matterbridge extends EventEmitter {
1789
1930
  plugin.paired = false;
1790
1931
  }
1791
1932
  }
1792
- await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
1933
+ await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
1793
1934
  }
1794
1935
  else {
1795
1936
  this.log.info(`*The commissioning server on port ${commissioningServer.getPort()} for ${plg}${pluginName}${nf} is already commissioned . Waiting for controllers to connect ...`);
@@ -1799,7 +1940,7 @@ export class Matterbridge extends EventEmitter {
1799
1940
  plugin.paired = true;
1800
1941
  }
1801
1942
  }
1802
- await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
1943
+ await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
1803
1944
  }
1804
1945
  }
1805
1946
  /**
@@ -1875,6 +2016,9 @@ export class Matterbridge extends EventEmitter {
1875
2016
  case 4937:
1876
2017
  vendorName = '(AppleHome)';
1877
2018
  break;
2019
+ case 4996:
2020
+ vendorName = '(AppleKeyChain)';
2021
+ break;
1878
2022
  case 4362:
1879
2023
  vendorName = '(SmartThings)';
1880
2024
  break;
@@ -1996,7 +2140,7 @@ export class Matterbridge extends EventEmitter {
1996
2140
  }
1997
2141
  Logger.defaultLogLevel = this.debugEnabled ? Level.DEBUG : Level.INFO;
1998
2142
  }
1999
- //logEndpoint(commissioningServer.getRootEndpoint());
2143
+ // logEndpoint(commissioningServer.getRootEndpoint());
2000
2144
  }, 2000);
2001
2145
  }
2002
2146
  },
@@ -2023,6 +2167,48 @@ export class Matterbridge extends EventEmitter {
2023
2167
  }
2024
2168
  },
2025
2169
  });
2170
+ const gdcCluster = commissioningServer.getRootClusterServer(GeneralDiagnosticsCluster);
2171
+ if (gdcCluster) {
2172
+ // We have like "30:f6:ef:69:2b:c5" in this.systemInformation.macAddress
2173
+ const macArray = this.systemInformation.macAddress.split(':').map((hex) => parseInt(hex, 16));
2174
+ let hardwareAddress = new Uint8Array(macArray);
2175
+ if (hardwareAddress.length === 6)
2176
+ hardwareAddress = Uint8Array.from([0, 0, ...hardwareAddress]);
2177
+ // We have like "192.168.1.189" in this.systemInformation.ipv4Address
2178
+ const ipv4Array = this.systemInformation.ipv4Address.split('.').map((num) => parseInt(num));
2179
+ const iPv4Address = new Uint8Array(ipv4Array);
2180
+ // We have like "fd78:cbf8:4939:746:d555:85a9:74f6:9c6" in this.systemInformation.ipv6Address
2181
+ const ipv6Groups = this.systemInformation.ipv6Address.split(':');
2182
+ const ipv6Array = [];
2183
+ for (const group of ipv6Groups) {
2184
+ const decimal = parseInt(group, 16);
2185
+ ipv6Array.push(decimal >> 8); // High byte
2186
+ ipv6Array.push(decimal & 0xff); // Low byte
2187
+ }
2188
+ const iPv6Address = new Uint8Array(ipv6Array);
2189
+ this.log.debug(`GeneralDiagnosticsCluster for ${plg}${pluginName}${db} hardwareAddress ${this.systemInformation.macAddress} => ${debugStringify(hardwareAddress)}`);
2190
+ this.log.debug(`GeneralDiagnosticsCluster for ${plg}${pluginName}${db} iPv4Address ${this.systemInformation.ipv4Address} => ${debugStringify(iPv4Address)}`);
2191
+ this.log.debug(`GeneralDiagnosticsCluster for ${plg}${pluginName}${db} iPv6Address ${this.systemInformation.ipv6Address} => ${debugStringify(iPv6Address)}`);
2192
+ try {
2193
+ gdcCluster.setNetworkInterfacesAttribute([
2194
+ {
2195
+ name: 'eth0',
2196
+ isOperational: true,
2197
+ offPremiseServicesReachableIPv4: null,
2198
+ offPremiseServicesReachableIPv6: null,
2199
+ hardwareAddress,
2200
+ iPv4Addresses: [iPv4Address],
2201
+ iPv6Addresses: [iPv6Address],
2202
+ type: GeneralDiagnostics.InterfaceType.Ethernet,
2203
+ },
2204
+ ]);
2205
+ }
2206
+ catch (error) {
2207
+ this.log.error(`GeneralDiagnosticsCluster.setNetworkInterfacesAttribute for ${plg}${pluginName}${er} error:`, error);
2208
+ }
2209
+ }
2210
+ else
2211
+ this.log.warn(`*GeneralDiagnosticsCluster not found for ${plg}${pluginName}${wr}`);
2026
2212
  commissioningServer.addCommandHandler('testEventTrigger', async ({ request: { enableKey, eventTrigger } }) => this.log.info(`testEventTrigger called on GeneralDiagnostic cluster: ${enableKey} ${eventTrigger}`));
2027
2213
  return commissioningServer;
2028
2214
  }
@@ -2049,7 +2235,7 @@ export class Matterbridge extends EventEmitter {
2049
2235
  const matterAggregator = new Aggregator();
2050
2236
  matterAggregator.addClusterServer(ClusterServer(BasicInformationCluster, {
2051
2237
  dataModelRevision: 1,
2052
- location: 'XX',
2238
+ location: 'FR',
2053
2239
  vendorId: VendorId(0xfff1),
2054
2240
  vendorName: 'Matterbridge',
2055
2241
  productId: 0x8000,
@@ -2058,10 +2244,10 @@ export class Matterbridge extends EventEmitter {
2058
2244
  nodeLabel: 'Matterbridge aggregator',
2059
2245
  serialNumber: await context.get('aggregatorSerialNumber'),
2060
2246
  uniqueId: await context.get('aggregatorUniqueId'),
2061
- softwareVersion: 1,
2062
- softwareVersionString: 'v.1.0',
2063
- hardwareVersion: 1,
2064
- hardwareVersionString: 'v.1.0',
2247
+ softwareVersion: await context.get('softwareVersion', 1),
2248
+ softwareVersionString: await context.get('softwareVersionString', '1.0.0'),
2249
+ hardwareVersion: await context.get('hardwareVersion', 1),
2250
+ hardwareVersionString: await context.get('hardwareVersionString', '1.0.0'),
2065
2251
  reachable: true,
2066
2252
  capabilityMinima: { caseSessionsPerFabric: 3, subscriptionsPerFabric: 3 },
2067
2253
  }, {}, {
@@ -2136,9 +2322,11 @@ export class Matterbridge extends EventEmitter {
2136
2322
  for (const detail of interfaceDetails) {
2137
2323
  if (detail.family === 'IPv4' && !detail.internal && this.systemInformation.ipv4Address === 'Not found') {
2138
2324
  this.systemInformation.ipv4Address = detail.address;
2325
+ this.systemInformation.macAddress = detail.mac;
2139
2326
  }
2140
2327
  else if (detail.family === 'IPv6' && !detail.internal && this.systemInformation.ipv6Address === 'Not found') {
2141
2328
  this.systemInformation.ipv6Address = detail.address;
2329
+ this.systemInformation.macAddress = detail.mac;
2142
2330
  }
2143
2331
  }
2144
2332
  // Break if both addresses are found to improve efficiency
@@ -2165,6 +2353,7 @@ export class Matterbridge extends EventEmitter {
2165
2353
  this.log.debug('Host System Information:');
2166
2354
  this.log.debug(`- Hostname: ${this.systemInformation.hostname}`);
2167
2355
  this.log.debug(`- User: ${this.systemInformation.user}`);
2356
+ this.log.debug(`- MAC Address: ${this.systemInformation.macAddress}`);
2168
2357
  this.log.debug(`- IPv4 Address: ${this.systemInformation.ipv4Address}`);
2169
2358
  this.log.debug(`- IPv6 Address: ${this.systemInformation.ipv6Address}`);
2170
2359
  this.log.debug(`- Node.js: ${versionMajor}.${versionMinor}.${versionPatch}`);
@@ -2280,24 +2469,30 @@ export class Matterbridge extends EventEmitter {
2280
2469
  *
2281
2470
  * @returns {BaseRegisteredPlugin[]} An array of base registered plugins.
2282
2471
  */
2283
- getBaseRegisteredPlugins() {
2284
- const baseRegisteredPlugins = this.registeredPlugins.map((plugin) => ({
2285
- path: plugin.path,
2286
- type: plugin.type,
2287
- name: plugin.name,
2288
- version: plugin.version,
2289
- description: plugin.description,
2290
- author: plugin.author,
2291
- enabled: plugin.enabled,
2292
- loaded: plugin.loaded,
2293
- started: plugin.started,
2294
- configured: plugin.configured,
2295
- paired: plugin.paired,
2296
- connected: plugin.connected,
2297
- registeredDevices: plugin.registeredDevices,
2298
- qrPairingCode: plugin.qrPairingCode,
2299
- manualPairingCode: plugin.manualPairingCode,
2300
- }));
2472
+ async getBaseRegisteredPlugins(includeConfigSchema = false) {
2473
+ const baseRegisteredPlugins = [];
2474
+ for (const plugin of this.registeredPlugins) {
2475
+ baseRegisteredPlugins.push({
2476
+ path: plugin.path,
2477
+ type: plugin.type,
2478
+ name: plugin.name,
2479
+ version: plugin.version,
2480
+ latestVersion: plugin.latestVersion,
2481
+ description: plugin.description,
2482
+ author: plugin.author,
2483
+ enabled: plugin.enabled,
2484
+ loaded: plugin.loaded,
2485
+ started: plugin.started,
2486
+ configured: plugin.configured,
2487
+ paired: plugin.paired,
2488
+ connected: plugin.connected,
2489
+ registeredDevices: plugin.registeredDevices,
2490
+ qrPairingCode: plugin.qrPairingCode,
2491
+ manualPairingCode: plugin.manualPairingCode,
2492
+ configJson: includeConfigSchema ? await this.loadPluginConfig(plugin) : {},
2493
+ schemaJson: includeConfigSchema ? await this.loadPluginSchema(plugin) : {},
2494
+ });
2495
+ }
2301
2496
  return baseRegisteredPlugins;
2302
2497
  }
2303
2498
  /**
@@ -2518,9 +2713,9 @@ export class Matterbridge extends EventEmitter {
2518
2713
  res.json(response);
2519
2714
  });
2520
2715
  // Endpoint to provide plugins
2521
- this.expressApp.get('/api/plugins', (req, res) => {
2716
+ this.expressApp.get('/api/plugins', async (req, res) => {
2522
2717
  this.log.debug('The frontend sent /api/plugins');
2523
- res.json(this.getBaseRegisteredPlugins());
2718
+ res.json(await this.getBaseRegisteredPlugins(true));
2524
2719
  });
2525
2720
  // Endpoint to provide devices
2526
2721
  this.expressApp.get('/api/devices', (req, res) => {
@@ -2625,7 +2820,7 @@ export class Matterbridge extends EventEmitter {
2625
2820
  res.json(data);
2626
2821
  });
2627
2822
  // Endpoint to receive commands
2628
- this.expressApp.post('/api/command/:command/:param', async (req, res) => {
2823
+ this.expressApp.post('/api/command/:command/:param', express.json(), async (req, res) => {
2629
2824
  const command = req.params.command;
2630
2825
  let param = req.params.param;
2631
2826
  this.log.debug(`The frontend sent /api/command/${command}/${param}`);
@@ -2642,6 +2837,7 @@ export class Matterbridge extends EventEmitter {
2642
2837
  }
2643
2838
  // Handle the command debugLevel from Settings
2644
2839
  if (command === 'setloglevel') {
2840
+ this.log.debug('setloglevel:', param);
2645
2841
  if (param === 'Debug') {
2646
2842
  this.log.setLogDebug(true);
2647
2843
  this.debugEnabled = true;
@@ -2657,6 +2853,11 @@ export class Matterbridge extends EventEmitter {
2657
2853
  this.debugEnabled = false;
2658
2854
  Logger.defaultLogLevel = Level.WARN;
2659
2855
  }
2856
+ else if (param === 'Error') {
2857
+ this.log.setLogDebug(false);
2858
+ this.debugEnabled = false;
2859
+ Logger.defaultLogLevel = Level.ERROR;
2860
+ }
2660
2861
  this.registeredPlugins.forEach((plugin) => {
2661
2862
  plugin.platform?.log.setLogDebug(this.debugEnabled);
2662
2863
  });
@@ -2695,10 +2896,23 @@ export class Matterbridge extends EventEmitter {
2695
2896
  }
2696
2897
  this.updateProcess();
2697
2898
  }
2899
+ // Handle the command saveschema from Home
2900
+ if (command === 'saveconfig') {
2901
+ param = param.replace(/\*/g, '\\');
2902
+ this.log.info(`Saving config for plugin ${plg}${param}${nf}...`);
2903
+ //console.log('Req.body:', JSON.stringify(req.body, null, 2));
2904
+ const plugins = await this.nodeContext?.get('plugins');
2905
+ if (!plugins)
2906
+ return;
2907
+ const plugin = plugins.find((plugin) => plugin.name === param);
2908
+ if (!plugin)
2909
+ return;
2910
+ this.savePluginConfigFromJson(plugin, req.body);
2911
+ }
2698
2912
  // Handle the command installplugin from Home
2699
2913
  if (command === 'installplugin') {
2700
2914
  param = param.replace(/\*/g, '\\');
2701
- this.log.info(`Installing plugin ${plg}${param}${db}...`);
2915
+ this.log.info(`Installing plugin ${plg}${param}${nf}...`);
2702
2916
  try {
2703
2917
  await this.spawnCommand('npm', ['install', '-g', param, '--loglevel=verbose']);
2704
2918
  this.log.info(`Plugin ${plg}${param}${nf} installed. Full restart required.`);
@@ -2710,7 +2924,7 @@ export class Matterbridge extends EventEmitter {
2710
2924
  }
2711
2925
  }
2712
2926
  // Handle the command addplugin from Home
2713
- if (command === 'addplugin') {
2927
+ if (command === 'addplugin' || command === 'installplugin') {
2714
2928
  param = param.replace(/\*/g, '\\');
2715
2929
  if (this.registeredPlugins.find((plugin) => plugin.name === param)) {
2716
2930
  this.log.warn(`Plugin ${plg}${param}${wr} already added to matterbridge`);
@@ -2730,7 +2944,7 @@ export class Matterbridge extends EventEmitter {
2730
2944
  const plugin = { path: packageJsonPath, type: '', name: packageJson.name, version: packageJson.version, description: packageJson.description, author: packageJson.author, enabled: true };
2731
2945
  if (await this.loadPlugin(plugin)) {
2732
2946
  this.registeredPlugins.push(plugin);
2733
- await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
2947
+ await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
2734
2948
  this.log.info(`Plugin ${plg}${packageJsonPath}${nf} type ${plugin.type} added to matterbridge. Restart required.`);
2735
2949
  }
2736
2950
  else {
@@ -2749,10 +2963,10 @@ export class Matterbridge extends EventEmitter {
2749
2963
  if (index !== -1) {
2750
2964
  if (this.registeredPlugins[index].platform) {
2751
2965
  await this.registeredPlugins[index].platform?.onShutdown('The plugin has been removed.');
2752
- await this.savePluginConfig(this.registeredPlugins[index]);
2966
+ // await this.savePluginConfig(this.registeredPlugins[index]);
2753
2967
  }
2754
2968
  this.registeredPlugins.splice(index, 1);
2755
- await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
2969
+ await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
2756
2970
  this.log.info(`Plugin ${plg}${param}${nf} removed from matterbridge`);
2757
2971
  }
2758
2972
  else {
@@ -2799,7 +3013,7 @@ export class Matterbridge extends EventEmitter {
2799
3013
  if (pluginToDisable) {
2800
3014
  if (pluginToDisable.platform) {
2801
3015
  await pluginToDisable.platform.onShutdown('The plugin has been removed.');
2802
- await this.savePluginConfig(pluginToDisable);
3016
+ // await this.savePluginConfig(pluginToDisable);
2803
3017
  }
2804
3018
  pluginToDisable.enabled = false;
2805
3019
  pluginToDisable.error = undefined;