matterbridge 1.3.12 → 1.3.28

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 (53) hide show
  1. package/CHANGELOG.md +48 -1
  2. package/dist/cli.d.ts +1 -1
  3. package/dist/cli.js +10 -7
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.d.ts +5 -3
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +13 -6
  8. package/dist/index.js.map +1 -1
  9. package/dist/matterbridge.d.ts +157 -261
  10. package/dist/matterbridge.d.ts.map +1 -1
  11. package/dist/matterbridge.js +1395 -1611
  12. package/dist/matterbridge.js.map +1 -1
  13. package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -1
  14. package/dist/matterbridgeAccessoryPlatform.js +1 -0
  15. package/dist/matterbridgeAccessoryPlatform.js.map +1 -1
  16. package/dist/matterbridgeDevice.d.ts +9 -3
  17. package/dist/matterbridgeDevice.d.ts.map +1 -1
  18. package/dist/matterbridgeDevice.js +11 -4
  19. package/dist/matterbridgeDevice.js.map +1 -1
  20. package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -1
  21. package/dist/matterbridgeDynamicPlatform.js +1 -0
  22. package/dist/matterbridgeDynamicPlatform.js.map +1 -1
  23. package/dist/matterbridgeTypes.d.ts +111 -0
  24. package/dist/matterbridgeTypes.d.ts.map +1 -0
  25. package/dist/matterbridgeTypes.js +2 -0
  26. package/dist/matterbridgeTypes.js.map +1 -0
  27. package/dist/plugins.d.ts +102 -0
  28. package/dist/plugins.d.ts.map +1 -0
  29. package/dist/plugins.js +674 -0
  30. package/dist/plugins.js.map +1 -0
  31. package/dist/utils/export.d.ts +3 -0
  32. package/dist/utils/export.d.ts.map +1 -0
  33. package/dist/utils/export.js +3 -0
  34. package/dist/utils/export.js.map +1 -0
  35. package/dist/utils/utils.d.ts +37 -2
  36. package/dist/utils/utils.d.ts.map +1 -1
  37. package/dist/utils/utils.js +79 -7
  38. package/dist/utils/utils.js.map +1 -1
  39. package/frontend/build/asset-manifest.json +6 -6
  40. package/frontend/build/index.html +1 -1
  41. package/frontend/build/static/css/{main.b4d28450.css → main.df840158.css} +2 -2
  42. package/frontend/build/static/css/main.df840158.css.map +1 -0
  43. package/frontend/build/static/js/{main.3105733e.js → main.2a46688a.js} +3 -3
  44. package/frontend/build/static/js/main.2a46688a.js.map +1 -0
  45. package/package.json +25 -20
  46. package/__mocks__/@project-chip/matter-node.js/util.js +0 -41
  47. package/dist/utils.d.ts +0 -94
  48. package/dist/utils.d.ts.map +0 -1
  49. package/dist/utils.js +0 -291
  50. package/dist/utils.js.map +0 -1
  51. package/frontend/build/static/css/main.b4d28450.css.map +0 -1
  52. package/frontend/build/static/js/main.3105733e.js.map +0 -1
  53. /package/frontend/build/static/js/{main.3105733e.js.LICENSE.txt → main.2a46688a.js.LICENSE.txt} +0 -0
@@ -21,7 +21,7 @@
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 } from 'node-ansi-logger';
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
25
  import { fileURLToPath, pathToFileURL } from 'url';
26
26
  import { promises as fs } from 'fs';
27
27
  import { exec, spawn } from 'child_process';
@@ -36,7 +36,7 @@ import WebSocket, { WebSocketServer } from 'ws';
36
36
  import { MatterbridgeDevice } from './matterbridgeDevice.js';
37
37
  import { shelly_config, somfytahoma_config, zigbee2mqtt_config } from './defaultConfigSchema.js';
38
38
  import { BridgedDeviceBasicInformation, BridgedDeviceBasicInformationCluster } from './cluster/BridgedDeviceBasicInformationCluster.js';
39
- import { logInterfaces } from './utils/utils.js';
39
+ import { logInterfaces, wait, waiter } from './utils/utils.js';
40
40
  // @project-chip/matter-node.js
41
41
  import { CommissioningController, CommissioningServer, MatterServer } from '@project-chip/matter-node.js';
42
42
  import { BasicInformationCluster, ClusterServer, FixedLabelCluster, GeneralCommissioning, PowerSourceCluster, SwitchCluster, ThreadNetworkDiagnosticsCluster, getClusterNameById } from '@project-chip/matter-node.js/cluster';
@@ -47,6 +47,7 @@ import { ManualPairingCodeCodec, QrCodeSchema } from '@project-chip/matter-node.
47
47
  import { StorageBackendDisk, StorageBackendJsonFile, StorageManager } from '@project-chip/matter-node.js/storage';
48
48
  import { requireMinNodeVersion, getParameter, getIntParameter, hasParameter } from '@project-chip/matter-node.js/util';
49
49
  import { CryptoNode } from '@project-chip/matter-node.js/crypto';
50
+ import { Plugins } from './plugins.js';
50
51
  // Default colors
51
52
  const plg = '\u001B[38;5;33m';
52
53
  const dev = '\u001B[38;5;79m';
@@ -96,29 +97,41 @@ export class Matterbridge extends EventEmitter {
96
97
  matterbridgeVersion = '';
97
98
  matterbridgeLatestVersion = '';
98
99
  matterbridgeFabricInformations = [];
100
+ matterbridgeSessionInformations = [];
99
101
  matterbridgePaired = false;
100
102
  matterbridgeConnected = false;
101
- matterbridgeSessionInformations = [];
102
- checkUpdateInterval; // = 24 * 60 * 60 * 1000; // 24 hours
103
103
  bridgeMode = '';
104
104
  restartMode = '';
105
105
  debugEnabled = false;
106
- mdnsInterface; // matter server mdnsInterface: e.g. 'eth0' or 'wlan0' or 'WiFi'
107
- port = 5540; // first commissioning server port
108
- passcode; // first commissioning server passcode
109
- discriminator; // first commissioning server discriminator
106
+ profile = getParameter('profile');
110
107
  log;
111
- hasCleanupStarted = false;
112
- // private plugins = new Map<string, RegisteredPlugin>();
113
- // private devices = new Map<string, RegisteredDevice>();
114
- registeredPlugins = [];
108
+ plugins;
115
109
  registeredDevices = [];
116
110
  nodeStorage;
117
111
  nodeContext;
112
+ matterStorageName = 'matterbridge' + (getParameter('profile') ? '.' + getParameter('profile') : '') + '.json';
113
+ nodeStorageName = 'storage' + (getParameter('profile') ? '.' + getParameter('profile') : '');
114
+ // Cleanup
115
+ hasCleanupStarted = false;
116
+ initialized = false;
117
+ execRunningCount = 0;
118
+ cleanupTimeout1;
119
+ cleanupTimeout2;
120
+ checkUpdateInterval;
121
+ configureTimeout;
122
+ reachabilityTimeout;
123
+ sigintHandler;
124
+ sigtermHandler;
125
+ // Frontend
118
126
  expressApp;
119
127
  httpServer;
120
128
  httpsServer;
121
129
  webSocketServer;
130
+ // Matter
131
+ mdnsInterface; // matter server mdnsInterface: e.g. 'eth0' or 'wlan0' or 'WiFi'
132
+ port = 5540; // first commissioning server port
133
+ passcode; // first commissioning server passcode
134
+ discriminator; // first commissioning server discriminator
122
135
  storageManager;
123
136
  matterbridgeContext;
124
137
  mattercontrollerContext;
@@ -131,6 +144,9 @@ export class Matterbridge extends EventEmitter {
131
144
  constructor() {
132
145
  super();
133
146
  }
147
+ /** ***********************************************************************************************************************************/
148
+ /** loadInstance() and cleanup() methods */
149
+ /** ***********************************************************************************************************************************/
134
150
  /**
135
151
  * Loads an instance of the Matterbridge class.
136
152
  * If an instance already exists, return that instance.
@@ -142,7 +158,7 @@ export class Matterbridge extends EventEmitter {
142
158
  if (!Matterbridge.instance) {
143
159
  // eslint-disable-next-line no-console
144
160
  if (hasParameter('debug'))
145
- console.log(wr + 'Creating a new instance of Matterbridge.', initialize ? 'Initializing...' : 'Not initializing...', rs);
161
+ console.log(GREEN + 'Creating a new instance of Matterbridge.', initialize ? 'Initializing...' : 'Not initializing...', rs);
146
162
  Matterbridge.instance = new Matterbridge();
147
163
  if (initialize)
148
164
  await Matterbridge.instance.initialize();
@@ -150,111 +166,16 @@ export class Matterbridge extends EventEmitter {
150
166
  return Matterbridge.instance;
151
167
  }
152
168
  /**
153
- * Call shutdownProcess.
154
- * @deprecated This method is deprecated and is only used for jest.
169
+ * Call cleanup().
170
+ * @deprecated This method is deprecated and is only used for jest tests.
155
171
  *
156
172
  */
157
173
  async destroyInstance() {
158
- await this.shutdownProcess();
159
- }
160
- /**
161
- * Initializes the Matterbridge instance as extension for zigbee2mqtt.
162
- * @deprecated This method is deprecated and will be removed in a future version.
163
- *
164
- * @returns A Promise that resolves when the initialization is complete.
165
- */
166
- async startExtension(dataPath, debugEnabled, extensionVersion, port = 5560) {
167
- // Set the bridge mode
168
- this.bridgeMode = 'bridge';
169
- // Set the first port to use
170
- this.port = port;
171
- // Set Matterbridge logger
172
- this.debugEnabled = debugEnabled;
173
- this.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logDebug: this.debugEnabled });
174
- this.log.debug('Matterbridge extension is starting...');
175
- // Initialize NodeStorage
176
- this.matterbridgeDirectory = dataPath;
177
- this.log.debug('Creating node storage manager dir: ' + path.join(this.matterbridgeDirectory, 'node_storage'));
178
- this.nodeStorage = new NodeStorageManager({ dir: path.join(this.matterbridgeDirectory, 'node_storage'), logging: false });
179
- this.log.debug('Creating node storage context for matterbridge: matterbridge');
180
- this.nodeContext = await this.nodeStorage.createStorage('matterbridge');
181
- const plugin = {
182
- path: '',
183
- type: 'DynamicPlatform',
184
- name: 'MatterbridgeExtension',
185
- version: '1.0.0',
186
- description: 'Matterbridge extension',
187
- author: 'https://github.com/Luligu',
188
- enabled: false,
189
- registeredDevices: 0,
190
- addedDevices: 0,
191
- };
192
- this.registeredPlugins.push(plugin);
193
- await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
194
- // Log system info and create .matterbridge directory
195
- await this.logNodeAndSystemInfo();
196
- this.matterbridgeDirectory = dataPath;
197
- // Set matter.js logger level and format
198
- Logger.defaultLogLevel = this.debugEnabled ? Level.DEBUG : Level.INFO;
199
- Logger.format = Format.ANSI;
200
- // Start the storage and create matterbridgeContext
201
- await this.startStorage('json', path.join(this.matterbridgeDirectory, 'matterbridge.json'));
202
- if (!this.storageManager)
203
- return false;
204
- this.matterbridgeContext = await this.createCommissioningServerContext('Matterbridge', 'Matterbridge zigbee2MQTT', DeviceTypes.AGGREGATOR.code, 0xfff1, 'Matterbridge', 0x8000, 'zigbee2MQTT Matter extension');
205
- if (!this.matterbridgeContext)
206
- return false;
207
- await this.matterbridgeContext.set('softwareVersion', 1);
208
- await this.matterbridgeContext.set('softwareVersionString', this.matterbridgeVersion);
209
- await this.matterbridgeContext.set('hardwareVersion', 1);
210
- await this.matterbridgeContext.set('hardwareVersionString', extensionVersion); // Update with the extension version
211
- this.matterServer = this.createMatterServer(this.storageManager);
212
- this.log.debug(`Creating commissioning server for ${plg}Matterbridge${db}`);
213
- this.commissioningServer = await this.createCommisioningServer(this.matterbridgeContext, 'Matterbridge');
214
- this.log.debug(`Creating matter aggregator for ${plg}Matterbridge${db}`);
215
- this.matterAggregator = await this.createMatterAggregator(this.matterbridgeContext, 'Matterbridge');
216
- this.log.debug('Adding matterbridge aggregator to commissioning server');
217
- this.commissioningServer.addDevice(this.matterAggregator);
218
- this.log.debug('Adding matterbridge commissioning server to matter server');
219
- await this.matterServer.addCommissioningServer(this.commissioningServer, { uniqueStorageKey: 'Matterbridge' });
220
- await this.startMatterServer();
221
- this.log.info('Matter server started');
222
- await this.showCommissioningQRCode(this.commissioningServer, this.matterbridgeContext, this.nodeContext, 'Matterbridge');
223
- // Set reachability to true and trigger event after 60 seconds
224
- setTimeout(() => {
225
- this.log.info(`Setting reachability to true for ${plg}Matterbridge${db}`);
226
- if (this.commissioningServer)
227
- this.setCommissioningServerReachability(this.commissioningServer, true);
228
- if (this.matterAggregator)
229
- this.setAggregatorReachability(this.matterAggregator, true);
230
- }, 60 * 1000);
231
- return this.commissioningServer.isCommissioned();
232
- }
233
- /**
234
- * Close the Matterbridge instance as extension for zigbee2mqtt.
235
- * @deprecated This method is deprecated and will be removed in a future version.
236
- *
237
- * @returns A Promise that resolves when the initialization is complete.
238
- */
239
- async stopExtension() {
240
- // Closing matter
241
- await this.stopMatter();
242
- // Clearing the session manager
243
- // this.matterbridgeContext?.createContext('SessionManager').clear();
244
- // Closing storage
245
- await this.stopStorage();
246
- this.log.info('Matter server stopped');
247
- }
248
- /**
249
- * Checks if the extension is commissioned.
250
- * @deprecated This method is deprecated and will be removed in a future version.
251
- *
252
- * @returns {boolean} Returns true if the extension is commissioned, false otherwise.
253
- */
254
- isExtensionCommissioned() {
255
- if (!this.commissioningServer)
256
- return false;
257
- return this.commissioningServer.isCommissioned();
174
+ await this.cleanup('destroying instance...', false);
175
+ await waiter('destroying instance...', () => {
176
+ return this.initialized === false && this.execRunningCount <= 0 ? true : false;
177
+ }, false, 60000, 100, false);
178
+ await wait(1000, 'Wait for the global node_modules and matterbridge version', false);
258
179
  }
259
180
  /**
260
181
  * Initializes the Matterbridge application.
@@ -267,37 +188,6 @@ export class Matterbridge extends EventEmitter {
267
188
  * @returns A Promise that resolves when the initialization is complete.
268
189
  */
269
190
  async initialize() {
270
- // Display the help
271
- if (hasParameter('help')) {
272
- // eslint-disable-next-line no-console
273
- console.log(`\nUsage: matterbridge [options]\n
274
- Options:
275
- - help: show the help
276
- - bridge: start Matterbridge in bridge mode
277
- - childbridge: start Matterbridge in childbridge mode
278
- - port [port]: start the commissioning server on the given port (default 5540)
279
- - mdnsinterface [name]: set the interface to use for the matter server mdnsInterface (default all interfaces)
280
- - frontend [port]: start the frontend on the given port (default 8283)
281
- - debug: enable the Matterbridge debug mode (default false)
282
- - matterlogger: set the matter.js logger level: debug | info | notice | warn | error | fatal (default info)
283
- - reset: remove the commissioning for Matterbridge (bridge mode). Shutdown Matterbridge before using it!
284
- - factoryreset: remove all commissioning information and reset all internal storages. Shutdown Matterbridge before using it!
285
- - list: list the registered plugins
286
- - loginterfaces: log the network interfaces (usefull for finding the name of the interface to use with -mdnsinterface option)
287
- - logstorage: log the node storage
288
- - ssl: enable SSL for the frontend and WebSockerServer (certificates in .matterbridge/certs directory cert.pem, key.pem and ca.pem (optional))
289
- - add [plugin path]: register the plugin from the given absolute or relative path
290
- - add [plugin name]: register the globally installed plugin with the given name
291
- - remove [plugin path]: remove the plugin from the given absolute or relative path
292
- - remove [plugin name]: remove the globally installed plugin with the given name
293
- - enable [plugin path]: enable the plugin from the given absolute or relative path
294
- - enable [plugin name]: enable the globally installed plugin with the given name
295
- - disable [plugin path]: disable the plugin from the given absolute or relative path
296
- - disable [plugin name]: disable the globally installed plugin with the given name
297
- - reset [plugin path]: remove the commissioning for the plugin from the given absolute or relative path (childbridge mode). Shutdown Matterbridge before using it!
298
- - reset [plugin name]: remove the commissioning for the globally installed plugin (childbridge mode). Shutdown Matterbridge before using it!\n`);
299
- process.exit(0);
300
- }
301
191
  // Set the interface to use for the matter server mdnsInterface
302
192
  this.mdnsInterface = getParameter('mdnsinterface');
303
193
  // Set the first port to use for the commissioning server (will be incremented in childbridge mode)
@@ -316,25 +206,51 @@ export class Matterbridge extends EventEmitter {
316
206
  this.debugEnabled = true;
317
207
  this.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logDebug: this.debugEnabled });
318
208
  this.log.debug('Matterbridge is starting...');
319
- // Initialize NodeStorage
209
+ // Set matter.js logger level and format
210
+ if (hasParameter('matterlogger')) {
211
+ const level = getParameter('matterlogger');
212
+ if (level === 'debug') {
213
+ Logger.defaultLogLevel = Level.DEBUG;
214
+ }
215
+ else if (level === 'info') {
216
+ Logger.defaultLogLevel = Level.INFO;
217
+ }
218
+ else if (level === 'notice') {
219
+ Logger.defaultLogLevel = Level.NOTICE;
220
+ }
221
+ else if (level === 'warn') {
222
+ Logger.defaultLogLevel = Level.WARN;
223
+ }
224
+ else if (level === 'error') {
225
+ Logger.defaultLogLevel = Level.ERROR;
226
+ }
227
+ else if (level === 'fatal') {
228
+ Logger.defaultLogLevel = Level.FATAL;
229
+ }
230
+ else {
231
+ this.log.warn(`Invalid matterlogger level: ${level}. Using default level ${this.debugEnabled ? 'debug' : 'info'}.`);
232
+ Logger.defaultLogLevel = Level.INFO;
233
+ }
234
+ }
235
+ else {
236
+ Logger.defaultLogLevel = Level.INFO;
237
+ }
238
+ Logger.format = Format.ANSI;
239
+ // Initialize nodeStorage and nodeContext
320
240
  this.homeDirectory = os.homedir();
321
241
  this.matterbridgeDirectory = path.join(this.homeDirectory, '.matterbridge');
322
- this.log.debug('Creating node storage manager');
323
- this.nodeStorage = new NodeStorageManager({ dir: path.join(this.matterbridgeDirectory, 'storage'), logging: false });
242
+ this.log.debug(`Creating node storage manager: ${CYAN}${this.nodeStorageName}${db}`);
243
+ this.nodeStorage = new NodeStorageManager({ dir: path.join(this.matterbridgeDirectory, this.nodeStorageName), writeQueue: false, expiredInterval: undefined, logging: false });
324
244
  this.log.debug('Creating node storage context for matterbridge');
325
245
  this.nodeContext = await this.nodeStorage.createStorage('matterbridge');
326
- // Get the plugins from node storage and create the plugin node storage contexts
327
- this.registeredPlugins = await this.nodeContext.get('plugins', []);
328
- for (const plugin of this.registeredPlugins) {
329
- const packageJson = await this.parsePlugin(plugin);
330
- if (packageJson) {
331
- // Update the plugin information
332
- plugin.name = packageJson.name;
333
- plugin.version = packageJson.version;
334
- plugin.description = packageJson.description;
335
- plugin.author = packageJson.author;
336
- }
337
- else {
246
+ // Initialize Plugins
247
+ this.plugins = new Plugins(this);
248
+ await this.plugins.loadFromStorage();
249
+ // Get the plugins from node storage and create the plugins node storage contexts
250
+ for (const plugin of this.plugins) {
251
+ const packageJson = await this.plugins.parse(plugin);
252
+ if (packageJson === null) {
253
+ // Try to reinstall the plugin from npm (for Docker pull and external plugins)
338
254
  this.log.info(`Error parsing plugin ${plg}${plugin.name}${nf}. Trying to reinstall it from npm.`);
339
255
  try {
340
256
  await this.spawnCommand('npm', ['install', '-g', plugin.name]);
@@ -347,7 +263,7 @@ export class Matterbridge extends EventEmitter {
347
263
  this.log.error(`Error installing plugin ${plg}${plugin.name}${er}. The plugin is disabled.`);
348
264
  }
349
265
  }
350
- this.log.debug(`Creating node storage context for plugin ${plugin.name}`);
266
+ this.log.debug(`Creating node storage context for plugin ${plg}${plugin.name}${db}`);
351
267
  plugin.nodeContext = await this.nodeStorage.createStorage(plugin.name);
352
268
  await plugin.nodeContext.set('name', plugin.name);
353
269
  await plugin.nodeContext.set('type', plugin.type);
@@ -364,38 +280,9 @@ export class Matterbridge extends EventEmitter {
364
280
  requireMinNodeVersion(18);
365
281
  // Register SIGINT SIGTERM signal handlers
366
282
  this.registerSignalHandlers();
367
- // Set matter.js logger level and format
368
- if (hasParameter('matterlogger')) {
369
- const level = getParameter('matterlogger');
370
- if (level === 'debug') {
371
- Logger.defaultLogLevel = Level.DEBUG;
372
- }
373
- else if (level === 'info') {
374
- Logger.defaultLogLevel = Level.INFO;
375
- }
376
- else if (level === 'notice') {
377
- Logger.defaultLogLevel = Level.NOTICE;
378
- }
379
- else if (level === 'warn') {
380
- Logger.defaultLogLevel = Level.WARN;
381
- }
382
- else if (level === 'error') {
383
- Logger.defaultLogLevel = Level.ERROR;
384
- }
385
- else if (level === 'fatal') {
386
- Logger.defaultLogLevel = Level.FATAL;
387
- }
388
- else {
389
- this.log.warn(`Invalid matterlogger level: ${level}. Using default level ${this.debugEnabled ? 'debug' : 'info'}.`);
390
- Logger.defaultLogLevel = Level.INFO;
391
- }
392
- }
393
- else {
394
- Logger.defaultLogLevel = Level.INFO;
395
- }
396
- Logger.format = Format.ANSI;
397
283
  // Parse command line
398
284
  await this.parseCommandLine();
285
+ this.initialized = true;
399
286
  }
400
287
  /**
401
288
  * Parses the command line arguments and performs the corresponding actions.
@@ -403,10 +290,41 @@ export class Matterbridge extends EventEmitter {
403
290
  * @returns {Promise<void>} A promise that resolves when the command line arguments have been processed, or the process exits.
404
291
  */
405
292
  async parseCommandLine() {
293
+ if (hasParameter('help')) {
294
+ this.log.info(`\nUsage: matterbridge [options]\n
295
+ Options:
296
+ - help: show the help
297
+ - bridge: start Matterbridge in bridge mode
298
+ - childbridge: start Matterbridge in childbridge mode
299
+ - port [port]: start the commissioning server on the given port (default 5540)
300
+ - mdnsinterface [name]: set the interface to use for the matter server mdnsInterface (default all interfaces)
301
+ - frontend [port]: start the frontend on the given port (default 8283)
302
+ - debug: enable the Matterbridge debug mode (default false)
303
+ - matterlogger: set the matter.js logger level: debug | info | notice | warn | error | fatal (default info)
304
+ - reset: remove the commissioning for Matterbridge (bridge mode). Shutdown Matterbridge before using it!
305
+ - factoryreset: remove all commissioning information and reset all internal storages. Shutdown Matterbridge before using it!
306
+ - list: list the registered plugins
307
+ - loginterfaces: log the network interfaces (usefull for finding the name of the interface to use with -mdnsinterface option)
308
+ - logstorage: log the node storage
309
+ - ssl: enable SSL for the frontend and WebSockerServer (certificates in .matterbridge/certs directory cert.pem, key.pem and ca.pem (optional))
310
+ - add [plugin path]: register the plugin from the given absolute or relative path
311
+ - add [plugin name]: register the globally installed plugin with the given name
312
+ - remove [plugin path]: remove the plugin from the given absolute or relative path
313
+ - remove [plugin name]: remove the globally installed plugin with the given name
314
+ - enable [plugin path]: enable the plugin from the given absolute or relative path
315
+ - enable [plugin name]: enable the globally installed plugin with the given name
316
+ - disable [plugin path]: disable the plugin from the given absolute or relative path
317
+ - disable [plugin name]: disable the globally installed plugin with the given name
318
+ - reset [plugin path]: remove the commissioning for the plugin from the given absolute or relative path (childbridge mode). Shutdown Matterbridge before using it!
319
+ - reset [plugin name]: remove the commissioning for the globally installed plugin (childbridge mode). Shutdown Matterbridge before using it!${rs}`);
320
+ this.emit('shutdown');
321
+ return;
322
+ }
406
323
  if (hasParameter('list')) {
407
- this.log.info(`│ Registered plugins (${this.registeredPlugins?.length})`);
408
- this.registeredPlugins.forEach((plugin, index) => {
409
- if (index !== this.registeredPlugins.length - 1) {
324
+ this.log.info(`│ Registered plugins (${this.plugins.length})`);
325
+ let index = 0;
326
+ for (const plugin of this.plugins) {
327
+ if (index !== this.plugins.length - 1) {
410
328
  this.log.info(`├─┬─ plugin ${plg}${plugin.name}${nf}: "${plg}${BRIGHT}${plugin.description}${RESET}${nf}" type: ${typ}${plugin.type}${nf} ${plugin.enabled ? GREEN : RED}enabled ${plugin.paired ? GREEN : RED}paired${nf}`);
411
329
  this.log.info(`│ └─ entry ${UNDERLINE}${db}${plugin.path}${UNDERLINEOFF}${db}`);
412
330
  }
@@ -414,7 +332,8 @@ export class Matterbridge extends EventEmitter {
414
332
  this.log.info(`└─┬─ plugin ${plg}${plugin.name}${nf}: "${plg}${BRIGHT}${plugin.description}${RESET}${nf}" type: ${typ}${plugin.type}${nf} ${plugin.enabled ? GREEN : RED}enabled ${plugin.paired ? GREEN : RED}paired${nf}`);
415
333
  this.log.info(` └─ entry ${UNDERLINE}${db}${plugin.path}${UNDERLINEOFF}${db}`);
416
334
  }
417
- });
335
+ index++;
336
+ }
418
337
  const serializedRegisteredDevices = await this.nodeContext?.get('devices', []);
419
338
  this.log.info(`│ Registered devices (${serializedRegisteredDevices?.length})`);
420
339
  serializedRegisteredDevices?.forEach((device, index) => {
@@ -428,69 +347,78 @@ export class Matterbridge extends EventEmitter {
428
347
  }
429
348
  });
430
349
  this.emit('shutdown');
431
- process.exit(0);
350
+ return;
432
351
  }
433
352
  if (hasParameter('logstorage')) {
434
353
  this.log.info(`${plg}Matterbridge${nf} storage log`);
435
354
  await this.nodeContext?.logStorage();
436
- for (const plugin of this.registeredPlugins) {
355
+ for (const plugin of this.plugins) {
437
356
  this.log.info(`${plg}${plugin.name}${nf} storage log`);
438
357
  await plugin.nodeContext?.logStorage();
439
358
  }
440
359
  this.emit('shutdown');
441
- process.exit(0);
360
+ return;
442
361
  }
443
362
  if (hasParameter('loginterfaces')) {
444
363
  this.log.info(`${plg}Matterbridge${nf} network interfaces log`);
445
364
  logInterfaces();
446
365
  this.emit('shutdown');
447
- process.exit(0);
366
+ return;
448
367
  }
449
368
  if (getParameter('add')) {
450
- this.log.debug(`Registering plugin ${getParameter('add')}`);
451
- await this.executeCommandLine(getParameter('add'), 'add');
369
+ this.log.debug(`Adding plugin ${getParameter('add')}`);
370
+ // await this.executeCommandLine(getParameter('add') as string, 'add');
371
+ await this.plugins.add(getParameter('add'));
452
372
  this.emit('shutdown');
453
- process.exit(0);
373
+ return;
454
374
  }
455
375
  if (getParameter('remove')) {
456
- this.log.debug(`Unregistering plugin ${getParameter('remove')}`);
457
- await this.executeCommandLine(getParameter('remove'), 'remove');
376
+ this.log.debug(`Removing plugin ${getParameter('remove')}`);
377
+ // await this.executeCommandLine(getParameter('remove') as string, 'remove');
378
+ await this.plugins.remove(getParameter('remove'));
458
379
  this.emit('shutdown');
459
- process.exit(0);
380
+ return;
460
381
  }
461
382
  if (getParameter('enable')) {
462
- this.log.debug(`Enable plugin ${getParameter('enable')}`);
463
- await this.executeCommandLine(getParameter('enable'), 'enable');
383
+ this.log.debug(`Enabling plugin ${getParameter('enable')}`);
384
+ // await this.executeCommandLine(getParameter('enable') as string, 'enable');
385
+ await this.plugins.enable(getParameter('enable'));
464
386
  this.emit('shutdown');
465
- process.exit(0);
387
+ return;
466
388
  }
467
389
  if (getParameter('disable')) {
468
- this.log.debug(`Disable plugin ${getParameter('disable')}`);
469
- await this.executeCommandLine(getParameter('disable'), 'disable');
390
+ this.log.debug(`Disabling plugin ${getParameter('disable')}`);
391
+ // await this.executeCommandLine(getParameter('disable') as string, 'disable');
392
+ await this.plugins.disable(getParameter('disable'));
470
393
  this.emit('shutdown');
471
- process.exit(0);
394
+ return;
472
395
  }
473
396
  if (hasParameter('factoryreset')) {
474
397
  try {
475
398
  // Delete matter storage file
476
- await fs.unlink(path.join(this.matterbridgeDirectory, 'matterbridge.json'));
399
+ await fs.unlink(path.join(this.matterbridgeDirectory, this.matterStorageName));
477
400
  }
478
401
  catch (err) {
479
402
  this.log.error(`Error deleting storage: ${err}`);
480
403
  }
481
404
  try {
482
405
  // Delete node storage directory with its subdirectories
483
- await fs.rm(path.join(this.matterbridgeDirectory, 'storage'), { recursive: true });
406
+ await fs.rm(path.join(this.matterbridgeDirectory, this.nodeStorageName), { recursive: true });
484
407
  }
485
408
  catch (err) {
486
409
  this.log.error(`Error removing storage directory: ${err}`);
487
410
  }
488
411
  this.log.info('Factory reset done! Remove all paired devices from the controllers.');
412
+ this.nodeContext = undefined;
413
+ this.nodeStorage = undefined;
414
+ this.plugins.clear();
415
+ this.registeredDevices = [];
489
416
  this.emit('shutdown');
490
- process.exit(0);
417
+ return;
491
418
  }
492
- // Start the storage and create matterbridgeContext (we need it now for frontend and later for matterbridge)
493
- await this.startStorage('json', path.join(this.matterbridgeDirectory, 'matterbridge.json'));
419
+ // Start the matter storage and create the matterbridge context
420
+ await this.startStorage('json', path.join(this.matterbridgeDirectory, this.matterStorageName));
421
+ this.log.debug(`Creating commissioning server context for ${plg}Matterbridge${db}`);
494
422
  this.matterbridgeContext = await this.createCommissioningServerContext('Matterbridge', 'Matterbridge', DeviceTypes.AGGREGATOR.code, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge aggregator');
495
423
  if (hasParameter('reset') && getParameter('reset') === undefined) {
496
424
  this.log.info('Resetting Matterbridge commissioning information...');
@@ -498,14 +426,26 @@ export class Matterbridge extends EventEmitter {
498
426
  await this.stopStorage();
499
427
  this.log.info('Reset done! Remove the device from the controller.');
500
428
  this.emit('shutdown');
501
- process.exit(0);
429
+ return;
502
430
  }
503
431
  if (getParameter('reset') && getParameter('reset') !== undefined) {
504
432
  this.log.debug(`Reset plugin ${getParameter('reset')}`);
505
- await this.executeCommandLine(getParameter('reset'), 'reset');
433
+ const plugin = this.plugins.get(getParameter('reset'));
434
+ if (plugin) {
435
+ if (!this.storageManager)
436
+ this.log.error(`Plugin ${plg}${plugin.name}${er} storageManager not found`);
437
+ const context = this.storageManager?.createContext(plugin.name);
438
+ if (!context)
439
+ this.log.error(`Plugin ${plg}${plugin.name}${er} context not found`);
440
+ await context?.clearAll();
441
+ this.log.info(`Reset commissionig for plugin ${plg}${plugin.name}${nf} done! Remove the device from the controller.`);
442
+ }
443
+ else {
444
+ this.log.warn(`Plugin ${plg}${getParameter('reset')}${wr} not registerd in matterbridge`);
445
+ }
506
446
  await this.stopStorage();
507
447
  this.emit('shutdown');
508
- process.exit(0);
448
+ return;
509
449
  }
510
450
  // Initialize frontend
511
451
  if (getIntParameter('frontend') !== 0 || getIntParameter('frontend') === undefined)
@@ -513,41 +453,28 @@ export class Matterbridge extends EventEmitter {
513
453
  // Check each 60 minutes the latest versions
514
454
  this.checkUpdateInterval = setInterval(() => {
515
455
  this.getMatterbridgeLatestVersion();
516
- this.registeredPlugins.forEach((plugin) => {
456
+ for (const plugin of this.plugins) {
517
457
  this.getPluginLatestVersion(plugin);
518
- });
458
+ }
519
459
  }, 60 * 60 * 1000);
520
460
  if (hasParameter('test')) {
521
461
  this.bridgeMode = 'childbridge';
522
462
  MatterbridgeDevice.bridgeMode = 'childbridge';
523
- await this.testStartMatterBridge();
463
+ await this.startTest();
524
464
  return;
525
465
  }
526
466
  if (hasParameter('controller')) {
527
467
  this.bridgeMode = 'controller';
528
- await this.startMattercontroller();
468
+ await this.startController();
529
469
  return;
530
470
  }
531
471
  if (hasParameter('bridge')) {
532
472
  this.bridgeMode = 'bridge';
533
473
  MatterbridgeDevice.bridgeMode = 'bridge';
534
- if (!this.storageManager) {
535
- this.log.error('No storage manager initialized');
536
- await this.cleanup('No storage manager initialized');
537
- return;
538
- }
474
+ if (!this.storageManager)
475
+ throw new Error('No storage manager initialized');
539
476
  this.log.debug('Starting matterbridge in mode', this.bridgeMode);
540
477
  this.matterServer = this.createMatterServer(this.storageManager);
541
- this.log.debug(`Creating commissioning server context for ${plg}Matterbridge${db}`);
542
- this.matterbridgeContext = await this.createCommissioningServerContext('Matterbridge', 'Matterbridge', DeviceTypes.AGGREGATOR.code, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge Aggregator');
543
- if (!this.matterbridgeContext) {
544
- this.log.error(`Error creating storage context for ${plg}Matterbridge${er}`);
545
- return;
546
- }
547
- if (!this.nodeContext) {
548
- this.log.error(`Node storage context undefined for ${plg}Matterbridge${er}`);
549
- return;
550
- }
551
478
  this.log.debug(`Creating commissioning server for ${plg}Matterbridge${db}`);
552
479
  this.commissioningServer = await this.createCommisioningServer(this.matterbridgeContext, 'Matterbridge');
553
480
  this.log.debug(`Creating matter aggregator for ${plg}Matterbridge${db}`);
@@ -556,18 +483,18 @@ export class Matterbridge extends EventEmitter {
556
483
  this.commissioningServer.addDevice(this.matterAggregator);
557
484
  this.log.debug('Adding matterbridge commissioning server to matter server');
558
485
  await this.matterServer.addCommissioningServer(this.commissioningServer, { uniqueStorageKey: 'Matterbridge' });
559
- for (const plugin of this.registeredPlugins) {
486
+ for (const plugin of this.plugins) {
560
487
  plugin.configJson = await this.loadPluginConfig(plugin);
561
488
  plugin.schemaJson = await this.loadPluginSchema(plugin);
562
489
  // Check if the plugin is available
563
- if (!(await this.resolvePluginName(plugin.path))) {
490
+ if (!(await this.plugins.resolve(plugin.path))) {
564
491
  this.log.error(`Plugin ${plg}${plugin.name}${er} not found. Disabling it.`);
565
492
  plugin.enabled = false;
566
493
  plugin.error = true;
567
494
  continue;
568
495
  }
569
496
  // Check if the plugin has a new version
570
- this.getPluginLatestVersion(plugin);
497
+ this.getPluginLatestVersion(plugin); // No await do it asyncronously
571
498
  if (!plugin.enabled) {
572
499
  this.log.info(`Plugin ${plg}${plugin.name}${nf} not enabled`);
573
500
  continue;
@@ -584,31 +511,28 @@ export class Matterbridge extends EventEmitter {
584
511
  plugin.manualPairingCode = undefined;
585
512
  this.loadPlugin(plugin, true, 'Matterbridge is starting'); // No await do it asyncronously
586
513
  }
587
- await this.startMatterbridge();
514
+ await this.startBridge();
588
515
  return;
589
516
  }
590
517
  if (hasParameter('childbridge')) {
591
518
  this.bridgeMode = 'childbridge';
592
519
  MatterbridgeDevice.bridgeMode = 'childbridge';
593
- if (!this.storageManager) {
594
- this.log.error('No storage manager initialized');
595
- await this.cleanup('No storage manager initialized');
596
- return;
597
- }
520
+ if (!this.storageManager)
521
+ throw new Error('No storage manager initialized');
598
522
  this.log.debug('Starting matterbridge in mode', this.bridgeMode);
599
523
  this.matterServer = this.createMatterServer(this.storageManager);
600
- for (const plugin of this.registeredPlugins) {
524
+ for (const plugin of this.plugins) {
601
525
  plugin.configJson = await this.loadPluginConfig(plugin);
602
526
  plugin.schemaJson = await this.loadPluginSchema(plugin);
603
527
  // Check if the plugin is available
604
- if (!(await this.resolvePluginName(plugin.path))) {
528
+ if (!(await this.plugins.resolve(plugin.path))) {
605
529
  this.log.error(`Plugin ${plg}${plugin.name}${er} not found. Disabling it.`);
606
530
  plugin.enabled = false;
607
531
  plugin.error = true;
608
532
  continue;
609
533
  }
610
534
  // Check if the plugin has a new version
611
- this.getPluginLatestVersion(plugin);
535
+ this.getPluginLatestVersion(plugin); // No await do it asyncronously
612
536
  if (!plugin.enabled) {
613
537
  this.log.info(`Plugin ${plg}${plugin.name}${nf} not enabled`);
614
538
  continue;
@@ -625,182 +549,283 @@ export class Matterbridge extends EventEmitter {
625
549
  plugin.manualPairingCode = (await plugin.nodeContext?.get('manualPairingCode', undefined)) ?? undefined;
626
550
  this.loadPlugin(plugin, true, 'Matterbridge is starting'); // No await do it asyncronously
627
551
  }
628
- await this.startMatterbridge();
552
+ await this.startChildbridge();
629
553
  return;
630
554
  }
631
555
  }
632
- async savePluginsToStorage() {
633
- if (!this.nodeContext) {
634
- this.log.error('loadPluginsFromStorage() error: the node context is not initialized');
635
- return;
636
- }
637
- // Convert the map to an array
638
- // const pluginArray = Array.from(this.plugins.values());
639
- // await this.nodeContext.set('plugins', pluginArray);
640
- // TODO remove after migration done
641
- await this.nodeContext.set('plugins', await this.getBaseRegisteredPlugins());
556
+ /**
557
+ * Registers the signal handlers for SIGINT and SIGTERM.
558
+ * When either of these signals are received, the cleanup method is called with an appropriate message.
559
+ */
560
+ registerSignalHandlers() {
561
+ this.log.debug(`Registering SIGINT and SIGTERM signal handlers...`);
562
+ this.sigintHandler = async () => {
563
+ await this.cleanup('SIGINT received, cleaning up...');
564
+ };
565
+ process.on('SIGINT', this.sigintHandler);
566
+ this.sigtermHandler = async () => {
567
+ await this.cleanup('SIGTERM received, cleaning up...');
568
+ };
569
+ process.on('SIGTERM', this.sigtermHandler);
642
570
  }
643
- async loadPluginsFromStorage() {
644
- if (!this.nodeContext) {
645
- this.log.error('loadPluginsFromStorage() error: the node context is not initialized');
646
- return;
647
- }
648
- // Load the array from storage and convert it back to a map
649
- // const pluginArray = await this.nodeContext.get<RegisteredPlugin[]>('plugins', []);
650
- // for (const plugin of pluginArray) this.plugins.set(plugin.name, plugin);
651
- // TODO remove after migration done
652
- this.registeredPlugins = await this.nodeContext.get('plugins', []);
571
+ /**
572
+ * Deregisters the SIGINT and SIGTERM signal handlers.
573
+ */
574
+ deregisterSignalHandlers() {
575
+ this.log.debug(`Deregistering SIGINT and SIGTERM signal handlers...`);
576
+ this.sigintHandler && process.off('SIGINT', this.sigintHandler);
577
+ this.sigintHandler = undefined;
578
+ this.sigtermHandler && process.off('SIGTERM', this.sigtermHandler);
579
+ this.sigtermHandler = undefined;
653
580
  }
654
581
  /**
655
- * Resolves the name of a plugin by loading and parsing its package.json file.
656
- * @param pluginPath - The path to the plugin or the path to the plugin's package.json file.
657
- * @returns The path to the resolved package.json file, or null if the package.json file is not found or does not contain a name.
582
+ * Logs the node and system information.
658
583
  */
659
- async resolvePluginName(pluginPath) {
660
- if (!pluginPath.endsWith('package.json'))
661
- pluginPath = path.join(pluginPath, 'package.json');
662
- // Resolve the package.json of the plugin
663
- let packageJsonPath = path.resolve(pluginPath);
664
- this.log.debug(`Resolving plugin from ${plg}${packageJsonPath}${db}`);
665
- // Check if the package.json file exists
666
- let packageJsonExists = false;
667
- try {
668
- await fs.access(packageJsonPath);
669
- packageJsonExists = true;
584
+ async logNodeAndSystemInfo() {
585
+ // IP address information
586
+ const networkInterfaces = os.networkInterfaces();
587
+ this.systemInformation.ipv4Address = '';
588
+ this.systemInformation.ipv6Address = '';
589
+ for (const [interfaceName, interfaceDetails] of Object.entries(networkInterfaces)) {
590
+ if (!interfaceDetails) {
591
+ break;
592
+ }
593
+ for (const detail of interfaceDetails) {
594
+ if (detail.family === 'IPv4' && !detail.internal && this.systemInformation.ipv4Address === '') {
595
+ this.systemInformation.interfaceName = interfaceName;
596
+ this.systemInformation.ipv4Address = detail.address;
597
+ this.systemInformation.macAddress = detail.mac;
598
+ }
599
+ else if (detail.family === 'IPv6' && !detail.internal && this.systemInformation.ipv6Address === '') {
600
+ this.systemInformation.interfaceName = interfaceName;
601
+ this.systemInformation.ipv6Address = detail.address;
602
+ this.systemInformation.macAddress = detail.mac;
603
+ }
604
+ }
605
+ if (this.systemInformation.ipv4Address !== '' /* && this.systemInformation.ipv6Address !== ''*/) {
606
+ this.log.debug(`Using interface: '${this.systemInformation.interfaceName}'`);
607
+ this.log.debug(`- with MAC address: '${this.systemInformation.macAddress}'`);
608
+ this.log.debug(`- with IPv4 address: '${this.systemInformation.ipv4Address}'`);
609
+ this.log.debug(`- with IPv6 address: '${this.systemInformation.ipv6Address}'`);
610
+ break;
611
+ }
670
612
  }
671
- catch {
672
- packageJsonExists = false;
613
+ // Node information
614
+ this.systemInformation.nodeVersion = process.versions.node;
615
+ const versionMajor = parseInt(this.systemInformation.nodeVersion.split('.')[0]);
616
+ const versionMinor = parseInt(this.systemInformation.nodeVersion.split('.')[1]);
617
+ const versionPatch = parseInt(this.systemInformation.nodeVersion.split('.')[2]);
618
+ // Host system information
619
+ this.systemInformation.hostname = os.hostname();
620
+ this.systemInformation.user = os.userInfo().username;
621
+ this.systemInformation.osType = os.type(); // "Windows_NT", "Darwin", etc.
622
+ this.systemInformation.osRelease = os.release(); // Kernel version
623
+ this.systemInformation.osPlatform = os.platform(); // "win32", "linux", "darwin", etc.
624
+ this.systemInformation.osArch = os.arch(); // "x64", "arm", etc.
625
+ this.systemInformation.totalMemory = (os.totalmem() / 1024 / 1024 / 1024).toFixed(2) + ' GB'; // Convert to GB
626
+ this.systemInformation.freeMemory = (os.freemem() / 1024 / 1024 / 1024).toFixed(2) + ' GB'; // Convert to GB
627
+ this.systemInformation.systemUptime = (os.uptime() / 60 / 60).toFixed(2) + ' hours'; // Convert to hours
628
+ // Log the system information
629
+ this.log.debug('Host System Information:');
630
+ this.log.debug(`- Hostname: ${this.systemInformation.hostname}`);
631
+ this.log.debug(`- User: ${this.systemInformation.user}`);
632
+ this.log.debug(`- Interface: ${this.systemInformation.interfaceName}`);
633
+ this.log.debug(`- MAC Address: ${this.systemInformation.macAddress}`);
634
+ this.log.debug(`- IPv4 Address: ${this.systemInformation.ipv4Address}`);
635
+ this.log.debug(`- IPv6 Address: ${this.systemInformation.ipv6Address}`);
636
+ this.log.debug(`- Node.js: ${versionMajor}.${versionMinor}.${versionPatch}`);
637
+ this.log.debug(`- OS Type: ${this.systemInformation.osType}`);
638
+ this.log.debug(`- OS Release: ${this.systemInformation.osRelease}`);
639
+ this.log.debug(`- Platform: ${this.systemInformation.osPlatform}`);
640
+ this.log.debug(`- Architecture: ${this.systemInformation.osArch}`);
641
+ this.log.debug(`- Total Memory: ${this.systemInformation.totalMemory}`);
642
+ this.log.debug(`- Free Memory: ${this.systemInformation.freeMemory}`);
643
+ this.log.debug(`- System Uptime: ${this.systemInformation.systemUptime}`);
644
+ // Home directory
645
+ this.homeDirectory = os.homedir();
646
+ this.matterbridgeInformation.homeDirectory = this.homeDirectory;
647
+ this.log.debug(`Home Directory: ${this.homeDirectory}`);
648
+ // Package root directory
649
+ const currentFileDirectory = path.dirname(fileURLToPath(import.meta.url));
650
+ this.rootDirectory = path.resolve(currentFileDirectory, '../');
651
+ this.matterbridgeInformation.rootDirectory = this.rootDirectory;
652
+ this.log.debug(`Root Directory: ${this.rootDirectory}`);
653
+ // Global node_modules directory
654
+ if (this.nodeContext)
655
+ this.globalModulesDirectory = await this.nodeContext.get('globalModulesDirectory', '');
656
+ // First run of Matterbridge so the node storage is empty
657
+ if (this.globalModulesDirectory === '') {
658
+ try {
659
+ this.globalModulesDirectory = await this.getGlobalNodeModules();
660
+ this.matterbridgeInformation.globalModulesDirectory = this.globalModulesDirectory;
661
+ this.log.debug(`Global node_modules Directory: ${this.globalModulesDirectory}`);
662
+ await this.nodeContext?.set('globalModulesDirectory', this.globalModulesDirectory);
663
+ }
664
+ catch (error) {
665
+ this.log.error(`Error getting global node_modules directory: ${error}`);
666
+ }
673
667
  }
674
- if (!packageJsonExists) {
675
- this.log.debug(`Package.json not found at ${packageJsonPath}`);
676
- this.log.debug(`Trying at ${this.globalModulesDirectory}`);
677
- packageJsonPath = path.join(this.globalModulesDirectory, pluginPath);
668
+ else {
669
+ this.getGlobalNodeModules()
670
+ .then(async (globalModulesDirectory) => {
671
+ this.globalModulesDirectory = globalModulesDirectory;
672
+ this.matterbridgeInformation.globalModulesDirectory = this.globalModulesDirectory;
673
+ this.log.debug(`Global node_modules Directory: ${this.globalModulesDirectory}`);
674
+ await this.nodeContext?.set('globalModulesDirectory', this.globalModulesDirectory);
675
+ })
676
+ .catch((error) => {
677
+ this.log.error(`Error getting global node_modules directory: ${error}`);
678
+ });
678
679
  }
680
+ // Create the data directory .matterbridge in the home directory
681
+ this.matterbridgeDirectory = path.join(this.homeDirectory, '.matterbridge');
682
+ this.matterbridgeInformation.matterbridgeDirectory = this.matterbridgeDirectory;
679
683
  try {
680
- // Load the package.json of the plugin
681
- const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
682
- if (!packageJson.name) {
683
- this.log.debug(`Package.json name not found at ${packageJsonPath}`);
684
- return null;
685
- }
686
- this.log.debug(`Package.json name: ${plg}${packageJson.name}${db} description: "${nf}${packageJson.description}${db}" found at "${nf}${packageJsonPath}${db}"`);
687
- return packageJsonPath;
684
+ await fs.access(this.matterbridgeDirectory);
688
685
  }
689
686
  catch (err) {
690
- this.log.debug(`Failed to load plugin from ${plg}${packageJsonPath}${er}: ${err}`);
691
- return null;
692
- }
693
- }
694
- /**
695
- * Loads a plugin from the specified package.json file path.
696
- * @param packageJsonPath - The path to the package.json file of the plugin.
697
- * @param mode - The mode of operation. Possible values are 'add', 'remove', 'enable', 'disable'.
698
- * @returns A Promise that resolves when the plugin is loaded successfully, or rejects with an error if loading fails.
699
- */
700
- async executeCommandLine(packageJsonPath, mode) {
701
- packageJsonPath = (await this.resolvePluginName(packageJsonPath)) ?? packageJsonPath;
702
- this.log.debug(`Loading plugin from ${plg}${packageJsonPath}${db}`);
703
- try {
704
- // Load the package.json of the plugin
705
- const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
706
- if (mode === 'add') {
707
- if (!this.registeredPlugins.find((plugin) => plugin.name === packageJson.name)) {
708
- const plugin = { path: packageJsonPath, type: '', name: packageJson.name, version: packageJson.version, description: packageJson.description, author: packageJson.author, enabled: true };
709
- if (await this.loadPlugin(plugin)) {
710
- this.registeredPlugins.push(plugin);
711
- await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
712
- this.log.info(`Plugin ${plg}${packageJsonPath}${nf} type ${plugin.type} added to matterbridge`);
687
+ if (err instanceof Error) {
688
+ const nodeErr = err;
689
+ if (nodeErr.code === 'ENOENT') {
690
+ try {
691
+ await fs.mkdir(this.matterbridgeDirectory, { recursive: true });
692
+ this.log.info(`Created Matterbridge Directory: ${this.matterbridgeDirectory}`);
713
693
  }
714
- else {
715
- this.log.error(`Plugin ${plg}${packageJsonPath}${wr} not added to matterbridge error`);
694
+ catch (err) {
695
+ this.log.error(`Error creating directory: ${err}`);
716
696
  }
717
697
  }
718
698
  else {
719
- this.log.warn(`Plugin ${plg}${packageJsonPath}${wr} already added to matterbridge`);
720
- }
721
- }
722
- else if (mode === 'remove') {
723
- if (this.registeredPlugins.find((registeredPlugin) => registeredPlugin.name === packageJson.name)) {
724
- this.registeredPlugins.splice(this.registeredPlugins.findIndex((registeredPlugin) => registeredPlugin.name === packageJson.name), 1);
725
- await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
726
- this.log.info(`Plugin ${plg}${packageJsonPath}${nf} removed from matterbridge`);
727
- }
728
- else {
729
- this.log.warn(`Plugin ${plg}${packageJsonPath}${wr} not registerd in matterbridge`);
699
+ this.log.error(`Error accessing directory: ${err}`);
730
700
  }
731
701
  }
732
- else if (mode === 'enable') {
733
- const plugin = this.registeredPlugins.find((registeredPlugin) => registeredPlugin.name === packageJson.name);
734
- if (plugin) {
735
- plugin.enabled = true;
736
- plugin.loaded = undefined;
737
- plugin.started = undefined;
738
- plugin.configured = undefined;
739
- plugin.connected = undefined;
740
- await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
741
- this.log.info(`Plugin ${plg}${packageJsonPath}${nf} enabled`);
702
+ }
703
+ this.log.debug(`Matterbridge Directory: ${this.matterbridgeDirectory}`);
704
+ // Create the plugin directory Matterbridge in the home directory
705
+ this.matterbridgePluginDirectory = path.join(this.homeDirectory, 'Matterbridge');
706
+ this.matterbridgeInformation.matterbridgePluginDirectory = this.matterbridgePluginDirectory;
707
+ try {
708
+ await fs.access(this.matterbridgePluginDirectory);
709
+ }
710
+ catch (err) {
711
+ if (err instanceof Error) {
712
+ const nodeErr = err;
713
+ if (nodeErr.code === 'ENOENT') {
714
+ try {
715
+ await fs.mkdir(this.matterbridgePluginDirectory, { recursive: true });
716
+ this.log.info(`Created Matterbridge Plugin Directory: ${this.matterbridgePluginDirectory}`);
717
+ }
718
+ catch (err) {
719
+ this.log.error(`Error creating directory: ${err}`);
720
+ }
742
721
  }
743
722
  else {
744
- this.log.warn(`Plugin ${plg}${packageJsonPath}${wr} not registerd in matterbridge`);
723
+ this.log.error(`Error accessing directory: ${err}`);
745
724
  }
746
725
  }
747
- else if (mode === 'disable') {
748
- const plugin = this.registeredPlugins.find((registeredPlugin) => registeredPlugin.name === packageJson.name);
749
- if (plugin) {
750
- plugin.enabled = false;
751
- plugin.loaded = undefined;
752
- plugin.started = undefined;
753
- plugin.configured = undefined;
754
- plugin.connected = undefined;
755
- await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
756
- this.log.info(`Plugin ${plg}${packageJsonPath}${nf} disabled`);
726
+ }
727
+ this.log.debug(`Matterbridge Plugin Directory: ${this.matterbridgePluginDirectory}`);
728
+ // Matterbridge version
729
+ const packageJson = JSON.parse(await fs.readFile(path.join(this.rootDirectory, 'package.json'), 'utf-8'));
730
+ this.matterbridgeVersion = packageJson.version;
731
+ this.matterbridgeInformation.matterbridgeVersion = this.matterbridgeVersion;
732
+ this.log.debug(`Matterbridge Version: ${this.matterbridgeVersion}`);
733
+ // Matterbridge latest version
734
+ if (this.nodeContext)
735
+ this.matterbridgeLatestVersion = await this.nodeContext.get('matterbridgeLatestVersion', '');
736
+ this.log.debug(`Matterbridge Latest Version: ${this.matterbridgeLatestVersion}`);
737
+ this.getMatterbridgeLatestVersion();
738
+ // Current working directory
739
+ const currentDir = process.cwd();
740
+ this.log.debug(`Current Working Directory: ${currentDir}`);
741
+ // Command line arguments (excluding 'node' and the script name)
742
+ const cmdArgs = process.argv.slice(2).join(' ');
743
+ this.log.debug(`Command Line Arguments: ${cmdArgs}`);
744
+ }
745
+ /**
746
+ * Retrieves the latest version of a package from the npm registry.
747
+ * @param packageName - The name of the package.
748
+ * @returns A Promise that resolves to the latest version of the package.
749
+ */
750
+ async getLatestVersion(packageName) {
751
+ return new Promise((resolve, reject) => {
752
+ this.execRunningCount++;
753
+ exec(`npm view ${packageName} version`, (error, stdout) => {
754
+ this.execRunningCount--;
755
+ if (error) {
756
+ reject(error);
757
757
  }
758
758
  else {
759
- this.log.warn(`Plugin ${plg}${packageJsonPath}${wr} not registerd in matterbridge`);
759
+ resolve(stdout.trim());
760
760
  }
761
- }
762
- else if (mode === 'reset') {
763
- const plugin = this.registeredPlugins.find((registeredPlugin) => registeredPlugin.name === packageJson.name);
764
- if (plugin) {
765
- plugin.loaded = undefined;
766
- plugin.started = undefined;
767
- plugin.configured = undefined;
768
- plugin.connected = undefined;
769
- plugin.paired = undefined;
770
- plugin.connected = undefined;
771
- if (!this.storageManager)
772
- this.log.error(`Plugin ${plg}${plugin.name}${er} storageManager not found`);
773
- const context = this.storageManager?.createContext(plugin.name);
774
- if (!context)
775
- this.log.error(`Plugin ${plg}${plugin.name}${er} context not found`);
776
- await context?.clearAll();
777
- await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
778
- this.log.info(`Reset commissionig for plugin ${plg}${plugin.name}${nf} done! Remove the device from the controller.`);
761
+ });
762
+ });
763
+ }
764
+ /**
765
+ * Retrieves the path to the global Node.js modules directory.
766
+ * @returns A promise that resolves to the path of the global Node.js modules directory.
767
+ */
768
+ async getGlobalNodeModules() {
769
+ return new Promise((resolve, reject) => {
770
+ this.execRunningCount++;
771
+ exec('npm root -g', (error, stdout) => {
772
+ this.execRunningCount--;
773
+ if (error) {
774
+ reject(error);
779
775
  }
780
776
  else {
781
- this.log.warn(`Plugin ${plg}${packageJsonPath}${wr} not registerd in matterbridge`);
777
+ resolve(stdout.trim());
782
778
  }
783
- }
784
- }
785
- catch (err) {
786
- this.log.error(`Failed to load plugin from ${plg}${packageJsonPath}${er}: ${err}`);
787
- }
779
+ });
780
+ });
788
781
  }
789
782
  /**
790
- * Registers the signal handlers for SIGINT and SIGTERM.
791
- * When either of these signals are received, the cleanup method is called with an appropriate message.
783
+ * Retrieves the latest version of Matterbridge and performs necessary actions based on the version comparison.
784
+ * @private
785
+ * @returns {Promise<void>} A promise that resolves when the latest version is retrieved and actions are performed.
792
786
  */
793
- async registerSignalHandlers() {
794
- this.log.debug(`Registering SIGINT and SIGTERM signal handlers...`);
795
- process.once('SIGINT', async () => {
796
- await this.cleanup('SIGINT received, cleaning up...');
787
+ async getMatterbridgeLatestVersion() {
788
+ this.getLatestVersion('matterbridge')
789
+ .then(async (matterbridgeLatestVersion) => {
790
+ this.matterbridgeLatestVersion = matterbridgeLatestVersion;
791
+ this.matterbridgeInformation.matterbridgeLatestVersion = this.matterbridgeLatestVersion;
792
+ this.log.debug(`Matterbridge Latest Version: ${this.matterbridgeLatestVersion}`);
793
+ await this.nodeContext?.set('matterbridgeLatestVersion', this.matterbridgeLatestVersion);
794
+ if (this.matterbridgeVersion !== this.matterbridgeLatestVersion) {
795
+ this.log.warn(`Matterbridge is out of date. Current version: ${this.matterbridgeVersion}, Latest version: ${this.matterbridgeLatestVersion}`);
796
+ }
797
+ })
798
+ .catch((error) => {
799
+ this.log.error(`Error getting Matterbridge latest version: ${error.message}`);
800
+ // error.stack && this.log.debug(error.stack);
797
801
  });
798
- process.once('SIGTERM', async () => {
799
- await this.cleanup('SIGTERM received, cleaning up...');
802
+ }
803
+ /**
804
+ * Retrieves the latest version of a plugin and updates the plugin's latestVersion property.
805
+ * If the plugin's version is different from the latest version, logs a warning message.
806
+ * If the plugin's version is the same as the latest version, logs an info message.
807
+ * If there is an error retrieving the latest version, logs an error message.
808
+ *
809
+ * @private
810
+ * @param {RegisteredPlugin} plugin - The plugin for which to retrieve the latest version.
811
+ * @returns {Promise<void>} A promise that resolves when the latest version is retrieved and actions are performed.
812
+ */
813
+ async getPluginLatestVersion(plugin) {
814
+ this.getLatestVersion(plugin.name)
815
+ .then(async (latestVersion) => {
816
+ plugin.latestVersion = latestVersion;
817
+ if (plugin.version !== latestVersion)
818
+ this.log.warn(`The plugin ${plg}${plugin.name}${wr} is out of date. Current version: ${plugin.version}, Latest version: ${latestVersion}`);
819
+ else
820
+ this.log.info(`The plugin ${plg}${plugin.name}${nf} is up to date. Current version: ${plugin.version}, Latest version: ${latestVersion}`);
821
+ })
822
+ .catch((error) => {
823
+ this.log.error(`Error getting ${plugin.name} latest version: ${error.message}`);
824
+ // error.stack && this.log.debug(error.stack);
800
825
  });
801
826
  }
802
827
  /**
803
- * Update matterbridge.
828
+ * Update matterbridge and cleanup.
804
829
  */
805
830
  async updateProcess() {
806
831
  await this.cleanup('updating...', false);
@@ -822,7 +847,7 @@ export class Matterbridge extends EventEmitter {
822
847
  */
823
848
  async unregisterAndShutdownProcess() {
824
849
  this.log.info('Unregistering all devices and shutting down...');
825
- for (const plugin of this.registeredPlugins.filter((plugin) => plugin.enabled && !plugin.error)) {
850
+ for (const plugin of this.plugins /* .filter((plugin) => plugin.enabled && !plugin.error))*/) {
826
851
  await this.removeAllBridgedDevices(plugin.name);
827
852
  }
828
853
  await this.cleanup('unregistered all devices and shutting down...', false);
@@ -846,82 +871,39 @@ export class Matterbridge extends EventEmitter {
846
871
  * @returns A promise that resolves when the cleanup is completed.
847
872
  */
848
873
  async cleanup(message, restart = false) {
849
- if (!this.hasCleanupStarted) {
874
+ if (this.initialized && !this.hasCleanupStarted) {
850
875
  this.hasCleanupStarted = true;
851
876
  this.log.info(message);
852
- // Remove all listeners from the process
853
- process.removeAllListeners();
854
- this.log.debug('All listeners removed');
855
- // Clear the update interval
877
+ // Deregisters the SIGINT and SIGTERM signal handlers
878
+ this.deregisterSignalHandlers();
879
+ // Clear the check update interval
856
880
  if (this.checkUpdateInterval)
857
881
  clearInterval(this.checkUpdateInterval);
858
882
  this.checkUpdateInterval = undefined;
859
- this.log.debug('Update interval cleared');
860
- // Calling the shutdown method of each plugin
861
- for (const plugin of this.registeredPlugins) {
883
+ this.log.debug('Check update interval cleared');
884
+ // Clear the configure timeout
885
+ if (this.configureTimeout) {
886
+ clearTimeout(this.configureTimeout);
887
+ this.configureTimeout = undefined;
888
+ this.log.debug('Matterbridge configure timeout cleared');
889
+ }
890
+ // Clear the reachability timeout
891
+ if (this.reachabilityTimeout) {
892
+ clearTimeout(this.reachabilityTimeout);
893
+ this.reachabilityTimeout = undefined;
894
+ this.log.debug('Matterbridge reachability timeout cleared');
895
+ }
896
+ // Calling the shutdown method of each plugin and clear the reachability timeout
897
+ for (const plugin of this.plugins) {
862
898
  if (!plugin.enabled || plugin.error)
863
899
  continue;
864
- this.log.info(`Shutting down plugin ${plg}${plugin.name}${nf}`);
865
- if (plugin.platform) {
866
- try {
867
- await plugin.platform.onShutdown('Matterbridge is closing: ' + message);
868
- // await this.savePluginConfig(plugin);
869
- }
870
- catch (error) {
871
- this.log.error(`Plugin ${plg}${plugin.name}${er} shutting down error: ${error}`);
872
- }
873
- }
874
- else {
875
- this.log.debug(`Plugin ${plg}${plugin.name}${db} platform not found`);
900
+ await this.plugins.shutdown(plugin, 'Matterbridge is closing: ' + message, false);
901
+ if (plugin.reachabilityTimeout) {
902
+ clearTimeout(plugin.reachabilityTimeout);
903
+ plugin.reachabilityTimeout = undefined;
904
+ this.log.debug(`Plugin ${plg}${plugin.name}${db} reachability timeout cleared`);
876
905
  }
877
906
  }
878
- // Set reachability to false
879
- /*
880
- this.log.debug(`*Changing reachability to false for ${this.registeredDevices.length} devices (${this.bridgeMode} mode):`);
881
- this.registeredDevices.forEach((registeredDevice) => {
882
- const plugin = this.registeredPlugins.find((plugin) => plugin.name === registeredDevice.plugin);
883
- if (!plugin) {
884
- this.log.error(`Plugin ${plg}${registeredDevice.plugin}${er} not found`);
885
- return;
886
- }
887
- if (this.bridgeMode === 'bridge' && registeredDevice.device.number) {
888
- this.log.debug(`*-- device: ${dev}${registeredDevice.device.name}${db} plugin ${plg}${registeredDevice.plugin}${db} type ${plugin.type}${db}`);
889
- registeredDevice.device.setBridgedDeviceReachability(false);
890
- registeredDevice.device.getClusterServerById(BridgedDeviceBasicInformation.Cluster.id)?.triggerReachableChangedEvent({ reachableNewValue: false });
891
- }
892
- if (this.bridgeMode === 'childbridge') {
893
- if (plugin.type === 'DynamicPlatform' && registeredDevice.device.number) {
894
- this.log.debug(`*-- device: ${dev}${registeredDevice.device.name}${db} plugin ${plg}${registeredDevice.plugin}${db} type ${plugin.type}${db}`);
895
- registeredDevice.device.setBridgedDeviceReachability(false);
896
- registeredDevice.device.getClusterServerById(BridgedDeviceBasicInformation.Cluster.id)?.triggerReachableChangedEvent({ reachableNewValue: false });
897
- }
898
- }
899
- });
900
- if (this.bridgeMode === 'bridge') {
901
- this.log.debug('*Changing reachability to false for Matterbridge');
902
- this.matterAggregator?.getClusterServerById(BasicInformation.Cluster.id)?.setReachableAttribute(false);
903
- this.matterAggregator?.getClusterServerById(BasicInformation.Cluster.id)?.triggerReachableChangedEvent({ reachableNewValue: false });
904
- this.commissioningServer?.setReachability(false);
905
- }
906
- if (this.bridgeMode === 'childbridge') {
907
- for (const plugin of this.registeredPlugins) {
908
- if (!plugin.enabled || plugin.error) continue;
909
- this.log.debug(`*Changing reachability to false for plugin ${plg}${plugin.name}${db} type ${plugin.type}`);
910
- plugin.aggregator?.getClusterServerById(BasicInformation.Cluster.id)?.setReachableAttribute(false);
911
- plugin.aggregator?.getClusterServerById(BasicInformation.Cluster.id)?.triggerReachableChangedEvent({ reachableNewValue: false });
912
- plugin.commissioningServer?.setReachability(false);
913
- }
914
- }
915
- */
916
- // Close the express server
917
- /*
918
- if (this.expressServer) {
919
- this.expressServer.close();
920
- this.expressServer.removeAllListeners();
921
- this.expressServer = undefined;
922
- this.log.debug('Express server closed successfully');
923
- }
924
- */
925
907
  // Close the http server
926
908
  if (this.httpServer) {
927
909
  this.httpServer.close();
@@ -960,75 +942,82 @@ export class Matterbridge extends EventEmitter {
960
942
  });
961
943
  this.webSocketServer = undefined;
962
944
  }
963
- const cleanupTimeout1 = setTimeout(async () => {
964
- // Closing matter
965
- await this.stopMatter();
966
- // Closing storage
967
- await this.stopStorage();
968
- // Serialize registeredDevices
969
- if (this.nodeStorage && this.nodeContext) {
970
- this.log.info('Saving registered devices...');
971
- const serializedRegisteredDevices = [];
972
- this.registeredDevices.forEach((registeredDevice) => {
973
- const serializedMatterbridgeDevice = registeredDevice.device.serialize(registeredDevice.plugin);
974
- // this.log.info(`- ${serializedMatterbridgeDevice.deviceName}${rs}\n`, serializedMatterbridgeDevice);
975
- if (serializedMatterbridgeDevice)
976
- serializedRegisteredDevices.push(serializedMatterbridgeDevice);
977
- });
978
- await this.nodeContext.set('devices', serializedRegisteredDevices);
979
- this.log.info(`Saved registered devices (${serializedRegisteredDevices?.length})`);
980
- // Clear nodeContext and nodeStorage (they just need 1000ms to write the data to disk)
981
- this.log.debug('Closing node storage context...');
982
- this.nodeContext.close();
983
- this.nodeContext = undefined;
984
- this.log.debug('Closing node storage manager...');
985
- this.nodeStorage.close();
986
- this.nodeStorage = undefined;
987
- }
988
- else {
989
- this.log.error('Error saving registered devices: nodeContext not found!');
990
- }
991
- this.registeredPlugins = [];
992
- this.registeredDevices = [];
993
- this.log.info('Waiting for matter to deliver last messages...');
994
- const cleanupTimeout2 = setTimeout(async () => {
995
- if (restart) {
996
- if (message === 'updating...') {
997
- this.log.info('Cleanup completed. Updating...');
998
- Matterbridge.instance = undefined;
999
- this.emit('update');
1000
- }
1001
- else if (message === 'restarting...') {
1002
- this.log.info('Cleanup completed. Restarting...');
1003
- Matterbridge.instance = undefined;
1004
- this.emit('restart');
1005
- }
1006
- }
1007
- else {
1008
- if (message === 'shutting down with reset...') {
1009
- // Delete matter storage file
1010
- this.log.info('Resetting Matterbridge commissioning information...');
1011
- await fs.unlink(path.join(this.matterbridgeDirectory, 'matterbridge.json'));
1012
- this.log.info('Reset done! Remove all paired devices from the controllers.');
1013
- }
1014
- if (message === 'shutting down with factory reset...') {
1015
- // Delete matter storage file
1016
- this.log.info('Resetting Matterbridge commissioning information...');
1017
- await fs.unlink(path.join(this.matterbridgeDirectory, 'matterbridge.json'));
1018
- // Delete node storage directory with its subdirectories
1019
- this.log.info('Resetting Matterbridge storage...');
1020
- await fs.rm(path.join(this.matterbridgeDirectory, 'storage'), { recursive: true });
1021
- this.log.info('Factory reset done! Remove all paired devices from the controllers.');
1022
- }
1023
- this.log.info('Cleanup completed. Shutting down...');
1024
- Matterbridge.instance = undefined;
1025
- this.emit('shutdown');
945
+ // this.cleanupTimeout1 = setTimeout(async () => {
946
+ // Closing matter
947
+ await this.stopMatterServer();
948
+ // Closing storage
949
+ await this.stopStorage();
950
+ // Serialize registeredDevices
951
+ if (this.nodeStorage && this.nodeContext) {
952
+ this.log.info('Saving registered devices...');
953
+ const serializedRegisteredDevices = [];
954
+ this.registeredDevices.forEach((registeredDevice) => {
955
+ const serializedMatterbridgeDevice = registeredDevice.device.serialize(registeredDevice.plugin);
956
+ // this.log.info(`- ${serializedMatterbridgeDevice.deviceName}${rs}\n`, serializedMatterbridgeDevice);
957
+ if (serializedMatterbridgeDevice)
958
+ serializedRegisteredDevices.push(serializedMatterbridgeDevice);
959
+ });
960
+ await this.nodeContext.set('devices', serializedRegisteredDevices);
961
+ this.log.info(`Saved registered devices (${serializedRegisteredDevices?.length})`);
962
+ // Clear nodeContext and nodeStorage (they just need 1000ms to write the data to disk)
963
+ this.log.debug(`Closing node storage context for ${plg}Matterbridge${db}...`);
964
+ await this.nodeContext.close();
965
+ this.nodeContext = undefined;
966
+ // Clear nodeContext for each plugin (they just need 1000ms to write the data to disk)
967
+ for (const plugin of this.plugins) {
968
+ if (plugin.nodeContext) {
969
+ this.log.debug(`Closing node storage context for plugin ${plg}${plugin.name}${db}...`);
970
+ await plugin.nodeContext.close();
971
+ plugin.nodeContext = undefined;
1026
972
  }
1027
- this.hasCleanupStarted = false;
1028
- }, 2 * 1000);
1029
- cleanupTimeout2.unref();
1030
- }, 3 * 1000);
1031
- cleanupTimeout1.unref();
973
+ }
974
+ this.log.debug('Closing node storage manager...');
975
+ await this.nodeStorage.close();
976
+ this.nodeStorage = undefined;
977
+ }
978
+ else {
979
+ this.log.error('Error saving registered devices: nodeContext not found!');
980
+ }
981
+ this.plugins.clear();
982
+ this.registeredDevices = [];
983
+ // this.log.info('Waiting for matter to deliver last messages...');
984
+ // this.cleanupTimeout2 = setTimeout(async () => {
985
+ if (restart) {
986
+ if (message === 'updating...') {
987
+ this.log.info('Cleanup completed. Updating...');
988
+ Matterbridge.instance = undefined;
989
+ this.emit('update');
990
+ }
991
+ else if (message === 'restarting...') {
992
+ this.log.info('Cleanup completed. Restarting...');
993
+ Matterbridge.instance = undefined;
994
+ this.emit('restart');
995
+ }
996
+ }
997
+ else {
998
+ if (message === 'shutting down with reset...') {
999
+ // Delete matter storage file
1000
+ this.log.info('Resetting Matterbridge commissioning information...');
1001
+ await fs.unlink(path.join(this.matterbridgeDirectory, this.matterStorageName));
1002
+ this.log.info('Reset done! Remove all paired devices from the controllers.');
1003
+ }
1004
+ if (message === 'shutting down with factory reset...') {
1005
+ // Delete matter storage file
1006
+ this.log.info('Resetting Matterbridge commissioning information...');
1007
+ await fs.unlink(path.join(this.matterbridgeDirectory, this.matterStorageName));
1008
+ // Delete node storage directory with its subdirectories
1009
+ this.log.info('Resetting Matterbridge storage...');
1010
+ await fs.rm(path.join(this.matterbridgeDirectory, this.nodeStorageName), { recursive: true });
1011
+ this.log.info('Factory reset done! Remove all paired devices from the controllers.');
1012
+ }
1013
+ this.log.info('Cleanup completed. Shutting down...');
1014
+ Matterbridge.instance = undefined;
1015
+ this.emit('shutdown');
1016
+ }
1017
+ this.hasCleanupStarted = false;
1018
+ this.initialized = false;
1019
+ // }, 2 * 1000);
1020
+ // }, 3 * 1000);
1032
1021
  }
1033
1022
  }
1034
1023
  /**
@@ -1044,7 +1033,7 @@ export class Matterbridge extends EventEmitter {
1044
1033
  }
1045
1034
  this.log.debug(`Adding bridged device ${dev}${device.deviceName}${db} (${dev}${device.name}${db}) for plugin ${plg}${pluginName}${db}`);
1046
1035
  // Check if the plugin is registered
1047
- const plugin = this.registeredPlugins.find((plugin) => plugin.name === pluginName);
1036
+ const plugin = this.plugins.get(pluginName);
1048
1037
  if (!plugin) {
1049
1038
  this.log.error(`Error adding bridged device ${dev}${device.deviceName}${er} (${dev}${device.name}${er}) plugin ${plg}${pluginName}${er} not found`);
1050
1039
  return;
@@ -1102,22 +1091,22 @@ export class Matterbridge extends EventEmitter {
1102
1091
  */
1103
1092
  async removeBridgedDevice(pluginName, device) {
1104
1093
  if (this.bridgeMode === 'bridge' && !this.matterAggregator) {
1105
- this.log.error(`Removing bridged device ${dev}${device.deviceName}${er} (${dev}${device.name}${er}) for plugin ${plg}${pluginName}${er} error: matterAggregator not found`);
1094
+ this.log.error(`Error removing bridged device ${dev}${device.deviceName}${er} (${dev}${device.name}${er}) for plugin ${plg}${pluginName}${er}: matterAggregator not found`);
1106
1095
  return;
1107
1096
  }
1108
1097
  this.log.debug(`Removing bridged device ${dev}${device.deviceName}${db} (${dev}${device.name}${db}) for plugin ${plg}${pluginName}${db}`);
1109
1098
  // Check if the plugin is registered
1110
- const plugin = this.findPlugin(pluginName);
1099
+ const plugin = this.plugins.get(pluginName);
1111
1100
  if (!plugin) {
1112
- this.log.error(`Error removing bridged device ${dev}${device.deviceName}${er} (${dev}${device.name}${er}) plugin ${plg}${pluginName}${er} not found`);
1101
+ this.log.error(`Error removing bridged device ${dev}${device.deviceName}${er} (${dev}${device.name}${er}) for plugin ${plg}${pluginName}${er}: plugin not found`);
1113
1102
  return;
1114
1103
  }
1115
- if (this.bridgeMode === 'childbridge' && !plugin.aggregator) {
1116
- this.log.error(`Error removing bridged device ${dev}${device.deviceName}${er} (${dev}${device.name}${er}) plugin ${plg}${pluginName}${er} aggregator not found`);
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`);
1117
1106
  return;
1118
1107
  }
1119
- if (this.bridgeMode === 'childbridge' && !plugin.connected) {
1120
- this.log.info(`Removing bridged device ${dev}${device.deviceName}${wr} (${dev}${device.name}${wr}) plugin ${plg}${pluginName}${wr} not connected`);
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`);
1121
1110
  return;
1122
1111
  }
1123
1112
  // Remove the device from matterbridge aggregator in bridge mode
@@ -1140,7 +1129,12 @@ export class Matterbridge extends EventEmitter {
1140
1129
  // Remove the device in childbridge mode
1141
1130
  if (this.bridgeMode === 'childbridge') {
1142
1131
  if (plugin.type === 'AccessoryPlatform') {
1143
- this.log.info(`Removing bridged device ${dev}${device.deviceName}${nf} (${dev}${device.name}${nf}) for plugin ${plg}${pluginName}${nf}: AccessoryPlatform not supported in childbridge mode`);
1132
+ this.registeredDevices.forEach((registeredDevice, index) => {
1133
+ if (registeredDevice.device === device) {
1134
+ this.registeredDevices.splice(index, 1);
1135
+ return;
1136
+ }
1137
+ });
1144
1138
  }
1145
1139
  else if (plugin.type === 'DynamicPlatform') {
1146
1140
  this.registeredDevices.forEach((registeredDevice, index) => {
@@ -1158,6 +1152,11 @@ export class Matterbridge extends EventEmitter {
1158
1152
  plugin.registeredDevices--;
1159
1153
  if (plugin.addedDevices !== undefined)
1160
1154
  plugin.addedDevices--;
1155
+ if (plugin.registeredDevices === 0 && plugin.addedDevices === 0 && plugin.commissioningServer) {
1156
+ this.matterServer?.removeCommissioningServer(plugin.commissioningServer);
1157
+ plugin.commissioningServer = undefined;
1158
+ this.log.info(`Removed commissioning server for plugin ${plg}${pluginName}${nf}`);
1159
+ }
1161
1160
  }
1162
1161
  }
1163
1162
  /**
@@ -1167,11 +1166,7 @@ export class Matterbridge extends EventEmitter {
1167
1166
  * @returns A promise that resolves when all devices have been removed.
1168
1167
  */
1169
1168
  async removeAllBridgedDevices(pluginName) {
1170
- const plugin = this.findPlugin(pluginName);
1171
- if (this.bridgeMode === 'childbridge' && plugin?.type === 'AccessoryPlatform') {
1172
- this.log.info(`Removing devices for plugin ${plg}${pluginName}${nf} type AccessoryPlatform is not supported in childbridge mode`);
1173
- return;
1174
- }
1169
+ this.log.debug(`Removing all bridged devices for plugin ${plg}${pluginName}${db}`);
1175
1170
  const devicesToRemove = [];
1176
1171
  for (const registeredDevice of this.registeredDevices) {
1177
1172
  if (registeredDevice.plugin === pluginName) {
@@ -1182,83 +1177,6 @@ export class Matterbridge extends EventEmitter {
1182
1177
  this.removeBridgedDevice(pluginName, registeredDevice.device);
1183
1178
  }
1184
1179
  }
1185
- /**
1186
- * Starts the storage process based on the specified storage type and name.
1187
- * @param {string} storageType - The type of storage to start (e.g., 'disk', 'json').
1188
- * @param {string} storageName - The name of the storage file.
1189
- * @returns {Promise<void>} - A promise that resolves when the storage process is started.
1190
- */
1191
- async startStorage(storageType, storageName) {
1192
- this.log.debug(`Starting storage ${storageType} ${storageName}`);
1193
- if (storageType === 'disk') {
1194
- const storageDisk = new StorageBackendDisk(storageName);
1195
- this.storageManager = new StorageManager(storageDisk);
1196
- }
1197
- else if (storageType === 'json') {
1198
- if (!storageName.endsWith('.json'))
1199
- storageName += '.json';
1200
- const storageJson = new StorageBackendJsonFile(storageName);
1201
- this.storageManager = new StorageManager(storageJson);
1202
- }
1203
- else {
1204
- this.log.error(`Unsupported storage type ${storageType}`);
1205
- await this.cleanup('Unsupported storage type');
1206
- return;
1207
- }
1208
- try {
1209
- await this.storageManager.initialize();
1210
- this.log.debug('Storage initialized');
1211
- if (storageType === 'json') {
1212
- await this.backupJsonStorage(storageName, storageName.replace('.json', '') + '.backup.json');
1213
- }
1214
- }
1215
- catch (error) {
1216
- this.log.error('Storage initialize() error! The file .matterbridge/matterbridge.json may be corrupted.');
1217
- this.log.error('Please delete it and rename matterbridge.backup.json to matterbridge.json and try to restart Matterbridge.');
1218
- await this.cleanup('Storage initialize() error!');
1219
- }
1220
- }
1221
- /**
1222
- * Makes a backup copy of the specified JSON storage file.
1223
- *
1224
- * @param storageName - The name of the JSON storage file to be backed up.
1225
- * @param backupName - The name of the backup file to be created.
1226
- */
1227
- async backupJsonStorage(storageName, backupName) {
1228
- try {
1229
- this.log.debug(`Making backup copy of ${storageName}`);
1230
- await fs.copyFile(storageName, backupName);
1231
- this.log.debug(`Successfully backed up ${storageName} to ${backupName}`);
1232
- }
1233
- catch (err) {
1234
- if (err instanceof Error && 'code' in err) {
1235
- if (err.code === 'ENOENT') {
1236
- this.log.info(`No existing file to back up for ${storageName}. This is expected on the first run.`);
1237
- }
1238
- else {
1239
- this.log.error(`Error making backup copy of ${storageName}: ${err.message}`);
1240
- }
1241
- }
1242
- else {
1243
- this.log.error(`An unexpected error occurred during the backup of ${storageName}: ${String(err)}`);
1244
- }
1245
- }
1246
- }
1247
- /**
1248
- * Stops the storage.
1249
- * @returns {Promise<void>} A promise that resolves when the storage is stopped.
1250
- */
1251
- async stopStorage() {
1252
- this.log.debug('Stopping storage');
1253
- await this.storageManager?.close();
1254
- this.log.debug('Storage closed');
1255
- this.storageManager = undefined;
1256
- this.matterbridgeContext = undefined;
1257
- this.mattercontrollerContext = undefined;
1258
- }
1259
- async testStartMatterBridge() {
1260
- // Start the Matterbridge
1261
- }
1262
1180
  /**
1263
1181
  * Loads the schema for a plugin.
1264
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.
@@ -1319,28 +1237,6 @@ export class Matterbridge extends EventEmitter {
1319
1237
  return schema;
1320
1238
  }
1321
1239
  }
1322
- /**
1323
- * Saves the plugin configuration to a JSON file.
1324
- * @param plugin - The registered plugin.
1325
- * @param config - The platform configuration.
1326
- * @returns A promise that resolves when the configuration is saved successfully, or with an error logged.
1327
- */
1328
- async savePluginConfigFromJson(plugin, config) {
1329
- if (!config.name || !config.type || config.name !== plugin.name) {
1330
- this.log.error(`Error saving plugin ${plg}${plugin.name}${er} config. Wrong config data content.`);
1331
- return;
1332
- }
1333
- const configFile = path.join(this.matterbridgeDirectory, `${plugin.name}.config.json`);
1334
- try {
1335
- await this.writeFile(configFile, JSON.stringify(config, null, 2));
1336
- plugin.configJson = config;
1337
- this.log.debug(`Saved config file ${configFile} for plugin ${plg}${plugin.name}${db}.\nConfig:${rs}\n`, config);
1338
- }
1339
- catch (err) {
1340
- this.log.error(`Error saving plugin ${plg}${plugin.name}${er} config: ${err}`);
1341
- return;
1342
- }
1343
- }
1344
1240
  /**
1345
1241
  * Loads the configuration for a plugin.
1346
1242
  * If the configuration file exists, it reads the file and returns the parsed JSON data.
@@ -1438,6 +1334,70 @@ export class Matterbridge extends EventEmitter {
1438
1334
  this.log.error(`Error writing to ${filePath}:`, error);
1439
1335
  }
1440
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
+ }
1441
1401
  /**
1442
1402
  * Starts a plugin.
1443
1403
  *
@@ -1493,13 +1453,13 @@ export class Matterbridge extends EventEmitter {
1493
1453
  this.log.info(`Plugin ${plg}${plugin.name}${nf} already configured`);
1494
1454
  return Promise.resolve();
1495
1455
  }
1496
- this.log.info(`Configuring plugin ${plg}${plugin.name}${db} type ${typ}${plugin.type}${db}`);
1456
+ this.log.info(`Configuring plugin ${plg}${plugin.name}${nf} type ${typ}${plugin.type}${nf}`);
1497
1457
  try {
1498
1458
  plugin.platform
1499
1459
  .onConfigure()
1500
1460
  .then(() => {
1501
1461
  plugin.configured = true;
1502
- this.log.info(`Configured plugin ${plg}${plugin.name}${db} type ${typ}${plugin.type}${db}`);
1462
+ this.log.info(`Configured plugin ${plg}${plugin.name}${nf} type ${typ}${plugin.type}${nf}`);
1503
1463
  this.savePluginConfig(plugin);
1504
1464
  return Promise.resolve();
1505
1465
  })
@@ -1515,96 +1475,167 @@ export class Matterbridge extends EventEmitter {
1515
1475
  return Promise.resolve();
1516
1476
  }
1517
1477
  }
1478
+ async startTest() {
1479
+ // Start the Matterbridge
1480
+ }
1518
1481
  /**
1519
- * Loads and parse the plugin package.json and returns it.
1520
- * @param plugin - The plugin to load the package from.
1521
- * @returns A Promise that resolves to the package.json object or undefined if the package.json could not be loaded.
1482
+ * Starts the Matterbridge in bridge mode.
1483
+ * @private
1484
+ * @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
1522
1485
  */
1523
- async parsePlugin(plugin) {
1524
- this.log.debug(`Parsing package.json of plugin ${plg}${plugin.name}${nf} type ${typ}${plugin.type}${nf}`);
1525
- try {
1526
- const packageJson = JSON.parse(await fs.readFile(plugin.path, 'utf8'));
1527
- return packageJson;
1528
- }
1529
- catch (err) {
1530
- this.log.error(`Failed to parse plugin ${plg}${plugin.name}${er} package.json: ${err}`);
1531
- plugin.error = true;
1532
- return undefined;
1533
- }
1486
+ async startBridge() {
1487
+ // Plugins are loaded and started by loadPlugin on startup and plugin.loaded and plugin.started are set to true
1488
+ // Plugins are configured by a timer when matter server is started and plugin.configured is set to true
1489
+ this.log.debug('***Starting startMatterInterval in bridge mode');
1490
+ let failCount = 0;
1491
+ const startMatterInterval = setInterval(async () => {
1492
+ for (const plugin of this.plugins) {
1493
+ // new code to not start the bridge if one plugin is in error cause the controllers will delete the devices loosing all the configuration
1494
+ if (!plugin.enabled)
1495
+ continue;
1496
+ if (plugin.error) {
1497
+ clearInterval(startMatterInterval);
1498
+ this.log.debug('***Cleared startMatterInterval interval for Matterbridge for plugin in error state');
1499
+ this.log.error(`The plugin ${plg}${plugin.name}${er} is in error state.`);
1500
+ this.log.error('The bridge will not start until the problem is solved to prevent the controllers from deleting all registered devices.');
1501
+ this.log.error('If you want to start the bridge disable the plugin in error state and restart.');
1502
+ return;
1503
+ }
1504
+ if (!plugin.loaded || !plugin.started) {
1505
+ this.log.debug(`***Waiting (failSafeCount=${failCount}/30) in startMatterInterval interval for plugin ${plg}${plugin.name}${db} loaded: ${plugin.loaded} started: ${plugin.started}...`);
1506
+ failCount++;
1507
+ if (failCount > 30) {
1508
+ this.log.error(`Error waiting for plugin ${plg}${plugin.name}${er} to load and start. Plugin is in error state.`);
1509
+ plugin.error = true;
1510
+ }
1511
+ return;
1512
+ }
1513
+ }
1514
+ clearInterval(startMatterInterval);
1515
+ this.log.debug('***Cleared startMatterInterval interval for Matterbridge');
1516
+ await this.startMatterServer();
1517
+ this.log.info('Matter server started');
1518
+ // Configure the plugins
1519
+ this.configureTimeout = setTimeout(async () => {
1520
+ for (const plugin of this.plugins) {
1521
+ if (!plugin.enabled || !plugin.loaded || !plugin.started || plugin.error)
1522
+ continue;
1523
+ try {
1524
+ await this.plugins.configure(plugin); // No await do it asyncronously
1525
+ }
1526
+ catch (error) {
1527
+ plugin.error = true;
1528
+ this.log.error(`Error configuring plugin ${plg}${plugin.name}${er}`, error);
1529
+ }
1530
+ }
1531
+ }, 30 * 1000);
1532
+ // Show the QR code for commissioning or log the already commissioned message
1533
+ await this.showCommissioningQRCode(this.commissioningServer, this.matterbridgeContext, this.nodeContext, 'Matterbridge');
1534
+ // Setting reachability to true
1535
+ this.reachabilityTimeout = setTimeout(() => {
1536
+ this.log.info(`Setting reachability to true for ${plg}Matterbridge${db}`);
1537
+ if (this.commissioningServer)
1538
+ this.setCommissioningServerReachability(this.commissioningServer, true);
1539
+ if (this.matterAggregator)
1540
+ this.setAggregatorReachability(this.matterAggregator, true);
1541
+ }, 60 * 1000);
1542
+ }, 1000);
1534
1543
  }
1535
1544
  /**
1536
- * Loads a plugin and returns the corresponding MatterbridgePlatform instance.
1537
- * @param plugin - The plugin to load.
1538
- * @param start - Optional flag indicating whether to start the plugin after loading. Default is false.
1539
- * @param message - Optional message to pass to the plugin when starting.
1540
- * @returns A Promise that resolves to the loaded MatterbridgePlatform instance.
1541
- * @throws An error if the plugin is not enabled, already loaded, or fails to load.
1545
+ * Starts the Matterbridge in childbridge mode.
1546
+ * @private
1547
+ * @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
1542
1548
  */
1543
- async loadPlugin(plugin, start = false, message = '') {
1544
- if (!plugin.enabled) {
1545
- this.log.error(`Plugin ${plg}${plugin.name}${er} not enabled`);
1546
- return Promise.resolve(undefined);
1547
- }
1548
- if (plugin.platform) {
1549
- this.log.error(`Plugin ${plg}${plugin.name}${er} already loaded`);
1550
- return Promise.resolve(plugin.platform);
1551
- }
1552
- this.log.info(`Loading plugin ${plg}${plugin.name}${nf} type ${typ}${plugin.type}${nf}`);
1553
- try {
1554
- // Load the package.json of the plugin
1555
- const packageJson = JSON.parse(await fs.readFile(plugin.path, 'utf8'));
1556
- // Resolve the main module path relative to package.json
1557
- const pluginEntry = path.resolve(path.dirname(plugin.path), packageJson.main);
1558
- // Dynamically import the plugin
1559
- const pluginUrl = pathToFileURL(pluginEntry);
1560
- this.log.debug(`Importing plugin ${plg}${plugin.name}${db} from ${pluginUrl.href}`);
1561
- const pluginInstance = await import(pluginUrl.href);
1562
- this.log.debug(`Imported plugin ${plg}${plugin.name}${db} from ${pluginUrl.href}`);
1563
- // Call the default export function of the plugin, passing this MatterBridge instance, the log and the config
1564
- if (pluginInstance.default) {
1565
- const config = await this.loadPluginConfig(plugin);
1566
- const log = new AnsiLogger({ logName: plugin.description, logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logDebug: config.debug ?? false });
1567
- const platform = pluginInstance.default(this, log, config);
1568
- platform.name = packageJson.name;
1569
- platform.config = config;
1570
- platform.version = packageJson.version;
1571
- plugin.name = packageJson.name;
1572
- plugin.description = packageJson.description;
1573
- plugin.version = packageJson.version;
1574
- plugin.author = packageJson.author;
1575
- plugin.type = platform.type;
1576
- plugin.platform = platform;
1577
- plugin.loaded = true;
1578
- plugin.registeredDevices = 0;
1579
- plugin.addedDevices = 0;
1580
- plugin.configJson = config;
1581
- plugin.schemaJson = await this.loadPluginSchema(plugin);
1582
- // Save the updated plugin data in the node storage
1583
- await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
1584
- // this.getPluginLatestVersion(plugin); moved to parseCommandLine
1585
- this.log.info(`Loaded plugin ${plg}${plugin.name}${nf} type ${typ}${platform.type} ${db}(entrypoint ${UNDERLINE}${pluginEntry}${UNDERLINEOFF})`);
1586
- if (start)
1587
- this.startPlugin(plugin, message); // No await do it asyncronously
1588
- return Promise.resolve(platform);
1549
+ async startChildbridge() {
1550
+ // Plugins are loaded and started by loadPlugin on startup and plugin.loaded and plugin.started are set to true
1551
+ // addDevice and addBridgedDeevice create the commissionig servers and add the devices to the the commissioning server or to the aggregator
1552
+ // Plugins are configured by a timer when matter server is started and plugin.configured is set to true
1553
+ this.log.debug('***Starting start matter interval in childbridge mode...');
1554
+ let failCount = 0;
1555
+ const startMatterInterval = setInterval(async () => {
1556
+ let allStarted = true;
1557
+ for (const plugin of this.plugins) {
1558
+ // new code to not start the bridge if one plugin is in error cause the controllers will delete the devices loosing all the configuration
1559
+ if (!plugin.enabled)
1560
+ continue;
1561
+ if (plugin.error) {
1562
+ clearInterval(startMatterInterval);
1563
+ this.log.debug('***Cleared startMatterInterval interval for Matterbridge for plugin in error state');
1564
+ this.log.error(`The plugin ${plg}${plugin.name}${er} is in error state.`);
1565
+ this.log.error('The bridge will not start until the problem is solved to prevent the controllers from deleting all registered devices.');
1566
+ this.log.error('If you want to start the bridge disable the plugin in error state and restart.');
1567
+ return;
1568
+ }
1569
+ this.log.debug(`***Checking plugin ${plg}${plugin.name}${db} to start matter in childbridge mode...`);
1570
+ if (!plugin.loaded || !plugin.started) {
1571
+ allStarted = false;
1572
+ this.log.debug(`***Waiting (failSafeCount=${failCount}/30) for plugin ${plg}${plugin.name}${db} to load (${plugin.loaded}) and start (${plugin.started}) ...`);
1573
+ failCount++;
1574
+ if (failCount > 30) {
1575
+ this.log.error(`Error waiting for plugin ${plg}${plugin.name}${er} to load and start. Plugin is in error mode.`);
1576
+ plugin.error = true;
1577
+ }
1578
+ }
1589
1579
  }
1590
- else {
1591
- this.log.error(`Plugin ${plg}${plugin.name}${er} does not provide a default export`);
1592
- plugin.error = true;
1593
- return Promise.resolve(undefined);
1580
+ if (!allStarted)
1581
+ return;
1582
+ clearInterval(startMatterInterval);
1583
+ this.log.debug('***Cleared startMatterInterval interval in childbridge mode');
1584
+ await this.startMatterServer();
1585
+ this.log.info('Matter server started');
1586
+ // Configure the plugins
1587
+ this.configureTimeout = setTimeout(async () => {
1588
+ for (const plugin of this.plugins) {
1589
+ if (!plugin.enabled || !plugin.loaded || !plugin.started || plugin.error)
1590
+ continue;
1591
+ try {
1592
+ await this.plugins.configure(plugin); // No await do it asyncronously
1593
+ }
1594
+ catch (error) {
1595
+ plugin.error = true;
1596
+ this.log.error(`Error configuring plugin ${plg}${plugin.name}${er}`, error);
1597
+ }
1598
+ }
1599
+ }, 30 * 1000);
1600
+ for (const plugin of this.plugins) {
1601
+ if (!plugin.enabled || plugin.error)
1602
+ continue;
1603
+ if (!plugin.addedDevices || plugin.addedDevices === 0) {
1604
+ this.log.error(`Plugin ${plg}${plugin.name}${er} didn't add any devices to Matterbridge. Verify the plugin configuration.`);
1605
+ continue;
1606
+ }
1607
+ if (!plugin.commissioningServer) {
1608
+ this.log.error(`Commissioning server not found for plugin ${plg}${plugin.name}${er}`);
1609
+ continue;
1610
+ }
1611
+ if (!plugin.storageContext) {
1612
+ this.log.error(`Storage context not found for plugin ${plg}${plugin.name}${er}`);
1613
+ continue;
1614
+ }
1615
+ if (!plugin.nodeContext) {
1616
+ this.log.error(`Node storage context not found for plugin ${plg}${plugin.name}${er}`);
1617
+ continue;
1618
+ }
1619
+ await this.showCommissioningQRCode(plugin.commissioningServer, plugin.storageContext, plugin.nodeContext, plugin.name);
1620
+ // Setting reachability to true
1621
+ plugin.reachabilityTimeout = setTimeout(() => {
1622
+ this.log.info(`Setting reachability to true for ${plg}${plugin.name}${db}`);
1623
+ if (plugin.commissioningServer)
1624
+ this.setCommissioningServerReachability(plugin.commissioningServer, true);
1625
+ if (plugin.type === 'AccessoryPlatform' && plugin.device)
1626
+ this.setDeviceReachability(plugin.device, true);
1627
+ if (plugin.type === 'DynamicPlatform' && plugin.aggregator)
1628
+ this.setAggregatorReachability(plugin.aggregator, true);
1629
+ }, 60 * 1000);
1594
1630
  }
1595
- }
1596
- catch (err) {
1597
- this.log.error(`Failed to load plugin ${plg}${plugin.name}${er}: ${err}`);
1598
- plugin.error = true;
1599
- return Promise.resolve(undefined);
1600
- }
1631
+ }, 1000);
1601
1632
  }
1602
1633
  /**
1603
1634
  * Starts the Matterbridge controller.
1604
1635
  * @private
1605
1636
  * @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
1606
1637
  */
1607
- async startMattercontroller() {
1638
+ async startController() {
1608
1639
  if (!this.storageManager) {
1609
1640
  this.log.error('No storage manager initialized');
1610
1641
  await this.cleanup('No storage manager initialized');
@@ -1775,143 +1806,105 @@ export class Matterbridge extends EventEmitter {
1775
1806
  this.log.info('Subscribed to all attributes and events');
1776
1807
  }
1777
1808
  }
1809
+ /** ***********************************************************************************************************************************/
1810
+ /** Matter.js methods */
1811
+ /** ***********************************************************************************************************************************/
1778
1812
  /**
1779
- * Starts the Matterbridge based on the bridge mode.
1780
- * If the bridge mode is 'bridge', it creates a commissioning server, matter aggregator,
1781
- * and starts the matter server.
1782
- * If the bridge mode is 'childbridge', it starts the plugins, creates commissioning servers,
1783
- * and starts the matter server when all plugins are loaded and started.
1784
- * @private
1785
- * @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
1813
+ * Starts the matter storage process based on the specified storage type and name.
1814
+ * @param {string} storageType - The type of storage to start (e.g., 'disk', 'json').
1815
+ * @param {string} storageName - The name of the storage file.
1816
+ * @returns {Promise<void>} - A promise that resolves when the storage process is started.
1786
1817
  */
1787
- async startMatterbridge() {
1788
- if (this.bridgeMode === 'bridge') {
1789
- // Plugins are loaded and started by loadPlugin on startup and plugin.loaded and plugin.loaded are set to true
1790
- // Plugins are configured by callback when Matterbridge is commissioned and plugin.configured is set to true
1791
- this.log.debug('***Starting startMatterbridge interval for Matterbridge');
1792
- let failCount = 0;
1793
- const startMatterInterval = setInterval(async () => {
1794
- for (const plugin of this.registeredPlugins) {
1795
- // new code to not start the bridge if one plugin is in error cause the controllers will delete the devices loosing all the configuration
1796
- if (!plugin.enabled)
1797
- continue;
1798
- if (plugin.error) {
1799
- clearInterval(startMatterInterval);
1800
- this.log.debug('***Cleared startMatterInterval interval for Matterbridge for plugin in error state');
1801
- this.log.error(`The plugin ${plg}${plugin.name}${er} is in error state.`);
1802
- this.log.error('The bridge will not start until the problem is solved to prevent the controllers from deleting all registered devices.');
1803
- this.log.error('If you want to start the bridge disable the plugin in error state and restart.');
1804
- return;
1805
- }
1806
- if (!plugin.loaded || !plugin.started) {
1807
- this.log.debug(`***Waiting (failSafeCount=${failCount}/30) in startMatterInterval interval for plugin ${plg}${plugin.name}${db} loaded: ${plugin.loaded} started: ${plugin.started}...`);
1808
- failCount++;
1809
- if (failCount > 30) {
1810
- this.log.error(`Error waiting for plugin ${plg}${plugin.name}${er} to load and start. Plugin is in error state.`);
1811
- plugin.error = true;
1812
- }
1813
- return;
1814
- }
1815
- }
1816
- clearInterval(startMatterInterval);
1817
- this.log.debug('***Cleared startMatterInterval interval for Matterbridge');
1818
- await this.startMatterServer();
1819
- this.log.info('Matter server started');
1820
- // Configure the plugins
1821
- /*
1822
- for (const plugin of this.registeredPlugins) {
1823
- if (!plugin.enabled || !plugin.loaded || !plugin.started || plugin.error) continue;
1824
- try {
1825
- this.configurePlugin(plugin); // No await do it asyncronously
1826
- } catch (error) {
1827
- plugin.error = true;
1828
- this.log.error(`Error configuring plugin ${plg}${plugin.name}${er}`, error);
1829
- }
1830
- }
1831
- */
1832
- // Show the QR code for commissioning or log the already commissioned message
1833
- await this.showCommissioningQRCode(this.commissioningServer, this.matterbridgeContext, this.nodeContext, 'Matterbridge');
1834
- // Setting reachability to true
1835
- setTimeout(() => {
1836
- this.log.info(`Setting reachability to true for ${plg}Matterbridge${db}`);
1837
- if (this.commissioningServer)
1838
- this.setCommissioningServerReachability(this.commissioningServer, true);
1839
- if (this.matterAggregator)
1840
- this.setAggregatorReachability(this.matterAggregator, true);
1841
- }, 60 * 1000);
1842
- }, 1000);
1818
+ async startStorage(storageType, storageName) {
1819
+ this.log.debug(`Starting ${storageType} storage ${CYAN}${storageName}${db}`);
1820
+ if (storageType === 'disk') {
1821
+ const storageDisk = new StorageBackendDisk(storageName);
1822
+ this.storageManager = new StorageManager(storageDisk);
1843
1823
  }
1844
- if (this.bridgeMode === 'childbridge') {
1845
- // Plugins are loaded and started by loadPlugin on startup
1846
- // addDevice and addBridgedDeevice create the commissionig servers and add the devices to the the commissioning server or to the aggregator
1847
- // Plugins are configured by callback when the plugin is commissioned
1848
- // Start the interval to check if all plugins are loaded and started and so start the matter server
1849
- this.log.debug('***Starting start matter interval in childbridge mode...');
1850
- let failCount = 0;
1851
- const startMatterInterval = setInterval(async () => {
1852
- let allStarted = true;
1853
- for (const plugin of this.registeredPlugins) {
1854
- // new code to not start the bridge if one plugin is in error cause the controllers will delete the devices loosing all the configuration
1855
- if (!plugin.enabled)
1856
- continue;
1857
- if (plugin.error) {
1858
- clearInterval(startMatterInterval);
1859
- this.log.debug('***Cleared startMatterInterval interval for Matterbridge for plugin in error state');
1860
- this.log.error(`The plugin ${plg}${plugin.name}${er} is in error state.`);
1861
- this.log.error('The bridge will not start until the problem is solved to prevent the controllers from deleting all registered devices.');
1862
- this.log.error('If you want to start the bridge disable the plugin in error state and restart.');
1863
- return;
1864
- }
1865
- this.log.debug(`***Checking plugin ${plg}${plugin.name}${db} to start matter in childbridge mode...`);
1866
- if (!plugin.loaded || !plugin.started) {
1867
- allStarted = false;
1868
- this.log.debug(`***Waiting (failSafeCount=${failCount}/30) for plugin ${plg}${plugin.name}${db} to load (${plugin.loaded}) and start (${plugin.started}) ...`);
1869
- failCount++;
1870
- if (failCount > 30) {
1871
- this.log.error(`Error waiting for plugin ${plg}${plugin.name}${er} to load and start. Plugin is in error mode.`);
1872
- plugin.error = true;
1873
- }
1874
- }
1824
+ else if (storageType === 'json') {
1825
+ if (!storageName.endsWith('.json'))
1826
+ storageName += '.json';
1827
+ const storageJson = new StorageBackendJsonFile(storageName);
1828
+ this.storageManager = new StorageManager(storageJson);
1829
+ }
1830
+ else {
1831
+ this.log.error(`Unsupported storage type ${storageType}`);
1832
+ await this.cleanup('Unsupported storage type');
1833
+ return;
1834
+ }
1835
+ try {
1836
+ await this.storageManager.initialize();
1837
+ this.log.debug('Storage initialized');
1838
+ if (storageType === 'json') {
1839
+ await this.backupJsonStorage(storageName, storageName.replace('.json', '') + '.backup.json');
1840
+ }
1841
+ }
1842
+ catch (error) {
1843
+ this.log.error(`Storage initialize() error! The file .matterbridge/${storageName} may be corrupted.`);
1844
+ this.log.error(`Please delete it and rename ${storageName.replace('.json', '.backup.json')} to ${storageName} and try to restart Matterbridge.`);
1845
+ await this.cleanup('Storage initialize() error!');
1846
+ }
1847
+ }
1848
+ /**
1849
+ * Makes a backup copy of the specified matter JSON storage file.
1850
+ *
1851
+ * @param storageName - The name of the JSON storage file to be backed up.
1852
+ * @param backupName - The name of the backup file to be created.
1853
+ */
1854
+ async backupJsonStorage(storageName, backupName) {
1855
+ try {
1856
+ this.log.debug(`Making backup copy of ${storageName}`);
1857
+ await fs.copyFile(storageName, backupName);
1858
+ this.log.debug(`Successfully backed up ${storageName} to ${backupName}`);
1859
+ }
1860
+ catch (err) {
1861
+ if (err instanceof Error && 'code' in err) {
1862
+ if (err.code === 'ENOENT') {
1863
+ this.log.info(`No existing file to back up for ${storageName}. This is expected on the first run.`);
1875
1864
  }
1876
- if (!allStarted)
1877
- return;
1878
- clearInterval(startMatterInterval);
1879
- this.log.debug('***Cleared startMatterInterval interval in childbridge mode');
1880
- await this.startMatterServer();
1881
- this.log.info('Matter server started');
1882
- for (const plugin of this.registeredPlugins) {
1883
- if (!plugin.enabled || plugin.error)
1884
- continue;
1885
- if (!plugin.addedDevices || plugin.addedDevices === 0) {
1886
- this.log.error(`Plugin ${plg}${plugin.name}${er} didn't add any devices to Matterbridge. Verify the plugin configuration.`);
1887
- continue;
1888
- }
1889
- if (!plugin.commissioningServer) {
1890
- this.log.error(`Commissioning server not found for plugin ${plg}${plugin.name}${er}`);
1891
- continue;
1892
- }
1893
- if (!plugin.storageContext) {
1894
- this.log.error(`Storage context not found for plugin ${plg}${plugin.name}${er}`);
1895
- continue;
1896
- }
1897
- if (!plugin.nodeContext) {
1898
- this.log.error(`Node storage context not found for plugin ${plg}${plugin.name}${er}`);
1899
- continue;
1900
- }
1901
- await this.showCommissioningQRCode(plugin.commissioningServer, plugin.storageContext, plugin.nodeContext, plugin.name);
1902
- // Setting reachability to true
1903
- setTimeout(() => {
1904
- this.log.info(`Setting reachability to true for ${plg}${plugin.name}${db}`);
1905
- if (plugin.commissioningServer)
1906
- this.setCommissioningServerReachability(plugin.commissioningServer, true);
1907
- if (plugin.type === 'AccessoryPlatform' && plugin.device)
1908
- this.setDeviceReachability(plugin.device, true);
1909
- if (plugin.type === 'DynamicPlatform' && plugin.aggregator)
1910
- this.setAggregatorReachability(plugin.aggregator, true);
1911
- }, 60 * 1000);
1865
+ else {
1866
+ this.log.error(`Error making backup copy of ${storageName}: ${err.message}`);
1912
1867
  }
1913
- }, 1000);
1868
+ }
1869
+ else {
1870
+ this.log.error(`An unexpected error occurred during the backup of ${storageName}: ${String(err)}`);
1871
+ }
1872
+ }
1873
+ }
1874
+ /**
1875
+ * Stops the matter storage.
1876
+ * @returns {Promise<void>} A promise that resolves when the storage is stopped.
1877
+ */
1878
+ async stopStorage() {
1879
+ this.log.debug('Stopping storage');
1880
+ await this.storageManager?.close();
1881
+ this.log.debug('Storage closed');
1882
+ this.storageManager = undefined;
1883
+ this.matterbridgeContext = undefined;
1884
+ this.mattercontrollerContext = undefined;
1885
+ }
1886
+ /**
1887
+ * Creates a Matter server using the provided storage manager and the provided mdnsInterface.
1888
+ * @param storageManager The storage manager to be used by the Matter server.
1889
+ *
1890
+ */
1891
+ createMatterServer(storageManager) {
1892
+ this.log.debug('Creating matter server');
1893
+ // Validate mdnsInterface
1894
+ if (this.mdnsInterface) {
1895
+ const networkInterfaces = os.networkInterfaces();
1896
+ const availableInterfaces = Object.keys(networkInterfaces);
1897
+ if (!availableInterfaces.includes(this.mdnsInterface)) {
1898
+ this.log.error(`Invalid mdnsInterface: ${this.mdnsInterface}. Available interfaces are: ${availableInterfaces.join(', ')}. Using all available interfaces.`);
1899
+ this.mdnsInterface = undefined;
1900
+ }
1901
+ else {
1902
+ this.log.info(`Using mdnsInterface '${this.mdnsInterface}' for the Matter server MdnsBroadcaster.`);
1903
+ }
1914
1904
  }
1905
+ const matterServer = new MatterServer(storageManager, { mdnsInterface: this.mdnsInterface });
1906
+ this.log.debug('Created matter server');
1907
+ return matterServer;
1915
1908
  }
1916
1909
  /**
1917
1910
  * Starts the Matter server.
@@ -1929,69 +1922,234 @@ export class Matterbridge extends EventEmitter {
1929
1922
  // this.commissioningServer?.getRootEndpoint() && logEndpoint(this.commissioningServer?.getRootEndpoint());
1930
1923
  }
1931
1924
  /**
1932
- * Imports the commissioning server context for a specific plugin and device.
1933
- * @param pluginName - The name of the plugin.
1934
- * @param device - The MatterbridgeDevice object representing the device.
1935
- * @returns The commissioning server context.
1936
- * @throws Error if the BasicInformationCluster is not found.
1925
+ * Stops the Matter server, commissioningServer and commissioningController.
1937
1926
  */
1938
- async importCommissioningServerContext(pluginName, device) {
1939
- this.log.debug(`Importing matter commissioning server storage context from device for ${plg}${pluginName}${db}`);
1940
- const basic = device.getClusterServer(BasicInformationCluster);
1941
- if (!basic) {
1942
- this.log.error('importCommissioningServerContext error: cannot find the BasicInformationCluster');
1943
- process.exit(1);
1944
- }
1945
- if (!this.storageManager) {
1946
- this.log.error('importCommissioningServerContext error: no storage manager initialized');
1947
- process.exit(1);
1948
- }
1949
- this.log.debug(`Importing commissioning server storage context for ${plg}${pluginName}${db}`);
1950
- const storageContext = this.storageManager.createContext(pluginName);
1951
- await storageContext.set('deviceName', basic.getNodeLabelAttribute());
1952
- await storageContext.set('deviceType', DeviceTypeId(device.deviceType));
1953
- await storageContext.set('vendorId', basic.getVendorIdAttribute());
1954
- await storageContext.set('vendorName', basic.getVendorNameAttribute());
1955
- await storageContext.set('productId', basic.getProductIdAttribute());
1956
- await storageContext.set('productName', basic.getProductNameAttribute());
1957
- await storageContext.set('nodeLabel', basic.getNodeLabelAttribute());
1958
- await storageContext.set('productLabel', basic.getNodeLabelAttribute());
1959
- await storageContext.set('serialNumber', basic.attributes.serialNumber?.getLocal());
1960
- await storageContext.set('uniqueId', basic.attributes.uniqueId?.getLocal());
1961
- await storageContext.set('softwareVersion', basic.getSoftwareVersionAttribute());
1962
- await storageContext.set('softwareVersionString', basic.getSoftwareVersionStringAttribute());
1963
- await storageContext.set('hardwareVersion', basic.getHardwareVersionAttribute());
1964
- await storageContext.set('hardwareVersionString', basic.getHardwareVersionStringAttribute());
1965
- this.log.debug(`Imported commissioning server storage context for ${plg}${pluginName}${db}`);
1966
- this.log.debug(`- deviceName: ${await storageContext.get('deviceName')} deviceType: ${await storageContext.get('deviceType')}(0x${(await storageContext.get('deviceType'))?.toString(16).padStart(4, '0')})`);
1967
- this.log.debug(`- serialNumber: ${await storageContext.get('serialNumber')} uniqueId: ${await storageContext.get('uniqueId')}`);
1968
- this.log.debug(`- softwareVersion: ${await storageContext.get('softwareVersion')} softwareVersionString: ${await storageContext.get('softwareVersionString')}`);
1969
- this.log.debug(`- hardwareVersion: ${await storageContext.get('hardwareVersion')} hardwareVersionString: ${await storageContext.get('hardwareVersionString')}`);
1970
- return storageContext;
1927
+ async stopMatterServer() {
1928
+ this.log.debug('Stopping matter commissioningServer');
1929
+ await this.commissioningServer?.close();
1930
+ this.log.debug('Stopping matter commissioningController');
1931
+ await this.commissioningController?.close();
1932
+ this.log.debug('Stopping matter server');
1933
+ await this.matterServer?.close();
1934
+ this.log.debug('Matter server closed');
1935
+ this.commissioningController = undefined;
1936
+ this.commissioningServer = undefined;
1937
+ this.matterAggregator = undefined;
1938
+ this.matterServer = undefined;
1971
1939
  }
1972
1940
  /**
1973
- * Creates a commissioning server storage context.
1941
+ * Creates a Matter Aggregator.
1942
+ * @param {StorageContext} context - The storage context.
1943
+ * @returns {Aggregator} - The created Matter Aggregator.
1944
+ */
1945
+ async createMatterAggregator(context, pluginName) {
1946
+ const random = 'AG' + CryptoNode.getRandomData(8).toHex();
1947
+ await context.set('aggregatorSerialNumber', await context.get('aggregatorSerialNumber', random));
1948
+ await context.set('aggregatorUniqueId', await context.get('aggregatorUniqueId', random));
1949
+ this.log.debug(`Creating matter aggregator for plugin ${plg}${pluginName}${db} with uniqueId ${await context.get('aggregatorUniqueId')} serialNumber ${await context.get('aggregatorSerialNumber')}`);
1950
+ this.log.debug(`Creating matter aggregator for plugin ${plg}${pluginName}${db} with softwareVersion ${await context.get('softwareVersion', 1)} softwareVersionString ${await context.get('softwareVersionString', '1.0.0')}`);
1951
+ this.log.debug(`Creating matter aggregator for plugin ${plg}${pluginName}${db} with hardwareVersion ${await context.get('hardwareVersion', 1)} hardwareVersionString ${await context.get('hardwareVersionString', '1.0.0')}`);
1952
+ const matterAggregator = new Aggregator();
1953
+ matterAggregator.addClusterServer(ClusterServer(BasicInformationCluster, {
1954
+ dataModelRevision: 1,
1955
+ location: 'FR',
1956
+ vendorId: VendorId(0xfff1),
1957
+ vendorName: 'Matterbridge',
1958
+ productId: 0x8000,
1959
+ productName: 'Matterbridge aggregator',
1960
+ productLabel: 'Matterbridge aggregator',
1961
+ nodeLabel: 'Matterbridge aggregator',
1962
+ serialNumber: await context.get('aggregatorSerialNumber'),
1963
+ uniqueId: await context.get('aggregatorUniqueId'),
1964
+ softwareVersion: await context.get('softwareVersion', 1),
1965
+ softwareVersionString: await context.get('softwareVersionString', '1.0.0'),
1966
+ hardwareVersion: await context.get('hardwareVersion', 1),
1967
+ hardwareVersionString: await context.get('hardwareVersionString', '1.0.0'),
1968
+ reachable: true,
1969
+ capabilityMinima: { caseSessionsPerFabric: 3, subscriptionsPerFabric: 3 },
1970
+ }, {}, {
1971
+ startUp: true,
1972
+ shutDown: true,
1973
+ leave: true,
1974
+ reachableChanged: true,
1975
+ }));
1976
+ return matterAggregator;
1977
+ }
1978
+ /**
1979
+ * Creates a matter commissioning server.
1974
1980
  *
1975
- * @param pluginName - The name of the plugin.
1976
- * @param deviceName - The name of the device.
1977
- * @param deviceType - The type of the device.
1978
- * @param vendorId - The vendor ID.
1979
- * @param vendorName - The vendor name.
1980
- * @param productId - The product ID.
1981
- * @param productName - The product name.
1982
- * @param serialNumber - The serial number of the device (optional).
1983
- * @param uniqueId - The unique ID of the device (optional).
1984
- * @param softwareVersion - The software version of the device (optional).
1985
- * @param softwareVersionString - The software version string of the device (optional).
1986
- * @param hardwareVersion - The hardware version of the device (optional).
1987
- * @param hardwareVersionString - The hardware version string of the device (optional).
1988
- * @returns The storage context for the commissioning server.
1981
+ * @param {StorageContext} context - The storage context.
1982
+ * @param {string} pluginName - The name of the commissioning server.
1983
+ * @returns {CommissioningServer} The created commissioning server.
1989
1984
  */
1990
- async createCommissioningServerContext(pluginName, deviceName, deviceType, vendorId, vendorName, productId, productName) {
1991
- if (!this.storageManager) {
1992
- this.log.error('No storage manager initialized');
1993
- process.exit(1);
1994
- }
1985
+ async createCommisioningServer(context, pluginName) {
1986
+ this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db}`);
1987
+ const deviceName = await context.get('deviceName');
1988
+ const deviceType = await context.get('deviceType');
1989
+ const vendorId = await context.get('vendorId');
1990
+ const vendorName = await context.get('vendorName'); // Home app = Manufacturer
1991
+ const productId = await context.get('productId');
1992
+ const productName = await context.get('productName'); // Home app = Model
1993
+ const serialNumber = await context.get('serialNumber');
1994
+ const uniqueId = await context.get('uniqueId');
1995
+ const softwareVersion = await context.get('softwareVersion', 1);
1996
+ const softwareVersionString = await context.get('softwareVersionString', '1.0.0'); // Home app = Firmware Revision
1997
+ const hardwareVersion = await context.get('hardwareVersion', 1);
1998
+ const hardwareVersionString = await context.get('hardwareVersionString', '1.0.0');
1999
+ this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db} with deviceName '${deviceName}' deviceType ${deviceType}(0x${deviceType.toString(16).padStart(4, '0')})`);
2000
+ this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db} with uniqueId ${uniqueId} serialNumber ${serialNumber}`);
2001
+ this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db} with softwareVersion ${softwareVersion} softwareVersionString ${softwareVersionString}`);
2002
+ this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db} with hardwareVersion ${hardwareVersion} hardwareVersionString ${hardwareVersionString}`);
2003
+ this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db} with nodeLabel '${productName}' port ${this.port} passcode ${this.passcode} discriminator ${this.discriminator}`);
2004
+ const commissioningServer = new CommissioningServer({
2005
+ port: this.port++,
2006
+ // listeningAddressIpv4
2007
+ // listeningAddressIpv6
2008
+ passcode: this.passcode,
2009
+ discriminator: this.discriminator,
2010
+ deviceName,
2011
+ deviceType,
2012
+ basicInformation: {
2013
+ vendorId: VendorId(vendorId),
2014
+ vendorName,
2015
+ productId,
2016
+ productName,
2017
+ nodeLabel: productName,
2018
+ productLabel: productName,
2019
+ softwareVersion,
2020
+ softwareVersionString, // Home app = Firmware Revision
2021
+ hardwareVersion,
2022
+ hardwareVersionString,
2023
+ uniqueId,
2024
+ serialNumber,
2025
+ reachable: true,
2026
+ },
2027
+ activeSessionsChangedCallback: (fabricIndex) => {
2028
+ const sessionInformations = commissioningServer.getActiveSessionInformation(fabricIndex);
2029
+ let connected = false;
2030
+ 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));
2032
+ 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}`);
2034
+ connected = true;
2035
+ }
2036
+ });
2037
+ if (connected) {
2038
+ if (this.bridgeMode === 'bridge') {
2039
+ this.matterbridgePaired = true;
2040
+ this.matterbridgeConnected = true;
2041
+ this.matterbridgeSessionInformations = this.sanitizeSessionInformation(sessionInformations);
2042
+ }
2043
+ if (this.bridgeMode === 'childbridge') {
2044
+ const plugin = this.plugins.get(pluginName);
2045
+ if (plugin) {
2046
+ plugin.paired = true;
2047
+ plugin.connected = true;
2048
+ plugin.sessionInformations = this.sanitizeSessionInformation(sessionInformations);
2049
+ }
2050
+ }
2051
+ /*
2052
+ setTimeout(() => {
2053
+ // We just need to configure the plugins after the controllers are connected
2054
+ if (this.bridgeMode === 'bridge') {
2055
+ for (const plugin of this.plugins) {
2056
+ if (!plugin.enabled || !plugin.loaded || !plugin.started || plugin.error) continue;
2057
+ try {
2058
+ this.configurePlugin(plugin); // No await do it asyncronously
2059
+ } catch (error) {
2060
+ plugin.error = true;
2061
+ this.log.error(`Error configuring plugin ${plg}${plugin.name}${er}`, error);
2062
+ }
2063
+ }
2064
+ }
2065
+ if (this.bridgeMode === 'childbridge') {
2066
+ for (const plugin of this.plugins) {
2067
+ if (plugin.name === pluginName && plugin.loaded === true && plugin.started === true && plugin.configured !== true) {
2068
+ try {
2069
+ this.configurePlugin(plugin); // No await do it asyncronously
2070
+ } catch (error) {
2071
+ plugin.error = true;
2072
+ this.log.error(`Error configuring plugin ${plg}${plugin.name}${er}`, error);
2073
+ }
2074
+ }
2075
+ }
2076
+ }
2077
+ // logEndpoint(commissioningServer.getRootEndpoint());
2078
+ }, 2000);
2079
+ */
2080
+ }
2081
+ },
2082
+ commissioningChangedCallback: async (fabricIndex) => {
2083
+ const fabricInfo = commissioningServer.getCommissionedFabricInformation(fabricIndex);
2084
+ this.log.debug(`*Commissioning changed on fabric ${zb}${fabricIndex}${nf} for ${plg}${pluginName}${nf}`, debugStringify(fabricInfo));
2085
+ if (commissioningServer.getCommissionedFabricInformation().length === 0) {
2086
+ this.log.warn(`*Commissioning removed from fabric ${zb}${fabricIndex}${nf} for ${plg}${pluginName}${wr}. Resetting the commissioning server ...`);
2087
+ await commissioningServer.factoryReset();
2088
+ if (pluginName === 'Matterbridge') {
2089
+ await this.matterbridgeContext?.clearAll();
2090
+ this.matterbridgeFabricInformations = [];
2091
+ this.matterbridgeSessionInformations = [];
2092
+ this.matterbridgePaired = false;
2093
+ this.matterbridgeConnected = false;
2094
+ }
2095
+ else {
2096
+ for (const plugin of this.plugins) {
2097
+ if (plugin.name === pluginName) {
2098
+ await plugin.platform?.onShutdown('Commissioning removed by the controller');
2099
+ plugin.fabricInformations = [];
2100
+ plugin.sessionInformations = [];
2101
+ plugin.paired = false;
2102
+ plugin.connected = false;
2103
+ await plugin.storageContext?.clearAll();
2104
+ }
2105
+ }
2106
+ }
2107
+ this.log.warn(`*Restart to activate the pairing for ${plg}${pluginName}${wr}.`);
2108
+ }
2109
+ else {
2110
+ const fabricInfo = commissioningServer.getCommissionedFabricInformation();
2111
+ if (pluginName === 'Matterbridge') {
2112
+ this.matterbridgeFabricInformations = this.sanitizeFabricInformations(fabricInfo);
2113
+ this.matterbridgePaired = true;
2114
+ }
2115
+ else {
2116
+ const plugin = this.plugins.get(pluginName);
2117
+ if (plugin) {
2118
+ plugin.fabricInformations = this.sanitizeFabricInformations(fabricInfo);
2119
+ plugin.paired = true;
2120
+ }
2121
+ }
2122
+ }
2123
+ },
2124
+ });
2125
+ if (this.passcode !== undefined)
2126
+ this.passcode++;
2127
+ if (this.discriminator !== undefined)
2128
+ this.discriminator++;
2129
+ commissioningServer.addCommandHandler('testEventTrigger', async ({ request: { enableKey, eventTrigger } }) => this.log.info(`testEventTrigger called on GeneralDiagnostic cluster: ${enableKey} ${eventTrigger}`));
2130
+ return commissioningServer;
2131
+ }
2132
+ /**
2133
+ * Creates a commissioning server storage context.
2134
+ *
2135
+ * @param pluginName - The name of the plugin.
2136
+ * @param deviceName - The name of the device.
2137
+ * @param deviceType - The type of the device.
2138
+ * @param vendorId - The vendor ID.
2139
+ * @param vendorName - The vendor name.
2140
+ * @param productId - The product ID.
2141
+ * @param productName - The product name.
2142
+ * @param serialNumber - The serial number of the device (optional).
2143
+ * @param uniqueId - The unique ID of the device (optional).
2144
+ * @param softwareVersion - The software version of the device (optional).
2145
+ * @param softwareVersionString - The software version string of the device (optional).
2146
+ * @param hardwareVersion - The hardware version of the device (optional).
2147
+ * @param hardwareVersionString - The hardware version string of the device (optional).
2148
+ * @returns The storage context for the commissioning server.
2149
+ */
2150
+ async createCommissioningServerContext(pluginName, deviceName, deviceType, vendorId, vendorName, productId, productName) {
2151
+ if (!this.storageManager)
2152
+ throw new Error('No storage manager initialized');
1995
2153
  this.log.debug(`Creating commissioning server storage context for ${plg}${pluginName}${db}`);
1996
2154
  const random = 'CS' + CryptoNode.getRandomData(8).toHex();
1997
2155
  const storageContext = this.storageManager.createContext(pluginName);
@@ -2017,11 +2175,52 @@ export class Matterbridge extends EventEmitter {
2017
2175
  return storageContext;
2018
2176
  }
2019
2177
  /**
2020
- * Shows the commissioning QR code for a given plugin.
2178
+ * Imports the commissioning server context for a specific plugin and device.
2179
+ * @param pluginName - The name of the plugin.
2180
+ * @param device - The MatterbridgeDevice object representing the device.
2181
+ * @returns The commissioning server context.
2182
+ * @throws Error if the BasicInformationCluster is not found.
2183
+ */
2184
+ async importCommissioningServerContext(pluginName, device) {
2185
+ this.log.debug(`Importing matter commissioning server storage context from device for ${plg}${pluginName}${db}`);
2186
+ const basic = device.getClusterServer(BasicInformationCluster);
2187
+ if (!basic) {
2188
+ this.log.error('importCommissioningServerContext error: cannot find the BasicInformationCluster');
2189
+ process.exit(1);
2190
+ }
2191
+ if (!this.storageManager) {
2192
+ this.log.error('importCommissioningServerContext error: no storage manager initialized');
2193
+ process.exit(1);
2194
+ }
2195
+ this.log.debug(`Importing commissioning server storage context for ${plg}${pluginName}${db}`);
2196
+ const storageContext = this.storageManager.createContext(pluginName);
2197
+ await storageContext.set('deviceName', basic.getNodeLabelAttribute());
2198
+ await storageContext.set('deviceType', DeviceTypeId(device.deviceType));
2199
+ await storageContext.set('vendorId', basic.getVendorIdAttribute());
2200
+ await storageContext.set('vendorName', basic.getVendorNameAttribute());
2201
+ await storageContext.set('productId', basic.getProductIdAttribute());
2202
+ await storageContext.set('productName', basic.getProductNameAttribute());
2203
+ await storageContext.set('nodeLabel', basic.getNodeLabelAttribute());
2204
+ await storageContext.set('productLabel', basic.getNodeLabelAttribute());
2205
+ await storageContext.set('serialNumber', basic.attributes.serialNumber?.getLocal());
2206
+ await storageContext.set('uniqueId', basic.attributes.uniqueId?.getLocal());
2207
+ await storageContext.set('softwareVersion', basic.getSoftwareVersionAttribute());
2208
+ await storageContext.set('softwareVersionString', basic.getSoftwareVersionStringAttribute());
2209
+ await storageContext.set('hardwareVersion', basic.getHardwareVersionAttribute());
2210
+ await storageContext.set('hardwareVersionString', basic.getHardwareVersionStringAttribute());
2211
+ this.log.debug(`Imported commissioning server storage context for ${plg}${pluginName}${db}`);
2212
+ this.log.debug(`- deviceName: ${await storageContext.get('deviceName')} deviceType: ${await storageContext.get('deviceType')}(0x${(await storageContext.get('deviceType'))?.toString(16).padStart(4, '0')})`);
2213
+ this.log.debug(`- serialNumber: ${await storageContext.get('serialNumber')} uniqueId: ${await storageContext.get('uniqueId')}`);
2214
+ this.log.debug(`- softwareVersion: ${await storageContext.get('softwareVersion')} softwareVersionString: ${await storageContext.get('softwareVersionString')}`);
2215
+ this.log.debug(`- hardwareVersion: ${await storageContext.get('hardwareVersion')} hardwareVersionString: ${await storageContext.get('hardwareVersionString')}`);
2216
+ return storageContext;
2217
+ }
2218
+ /**
2219
+ * Shows the commissioning server QR code for a given plugin.
2021
2220
  * @param {CommissioningServer} commissioningServer - The commissioning server instance.
2022
2221
  * @param {StorageContext} storageContext - The storage context instance.
2023
2222
  * @param {NodeStorage} nodeContext - The node storage instance.
2024
- * @param {string} pluginName - The name of the plugin.
2223
+ * @param {string} pluginName - The name of the plugin of Matterbridge in bridge mode.
2025
2224
  * @returns {Promise<void>} - A promise that resolves when the QR code is shown.
2026
2225
  */
2027
2226
  async showCommissioningQRCode(commissioningServer, storageContext, nodeContext, pluginName) {
@@ -2037,8 +2236,10 @@ export class Matterbridge extends EventEmitter {
2037
2236
  await nodeContext.set('qrPairingCode', qrPairingCode);
2038
2237
  await nodeContext.set('manualPairingCode', manualPairingCode);
2039
2238
  const QrCode = new QrCodeSchema();
2040
- this.log.info(`***The commissioning server on port ${commissioningServer.getPort()} for ${plg}${pluginName}${nf} is not commissioned. Pair it scanning the QR code:\n\n` +
2041
- `${QrCode.encode(qrPairingCode)}\n${plg}${pluginName}${nf}\n\nqrPairingCode: ${qrPairingCode}\n\nManual pairing code: ${manualPairingCode}\n`);
2239
+ this.log.info(`*The commissioning server on port ${commissioningServer.getPort()} for ${plg}${pluginName}${nf} is not commissioned. Pair it scanning the QR code:\n\n`);
2240
+ // eslint-disable-next-line no-console
2241
+ console.log(`${QrCode.encode(qrPairingCode)}\n`);
2242
+ this.log.info(`${plg}${pluginName}${nf}\n\nqrPairingCode: ${qrPairingCode}\n\nManual pairing code: ${manualPairingCode}\n`);
2042
2243
  if (pluginName === 'Matterbridge') {
2043
2244
  this.matterbridgeFabricInformations = [];
2044
2245
  this.matterbridgeSessionInformations = [];
@@ -2046,7 +2247,7 @@ export class Matterbridge extends EventEmitter {
2046
2247
  this.matterbridgeConnected = false;
2047
2248
  }
2048
2249
  if (pluginName !== 'Matterbridge') {
2049
- const plugin = this.findPlugin(pluginName);
2250
+ const plugin = this.plugins.get(pluginName);
2050
2251
  if (plugin) {
2051
2252
  plugin.qrPairingCode = qrPairingCode;
2052
2253
  plugin.manualPairingCode = manualPairingCode;
@@ -2056,7 +2257,6 @@ export class Matterbridge extends EventEmitter {
2056
2257
  plugin.connected = false;
2057
2258
  }
2058
2259
  }
2059
- await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
2060
2260
  }
2061
2261
  else {
2062
2262
  this.log.info(`*The commissioning server on port ${commissioningServer.getPort()} for ${plg}${pluginName}${nf} is already commissioned. Waiting for controllers to connect ...`);
@@ -2072,14 +2272,13 @@ export class Matterbridge extends EventEmitter {
2072
2272
  this.matterbridgePaired = true;
2073
2273
  }
2074
2274
  if (pluginName !== 'Matterbridge') {
2075
- const plugin = this.findPlugin(pluginName);
2275
+ const plugin = this.plugins.get(pluginName);
2076
2276
  if (plugin) {
2077
2277
  plugin.fabricInformations = this.sanitizeFabricInformations(fabricInfo);
2078
2278
  plugin.sessionInformations = [];
2079
2279
  plugin.paired = true;
2080
2280
  }
2081
2281
  }
2082
- await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
2083
2282
  }
2084
2283
  }
2085
2284
  /**
@@ -2126,26 +2325,12 @@ export class Matterbridge extends EventEmitter {
2126
2325
  : undefined,
2127
2326
  isPeerActive: info.isPeerActive,
2128
2327
  secure: info.secure,
2129
- lastInteractionTimestamp: info.lastInteractionTimestamp,
2130
- lastActiveTimestamp: info.lastActiveTimestamp,
2328
+ lastInteractionTimestamp: info.lastInteractionTimestamp?.toString(),
2329
+ lastActiveTimestamp: info.lastActiveTimestamp?.toString(),
2131
2330
  numberOfActiveSubscriptions: info.numberOfActiveSubscriptions,
2132
2331
  };
2133
2332
  });
2134
2333
  }
2135
- /**
2136
- * Finds a plugin by its name.
2137
- *
2138
- * @param pluginName - The name of the plugin to find.
2139
- * @returns The found plugin, or undefined if not found.
2140
- */
2141
- findPlugin(pluginName) {
2142
- const plugin = this.registeredPlugins.find((registeredPlugin) => registeredPlugin.name === pluginName);
2143
- if (!plugin) {
2144
- this.log.error(`Plugin ${plg}${pluginName}${er} not found`);
2145
- return;
2146
- }
2147
- return plugin;
2148
- }
2149
2334
  /**
2150
2335
  * Sets the reachability of a commissioning server and trigger.
2151
2336
  *
@@ -2154,562 +2339,87 @@ export class Matterbridge extends EventEmitter {
2154
2339
  */
2155
2340
  setCommissioningServerReachability(commissioningServer, reachable) {
2156
2341
  const basicInformationCluster = commissioningServer?.getRootClusterServer(BasicInformationCluster);
2157
- if (basicInformationCluster && basicInformationCluster.attributes.reachable !== undefined)
2158
- basicInformationCluster.setReachableAttribute(reachable);
2159
- if (basicInformationCluster && basicInformationCluster.triggerReachableChangedEvent)
2160
- basicInformationCluster.triggerReachableChangedEvent({ reachableNewValue: reachable });
2161
- }
2162
- /**
2163
- * Sets the reachability of the specified matter aggregator and its bridged devices and trigger.
2164
- * @param {Aggregator} matterAggregator - The matter aggregator to set the reachability for.
2165
- * @param {boolean} reachable - A boolean indicating the reachability status to set.
2166
- */
2167
- setAggregatorReachability(matterAggregator, reachable) {
2168
- const basicInformationCluster = matterAggregator.getClusterServer(BasicInformationCluster);
2169
- if (basicInformationCluster && basicInformationCluster.attributes.reachable !== undefined)
2170
- basicInformationCluster.setReachableAttribute(reachable);
2171
- if (basicInformationCluster && basicInformationCluster.triggerReachableChangedEvent)
2172
- basicInformationCluster.triggerReachableChangedEvent({ reachableNewValue: reachable });
2173
- matterAggregator.getBridgedDevices().forEach((device) => {
2174
- this.log.debug(`Setting reachability to true for bridged device: ${dev}${device.name}${nf}`);
2175
- device.getClusterServer(BridgedDeviceBasicInformationCluster)?.setReachableAttribute(reachable);
2176
- device.getClusterServer(BridgedDeviceBasicInformationCluster)?.triggerReachableChangedEvent({ reachableNewValue: reachable });
2177
- });
2178
- }
2179
- /**
2180
- * Sets the reachability of a device and trigger.
2181
- *
2182
- * @param {MatterbridgeDevice} device - The device to set the reachability for.
2183
- * @param {boolean} reachable - The new reachability status of the device.
2184
- */
2185
- setDeviceReachability(device, reachable) {
2186
- const basicInformationCluster = device.getClusterServer(BasicInformationCluster);
2187
- if (basicInformationCluster && basicInformationCluster.attributes.reachable !== undefined)
2188
- basicInformationCluster.setReachableAttribute(reachable);
2189
- if (basicInformationCluster && basicInformationCluster.triggerReachableChangedEvent)
2190
- basicInformationCluster.triggerReachableChangedEvent({ reachableNewValue: reachable });
2191
- }
2192
- getVendorIdName = (vendorId) => {
2193
- if (!vendorId)
2194
- return '';
2195
- let vendorName = '';
2196
- switch (vendorId) {
2197
- case 4937:
2198
- vendorName = '(AppleHome)';
2199
- break;
2200
- case 4996:
2201
- vendorName = '(AppleKeyChain)';
2202
- break;
2203
- case 4362:
2204
- vendorName = '(SmartThings)';
2205
- break;
2206
- case 4939:
2207
- vendorName = '(HomeAssistant)';
2208
- break;
2209
- case 24582:
2210
- vendorName = '(GoogleHome)';
2211
- break;
2212
- case 4631:
2213
- vendorName = '(Alexa)';
2214
- break;
2215
- case 4701:
2216
- vendorName = '(Tuya)';
2217
- break;
2218
- case 4742:
2219
- vendorName = '(eWeLink)';
2220
- break;
2221
- case 65521:
2222
- vendorName = '(PythonMatterServer)';
2223
- break;
2224
- default:
2225
- vendorName = '(unknown)';
2226
- break;
2227
- }
2228
- return vendorName;
2229
- };
2230
- /**
2231
- * Creates a matter commissioning server.
2232
- *
2233
- * @param {StorageContext} context - The storage context.
2234
- * @param {string} pluginName - The name of the commissioning server.
2235
- * @returns {CommissioningServer} The created commissioning server.
2236
- */
2237
- async createCommisioningServer(context, pluginName) {
2238
- this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db}`);
2239
- const deviceName = await context.get('deviceName');
2240
- const deviceType = await context.get('deviceType');
2241
- const vendorId = await context.get('vendorId');
2242
- const vendorName = await context.get('vendorName'); // Home app = Manufacturer
2243
- const productId = await context.get('productId');
2244
- const productName = await context.get('productName'); // Home app = Model
2245
- const serialNumber = await context.get('serialNumber');
2246
- const uniqueId = await context.get('uniqueId');
2247
- const softwareVersion = await context.get('softwareVersion', 1);
2248
- const softwareVersionString = await context.get('softwareVersionString', '1.0.0'); // Home app = Firmware Revision
2249
- const hardwareVersion = await context.get('hardwareVersion', 1);
2250
- const hardwareVersionString = await context.get('hardwareVersionString', '1.0.0');
2251
- this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db} with deviceName '${deviceName}' deviceType ${deviceType}(0x${deviceType.toString(16).padStart(4, '0')})`);
2252
- this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db} with uniqueId ${uniqueId} serialNumber ${serialNumber}`);
2253
- this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db} with softwareVersion ${softwareVersion} softwareVersionString ${softwareVersionString}`);
2254
- this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db} with hardwareVersion ${hardwareVersion} hardwareVersionString ${hardwareVersionString}`);
2255
- this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db} with nodeLabel '${productName}' port ${this.port} passcode ${this.passcode} discriminator ${this.discriminator}`);
2256
- const commissioningServer = new CommissioningServer({
2257
- port: this.port++,
2258
- // listeningAddressIpv4
2259
- // listeningAddressIpv6
2260
- passcode: this.passcode,
2261
- discriminator: this.discriminator,
2262
- deviceName,
2263
- deviceType,
2264
- basicInformation: {
2265
- vendorId: VendorId(vendorId),
2266
- vendorName,
2267
- productId,
2268
- productName,
2269
- nodeLabel: productName,
2270
- productLabel: productName,
2271
- softwareVersion,
2272
- softwareVersionString, // Home app = Firmware Revision
2273
- hardwareVersion,
2274
- hardwareVersionString,
2275
- uniqueId,
2276
- serialNumber,
2277
- reachable: true,
2278
- },
2279
- activeSessionsChangedCallback: (fabricIndex) => {
2280
- const sessionInformations = commissioningServer.getActiveSessionInformation(fabricIndex);
2281
- let connected = false;
2282
- sessionInformations.forEach((session) => {
2283
- 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));
2284
- if (session.isPeerActive === true && session.secure === true && session.numberOfActiveSubscriptions >= 1) {
2285
- 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}`);
2286
- connected = true;
2287
- }
2288
- });
2289
- if (connected) {
2290
- if (this.bridgeMode === 'bridge') {
2291
- this.matterbridgePaired = true;
2292
- this.matterbridgeConnected = true;
2293
- this.matterbridgeSessionInformations = this.sanitizeSessionInformation(sessionInformations);
2294
- }
2295
- if (this.bridgeMode === 'childbridge') {
2296
- const plugin = this.findPlugin(pluginName);
2297
- if (plugin) {
2298
- plugin.paired = true;
2299
- plugin.connected = true;
2300
- plugin.sessionInformations = this.sanitizeSessionInformation(sessionInformations);
2301
- }
2302
- }
2303
- setTimeout(() => {
2304
- // We just need to configure the plugins after the controllers are connected
2305
- if (this.bridgeMode === 'bridge') {
2306
- for (const plugin of this.registeredPlugins) {
2307
- if (!plugin.enabled || !plugin.loaded || !plugin.started || plugin.error)
2308
- continue;
2309
- try {
2310
- this.configurePlugin(plugin); // No await do it asyncronously
2311
- }
2312
- catch (error) {
2313
- plugin.error = true;
2314
- this.log.error(`Error configuring plugin ${plg}${plugin.name}${er}`, error);
2315
- }
2316
- }
2317
- }
2318
- if (this.bridgeMode === 'childbridge') {
2319
- for (const plugin of this.registeredPlugins) {
2320
- if (plugin.name === pluginName && plugin.loaded === true && plugin.started === true && plugin.configured !== true) {
2321
- try {
2322
- this.configurePlugin(plugin); // No await do it asyncronously
2323
- }
2324
- catch (error) {
2325
- plugin.error = true;
2326
- this.log.error(`Error configuring plugin ${plg}${plugin.name}${er}`, error);
2327
- }
2328
- }
2329
- }
2330
- }
2331
- // logEndpoint(commissioningServer.getRootEndpoint());
2332
- }, 2000);
2333
- }
2334
- },
2335
- commissioningChangedCallback: async (fabricIndex) => {
2336
- const fabricInfo = commissioningServer.getCommissionedFabricInformation(fabricIndex);
2337
- this.log.debug(`*Commissioning changed on fabric ${zb}${fabricIndex}${nf} for ${plg}${pluginName}${nf}`, debugStringify(fabricInfo));
2338
- if (commissioningServer.getCommissionedFabricInformation().length === 0) {
2339
- this.log.warn(`*Commissioning removed from fabric ${zb}${fabricIndex}${nf} for ${plg}${pluginName}${wr}. Resetting the commissioning server ...`);
2340
- await commissioningServer.factoryReset();
2341
- if (pluginName === 'Matterbridge') {
2342
- await this.matterbridgeContext?.clearAll();
2343
- this.matterbridgeFabricInformations = [];
2344
- this.matterbridgeSessionInformations = [];
2345
- this.matterbridgePaired = false;
2346
- this.matterbridgeConnected = false;
2347
- }
2348
- else {
2349
- for (const plugin of this.registeredPlugins) {
2350
- if (plugin.name === pluginName) {
2351
- await plugin.platform?.onShutdown('Commissioning removed by the controller');
2352
- plugin.fabricInformations = [];
2353
- plugin.sessionInformations = [];
2354
- plugin.paired = false;
2355
- plugin.connected = false;
2356
- await plugin.storageContext?.clearAll();
2357
- }
2358
- }
2359
- }
2360
- this.log.warn(`*Restart to activate the pairing for ${plg}${pluginName}${wr}.`);
2361
- }
2362
- else {
2363
- const fabricInfo = commissioningServer.getCommissionedFabricInformation();
2364
- if (pluginName === 'Matterbridge') {
2365
- this.matterbridgeFabricInformations = this.sanitizeFabricInformations(fabricInfo);
2366
- this.matterbridgePaired = true;
2367
- }
2368
- else {
2369
- const plugin = this.findPlugin(pluginName);
2370
- if (plugin) {
2371
- plugin.fabricInformations = this.sanitizeFabricInformations(fabricInfo);
2372
- plugin.paired = true;
2373
- }
2374
- }
2375
- }
2376
- },
2377
- });
2378
- if (this.passcode !== undefined)
2379
- this.passcode++;
2380
- if (this.discriminator !== undefined)
2381
- this.discriminator++;
2382
- commissioningServer.addCommandHandler('testEventTrigger', async ({ request: { enableKey, eventTrigger } }) => this.log.info(`testEventTrigger called on GeneralDiagnostic cluster: ${enableKey} ${eventTrigger}`));
2383
- return commissioningServer;
2384
- }
2385
- /**
2386
- * Creates a Matter server using the provided storage manager and the provided mdnsInterface.
2387
- * @param storageManager The storage manager to be used by the Matter server.
2388
- *
2389
- */
2390
- createMatterServer(storageManager) {
2391
- this.log.debug('Creating matter server');
2392
- // Validate mdnsInterface
2393
- if (this.mdnsInterface) {
2394
- const networkInterfaces = os.networkInterfaces();
2395
- const availableInterfaces = Object.keys(networkInterfaces);
2396
- if (!availableInterfaces.includes(this.mdnsInterface)) {
2397
- this.log.error(`Invalid mdnsInterface: ${this.mdnsInterface}. Available interfaces are: ${availableInterfaces.join(', ')}. Using all available interfaces.`);
2398
- this.mdnsInterface = undefined;
2399
- }
2400
- else {
2401
- this.log.info(`Using mdnsInterface '${this.mdnsInterface}' for the Matter server MdnsBroadcaster.`);
2402
- }
2403
- }
2404
- const matterServer = new MatterServer(storageManager, { mdnsInterface: this.mdnsInterface });
2405
- this.log.debug('Created matter server');
2406
- return matterServer;
2407
- }
2408
- /**
2409
- * Creates a Matter Aggregator.
2410
- * @param {StorageContext} context - The storage context.
2411
- * @returns {Aggregator} - The created Matter Aggregator.
2412
- */
2413
- async createMatterAggregator(context, pluginName) {
2414
- const random = 'AG' + CryptoNode.getRandomData(8).toHex();
2415
- await context.set('aggregatorSerialNumber', await context.get('aggregatorSerialNumber', random));
2416
- await context.set('aggregatorUniqueId', await context.get('aggregatorUniqueId', random));
2417
- this.log.debug(`Creating matter aggregator for plugin ${plg}${pluginName}${db} with uniqueId ${await context.get('aggregatorUniqueId')} serialNumber ${await context.get('aggregatorSerialNumber')}`);
2418
- this.log.debug(`Creating matter aggregator for plugin ${plg}${pluginName}${db} with softwareVersion ${await context.get('softwareVersion', 1)} softwareVersionString ${await context.get('softwareVersionString', '1.0.0')}`);
2419
- this.log.debug(`Creating matter aggregator for plugin ${plg}${pluginName}${db} with hardwareVersion ${await context.get('hardwareVersion', 1)} hardwareVersionString ${await context.get('hardwareVersionString', '1.0.0')}`);
2420
- const matterAggregator = new Aggregator();
2421
- matterAggregator.addClusterServer(ClusterServer(BasicInformationCluster, {
2422
- dataModelRevision: 1,
2423
- location: 'FR',
2424
- vendorId: VendorId(0xfff1),
2425
- vendorName: 'Matterbridge',
2426
- productId: 0x8000,
2427
- productName: 'Matterbridge aggregator',
2428
- productLabel: 'Matterbridge aggregator',
2429
- nodeLabel: 'Matterbridge aggregator',
2430
- serialNumber: await context.get('aggregatorSerialNumber'),
2431
- uniqueId: await context.get('aggregatorUniqueId'),
2432
- softwareVersion: await context.get('softwareVersion', 1),
2433
- softwareVersionString: await context.get('softwareVersionString', '1.0.0'),
2434
- hardwareVersion: await context.get('hardwareVersion', 1),
2435
- hardwareVersionString: await context.get('hardwareVersionString', '1.0.0'),
2436
- reachable: true,
2437
- capabilityMinima: { caseSessionsPerFabric: 3, subscriptionsPerFabric: 3 },
2438
- }, {}, {
2439
- startUp: true,
2440
- shutDown: true,
2441
- leave: true,
2442
- reachableChanged: true,
2443
- }));
2444
- return matterAggregator;
2445
- }
2446
- /**
2447
- * Stops the Matter server and associated controllers.
2448
- */
2449
- async stopMatter() {
2450
- this.log.debug('Stopping matter commissioningServer');
2451
- await this.commissioningServer?.close();
2452
- this.log.debug('Stopping matter commissioningController');
2453
- await this.commissioningController?.close();
2454
- this.log.debug('Stopping matter server');
2455
- await this.matterServer?.close();
2456
- this.log.debug('Matter server closed');
2457
- this.commissioningController = undefined;
2458
- this.commissioningServer = undefined;
2459
- this.matterAggregator = undefined;
2460
- this.matterServer = undefined;
2461
- }
2462
- /**
2463
- * Retrieves the latest version of a package from the npm registry.
2464
- * @param packageName - The name of the package.
2465
- * @returns A Promise that resolves to the latest version of the package.
2466
- */
2467
- async getLatestVersion(packageName) {
2468
- return new Promise((resolve, reject) => {
2469
- exec(`npm view ${packageName} version`, (error, stdout) => {
2470
- if (error) {
2471
- reject(error);
2472
- }
2473
- else {
2474
- resolve(stdout.trim());
2475
- }
2476
- });
2477
- });
2478
- }
2479
- /**
2480
- * Retrieves the path to the global Node.js modules directory.
2481
- * @returns A promise that resolves to the path of the global Node.js modules directory.
2482
- */
2483
- async getGlobalNodeModules() {
2484
- return new Promise((resolve, reject) => {
2485
- exec('npm root -g', (error, stdout) => {
2486
- if (error) {
2487
- reject(error);
2488
- }
2489
- else {
2490
- resolve(stdout.trim());
2491
- }
2492
- });
2493
- });
2494
- }
2495
- /**
2496
- * Logs the node and system information.
2497
- */
2498
- async logNodeAndSystemInfo() {
2499
- // IP address information
2500
- const networkInterfaces = os.networkInterfaces();
2501
- this.systemInformation.ipv4Address = '';
2502
- this.systemInformation.ipv6Address = '';
2503
- for (const [interfaceName, interfaceDetails] of Object.entries(networkInterfaces)) {
2504
- if (!interfaceDetails) {
2505
- break;
2506
- }
2507
- for (const detail of interfaceDetails) {
2508
- if (detail.family === 'IPv4' && !detail.internal && this.systemInformation.ipv4Address === '') {
2509
- this.systemInformation.interfaceName = interfaceName;
2510
- this.systemInformation.ipv4Address = detail.address;
2511
- this.systemInformation.macAddress = detail.mac;
2512
- }
2513
- else if (detail.family === 'IPv6' && !detail.internal && this.systemInformation.ipv6Address === '') {
2514
- this.systemInformation.interfaceName = interfaceName;
2515
- this.systemInformation.ipv6Address = detail.address;
2516
- this.systemInformation.macAddress = detail.mac;
2517
- }
2518
- }
2519
- if (this.systemInformation.ipv4Address !== '' /* && this.systemInformation.ipv6Address !== ''*/) {
2520
- this.log.debug(`Using interface: '${this.systemInformation.interfaceName}'`);
2521
- this.log.debug(`- with MAC address: '${this.systemInformation.macAddress}'`);
2522
- this.log.debug(`- with IPv4 address: '${this.systemInformation.ipv4Address}'`);
2523
- this.log.debug(`- with IPv6 address: '${this.systemInformation.ipv6Address}'`);
2524
- break;
2525
- }
2526
- }
2527
- // Node information
2528
- this.systemInformation.nodeVersion = process.versions.node;
2529
- const versionMajor = parseInt(this.systemInformation.nodeVersion.split('.')[0]);
2530
- const versionMinor = parseInt(this.systemInformation.nodeVersion.split('.')[1]);
2531
- const versionPatch = parseInt(this.systemInformation.nodeVersion.split('.')[2]);
2532
- // Host system information
2533
- this.systemInformation.hostname = os.hostname();
2534
- this.systemInformation.user = os.userInfo().username;
2535
- this.systemInformation.osType = os.type(); // "Windows_NT", "Darwin", etc.
2536
- this.systemInformation.osRelease = os.release(); // Kernel version
2537
- this.systemInformation.osPlatform = os.platform(); // "win32", "linux", "darwin", etc.
2538
- this.systemInformation.osArch = os.arch(); // "x64", "arm", etc.
2539
- this.systemInformation.totalMemory = (os.totalmem() / 1024 / 1024 / 1024).toFixed(2) + ' GB'; // Convert to GB
2540
- this.systemInformation.freeMemory = (os.freemem() / 1024 / 1024 / 1024).toFixed(2) + ' GB'; // Convert to GB
2541
- this.systemInformation.systemUptime = (os.uptime() / 60 / 60).toFixed(2) + ' hours'; // Convert to hours
2542
- // Log the system information
2543
- this.log.debug('Host System Information:');
2544
- this.log.debug(`- Hostname: ${this.systemInformation.hostname}`);
2545
- this.log.debug(`- User: ${this.systemInformation.user}`);
2546
- this.log.debug(`- Interface: ${this.systemInformation.interfaceName}`);
2547
- this.log.debug(`- MAC Address: ${this.systemInformation.macAddress}`);
2548
- this.log.debug(`- IPv4 Address: ${this.systemInformation.ipv4Address}`);
2549
- this.log.debug(`- IPv6 Address: ${this.systemInformation.ipv6Address}`);
2550
- this.log.debug(`- Node.js: ${versionMajor}.${versionMinor}.${versionPatch}`);
2551
- this.log.debug(`- OS Type: ${this.systemInformation.osType}`);
2552
- this.log.debug(`- OS Release: ${this.systemInformation.osRelease}`);
2553
- this.log.debug(`- Platform: ${this.systemInformation.osPlatform}`);
2554
- this.log.debug(`- Architecture: ${this.systemInformation.osArch}`);
2555
- this.log.debug(`- Total Memory: ${this.systemInformation.totalMemory}`);
2556
- this.log.debug(`- Free Memory: ${this.systemInformation.freeMemory}`);
2557
- this.log.debug(`- System Uptime: ${this.systemInformation.systemUptime}`);
2558
- // Home directory
2559
- this.homeDirectory = os.homedir();
2560
- this.matterbridgeInformation.homeDirectory = this.homeDirectory;
2561
- this.log.debug(`Home Directory: ${this.homeDirectory}`);
2562
- // Package root directory
2563
- const currentFileDirectory = path.dirname(fileURLToPath(import.meta.url));
2564
- this.rootDirectory = path.resolve(currentFileDirectory, '../');
2565
- this.matterbridgeInformation.rootDirectory = this.rootDirectory;
2566
- this.log.debug(`Root Directory: ${this.rootDirectory}`);
2567
- // Global node_modules directory
2568
- if (this.nodeContext)
2569
- this.globalModulesDirectory = await this.nodeContext.get('globalModulesDirectory', '');
2570
- // First run of Matterbridge so the node storage is empty
2571
- if (this.globalModulesDirectory === '') {
2572
- try {
2573
- this.globalModulesDirectory = await this.getGlobalNodeModules();
2574
- this.matterbridgeInformation.globalModulesDirectory = this.globalModulesDirectory;
2575
- this.log.debug(`Global node_modules Directory: ${this.globalModulesDirectory}`);
2576
- await this.nodeContext?.set('globalModulesDirectory', this.globalModulesDirectory);
2577
- }
2578
- catch (error) {
2579
- this.log.error(`Error getting global node_modules directory: ${error}`);
2580
- }
2581
- }
2582
- else {
2583
- this.getGlobalNodeModules()
2584
- .then(async (globalModulesDirectory) => {
2585
- this.globalModulesDirectory = globalModulesDirectory;
2586
- this.matterbridgeInformation.globalModulesDirectory = this.globalModulesDirectory;
2587
- this.log.debug(`Global node_modules Directory: ${this.globalModulesDirectory}`);
2588
- await this.nodeContext?.set('globalModulesDirectory', this.globalModulesDirectory);
2589
- })
2590
- .catch((error) => {
2591
- this.log.error(`Error getting global node_modules directory: ${error}`);
2592
- });
2593
- }
2594
- // Create the data directory .matterbridge in the home directory
2595
- this.matterbridgeDirectory = path.join(this.homeDirectory, '.matterbridge');
2596
- this.matterbridgeInformation.matterbridgeDirectory = this.matterbridgeDirectory;
2597
- try {
2598
- await fs.access(this.matterbridgeDirectory);
2599
- }
2600
- catch (err) {
2601
- if (err instanceof Error) {
2602
- const nodeErr = err;
2603
- if (nodeErr.code === 'ENOENT') {
2604
- try {
2605
- await fs.mkdir(this.matterbridgeDirectory, { recursive: true });
2606
- this.log.info(`Created Matterbridge Directory: ${this.matterbridgeDirectory}`);
2607
- }
2608
- catch (err) {
2609
- this.log.error(`Error creating directory: ${err}`);
2610
- }
2611
- }
2612
- else {
2613
- this.log.error(`Error accessing directory: ${err}`);
2614
- }
2615
- }
2616
- }
2617
- this.log.debug(`Matterbridge Directory: ${this.matterbridgeDirectory}`);
2618
- // Create the plugin directory Matterbridge in the home directory
2619
- this.matterbridgePluginDirectory = path.join(this.homeDirectory, 'Matterbridge');
2620
- this.matterbridgeInformation.matterbridgePluginDirectory = this.matterbridgePluginDirectory;
2621
- try {
2622
- await fs.access(this.matterbridgePluginDirectory);
2623
- }
2624
- catch (err) {
2625
- if (err instanceof Error) {
2626
- const nodeErr = err;
2627
- if (nodeErr.code === 'ENOENT') {
2628
- try {
2629
- await fs.mkdir(this.matterbridgePluginDirectory, { recursive: true });
2630
- this.log.info(`Created Matterbridge Plugin Directory: ${this.matterbridgePluginDirectory}`);
2631
- }
2632
- catch (err) {
2633
- this.log.error(`Error creating directory: ${err}`);
2634
- }
2635
- }
2636
- else {
2637
- this.log.error(`Error accessing directory: ${err}`);
2638
- }
2639
- }
2640
- }
2641
- this.log.debug(`Matterbridge Plugin Directory: ${this.matterbridgePluginDirectory}`);
2642
- // Matterbridge version
2643
- const packageJson = JSON.parse(await fs.readFile(path.join(this.rootDirectory, 'package.json'), 'utf-8'));
2644
- this.matterbridgeVersion = packageJson.version;
2645
- this.matterbridgeInformation.matterbridgeVersion = this.matterbridgeVersion;
2646
- this.log.debug(`Matterbridge Version: ${this.matterbridgeVersion}`);
2647
- // Matterbridge latest version
2648
- if (this.nodeContext)
2649
- this.matterbridgeLatestVersion = await this.nodeContext.get('matterbridgeLatestVersion', '');
2650
- this.log.debug(`Matterbridge Latest Version: ${this.matterbridgeLatestVersion}`);
2651
- this.getMatterbridgeLatestVersion();
2652
- // Current working directory
2653
- const currentDir = process.cwd();
2654
- this.log.debug(`Current Working Directory: ${currentDir}`);
2655
- // Command line arguments (excluding 'node' and the script name)
2656
- const cmdArgs = process.argv.slice(2).join(' ');
2657
- this.log.debug(`Command Line Arguments: ${cmdArgs}`);
2342
+ if (basicInformationCluster && basicInformationCluster.attributes.reachable !== undefined)
2343
+ basicInformationCluster.setReachableAttribute(reachable);
2344
+ if (basicInformationCluster && basicInformationCluster.triggerReachableChangedEvent)
2345
+ basicInformationCluster.triggerReachableChangedEvent({ reachableNewValue: reachable });
2658
2346
  }
2659
2347
  /**
2660
- * Retrieves the latest version of Matterbridge and performs necessary actions based on the version comparison.
2661
- * @private
2662
- * @returns {Promise<void>} A promise that resolves when the latest version is retrieved and actions are performed.
2348
+ * Sets the reachability of the specified matter aggregator and its bridged devices and trigger.
2349
+ * @param {Aggregator} matterAggregator - The matter aggregator to set the reachability for.
2350
+ * @param {boolean} reachable - A boolean indicating the reachability status to set.
2663
2351
  */
2664
- async getMatterbridgeLatestVersion() {
2665
- this.getLatestVersion('matterbridge')
2666
- .then(async (matterbridgeLatestVersion) => {
2667
- this.matterbridgeLatestVersion = matterbridgeLatestVersion;
2668
- this.matterbridgeInformation.matterbridgeLatestVersion = this.matterbridgeLatestVersion;
2669
- this.log.debug(`Matterbridge Latest Version: ${this.matterbridgeLatestVersion}`);
2670
- await this.nodeContext?.set('matterbridgeLatestVersion', this.matterbridgeLatestVersion);
2671
- if (this.matterbridgeVersion !== this.matterbridgeLatestVersion) {
2672
- this.log.warn(`Matterbridge is out of date. Current version: ${this.matterbridgeVersion}, Latest version: ${this.matterbridgeLatestVersion}`);
2673
- }
2674
- })
2675
- .catch((error) => {
2676
- this.log.error(`Error getting Matterbridge latest version: ${error.message}`);
2677
- // error.stack && this.log.debug(error.stack);
2352
+ setAggregatorReachability(matterAggregator, reachable) {
2353
+ const basicInformationCluster = matterAggregator.getClusterServer(BasicInformationCluster);
2354
+ if (basicInformationCluster && basicInformationCluster.attributes.reachable !== undefined)
2355
+ basicInformationCluster.setReachableAttribute(reachable);
2356
+ if (basicInformationCluster && basicInformationCluster.triggerReachableChangedEvent)
2357
+ basicInformationCluster.triggerReachableChangedEvent({ reachableNewValue: reachable });
2358
+ matterAggregator.getBridgedDevices().forEach((device) => {
2359
+ this.log.debug(`Setting reachability to true for bridged device: ${dev}${device.name}${nf}`);
2360
+ device.getClusterServer(BridgedDeviceBasicInformationCluster)?.setReachableAttribute(reachable);
2361
+ device.getClusterServer(BridgedDeviceBasicInformationCluster)?.triggerReachableChangedEvent({ reachableNewValue: reachable });
2678
2362
  });
2679
2363
  }
2680
2364
  /**
2681
- * Retrieves the latest version of a plugin and updates the plugin's latestVersion property.
2682
- * If the plugin's version is different from the latest version, logs a warning message.
2683
- * If the plugin's version is the same as the latest version, logs an info message.
2684
- * If there is an error retrieving the latest version, logs an error message.
2365
+ * Sets the reachability of a device and trigger.
2685
2366
  *
2686
- * @private
2687
- * @param {RegisteredPlugin} plugin - The plugin for which to retrieve the latest version.
2688
- * @returns {Promise<void>} A promise that resolves when the latest version is retrieved and actions are performed.
2367
+ * @param {MatterbridgeDevice} device - The device to set the reachability for.
2368
+ * @param {boolean} reachable - The new reachability status of the device.
2689
2369
  */
2690
- async getPluginLatestVersion(plugin) {
2691
- this.getLatestVersion(plugin.name)
2692
- .then(async (latestVersion) => {
2693
- plugin.latestVersion = latestVersion;
2694
- await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
2695
- if (plugin.version !== latestVersion)
2696
- this.log.warn(`The plugin ${plg}${plugin.name}${wr} is out of date. Current version: ${plugin.version}, Latest version: ${latestVersion}`);
2697
- else
2698
- this.log.info(`The plugin ${plg}${plugin.name}${nf} is up to date. Current version: ${plugin.version}, Latest version: ${latestVersion}`);
2699
- })
2700
- .catch((error) => {
2701
- this.log.error(`Error getting ${plugin.name} latest version: ${error.message}`);
2702
- // error.stack && this.log.debug(error.stack);
2703
- });
2370
+ setDeviceReachability(device, reachable) {
2371
+ const basicInformationCluster = device.getClusterServer(BasicInformationCluster);
2372
+ if (basicInformationCluster && basicInformationCluster.attributes.reachable !== undefined)
2373
+ basicInformationCluster.setReachableAttribute(reachable);
2374
+ if (basicInformationCluster && basicInformationCluster.triggerReachableChangedEvent)
2375
+ basicInformationCluster.triggerReachableChangedEvent({ reachableNewValue: reachable });
2704
2376
  }
2377
+ getVendorIdName = (vendorId) => {
2378
+ if (!vendorId)
2379
+ return '';
2380
+ let vendorName = '';
2381
+ switch (vendorId) {
2382
+ case 4937:
2383
+ vendorName = '(AppleHome)';
2384
+ break;
2385
+ case 4996:
2386
+ vendorName = '(AppleKeyChain)';
2387
+ break;
2388
+ case 4362:
2389
+ vendorName = '(SmartThings)';
2390
+ break;
2391
+ case 4939:
2392
+ vendorName = '(HomeAssistant)';
2393
+ break;
2394
+ case 24582:
2395
+ vendorName = '(GoogleHome)';
2396
+ break;
2397
+ case 4631:
2398
+ vendorName = '(Alexa)';
2399
+ break;
2400
+ case 4701:
2401
+ vendorName = '(Tuya)';
2402
+ break;
2403
+ case 4742:
2404
+ vendorName = '(eWeLink)';
2405
+ break;
2406
+ case 65521:
2407
+ vendorName = '(PythonMatterServer)';
2408
+ break;
2409
+ default:
2410
+ vendorName = '(unknown)';
2411
+ break;
2412
+ }
2413
+ return vendorName;
2414
+ };
2705
2415
  /**
2706
- * Retrieves an array of base registered plugins.
2707
- *
2708
- * @returns {BaseRegisteredPlugin[]} An array of base registered plugins.
2416
+ * Retrieves the base registered plugins sanitized for res.json().
2417
+ * @param {boolean} includeAll - Whether to include all information for each plugin.
2418
+ * @returns {BaseRegisteredPlugin[]} A promise that resolves to an array of BaseRegisteredPlugin objects.
2709
2419
  */
2710
- async getBaseRegisteredPlugins(includeConfigSchema = false) {
2420
+ async getBaseRegisteredPlugins(includeAll = false) {
2711
2421
  const baseRegisteredPlugins = [];
2712
- for (const plugin of this.registeredPlugins) {
2422
+ for (const plugin of this.plugins) {
2713
2423
  baseRegisteredPlugins.push({
2714
2424
  path: plugin.path,
2715
2425
  type: plugin.type,
@@ -2726,14 +2436,14 @@ export class Matterbridge extends EventEmitter {
2726
2436
  configured: plugin.configured,
2727
2437
  paired: plugin.paired,
2728
2438
  connected: plugin.connected,
2729
- fabricInformations: plugin.fabricInformations,
2730
- sessionInformations: plugin.sessionInformations,
2439
+ fabricInformations: includeAll ? plugin.fabricInformations : undefined,
2440
+ sessionInformations: includeAll ? plugin.sessionInformations : undefined,
2731
2441
  registeredDevices: plugin.registeredDevices,
2732
2442
  addedDevices: plugin.addedDevices,
2733
2443
  qrPairingCode: plugin.qrPairingCode,
2734
2444
  manualPairingCode: plugin.manualPairingCode,
2735
- configJson: includeConfigSchema ? plugin.configJson : {},
2736
- schemaJson: includeConfigSchema ? plugin.schemaJson : {},
2445
+ configJson: includeAll ? plugin.configJson : undefined,
2446
+ schemaJson: includeAll ? plugin.schemaJson : undefined,
2737
2447
  });
2738
2448
  }
2739
2449
  return baseRegisteredPlugins;
@@ -2835,9 +2545,10 @@ export class Matterbridge extends EventEmitter {
2835
2545
  /**
2836
2546
  * Initializes the frontend of Matterbridge.
2837
2547
  *
2838
- * @param port The port number to run the frontend server on. Default is 3000.
2548
+ * @param port The port number to run the frontend server on. Default is 8283.
2839
2549
  */
2840
2550
  async initializeFrontend(port = 8283) {
2551
+ let initializeError = false;
2841
2552
  this.log.debug(`Initializing the frontend ${hasParameter('ssl') ? 'https' : 'http'} server on port ${YELLOW}${port}${db}`);
2842
2553
  // Create the express app that serves the frontend
2843
2554
  this.expressApp = express();
@@ -2863,7 +2574,8 @@ export class Matterbridge extends EventEmitter {
2863
2574
  this.log.error(`Port ${port} is already in use`);
2864
2575
  break;
2865
2576
  }
2866
- process.exit(1);
2577
+ initializeError = true;
2578
+ return;
2867
2579
  });
2868
2580
  }
2869
2581
  else {
@@ -2874,8 +2586,8 @@ export class Matterbridge extends EventEmitter {
2874
2586
  this.log.info(`Loaded certificate file ${path.join(this.matterbridgeDirectory, 'certs/cert.pem')}`);
2875
2587
  }
2876
2588
  catch (error) {
2877
- this.log.error(`Error reading certificate file: ${error}`);
2878
- process.exit(1);
2589
+ this.log.error(`Error reading certificate file ${path.join(this.matterbridgeDirectory, 'certs/cert.pem')}: ${error}`);
2590
+ return;
2879
2591
  }
2880
2592
  let key;
2881
2593
  try {
@@ -2883,8 +2595,8 @@ export class Matterbridge extends EventEmitter {
2883
2595
  this.log.info(`Loaded key file ${path.join(this.matterbridgeDirectory, 'certs/key.pem')}`);
2884
2596
  }
2885
2597
  catch (error) {
2886
- this.log.error(`Error reading key file: ${error}`);
2887
- process.exit(1);
2598
+ this.log.error(`Error reading key file ${path.join(this.matterbridgeDirectory, 'certs/key.pem')}: ${error}`);
2599
+ return;
2888
2600
  }
2889
2601
  let ca;
2890
2602
  try {
@@ -2915,9 +2627,12 @@ export class Matterbridge extends EventEmitter {
2915
2627
  this.log.error(`Port ${port} is already in use`);
2916
2628
  break;
2917
2629
  }
2918
- process.exit(1);
2630
+ initializeError = true;
2631
+ return;
2919
2632
  });
2920
2633
  }
2634
+ if (initializeError)
2635
+ return;
2921
2636
  // Createe a WebSocket server and attach it to the http server
2922
2637
  const wssPort = port;
2923
2638
  const wssHost = hasParameter('ssl') ? `wss://${this.systemInformation.ipv4Address}:${wssPort}` : `ws://${this.systemInformation.ipv4Address}:${wssPort}`;
@@ -2926,7 +2641,7 @@ export class Matterbridge extends EventEmitter {
2926
2641
  const clientIp = request.socket.remoteAddress;
2927
2642
  this.log.info(`WebSocketServer client ${clientIp} connected`);
2928
2643
  this.log.setGlobalCallback(this.wssSendMessage.bind(this));
2929
- this.log.debug('WebSocketServer logger callback added');
2644
+ this.log.debug('WebSocketServer logger global callback added');
2930
2645
  this.wssSendMessage('Matterbridge', 'info', `WebSocketServer client ${clientIp} connected to Matterbridge`);
2931
2646
  ws.on('message', (message) => {
2932
2647
  this.log.debug(`WebSocket client message: ${message}`);
@@ -2935,7 +2650,7 @@ export class Matterbridge extends EventEmitter {
2935
2650
  this.log.info('WebSocket client disconnected');
2936
2651
  if (this.webSocketServer?.clients.size === 0) {
2937
2652
  this.log.setGlobalCallback(undefined);
2938
- this.log.debug('All WebSocket clients disconnected. WebSocketServer logger callback removed');
2653
+ this.log.debug('All WebSocket clients disconnected. WebSocketServer logger global callback removed');
2939
2654
  }
2940
2655
  });
2941
2656
  ws.on('error', (error) => {
@@ -3004,6 +2719,12 @@ export class Matterbridge extends EventEmitter {
3004
2719
  this.matterbridgeInformation.matterbridgeSessionInformations = this.matterbridgeSessionInformations;
3005
2720
  const response = { wssHost, qrPairingCode, manualPairingCode, systemInformation: this.systemInformation, matterbridgeInformation: this.matterbridgeInformation };
3006
2721
  // this.log.debug('Response:', debugStringify(response));
2722
+ this.log.debug(`WebSocketServer logger local callback: ${this.log.getCallback() ? 'active' : 'inactive'}`);
2723
+ this.log.debug(`WebSocketServer logger global callback: ${this.log.getGlobalCallback() ? 'active' : 'inactive'}`);
2724
+ if (this.webSocketServer && this.webSocketServer.clients.size > 0 && !this.log.getGlobalCallback()) {
2725
+ this.log.setGlobalCallback(this.wssSendMessage.bind(this));
2726
+ this.log.debug('WebSocketServer logger global callback added');
2727
+ }
3007
2728
  res.json(response);
3008
2729
  });
3009
2730
  // Endpoint to provide plugins
@@ -3131,6 +2852,8 @@ export class Matterbridge extends EventEmitter {
3131
2852
  const password = param.slice(1, -1); // Remove the first and last characters
3132
2853
  this.log.debug('setpassword', param, password);
3133
2854
  await this.nodeContext?.set('password', password);
2855
+ res.json({ message: 'Command received' });
2856
+ return;
3134
2857
  }
3135
2858
  // Handle the command setmbloglevel from Settings
3136
2859
  if (command === 'setmbloglevel') {
@@ -3143,6 +2866,8 @@ export class Matterbridge extends EventEmitter {
3143
2866
  this.log.setLogDebug(false);
3144
2867
  this.debugEnabled = false;
3145
2868
  }
2869
+ res.json({ message: 'Command received' });
2870
+ return;
3146
2871
  }
3147
2872
  // Handle the command setmbloglevel from Settings
3148
2873
  if (command === 'setmjloglevel') {
@@ -3165,184 +2890,122 @@ export class Matterbridge extends EventEmitter {
3165
2890
  else if (param === 'Fatal') {
3166
2891
  Logger.defaultLogLevel = Level.FATAL;
3167
2892
  }
2893
+ res.json({ message: 'Command received' });
2894
+ return;
3168
2895
  }
3169
2896
  // Handle the command unregister from Settings
3170
2897
  if (command === 'unregister') {
3171
2898
  await this.unregisterAndShutdownProcess();
2899
+ res.json({ message: 'Command received' });
2900
+ return;
3172
2901
  }
3173
2902
  // Handle the command reset from Settings
3174
2903
  if (command === 'reset') {
3175
- this.shutdownProcessAndReset(); // No await do it asyncronously
2904
+ await this.shutdownProcessAndReset();
2905
+ res.json({ message: 'Command received' });
2906
+ return;
3176
2907
  }
3177
2908
  // Handle the command factoryreset from Settings
3178
2909
  if (command === 'factoryreset') {
3179
- this.shutdownProcessAndFactoryReset(); // No await do it asyncronously
2910
+ await this.shutdownProcessAndFactoryReset();
2911
+ res.json({ message: 'Command received' });
2912
+ return;
3180
2913
  }
3181
2914
  // Handle the command shutdown from Header
3182
2915
  if (command === 'shutdown') {
3183
- this.shutdownProcess(); // No await do it asyncronously
2916
+ await this.shutdownProcess();
2917
+ res.json({ message: 'Command received' });
2918
+ return;
3184
2919
  }
3185
2920
  // Handle the command restart from Header
3186
2921
  if (command === 'restart') {
3187
- this.restartProcess(); // No await do it asyncronously
2922
+ await this.restartProcess();
2923
+ res.json({ message: 'Command received' });
2924
+ return;
3188
2925
  }
3189
2926
  // Handle the command update from Header
3190
2927
  if (command === 'update') {
3191
2928
  this.log.info('Updating matterbridge...');
3192
2929
  try {
3193
- await this.spawnCommand('npm', ['install', '-g', 'matterbridge', '--loglevel=verbose']);
2930
+ await this.spawnCommand('npm', ['install', '-g', 'matterbridge']);
3194
2931
  this.log.info('Matterbridge has been updated. Full restart required.');
3195
2932
  }
3196
2933
  catch (error) {
3197
2934
  this.log.error('Error updating matterbridge');
3198
- res.json({ message: 'Command received' });
3199
- return;
3200
2935
  }
3201
- this.updateProcess();
2936
+ await this.updateProcess();
2937
+ res.json({ message: 'Command received' });
2938
+ return;
3202
2939
  }
3203
2940
  // Handle the command saveconfig from Home
3204
2941
  if (command === 'saveconfig') {
3205
2942
  param = param.replace(/\*/g, '\\');
3206
2943
  this.log.info(`Saving config for plugin ${plg}${param}${nf}...`);
3207
2944
  // console.log('Req.body:', JSON.stringify(req.body, null, 2));
3208
- const plugins = await this.nodeContext?.get('plugins');
3209
- if (!plugins)
3210
- return;
3211
- const plugin = plugins.find((plugin) => plugin.name === param);
3212
- if (!plugin)
3213
- return;
3214
- this.savePluginConfigFromJson(plugin, req.body);
2945
+ if (!this.plugins.has(param)) {
2946
+ this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
2947
+ }
2948
+ else {
2949
+ const plugin = this.plugins.get(param);
2950
+ if (!plugin)
2951
+ return;
2952
+ this.plugins.saveConfigFromJson(plugin, req.body);
2953
+ }
2954
+ res.json({ message: 'Command received' });
2955
+ return;
3215
2956
  }
3216
2957
  // Handle the command installplugin from Home
3217
2958
  if (command === 'installplugin') {
3218
2959
  param = param.replace(/\*/g, '\\');
3219
2960
  this.log.info(`Installing plugin ${plg}${param}${nf}...`);
3220
2961
  try {
3221
- await this.spawnCommand('npm', ['install', '-g', param, '--loglevel=verbose']);
2962
+ await this.spawnCommand('npm', ['install', '-g', param]);
3222
2963
  this.log.info(`Plugin ${plg}${param}${nf} installed. Full restart required.`);
3223
2964
  }
3224
2965
  catch (error) {
3225
2966
  this.log.error(`Error installing plugin ${plg}${param}${er}`);
3226
- res.json({ message: 'Command received' });
3227
- return;
3228
2967
  }
2968
+ // Also add the plugin to matterbridge
2969
+ // res.json({ message: 'Command received' });
2970
+ // return;
3229
2971
  }
3230
2972
  // Handle the command addplugin from Home
3231
2973
  if (command === 'addplugin' || command === 'installplugin') {
3232
2974
  param = param.replace(/\*/g, '\\');
3233
- if (this.registeredPlugins.find((plugin) => plugin.name === param)) {
3234
- this.log.warn(`Plugin ${plg}${param}${wr} already added to matterbridge`);
3235
- res.json({ message: 'Command received' });
3236
- return;
3237
- }
3238
- const packageJsonPath = await this.resolvePluginName(param);
3239
- if (!packageJsonPath) {
3240
- this.log.error(`Error resolving plugin ${plg}${param}${er}`);
3241
- res.json({ message: 'Command received' });
3242
- return;
3243
- }
3244
- this.log.debug(`Loading plugin from ${plg}${packageJsonPath}${db}`);
3245
- try {
3246
- // Load the package.json of the plugin
3247
- const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
3248
- const plugin = { path: packageJsonPath, type: '', name: packageJson.name, version: packageJson.version, description: packageJson.description, author: packageJson.author, enabled: true };
3249
- if (await this.loadPlugin(plugin)) {
3250
- this.registeredPlugins.push(plugin);
3251
- await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
3252
- this.log.info(`Plugin ${plg}${packageJsonPath}${nf} type ${plugin.type} added to matterbridge. Restart required.`);
3253
- this.startPlugin(plugin, 'The plugin has been added to Matterbridge', true);
3254
- }
3255
- else {
3256
- this.log.error(`Error adding plugin ${plg}${packageJsonPath}${er}`);
2975
+ const plugin = await this.plugins.add(param);
2976
+ if (plugin) {
2977
+ 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);
3257
2982
  }
2983
+ */
3258
2984
  }
3259
- catch (error) {
3260
- this.log.error(`Error adding plugin ${plg}${packageJsonPath}${er}`);
3261
- res.json({ message: 'Command received' });
3262
- return;
3263
- }
2985
+ res.json({ message: 'Command received' });
2986
+ return;
3264
2987
  }
3265
2988
  // Handle the command removeplugin from Home
3266
2989
  if (command === 'removeplugin') {
3267
- const index = this.registeredPlugins.findIndex((registeredPlugin) => registeredPlugin.name === param);
3268
- if (index !== -1) {
3269
- if (this.registeredPlugins[index].platform) {
3270
- await this.registeredPlugins[index].platform?.onShutdown('The plugin has been removed.');
3271
- }
3272
- // Remove all devices from the plugin
3273
- await this.removeAllBridgedDevices(this.registeredPlugins[index].name);
3274
- // Remove the plugin from the registered plugins
3275
- this.registeredPlugins.splice(index, 1);
3276
- await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
3277
- this.log.info(`Plugin ${plg}${param}${nf} removed from matterbridge`);
2990
+ if (!this.plugins.has(param)) {
2991
+ this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
3278
2992
  }
3279
2993
  else {
3280
- this.log.warn(`Plugin ${plg}${param}${wr} not registered in matterbridge`);
2994
+ const plugin = this.plugins.get(param);
2995
+ await this.plugins.shutdown(plugin, 'The plugin has been removed.', true);
2996
+ await this.plugins.remove(param);
3281
2997
  }
2998
+ res.json({ message: 'Command received' });
2999
+ return;
3282
3000
  }
3283
3001
  // Handle the command enableplugin from Home
3284
3002
  if (command === 'enableplugin') {
3285
- const plugins = await this.nodeContext?.get('plugins');
3286
- if (!plugins)
3287
- return;
3288
- const plugin = plugins.find((plugin) => plugin.name === param);
3289
- if (plugin) {
3290
- plugin.enabled = true;
3291
- plugin.locked = undefined;
3292
- plugin.error = undefined;
3293
- plugin.loaded = undefined;
3294
- plugin.started = undefined;
3295
- plugin.configured = undefined;
3296
- plugin.connected = undefined;
3297
- plugin.platform = undefined;
3298
- plugin.registeredDevices = undefined;
3299
- plugin.addedDevices = undefined;
3300
- await this.nodeContext?.set('plugins', plugins);
3301
- this.log.info(`Enabled plugin ${plg}${param}${nf}`);
3302
- }
3303
- if (this.bridgeMode === 'bridge') {
3304
- const pluginToEnable = this.findPlugin(param);
3305
- if (pluginToEnable) {
3306
- pluginToEnable.enabled = true;
3307
- pluginToEnable.platform = await this.loadPlugin(pluginToEnable);
3308
- if (pluginToEnable.platform) {
3309
- await this.startPlugin(pluginToEnable, 'The plugin has been enabled', true);
3310
- }
3311
- else {
3312
- pluginToEnable.enabled = false;
3313
- pluginToEnable.error = true;
3314
- this.log.error(`Error enabling plugin ${plg}${param}${er}`);
3315
- }
3316
- }
3003
+ if (!this.plugins.has(param)) {
3004
+ this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
3317
3005
  }
3318
- }
3319
- // Handle the command disableplugin from Home
3320
- if (command === 'disableplugin') {
3321
- const pluginToDisable = this.findPlugin(param);
3322
- if (pluginToDisable) {
3323
- if (pluginToDisable.platform) {
3324
- await pluginToDisable.platform.onShutdown('The plugin has been disabled.');
3325
- }
3326
- // Remove all devices from the plugin
3327
- this.log.info(`Unregistering devices for plugin ${plg}${pluginToDisable.name}${nf}...`);
3328
- await this.removeAllBridgedDevices(pluginToDisable.name);
3329
- pluginToDisable.enabled = false;
3330
- pluginToDisable.locked = undefined;
3331
- pluginToDisable.error = undefined;
3332
- pluginToDisable.loaded = undefined;
3333
- pluginToDisable.started = undefined;
3334
- pluginToDisable.configured = undefined;
3335
- pluginToDisable.connected = undefined;
3336
- pluginToDisable.platform = undefined;
3337
- pluginToDisable.registeredDevices = undefined;
3338
- pluginToDisable.addedDevices = undefined;
3339
- // Save the plugins state in node storage
3340
- const plugins = await this.nodeContext?.get('plugins');
3341
- if (!plugins)
3342
- return;
3343
- const plugin = plugins.find((plugin) => plugin.name === param);
3344
- if (plugin) {
3345
- plugin.enabled = false;
3006
+ else {
3007
+ const plugin = this.plugins.get(param);
3008
+ if (plugin && !plugin.enabled) {
3346
3009
  plugin.locked = undefined;
3347
3010
  plugin.error = undefined;
3348
3011
  plugin.loaded = undefined;
@@ -3352,12 +3015,34 @@ export class Matterbridge extends EventEmitter {
3352
3015
  plugin.platform = undefined;
3353
3016
  plugin.registeredDevices = undefined;
3354
3017
  plugin.addedDevices = undefined;
3355
- await this.nodeContext?.set('plugins', plugins);
3356
- this.log.info(`Disabled plugin ${plg}${param}${nf}`);
3018
+ await this.plugins.enable(param);
3019
+ 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
+ }
3027
+ }
3028
+ res.json({ message: 'Command received' });
3029
+ return;
3030
+ }
3031
+ // Handle the command disableplugin from Home
3032
+ if (command === 'disableplugin') {
3033
+ if (!this.plugins.has(param)) {
3034
+ this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
3035
+ }
3036
+ else {
3037
+ const plugin = this.plugins.get(param);
3038
+ if (plugin && plugin.enabled) {
3039
+ await this.plugins.shutdown(plugin, 'The plugin has been disabled.', true);
3040
+ await this.plugins.disable(param);
3357
3041
  }
3358
3042
  }
3043
+ res.json({ message: 'Command received' });
3044
+ return;
3359
3045
  }
3360
- res.json({ message: 'Command received' });
3361
3046
  });
3362
3047
  // Fallback for routing (must be the last route)
3363
3048
  this.expressApp.get('*', (req, res) => {
@@ -3425,5 +3110,104 @@ export class Matterbridge extends EventEmitter {
3425
3110
  });
3426
3111
  return attributes;
3427
3112
  }
3113
+ /**
3114
+ * Initializes the Matterbridge instance as extension for zigbee2mqtt.
3115
+ * @deprecated This method is deprecated and will be removed in a future version.
3116
+ *
3117
+ * @returns A Promise that resolves when the initialization is complete.
3118
+ */
3119
+ async startExtension(dataPath, debugEnabled, extensionVersion, port = 5560) {
3120
+ // Set the bridge mode
3121
+ this.bridgeMode = 'bridge';
3122
+ // Set the first port to use
3123
+ this.port = port;
3124
+ // Set Matterbridge logger
3125
+ this.debugEnabled = debugEnabled;
3126
+ this.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logDebug: this.debugEnabled });
3127
+ this.log.debug('Matterbridge extension is starting...');
3128
+ // Initialize NodeStorage
3129
+ this.matterbridgeDirectory = dataPath;
3130
+ this.log.debug('Creating node storage manager dir: ' + path.join(this.matterbridgeDirectory, 'node_storage'));
3131
+ this.nodeStorage = new NodeStorageManager({ dir: path.join(this.matterbridgeDirectory, 'node_storage'), logging: false });
3132
+ this.log.debug('Creating node storage context for matterbridge: matterbridge');
3133
+ this.nodeContext = await this.nodeStorage.createStorage('matterbridge');
3134
+ const plugin = {
3135
+ path: '',
3136
+ type: 'DynamicPlatform',
3137
+ name: 'MatterbridgeExtension',
3138
+ version: '1.0.0',
3139
+ description: 'Matterbridge extension',
3140
+ author: 'https://github.com/Luligu',
3141
+ enabled: false,
3142
+ registeredDevices: 0,
3143
+ addedDevices: 0,
3144
+ };
3145
+ this.plugins.set(plugin);
3146
+ this.plugins.saveToStorage();
3147
+ // Log system info and create .matterbridge directory
3148
+ await this.logNodeAndSystemInfo();
3149
+ this.matterbridgeDirectory = dataPath;
3150
+ // Set matter.js logger level and format
3151
+ Logger.defaultLogLevel = this.debugEnabled ? Level.DEBUG : Level.INFO;
3152
+ Logger.format = Format.ANSI;
3153
+ // Start the storage and create matterbridgeContext
3154
+ await this.startStorage('json', path.join(this.matterbridgeDirectory, this.matterStorageName));
3155
+ if (!this.storageManager)
3156
+ return false;
3157
+ this.matterbridgeContext = await this.createCommissioningServerContext('Matterbridge', 'Matterbridge zigbee2MQTT', DeviceTypes.AGGREGATOR.code, 0xfff1, 'Matterbridge', 0x8000, 'zigbee2MQTT Matter extension');
3158
+ if (!this.matterbridgeContext)
3159
+ return false;
3160
+ await this.matterbridgeContext.set('softwareVersion', 1);
3161
+ await this.matterbridgeContext.set('softwareVersionString', this.matterbridgeVersion);
3162
+ await this.matterbridgeContext.set('hardwareVersion', 1);
3163
+ await this.matterbridgeContext.set('hardwareVersionString', extensionVersion); // Update with the extension version
3164
+ this.matterServer = this.createMatterServer(this.storageManager);
3165
+ this.log.debug(`Creating commissioning server for ${plg}Matterbridge${db}`);
3166
+ this.commissioningServer = await this.createCommisioningServer(this.matterbridgeContext, 'Matterbridge');
3167
+ this.log.debug(`Creating matter aggregator for ${plg}Matterbridge${db}`);
3168
+ this.matterAggregator = await this.createMatterAggregator(this.matterbridgeContext, 'Matterbridge');
3169
+ this.log.debug('Adding matterbridge aggregator to commissioning server');
3170
+ this.commissioningServer.addDevice(this.matterAggregator);
3171
+ this.log.debug('Adding matterbridge commissioning server to matter server');
3172
+ await this.matterServer.addCommissioningServer(this.commissioningServer, { uniqueStorageKey: 'Matterbridge' });
3173
+ await this.startMatterServer();
3174
+ this.log.info('Matter server started');
3175
+ await this.showCommissioningQRCode(this.commissioningServer, this.matterbridgeContext, this.nodeContext, 'Matterbridge');
3176
+ // Set reachability to true and trigger event after 60 seconds
3177
+ setTimeout(() => {
3178
+ this.log.info(`Setting reachability to true for ${plg}Matterbridge${db}`);
3179
+ if (this.commissioningServer)
3180
+ this.setCommissioningServerReachability(this.commissioningServer, true);
3181
+ if (this.matterAggregator)
3182
+ this.setAggregatorReachability(this.matterAggregator, true);
3183
+ }, 60 * 1000);
3184
+ return this.commissioningServer.isCommissioned();
3185
+ }
3186
+ /**
3187
+ * Close the Matterbridge instance as extension for zigbee2mqtt.
3188
+ * @deprecated This method is deprecated and will be removed in a future version.
3189
+ *
3190
+ * @returns A Promise that resolves when the initialization is complete.
3191
+ */
3192
+ async stopExtension() {
3193
+ // Closing matter
3194
+ await this.stopMatterServer();
3195
+ // Clearing the session manager
3196
+ // this.matterbridgeContext?.createContext('SessionManager').clear();
3197
+ // Closing storage
3198
+ await this.stopStorage();
3199
+ this.log.info('Matter server stopped');
3200
+ }
3201
+ /**
3202
+ * Checks if the extension is commissioned.
3203
+ * @deprecated This method is deprecated and will be removed in a future version.
3204
+ *
3205
+ * @returns {boolean} Returns true if the extension is commissioned, false otherwise.
3206
+ */
3207
+ isExtensionCommissioned() {
3208
+ if (!this.commissioningServer)
3209
+ return false;
3210
+ return this.commissioningServer.isCommissioned();
3211
+ }
3428
3212
  }
3429
3213
  //# sourceMappingURL=matterbridge.js.map