matterbridge 3.0.0-edge.8 → 3.0.1-dev-20250430-a3ea2b7

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.
package/dist/frontend.js CHANGED
@@ -11,7 +11,7 @@ import { AnsiLogger, stringify, debugStringify, CYAN, db, er, nf, rs, UNDERLINE,
11
11
  import { createZip, deepCopy, isValidArray, isValidNumber, isValidObject, isValidString } from './utils/export.js';
12
12
  import { plg } from './matterbridgeTypes.js';
13
13
  import { hasParameter } from './utils/export.js';
14
- import { BridgedDeviceBasicInformation } from '@matter/main/clusters';
14
+ import { BridgedDeviceBasicInformation, PowerSource } from '@matter/main/clusters';
15
15
  export const WS_ID_LOG = 0;
16
16
  export const WS_ID_REFRESH_NEEDED = 1;
17
17
  export const WS_ID_RESTART_NEEDED = 2;
@@ -369,27 +369,55 @@ export class Frontend {
369
369
  });
370
370
  res.json(data);
371
371
  });
372
- this.expressApp.get('/api/view-log', async (req, res) => {
373
- this.log.debug('The frontend sent /api/log');
372
+ this.expressApp.get('/api/view-mblog', async (req, res) => {
373
+ this.log.debug('The frontend sent /api/view-mblog');
374
374
  try {
375
375
  const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), 'utf8');
376
376
  res.type('text/plain');
377
377
  res.send(data);
378
378
  }
379
379
  catch (error) {
380
- this.log.error(`Error reading log file ${this.matterbridge.matterbrideLoggerFile}: ${error instanceof Error ? error.message : error}`);
381
- res.status(500).send('Error reading log file');
380
+ this.log.error(`Error reading matterbridge log file ${this.matterbridge.matterbrideLoggerFile}: ${error instanceof Error ? error.message : error}`);
381
+ res.status(500).send('Error reading matterbridge log file. Please enable the matterbridge log on file in the settings.');
382
+ }
383
+ });
384
+ this.expressApp.get('/api/view-mjlog', async (req, res) => {
385
+ this.log.debug('The frontend sent /api/view-mjlog');
386
+ try {
387
+ const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), 'utf8');
388
+ res.type('text/plain');
389
+ res.send(data);
390
+ }
391
+ catch (error) {
392
+ this.log.error(`Error reading matter log file ${this.matterbridge.matterLoggerFile}: ${error instanceof Error ? error.message : error}`);
393
+ res.status(500).send('Error reading matter log file. Please enable the matter log on file in the settings.');
394
+ }
395
+ });
396
+ this.expressApp.get('/api/shellyviewsystemlog', async (req, res) => {
397
+ this.log.debug('The frontend sent /api/shellyviewsystemlog');
398
+ try {
399
+ const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'shelly.log'), 'utf8');
400
+ res.type('text/plain');
401
+ res.send(data);
402
+ }
403
+ catch (error) {
404
+ this.log.error(`Error reading shelly log file ${this.matterbridge.matterbrideLoggerFile}: ${error instanceof Error ? error.message : error}`);
405
+ res.status(500).send('Error reading shelly log file. Please create the shelly system log before loading it.');
382
406
  }
383
407
  });
384
408
  this.expressApp.get('/api/download-mblog', async (req, res) => {
385
- this.log.debug('The frontend sent /api/download-mblog');
409
+ this.log.debug(`The frontend sent /api/download-mblog ${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile)}`);
386
410
  try {
387
411
  await fs.access(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), fs.constants.F_OK);
412
+ const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), 'utf8');
413
+ await fs.writeFile(path.join(os.tmpdir(), this.matterbridge.matterbrideLoggerFile), data, 'utf-8');
388
414
  }
389
415
  catch (error) {
390
- fs.appendFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), 'Enable the log on file in the settings to enable the file logger');
416
+ await fs.writeFile(path.join(os.tmpdir(), this.matterbridge.matterbrideLoggerFile), 'Enable the matterbridge log on file in the settings to download the matterbridge log.', 'utf-8');
417
+ this.log.debug(`Error in /api/download-mblog: ${error instanceof Error ? error.message : error}`);
391
418
  }
392
- res.download(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), 'matterbridge.log', (error) => {
419
+ res.type('text/plain');
420
+ res.download(path.join(os.tmpdir(), this.matterbridge.matterbrideLoggerFile), 'matterbridge.log', (error) => {
393
421
  if (error) {
394
422
  this.log.error(`Error downloading log file ${this.matterbridge.matterbrideLoggerFile}: ${error instanceof Error ? error.message : error}`);
395
423
  res.status(500).send('Error downloading the matterbridge log file');
@@ -397,14 +425,18 @@ export class Frontend {
397
425
  });
398
426
  });
399
427
  this.expressApp.get('/api/download-mjlog', async (req, res) => {
400
- this.log.debug('The frontend sent /api/download-mjlog');
428
+ this.log.debug(`The frontend sent /api/download-mjlog ${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile)}`);
401
429
  try {
402
430
  await fs.access(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), fs.constants.F_OK);
431
+ const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), 'utf8');
432
+ await fs.writeFile(path.join(os.tmpdir(), this.matterbridge.matterLoggerFile), data, 'utf-8');
403
433
  }
404
434
  catch (error) {
405
- fs.appendFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), 'Enable the log on file in the settings to enable the file logger');
435
+ await fs.writeFile(path.join(os.tmpdir(), this.matterbridge.matterLoggerFile), 'Enable the matter log on file in the settings to download the matter log.', 'utf-8');
436
+ this.log.debug(`Error in /api/download-mblog: ${error instanceof Error ? error.message : error}`);
406
437
  }
407
- res.download(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), 'matter.log', (error) => {
438
+ res.type('text/plain');
439
+ res.download(path.join(os.tmpdir(), this.matterbridge.matterLoggerFile), 'matter.log', (error) => {
408
440
  if (error) {
409
441
  this.log.error(`Error downloading log file ${this.matterbridge.matterLoggerFile}: ${error instanceof Error ? error.message : error}`);
410
442
  res.status(500).send('Error downloading the matter log file');
@@ -415,11 +447,15 @@ export class Frontend {
415
447
  this.log.debug('The frontend sent /api/shellydownloadsystemlog');
416
448
  try {
417
449
  await fs.access(path.join(this.matterbridge.matterbridgeDirectory, 'shelly.log'), fs.constants.F_OK);
450
+ const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'shelly.log'), 'utf8');
451
+ await fs.writeFile(path.join(os.tmpdir(), 'shelly.log'), data, 'utf-8');
418
452
  }
419
453
  catch (error) {
420
- fs.appendFile(path.join(this.matterbridge.matterbridgeDirectory, 'shelly.log'), 'Create the Shelly system log before downloading it.');
454
+ await fs.writeFile(path.join(os.tmpdir(), 'shelly.log'), 'Create the Shelly system log before downloading it.', 'utf-8');
455
+ this.log.debug(`Error in /api/shellydownloadsystemlog: ${error instanceof Error ? error.message : error}`);
421
456
  }
422
- res.download(path.join(this.matterbridge.matterbridgeDirectory, 'shelly.log'), 'shelly.log', (error) => {
457
+ res.type('text/plain');
458
+ res.download(path.join(os.tmpdir(), 'shelly.log'), 'shelly.log', (error) => {
423
459
  if (error) {
424
460
  this.log.error(`Error downloading Shelly system log file: ${error instanceof Error ? error.message : error}`);
425
461
  res.status(500).send('Error downloading Shelly system log file');
@@ -814,9 +850,8 @@ export class Frontend {
814
850
  res.status(500).send(`Error uploading or installing plugin package ${filename}`);
815
851
  }
816
852
  });
817
- this.expressApp.get('*', (req, res) => {
853
+ this.expressApp.use((req, res) => {
818
854
  this.log.debug('The frontend sent:', req.url);
819
- this.log.debug('Response send file:', path.join(this.matterbridge.rootDirectory, 'frontend/build/index.html'));
820
855
  res.sendFile(path.join(this.matterbridge.rootDirectory, 'frontend/build/index.html'));
821
856
  });
822
857
  this.log.debug(`Frontend initialized on port ${YELLOW}${this.port}${db} static ${UNDERLINE}${path.join(this.matterbridge.rootDirectory, 'frontend/build')}${UNDERLINEOFF}${rs}`);
@@ -919,6 +954,28 @@ export class Frontend {
919
954
  return true;
920
955
  return false;
921
956
  }
957
+ getPowerSource(device) {
958
+ if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
959
+ return undefined;
960
+ const powerSource = (device) => {
961
+ const featureMap = device.getAttribute(PowerSource.Cluster.id, 'featureMap');
962
+ if (featureMap.wired) {
963
+ const wiredCurrentType = device.getAttribute(PowerSource.Cluster.id, 'wiredCurrentType');
964
+ return ['ac', 'dc'][wiredCurrentType];
965
+ }
966
+ if (featureMap.battery) {
967
+ const batChargeLevel = device.getAttribute(PowerSource.Cluster.id, 'batChargeLevel');
968
+ return ['ok', 'warning', 'critical'][batChargeLevel];
969
+ }
970
+ return;
971
+ };
972
+ if (device.hasClusterServer(PowerSource.Cluster.id))
973
+ return powerSource(device);
974
+ for (const child of device.getChildEndpoints()) {
975
+ if (child.hasClusterServer(PowerSource.Cluster.id))
976
+ return powerSource(child);
977
+ }
978
+ }
922
979
  getClusterTextFromDevice(device) {
923
980
  if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
924
981
  return '';
@@ -958,6 +1015,7 @@ export class Frontend {
958
1015
  return '';
959
1016
  };
960
1017
  let attributes = '';
1018
+ let supportedModes = [];
961
1019
  device.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
962
1020
  if (typeof attributeValue === 'undefined')
963
1021
  return;
@@ -975,6 +1033,20 @@ export class Frontend {
975
1033
  attributes += `Heat to: ${attributeValue / 100}°C `;
976
1034
  if (clusterName === 'thermostat' && attributeName === 'occupiedCoolingSetpoint' && isValidNumber(attributeValue))
977
1035
  attributes += `Cool to: ${attributeValue / 100}°C `;
1036
+ const modeClusters = ['modeSelect', 'rvcRunMode', 'rvcCleanMode', 'laundryWasherMode', 'ovenMode', 'microwaveOvenMode'];
1037
+ if (modeClusters.includes(clusterName) && attributeName === 'supportedModes') {
1038
+ supportedModes = attributeValue;
1039
+ }
1040
+ if (modeClusters.includes(clusterName) && attributeName === 'currentMode') {
1041
+ const supportedMode = supportedModes.find((mode) => mode.mode === attributeValue);
1042
+ if (supportedMode)
1043
+ attributes += `Mode: ${supportedMode.label} `;
1044
+ else
1045
+ attributes += `Mode: ${attributeValue} `;
1046
+ }
1047
+ const operationalStateClusters = ['operationalState', 'rvcOperationalState'];
1048
+ if (operationalStateClusters.includes(clusterName) && attributeName === 'operationalState')
1049
+ attributes += `OpState: ${attributeValue} `;
978
1050
  if (clusterName === 'pumpConfigurationAndControl' && attributeName === 'operationMode')
979
1051
  attributes += `Mode: ${attributeValue} `;
980
1052
  if (clusterName === 'valveConfigurationAndControl' && attributeName === 'currentState')
@@ -1053,6 +1125,7 @@ export class Frontend {
1053
1125
  changelog: plugin.changelog,
1054
1126
  funding: plugin.funding,
1055
1127
  latestVersion: plugin.latestVersion,
1128
+ serialNumber: plugin.serialNumber,
1056
1129
  locked: plugin.locked,
1057
1130
  error: plugin.error,
1058
1131
  enabled: plugin.enabled,
@@ -1244,6 +1317,7 @@ export class Frontend {
1244
1317
  configUrl: device.configUrl,
1245
1318
  uniqueId: device.uniqueId,
1246
1319
  reachable: this.getReachability(device),
1320
+ powerSource: this.getPowerSource(device),
1247
1321
  cluster: cluster,
1248
1322
  });
1249
1323
  });
@@ -1396,7 +1470,7 @@ export class Frontend {
1396
1470
  return;
1397
1471
  }
1398
1472
  this.log.notice(`Action ${CYAN}${data.params.action}${nt}${data.params.value ? ' with ' + CYAN + data.params.value + nt : ''} for plugin ${CYAN}${plugin.name}${nt}`);
1399
- plugin.platform?.onAction(data.params.action, data.params.value, data.params.id).catch((error) => {
1473
+ plugin.platform?.onAction(data.params.action, data.params.value, data.params.id, data.params.formData).catch((error) => {
1400
1474
  this.log.error(`Error in plugin ${plugin.name} action ${data.params.action}: ${error}`);
1401
1475
  });
1402
1476
  }
@@ -1503,6 +1577,7 @@ export class Frontend {
1503
1577
  message = message.replace(/[\t\n]/g, '');
1504
1578
  message = message.replace(/[\x00-\x1F\x7F]/g, '');
1505
1579
  message = message.replace(/\\"/g, '"');
1580
+ message = message.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1506
1581
  const maxContinuousLength = 100;
1507
1582
  const keepStartLength = 20;
1508
1583
  const keepEndLength = 20;
@@ -5,7 +5,7 @@ import EventEmitter from 'node:events';
5
5
  import { inspect } from 'node:util';
6
6
  import { AnsiLogger, UNDERLINE, UNDERLINEOFF, YELLOW, db, debugStringify, BRIGHT, RESET, er, nf, rs, wr, RED, GREEN, zb, CYAN } from './logger/export.js';
7
7
  import { NodeStorageManager } from './storage/export.js';
8
- import { getParameter, getIntParameter, hasParameter, copyDirectory, withTimeout } from './utils/export.js';
8
+ import { getParameter, getIntParameter, hasParameter, copyDirectory, withTimeout, waiter } from './utils/export.js';
9
9
  import { logInterfaces, getGlobalNodeModules } from './utils/network.js';
10
10
  import { PluginManager } from './pluginManager.js';
11
11
  import { DeviceManager } from './deviceManager.js';
@@ -51,6 +51,8 @@ export class Matterbridge extends EventEmitter {
51
51
  globalModulesDirectory: '',
52
52
  matterbridgeVersion: '',
53
53
  matterbridgeLatestVersion: '',
54
+ matterbridgeDevVersion: '',
55
+ matterbridgeSerialNumber: '',
54
56
  matterbridgeQrPairingCode: undefined,
55
57
  matterbridgeManualPairingCode: undefined,
56
58
  matterbridgeFabricInformations: [],
@@ -85,6 +87,7 @@ export class Matterbridge extends EventEmitter {
85
87
  globalModulesDirectory = '';
86
88
  matterbridgeVersion = '';
87
89
  matterbridgeLatestVersion = '';
90
+ matterbridgeDevVersion = '';
88
91
  matterbridgeQrPairingCode = undefined;
89
92
  matterbridgeManualPairingCode = undefined;
90
93
  matterbridgeFabricInformations = undefined;
@@ -491,6 +494,13 @@ export class Matterbridge extends EventEmitter {
491
494
  - nosudo: force not to use sudo to install or update packages if the internal logic fails
492
495
  - norestore: force not to automatically restore the matterbridge node storage and the matter storage from backup if it is corrupted
493
496
  - ssl: enable SSL for the frontend and WebSockerServer (certificates in .matterbridge/certs directory cert.pem, key.pem and ca.pem (optional))
497
+ - vendorId: override the default vendorId 0xfff1
498
+ - vendorName: override the default vendorName "Matterbridge"
499
+ - productId: override the default productId 0x8000
500
+ - productName: override the default productName "Matterbridge aggregator"
501
+ - service: enable the service mode (used in the systemctl configuration file)
502
+ - docker: enable the docker mode (used in the Dockerfile to build the docker image)
503
+ - homedir: override the home directory (default: os.homedir())
494
504
  - add [plugin path]: register the plugin from the given absolute or relative path
495
505
  - add [plugin name]: register the globally installed plugin with the given name
496
506
  - remove [plugin path]: remove the plugin from the given absolute or relative path
@@ -625,7 +635,7 @@ export class Matterbridge extends EventEmitter {
625
635
  this.checkUpdateInterval = setInterval(async () => {
626
636
  const { checkUpdates } = await import('./update.js');
627
637
  checkUpdates(this);
628
- }, 24 * 60 * 60 * 1000).unref();
638
+ }, 12 * 60 * 60 * 1000).unref();
629
639
  if (hasParameter('test')) {
630
640
  this.bridgeMode = 'bridge';
631
641
  MatterbridgeEndpoint.bridgeMode = 'bridge';
@@ -687,11 +697,15 @@ export class Matterbridge extends EventEmitter {
687
697
  process.removeAllListeners('uncaughtException');
688
698
  process.removeAllListeners('unhandledRejection');
689
699
  this.exceptionHandler = async (error) => {
690
- this.log.error('Unhandled Exception detected at:', error.stack || error, rs);
700
+ const errorMessage = error instanceof Error ? error.message : error;
701
+ const errorInspect = inspect(error, { depth: 10 });
702
+ this.log.error(`Unhandled Exception detected: ${errorMessage}\nstack: ${errorInspect}}`);
691
703
  };
692
704
  process.on('uncaughtException', this.exceptionHandler);
693
705
  this.rejectionHandler = async (reason, promise) => {
694
- this.log.error('Unhandled Rejection detected at:', promise, 'reason:', reason instanceof Error ? reason.stack : reason, rs);
706
+ const errorMessage = reason instanceof Error ? reason.message : reason;
707
+ const errorInspect = inspect(reason, { depth: 10 });
708
+ this.log.error(`Unhandled Rejection detected: ${promise}\nreason: ${errorMessage}\nstack: ${errorInspect}`);
695
709
  };
696
710
  process.on('unhandledRejection', this.rejectionHandler);
697
711
  this.log.debug(`Registering SIGINT and SIGTERM signal handlers...`);
@@ -1165,6 +1179,7 @@ export class Matterbridge extends EventEmitter {
1165
1179
  plugin.device = device;
1166
1180
  plugin.storageContext = await this.createServerNodeContext(plugin.name, device.deviceName, DeviceTypeId(device.deviceType), device.vendorId, device.vendorName, device.productId, device.productName);
1167
1181
  plugin.serverNode = await this.createServerNode(plugin.storageContext, this.port ? this.port++ : undefined, this.passcode ? this.passcode++ : undefined, this.discriminator ? this.discriminator++ : undefined);
1182
+ plugin.serialNumber = await plugin.storageContext.get('serialNumber', '');
1168
1183
  this.log.debug(`Adding ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db} to ${plg}${plugin.name}${db} server node`);
1169
1184
  await plugin.serverNode.add(device);
1170
1185
  if (start)
@@ -1174,9 +1189,10 @@ export class Matterbridge extends EventEmitter {
1174
1189
  async createDynamicPlugin(plugin, start = false) {
1175
1190
  if (!plugin.locked) {
1176
1191
  plugin.locked = true;
1177
- plugin.storageContext = await this.createServerNodeContext(plugin.name, 'Matterbridge', bridge.code, this.aggregatorVendorId, 'Matterbridge', this.aggregatorProductId, plugin.description);
1192
+ plugin.storageContext = await this.createServerNodeContext(plugin.name, 'Matterbridge', bridge.code, this.aggregatorVendorId, this.aggregatorVendorName, this.aggregatorProductId, plugin.description);
1178
1193
  plugin.serverNode = await this.createServerNode(plugin.storageContext, this.port ? this.port++ : undefined, this.passcode ? this.passcode++ : undefined, this.discriminator ? this.discriminator++ : undefined);
1179
1194
  plugin.aggregatorNode = await this.createAggregatorNode(plugin.storageContext);
1195
+ plugin.serialNumber = await plugin.storageContext.get('serialNumber', '');
1180
1196
  await plugin.serverNode.add(plugin.aggregatorNode);
1181
1197
  if (start)
1182
1198
  await this.startServerNode(plugin.serverNode);
@@ -1249,13 +1265,6 @@ export class Matterbridge extends EventEmitter {
1249
1265
  async startChildbridge() {
1250
1266
  if (!this.matterStorageManager)
1251
1267
  throw new Error('No storage manager initialized');
1252
- for (const plugin of this.plugins) {
1253
- if (!plugin.enabled)
1254
- continue;
1255
- if (plugin.type === 'DynamicPlatform') {
1256
- await this.createDynamicPlugin(plugin);
1257
- }
1258
- }
1259
1268
  await this.startPlugins();
1260
1269
  this.log.debug('Starting start matter interval in childbridge mode...');
1261
1270
  let failCount = 0;
@@ -1358,6 +1367,7 @@ export class Matterbridge extends EventEmitter {
1358
1367
  this.matterStorageManager = await this.matterStorageService.open('Matterbridge');
1359
1368
  this.log.info('Matter node storage manager "Matterbridge" created');
1360
1369
  this.matterbridgeContext = await this.createServerNodeContext('Matterbridge', 'Matterbridge', bridge.code, this.aggregatorVendorId, this.aggregatorVendorName, this.aggregatorProductId, this.aggregatorProductName);
1370
+ this.matterbridgeInformation.matterbridgeSerialNumber = await this.matterbridgeContext.get('serialNumber', '');
1361
1371
  this.log.info('Matter node storage started');
1362
1372
  await this.backupMatterStorage(path.join(this.matterbridgeDirectory, this.matterStorageName), path.join(this.matterbridgeDirectory, this.matterStorageName + '.backup'));
1363
1373
  }
@@ -1368,7 +1378,7 @@ export class Matterbridge extends EventEmitter {
1368
1378
  }
1369
1379
  async stopMatterStorage() {
1370
1380
  this.log.info('Closing matter node storage...');
1371
- this.matterStorageManager?.close();
1381
+ await this.matterStorageManager?.close();
1372
1382
  this.matterStorageService = undefined;
1373
1383
  this.matterStorageManager = undefined;
1374
1384
  this.matterbridgeContext = undefined;
@@ -1507,6 +1517,8 @@ export class Matterbridge extends EventEmitter {
1507
1517
  }
1508
1518
  }
1509
1519
  setTimeout(() => {
1520
+ if (serverNode.lifecycle.isCommissioned)
1521
+ return;
1510
1522
  if (this.bridgeMode === 'bridge') {
1511
1523
  this.matterbridgeQrPairingCode = undefined;
1512
1524
  this.matterbridgeManualPairingCode = undefined;
@@ -1516,10 +1528,12 @@ export class Matterbridge extends EventEmitter {
1516
1528
  if (plugin) {
1517
1529
  plugin.qrPairingCode = undefined;
1518
1530
  plugin.manualPairingCode = undefined;
1519
- this.frontend.wssSendRefreshRequired('plugins');
1520
1531
  }
1521
1532
  }
1533
+ this.frontend.wssSendRefreshRequired('plugins');
1522
1534
  this.frontend.wssSendRefreshRequired('settings');
1535
+ this.frontend.wssSendRefreshRequired('fabrics');
1536
+ this.frontend.wssSendRefreshRequired('sessions');
1523
1537
  this.frontend.wssSendSnackbarMessage(`Advertising on server node for ${storeId} stopped. Restart to commission.`, 0);
1524
1538
  this.log.notice(`Advertising on server node for ${storeId} stopped. Restart to commission.`);
1525
1539
  }, 15 * 60 * 1000).unref();
@@ -1651,16 +1665,17 @@ export class Matterbridge extends EventEmitter {
1651
1665
  }
1652
1666
  if (this.bridgeMode === 'bridge') {
1653
1667
  this.log.debug(`Adding bridged endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} to Matterbridge aggregator node`);
1654
- if (!this.aggregatorNode)
1668
+ if (!this.aggregatorNode) {
1655
1669
  this.log.error('Aggregator node not found for Matterbridge');
1670
+ return;
1671
+ }
1656
1672
  try {
1657
- await this.aggregatorNode?.add(device);
1673
+ await this.aggregatorNode.add(device);
1658
1674
  }
1659
1675
  catch (error) {
1660
- const errorMessage = error instanceof Error ? error.message : '';
1661
- const errorStack = error instanceof Error ? error.stack : '';
1662
- const errorDebug = inspect(error, { depth: 10 });
1663
- this.log.error(`Error adding bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.id}${er}) for plugin ${plg}${pluginName}${er}: ${error} ${errorMessage} ${errorStack} ${errorDebug}`);
1676
+ const errorMessage = error instanceof Error ? error.message : error;
1677
+ const errorInspect = inspect(error, { depth: 10 });
1678
+ this.log.error(`Error adding bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.id}${er}) for plugin ${plg}${pluginName}${er}: ${errorMessage}\nstack: ${errorInspect}`);
1664
1679
  return;
1665
1680
  }
1666
1681
  }
@@ -1668,29 +1683,34 @@ export class Matterbridge extends EventEmitter {
1668
1683
  if (plugin.type === 'AccessoryPlatform') {
1669
1684
  try {
1670
1685
  this.log.debug(`Creating endpoint ${dev}${device.deviceName}${db} for AccessoryPlatform plugin ${plg}${plugin.name}${db} server node`);
1686
+ if (plugin.serverNode) {
1687
+ this.log.error(`The plugin ${plg}${plugin.name}${er} has already added a device. Only one device is allowed per AccessoryPlatform plugin.`);
1688
+ return;
1689
+ }
1671
1690
  await this.createAccessoryPlugin(plugin, device);
1672
1691
  }
1673
1692
  catch (error) {
1674
- const errorMessage = error instanceof Error ? error.message : '';
1675
- const errorStack = error instanceof Error ? error.stack : '';
1676
- const errorDebug = inspect(error, { depth: 10 });
1677
- this.log.error(`Error creating endpoint ${dev}${device.deviceName}${er} (${zb}${device.id}${er}) for AccessoryPlatform plugin ${plg}${pluginName}${er} server node: ${error} ${errorMessage} ${errorStack} ${errorDebug}`);
1693
+ const errorMessage = error instanceof Error ? error.message : error;
1694
+ const errorInspect = inspect(error, { depth: 10 });
1695
+ this.log.error(`Error creating endpoint ${dev}${device.deviceName}${er} (${zb}${device.id}${er}) for AccessoryPlatform plugin ${plg}${pluginName}${er} server node: ${errorMessage}\nstack: ${errorInspect}`);
1678
1696
  return;
1679
1697
  }
1680
1698
  }
1681
1699
  if (plugin.type === 'DynamicPlatform') {
1682
- plugin.locked = true;
1683
- this.log.debug(`Adding bridged endpoint ${dev}${device.deviceName}${db} for DynamicPlatform plugin ${plg}${plugin.name}${db} aggregator node`);
1684
- if (!plugin.aggregatorNode)
1685
- this.log.error(`Aggregator node not found for plugin ${plg}${plugin.name}${db}`);
1686
1700
  try {
1687
- await plugin.aggregatorNode?.add(device);
1701
+ this.log.debug(`Adding bridged endpoint ${dev}${device.deviceName}${db} for DynamicPlatform plugin ${plg}${plugin.name}${db} aggregator node`);
1702
+ await this.createDynamicPlugin(plugin);
1703
+ await waiter(`createDynamicPlugin(${plugin.name})`, () => plugin.serverNode?.hasParts === true);
1704
+ if (!plugin.aggregatorNode) {
1705
+ this.log.error(`Aggregator node not found for plugin ${plg}${plugin.name}${er}`);
1706
+ return;
1707
+ }
1708
+ await plugin.aggregatorNode.add(device);
1688
1709
  }
1689
1710
  catch (error) {
1690
- const errorMessage = error instanceof Error ? error.message : '';
1691
- const errorStack = error instanceof Error ? error.stack : '';
1692
- const errorDebug = inspect(error, { depth: 10 });
1693
- this.log.error(`Error adding bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.id}${er}) for DynamicPlatform plugin ${plg}${pluginName}${er} aggregator node: ${error} ${errorMessage} ${errorStack} ${errorDebug}`);
1711
+ const errorMessage = error instanceof Error ? error.message : error;
1712
+ const errorInspect = inspect(error, { depth: 10 });
1713
+ this.log.error(`Error adding bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.id}${er}) for DynamicPlatform plugin ${plg}${pluginName}${er} aggregator node: ${errorMessage}\nstack: ${errorInspect}`);
1694
1714
  return;
1695
1715
  }
1696
1716
  }
@@ -27,6 +27,7 @@ import { ElectricalEnergyMeasurement } from '@matter/main/clusters/electrical-en
27
27
  import { AirQuality } from '@matter/main/clusters/air-quality';
28
28
  import { ConcentrationMeasurement } from '@matter/main/clusters/concentration-measurement';
29
29
  import { OccupancySensing } from '@matter/main/clusters/occupancy-sensing';
30
+ import { ThermostatUserInterfaceConfiguration } from '@matter/main/clusters/thermostat-user-interface-configuration';
30
31
  import { DescriptorServer } from '@matter/main/behaviors/descriptor';
31
32
  import { PowerSourceServer } from '@matter/main/behaviors/power-source';
32
33
  import { BridgedDeviceBasicInformationServer } from '@matter/main/behaviors/bridged-device-basic-information';
@@ -59,6 +60,7 @@ import { FanControlServer } from '@matter/main/behaviors/fan-control';
59
60
  import { ResourceMonitoring } from '@matter/main/clusters/resource-monitoring';
60
61
  import { HepaFilterMonitoringServer } from '@matter/main/behaviors/hepa-filter-monitoring';
61
62
  import { ActivatedCarbonFilterMonitoringServer } from '@matter/main/behaviors/activated-carbon-filter-monitoring';
63
+ import { ThermostatUserInterfaceConfigurationServer } from '@matter/main/behaviors/thermostat-user-interface-configuration';
62
64
  export class MatterbridgeEndpoint extends Endpoint {
63
65
  static bridgeMode = '';
64
66
  static logLevel = "info";
@@ -760,6 +762,14 @@ export class MatterbridgeEndpoint extends Endpoint {
760
762
  });
761
763
  return this;
762
764
  }
765
+ createDefaultThermostatUserInterfaceConfigurationClusterServer() {
766
+ this.behaviors.require(ThermostatUserInterfaceConfigurationServer, {
767
+ temperatureDisplayMode: ThermostatUserInterfaceConfiguration.TemperatureDisplayMode.Celsius,
768
+ keypadLockout: ThermostatUserInterfaceConfiguration.KeypadLockout.NoLockout,
769
+ scheduleProgrammingVisibility: ThermostatUserInterfaceConfiguration.ScheduleProgrammingVisibility.ScheduleProgrammingPermitted,
770
+ });
771
+ return this;
772
+ }
763
773
  createDefaultFanControlClusterServer(fanMode = FanControl.FanMode.Off) {
764
774
  this.behaviors.require(MatterbridgeFanControlServer.with(FanControl.Feature.MultiSpeed, FanControl.Feature.Auto, FanControl.Feature.Step), {
765
775
  fanMode,
@@ -782,18 +792,24 @@ export class MatterbridgeEndpoint extends Endpoint {
782
792
  return this;
783
793
  }
784
794
  createDefaultHepaFilterMonitoringClusterServer(changeIndication = ResourceMonitoring.ChangeIndication.Ok, inPlaceIndicator = undefined, lastChangedTime = undefined) {
785
- this.behaviors.require(HepaFilterMonitoringServer, {
795
+ this.behaviors.require(HepaFilterMonitoringServer.with(ResourceMonitoring.Feature.Condition, ResourceMonitoring.Feature.ReplacementProductList), {
796
+ condition: 100,
797
+ degradationDirection: ResourceMonitoring.DegradationDirection.Down,
786
798
  changeIndication,
787
799
  inPlaceIndicator,
788
800
  lastChangedTime,
801
+ replacementProductList: [],
789
802
  });
790
803
  return this;
791
804
  }
792
805
  createDefaultActivatedCarbonFilterMonitoringClusterServer(changeIndication = ResourceMonitoring.ChangeIndication.Ok, inPlaceIndicator = undefined, lastChangedTime = undefined) {
793
- this.behaviors.require(ActivatedCarbonFilterMonitoringServer, {
806
+ this.behaviors.require(ActivatedCarbonFilterMonitoringServer.with(ResourceMonitoring.Feature.Condition, ResourceMonitoring.Feature.ReplacementProductList), {
807
+ condition: 100,
808
+ degradationDirection: ResourceMonitoring.DegradationDirection.Down,
794
809
  changeIndication,
795
810
  inPlaceIndicator,
796
811
  lastChangedTime,
812
+ replacementProductList: [],
797
813
  });
798
814
  return this;
799
815
  }
@@ -399,6 +399,20 @@ export function getAttributeId(endpoint, cluster, attribute) {
399
399
  else if (attribute === 'levelValue')
400
400
  return 0xa;
401
401
  }
402
+ if (endpoint.behaviors.supported[lowercaseFirstLetter(cluster)]?.schema?.type === 'OperationalState') {
403
+ if (attribute === 'phaseList')
404
+ return 0x0;
405
+ else if (attribute === 'currentPhase')
406
+ return 0x1;
407
+ else if (attribute === 'countdownTime')
408
+ return 0x2;
409
+ else if (attribute === 'operationalStateList')
410
+ return 0x3;
411
+ else if (attribute === 'operationalState')
412
+ return 0x4;
413
+ else if (attribute === 'operationalError')
414
+ return 0x5;
415
+ }
402
416
  return endpoint.behaviors.supported[lowercaseFirstLetter(cluster)]?.schema?.children?.find((child) => child.name === capitalizeFirstLetter(attribute))?.id;
403
417
  }
404
418
  }
@@ -84,8 +84,8 @@ export class MatterbridgePlatform {
84
84
  async onChangeLoggerLevel(logLevel) {
85
85
  this.log.debug(`The plugin doesn't override onChangeLoggerLevel. Logger level set to: ${logLevel}`);
86
86
  }
87
- async onAction(action, value, id) {
88
- this.log.debug(`The plugin ${CYAN}${this.name}${db} doesn't override onAction. Received action ${CYAN}${action}${db}${value ? ' with ' + CYAN + value + db : ''} ${id ? ' for schema ' + CYAN + id + db : ''}`);
87
+ async onAction(action, value, id, formData) {
88
+ this.log.debug(`The plugin ${CYAN}${this.name}${db} doesn't override onAction. Received action ${CYAN}${action}${db}${value ? ' with ' + CYAN + value + db : ''} ${id ? ' for schema ' + CYAN + id + db : ''}`, formData);
89
89
  }
90
90
  async onConfigChanged(config) {
91
91
  this.log.debug(`The plugin ${CYAN}${config.name}${db} doesn't override onConfigChanged. Received new config.`);
package/dist/update.js CHANGED
@@ -3,6 +3,7 @@ import { db, nt, wr } from './logger/export.js';
3
3
  export async function checkUpdates(matterbridge) {
4
4
  const { hasParameter } = await import('./utils/parameter.js');
5
5
  getMatterbridgeLatestVersion(matterbridge);
6
+ getMatterbridgeDevVersion(matterbridge);
6
7
  for (const plugin of matterbridge.plugins) {
7
8
  getPluginLatestVersion(matterbridge, plugin);
8
9
  }
@@ -32,6 +33,23 @@ async function getMatterbridgeLatestVersion(matterbridge) {
32
33
  matterbridge.log.warn(`Error getting Matterbridge latest version: ${error.message}`);
33
34
  });
34
35
  }
36
+ async function getMatterbridgeDevVersion(matterbridge) {
37
+ const { getNpmPackageVersion } = await import('./utils/network.js');
38
+ getNpmPackageVersion('matterbridge', 'dev')
39
+ .then(async (version) => {
40
+ matterbridge.matterbridgeDevVersion = version;
41
+ matterbridge.matterbridgeInformation.matterbridgeDevVersion = version;
42
+ await matterbridge.nodeContext?.set('matterbridgeDevVersion', version);
43
+ if (matterbridge.matterbridgeVersion.includes('-dev.') && matterbridge.matterbridgeVersion !== version) {
44
+ matterbridge.log.notice(`Matterbridge@dev is out of date. Current version: ${matterbridge.matterbridgeVersion}. Latest dev version: ${matterbridge.matterbridgeDevVersion}.`);
45
+ matterbridge.frontend.wssSendRefreshRequired('matterbridgeDevVersion');
46
+ matterbridge.frontend.wssSendUpdateRequired();
47
+ }
48
+ })
49
+ .catch((error) => {
50
+ matterbridge.log.warn(`Error getting Matterbridge latest dev version: ${error.message}`);
51
+ });
52
+ }
35
53
  async function getPluginLatestVersion(matterbridge, plugin) {
36
54
  const { getNpmPackageVersion } = await import('./utils/network.js');
37
55
  getNpmPackageVersion(plugin.name)
@@ -1,3 +1,10 @@
1
+ export function hasParameter(name) {
2
+ const commandArguments = process.argv.slice(2);
3
+ let markerIncluded = commandArguments.includes(`-${name}`);
4
+ if (!markerIncluded)
5
+ markerIncluded = commandArguments.includes(`--${name}`);
6
+ return markerIncluded;
7
+ }
1
8
  import { isValidNumber } from './export.js';
2
9
  export function getParameter(name) {
3
10
  const commandArguments = process.argv.slice(2);
@@ -8,13 +15,6 @@ export function getParameter(name) {
8
15
  return undefined;
9
16
  return commandArguments[markerIndex + 1];
10
17
  }
11
- export function hasParameter(name) {
12
- const commandArguments = process.argv.slice(2);
13
- let markerIncluded = commandArguments.includes(`-${name}`);
14
- if (!markerIncluded)
15
- markerIncluded = commandArguments.includes(`--${name}`);
16
- return markerIncluded;
17
- }
18
18
  export function getIntParameter(name) {
19
19
  const value = getParameter(name);
20
20
  if (value === undefined)
@@ -24,3 +24,35 @@ export function getIntParameter(name) {
24
24
  return undefined;
25
25
  return intValue;
26
26
  }
27
+ export function getIntArrayParameter(name) {
28
+ const commandArguments = process.argv.slice(2);
29
+ let markerIndex = commandArguments.indexOf(`--${name}`);
30
+ if (markerIndex < 0)
31
+ markerIndex = commandArguments.indexOf(`-${name}`);
32
+ if (markerIndex < 0)
33
+ return undefined;
34
+ const intValues = [];
35
+ for (let i = markerIndex + 1; i < commandArguments.length && !commandArguments[i].startsWith('-'); i++) {
36
+ const intValue = parseInt(commandArguments[i], 10);
37
+ if (isValidNumber(intValue))
38
+ intValues.push(intValue);
39
+ }
40
+ if (intValues.length === 0)
41
+ return undefined;
42
+ return intValues;
43
+ }
44
+ export function getStringArrayParameter(name) {
45
+ const commandArguments = process.argv.slice(2);
46
+ let markerIndex = commandArguments.indexOf(`--${name}`);
47
+ if (markerIndex < 0)
48
+ markerIndex = commandArguments.indexOf(`-${name}`);
49
+ if (markerIndex < 0)
50
+ return undefined;
51
+ const values = [];
52
+ for (let i = markerIndex + 1; i < commandArguments.length && !commandArguments[i].startsWith('-'); i++) {
53
+ values.push(commandArguments[i]);
54
+ }
55
+ if (values.length === 0)
56
+ return undefined;
57
+ return values;
58
+ }
@@ -1,6 +1,8 @@
1
1
  import { AnsiLogger } from '../logger/export.js';
2
2
  const log = new AnsiLogger({ logName: 'MatterbridgeUtils', logTimestampFormat: 4, logLevel: "info" });
3
3
  export async function waiter(name, check, exitWithReject = false, resolveTimeout = 5000, resolveInterval = 500, debug = false) {
4
+ if (check())
5
+ return true;
4
6
  log.logLevel = "debug";
5
7
  log.logName = 'Waiter';
6
8
  if (debug)