matterbridge 1.2.13 → 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.
@@ -42,6 +42,7 @@ import { ManualPairingCodeCodec, QrCodeSchema } from '@project-chip/matter-node.
42
42
  import { StorageBackendDisk, StorageBackendJsonFile, StorageManager } from '@project-chip/matter-node.js/storage';
43
43
  import { requireMinNodeVersion, getParameter, getIntParameter, hasParameter } from '@project-chip/matter-node.js/util';
44
44
  import { CryptoNode } from '@project-chip/matter-node.js/crypto';
45
+ import { somfytahoma_config, somfytahoma_schema, zigbee2mqtt_config, zigbee2mqtt_schema } from './defaultConfigSchema.js';
45
46
  const plg = '\u001B[38;5;33m';
46
47
  const dev = '\u001B[38;5;79m';
47
48
  const typ = '\u001B[38;5;207m';
@@ -50,6 +51,7 @@ const typ = '\u001B[38;5;207m';
50
51
  */
51
52
  export class Matterbridge extends EventEmitter {
52
53
  systemInformation = {
54
+ macAddress: '',
53
55
  ipv4Address: '',
54
56
  ipv6Address: '',
55
57
  nodeVersion: '',
@@ -157,7 +159,7 @@ export class Matterbridge extends EventEmitter {
157
159
  addedDevices: 0,
158
160
  };
159
161
  this.registeredPlugins.push(plugin);
160
- await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
162
+ await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
161
163
  // Log system info and create .matterbridge directory
162
164
  await this.logNodeAndSystemInfo();
163
165
  this.matterbridgeDirectory = dataPath;
@@ -499,7 +501,7 @@ export class Matterbridge extends EventEmitter {
499
501
  const plugin = { path: packageJsonPath, type: '', name: packageJson.name, version: packageJson.version, description: packageJson.description, author: packageJson.author, enabled: true };
500
502
  if (await this.loadPlugin(plugin)) {
501
503
  this.registeredPlugins.push(plugin);
502
- await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
504
+ await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
503
505
  this.log.info(`Plugin ${plg}${packageJsonPath}${nf} type ${plugin.type} added to matterbridge`);
504
506
  }
505
507
  else {
@@ -513,7 +515,7 @@ export class Matterbridge extends EventEmitter {
513
515
  else if (mode === 'remove') {
514
516
  if (this.registeredPlugins.find((registeredPlugin) => registeredPlugin.name === packageJson.name)) {
515
517
  this.registeredPlugins.splice(this.registeredPlugins.findIndex((registeredPlugin) => registeredPlugin.name === packageJson.name), 1);
516
- await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
518
+ await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
517
519
  this.log.info(`Plugin ${plg}${packageJsonPath}${nf} removed from matterbridge`);
518
520
  }
519
521
  else {
@@ -528,7 +530,7 @@ export class Matterbridge extends EventEmitter {
528
530
  plugin.started = undefined;
529
531
  plugin.configured = undefined;
530
532
  plugin.connected = undefined;
531
- await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
533
+ await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
532
534
  this.log.info(`Plugin ${plg}${packageJsonPath}${nf} enabled`);
533
535
  }
534
536
  else {
@@ -543,7 +545,7 @@ export class Matterbridge extends EventEmitter {
543
545
  plugin.started = undefined;
544
546
  plugin.configured = undefined;
545
547
  plugin.connected = undefined;
546
- await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
548
+ await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
547
549
  this.log.info(`Plugin ${plg}${packageJsonPath}${nf} disabled`);
548
550
  }
549
551
  else {
@@ -565,7 +567,7 @@ export class Matterbridge extends EventEmitter {
565
567
  if (!context)
566
568
  this.log.error(`Plugin ${plg}${plugin.name}${er} context not found`);
567
569
  await context?.clearAll();
568
- await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
570
+ await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
569
571
  this.log.info(`Reset commissionig for plugin ${plg}${plugin.name}${nf} done! Remove the device from the controller.`);
570
572
  }
571
573
  else {
@@ -992,7 +994,8 @@ export class Matterbridge extends EventEmitter {
992
994
  }
993
995
  }
994
996
  catch (error) {
995
- 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.');
996
999
  await this.cleanup('Storage initialize() error!');
997
1000
  }
998
1001
  }
@@ -1123,6 +1126,97 @@ export class Matterbridge extends EventEmitter {
1123
1126
  }, 60 * 1000);
1124
1127
  }, 1000);
1125
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
+ }
1126
1220
  /**
1127
1221
  * Loads the configuration for a plugin.
1128
1222
  * If the configuration file exists, it reads the file and returns the parsed JSON data.
@@ -1148,22 +1242,30 @@ export class Matterbridge extends EventEmitter {
1148
1242
  if (err instanceof Error) {
1149
1243
  const nodeErr = err;
1150
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 };
1151
1252
  try {
1152
- await this.writeFile(configFile, JSON.stringify({ name: plugin.name, type: plugin.type }, null, 2));
1153
- this.log.debug(`Created config file: ${configFile}.\nConfig:${rs}\n`, { name: plugin.name, type: plugin.type });
1154
- 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;
1155
1256
  }
1156
1257
  catch (err) {
1157
1258
  this.log.error(`Error creating config file ${configFile}: ${err}`);
1158
- return Promise.reject(err);
1259
+ return config;
1159
1260
  }
1160
1261
  }
1161
1262
  else {
1162
1263
  this.log.error(`Error accessing config file ${configFile}: ${err}`);
1163
- return Promise.reject(err);
1264
+ return {};
1164
1265
  }
1165
1266
  }
1166
- return Promise.reject(err);
1267
+ this.log.error(`Error loading config file ${configFile}: ${err}`);
1268
+ return {};
1167
1269
  }
1168
1270
  }
1169
1271
  /**
@@ -1328,11 +1430,11 @@ export class Matterbridge extends EventEmitter {
1328
1430
  plugin.loaded = true;
1329
1431
  plugin.registeredDevices = 0;
1330
1432
  plugin.addedDevices = 0;
1331
- await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
1433
+ await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
1332
1434
  this.getLatestVersion(plugin.name)
1333
1435
  .then(async (latestVersion) => {
1334
1436
  plugin.latestVersion = latestVersion;
1335
- await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
1437
+ await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
1336
1438
  if (plugin.version !== latestVersion)
1337
1439
  this.log.warn(`The plugin ${plg}${plugin.name}${wr} is out of date. Current version: ${plugin.version}, Latest version: ${latestVersion}`);
1338
1440
  else
@@ -1570,8 +1672,6 @@ export class Matterbridge extends EventEmitter {
1570
1672
  this.log.error(`Node storage context undefined for ${plg}Matterbridge${er}`);
1571
1673
  return;
1572
1674
  }
1573
- await this.matterbridgeContext.set('softwareVersion', 1);
1574
- await this.matterbridgeContext.set('softwareVersionString', this.matterbridgeVersion);
1575
1675
  this.log.debug(`Creating commissioning server for ${plg}Matterbridge${db}`);
1576
1676
  this.commissioningServer = await this.createCommisioningServer(this.matterbridgeContext, 'Matterbridge');
1577
1677
  this.log.debug(`Creating matter aggregator for ${plg}Matterbridge${db}`);
@@ -1622,8 +1722,7 @@ export class Matterbridge extends EventEmitter {
1622
1722
  continue;
1623
1723
  if (!plugin.storageContext)
1624
1724
  plugin.storageContext = await this.importCommissioningServerContext(plugin.name, registeredDevice.device);
1625
- await plugin.storageContext.set('softwareVersion', 1);
1626
- await plugin.storageContext.set('softwareVersionString', this.matterbridgeVersion);
1725
+ this.log.debug(`Creating commissioning server for ${plg}${plugin.name}${db}`);
1627
1726
  if (!plugin.commissioningServer)
1628
1727
  plugin.commissioningServer = await this.createCommisioningServer(plugin.storageContext, plugin.name);
1629
1728
  this.log.debug(`Adding device ${dev}${registeredDevice.device.name}${db} to commissioning server for plugin ${plg}${plugin.name}${db}`);
@@ -1635,10 +1734,9 @@ export class Matterbridge extends EventEmitter {
1635
1734
  await this.matterServer?.addCommissioningServer(plugin.commissioningServer, { uniqueStorageKey: plugin.name });
1636
1735
  }
1637
1736
  if (plugin.type === 'DynamicPlatform') {
1638
- // eslint-disable-next-line max-len
1737
+ this.log.debug(`Creating commissioning server context for ${plg}${plugin.name}${db}`);
1639
1738
  plugin.storageContext = await this.createCommissioningServerContext(plugin.name, 'Matterbridge', DeviceTypes.AGGREGATOR.code, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge Dynamic Platform');
1640
- await plugin.storageContext.set('softwareVersion', 1);
1641
- await plugin.storageContext.set('softwareVersionString', this.matterbridgeVersion);
1739
+ this.log.debug(`Creating commissioning server for ${plg}${plugin.name}${db}`);
1642
1740
  plugin.commissioningServer = await this.createCommisioningServer(plugin.storageContext, plugin.name);
1643
1741
  this.log.debug(`Creating aggregator for plugin ${plg}${plugin.name}${db}`);
1644
1742
  plugin.aggregator = await this.createMatterAggregator(plugin.storageContext); // Generate serialNumber and uniqueId
@@ -1727,10 +1825,35 @@ export class Matterbridge extends EventEmitter {
1727
1825
  this.log.debug(`Importing matter commissioning server storage context from device for ${plg}${pluginName}${db}`);
1728
1826
  const basic = device.getClusterServer(BasicInformationCluster);
1729
1827
  if (!basic) {
1730
- throw new Error('importCommissioningServerContext error: cannot find the BasicInformationCluster');
1828
+ this.log.error('importCommissioningServerContext error: cannot find the BasicInformationCluster');
1829
+ process.exit(1);
1731
1830
  }
1732
- //const random = 'CS' + CryptoNode.getRandomData(8).toHex();
1733
- 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());
1831
+ if (!this.storageManager) {
1832
+ this.log.error('importCommissioningServerContext error: no storage manager initialized');
1833
+ process.exit(1);
1834
+ }
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;
1734
1857
  }
1735
1858
  /**
1736
1859
  * Creates a commissioning server storage context.
@@ -1750,7 +1873,7 @@ export class Matterbridge extends EventEmitter {
1750
1873
  * @param hardwareVersionString - The hardware version string of the device (optional).
1751
1874
  * @returns The storage context for the commissioning server.
1752
1875
  */
1753
- 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) {
1754
1877
  if (!this.storageManager) {
1755
1878
  this.log.error('No storage manager initialized');
1756
1879
  process.exit(1);
@@ -1768,11 +1891,13 @@ export class Matterbridge extends EventEmitter {
1768
1891
  await storageContext.set('productLabel', productName.slice(0, 32));
1769
1892
  await storageContext.set('serialNumber', await storageContext.get('serialNumber', random));
1770
1893
  await storageContext.set('uniqueId', await storageContext.get('uniqueId', random));
1771
- await storageContext.set('softwareVersion', softwareVersion ?? 1);
1772
- await storageContext.set('softwareVersionString', softwareVersionString ?? '1.0.0');
1773
- await storageContext.set('hardwareVersion', hardwareVersion ?? 1);
1774
- 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');
1775
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')}`);
1776
1901
  this.log.debug(`- softwareVersion: ${await storageContext.get('softwareVersion')} softwareVersionString: ${await storageContext.get('softwareVersionString')}`);
1777
1902
  this.log.debug(`- hardwareVersion: ${await storageContext.get('hardwareVersion')} hardwareVersionString: ${await storageContext.get('hardwareVersionString')}`);
1778
1903
  return storageContext;
@@ -1805,7 +1930,7 @@ export class Matterbridge extends EventEmitter {
1805
1930
  plugin.paired = false;
1806
1931
  }
1807
1932
  }
1808
- await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
1933
+ await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
1809
1934
  }
1810
1935
  else {
1811
1936
  this.log.info(`*The commissioning server on port ${commissioningServer.getPort()} for ${plg}${pluginName}${nf} is already commissioned . Waiting for controllers to connect ...`);
@@ -1815,7 +1940,7 @@ export class Matterbridge extends EventEmitter {
1815
1940
  plugin.paired = true;
1816
1941
  }
1817
1942
  }
1818
- await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
1943
+ await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
1819
1944
  }
1820
1945
  }
1821
1946
  /**
@@ -2044,18 +2169,43 @@ export class Matterbridge extends EventEmitter {
2044
2169
  });
2045
2170
  const gdcCluster = commissioningServer.getRootClusterServer(GeneralDiagnosticsCluster);
2046
2171
  if (gdcCluster) {
2047
- gdcCluster.setNetworkInterfacesAttribute([
2048
- {
2049
- name: 'eth0',
2050
- isOperational: true,
2051
- offPremiseServicesReachableIPv4: null,
2052
- offPremiseServicesReachableIPv6: null,
2053
- hardwareAddress: Uint8Array.fromString('00000000'),
2054
- iPv4Addresses: [Uint8Array.fromString('0000')],
2055
- iPv6Addresses: [Uint8Array.fromString('0000000000000000')],
2056
- type: GeneralDiagnostics.InterfaceType.Ethernet,
2057
- },
2058
- ]);
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
+ }
2059
2209
  }
2060
2210
  else
2061
2211
  this.log.warn(`*GeneralDiagnosticsCluster not found for ${plg}${pluginName}${wr}`);
@@ -2094,10 +2244,10 @@ export class Matterbridge extends EventEmitter {
2094
2244
  nodeLabel: 'Matterbridge aggregator',
2095
2245
  serialNumber: await context.get('aggregatorSerialNumber'),
2096
2246
  uniqueId: await context.get('aggregatorUniqueId'),
2097
- softwareVersion: 1,
2098
- softwareVersionString: 'v.1.0',
2099
- hardwareVersion: 1,
2100
- 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'),
2101
2251
  reachable: true,
2102
2252
  capabilityMinima: { caseSessionsPerFabric: 3, subscriptionsPerFabric: 3 },
2103
2253
  }, {}, {
@@ -2172,9 +2322,11 @@ export class Matterbridge extends EventEmitter {
2172
2322
  for (const detail of interfaceDetails) {
2173
2323
  if (detail.family === 'IPv4' && !detail.internal && this.systemInformation.ipv4Address === 'Not found') {
2174
2324
  this.systemInformation.ipv4Address = detail.address;
2325
+ this.systemInformation.macAddress = detail.mac;
2175
2326
  }
2176
2327
  else if (detail.family === 'IPv6' && !detail.internal && this.systemInformation.ipv6Address === 'Not found') {
2177
2328
  this.systemInformation.ipv6Address = detail.address;
2329
+ this.systemInformation.macAddress = detail.mac;
2178
2330
  }
2179
2331
  }
2180
2332
  // Break if both addresses are found to improve efficiency
@@ -2201,6 +2353,7 @@ export class Matterbridge extends EventEmitter {
2201
2353
  this.log.debug('Host System Information:');
2202
2354
  this.log.debug(`- Hostname: ${this.systemInformation.hostname}`);
2203
2355
  this.log.debug(`- User: ${this.systemInformation.user}`);
2356
+ this.log.debug(`- MAC Address: ${this.systemInformation.macAddress}`);
2204
2357
  this.log.debug(`- IPv4 Address: ${this.systemInformation.ipv4Address}`);
2205
2358
  this.log.debug(`- IPv6 Address: ${this.systemInformation.ipv6Address}`);
2206
2359
  this.log.debug(`- Node.js: ${versionMajor}.${versionMinor}.${versionPatch}`);
@@ -2316,25 +2469,30 @@ export class Matterbridge extends EventEmitter {
2316
2469
  *
2317
2470
  * @returns {BaseRegisteredPlugin[]} An array of base registered plugins.
2318
2471
  */
2319
- getBaseRegisteredPlugins() {
2320
- const baseRegisteredPlugins = this.registeredPlugins.map((plugin) => ({
2321
- path: plugin.path,
2322
- type: plugin.type,
2323
- name: plugin.name,
2324
- version: plugin.version,
2325
- latestVersion: plugin.latestVersion,
2326
- description: plugin.description,
2327
- author: plugin.author,
2328
- enabled: plugin.enabled,
2329
- loaded: plugin.loaded,
2330
- started: plugin.started,
2331
- configured: plugin.configured,
2332
- paired: plugin.paired,
2333
- connected: plugin.connected,
2334
- registeredDevices: plugin.registeredDevices,
2335
- qrPairingCode: plugin.qrPairingCode,
2336
- manualPairingCode: plugin.manualPairingCode,
2337
- }));
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
+ }
2338
2496
  return baseRegisteredPlugins;
2339
2497
  }
2340
2498
  /**
@@ -2555,9 +2713,9 @@ export class Matterbridge extends EventEmitter {
2555
2713
  res.json(response);
2556
2714
  });
2557
2715
  // Endpoint to provide plugins
2558
- this.expressApp.get('/api/plugins', (req, res) => {
2716
+ this.expressApp.get('/api/plugins', async (req, res) => {
2559
2717
  this.log.debug('The frontend sent /api/plugins');
2560
- res.json(this.getBaseRegisteredPlugins());
2718
+ res.json(await this.getBaseRegisteredPlugins(true));
2561
2719
  });
2562
2720
  // Endpoint to provide devices
2563
2721
  this.expressApp.get('/api/devices', (req, res) => {
@@ -2662,7 +2820,7 @@ export class Matterbridge extends EventEmitter {
2662
2820
  res.json(data);
2663
2821
  });
2664
2822
  // Endpoint to receive commands
2665
- this.expressApp.post('/api/command/:command/:param', async (req, res) => {
2823
+ this.expressApp.post('/api/command/:command/:param', express.json(), async (req, res) => {
2666
2824
  const command = req.params.command;
2667
2825
  let param = req.params.param;
2668
2826
  this.log.debug(`The frontend sent /api/command/${command}/${param}`);
@@ -2679,6 +2837,7 @@ export class Matterbridge extends EventEmitter {
2679
2837
  }
2680
2838
  // Handle the command debugLevel from Settings
2681
2839
  if (command === 'setloglevel') {
2840
+ this.log.debug('setloglevel:', param);
2682
2841
  if (param === 'Debug') {
2683
2842
  this.log.setLogDebug(true);
2684
2843
  this.debugEnabled = true;
@@ -2737,10 +2896,23 @@ export class Matterbridge extends EventEmitter {
2737
2896
  }
2738
2897
  this.updateProcess();
2739
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
+ }
2740
2912
  // Handle the command installplugin from Home
2741
2913
  if (command === 'installplugin') {
2742
2914
  param = param.replace(/\*/g, '\\');
2743
- this.log.info(`Installing plugin ${plg}${param}${db}...`);
2915
+ this.log.info(`Installing plugin ${plg}${param}${nf}...`);
2744
2916
  try {
2745
2917
  await this.spawnCommand('npm', ['install', '-g', param, '--loglevel=verbose']);
2746
2918
  this.log.info(`Plugin ${plg}${param}${nf} installed. Full restart required.`);
@@ -2772,7 +2944,7 @@ export class Matterbridge extends EventEmitter {
2772
2944
  const plugin = { path: packageJsonPath, type: '', name: packageJson.name, version: packageJson.version, description: packageJson.description, author: packageJson.author, enabled: true };
2773
2945
  if (await this.loadPlugin(plugin)) {
2774
2946
  this.registeredPlugins.push(plugin);
2775
- await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
2947
+ await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
2776
2948
  this.log.info(`Plugin ${plg}${packageJsonPath}${nf} type ${plugin.type} added to matterbridge. Restart required.`);
2777
2949
  }
2778
2950
  else {
@@ -2794,7 +2966,7 @@ export class Matterbridge extends EventEmitter {
2794
2966
  // await this.savePluginConfig(this.registeredPlugins[index]);
2795
2967
  }
2796
2968
  this.registeredPlugins.splice(index, 1);
2797
- await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
2969
+ await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
2798
2970
  this.log.info(`Plugin ${plg}${param}${nf} removed from matterbridge`);
2799
2971
  }
2800
2972
  else {