matterbridge 1.4.0 → 1.4.1

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 (37) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/README.md +7 -4
  3. package/dist/deviceManager.d.ts +46 -0
  4. package/dist/deviceManager.d.ts.map +1 -0
  5. package/dist/deviceManager.js +92 -0
  6. package/dist/deviceManager.js.map +1 -0
  7. package/dist/matterbridge.d.ts +18 -76
  8. package/dist/matterbridge.d.ts.map +1 -1
  9. package/dist/matterbridge.js +218 -440
  10. package/dist/matterbridge.js.map +1 -1
  11. package/dist/matterbridgeDevice.d.ts +18 -5
  12. package/dist/matterbridgeDevice.d.ts.map +1 -1
  13. package/dist/matterbridgeDevice.js +9 -5
  14. package/dist/matterbridgeDevice.js.map +1 -1
  15. package/dist/matterbridgePlatform.d.ts +6 -1
  16. package/dist/matterbridgePlatform.d.ts.map +1 -1
  17. package/dist/matterbridgePlatform.js +8 -0
  18. package/dist/matterbridgePlatform.js.map +1 -1
  19. package/dist/matterbridgeTypes.d.ts +28 -1
  20. package/dist/matterbridgeTypes.d.ts.map +1 -1
  21. package/dist/matterbridgeTypes.js +26 -1
  22. package/dist/matterbridgeTypes.js.map +1 -1
  23. package/dist/{plugins.d.ts → pluginManager.d.ts} +6 -3
  24. package/dist/pluginManager.d.ts.map +1 -0
  25. package/dist/{plugins.js → pluginManager.js} +26 -16
  26. package/dist/pluginManager.js.map +1 -0
  27. package/frontend/build/asset-manifest.json +6 -6
  28. package/frontend/build/index.html +1 -1
  29. package/frontend/build/static/css/{main.df840158.css → main.5174e68c.css} +2 -2
  30. package/frontend/build/static/css/{main.df840158.css.map → main.5174e68c.css.map} +1 -1
  31. package/frontend/build/static/js/{main.2a46688a.js → main.fd6f85a1.js} +3 -3
  32. package/frontend/build/static/js/main.fd6f85a1.js.map +1 -0
  33. package/package.json +12 -11
  34. package/dist/plugins.d.ts.map +0 -1
  35. package/dist/plugins.js.map +0 -1
  36. package/frontend/build/static/js/main.2a46688a.js.map +0 -1
  37. /package/frontend/build/static/js/{main.2a46688a.js.LICENSE.txt → main.fd6f85a1.js.LICENSE.txt} +0 -0
@@ -4,7 +4,7 @@
4
4
  * @file matterbridge.ts
5
5
  * @author Luca Liguori
6
6
  * @date 2023-12-29
7
- * @version 1.3.2
7
+ * @version 1.4.0
8
8
  *
9
9
  * Copyright 2023, 2024 Luca Liguori.
10
10
  *
@@ -21,8 +21,8 @@
21
21
  * limitations under the License. *
22
22
  */
23
23
  import { NodeStorageManager } from 'node-persist-manager';
24
- import { AnsiLogger, BRIGHT, RESET, UNDERLINE, UNDERLINEOFF, YELLOW, db, debugStringify, stringify, er, nf, rs, wr, RED, GREEN, zb, hk, or, idn, BLUE, CYAN } from 'node-ansi-logger';
25
- import { fileURLToPath, pathToFileURL } from 'url';
24
+ import { AnsiLogger, UNDERLINE, UNDERLINEOFF, YELLOW, db, debugStringify, stringify, BRIGHT, RESET, er, nf, rs, wr, RED, GREEN, zb, hk, or, idn, BLUE, CYAN, nt } from 'node-ansi-logger';
25
+ import { fileURLToPath } from 'url';
26
26
  import { promises as fs } from 'fs';
27
27
  import { exec, spawn } from 'child_process';
28
28
  import { createServer } from 'http';
@@ -34,7 +34,6 @@ import path from 'path';
34
34
  import WebSocket, { WebSocketServer } from 'ws';
35
35
  // Matterbridge
36
36
  import { MatterbridgeDevice } from './matterbridgeDevice.js';
37
- import { shelly_config, somfytahoma_config, zigbee2mqtt_config } from './defaultConfigSchema.js';
38
37
  import { BridgedDeviceBasicInformation, BridgedDeviceBasicInformationCluster } from './cluster/BridgedDeviceBasicInformationCluster.js';
39
38
  import { logInterfaces, wait, waiter } from './utils/utils.js';
40
39
  // @project-chip/matter-node.js
@@ -47,7 +46,8 @@ import { ManualPairingCodeCodec, QrCodeSchema } from '@project-chip/matter-node.
47
46
  import { StorageBackendDisk, StorageBackendJsonFile, StorageManager } from '@project-chip/matter-node.js/storage';
48
47
  import { requireMinNodeVersion, getParameter, getIntParameter, hasParameter } from '@project-chip/matter-node.js/util';
49
48
  import { CryptoNode } from '@project-chip/matter-node.js/crypto';
50
- import { Plugins } from './plugins.js';
49
+ import { PluginManager } from './pluginManager.js';
50
+ import { DeviceManager } from './deviceManager.js';
51
51
  // Default colors
52
52
  const plg = '\u001B[38;5;33m';
53
53
  const dev = '\u001B[38;5;79m';
@@ -86,7 +86,7 @@ export class Matterbridge extends EventEmitter {
86
86
  matterbridgeConnected: false,
87
87
  bridgeMode: '',
88
88
  restartMode: '',
89
- debugEnabled: false,
89
+ loggerLevel: "info" /* LogLevel.INFO */,
90
90
  matterLoggerLevel: Level.INFO,
91
91
  };
92
92
  homeDirectory = '';
@@ -102,10 +102,12 @@ export class Matterbridge extends EventEmitter {
102
102
  matterbridgeConnected = false;
103
103
  bridgeMode = '';
104
104
  restartMode = '';
105
- debugEnabled = false;
105
+ // public debugEnabled = false;
106
+ // public loggerLevel: LogLevel = LogLevel.INFO;
106
107
  profile = getParameter('profile');
107
108
  log;
108
109
  plugins;
110
+ devices;
109
111
  registeredDevices = [];
110
112
  nodeStorage;
111
113
  nodeContext;
@@ -202,9 +204,37 @@ export class Matterbridge extends EventEmitter {
202
204
  if (hasParameter('docker'))
203
205
  this.restartMode = 'docker';
204
206
  // Set Matterbridge logger
205
- if (hasParameter('debug'))
206
- this.debugEnabled = true;
207
- this.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logDebug: this.debugEnabled });
207
+ this.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: "info" /* LogLevel.INFO */ });
208
+ // Set matter.js logger level and format
209
+ if (hasParameter('logger')) {
210
+ const level = getParameter('logger');
211
+ if (level === 'debug') {
212
+ this.log.logLevel = "debug" /* LogLevel.DEBUG */;
213
+ }
214
+ else if (level === 'info') {
215
+ this.log.logLevel = "info" /* LogLevel.INFO */;
216
+ }
217
+ else if (level === 'notice') {
218
+ this.log.logLevel = "notice" /* LogLevel.NOTICE */;
219
+ }
220
+ else if (level === 'warn') {
221
+ this.log.logLevel = "warn" /* LogLevel.WARN */;
222
+ }
223
+ else if (level === 'error') {
224
+ this.log.logLevel = "error" /* LogLevel.ERROR */;
225
+ }
226
+ else if (level === 'fatal') {
227
+ this.log.logLevel = "fatal" /* LogLevel.FATAL */;
228
+ }
229
+ else {
230
+ this.log.warn(`Invalid matterbridge logger level: ${level}. Using default level "info".`);
231
+ this.log.logLevel = "info" /* LogLevel.INFO */;
232
+ }
233
+ }
234
+ else {
235
+ this.log.logLevel = "info" /* LogLevel.INFO */;
236
+ }
237
+ MatterbridgeDevice.logLevel = this.log.logLevel;
208
238
  this.log.debug('Matterbridge is starting...');
209
239
  // Set matter.js logger level and format
210
240
  if (hasParameter('matterlogger')) {
@@ -228,7 +258,7 @@ export class Matterbridge extends EventEmitter {
228
258
  Logger.defaultLogLevel = Level.FATAL;
229
259
  }
230
260
  else {
231
- this.log.warn(`Invalid matterlogger level: ${level}. Using default level ${this.debugEnabled ? 'debug' : 'info'}.`);
261
+ this.log.warn(`Invalid matter.js logger level: ${level}. Using default level "info".`);
232
262
  Logger.defaultLogLevel = Level.INFO;
233
263
  }
234
264
  }
@@ -243,9 +273,11 @@ export class Matterbridge extends EventEmitter {
243
273
  this.nodeStorage = new NodeStorageManager({ dir: path.join(this.matterbridgeDirectory, this.nodeStorageName), writeQueue: false, expiredInterval: undefined, logging: false });
244
274
  this.log.debug('Creating node storage context for matterbridge');
245
275
  this.nodeContext = await this.nodeStorage.createStorage('matterbridge');
246
- // Initialize Plugins
247
- this.plugins = new Plugins(this);
276
+ // Initialize PluginManager
277
+ this.plugins = new PluginManager(this);
248
278
  await this.plugins.loadFromStorage();
279
+ // Initialize DeviceManager
280
+ this.devices = new DeviceManager(this, this.nodeContext);
249
281
  // Get the plugins from node storage and create the plugins node storage contexts
250
282
  for (const plugin of this.plugins) {
251
283
  const packageJson = await this.plugins.parse(plugin);
@@ -274,7 +306,7 @@ export class Matterbridge extends EventEmitter {
274
306
  }
275
307
  // Log system info and create .matterbridge directory
276
308
  await this.logNodeAndSystemInfo();
277
- this.log.info(`Matterbridge version ${this.matterbridgeVersion} mode ${hasParameter('bridge') ? 'bridge' : ''}${hasParameter('childbridge') ? 'childbridge' : ''}${hasParameter('controller') ? 'controller' : ''} ` +
309
+ this.log.notice(`Matterbridge version ${this.matterbridgeVersion} ${hasParameter('bridge') ? 'mode bridge' : ''}${hasParameter('childbridge') ? 'mode childbridge' : ''}${hasParameter('controller') ? 'mode controller' : ''} ` +
278
310
  `${this.restartMode !== '' ? 'restart mode ' + this.restartMode + ' ' : ''}running on ${this.systemInformation.osType} ${this.systemInformation.osRelease} ${this.systemInformation.osPlatform} ${this.systemInformation.osArch} `);
279
311
  // Check node version and throw error
280
312
  requireMinNodeVersion(18);
@@ -299,7 +331,7 @@ export class Matterbridge extends EventEmitter {
299
331
  - port [port]: start the commissioning server on the given port (default 5540)
300
332
  - mdnsinterface [name]: set the interface to use for the matter server mdnsInterface (default all interfaces)
301
333
  - frontend [port]: start the frontend on the given port (default 8283)
302
- - debug: enable the Matterbridge debug mode (default false)
334
+ - logger: set the matterbridge logger level: debug | info | notice | warn | error | fatal (default info)
303
335
  - matterlogger: set the matter.js logger level: debug | info | notice | warn | error | fatal (default info)
304
336
  - reset: remove the commissioning for Matterbridge (bridge mode). Shutdown Matterbridge before using it!
305
337
  - factoryreset: remove all commissioning information and reset all internal storages. Shutdown Matterbridge before using it!
@@ -417,13 +449,13 @@ export class Matterbridge extends EventEmitter {
417
449
  return;
418
450
  }
419
451
  // Start the matter storage and create the matterbridge context
420
- await this.startStorage('json', path.join(this.matterbridgeDirectory, this.matterStorageName));
452
+ await this.startMatterStorage('json', path.join(this.matterbridgeDirectory, this.matterStorageName));
421
453
  this.log.debug(`Creating commissioning server context for ${plg}Matterbridge${db}`);
422
454
  this.matterbridgeContext = await this.createCommissioningServerContext('Matterbridge', 'Matterbridge', DeviceTypes.AGGREGATOR.code, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge aggregator');
423
455
  if (hasParameter('reset') && getParameter('reset') === undefined) {
424
456
  this.log.info('Resetting Matterbridge commissioning information...');
425
457
  await this.matterbridgeContext?.clearAll();
426
- await this.stopStorage();
458
+ await this.stopMatterStorage();
427
459
  this.log.info('Reset done! Remove the device from the controller.');
428
460
  this.emit('shutdown');
429
461
  return;
@@ -443,7 +475,7 @@ export class Matterbridge extends EventEmitter {
443
475
  else {
444
476
  this.log.warn(`Plugin ${plg}${getParameter('reset')}${wr} not registerd in matterbridge`);
445
477
  }
446
- await this.stopStorage();
478
+ await this.stopMatterStorage();
447
479
  this.emit('shutdown');
448
480
  return;
449
481
  }
@@ -484,8 +516,8 @@ export class Matterbridge extends EventEmitter {
484
516
  this.log.debug('Adding matterbridge commissioning server to matter server');
485
517
  await this.matterServer.addCommissioningServer(this.commissioningServer, { uniqueStorageKey: 'Matterbridge' });
486
518
  for (const plugin of this.plugins) {
487
- plugin.configJson = await this.loadPluginConfig(plugin);
488
- plugin.schemaJson = await this.loadPluginSchema(plugin);
519
+ plugin.configJson = await this.plugins.loadConfig(plugin);
520
+ plugin.schemaJson = await this.plugins.loadSchema(plugin);
489
521
  // Check if the plugin is available
490
522
  if (!(await this.plugins.resolve(plugin.path))) {
491
523
  this.log.error(`Plugin ${plg}${plugin.name}${er} not found. Disabling it.`);
@@ -509,7 +541,7 @@ export class Matterbridge extends EventEmitter {
509
541
  plugin.addedDevices = undefined;
510
542
  plugin.qrPairingCode = undefined;
511
543
  plugin.manualPairingCode = undefined;
512
- this.loadPlugin(plugin, true, 'Matterbridge is starting'); // No await do it asyncronously
544
+ this.plugins.load(plugin, true, 'Matterbridge is starting'); // this.loadPlugin(plugin, true, 'Matterbridge is starting'); // No await do it asyncronously
513
545
  }
514
546
  await this.startBridge();
515
547
  return;
@@ -522,8 +554,8 @@ export class Matterbridge extends EventEmitter {
522
554
  this.log.debug('Starting matterbridge in mode', this.bridgeMode);
523
555
  this.matterServer = this.createMatterServer(this.storageManager);
524
556
  for (const plugin of this.plugins) {
525
- plugin.configJson = await this.loadPluginConfig(plugin);
526
- plugin.schemaJson = await this.loadPluginSchema(plugin);
557
+ plugin.configJson = await this.plugins.loadConfig(plugin);
558
+ plugin.schemaJson = await this.plugins.loadSchema(plugin);
527
559
  // Check if the plugin is available
528
560
  if (!(await this.plugins.resolve(plugin.path))) {
529
561
  this.log.error(`Plugin ${plg}${plugin.name}${er} not found. Disabling it.`);
@@ -547,7 +579,7 @@ export class Matterbridge extends EventEmitter {
547
579
  plugin.addedDevices = undefined;
548
580
  plugin.qrPairingCode = (await plugin.nodeContext?.get('qrPairingCode', undefined)) ?? undefined;
549
581
  plugin.manualPairingCode = (await plugin.nodeContext?.get('manualPairingCode', undefined)) ?? undefined;
550
- this.loadPlugin(plugin, true, 'Matterbridge is starting'); // No await do it asyncronously
582
+ this.plugins.load(plugin, true, 'Matterbridge is starting'); // this.loadPlugin(plugin, true, 'Matterbridge is starting'); // No await do it asyncronously
551
583
  }
552
584
  await this.startChildbridge();
553
585
  return;
@@ -945,8 +977,8 @@ export class Matterbridge extends EventEmitter {
945
977
  // this.cleanupTimeout1 = setTimeout(async () => {
946
978
  // Closing matter
947
979
  await this.stopMatterServer();
948
- // Closing storage
949
- await this.stopStorage();
980
+ // Closing matter storage
981
+ await this.stopMatterStorage();
950
982
  // Serialize registeredDevices
951
983
  if (this.nodeStorage && this.nodeContext) {
952
984
  this.log.info('Saving registered devices...');
@@ -1010,7 +1042,7 @@ export class Matterbridge extends EventEmitter {
1010
1042
  await fs.rm(path.join(this.matterbridgeDirectory, this.nodeStorageName), { recursive: true });
1011
1043
  this.log.info('Factory reset done! Remove all paired devices from the controllers.');
1012
1044
  }
1013
- this.log.info('Cleanup completed. Shutting down...');
1045
+ this.log.notice('Cleanup completed. Shutting down...');
1014
1046
  Matterbridge.instance = undefined;
1015
1047
  this.emit('shutdown');
1016
1048
  }
@@ -1027,10 +1059,6 @@ export class Matterbridge extends EventEmitter {
1027
1059
  * @returns {Promise<void>} - A promise that resolves when the device is added.
1028
1060
  */
1029
1061
  async addBridgedDevice(pluginName, device) {
1030
- if (this.bridgeMode === 'bridge' && !this.matterAggregator) {
1031
- this.log.error(`Adding bridged device ${dev}${device.deviceName}${er} (${dev}${device.name}${er}) for plugin ${plg}${pluginName}${er} error: matterAggregator not found`);
1032
- return;
1033
- }
1034
1062
  this.log.debug(`Adding bridged device ${dev}${device.deviceName}${db} (${dev}${device.name}${db}) for plugin ${plg}${pluginName}${db}`);
1035
1063
  // Check if the plugin is registered
1036
1064
  const plugin = this.plugins.get(pluginName);
@@ -1040,12 +1068,17 @@ export class Matterbridge extends EventEmitter {
1040
1068
  }
1041
1069
  // Register and add the device to matterbridge aggregator in bridge mode
1042
1070
  if (this.bridgeMode === 'bridge') {
1043
- this.matterAggregator?.addBridgedDevice(device);
1071
+ if (!this.matterAggregator) {
1072
+ this.log.error(`Adding bridged device ${dev}${device.deviceName}${er} (${dev}${device.name}${er}) for plugin ${plg}${pluginName}${er} error: matterAggregator not found`);
1073
+ return;
1074
+ }
1075
+ this.matterAggregator.addBridgedDevice(device);
1044
1076
  }
1045
1077
  // The first time create the commissioning server and the aggregator for DynamicPlatform
1046
1078
  // Register and add the device in childbridge mode
1047
1079
  if (this.bridgeMode === 'childbridge') {
1048
1080
  if (plugin.type === 'AccessoryPlatform') {
1081
+ // Check if the plugin is locked with the commissioning server
1049
1082
  if (!plugin.locked) {
1050
1083
  plugin.locked = true;
1051
1084
  plugin.storageContext = await this.importCommissioningServerContext(plugin.name, device);
@@ -1059,10 +1092,10 @@ export class Matterbridge extends EventEmitter {
1059
1092
  }
1060
1093
  }
1061
1094
  if (plugin.type === 'DynamicPlatform') {
1095
+ // Check if the plugin is locked with the commissioning server and the aggregator
1062
1096
  if (!plugin.locked) {
1063
1097
  plugin.locked = true;
1064
1098
  this.log.debug(`Creating commissioning server context for ${plg}${plugin.name}${db}`);
1065
- // plugin.storageContext = await this.createCommissioningServerContext(plugin.name, 'Matterbridge', DeviceTypes.AGGREGATOR.code, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge Dynamic Platform');
1066
1099
  plugin.storageContext = await this.createCommissioningServerContext(plugin.name, 'Matterbridge', DeviceTypes.AGGREGATOR.code, 0xfff1, 'Matterbridge', 0x8000, plugin.description);
1067
1100
  this.log.debug(`Creating commissioning server for ${plg}${plugin.name}${db}`);
1068
1101
  plugin.commissioningServer = await this.createCommisioningServer(plugin.storageContext, plugin.name);
@@ -1081,6 +1114,8 @@ export class Matterbridge extends EventEmitter {
1081
1114
  plugin.registeredDevices++;
1082
1115
  if (plugin.addedDevices !== undefined)
1083
1116
  plugin.addedDevices++;
1117
+ // Add the device to the DeviceManager
1118
+ this.devices.set(device);
1084
1119
  this.log.info(`Added and registered bridged device (${plugin.registeredDevices}/${plugin.addedDevices}) ${dev}${device.deviceName}${nf} (${dev}${device.name}${nf}) for plugin ${plg}${pluginName}${nf}`);
1085
1120
  }
1086
1121
  /**
@@ -1090,10 +1125,6 @@ export class Matterbridge extends EventEmitter {
1090
1125
  * @returns A Promise that resolves when the device is successfully removed.
1091
1126
  */
1092
1127
  async removeBridgedDevice(pluginName, device) {
1093
- if (this.bridgeMode === 'bridge' && !this.matterAggregator) {
1094
- this.log.error(`Error removing bridged device ${dev}${device.deviceName}${er} (${dev}${device.name}${er}) for plugin ${plg}${pluginName}${er}: matterAggregator not found`);
1095
- return;
1096
- }
1097
1128
  this.log.debug(`Removing bridged device ${dev}${device.deviceName}${db} (${dev}${device.name}${db}) for plugin ${plg}${pluginName}${db}`);
1098
1129
  // Check if the plugin is registered
1099
1130
  const plugin = this.plugins.get(pluginName);
@@ -1101,18 +1132,16 @@ export class Matterbridge extends EventEmitter {
1101
1132
  this.log.error(`Error removing bridged device ${dev}${device.deviceName}${er} (${dev}${device.name}${er}) for plugin ${plg}${pluginName}${er}: plugin not found`);
1102
1133
  return;
1103
1134
  }
1104
- if (this.bridgeMode === 'childbridge' && plugin.type === 'AccessoryPlatfoem' && !plugin.aggregator) {
1105
- this.log.error(`Error removing bridged device ${dev}${device.deviceName}${er} (${dev}${device.name}${er}) for plugin ${plg}${pluginName}${er}: aggregator not found`);
1106
- return;
1107
- }
1108
- if (this.bridgeMode === 'childbridge' && plugin.type === 'DynamicPlatfoem' && !plugin.commissioningServer) {
1109
- this.log.error(`Error removing bridged device ${dev}${device.deviceName}${er} (${dev}${device.name}${er}) for plugin ${plg}${pluginName}${er}: commissioning server not found`);
1110
- return;
1111
- }
1112
1135
  // Remove the device from matterbridge aggregator in bridge mode
1113
1136
  if (this.bridgeMode === 'bridge') {
1137
+ if (!this.matterAggregator) {
1138
+ this.log.error(`Error removing bridged device ${dev}${device.deviceName}${er} (${dev}${device.name}${er}) for plugin ${plg}${pluginName}${er}: matterAggregator not found`);
1139
+ return;
1140
+ }
1114
1141
  device.setBridgedDeviceReachability(false);
1115
1142
  device.getClusterServerById(BridgedDeviceBasicInformation.Cluster.id)?.triggerReachableChangedEvent({ reachableNewValue: false });
1143
+ // device.getClusterServerById(BridgedDeviceBasicInformation.Cluster.id)?.triggerShutDownEvent({});
1144
+ // device.getClusterServerById(BridgedDeviceBasicInformation.Cluster.id)?.triggerLeaveEvent({});
1116
1145
  this.matterAggregator?.removeBridgedDevice(device);
1117
1146
  this.registeredDevices.forEach((registeredDevice, index) => {
1118
1147
  if (registeredDevice.device === device) {
@@ -1129,6 +1158,10 @@ export class Matterbridge extends EventEmitter {
1129
1158
  // Remove the device in childbridge mode
1130
1159
  if (this.bridgeMode === 'childbridge') {
1131
1160
  if (plugin.type === 'AccessoryPlatform') {
1161
+ if (!plugin.commissioningServer) {
1162
+ this.log.error(`Error removing bridged device ${dev}${device.deviceName}${er} (${dev}${device.name}${er}) for plugin ${plg}${pluginName}${er}: commissioning server not found`);
1163
+ return;
1164
+ }
1132
1165
  this.registeredDevices.forEach((registeredDevice, index) => {
1133
1166
  if (registeredDevice.device === device) {
1134
1167
  this.registeredDevices.splice(index, 1);
@@ -1137,6 +1170,10 @@ export class Matterbridge extends EventEmitter {
1137
1170
  });
1138
1171
  }
1139
1172
  else if (plugin.type === 'DynamicPlatform') {
1173
+ if (!plugin.aggregator) {
1174
+ this.log.error(`Error removing bridged device ${dev}${device.deviceName}${er} (${dev}${device.name}${er}) for plugin ${plg}${pluginName}${er}: aggregator not found`);
1175
+ return;
1176
+ }
1140
1177
  this.registeredDevices.forEach((registeredDevice, index) => {
1141
1178
  if (registeredDevice.device === device) {
1142
1179
  this.registeredDevices.splice(index, 1);
@@ -1145,19 +1182,22 @@ export class Matterbridge extends EventEmitter {
1145
1182
  });
1146
1183
  device.setBridgedDeviceReachability(false);
1147
1184
  device.getClusterServerById(BridgedDeviceBasicInformation.Cluster.id)?.triggerReachableChangedEvent({ reachableNewValue: false });
1148
- plugin.aggregator?.removeBridgedDevice(device);
1185
+ plugin.aggregator.removeBridgedDevice(device);
1149
1186
  }
1150
1187
  this.log.info(`Removed bridged device(${plugin.registeredDevices}/${plugin.addedDevices}) ${dev}${device.deviceName}${nf} (${dev}${device.name}${nf}) for plugin ${plg}${pluginName}${nf}`);
1151
1188
  if (plugin.registeredDevices !== undefined)
1152
1189
  plugin.registeredDevices--;
1153
1190
  if (plugin.addedDevices !== undefined)
1154
1191
  plugin.addedDevices--;
1192
+ // Remove the commissioning server
1155
1193
  if (plugin.registeredDevices === 0 && plugin.addedDevices === 0 && plugin.commissioningServer) {
1156
1194
  this.matterServer?.removeCommissioningServer(plugin.commissioningServer);
1157
1195
  plugin.commissioningServer = undefined;
1158
1196
  this.log.info(`Removed commissioning server for plugin ${plg}${pluginName}${nf}`);
1159
1197
  }
1160
1198
  }
1199
+ // Remove the device from the DeviceManager
1200
+ this.devices.remove(device);
1161
1201
  }
1162
1202
  /**
1163
1203
  * Removes all bridged devices associated with a specific plugin.
@@ -1177,304 +1217,6 @@ export class Matterbridge extends EventEmitter {
1177
1217
  this.removeBridgedDevice(pluginName, registeredDevice.device);
1178
1218
  }
1179
1219
  }
1180
- /**
1181
- * Loads the schema for a plugin.
1182
- * If the schema file exists in the plugin directory, it reads the file and returns the parsed JSON data and delete the schema form .matterbridge.
1183
- * If the schema file does not exist, it creates a new schema with the default configuration and returns it.
1184
- *
1185
- * @param plugin - The plugin for which to load the schema.
1186
- * @returns A promise that resolves to the loaded or created schema.
1187
- */
1188
- async loadPluginSchema(plugin) {
1189
- let schemaFile = plugin.path.replace('package.json', `${plugin.name}.schema.json`);
1190
- try {
1191
- await fs.access(schemaFile);
1192
- const data = await fs.readFile(schemaFile, 'utf8');
1193
- const schema = JSON.parse(data);
1194
- schema.title = plugin.description;
1195
- schema.description = plugin.name + ' v. ' + plugin.version + ' by ' + plugin.author;
1196
- this.log.debug(`Schema file found: ${schemaFile}.`);
1197
- // this.log.debug(`Schema file found: ${schemaFile}.\nSchema:${rs}\n`, schema);
1198
- schemaFile = path.join(this.matterbridgeDirectory, `${plugin.name}.schema.json`);
1199
- try {
1200
- await fs.unlink(schemaFile);
1201
- this.log.debug(`Schema file ${schemaFile} deleted.`);
1202
- }
1203
- catch (err) {
1204
- this.log.debug(`Schema file ${schemaFile} to delete not found.`);
1205
- }
1206
- return schema;
1207
- }
1208
- catch (err) {
1209
- this.log.debug(`Schema file ${schemaFile} not found. Loading default schema.`);
1210
- const schema = {
1211
- title: plugin.description,
1212
- description: plugin.name + ' v. ' + plugin.version + ' by ' + plugin.author,
1213
- type: 'object',
1214
- properties: {
1215
- name: {
1216
- description: 'Plugin name',
1217
- type: 'string',
1218
- readOnly: true,
1219
- },
1220
- type: {
1221
- description: 'Plugin type',
1222
- type: 'string',
1223
- readOnly: true,
1224
- },
1225
- debug: {
1226
- description: 'Enable the debug for the plugin (development only)',
1227
- type: 'boolean',
1228
- default: false,
1229
- },
1230
- unregisterOnShutdown: {
1231
- description: 'Unregister all devices on shutdown (development only)',
1232
- type: 'boolean',
1233
- default: false,
1234
- },
1235
- },
1236
- };
1237
- return schema;
1238
- }
1239
- }
1240
- /**
1241
- * Loads the configuration for a plugin.
1242
- * If the configuration file exists, it reads the file and returns the parsed JSON data.
1243
- * If the configuration file does not exist, it creates a new file with default configuration and returns it.
1244
- * If any error occurs during file access or creation, it logs an error and return un empty config.
1245
- *
1246
- * @param plugin - The plugin for which to load the configuration.
1247
- * @returns A promise that resolves to the loaded or created configuration.
1248
- */
1249
- async loadPluginConfig(plugin) {
1250
- const configFile = path.join(this.matterbridgeDirectory, `${plugin.name}.config.json`);
1251
- try {
1252
- await fs.access(configFile);
1253
- const data = await fs.readFile(configFile, 'utf8');
1254
- const config = JSON.parse(data);
1255
- this.log.debug(`Config file found: ${configFile}.`);
1256
- // this.log.debug(`Config file found: ${configFile}.\nConfig:${rs}\n`, config);
1257
- /* The first time a plugin is added to the system, the config file is created with the plugin name and type "".*/
1258
- config.name = plugin.name;
1259
- config.type = plugin.type;
1260
- if (config.debug === undefined)
1261
- config.debug = false;
1262
- if (config.unregisterOnShutdown === undefined)
1263
- config.unregisterOnShutdown = false;
1264
- return config;
1265
- }
1266
- catch (err) {
1267
- if (err instanceof Error) {
1268
- const nodeErr = err;
1269
- if (nodeErr.code === 'ENOENT') {
1270
- let config;
1271
- if (plugin.name === 'matterbridge-zigbee2mqtt')
1272
- config = zigbee2mqtt_config;
1273
- else if (plugin.name === 'matterbridge-somfy-tahoma')
1274
- config = somfytahoma_config;
1275
- else if (plugin.name === 'matterbridge-shelly')
1276
- config = shelly_config;
1277
- else
1278
- config = { name: plugin.name, type: plugin.type, debug: false, unregisterOnShutdown: false };
1279
- try {
1280
- await this.writeFile(configFile, JSON.stringify(config, null, 2));
1281
- this.log.debug(`Created config file: ${configFile}.`);
1282
- // this.log.debug(`Created config file: ${configFile}.\nConfig:${rs}\n`, config);
1283
- return config;
1284
- }
1285
- catch (err) {
1286
- this.log.error(`Error creating config file ${configFile}: ${err}`);
1287
- return config;
1288
- }
1289
- }
1290
- else {
1291
- this.log.error(`Error accessing config file ${configFile}: ${err}`);
1292
- return { name: plugin.name, type: plugin.type, debug: false, unregisterOnShutdown: false };
1293
- }
1294
- }
1295
- this.log.error(`Error loading config file ${configFile}: ${err}`);
1296
- return { name: plugin.name, type: plugin.type, debug: false, unregisterOnShutdown: false };
1297
- }
1298
- }
1299
- /**
1300
- * Saves the configuration of a registered plugin.
1301
- * @param {RegisteredPlugin} plugin - The plugin whose configuration needs to be saved.
1302
- * @returns {Promise<void>} - A promise that resolves when the configuration is successfully saved.
1303
- * @throws {Error} - If the plugin's configuration is not found or if there is an error while saving the configuration.
1304
- */
1305
- async savePluginConfig(plugin) {
1306
- if (!plugin.platform?.config) {
1307
- this.log.error(`Error saving plugin ${plg}${plugin.name}${er} config: config not found`);
1308
- return Promise.reject(new Error(`Error saving plugin ${plg}${plugin.name}${er} config: config not found`));
1309
- }
1310
- const configFile = path.join(this.matterbridgeDirectory, `${plugin.name}.config.json`);
1311
- try {
1312
- await this.writeFile(configFile, JSON.stringify(plugin.platform.config, null, 2));
1313
- this.log.debug(`Saved config file: ${configFile}.`);
1314
- // this.log.debug(`Saved config file: ${configFile}.\nConfig:${rs}\n`, plugin.platform.config);
1315
- }
1316
- catch (err) {
1317
- this.log.error(`Error saving plugin ${plg}${plugin.name}${er} config: ${err}`);
1318
- return Promise.reject(err);
1319
- }
1320
- }
1321
- /**
1322
- * Writes data to a file.
1323
- *
1324
- * @param {string} filePath - The path of the file to write to.
1325
- * @param {string} data - The data to write to the file.
1326
- * @returns {Promise<void>} - A promise that resolves when the data is successfully written to the file.
1327
- */
1328
- async writeFile(filePath, data) {
1329
- try {
1330
- await fs.writeFile(`${filePath}`, data, 'utf8');
1331
- this.log.debug(`Successfully wrote to ${filePath}`);
1332
- }
1333
- catch (error) {
1334
- this.log.error(`Error writing to ${filePath}:`, error);
1335
- }
1336
- }
1337
- /**
1338
- * Loads a plugin and returns the corresponding MatterbridgePlatform instance.
1339
- * @param plugin - The plugin to load.
1340
- * @param start - Optional flag indicating whether to start the plugin after loading. Default is false.
1341
- * @param message - Optional message to pass to the plugin when starting.
1342
- * @returns A Promise that resolves to the loaded MatterbridgePlatform instance.
1343
- * @throws An error if the plugin is not enabled, already loaded, or fails to load.
1344
- */
1345
- async loadPlugin(plugin, start = false, message = '') {
1346
- if (!plugin.enabled) {
1347
- this.log.error(`Plugin ${plg}${plugin.name}${er} not enabled`);
1348
- return Promise.resolve(undefined);
1349
- }
1350
- if (plugin.platform) {
1351
- this.log.error(`Plugin ${plg}${plugin.name}${er} already loaded`);
1352
- return Promise.resolve(plugin.platform);
1353
- }
1354
- this.log.info(`Loading plugin ${plg}${plugin.name}${nf} type ${typ}${plugin.type}${nf}`);
1355
- try {
1356
- // Load the package.json of the plugin
1357
- const packageJson = JSON.parse(await fs.readFile(plugin.path, 'utf8'));
1358
- // Resolve the main module path relative to package.json
1359
- const pluginEntry = path.resolve(path.dirname(plugin.path), packageJson.main);
1360
- // Dynamically import the plugin
1361
- const pluginUrl = pathToFileURL(pluginEntry);
1362
- this.log.debug(`Importing plugin ${plg}${plugin.name}${db} from ${pluginUrl.href}`);
1363
- const pluginInstance = await import(pluginUrl.href);
1364
- this.log.debug(`Imported plugin ${plg}${plugin.name}${db} from ${pluginUrl.href}`);
1365
- // Call the default export function of the plugin, passing this MatterBridge instance, the log and the config
1366
- if (pluginInstance.default) {
1367
- const config = await this.loadPluginConfig(plugin);
1368
- const log = new AnsiLogger({ logName: plugin.description ?? 'No description', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logDebug: config.debug ?? false });
1369
- const platform = pluginInstance.default(this, log, config);
1370
- platform.name = packageJson.name;
1371
- platform.config = config;
1372
- platform.version = packageJson.version;
1373
- plugin.name = packageJson.name;
1374
- plugin.description = packageJson.description ?? 'No description';
1375
- plugin.version = packageJson.version;
1376
- plugin.author = packageJson.author ?? 'Unknown';
1377
- plugin.type = platform.type;
1378
- plugin.platform = platform;
1379
- plugin.loaded = true;
1380
- plugin.registeredDevices = 0;
1381
- plugin.addedDevices = 0;
1382
- plugin.configJson = config;
1383
- plugin.schemaJson = await this.loadPluginSchema(plugin);
1384
- this.log.info(`Loaded plugin ${plg}${plugin.name}${nf} type ${typ}${platform.type} ${db}(entrypoint ${UNDERLINE}${pluginEntry}${UNDERLINEOFF})`);
1385
- if (start)
1386
- this.startPlugin(plugin, message); // No await do it asyncronously
1387
- return Promise.resolve(platform);
1388
- }
1389
- else {
1390
- this.log.error(`Plugin ${plg}${plugin.name}${er} does not provide a default export`);
1391
- plugin.error = true;
1392
- return Promise.resolve(undefined);
1393
- }
1394
- }
1395
- catch (err) {
1396
- this.log.error(`Failed to load plugin ${plg}${plugin.name}${er}: ${err}`);
1397
- plugin.error = true;
1398
- return Promise.resolve(undefined);
1399
- }
1400
- }
1401
- /**
1402
- * Starts a plugin.
1403
- *
1404
- * @param {RegisteredPlugin} plugin - The plugin to start.
1405
- * @param {string} [message] - Optional message to pass to the plugin's onStart method.
1406
- * @param {boolean} [configure] - Indicates whether to configure the plugin after starting (default false).
1407
- * @returns {Promise<void>} A promise that resolves when the plugin is started successfully, or rejects with an error if starting the plugin fails.
1408
- */
1409
- async startPlugin(plugin, message, configure = false) {
1410
- if (!plugin.loaded || !plugin.platform) {
1411
- this.log.error(`Plugin ${plg}${plugin.name}${er} not loaded or no platform`);
1412
- return Promise.resolve();
1413
- }
1414
- if (plugin.started) {
1415
- this.log.debug(`Plugin ${plg}${plugin.name}${db} already started`);
1416
- return Promise.resolve();
1417
- }
1418
- this.log.info(`Starting plugin ${plg}${plugin.name}${db} type ${typ}${plugin.type}${db}`);
1419
- try {
1420
- plugin.platform
1421
- .onStart(message)
1422
- .then(() => {
1423
- plugin.started = true;
1424
- this.log.info(`Started plugin ${plg}${plugin.name}${db} type ${typ}${plugin.type}${db}`);
1425
- if (configure)
1426
- this.configurePlugin(plugin); // No await do it asyncronously
1427
- return Promise.resolve();
1428
- })
1429
- .catch((err) => {
1430
- plugin.error = true;
1431
- this.log.error(`Failed to start plugin ${plg}${plugin.name}${er}: ${err}`);
1432
- return Promise.resolve();
1433
- });
1434
- }
1435
- catch (err) {
1436
- plugin.error = true;
1437
- this.log.error(`Failed to start plugin ${plg}${plugin.name}${er}: ${err}`);
1438
- return Promise.resolve();
1439
- }
1440
- }
1441
- /**
1442
- * Configures a plugin.
1443
- *
1444
- * @param {RegisteredPlugin} plugin - The plugin to configure.
1445
- * @returns {Promise<void>} A promise that resolves when the plugin is configured successfully, or rejects with an error if configuration fails.
1446
- */
1447
- async configurePlugin(plugin) {
1448
- if (!plugin.loaded || !plugin.started || !plugin.platform) {
1449
- this.log.error(`Plugin ${plg}${plugin.name}${er} not loaded (${plugin.loaded}) or not started (${plugin.started}) or not platform (${plugin.platform?.name})`);
1450
- return Promise.resolve();
1451
- }
1452
- if (plugin.configured) {
1453
- this.log.info(`Plugin ${plg}${plugin.name}${nf} already configured`);
1454
- return Promise.resolve();
1455
- }
1456
- this.log.info(`Configuring plugin ${plg}${plugin.name}${nf} type ${typ}${plugin.type}${nf}`);
1457
- try {
1458
- plugin.platform
1459
- .onConfigure()
1460
- .then(() => {
1461
- plugin.configured = true;
1462
- this.log.info(`Configured plugin ${plg}${plugin.name}${nf} type ${typ}${plugin.type}${nf}`);
1463
- this.savePluginConfig(plugin);
1464
- return Promise.resolve();
1465
- })
1466
- .catch((err) => {
1467
- plugin.error = true;
1468
- this.log.error(`Failed to configure plugin ${plg}${plugin.name}${er}: ${err}`);
1469
- return Promise.resolve();
1470
- });
1471
- }
1472
- catch (err) {
1473
- plugin.error = true;
1474
- this.log.error(`Failed to configure plugin ${plg}${plugin.name}${er}: ${err}`);
1475
- return Promise.resolve();
1476
- }
1477
- }
1478
1220
  async startTest() {
1479
1221
  // Start the Matterbridge
1480
1222
  }
@@ -1514,14 +1256,14 @@ export class Matterbridge extends EventEmitter {
1514
1256
  clearInterval(startMatterInterval);
1515
1257
  this.log.debug('***Cleared startMatterInterval interval for Matterbridge');
1516
1258
  await this.startMatterServer();
1517
- this.log.info('Matter server started');
1259
+ this.log.notice('Matter server started');
1518
1260
  // Configure the plugins
1519
1261
  this.configureTimeout = setTimeout(async () => {
1520
1262
  for (const plugin of this.plugins) {
1521
1263
  if (!plugin.enabled || !plugin.loaded || !plugin.started || plugin.error)
1522
1264
  continue;
1523
1265
  try {
1524
- await this.plugins.configure(plugin); // No await do it asyncronously
1266
+ await this.plugins.configure(plugin); // TODO No await do it in parallel
1525
1267
  }
1526
1268
  catch (error) {
1527
1269
  plugin.error = true;
@@ -1582,14 +1324,14 @@ export class Matterbridge extends EventEmitter {
1582
1324
  clearInterval(startMatterInterval);
1583
1325
  this.log.debug('***Cleared startMatterInterval interval in childbridge mode');
1584
1326
  await this.startMatterServer();
1585
- this.log.info('Matter server started');
1327
+ this.log.notice('Matter server started');
1586
1328
  // Configure the plugins
1587
1329
  this.configureTimeout = setTimeout(async () => {
1588
1330
  for (const plugin of this.plugins) {
1589
1331
  if (!plugin.enabled || !plugin.loaded || !plugin.started || plugin.error)
1590
1332
  continue;
1591
1333
  try {
1592
- await this.plugins.configure(plugin); // No await do it asyncronously
1334
+ await this.plugins.configure(plugin); // TODO No await do it in parallel
1593
1335
  }
1594
1336
  catch (error) {
1595
1337
  plugin.error = true;
@@ -1815,7 +1557,7 @@ export class Matterbridge extends EventEmitter {
1815
1557
  * @param {string} storageName - The name of the storage file.
1816
1558
  * @returns {Promise<void>} - A promise that resolves when the storage process is started.
1817
1559
  */
1818
- async startStorage(storageType, storageName) {
1560
+ async startMatterStorage(storageType, storageName) {
1819
1561
  this.log.debug(`Starting ${storageType} storage ${CYAN}${storageName}${db}`);
1820
1562
  if (storageType === 'disk') {
1821
1563
  const storageDisk = new StorageBackendDisk(storageName);
@@ -1836,7 +1578,7 @@ export class Matterbridge extends EventEmitter {
1836
1578
  await this.storageManager.initialize();
1837
1579
  this.log.debug('Storage initialized');
1838
1580
  if (storageType === 'json') {
1839
- await this.backupJsonStorage(storageName, storageName.replace('.json', '') + '.backup.json');
1581
+ await this.backupJsonMatterStorage(storageName, storageName.replace('.json', '') + '.backup.json');
1840
1582
  }
1841
1583
  }
1842
1584
  catch (error) {
@@ -1851,7 +1593,7 @@ export class Matterbridge extends EventEmitter {
1851
1593
  * @param storageName - The name of the JSON storage file to be backed up.
1852
1594
  * @param backupName - The name of the backup file to be created.
1853
1595
  */
1854
- async backupJsonStorage(storageName, backupName) {
1596
+ async backupJsonMatterStorage(storageName, backupName) {
1855
1597
  try {
1856
1598
  this.log.debug(`Making backup copy of ${storageName}`);
1857
1599
  await fs.copyFile(storageName, backupName);
@@ -1875,7 +1617,7 @@ export class Matterbridge extends EventEmitter {
1875
1617
  * Stops the matter storage.
1876
1618
  * @returns {Promise<void>} A promise that resolves when the storage is stopped.
1877
1619
  */
1878
- async stopStorage() {
1620
+ async stopMatterStorage() {
1879
1621
  this.log.debug('Stopping storage');
1880
1622
  await this.storageManager?.close();
1881
1623
  this.log.debug('Storage closed');
@@ -2028,9 +1770,9 @@ export class Matterbridge extends EventEmitter {
2028
1770
  const sessionInformations = commissioningServer.getActiveSessionInformation(fabricIndex);
2029
1771
  let connected = false;
2030
1772
  sessionInformations.forEach((session) => {
2031
- this.log.info(`*Active session changed on fabric ${zb}${fabricIndex}${nf} id ${zb}${session.fabric?.fabricId}${nf} vendor ${zb}${session.fabric?.rootVendorId}${nf} ${this.getVendorIdName(session.fabric?.rootVendorId)} ${session.fabric?.label} for ${plg}${pluginName}${nf}`, debugStringify(session));
1773
+ this.log.info(`Active session changed on fabric ${zb}${fabricIndex}${nf} id ${zb}${session.fabric?.fabricId}${nf} vendor ${zb}${session.fabric?.rootVendorId}${nf} ${this.getVendorIdName(session.fabric?.rootVendorId)} ${session.fabric?.label} for ${plg}${pluginName}${nf}`, debugStringify(session));
2032
1774
  if (session.isPeerActive === true && session.secure === true && session.numberOfActiveSubscriptions >= 1) {
2033
- this.log.info(`*Controller ${zb}${session.fabric?.rootVendorId}${nf} ${this.getVendorIdName(session.fabric?.rootVendorId)} ${session.fabric?.label} connected to ${plg}${pluginName}${nf} on session ${session.name}`);
1775
+ this.log.notice(`Controller ${zb}${session.fabric?.rootVendorId}${nt} ${this.getVendorIdName(session.fabric?.rootVendorId)} ${session.fabric?.label} connected to ${plg}${pluginName}${nt} on session ${session.name}`);
2034
1776
  connected = true;
2035
1777
  }
2036
1778
  });
@@ -2081,9 +1823,9 @@ export class Matterbridge extends EventEmitter {
2081
1823
  },
2082
1824
  commissioningChangedCallback: async (fabricIndex) => {
2083
1825
  const fabricInfo = commissioningServer.getCommissionedFabricInformation(fabricIndex);
2084
- this.log.debug(`*Commissioning changed on fabric ${zb}${fabricIndex}${nf} for ${plg}${pluginName}${nf}`, debugStringify(fabricInfo));
1826
+ this.log.debug(`Commissioning changed on fabric ${zb}${fabricIndex}${db} for ${plg}${pluginName}${db}`, debugStringify(fabricInfo));
2085
1827
  if (commissioningServer.getCommissionedFabricInformation().length === 0) {
2086
- this.log.warn(`*Commissioning removed from fabric ${zb}${fabricIndex}${nf} for ${plg}${pluginName}${wr}. Resetting the commissioning server ...`);
1828
+ this.log.warn(`Commissioning removed from fabric ${zb}${fabricIndex}${wr} for ${plg}${pluginName}${wr}. Resetting the commissioning server ...`);
2087
1829
  await commissioningServer.factoryReset();
2088
1830
  if (pluginName === 'Matterbridge') {
2089
1831
  await this.matterbridgeContext?.clearAll();
@@ -2104,7 +1846,7 @@ export class Matterbridge extends EventEmitter {
2104
1846
  }
2105
1847
  }
2106
1848
  }
2107
- this.log.warn(`*Restart to activate the pairing for ${plg}${pluginName}${wr}.`);
1849
+ this.log.warn(`Restart to activate the pairing for ${plg}${pluginName}${wr}.`);
2108
1850
  }
2109
1851
  else {
2110
1852
  const fabricInfo = commissioningServer.getCommissionedFabricInformation();
@@ -2282,7 +2024,7 @@ export class Matterbridge extends EventEmitter {
2282
2024
  }
2283
2025
  }
2284
2026
  /**
2285
- * Sanitizes the fabric information by converting bigint properties to string cause res..
2027
+ * Sanitizes the fabric information by converting bigint properties to string cause res.json doesn't know bigint.
2286
2028
  *
2287
2029
  * @param fabricInfo - The array of exposed fabric information objects.
2288
2030
  * @returns An array of sanitized exposed fabric information objects.
@@ -2414,10 +2156,9 @@ export class Matterbridge extends EventEmitter {
2414
2156
  };
2415
2157
  /**
2416
2158
  * Retrieves the base registered plugins sanitized for res.json().
2417
- * @param {boolean} includeAll - Whether to include all information for each plugin.
2418
2159
  * @returns {BaseRegisteredPlugin[]} A promise that resolves to an array of BaseRegisteredPlugin objects.
2419
2160
  */
2420
- async getBaseRegisteredPlugins(includeAll = false) {
2161
+ async getBaseRegisteredPlugins() {
2421
2162
  const baseRegisteredPlugins = [];
2422
2163
  for (const plugin of this.plugins) {
2423
2164
  baseRegisteredPlugins.push({
@@ -2436,23 +2177,23 @@ export class Matterbridge extends EventEmitter {
2436
2177
  configured: plugin.configured,
2437
2178
  paired: plugin.paired,
2438
2179
  connected: plugin.connected,
2439
- fabricInformations: includeAll ? plugin.fabricInformations : undefined,
2440
- sessionInformations: includeAll ? plugin.sessionInformations : undefined,
2180
+ fabricInformations: plugin.fabricInformations,
2181
+ sessionInformations: plugin.sessionInformations,
2441
2182
  registeredDevices: plugin.registeredDevices,
2442
2183
  addedDevices: plugin.addedDevices,
2443
2184
  qrPairingCode: plugin.qrPairingCode,
2444
2185
  manualPairingCode: plugin.manualPairingCode,
2445
- configJson: includeAll ? plugin.configJson : undefined,
2446
- schemaJson: includeAll ? plugin.schemaJson : undefined,
2186
+ configJson: plugin.configJson,
2187
+ schemaJson: plugin.schemaJson,
2447
2188
  });
2448
2189
  }
2449
2190
  return baseRegisteredPlugins;
2450
2191
  }
2451
2192
  /**
2452
2193
  * Spawns a child process with the given command and arguments.
2453
- * @param command - The command to execute.
2454
- * @param args - The arguments to pass to the command (default: []).
2455
- * @returns A promise that resolves when the child process exits successfully, or rejects if there is an error.
2194
+ * @param {string} command - The command to execute.
2195
+ * @param {string[]} args - The arguments to pass to the command (default: []).
2196
+ * @returns {Promise<void>} A promise that resolves when the child process exits successfully, or rejects if there is an error.
2456
2197
  */
2457
2198
  async spawnCommand(command, args = []) {
2458
2199
  /*
@@ -2510,14 +2251,14 @@ export class Matterbridge extends EventEmitter {
2510
2251
  childProcess.stdout.on('data', (data) => {
2511
2252
  const message = data.toString().trim();
2512
2253
  // this.log.info('\n' + message);
2513
- this.wssSendMessage('Matterbridge:spawn', 'spawn', message);
2254
+ this.wssSendMessage('spawn', this.log.now(), 'Matterbridge:spawn', message);
2514
2255
  });
2515
2256
  }
2516
2257
  if (childProcess.stderr) {
2517
2258
  childProcess.stderr.on('data', (data) => {
2518
2259
  const message = data.toString().trim();
2519
2260
  // this.log.debug('\n' + message);
2520
- this.wssSendMessage('Matterbridge:spawn', 'spawn', message);
2261
+ this.wssSendMessage('spawn', this.log.now(), 'Matterbridge:spawn', message);
2521
2262
  });
2522
2263
  }
2523
2264
  });
@@ -2525,20 +2266,35 @@ export class Matterbridge extends EventEmitter {
2525
2266
  /**
2526
2267
  * Sends a WebSocket message to all connected clients.
2527
2268
  *
2528
- * @param {string} type - The type of the message: Matterbridge, Plugin, Device, ...
2529
- * @param {string} subType - The subtype of the message: debug info warn error ....
2269
+ * @param {string} level - The logger level of the message: debug info notice warn error fatal...
2270
+ * @param {string} time - The time string of the message
2271
+ * @param {string} name - The logger name of the message
2530
2272
  * @param {string} message - The content of the message.
2531
2273
  */
2532
- wssSendMessage(type, subType, message) {
2274
+ wssSendMessage(level, time, name, message) {
2275
+ if (!level || !time || !name || !message)
2276
+ return;
2533
2277
  // Remove ANSI escape codes from the message
2534
2278
  // eslint-disable-next-line no-control-regex
2535
- const cleanMessage = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
2279
+ message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
2536
2280
  // Remove leading asterisks from the message
2537
- const finalMessage = cleanMessage.replace(/^\*+/, '');
2281
+ message = message.replace(/^\*+/, '');
2282
+ // Replace all occurrences of \t and \n
2283
+ message = message.replace(/[\t\n]/g, '');
2284
+ // Remove non-printable characters
2285
+ // eslint-disable-next-line no-control-regex
2286
+ message = message.replace(/[\x00-\x1F\x7F]/g, '');
2287
+ // Replace all occurrences of \" with "
2288
+ message = message.replace(/\\"/g, '"');
2289
+ /*
2290
+ if (message.length > 500) {
2291
+ console.error(`${er}Message long: ${level} ${time} ${name} ${message} `);
2292
+ }
2293
+ */
2538
2294
  // Send the message to all connected clients
2539
2295
  this.webSocketServer?.clients.forEach((client) => {
2540
2296
  if (client.readyState === WebSocket.OPEN) {
2541
- client.send(JSON.stringify({ type, subType, message: finalMessage }));
2297
+ client.send(JSON.stringify({ level, time, name, message }));
2542
2298
  }
2543
2299
  });
2544
2300
  }
@@ -2639,10 +2395,10 @@ export class Matterbridge extends EventEmitter {
2639
2395
  this.webSocketServer = new WebSocketServer(hasParameter('ssl') ? { server: this.httpsServer } : { server: this.httpServer });
2640
2396
  this.webSocketServer.on('connection', (ws, request) => {
2641
2397
  const clientIp = request.socket.remoteAddress;
2642
- this.log.info(`WebSocketServer client ${clientIp} connected`);
2643
2398
  this.log.setGlobalCallback(this.wssSendMessage.bind(this));
2644
2399
  this.log.debug('WebSocketServer logger global callback added');
2645
- this.wssSendMessage('Matterbridge', 'info', `WebSocketServer client ${clientIp} connected to Matterbridge`);
2400
+ this.log.info(`WebSocketServer client "${clientIp}" connected to Matterbridge`);
2401
+ // this.wssSendMessage('info', this.log.now(), 'Matterbridge', `WebSocketServer client "${clientIp}" connected to Matterbridge`);
2646
2402
  ws.on('message', (message) => {
2647
2403
  this.log.debug(`WebSocket client message: ${message}`);
2648
2404
  });
@@ -2711,13 +2467,15 @@ export class Matterbridge extends EventEmitter {
2711
2467
  }
2712
2468
  this.matterbridgeInformation.bridgeMode = this.bridgeMode;
2713
2469
  this.matterbridgeInformation.restartMode = this.restartMode;
2714
- this.matterbridgeInformation.debugEnabled = this.debugEnabled;
2470
+ this.matterbridgeInformation.loggerLevel = this.log.logLevel;
2715
2471
  this.matterbridgeInformation.matterLoggerLevel = Logger.defaultLogLevel;
2716
2472
  this.matterbridgeInformation.matterbridgePaired = this.matterbridgePaired;
2717
2473
  this.matterbridgeInformation.matterbridgeConnected = this.matterbridgeConnected;
2718
2474
  this.matterbridgeInformation.matterbridgeFabricInformations = this.matterbridgeFabricInformations;
2719
2475
  this.matterbridgeInformation.matterbridgeSessionInformations = this.matterbridgeSessionInformations;
2720
- const response = { wssHost, qrPairingCode, manualPairingCode, systemInformation: this.systemInformation, matterbridgeInformation: this.matterbridgeInformation };
2476
+ if (this.profile)
2477
+ this.matterbridgeInformation.profile = this.profile;
2478
+ const response = { wssHost, ssl: hasParameter('ssl'), qrPairingCode, manualPairingCode, systemInformation: this.systemInformation, matterbridgeInformation: this.matterbridgeInformation };
2721
2479
  // this.log.debug('Response:', debugStringify(response));
2722
2480
  this.log.debug(`WebSocketServer logger local callback: ${this.log.getCallback() ? 'active' : 'inactive'}`);
2723
2481
  this.log.debug(`WebSocketServer logger global callback: ${this.log.getGlobalCallback() ? 'active' : 'inactive'}`);
@@ -2730,7 +2488,7 @@ export class Matterbridge extends EventEmitter {
2730
2488
  // Endpoint to provide plugins
2731
2489
  this.expressApp.get('/api/plugins', async (req, res) => {
2732
2490
  this.log.debug('The frontend sent /api/plugins');
2733
- const response = await this.getBaseRegisteredPlugins(true);
2491
+ const response = await this.getBaseRegisteredPlugins();
2734
2492
  // this.log.debug('Response:', debugStringify(response));
2735
2493
  res.json(response);
2736
2494
  });
@@ -2859,12 +2617,31 @@ export class Matterbridge extends EventEmitter {
2859
2617
  if (command === 'setmbloglevel') {
2860
2618
  this.log.debug('Matterbridge log level:', param);
2861
2619
  if (param === 'Debug') {
2862
- this.log.setLogDebug(true);
2863
- this.debugEnabled = true;
2620
+ this.log.logLevel = "debug" /* LogLevel.DEBUG */;
2864
2621
  }
2865
2622
  else if (param === 'Info') {
2866
- this.log.setLogDebug(false);
2867
- this.debugEnabled = false;
2623
+ this.log.logLevel = "info" /* LogLevel.INFO */;
2624
+ }
2625
+ else if (param === 'Notice') {
2626
+ this.log.logLevel = "notice" /* LogLevel.NOTICE */;
2627
+ }
2628
+ else if (param === 'Warn') {
2629
+ this.log.logLevel = "warn" /* LogLevel.WARN */;
2630
+ }
2631
+ else if (param === 'Error') {
2632
+ this.log.logLevel = "error" /* LogLevel.ERROR */;
2633
+ }
2634
+ else if (param === 'Fatal') {
2635
+ this.log.logLevel = "fatal" /* LogLevel.FATAL */;
2636
+ }
2637
+ await this.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
2638
+ MatterbridgeDevice.logLevel = this.log.logLevel;
2639
+ this.plugins.logLevel = this.log.logLevel;
2640
+ for (const plugin of this.plugins) {
2641
+ if (!plugin.platform || !plugin.platform.config)
2642
+ continue;
2643
+ plugin.platform.log.logLevel = plugin.platform.config.debug ? "debug" /* LogLevel.DEBUG */ : this.log.logLevel;
2644
+ await plugin.platform.onChangeLoggerLevel(plugin.platform.config.debug ? "debug" /* LogLevel.DEBUG */ : this.log.logLevel);
2868
2645
  }
2869
2646
  res.json({ message: 'Command received' });
2870
2647
  return;
@@ -2890,6 +2667,7 @@ export class Matterbridge extends EventEmitter {
2890
2667
  else if (param === 'Fatal') {
2891
2668
  Logger.defaultLogLevel = Level.FATAL;
2892
2669
  }
2670
+ await this.nodeContext?.set('matterLogLevel', Logger.defaultLogLevel);
2893
2671
  res.json({ message: 'Command received' });
2894
2672
  return;
2895
2673
  }
@@ -2975,12 +2753,6 @@ export class Matterbridge extends EventEmitter {
2975
2753
  const plugin = await this.plugins.add(param);
2976
2754
  if (plugin) {
2977
2755
  this.plugins.load(plugin, true, 'The plugin has been added', true); // No await do it in the background
2978
- /*
2979
- plugin.platform = await this.plugins.load(plugin, false, 'The plugin has been added');
2980
- if (plugin.platform) {
2981
- await this.plugins.start(plugin, 'The plugin has been added', true);
2982
- }
2983
- */
2984
2756
  }
2985
2757
  res.json({ message: 'Command received' });
2986
2758
  return;
@@ -3017,12 +2789,6 @@ export class Matterbridge extends EventEmitter {
3017
2789
  plugin.addedDevices = undefined;
3018
2790
  await this.plugins.enable(param);
3019
2791
  this.plugins.load(plugin, true, 'The plugin has been enabled', true); // No await do it in the background
3020
- /*
3021
- plugin.platform = await this.plugins.load(plugin, false, 'The plugin has been enabled');
3022
- if (plugin.platform) {
3023
- await this.plugins.start(plugin, 'The plugin has been enabled', true);
3024
- }
3025
- */
3026
2792
  }
3027
2793
  }
3028
2794
  res.json({ message: 'Command received' });
@@ -3053,9 +2819,9 @@ export class Matterbridge extends EventEmitter {
3053
2819
  this.log.debug(`Frontend initialized on port ${YELLOW}${port}${db} static ${UNDERLINE}${path.join(this.rootDirectory, 'frontend/build')}${UNDERLINEOFF}${rs}`);
3054
2820
  }
3055
2821
  /**
3056
- * Retrieves the cluster text from a given device.
3057
- * @param device - The MatterbridgeDevice object.
3058
- * @returns The attributes of the cluster servers in the device.
2822
+ * Retrieves the cluster text description from a given device.
2823
+ * @param {MatterbridgeDevice} device - The MatterbridgeDevice object.
2824
+ * @returns {string} The attributes description of the cluster servers in the device.
3059
2825
  */
3060
2826
  getClusterTextFromDevice(device) {
3061
2827
  const stringifyFixedLabel = (endpoint) => {
@@ -3072,41 +2838,54 @@ export class Matterbridge extends EventEmitter {
3072
2838
  // this.log.debug(`getClusterTextFromDevice: ${device.name}`);
3073
2839
  const clusterServers = device.getAllClusterServers();
3074
2840
  clusterServers.forEach((clusterServer) => {
3075
- // this.log.debug(`***--clusterServer: ${clusterServer.id} (${clusterServer.name})`);
3076
- if (clusterServer.name === 'OnOff')
3077
- attributes += `OnOff: ${clusterServer.getOnOffAttribute()} `;
3078
- if (clusterServer.name === 'Switch')
3079
- attributes += `Position: ${clusterServer.getCurrentPositionAttribute()} `;
3080
- if (clusterServer.name === 'WindowCovering')
3081
- attributes += `Cover position: ${clusterServer.attributes.currentPositionLiftPercent100ths.getLocal() / 100}% `;
3082
- if (clusterServer.name === 'DoorLock')
3083
- attributes += `State: ${clusterServer.attributes.lockState.getLocal() === 1 ? 'Locked' : 'Not locked'} `;
3084
- if (clusterServer.name === 'Thermostat')
3085
- attributes += `Temperature: ${clusterServer.attributes.localTemperature.getLocal() / 100}°C `;
3086
- if (clusterServer.name === 'LevelControl')
3087
- attributes += `Level: ${clusterServer.getCurrentLevelAttribute()}% `;
3088
- if (clusterServer.name === 'ColorControl')
3089
- attributes += `Hue: ${Math.round(clusterServer.getCurrentHueAttribute())} Saturation: ${Math.round(clusterServer.getCurrentSaturationAttribute())}% `;
3090
- if (clusterServer.name === 'BooleanState')
3091
- attributes += `Contact: ${clusterServer.getStateValueAttribute()} `;
3092
- if (clusterServer.name === 'OccupancySensing')
3093
- attributes += `Occupancy: ${clusterServer.getOccupancyAttribute().occupied} `;
3094
- if (clusterServer.name === 'IlluminanceMeasurement')
3095
- attributes += `Illuminance: ${clusterServer.getMeasuredValueAttribute()} `;
3096
- if (clusterServer.name === 'AirQuality')
3097
- attributes += `Air quality: ${clusterServer.getAirQualityAttribute()} `;
3098
- if (clusterServer.name === 'TvocMeasurement')
3099
- attributes += `Voc: ${clusterServer.getMeasuredValueAttribute()} `;
3100
- if (clusterServer.name === 'TemperatureMeasurement')
3101
- attributes += `Temperature: ${clusterServer.getMeasuredValueAttribute() / 100}°C `;
3102
- if (clusterServer.name === 'RelativeHumidityMeasurement')
3103
- attributes += `Humidity: ${clusterServer.getMeasuredValueAttribute() / 100}% `;
3104
- if (clusterServer.name === 'PressureMeasurement')
3105
- attributes += `Pressure: ${clusterServer.getMeasuredValueAttribute()} `;
3106
- if (clusterServer.name === 'FlowMeasurement')
3107
- attributes += `Flow: ${clusterServer.getMeasuredValueAttribute()} `;
3108
- if (clusterServer.name === 'FixedLabel')
3109
- attributes += `${stringifyFixedLabel(device)} `;
2841
+ try {
2842
+ // this.log.debug(`***--clusterServer: ${clusterServer.id} (${clusterServer.name})`);
2843
+ if (clusterServer.name === 'OnOff')
2844
+ attributes += `OnOff: ${clusterServer.getOnOffAttribute()} `;
2845
+ if (clusterServer.name === 'Switch')
2846
+ attributes += `Position: ${clusterServer.getCurrentPositionAttribute()} `;
2847
+ if (clusterServer.name === 'WindowCovering')
2848
+ attributes += `Cover position: ${clusterServer.attributes.currentPositionLiftPercent100ths.getLocal() / 100}% `;
2849
+ if (clusterServer.name === 'DoorLock')
2850
+ attributes += `State: ${clusterServer.attributes.lockState.getLocal() === 1 ? 'Locked' : 'Not locked'} `;
2851
+ if (clusterServer.name === 'Thermostat')
2852
+ attributes += `Temperature: ${clusterServer.attributes.localTemperature.getLocal() / 100}°C `;
2853
+ if (clusterServer.name === 'LevelControl')
2854
+ attributes += `Level: ${clusterServer.getCurrentLevelAttribute()}% `;
2855
+ if (clusterServer.name === 'ColorControl' && clusterServer.isAttributeSupportedByName('currentHue'))
2856
+ attributes += `Hue: ${Math.round(clusterServer.getCurrentHueAttribute())} Saturation: ${Math.round(clusterServer.getCurrentSaturationAttribute())}% `;
2857
+ if (clusterServer.name === 'ColorControl' && clusterServer.isAttributeSupportedByName('colorTemperatureMireds'))
2858
+ attributes += `ColorTemp: ${Math.round(clusterServer.getColorTemperatureMiredsAttribute())} `;
2859
+ if (clusterServer.name === 'BooleanState')
2860
+ attributes += `Contact: ${clusterServer.getStateValueAttribute()} `;
2861
+ if (clusterServer.name === 'BooleanStateConfiguration' && clusterServer.isAttributeSupportedByName('alarmsActive'))
2862
+ attributes += `Active alarms: ${stringify(clusterServer.getAlarmsActiveAttribute())} `;
2863
+ if (clusterServer.name === 'FanControl')
2864
+ attributes += `Mode: ${clusterServer.getFanModeAttribute()} Speed: ${clusterServer.getPercentCurrentAttribute()} `;
2865
+ if (clusterServer.name === 'FanControl' && clusterServer.isAttributeSupportedByName('speedCurrent'))
2866
+ attributes += `MultiSpeed: ${clusterServer.getSpeedCurrentAttribute()} `;
2867
+ if (clusterServer.name === 'OccupancySensing')
2868
+ attributes += `Occupancy: ${clusterServer.getOccupancyAttribute().occupied} `;
2869
+ if (clusterServer.name === 'IlluminanceMeasurement')
2870
+ attributes += `Illuminance: ${clusterServer.getMeasuredValueAttribute()} `;
2871
+ if (clusterServer.name === 'AirQuality')
2872
+ attributes += `Air quality: ${clusterServer.getAirQualityAttribute()} `;
2873
+ if (clusterServer.name === 'TvocMeasurement')
2874
+ attributes += `Voc: ${clusterServer.getMeasuredValueAttribute()} `;
2875
+ if (clusterServer.name === 'TemperatureMeasurement')
2876
+ attributes += `Temperature: ${clusterServer.getMeasuredValueAttribute() / 100}°C `;
2877
+ if (clusterServer.name === 'RelativeHumidityMeasurement')
2878
+ attributes += `Humidity: ${clusterServer.getMeasuredValueAttribute() / 100}% `;
2879
+ if (clusterServer.name === 'PressureMeasurement')
2880
+ attributes += `Pressure: ${clusterServer.getMeasuredValueAttribute()} `;
2881
+ if (clusterServer.name === 'FlowMeasurement')
2882
+ attributes += `Flow: ${clusterServer.getMeasuredValueAttribute()} `;
2883
+ if (clusterServer.name === 'FixedLabel')
2884
+ attributes += `${stringifyFixedLabel(device)} `;
2885
+ }
2886
+ catch (error) {
2887
+ this.log.error(`getClusterTextFromDevice with ${clusterServer.name} error: ${error}`);
2888
+ }
3110
2889
  });
3111
2890
  return attributes;
3112
2891
  }
@@ -3116,14 +2895,13 @@ export class Matterbridge extends EventEmitter {
3116
2895
  *
3117
2896
  * @returns A Promise that resolves when the initialization is complete.
3118
2897
  */
3119
- async startExtension(dataPath, debugEnabled, extensionVersion, port = 5560) {
2898
+ async startExtension(dataPath, extensionVersion, port = 5540) {
3120
2899
  // Set the bridge mode
3121
2900
  this.bridgeMode = 'bridge';
3122
2901
  // Set the first port to use
3123
2902
  this.port = port;
3124
2903
  // Set Matterbridge logger
3125
- this.debugEnabled = debugEnabled;
3126
- this.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logDebug: this.debugEnabled });
2904
+ this.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: "info" /* LogLevel.INFO */ });
3127
2905
  this.log.debug('Matterbridge extension is starting...');
3128
2906
  // Initialize NodeStorage
3129
2907
  this.matterbridgeDirectory = dataPath;
@@ -3148,10 +2926,10 @@ export class Matterbridge extends EventEmitter {
3148
2926
  await this.logNodeAndSystemInfo();
3149
2927
  this.matterbridgeDirectory = dataPath;
3150
2928
  // Set matter.js logger level and format
3151
- Logger.defaultLogLevel = this.debugEnabled ? Level.DEBUG : Level.INFO;
2929
+ Logger.defaultLogLevel = Level.INFO;
3152
2930
  Logger.format = Format.ANSI;
3153
2931
  // Start the storage and create matterbridgeContext
3154
- await this.startStorage('json', path.join(this.matterbridgeDirectory, this.matterStorageName));
2932
+ await this.startMatterStorage('json', path.join(this.matterbridgeDirectory, this.matterStorageName));
3155
2933
  if (!this.storageManager)
3156
2934
  return false;
3157
2935
  this.matterbridgeContext = await this.createCommissioningServerContext('Matterbridge', 'Matterbridge zigbee2MQTT', DeviceTypes.AGGREGATOR.code, 0xfff1, 'Matterbridge', 0x8000, 'zigbee2MQTT Matter extension');
@@ -3195,7 +2973,7 @@ export class Matterbridge extends EventEmitter {
3195
2973
  // Clearing the session manager
3196
2974
  // this.matterbridgeContext?.createContext('SessionManager').clear();
3197
2975
  // Closing storage
3198
- await this.stopStorage();
2976
+ await this.stopMatterStorage();
3199
2977
  this.log.info('Matter server stopped');
3200
2978
  }
3201
2979
  /**