matterbridge 1.1.4 → 1.1.5

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.
@@ -20,8 +20,9 @@
20
20
  * See the License for the specific language governing permissions and
21
21
  * limitations under the License. *
22
22
  */
23
+ import { MatterbridgeDevice } from './matterbridgeDevice.js';
23
24
  import { NodeStorageManager } from 'node-persist-manager';
24
- import { AnsiLogger, BRIGHT, GREEN, RESET, UNDERLINE, UNDERLINEOFF, YELLOW, db, debugStringify, stringify, er, nf, rs, wr } from 'node-ansi-logger';
25
+ import { AnsiLogger, BRIGHT, RESET, UNDERLINE, UNDERLINEOFF, YELLOW, db, debugStringify, stringify, er, nf, rs, wr } from 'node-ansi-logger';
25
26
  import { fileURLToPath, pathToFileURL } from 'url';
26
27
  import { promises as fs } from 'fs';
27
28
  import express from 'express';
@@ -38,6 +39,7 @@ import { requireMinNodeVersion, getParameter, getIntParameter, hasParameter } fr
38
39
  import { CryptoNode } from '@project-chip/matter-node.js/crypto';
39
40
  const plg = '\u001B[38;5;33m';
40
41
  const dev = '\u001B[38;5;79m';
42
+ const typ = '\u001B[38;5;207m';
41
43
  /**
42
44
  * Represents the Matterbridge application.
43
45
  */
@@ -55,16 +57,18 @@ export class Matterbridge {
55
57
  freeMemory: '',
56
58
  systemUptime: '',
57
59
  };
58
- homeDirectory;
59
- rootDirectory;
60
- matterbridgeDirectory;
60
+ homeDirectory = '';
61
+ rootDirectory = '';
62
+ matterbridgeDirectory = '';
63
+ matterbridgeVersion = '';
61
64
  bridgeMode = '';
65
+ debugEnabled = false;
62
66
  log;
63
67
  hasCleanupStarted = false;
64
68
  registeredPlugins = [];
65
69
  registeredDevices = [];
66
- nodeStorage = undefined;
67
- nodeContext = undefined;
70
+ nodeStorage;
71
+ nodeContext;
68
72
  app;
69
73
  storageManager;
70
74
  matterbridgeContext;
@@ -79,7 +83,7 @@ export class Matterbridge {
79
83
  }
80
84
  /**
81
85
  * Loads an instance of the Matterbridge class.
82
- * If an instance already exists, an error will be thrown.
86
+ * If an instance already exists, return that instance.
83
87
  * @returns The loaded instance of the Matterbridge class.
84
88
  */
85
89
  static async loadInstance(cli = false) {
@@ -118,6 +122,7 @@ export class Matterbridge {
118
122
  - bridge: start Matterbridge in bridge mode
119
123
  - childbridge: start Matterbridge in childbridge mode
120
124
  - frontend [port]: start the frontend on the given port (default 3000)
125
+ - debug: enable debug mode (default false)
121
126
  - list: list the registered plugins
122
127
  - add [plugin path]: register the plugin
123
128
  - remove [plugin path]: remove the plugin
@@ -126,29 +131,38 @@ export class Matterbridge {
126
131
  process.exit(0);
127
132
  }
128
133
  // set Matterbridge logger
129
- this.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */ });
130
- this.log.info('Matterbridge is running...');
134
+ if (hasParameter('debug'))
135
+ this.debugEnabled = true;
136
+ this.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logDebug: this.debugEnabled });
137
+ this.log.debug('Matterbridge is starting...');
131
138
  // log system info and create .matterbridge directory
132
139
  await this.logNodeAndSystemInfo();
140
+ this.log.info(
141
+ // eslint-disable-next-line max-len
142
+ `Matterbridge version ${this.matterbridgeVersion} mode ${hasParameter('bridge') ? 'bridge' : ''}${hasParameter('childbridge') ? 'childbridge' : ''} running on ${this.systemInformation.osType} ${this.systemInformation.osRelease} ${this.systemInformation.osPlatform} ${this.systemInformation.osArch}`);
133
143
  // check node version and throw error
134
144
  requireMinNodeVersion(18);
135
145
  // register SIGINT SIGTERM signal handlers
136
146
  this.registerSignalHandlers();
137
147
  // set matter.js logger level and format
138
- Logger.defaultLogLevel = Level.DEBUG;
148
+ Logger.defaultLogLevel = this.debugEnabled ? Level.DEBUG : Level.INFO;
139
149
  Logger.format = Format.ANSI;
140
150
  // Initialize NodeStorage
141
151
  this.log.debug('Creating node storage manager');
142
- this.nodeStorage = new NodeStorageManager({ dir: path.join(this.matterbridgeDirectory, 'storage') });
152
+ this.nodeStorage = new NodeStorageManager({ dir: path.join(this.matterbridgeDirectory, 'storage'), logging: false });
143
153
  this.log.debug('Creating node storage context for matterbridge');
144
154
  this.nodeContext = await this.nodeStorage.createStorage('matterbridge');
145
155
  this.registeredPlugins = await this.nodeContext.get('plugins', []);
146
- /*
147
- this.registeredPlugins.forEach(async (plugin) => {
148
- this.log.debug(`Creating node storage context for plugin ${plg}${plugin.name}${db}`);
149
- plugin.nodeContext = await this.nodeStorage?.createStorage(plugin.name);
150
- });
151
- */
156
+ for (const plugin of this.registeredPlugins) {
157
+ this.log.debug(`Creating node storage context for plugin ${plugin.name}`);
158
+ plugin.nodeContext = await this.nodeStorage?.createStorage(plugin.name);
159
+ await plugin.nodeContext?.set('name', plugin.name);
160
+ await plugin.nodeContext?.set('type', plugin.type);
161
+ await plugin.nodeContext?.set('path', plugin.path);
162
+ await plugin.nodeContext?.set('version', plugin.version);
163
+ await plugin.nodeContext?.set('description', plugin.description);
164
+ await plugin.nodeContext?.set('author', plugin.author);
165
+ }
152
166
  // Parse command line
153
167
  this.parseCommandLine();
154
168
  }
@@ -161,30 +175,28 @@ export class Matterbridge {
161
175
  if (hasParameter('list')) {
162
176
  this.log.info('Registered plugins:');
163
177
  this.registeredPlugins.forEach((plugin) => {
164
- this.log.info(`- ${plg}${plugin.name}${nf}: "${plg}${BRIGHT}${plugin.description}${RESET}${nf}" version: ${plugin.version}` +
165
- ` author: "${plugin.author}" type: ${GREEN}${plugin.type}${nf} ${YELLOW}${plugin.enabled ? 'enabled' : 'disabled'}${nf}`);
166
- // loaded: ${plugin.loaded} started: ${plugin.started} paired: ${plugin.paired} connected: ${plugin.connected}
178
+ this.log.info(`- ${plg}${plugin.name}${nf}: "${plg}${BRIGHT}${plugin.description}${RESET}${nf}" type: ${typ}${plugin.type}${nf} ${YELLOW}${plugin.enabled ? 'enabled' : 'disabled'}${nf}`);
167
179
  });
168
180
  process.exit(0);
169
181
  }
170
182
  if (getParameter('add')) {
171
183
  this.log.debug(`Registering plugin ${getParameter('add')}`);
172
- await this.loadPlugin(getParameter('add'), 'add');
184
+ await this.executeCommandLine(getParameter('add'), 'add');
173
185
  process.exit(0);
174
186
  }
175
187
  if (getParameter('remove')) {
176
188
  this.log.debug(`Unregistering plugin ${getParameter('remove')}`);
177
- await this.loadPlugin(getParameter('remove'), 'remove');
189
+ await this.executeCommandLine(getParameter('remove'), 'remove');
178
190
  process.exit(0);
179
191
  }
180
192
  if (getParameter('enable')) {
181
193
  this.log.debug(`Enable plugin ${getParameter('enable')}`);
182
- await this.loadPlugin(getParameter('enable'), 'enable');
194
+ await this.executeCommandLine(getParameter('enable'), 'enable');
183
195
  process.exit(0);
184
196
  }
185
197
  if (getParameter('disable')) {
186
198
  this.log.debug(`Disable plugin ${getParameter('disable')}`);
187
- await this.loadPlugin(getParameter('disable'), 'disable');
199
+ await this.executeCommandLine(getParameter('disable'), 'disable');
188
200
  process.exit(0);
189
201
  }
190
202
  // Start the storage (we need it now for frontend and later for matterbridge)
@@ -193,128 +205,96 @@ export class Matterbridge {
193
205
  this.matterbridgeContext = this.createCommissioningServerContext('Matterbridge', 'Matterbridge', DeviceTypes.AGGREGATOR.code, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge aggregator');
194
206
  // Initialize frontend
195
207
  await this.initializeFrontend(getIntParameter('frontend'));
196
- if (hasParameter('childbridge')) {
208
+ if (hasParameter('test')) {
197
209
  this.bridgeMode = 'childbridge';
198
- this.registeredPlugins.forEach(async (plugin) => {
199
- if (!plugin.enabled)
200
- return;
201
- this.log.info(`Loading registered plugin ${plg}${plugin.name}${nf} type ${GREEN}${plugin.type}${nf}`);
202
- await this.loadPlugin(plugin.path, 'load');
203
- });
204
- await this.startMatterBridge();
210
+ MatterbridgeDevice.bridgeMode = 'childbridge';
211
+ this.testStartMatterBridge(); // No await do it asyncronously
205
212
  }
206
213
  if (hasParameter('bridge')) {
207
214
  this.bridgeMode = 'bridge';
208
- this.registeredPlugins.forEach(async (plugin) => {
215
+ MatterbridgeDevice.bridgeMode = 'bridge';
216
+ for (const plugin of this.registeredPlugins) {
209
217
  if (!plugin.enabled)
210
- return;
211
- this.log.info(`Loading registered plugin ${plg}${plugin.name}${nf} type ${GREEN}${plugin.type}${nf}`);
212
- await this.loadPlugin(plugin.path, 'load');
213
- });
218
+ continue;
219
+ this.loadPlugin(plugin); // No await do it asyncronously
220
+ }
221
+ await this.startMatterBridge();
222
+ }
223
+ if (hasParameter('childbridge')) {
224
+ this.bridgeMode = 'childbridge';
225
+ MatterbridgeDevice.bridgeMode = 'childbridge';
226
+ for (const plugin of this.registeredPlugins) {
227
+ if (!plugin.enabled)
228
+ continue;
229
+ this.loadPlugin(plugin); // No await do it asyncronously
230
+ }
214
231
  await this.startMatterBridge();
215
232
  }
216
233
  }
217
234
  /**
218
235
  * Loads a plugin from the specified package.json file path.
219
236
  * @param packageJsonPath - The path to the package.json file of the plugin.
220
- * @param mode - The mode of operation. Possible values are 'load', 'add', 'remove', 'enable', 'disable'.
237
+ * @param mode - The mode of operation. Possible values are 'add', 'remove', 'enable', 'disable'.
221
238
  * @returns A Promise that resolves when the plugin is loaded successfully, or rejects with an error if loading fails.
222
239
  */
223
- async loadPlugin(packageJsonPath, mode = 'load') {
240
+ async executeCommandLine(packageJsonPath, mode) {
224
241
  if (!packageJsonPath.endsWith('package.json'))
225
242
  packageJsonPath = path.join(packageJsonPath, 'package.json');
243
+ // Resolve the package.json of the plugin
226
244
  packageJsonPath = path.resolve(packageJsonPath);
227
245
  this.log.debug(`Loading plugin from ${plg}${packageJsonPath}${db}`);
228
246
  try {
229
247
  // Load the package.json of the plugin
230
248
  const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
231
- const plugin = this.registeredPlugins.find((plugin) => plugin.name === packageJson.name);
232
- if (plugin && plugin.platform) {
233
- this.log.error(`Plugin ${plg}${plugin.name}${er} already loaded`);
234
- }
235
- // Resolve the main module path relative to package.json
236
- const pluginPath = path.resolve(path.dirname(packageJsonPath), packageJson.main);
237
- // Convert the file path to a URL
238
- const pluginUrl = pathToFileURL(pluginPath);
239
- // Dynamically import the plugin
240
- this.log.debug(`Importing plugin ${plg}${plugin?.name}${db} from ${pluginUrl.href}`);
241
- const pluginInstance = await import(pluginUrl.href);
242
- this.log.debug(`Imported plugin ${plg}${plugin?.name}${db} from ${pluginUrl.href}`);
243
- // Call the default export function of the plugin, passing this MatterBridge instance
244
- if (pluginInstance.default) {
245
- const platform = pluginInstance.default(this, new AnsiLogger({ logName: packageJson.description, logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */ }));
246
- platform.name = packageJson.name;
247
- if (mode === 'load') {
248
- this.log.info(`Plugin ${plg}${plugin?.name}${nf} type ${GREEN}${platform.type}${nf} loaded (entrypoint ${UNDERLINE}${pluginPath}${UNDERLINEOFF})`);
249
- // Update plugin info
250
- if (plugin) {
251
- plugin.path = packageJsonPath;
252
- plugin.name = packageJson.name;
253
- plugin.description = packageJson.description;
254
- plugin.version = packageJson.version;
255
- plugin.author = packageJson.author;
256
- plugin.type = platform.type;
257
- plugin.loaded = true;
258
- plugin.platform = platform;
249
+ if (mode === 'add') {
250
+ if (!this.registeredPlugins.find((plugin) => plugin.name === packageJson.name)) {
251
+ const plugin = { path: packageJsonPath, type: '', name: packageJson.name, version: packageJson.version, description: packageJson.description, author: packageJson.author, enabled: true };
252
+ if (await this.loadPlugin(plugin)) {
253
+ this.registeredPlugins.push(plugin);
254
+ await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
255
+ this.log.info(`Plugin ${plg}${packageJsonPath}${nf} type ${plugin.type} added to matterbridge`);
259
256
  }
260
257
  else {
261
- this.log.error(`Plugin ${plg}${packageJson.name}${er} not found`);
258
+ this.log.error(`Plugin ${plg}${packageJsonPath}${wr} not added to matterbridge error`);
262
259
  }
263
260
  }
264
- else if (mode === 'add') {
265
- if (!this.registeredPlugins.find((plugin) => plugin.name === packageJson.name)) {
266
- this.registeredPlugins.push({
267
- path: packageJsonPath,
268
- type: platform.type,
269
- name: packageJson.name,
270
- version: packageJson.version,
271
- description: packageJson.description,
272
- author: packageJson.author,
273
- enabled: true,
274
- });
275
- await this.nodeContext?.set('plugins', this.registeredPlugins);
276
- this.log.info(`Plugin ${plg}${packageJsonPath}${nf} type ${platform.type} added to matterbridge`);
277
- }
278
- else {
279
- this.log.warn(`Plugin ${plg}${packageJsonPath}${wr} already added to matterbridge`);
280
- }
261
+ else {
262
+ this.log.warn(`Plugin ${plg}${packageJsonPath}${wr} already added to matterbridge`);
281
263
  }
282
- else if (mode === 'remove') {
283
- if (this.registeredPlugins.find((registeredPlugin) => registeredPlugin.name === packageJson.name)) {
284
- this.registeredPlugins.splice(this.registeredPlugins.findIndex((registeredPlugin) => registeredPlugin.name === packageJson.name), 1);
285
- await this.nodeContext?.set('plugins', this.registeredPlugins);
286
- this.log.info(`Plugin ${plg}${packageJsonPath}${nf} removed from matterbridge`);
287
- }
288
- else {
289
- this.log.warn(`Plugin ${plg}${packageJsonPath}${wr} not registerd in matterbridge`);
290
- }
264
+ }
265
+ else if (mode === 'remove') {
266
+ if (this.registeredPlugins.find((registeredPlugin) => registeredPlugin.name === packageJson.name)) {
267
+ this.registeredPlugins.splice(this.registeredPlugins.findIndex((registeredPlugin) => registeredPlugin.name === packageJson.name), 1);
268
+ await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
269
+ this.log.info(`Plugin ${plg}${packageJsonPath}${nf} removed from matterbridge`);
291
270
  }
292
- else if (mode === 'enable') {
293
- const plugin = this.registeredPlugins.find((registeredPlugin) => registeredPlugin.name === packageJson.name);
294
- if (plugin) {
295
- plugin.enabled = true;
296
- await this.nodeContext?.set('plugins', this.registeredPlugins);
297
- this.log.info(`Plugin ${plg}${packageJsonPath}${nf} enabled`);
298
- }
299
- else {
300
- this.log.warn(`Plugin ${plg}${packageJsonPath}${wr} not registerd in matterbridge`);
301
- }
271
+ else {
272
+ this.log.warn(`Plugin ${plg}${packageJsonPath}${wr} not registerd in matterbridge`);
302
273
  }
303
- else if (mode === 'disable') {
304
- const plugin = this.registeredPlugins.find((registeredPlugin) => registeredPlugin.name === packageJson.name);
305
- if (plugin) {
306
- plugin.enabled = false;
307
- await this.nodeContext?.set('plugins', this.registeredPlugins);
308
- this.log.info(`Plugin ${plg}${packageJsonPath}${nf} disabled`);
309
- }
310
- else {
311
- this.log.warn(`Plugin ${plg}${packageJsonPath}${wr} not registerd in matterbridge`);
312
- }
274
+ }
275
+ else if (mode === 'enable') {
276
+ const plugin = this.registeredPlugins.find((registeredPlugin) => registeredPlugin.name === packageJson.name);
277
+ if (plugin) {
278
+ plugin.enabled = true;
279
+ await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
280
+ this.log.info(`Plugin ${plg}${packageJsonPath}${nf} enabled`);
313
281
  }
314
282
  else {
315
- this.log.error(`Plugin at ${plg}${pluginPath}${er} does not provide a default export`);
283
+ this.log.warn(`Plugin ${plg}${packageJsonPath}${wr} not registerd in matterbridge`);
316
284
  }
317
285
  }
286
+ else if (mode === 'disable') {
287
+ const plugin = this.registeredPlugins.find((registeredPlugin) => registeredPlugin.name === packageJson.name);
288
+ if (plugin) {
289
+ plugin.enabled = false;
290
+ await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
291
+ this.log.info(`Plugin ${plg}${packageJsonPath}${nf} disabled`);
292
+ }
293
+ else {
294
+ this.log.warn(`Plugin ${plg}${packageJsonPath}${wr} not registerd in matterbridge`);
295
+ }
296
+ }
297
+ //}
318
298
  }
319
299
  catch (err) {
320
300
  this.log.error(`Failed to load plugin from ${plg}${packageJsonPath}${er}: ${err}`);
@@ -339,13 +319,12 @@ export class Matterbridge {
339
319
  async cleanup(message) {
340
320
  if (!this.hasCleanupStarted) {
341
321
  this.hasCleanupStarted = true;
342
- this.log.debug(message);
322
+ this.log.info(message);
343
323
  // Callint the shutdown functions with a reason
344
- this.registeredPlugins.forEach((plugin) => {
345
- if (plugin.platform) {
346
- plugin.platform.onShutdown('Matterbridge is closing: ' + message);
347
- }
348
- });
324
+ for (const plugin of this.registeredPlugins) {
325
+ if (plugin.platform)
326
+ await plugin.platform.onShutdown('Matterbridge is closing: ' + message);
327
+ }
349
328
  // Set reachability to false
350
329
  /*
351
330
  this.log.debug(`*Changing reachability to false for ${this.registeredDevices.length} devices (${this.bridgeMode} mode):`);
@@ -368,10 +347,15 @@ export class Matterbridge {
368
347
  await this.stopMatter();
369
348
  // Closing storage
370
349
  await this.stopStorage();
371
- //await this.context?.set<RegisteredDevice[]>('plugins', this.registeredDevices);
372
- this.log.debug('Cleanup completed.');
350
+ const serializedRegisteredDevices = [];
351
+ this.registeredDevices.forEach((registeredDevice) => {
352
+ serializedRegisteredDevices.push(registeredDevice.device.serialize(registeredDevice.plugin));
353
+ });
354
+ //console.log('serializedRegisteredDevices:', serializedRegisteredDevices);
355
+ await this.nodeContext?.set('devices', serializedRegisteredDevices);
356
+ this.log.info('Cleanup completed.');
373
357
  process.exit(0);
374
- }, 2 * 1000);
358
+ }, 3 * 1000);
375
359
  }
376
360
  }
377
361
  /**
@@ -392,6 +376,7 @@ export class Matterbridge {
392
376
  * Adds a device to the Matterbridge.
393
377
  * @param pluginName - The name of the plugin.
394
378
  * @param device - The device to be added.
379
+ * @returns A Promise that resolves when the device is added successfully.
395
380
  */
396
381
  async addDevice(pluginName, device) {
397
382
  this.log.info(`Adding device ${dev}${device.name}${nf} for plugin ${plg}${pluginName}${nf}`);
@@ -403,20 +388,19 @@ export class Matterbridge {
403
388
  }
404
389
  // Add and register the device to the matterbridge in bridge mode
405
390
  if (this.bridgeMode === 'bridge') {
406
- const basic = device.getClusterServerById(BasicInformationCluster.id);
407
- if (!basic) {
408
- this.log.error(`addDevice error: cannot find the BasicInformationCluster device ${dev}${device.name}${nf} plugin ${plg}${pluginName}${nf}`);
409
- return;
410
- }
411
- device.createDefaultBridgedDeviceBasicInformationClusterServer(basic.getNodeLabelAttribute(), basic.getSerialNumberAttribute(), basic.getVendorIdAttribute(), basic.getVendorNameAttribute(), basic.getProductNameAttribute(), basic.getSoftwareVersionAttribute(), basic.getSoftwareVersionStringAttribute(), basic.getHardwareVersionAttribute(), basic.getHardwareVersionStringAttribute());
412
- //console.log(basic.getSoftwareVersionAttribute(), basic.getSoftwareVersionStringAttribute());
413
391
  this.matterAggregator.addBridgedDevice(device);
414
392
  this.registeredDevices.push({ plugin: pluginName, device, added: true });
415
- this.log.info(`Added and registered device ${dev}${device.name}${nf} for plugin ${plg}${pluginName}${nf}`);
393
+ if (plugin.registeredDevices !== undefined)
394
+ plugin.registeredDevices++;
395
+ if (plugin.addedDevices !== undefined)
396
+ plugin.addedDevices++;
397
+ this.log.info(`Added and registered device(${plugin.registeredDevices}/${plugin.addedDevices}) ${dev}${device.name}${nf} for plugin ${plg}${pluginName}${nf}`);
416
398
  }
417
399
  // Only register the device in childbridge mode
418
400
  if (this.bridgeMode === 'childbridge') {
419
401
  this.registeredDevices.push({ plugin: pluginName, device, added: false });
402
+ if (plugin.registeredDevices !== undefined)
403
+ plugin.registeredDevices++;
420
404
  this.log.info(`Registered device ${dev}${device.name}${nf} for plugin ${plg}${pluginName}${nf}`);
421
405
  }
422
406
  }
@@ -424,6 +408,7 @@ export class Matterbridge {
424
408
  * Adds a bridged device to the Matterbridge.
425
409
  * @param pluginName - The name of the plugin.
426
410
  * @param device - The bridged device to add.
411
+ * @returns {Promise<void>} - A promise that resolves when the storage process is started.
427
412
  */
428
413
  async addBridgedDevice(pluginName, device) {
429
414
  this.log.info(`Adding bridged device ${dev}${device.name}${nf} for plugin ${plg}${pluginName}${nf}`);
@@ -437,14 +422,18 @@ export class Matterbridge {
437
422
  if (this.bridgeMode === 'bridge') {
438
423
  this.matterAggregator.addBridgedDevice(device);
439
424
  this.registeredDevices.push({ plugin: pluginName, device, added: true });
440
- this.log.info(`Added and registered bridged device ${dev}${device.name}${nf} for plugin ${plg}${pluginName}${nf}`);
425
+ if (plugin.registeredDevices !== undefined)
426
+ plugin.registeredDevices++;
427
+ if (plugin.addedDevices !== undefined)
428
+ plugin.addedDevices++;
429
+ this.log.info(`Added and registered bridged device(${plugin.registeredDevices}/${plugin.addedDevices}) ${dev}${device.name}${nf} for plugin ${plg}${pluginName}${nf}`);
441
430
  }
442
431
  // Only register the device in childbridge mode
443
432
  if (this.bridgeMode === 'childbridge') {
444
433
  this.registeredDevices.push({ plugin: pluginName, device, added: false });
434
+ if (plugin.registeredDevices !== undefined)
435
+ plugin.registeredDevices++;
445
436
  this.log.info(`Registered bridged device ${dev}${device.name}${nf} for plugin ${plg}${pluginName}${nf}`);
446
- //const basic = device.getClusterServerById(BridgedDeviceBasicInformationCluster.id);
447
- //console.log(JSON.stringify(basic, null, 2));
448
437
  }
449
438
  }
450
439
  /**
@@ -513,6 +502,144 @@ export class Matterbridge {
513
502
  await this.storageManager.close();
514
503
  this.log.debug('Storage closed');
515
504
  }
505
+ async testStartMatterBridge() {
506
+ for (const plugin of this.registeredPlugins) {
507
+ if (!plugin.enabled)
508
+ continue;
509
+ // No await do it asyncronously
510
+ this.loadPlugin(plugin)
511
+ .then(() => {
512
+ // No await do it asyncronously
513
+ this.startPlugin(plugin)
514
+ .then(() => { })
515
+ .catch((err) => {
516
+ this.log.error(`Failed to start plugin ${plg}${plugin.name}${er}: ${err}`);
517
+ });
518
+ })
519
+ .catch((err) => {
520
+ this.log.error(`Failed to load plugin ${plg}${plugin.name}${er}: ${err}`);
521
+ });
522
+ }
523
+ for (const plugin of this.registeredPlugins) {
524
+ if (!plugin.enabled)
525
+ continue;
526
+ // Start the interval to check if the plugin is loaded and started
527
+ let times = 0;
528
+ const interval = setInterval(() => {
529
+ times++;
530
+ this.log.info(`Waiting ${times} secs for plugin ${plg}${plugin.name}${db} to load (${plugin.loaded}) and start (${plugin.started}) and send devices ...`);
531
+ if (!plugin.loaded || !plugin.started)
532
+ return;
533
+ this.log.info(`Plugin ${plg}${plugin.name}${db} sent ${plugin.registeredDevices} devices`);
534
+ clearInterval(interval);
535
+ }, 1000);
536
+ }
537
+ }
538
+ async startPlugin(plugin, message, configure = false) {
539
+ if (!plugin.loaded || !plugin.platform) {
540
+ this.log.error(`Plugin ${plg}${plugin.name}${er} not loaded or no platform`);
541
+ return Promise.reject(new Error(`Plugin ${plg}${plugin.name}${er} not loaded or no platform`));
542
+ }
543
+ if (plugin.started) {
544
+ this.log.error(`Plugin ${plg}${plugin.name}${er} already started`);
545
+ return Promise.resolve();
546
+ }
547
+ this.log.info(`Starting plugin ${plg}${plugin.name}${db} type ${typ}${plugin.type}${db}`);
548
+ try {
549
+ plugin.platform
550
+ .onStart(message)
551
+ .then(() => {
552
+ plugin.started = true;
553
+ this.log.info(`Started plugin ${plg}${plugin.name}${db} type ${typ}${plugin.type}${db}`);
554
+ if (configure)
555
+ this.configurePlugin(plugin); // No await do it asyncronously
556
+ return Promise.resolve();
557
+ })
558
+ .catch((err) => {
559
+ this.log.error(`Failed to start plugin ${plg}${plugin.name}${er}: ${err}`);
560
+ return Promise.reject(new Error(`Failed to start plugin ${plg}${plugin.name}${er}: ${err}`));
561
+ });
562
+ }
563
+ catch (err) {
564
+ this.log.error(`Failed to start plugin ${plg}${plugin.name}${er}: ${err}`);
565
+ return Promise.reject(new Error(`Failed to start plugin ${plg}${plugin.name}${er}: ${err}`));
566
+ }
567
+ }
568
+ async configurePlugin(plugin) {
569
+ if (!plugin.loaded || !plugin.started || !plugin.platform) {
570
+ this.log.error(`Plugin ${plg}${plugin.name}${er} not loaded or not started or not platform`);
571
+ return Promise.reject(new Error(`Plugin ${plg}${plugin.name}${er} not loaded or not started or not platform`));
572
+ }
573
+ if (plugin.configured) {
574
+ this.log.error(`Plugin ${plg}${plugin.name}${er} already configured`);
575
+ return Promise.resolve();
576
+ }
577
+ this.log.info(`Configuring plugin ${plg}${plugin.name}${db} type ${typ}${plugin.type}${db}`);
578
+ try {
579
+ plugin.platform
580
+ .onConfigure()
581
+ .then(() => {
582
+ plugin.configured = true;
583
+ this.log.info(`Configured plugin ${plg}${plugin.name}${db} type ${typ}${plugin.type}${db}`);
584
+ return Promise.resolve();
585
+ })
586
+ .catch((err) => {
587
+ this.log.error(`Failed to configure plugin ${plg}${plugin.name}${er}: ${err}`);
588
+ return Promise.reject(new Error(`Failed to configure plugin ${plg}${plugin.name}${er}: ${err}`));
589
+ });
590
+ }
591
+ catch (err) {
592
+ this.log.error(`Failed to configure plugin ${plg}${plugin.name}${er}: ${err}`);
593
+ return Promise.reject(new Error(`Failed to configure plugin ${plg}${plugin.name}${er}: ${err}`));
594
+ }
595
+ }
596
+ async loadPlugin(plugin) {
597
+ if (!plugin.enabled) {
598
+ this.log.error(`Plugin ${plg}${plugin.name}${er} not enabled`);
599
+ return Promise.reject(new Error(`Plugin ${plg}${plugin.name}${er} not enabled`));
600
+ }
601
+ if (plugin.platform) {
602
+ this.log.error(`Plugin ${plg}${plugin.name}${er} already loaded`);
603
+ return Promise.resolve(plugin.platform);
604
+ }
605
+ this.log.info(`Loading plugin ${plg}${plugin.name}${db} type ${typ}${plugin.type}${db}`);
606
+ try {
607
+ // Load the package.json of the plugin
608
+ const packageJson = JSON.parse(await fs.readFile(plugin.path, 'utf8'));
609
+ // Resolve the main module path relative to package.json
610
+ const pluginEntry = path.resolve(path.dirname(plugin.path), packageJson.main);
611
+ // Dynamically import the plugin
612
+ const pluginUrl = pathToFileURL(pluginEntry);
613
+ this.log.debug(`Importing plugin ${plg}${plugin.name}${db} from ${pluginUrl.href}`);
614
+ const pluginInstance = await import(pluginUrl.href);
615
+ this.log.debug(`Imported plugin ${plg}${plugin.name}${db} from ${pluginUrl.href}`);
616
+ // Call the default export function of the plugin, passing this MatterBridge instance
617
+ if (pluginInstance.default) {
618
+ const platform = pluginInstance.default(this, new AnsiLogger({ logName: plugin.description, logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logDebug: this.debugEnabled }));
619
+ platform.name = packageJson.name;
620
+ plugin.name = packageJson.name;
621
+ plugin.description = packageJson.description;
622
+ plugin.version = packageJson.version;
623
+ plugin.author = packageJson.author;
624
+ plugin.type = platform.type;
625
+ plugin.platform = platform;
626
+ plugin.loaded = true;
627
+ plugin.registeredDevices = 0;
628
+ plugin.addedDevices = 0;
629
+ await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
630
+ this.log.info(`Loaded plugin ${plg}${plugin.name}${db} type ${typ}${platform.type}${db} (entrypoint ${UNDERLINE}${pluginEntry}${UNDERLINEOFF})`);
631
+ return Promise.resolve(platform);
632
+ }
633
+ else {
634
+ this.log.error(`Plugin ${plg}${plugin.name}${er} does not provide a default export`);
635
+ return Promise.reject(new Error(`Plugin ${plg}${plugin.name}${er} does not provide a default export`));
636
+ }
637
+ }
638
+ catch (err) {
639
+ this.log.error(`Failed to load plugin ${plg}${plugin.name}${er}: ${err}`);
640
+ return Promise.reject(new Error(`Failed to load plugin ${plg}${plugin.name}${er}: ${err}`));
641
+ }
642
+ }
516
643
  /**
517
644
  * Starts the Matterbridge based on the bridge mode.
518
645
  * If the bridge mode is 'bridge', it creates a commissioning server, matter aggregator,
@@ -527,7 +654,7 @@ export class Matterbridge {
527
654
  this.createMatterServer(this.storageManager);
528
655
  if (this.bridgeMode === 'bridge') {
529
656
  // Plugins are loaded by loadPlugin on startup and plugin.loaded is set to true
530
- // Plugins are started by callback when Matterbridge is commissioned and plugin.started is set to true
657
+ // Plugins are started and configured by callback when Matterbridge is commissioned and plugin.started is set to true
531
658
  this.log.debug(`Creating commissioning server context for ${plg}Matterbridge${db}`);
532
659
  this.matterbridgeContext = this.createCommissioningServerContext('Matterbridge', 'Matterbridge', DeviceTypes.AGGREGATOR.code, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge aggregator');
533
660
  this.log.debug(`Creating commissioning server for ${plg}Matterbridge${db}`);
@@ -540,10 +667,6 @@ export class Matterbridge {
540
667
  await this.matterServer.addCommissioningServer(this.commissioningServer, { uniqueStorageKey: 'Matterbridge' });
541
668
  this.log.debug('Starting matter server');
542
669
  await this.startMatterServer();
543
- this.registeredPlugins.forEach(async (plugin) => {
544
- if (plugin.platform)
545
- await plugin.platform.onMatterStarted();
546
- });
547
670
  this.showCommissioningQRCode(this.commissioningServer, this.matterbridgeContext, 'Matterbridge');
548
671
  }
549
672
  if (this.bridgeMode === 'childbridge') {
@@ -554,26 +677,13 @@ export class Matterbridge {
554
677
  this.registeredPlugins.forEach(async (plugin) => {
555
678
  if (!plugin.enabled)
556
679
  return;
557
- // Start the interval to check if the plugin is loaded
558
- const loadedInterval = setInterval(async () => {
559
- this.log.debug(`Waiting in load interval for plugin ${plg}${plugin.name}${db} to load (${plugin.loaded}) and start (${plugin.started}) and send devices ...`);
560
- if (!plugin.loaded)
561
- return;
562
- plugin.started = true;
563
- plugin.registeredDevices = 0;
564
- clearInterval(loadedInterval);
565
- }, 1000);
566
680
  // Start the interval to check if the plugins is started
567
- const startedInterval = setInterval(async () => {
568
- this.log.debug(`Waiting in started interval for plugin ${plg}${plugin.name}${db} to load (${plugin.loaded}) and start (${plugin.started}) and send devices ...`);
569
- if (!plugin.started)
681
+ const startInterval = setInterval(async () => {
682
+ this.log.debug(`Waiting in start interval for plugin ${plg}${plugin.name}${db} to load (${plugin.loaded}) and start (${plugin.started}) and send devices ...`);
683
+ if (!plugin.loaded)
570
684
  return;
571
685
  if (plugin.type === 'AccessoryPlatform') {
572
- this.log.debug(`Starting accessory platform for plugin ${plg}${plugin.name}${db}`);
573
- if (plugin.platform)
574
- await plugin.platform.onStart('Matterbridge Accessory platform has started commissioning');
575
- else
576
- this.log.error(`Platform not found for plugin ${plg}${plugin.name}${er}`);
686
+ await this.startPlugin(plugin, 'Matterbridge Accessory platform has started');
577
687
  this.registeredDevices.forEach(async (registeredDevice) => {
578
688
  if (registeredDevice.plugin === plugin.name) {
579
689
  plugin.storageContext = this.importCommissioningServerContext(plugin.name, registeredDevice.device); // Generate serialNumber and uniqueId
@@ -595,17 +705,13 @@ export class Matterbridge {
595
705
  plugin.commissioningServer = this.createCommisioningServer(plugin.storageContext, plugin.name);
596
706
  this.log.debug(`Creating aggregator for plugin ${plg}${plugin.name}${db}`);
597
707
  plugin.aggregator = this.createMatterAggregator(plugin.storageContext); // Generate serialNumber and uniqueId
598
- this.log.debug(`Starting dynamic platform for plugin ${plg}${plugin.name}${db}`);
599
- if (plugin.platform)
600
- await plugin.platform.onStart('Matterbridge Dynamic platform has started commissioning');
601
- else
602
- this.log.error(`Platform not found for plugin ${plg}${plugin.name}${er}`);
708
+ await this.startPlugin(plugin, 'Matterbridge Dynamic platform has started');
603
709
  this.log.debug(`Adding matter aggregator to commissioning server for plugin ${plg}${plugin.name}${db}`);
604
710
  plugin.commissioningServer.addDevice(plugin.aggregator);
605
711
  this.log.debug(`Adding commissioning server to matter server for plugin ${plg}${plugin.name}${db}`);
606
712
  await this.matterServer.addCommissioningServer(plugin.commissioningServer, { uniqueStorageKey: plugin.name });
607
713
  }
608
- clearInterval(startedInterval);
714
+ clearInterval(startInterval);
609
715
  }, 1000);
610
716
  });
611
717
  // Start the interval to check if all plugins are loaded and started and so start the matter server
@@ -635,20 +741,18 @@ export class Matterbridge {
635
741
  }
636
742
  });
637
743
  });
638
- this.log.debug('Starting matter server');
639
744
  await this.startMatterServer();
640
- this.registeredPlugins.forEach(async (plugin) => {
641
- if (plugin.platform)
642
- await plugin.platform.onMatterStarted();
745
+ for (const plugin of this.registeredPlugins) {
643
746
  this.showCommissioningQRCode(plugin.commissioningServer, plugin.storageContext, plugin.name);
644
- });
645
- Logger.defaultLogLevel = Level.DEBUG;
747
+ }
748
+ Logger.defaultLogLevel = this.debugEnabled ? Level.DEBUG : Level.INFO;
646
749
  clearInterval(startMatterInterval);
647
750
  }, 1000);
648
751
  return;
649
752
  }
650
753
  }
651
754
  async startMatterServer() {
755
+ this.log.debug('Starting matter server');
652
756
  await this.matterServer.start();
653
757
  this.log.debug('Started matter server');
654
758
  }
@@ -794,14 +898,18 @@ export class Matterbridge {
794
898
  },
795
899
  activeSessionsChangedCallback: (fabricIndex) => {
796
900
  const info = commissioningServer.getActiveSessionInformation(fabricIndex);
797
- this.log.debug(`***Active sessions changed on fabric ${fabricIndex} for ${plg}${name}${nf}`, debugStringify(info));
798
- if (info && info[0]?.isPeerActive === true && info[0]?.secure === true && info[0]?.numberOfActiveSubscriptions >= 1) {
799
- this.log.info(`***Controller connected to ${plg}${name}${nf} ready to start...`);
901
+ let connected = false;
902
+ info.forEach((session) => {
903
+ this.log.debug(`***Active session changed on fabric ${fabricIndex} ${session.fabric?.rootVendorId}/${session.fabric?.label} for ${plg}${name}${nf}`, debugStringify(session));
904
+ if (session.isPeerActive === true && session.secure === true && session.numberOfActiveSubscriptions >= 1) {
905
+ this.log.info(`***Controller ${session.fabric?.rootVendorId}/${session.fabric?.label} connected to ${plg}${name}${nf}`);
906
+ connected = true;
907
+ }
908
+ });
909
+ if (connected) {
800
910
  if (this.bridgeMode === 'childbridge') {
801
911
  const plugin = this.findPlugin(name);
802
912
  if (plugin) {
803
- if (plugin.connected === true)
804
- return; // Only once cause the devices are already added to the plugins aggregator
805
913
  plugin.paired = true;
806
914
  plugin.connected = true;
807
915
  }
@@ -809,45 +917,40 @@ export class Matterbridge {
809
917
  setTimeout(() => {
810
918
  if (this.bridgeMode === 'bridge') {
811
919
  //Logger.defaultLogLevel = Level.INFO;
812
- this.registeredPlugins.forEach(async (plugin) => {
920
+ for (const plugin of this.registeredPlugins) {
813
921
  if (!plugin.enabled)
814
- return;
815
- if (plugin.platform && !plugin.started) {
816
- this.log.info(`***Starting plugin ${plg}${plugin.name}${nf}`);
817
- await plugin.platform.onStart('Matterbridge is commissioned and controllers are connected');
818
- plugin.started = true;
819
- this.log.info(`***Started plugin ${plg}${plugin.name}${nf}`);
820
- }
821
- else {
822
- this.log.error(`***Platform not found for plugin ${plg}${plugin.name}${er}`);
823
- }
824
- });
825
- Logger.defaultLogLevel = Level.DEBUG;
922
+ continue;
923
+ this.startPlugin(plugin, 'Matterbridge is commissioned and controllers are connected', true); // No await do it asyncronously
924
+ //this.configurePlugin(plugin); // No await do it asyncronously
925
+ }
926
+ Logger.defaultLogLevel = this.debugEnabled ? Level.DEBUG : Level.INFO;
826
927
  }
827
928
  if (this.bridgeMode === 'childbridge') {
828
929
  //Logger.defaultLogLevel = Level.INFO;
829
930
  const plugin = this.findPlugin(name);
830
- if (!plugin || plugin.type === 'AccessoryPlatform')
831
- return;
832
- this.registeredDevices.forEach(async (registeredDevice) => {
833
- if (registeredDevice.plugin !== name)
834
- return;
835
- this.log.debug(`***Adding device ${registeredDevice.device.name} to aggregator for plugin ${plg}${plugin.name}${db}`);
836
- if (!plugin.aggregator) {
837
- this.log.error(`***Aggregator not found for plugin ${plg}${plugin.name}${er}`);
838
- return;
931
+ if (plugin && plugin.type === 'DynamicPlatform') {
932
+ for (const registeredDevice of this.registeredDevices) {
933
+ if (registeredDevice.plugin === name) {
934
+ this.log.info(`Adding bridged device ${dev}${registeredDevice.device.name}${nf} to aggregator for plugin ${plg}${plugin.name}${db}`);
935
+ if (!plugin.aggregator) {
936
+ this.log.error(`****Aggregator not found for plugin ${plg}${plugin.name}${er}`);
937
+ continue;
938
+ }
939
+ plugin.aggregator.addBridgedDevice(registeredDevice.device);
940
+ if (plugin.addedDevices !== undefined)
941
+ plugin.addedDevices++;
942
+ this.log.info(`Added bridged device(${plugin.registeredDevices}/${plugin.addedDevices}) ${dev}${registeredDevice.device.name}${nf} for plugin ${plg}${plugin.name}${nf}`);
943
+ registeredDevice.added = true;
944
+ }
839
945
  }
840
- plugin.aggregator.addBridgedDevice(registeredDevice.device);
841
- if (plugin.registeredDevices !== undefined)
842
- plugin.registeredDevices++;
843
- registeredDevice.added = true;
844
- });
845
- Logger.defaultLogLevel = Level.DEBUG;
946
+ }
947
+ for (const plugin of this.registeredPlugins) {
948
+ if (plugin.name === name && plugin.platform) {
949
+ this.configurePlugin(plugin); // No await do it asyncronously
950
+ }
951
+ }
952
+ Logger.defaultLogLevel = this.debugEnabled ? Level.DEBUG : Level.INFO;
846
953
  }
847
- this.registeredPlugins.forEach(async (plugin) => {
848
- if (plugin.name === name && plugin.platform)
849
- plugin.platform.onConfigure();
850
- });
851
954
  //logEndpoint(commissioningServer.getRootEndpoint());
852
955
  }, 2000);
853
956
  }
@@ -987,6 +1090,10 @@ export class Matterbridge {
987
1090
  await fs.mkdir(this.matterbridgeDirectory);
988
1091
  }
989
1092
  this.log.debug(`Matterbridge Directory: ${this.matterbridgeDirectory}`);
1093
+ // Matterbridge version
1094
+ const packageJson = JSON.parse(await fs.readFile(path.join(this.rootDirectory, 'package.json'), 'utf-8'));
1095
+ this.matterbridgeVersion = packageJson.version;
1096
+ this.log.debug(`Matterbridge Version: ${this.matterbridgeVersion}`);
990
1097
  // Current working directory
991
1098
  const currentDir = process.cwd();
992
1099
  this.log.debug(`Current Working Directory: ${currentDir}`);
@@ -994,7 +1101,24 @@ export class Matterbridge {
994
1101
  const cmdArgs = process.argv.slice(2).join(' ');
995
1102
  this.log.debug(`Command Line Arguments: ${cmdArgs}`);
996
1103
  }
997
- getBaseRegisteredPlugin() {
1104
+ getBaseRegisteredPlugins() {
1105
+ const baseRegisteredPlugins = this.registeredPlugins.map((plugin) => ({
1106
+ path: plugin.path,
1107
+ type: plugin.type,
1108
+ name: plugin.name,
1109
+ version: plugin.version,
1110
+ description: plugin.description,
1111
+ author: plugin.author,
1112
+ enabled: plugin.enabled,
1113
+ loaded: plugin.loaded,
1114
+ started: plugin.started,
1115
+ paired: plugin.paired,
1116
+ connected: plugin.connected,
1117
+ registeredDevices: plugin.registeredDevices,
1118
+ }));
1119
+ return baseRegisteredPlugins;
1120
+ }
1121
+ getBaseRegisteredDevices() {
998
1122
  const baseRegisteredPlugins = this.registeredPlugins.map((plugin) => ({
999
1123
  path: plugin.path,
1000
1124
  type: plugin.type,
@@ -1044,8 +1168,7 @@ export class Matterbridge {
1044
1168
  // Endpoint to provide plugins
1045
1169
  this.app.get('/api/plugins', (req, res) => {
1046
1170
  this.log.debug('The frontend sent /api/plugins');
1047
- const baseRegisteredPlugins = this.getBaseRegisteredPlugin();
1048
- res.json(baseRegisteredPlugins);
1171
+ res.json(this.getBaseRegisteredPlugins());
1049
1172
  });
1050
1173
  // Endpoint to provide devices
1051
1174
  this.app.get('/api/devices', (req, res) => {
@@ -1077,15 +1200,15 @@ export class Matterbridge {
1077
1200
  // Endpoint to provide the cluster servers of the devices
1078
1201
  this.app.get('/api/devices_clusters/:selectedPluginName/:selectedDeviceEndpoint', (req, res) => {
1079
1202
  const selectedPluginName = req.params.selectedPluginName;
1080
- const selectedDeviceEndpoint = req.params.selectedDeviceEndpoint;
1203
+ const selectedDeviceEndpoint = parseInt(req.params.selectedDeviceEndpoint, 10);
1081
1204
  this.log.debug(`The frontend sent /api/devices_clusters plugin:${selectedPluginName} endpoint:${selectedDeviceEndpoint}`);
1082
- if (selectedPluginName === 'none' /* || selectedDeviceEndpoint === 'none'*/) {
1205
+ if (selectedPluginName === 'none') {
1083
1206
  res.json([]);
1084
1207
  return;
1085
1208
  }
1086
1209
  const data = [];
1087
1210
  this.registeredDevices.forEach((registeredDevice) => {
1088
- if (registeredDevice.plugin === selectedPluginName) {
1211
+ if (registeredDevice.plugin === selectedPluginName && registeredDevice.device.id === selectedDeviceEndpoint) {
1089
1212
  const clusterServers = registeredDevice.device.getAllClusterServers();
1090
1213
  clusterServers.forEach((clusterServer) => {
1091
1214
  Object.entries(clusterServer.attributes).forEach(([key, value]) => {
@@ -1189,6 +1312,13 @@ ws.onmessage = (event) => {
1189
1312
  console.log(`Received message => ${event.data}`);
1190
1313
  };
1191
1314
 
1315
+ */
1316
+ /*
1317
+ // In Matterbridge
1318
+ global.matterbridgeInstance = Matterbridge.loadInstance();
1319
+
1320
+ // In plugins
1321
+ const matterbridge = global.matterbridgeInstance;
1192
1322
  */
1193
1323
  /*
1194
1324
  npx create-react-app matterbridge-frontend