matterbridge 1.1.7 → 1.1.9

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 (31) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +1 -1
  3. package/dist/cli.d.ts +22 -0
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +26 -2
  6. package/dist/cli.js.map +1 -1
  7. package/dist/index.js +4 -2
  8. package/dist/index.js.map +1 -1
  9. package/dist/matterbridge.d.ts +17 -2
  10. package/dist/matterbridge.d.ts.map +1 -1
  11. package/dist/matterbridge.js +387 -102
  12. package/dist/matterbridge.js.map +1 -1
  13. package/dist/matterbridgeDevice.d.ts +2 -0
  14. package/dist/matterbridgeDevice.d.ts.map +1 -1
  15. package/dist/matterbridgeDevice.js +74 -1
  16. package/dist/matterbridgeDevice.js.map +1 -1
  17. package/frontend/build/asset-manifest.json +8 -8
  18. package/frontend/build/index.html +1 -1
  19. package/frontend/build/static/css/main.ce1ee9e7.css +2 -0
  20. package/frontend/build/static/css/main.ce1ee9e7.css.map +1 -0
  21. package/frontend/build/static/js/453.d855a71b.chunk.js +2 -0
  22. package/frontend/build/static/js/{453.8ab44547.chunk.js.map → 453.d855a71b.chunk.js.map} +1 -1
  23. package/frontend/build/static/js/main.cc840fb3.js +3 -0
  24. package/frontend/build/static/js/{main.e5888ebb.js.LICENSE.txt → main.cc840fb3.js.LICENSE.txt} +19 -0
  25. package/frontend/build/static/js/main.cc840fb3.js.map +1 -0
  26. package/package.json +2 -2
  27. package/frontend/build/static/css/main.6d93e0db.css +0 -2
  28. package/frontend/build/static/css/main.6d93e0db.css.map +0 -1
  29. package/frontend/build/static/js/453.8ab44547.chunk.js +0 -2
  30. package/frontend/build/static/js/main.e5888ebb.js +0 -3
  31. package/frontend/build/static/js/main.e5888ebb.js.map +0 -1
@@ -25,6 +25,7 @@ import { NodeStorageManager } from 'node-persist-manager';
25
25
  import { AnsiLogger, BRIGHT, RESET, UNDERLINE, UNDERLINEOFF, YELLOW, db, debugStringify, stringify, er, nf, rs, wr } from 'node-ansi-logger';
26
26
  import { fileURLToPath, pathToFileURL } from 'url';
27
27
  import { promises as fs } from 'fs';
28
+ import { exec, execSync } from 'child_process';
28
29
  import express from 'express';
29
30
  import os from 'os';
30
31
  import path from 'path';
@@ -60,7 +61,10 @@ export class Matterbridge {
60
61
  homeDirectory = '';
61
62
  rootDirectory = '';
62
63
  matterbridgeDirectory = '';
64
+ matterbridgePluginDirectory = '';
63
65
  matterbridgeVersion = '';
66
+ matterbridgeLatestVersion = '';
67
+ globalModulesDir = '';
64
68
  bridgeMode = '';
65
69
  debugEnabled = false;
66
70
  log;
@@ -69,7 +73,8 @@ export class Matterbridge {
69
73
  registeredDevices = [];
70
74
  nodeStorage;
71
75
  nodeContext;
72
- app;
76
+ expressApp;
77
+ expressServer;
73
78
  storageManager;
74
79
  matterbridgeContext;
75
80
  mattercontrollerContext;
@@ -79,26 +84,24 @@ export class Matterbridge {
79
84
  commissioningController;
80
85
  static instance;
81
86
  constructor() {
82
- // we load asynchroneously the instance
87
+ // We load asyncronously
83
88
  }
84
89
  /**
85
90
  * Loads an instance of the Matterbridge class.
86
91
  * If an instance already exists, return that instance.
87
92
  * @returns The loaded instance of the Matterbridge class.
88
93
  */
89
- static async loadInstance(cli = false) {
90
- // eslint-disable-next-line no-console
91
- console.error('loadInstance cli:', cli);
94
+ static async loadInstance(initialize = false) {
92
95
  if (!Matterbridge.instance) {
93
96
  // eslint-disable-next-line no-console
94
- console.error('Matterbridge instance does not exists');
97
+ console.log('Matterbridge instance does not exists');
95
98
  Matterbridge.instance = new Matterbridge();
96
- if (cli)
99
+ if (initialize)
97
100
  await Matterbridge.instance.initialize();
98
101
  }
99
102
  else {
100
103
  // eslint-disable-next-line no-console
101
- console.error('Matterbridge instance already exists');
104
+ console.log('Matterbridge instance already exists');
102
105
  }
103
106
  return Matterbridge.instance;
104
107
  }
@@ -124,10 +127,14 @@ export class Matterbridge {
124
127
  - frontend [port]: start the frontend on the given port (default 3000)
125
128
  - debug: enable debug mode (default false)
126
129
  - list: list the registered plugins
127
- - add [plugin path]: register the plugin
128
- - remove [plugin path]: remove the plugin
129
- - enable [plugin path]: enable the plugin
130
- - disable [plugin path]: disable the plugin\n`);
130
+ - add [plugin path]: register the plugin from the given absolute or relative path
131
+ - add [plugin name]: register the globally installed plugin with the given name
132
+ - remove [plugin path]: remove the plugin from the given absolute or relative path
133
+ - remove [plugin name]: remove the globally installed plugin with the given name
134
+ - enable [plugin path]: enable the plugin from the given absolute or relative path
135
+ - enable [plugin name]: enable the globally installed plugin with the given name
136
+ - disable [plugin path]: disable the plugin from the given absolute or relative path
137
+ - disable [plugin name]: disable the globally installed plugin with the given name\n`);
131
138
  process.exit(0);
132
139
  }
133
140
  // set Matterbridge logger
@@ -155,13 +162,13 @@ export class Matterbridge {
155
162
  this.registeredPlugins = await this.nodeContext.get('plugins', []);
156
163
  for (const plugin of this.registeredPlugins) {
157
164
  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
+ plugin.nodeContext = await this.nodeStorage.createStorage(plugin.name);
166
+ await plugin.nodeContext.set('name', plugin.name);
167
+ await plugin.nodeContext.set('type', plugin.type);
168
+ await plugin.nodeContext.set('path', plugin.path);
169
+ await plugin.nodeContext.set('version', plugin.version);
170
+ await plugin.nodeContext.set('description', plugin.description);
171
+ await plugin.nodeContext.set('author', plugin.author);
165
172
  }
166
173
  // Parse command line
167
174
  this.parseCommandLine();
@@ -215,7 +222,7 @@ export class Matterbridge {
215
222
  if (hasParameter('test')) {
216
223
  this.bridgeMode = 'childbridge';
217
224
  MatterbridgeDevice.bridgeMode = 'childbridge';
218
- this.testStartMatterBridge(); // No await do it asyncronously
225
+ await this.testStartMatterBridge(); // No await do it asyncronously
219
226
  }
220
227
  if (hasParameter('bridge')) {
221
228
  this.bridgeMode = 'bridge';
@@ -227,6 +234,8 @@ export class Matterbridge {
227
234
  plugin.started = false;
228
235
  plugin.configured = false;
229
236
  plugin.connected = undefined;
237
+ plugin.qrPairingCode = undefined;
238
+ plugin.manualPairingCode = undefined;
230
239
  this.loadPlugin(plugin); // No await do it asyncronously
231
240
  }
232
241
  await this.startMatterBridge();
@@ -241,11 +250,49 @@ export class Matterbridge {
241
250
  plugin.started = false;
242
251
  plugin.configured = false;
243
252
  plugin.connected = false;
253
+ plugin.qrPairingCode = (await plugin.nodeContext?.get('qrPairingCode', undefined)) ?? undefined;
254
+ plugin.manualPairingCode = (await plugin.nodeContext?.get('manualPairingCode', undefined)) ?? undefined;
244
255
  this.loadPlugin(plugin, true, 'Matterbridge is starting'); // No await do it asyncronously
245
256
  }
246
257
  await this.startMatterBridge();
247
258
  }
248
259
  }
260
+ async resolvePluginName(pluginPath) {
261
+ if (!pluginPath.endsWith('package.json'))
262
+ pluginPath = path.join(pluginPath, 'package.json');
263
+ // Resolve the package.json of the plugin
264
+ let packageJsonPath = path.resolve(pluginPath);
265
+ this.log.debug(`Loading plugin from ${plg}${packageJsonPath}${db}`);
266
+ // Check if the package.json file exists
267
+ let packageJsonExists = false;
268
+ try {
269
+ await fs.access(packageJsonPath);
270
+ packageJsonExists = true;
271
+ }
272
+ catch {
273
+ packageJsonExists = false;
274
+ }
275
+ if (!packageJsonExists) {
276
+ this.log.debug(`Package.json not found at ${packageJsonPath}`);
277
+ this.log.debug(`Trying at ${this.globalModulesDir}`);
278
+ packageJsonPath = path.join(this.globalModulesDir, pluginPath);
279
+ //this.log.debug(`Got ${packageJsonPath}`);
280
+ }
281
+ try {
282
+ // Load the package.json of the plugin
283
+ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
284
+ if (!packageJson.name) {
285
+ this.log.debug(`Package.json name not found at ${packageJsonPath}`);
286
+ return null;
287
+ }
288
+ this.log.debug(`Package.json name: ${plg}${packageJson.name}${db} description: "${nf}${packageJson.description}${db}" found at "${nf}${packageJsonPath}${db}"`);
289
+ return packageJsonPath;
290
+ }
291
+ catch (err) {
292
+ this.log.debug(`Failed to load plugin from ${plg}${packageJsonPath}${er}: ${err}`);
293
+ return null;
294
+ }
295
+ }
249
296
  /**
250
297
  * Loads a plugin from the specified package.json file path.
251
298
  * @param packageJsonPath - The path to the package.json file of the plugin.
@@ -253,10 +300,7 @@ export class Matterbridge {
253
300
  * @returns A Promise that resolves when the plugin is loaded successfully, or rejects with an error if loading fails.
254
301
  */
255
302
  async executeCommandLine(packageJsonPath, mode) {
256
- if (!packageJsonPath.endsWith('package.json'))
257
- packageJsonPath = path.join(packageJsonPath, 'package.json');
258
- // Resolve the package.json of the plugin
259
- packageJsonPath = path.resolve(packageJsonPath);
303
+ packageJsonPath = (await this.resolvePluginName(packageJsonPath)) ?? packageJsonPath;
260
304
  this.log.debug(`Loading plugin from ${plg}${packageJsonPath}${db}`);
261
305
  try {
262
306
  // Load the package.json of the plugin
@@ -317,7 +361,6 @@ export class Matterbridge {
317
361
  this.log.warn(`Plugin ${plg}${packageJsonPath}${wr} not registerd in matterbridge`);
318
362
  }
319
363
  }
320
- //}
321
364
  }
322
365
  catch (err) {
323
366
  this.log.error(`Failed to load plugin from ${plg}${packageJsonPath}${er}: ${err}`);
@@ -328,21 +371,32 @@ export class Matterbridge {
328
371
  * When either of these signals are received, the cleanup method is called with an appropriate message.
329
372
  */
330
373
  async registerSignalHandlers() {
331
- process.on('SIGINT', async () => {
374
+ process.once('SIGINT', async () => {
332
375
  await this.cleanup('SIGINT received, cleaning up...');
333
376
  });
334
- process.on('SIGTERM', async () => {
377
+ process.once('SIGTERM', async () => {
335
378
  await this.cleanup('SIGTERM received, cleaning up...');
336
379
  });
337
380
  }
381
+ /**
382
+ * Restarts the process by spawning a new process and exiting the current process.
383
+ */
384
+ async restartProcess() {
385
+ //this.log.info('Restarting still not implemented');
386
+ //return;
387
+ await this.cleanup('Matterbridge is restarting...', true);
388
+ this.hasCleanupStarted = false;
389
+ }
338
390
  /**
339
391
  * Performs cleanup operations before shutting down Matterbridge.
340
392
  * @param message - The reason for the cleanup.
341
393
  */
342
- async cleanup(message) {
394
+ async cleanup(message, restart = false) {
343
395
  if (!this.hasCleanupStarted) {
344
396
  this.hasCleanupStarted = true;
345
397
  this.log.info(message);
398
+ process.removeAllListeners('SIGINT');
399
+ process.removeAllListeners('SIGTERM');
346
400
  // Callint the shutdown functions with a reason
347
401
  for (const plugin of this.registeredPlugins) {
348
402
  if (plugin.platform)
@@ -365,21 +419,47 @@ export class Matterbridge {
365
419
  if (this.bridgeMode === 'childbridge' && plugin.type === 'DynamicPlatform') registeredDevice.device.setBridgedDeviceReachability(false);
366
420
  });
367
421
  */
422
+ // Close the express server
423
+ if (this.expressServer) {
424
+ this.expressServer.close();
425
+ this.expressServer = undefined;
426
+ }
427
+ // Remove listeners
428
+ if (this.expressApp) {
429
+ this.expressApp.removeAllListeners();
430
+ this.expressApp = undefined;
431
+ }
368
432
  setTimeout(async () => {
369
433
  // Closing matter
370
434
  await this.stopMatter();
371
435
  // Closing storage
372
436
  await this.stopStorage();
373
437
  // Serialize registeredDevices
374
- const serializedRegisteredDevices = [];
375
- this.registeredDevices.forEach((registeredDevice) => {
376
- serializedRegisteredDevices.push(registeredDevice.device.serialize(registeredDevice.plugin));
377
- });
378
- //console.log('serializedRegisteredDevices:', serializedRegisteredDevices);
379
- await this.nodeContext?.set('devices', serializedRegisteredDevices);
380
- setTimeout(() => {
438
+ if (this.nodeContext) {
439
+ this.log.info('Saving registered devices...');
440
+ const serializedRegisteredDevices = [];
441
+ this.registeredDevices.forEach((registeredDevice) => {
442
+ serializedRegisteredDevices.push(registeredDevice.device.serialize(registeredDevice.plugin));
443
+ });
444
+ //console.log('serializedRegisteredDevices:', serializedRegisteredDevices);
445
+ await this.nodeContext.set('devices', serializedRegisteredDevices);
446
+ this.log.info('Saved registered devices');
447
+ // Clear nodeContext and nodeStorage (they just need 1000ms to write the data to disk)
448
+ this.nodeContext = undefined;
449
+ this.nodeStorage = undefined;
450
+ }
451
+ else {
452
+ this.log.error('Error saving registered devices: nodeContext not found!');
453
+ }
454
+ this.registeredPlugins = [];
455
+ this.registeredDevices = [];
456
+ setTimeout(async () => {
381
457
  this.log.info('Cleanup completed.');
382
- process.exit(0);
458
+ //if (restart) console.log(this);
459
+ if (restart)
460
+ await this.initialize();
461
+ else
462
+ process.exit(0);
383
463
  }, 2 * 1000);
384
464
  }, 3 * 1000);
385
465
  }
@@ -405,16 +485,20 @@ export class Matterbridge {
405
485
  * @returns A Promise that resolves when the device is added successfully.
406
486
  */
407
487
  async addDevice(pluginName, device) {
408
- this.log.info(`Adding device ${dev}${device.name}${nf} for plugin ${plg}${pluginName}${nf}`);
488
+ if (this.bridgeMode === 'bridge' && !this.matterAggregator) {
489
+ this.log.error(`Adding device ${dev}${device.name}${er} for plugin ${plg}${pluginName}${er} error: matterAggregator not found`);
490
+ return;
491
+ }
492
+ this.log.debug(`Adding device ${dev}${device.name}${db} for plugin ${plg}${pluginName}${db}`);
409
493
  // Check if the plugin is registered
410
494
  const plugin = this.registeredPlugins.find((plugin) => plugin.name === pluginName);
411
495
  if (!plugin) {
412
- this.log.error(`addDevice error: device ${dev}${device.name}${nf} plugin ${plg}${pluginName}${er} not found`);
496
+ this.log.error(`Error adding device ${dev}${device.name}${er} plugin ${plg}${pluginName}${er} not found`);
413
497
  return;
414
498
  }
415
- // Add and register the device to the matterbridge in bridge mode
499
+ // Register and add the device to matterbridge aggregator in bridge mode
416
500
  if (this.bridgeMode === 'bridge') {
417
- this.matterAggregator.addBridgedDevice(device);
501
+ this.matterAggregator?.addBridgedDevice(device);
418
502
  this.registeredDevices.push({ plugin: pluginName, device, added: true });
419
503
  if (plugin.registeredDevices !== undefined)
420
504
  plugin.registeredDevices++;
@@ -437,16 +521,20 @@ export class Matterbridge {
437
521
  * @returns {Promise<void>} - A promise that resolves when the storage process is started.
438
522
  */
439
523
  async addBridgedDevice(pluginName, device) {
440
- this.log.info(`Adding bridged device ${dev}${device.name}${nf} for plugin ${plg}${pluginName}${nf}`);
524
+ if (this.bridgeMode === 'bridge' && !this.matterAggregator) {
525
+ this.log.error(`Adding bridged device ${dev}${device.name}${er} for plugin ${plg}${pluginName}${er} error: matterAggregator not found`);
526
+ return;
527
+ }
528
+ this.log.debug(`Adding bridged device ${db}${device.name}${nf} for plugin ${plg}${pluginName}${db}`);
441
529
  // Check if the plugin is registered
442
530
  const plugin = this.registeredPlugins.find((plugin) => plugin.name === pluginName);
443
531
  if (!plugin) {
444
- this.log.error(`addBridgedDevice error: device ${dev}${device.name}${nf} plugin ${plg}${pluginName}${er} not found`);
532
+ this.log.error(`Error adding bridged device ${dev}${device.name}${er} plugin ${plg}${pluginName}${er} not found`);
445
533
  return;
446
534
  }
447
- // Add and register the device to the matterbridge in bridge mode
535
+ // Register and add the device to matterbridge aggregator in bridge mode
448
536
  if (this.bridgeMode === 'bridge') {
449
- this.matterAggregator.addBridgedDevice(device);
537
+ this.matterAggregator?.addBridgedDevice(device);
450
538
  this.registeredDevices.push({ plugin: pluginName, device, added: true });
451
539
  if (plugin.registeredDevices !== undefined)
452
540
  plugin.registeredDevices++;
@@ -469,18 +557,22 @@ export class Matterbridge {
469
557
  * @returns {Promise<void>} - A promise that resolves when the storage process is started.
470
558
  */
471
559
  async startStorage(storageType, storageName) {
472
- if (!storageName.endsWith('.json')) {
473
- storageName += '.json';
474
- }
475
560
  this.log.debug(`Starting storage ${storageType} ${storageName}`);
476
561
  if (storageType === 'disk') {
477
562
  const storageDisk = new StorageBackendDisk(storageName);
478
563
  this.storageManager = new StorageManager(storageDisk);
479
564
  }
480
- if (storageType === 'json') {
565
+ else if (storageType === 'json') {
566
+ if (!storageName.endsWith('.json'))
567
+ storageName += '.json';
481
568
  const storageJson = new StorageBackendJsonFile(storageName);
482
569
  this.storageManager = new StorageManager(storageJson);
483
570
  }
571
+ else {
572
+ this.log.error(`Unsupported storage type ${storageType}`);
573
+ await this.cleanup('Unsupported storage type');
574
+ return;
575
+ }
484
576
  try {
485
577
  await this.storageManager.initialize();
486
578
  this.log.debug('Storage initialized');
@@ -490,7 +582,7 @@ export class Matterbridge {
490
582
  }
491
583
  catch (error) {
492
584
  this.log.error('Storage initialize() error!');
493
- process.exit(1);
585
+ await this.cleanup('Storage initialize() error!');
494
586
  }
495
587
  }
496
588
  /**
@@ -525,41 +617,57 @@ export class Matterbridge {
525
617
  */
526
618
  async stopStorage() {
527
619
  this.log.debug('Stopping storage');
528
- await this.storageManager.close();
620
+ await this.storageManager?.close();
529
621
  this.log.debug('Storage closed');
622
+ this.storageManager = undefined;
623
+ this.matterbridgeContext = undefined;
624
+ this.mattercontrollerContext = undefined;
530
625
  }
531
626
  async testStartMatterBridge() {
627
+ /*
628
+ this.log.error('****Start forEach registeredPlugin');
629
+ this.registeredPlugins
630
+ .filter((plugin) => plugin.enabled === true)
631
+ .forEach(async (plugin) => {
632
+ plugin.loaded = false;
633
+ plugin.started = false;
634
+ plugin.configured = false;
635
+ plugin.connected = undefined;
636
+ this.log.error(`****Starting registeredPlugin ${plugin.name}`);
637
+ this.loadPlugin(plugin, true, 'Matterbridge is starting');
638
+ });
639
+ this.log.error('****Stop forEach registeredPlugin');
640
+ */
641
+ /*
532
642
  for (const plugin of this.registeredPlugins) {
533
- if (!plugin.enabled)
534
- continue;
535
- // No await do it asyncronously
536
- this.loadPlugin(plugin)
537
- .then(() => {
538
- // No await do it asyncronously
539
- this.startPlugin(plugin)
540
- .then(() => { })
541
- .catch((err) => {
542
- this.log.error(`Failed to start plugin ${plg}${plugin.name}${er}: ${err}`);
643
+ if (!plugin.enabled) continue;
644
+ // No await do it asyncronously
645
+ this.loadPlugin(plugin)
646
+ .then(() => {
647
+ // No await do it asyncronously
648
+ this.startPlugin(plugin)
649
+ .then(() => {})
650
+ .catch((err) => {
651
+ this.log.error(`Failed to start plugin ${plg}${plugin.name}${er}: ${err}`);
543
652
  });
544
653
  })
545
- .catch((err) => {
546
- this.log.error(`Failed to load plugin ${plg}${plugin.name}${er}: ${err}`);
654
+ .catch((err) => {
655
+ this.log.error(`Failed to load plugin ${plg}${plugin.name}${er}: ${err}`);
547
656
  });
548
657
  }
549
658
  for (const plugin of this.registeredPlugins) {
550
- if (!plugin.enabled)
551
- continue;
552
- // Start the interval to check if the plugin is loaded and started
553
- let times = 0;
554
- const interval = setInterval(() => {
555
- times++;
556
- this.log.info(`Waiting ${times} secs for plugin ${plg}${plugin.name}${db} to load (${plugin.loaded}) and start (${plugin.started}) and send devices ...`);
557
- if (!plugin.loaded || !plugin.started)
558
- return;
559
- this.log.info(`Plugin ${plg}${plugin.name}${db} sent ${plugin.registeredDevices} devices`);
560
- clearInterval(interval);
561
- }, 1000);
659
+ if (!plugin.enabled) continue;
660
+ // Start the interval to check if the plugin is loaded and started
661
+ let times = 0;
662
+ const interval = setInterval(() => {
663
+ times++;
664
+ this.log.info(`Waiting ${times} secs for plugin ${plg}${plugin.name}${db} to load (${plugin.loaded}) and start (${plugin.started}) and send devices ...`);
665
+ if (!plugin.loaded || !plugin.started) return;
666
+ this.log.info(`Plugin ${plg}${plugin.name}${db} sent ${plugin.registeredDevices} devices`);
667
+ clearInterval(interval);
668
+ }, 1000);
562
669
  }
670
+ */
563
671
  }
564
672
  async startPlugin(plugin, message, configure = false) {
565
673
  if (!plugin.loaded || !plugin.platform) {
@@ -678,13 +786,27 @@ export class Matterbridge {
678
786
  * @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
679
787
  */
680
788
  async startMatterBridge() {
789
+ if (!this.storageManager) {
790
+ this.log.error('No storage manager initialized');
791
+ await this.cleanup('No storage manager initialized');
792
+ return;
793
+ }
681
794
  this.log.debug('Starting matterbridge in mode', this.bridgeMode);
682
795
  this.createMatterServer(this.storageManager);
796
+ if (!this.matterServer) {
797
+ this.log.error('No matter server initialized');
798
+ await this.cleanup('No matter server initialized');
799
+ return;
800
+ }
683
801
  if (this.bridgeMode === 'bridge') {
684
802
  // Plugins are loaded by loadPlugin on startup and plugin.loaded is set to true
685
- // Plugins are started and configured by callback when Matterbridge is commissioned and plugin.started is set to true
803
+ // Plugins are started and configured by callback when Matterbridge is commissioned
686
804
  this.log.debug(`Creating commissioning server context for ${plg}Matterbridge${db}`);
687
805
  this.matterbridgeContext = this.createCommissioningServerContext('Matterbridge', 'Matterbridge', DeviceTypes.AGGREGATOR.code, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge aggregator');
806
+ if (!this.matterbridgeContext) {
807
+ this.log.error(`Error creating storage context${er}`);
808
+ return;
809
+ }
688
810
  this.log.debug(`Creating commissioning server for ${plg}Matterbridge${db}`);
689
811
  this.commissioningServer = this.createCommisioningServer(this.matterbridgeContext, 'Matterbridge');
690
812
  this.log.debug(`Creating matter aggregator for ${plg}Matterbridge${db}`);
@@ -698,19 +820,24 @@ export class Matterbridge {
698
820
  this.showCommissioningQRCode(this.commissioningServer, this.matterbridgeContext, 'Matterbridge');
699
821
  }
700
822
  if (this.bridgeMode === 'childbridge') {
701
- // Plugins are loaded and started by loadPlugin on startup and plugin.loaded is set to true and plugin.started is set to true
823
+ // Plugins are loaded and started by loadPlugin on startup
702
824
  // addDevice and addBridgedDeevice just register the devices that are added here to the plugin commissioning server for Accessory Platform
703
825
  // or to the plugin aggregator for Dynamic Platform after the commissioning is done
704
826
  // Plugins are configured by callback when the plugin is commissioned
705
- this.registeredPlugins.forEach(async (plugin) => {
827
+ this.registeredPlugins.forEach((plugin) => {
706
828
  if (!plugin.enabled)
707
829
  return;
708
830
  // Start the interval to check if the plugins is started
709
831
  // TODO set a counter or a timeout
710
- this.log.info(`**Starting startMatterBridge interval for plugin ${plg}${plugin.name}${db} loaded: ${plugin.loaded} started: ${plugin.started}...`);
832
+ this.log.debug(`*Starting startMatterBridge interval for plugin ${plg}${plugin.name}${db} loaded: ${plugin.loaded} started: ${plugin.started}...`);
711
833
  const startInterval = setInterval(async () => {
834
+ if (!this.matterServer) {
835
+ this.log.error('No matter server initialized');
836
+ await this.cleanup('No matter server initialized');
837
+ return;
838
+ }
712
839
  if (!plugin.loaded || !plugin.started) {
713
- this.log.info(`***Returning in startMatterBridge interval for plugin ${plg}${plugin.name}${db} loaded: ${plugin.loaded} started: ${plugin.started}...`);
840
+ this.log.info(`**Waiting in startMatterBridge interval for plugin ${plg}${plugin.name}${db} loaded: ${plugin.loaded} started: ${plugin.started}...`);
714
841
  return;
715
842
  }
716
843
  if (plugin.type === 'AccessoryPlatform') {
@@ -718,7 +845,11 @@ export class Matterbridge {
718
845
  .filter((registeredDevice) => registeredDevice.plugin === plugin.name)
719
846
  .forEach((registeredDevice) => {
720
847
  if (!plugin.storageContext)
721
- plugin.storageContext = this.importCommissioningServerContext(plugin.name, registeredDevice.device); // Generate serialNumber and uniqueId
848
+ plugin.storageContext = this.importCommissioningServerContext(plugin.name, registeredDevice.device);
849
+ if (!plugin.storageContext) {
850
+ this.log.error(`Error importing storage context for plugin ${plg}${plugin.name}${er}`);
851
+ return;
852
+ }
722
853
  if (!plugin.commissioningServer)
723
854
  plugin.commissioningServer = this.createCommisioningServer(plugin.storageContext, plugin.name);
724
855
  this.log.debug(`Adding device ${dev}${registeredDevice.device.name}${db} to commissioning server for plugin ${plg}${plugin.name}${db}`);
@@ -728,9 +859,11 @@ export class Matterbridge {
728
859
  await this.matterServer.addCommissioningServer(plugin.commissioningServer, { uniqueStorageKey: plugin.name });
729
860
  }
730
861
  if (plugin.type === 'DynamicPlatform') {
731
- plugin.storageContext = this.createCommissioningServerContext(
732
- // Generate serialNumber and uniqueId
733
- plugin.name, 'Matterbridge Dynamic Platform', DeviceTypes.AGGREGATOR.code, 0xfff1, 'Matterbridge', 0x8000, 'Dynamic Platform');
862
+ plugin.storageContext = this.createCommissioningServerContext(plugin.name, 'Matterbridge Dynamic Platform', DeviceTypes.AGGREGATOR.code, 0xfff1, 'Matterbridge', 0x8000, 'Dynamic Platform');
863
+ if (!plugin.storageContext) {
864
+ this.log.error(`Error creating storage context for plugin ${plg}${plugin.name}${er}`);
865
+ return;
866
+ }
734
867
  plugin.commissioningServer = this.createCommisioningServer(plugin.storageContext, plugin.name);
735
868
  this.log.debug(`Creating aggregator for plugin ${plg}${plugin.name}${db}`);
736
869
  plugin.aggregator = this.createMatterAggregator(plugin.storageContext); // Generate serialNumber and uniqueId
@@ -744,19 +877,20 @@ export class Matterbridge {
744
877
  });
745
878
  // Start the interval to check if all plugins are loaded and started and so start the matter server
746
879
  // TODO set a counter or a timeout
747
- this.log.info('**Starting start matter interval...');
880
+ this.log.debug('*Starting start matter interval...');
748
881
  const startMatterInterval = setInterval(async () => {
749
882
  let allStarted = true;
750
883
  this.registeredPlugins.forEach((plugin) => {
751
884
  if (!plugin.enabled)
752
885
  return;
753
- this.log.info(`**Waiting in start matter server interval for plugin ${plg}${plugin.name}${db} to load (${plugin.loaded}) and start (${plugin.started}) ...`);
754
886
  if (plugin.enabled && (!plugin.loaded || !plugin.started))
755
887
  allStarted = false;
888
+ if (!allStarted)
889
+ this.log.info(`**Waiting in start matter server interval for plugin ${plg}${plugin.name}${db} to load (${plugin.loaded}) and start (${plugin.started}) ...`);
756
890
  });
757
891
  if (!allStarted)
758
892
  return;
759
- this.log.info('**Starting matter server in start matter server interval...');
893
+ this.log.info('Starting matter server');
760
894
  // Setting reachability to true
761
895
  this.registeredPlugins.forEach((plugin) => {
762
896
  if (!plugin.enabled)
@@ -783,6 +917,11 @@ export class Matterbridge {
783
917
  }
784
918
  }
785
919
  async startMatterServer() {
920
+ if (!this.matterServer) {
921
+ this.log.error('No matter server initialized');
922
+ await this.cleanup('No matter server initialized');
923
+ return;
924
+ }
786
925
  this.log.debug('Starting matter server');
787
926
  await this.matterServer.start();
788
927
  this.log.debug('Started matter server');
@@ -822,6 +961,10 @@ export class Matterbridge {
822
961
  * @returns The storage context for the commissioning server.
823
962
  */
824
963
  createCommissioningServerContext(pluginName, deviceName, deviceType, vendorId, vendorName, productId, productName, serialNumber, uniqueId, softwareVersion, softwareVersionString, hardwareVersion, hardwareVersionString) {
964
+ if (!this.storageManager) {
965
+ this.log.error('No storage manager initialized');
966
+ process.exit(1);
967
+ }
825
968
  this.log.debug(`Creating commissioning server storage context for ${plg}${pluginName}${db}`);
826
969
  const random = 'CS' + CryptoNode.getRandomData(8).toHex();
827
970
  const storageContext = this.storageManager.createContext(pluginName);
@@ -862,9 +1005,15 @@ export class Matterbridge {
862
1005
  storageContext.set('manualPairingCode', manualPairingCode);
863
1006
  const QrCode = new QrCodeSchema();
864
1007
  this.log.info(`Pairing code:\n\n${QrCode.encode(qrPairingCode)}\nManual pairing code: ${manualPairingCode}\n`);
1008
+ if (this.bridgeMode === 'bridge') {
1009
+ await this.nodeContext?.set('qrPairingCode', qrPairingCode);
1010
+ await this.nodeContext?.set('manualPairingCode', manualPairingCode);
1011
+ }
865
1012
  if (this.bridgeMode === 'childbridge') {
866
1013
  const plugin = this.findPlugin(pluginName);
867
1014
  if (plugin) {
1015
+ await plugin.nodeContext?.set('qrPairingCode', qrPairingCode);
1016
+ await plugin.nodeContext?.set('manualPairingCode', manualPairingCode);
868
1017
  await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
869
1018
  plugin.paired = false;
870
1019
  }
@@ -872,9 +1021,19 @@ export class Matterbridge {
872
1021
  }
873
1022
  else {
874
1023
  this.log.info(`***The commissioning server for ${plg}${pluginName}${nf} is already commissioned. Waiting for controllers to connect ...`);
1024
+ if (this.bridgeMode === 'bridge') {
1025
+ const qrPairingCode = storageContext.get('qrPairingCode', '');
1026
+ const manualPairingCode = storageContext.get('manualPairingCode', '');
1027
+ await this.nodeContext?.set('qrPairingCode', qrPairingCode);
1028
+ await this.nodeContext?.set('manualPairingCode', manualPairingCode);
1029
+ }
875
1030
  if (this.bridgeMode === 'childbridge') {
876
1031
  const plugin = this.findPlugin(pluginName);
877
- if (plugin) {
1032
+ if (plugin && plugin.storageContext && plugin.nodeContext) {
1033
+ plugin.qrPairingCode = plugin.storageContext.get('qrPairingCode', '');
1034
+ plugin.manualPairingCode = plugin.storageContext.get('manualPairingCode', '');
1035
+ await plugin.nodeContext.set('qrPairingCode', plugin.qrPairingCode);
1036
+ await plugin.nodeContext.set('manualPairingCode', plugin.manualPairingCode);
878
1037
  await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
879
1038
  plugin.paired = true;
880
1039
  }
@@ -960,7 +1119,7 @@ export class Matterbridge {
960
1119
  for (const plugin of this.registeredPlugins) {
961
1120
  if (!plugin.enabled)
962
1121
  continue;
963
- this.startPlugin(plugin, 'Matterbridge is commissioned and controllers are connected', true); // No await do it asyncronously
1122
+ this.startPlugin(plugin, 'Matterbridge is commissioned and controllers are connected', true); // No await do it asyncronously with also configurePlugin
964
1123
  //this.configurePlugin(plugin); // No await do it asyncronously
965
1124
  }
966
1125
  Logger.defaultLogLevel = this.debugEnabled ? Level.DEBUG : Level.INFO;
@@ -968,7 +1127,7 @@ export class Matterbridge {
968
1127
  if (this.bridgeMode === 'childbridge') {
969
1128
  //Logger.defaultLogLevel = Level.INFO;
970
1129
  const plugin = this.findPlugin(name);
971
- if (plugin && plugin.type === 'DynamicPlatform') {
1130
+ if (plugin && plugin.type === 'DynamicPlatform' && plugin.configured !== true) {
972
1131
  for (const registeredDevice of this.registeredDevices) {
973
1132
  if (registeredDevice.plugin === name) {
974
1133
  this.log.info(`Adding bridged device ${dev}${registeredDevice.device.name}${nf} to aggregator for plugin ${plg}${plugin.name}${db}`);
@@ -985,7 +1144,7 @@ export class Matterbridge {
985
1144
  }
986
1145
  }
987
1146
  for (const plugin of this.registeredPlugins) {
988
- if (plugin.name === name && plugin.platform) {
1147
+ if (plugin.name === name && plugin.platform && plugin.configured !== true) {
989
1148
  this.configurePlugin(plugin); // No await do it asyncronously
990
1149
  }
991
1150
  }
@@ -1015,6 +1174,7 @@ export class Matterbridge {
1015
1174
  createMatterServer(storageManager) {
1016
1175
  this.log.debug('Creating matter server');
1017
1176
  this.matterServer = new MatterServer(storageManager, { mdnsAnnounceInterface: undefined });
1177
+ this.log.debug('Created matter server');
1018
1178
  }
1019
1179
  /**
1020
1180
  * Creates a Matter Aggregator.
@@ -1062,6 +1222,27 @@ export class Matterbridge {
1062
1222
  this.log.debug('Stopping matter server');
1063
1223
  await this.matterServer?.close();
1064
1224
  this.log.debug('Matter server closed');
1225
+ this.commissioningController = undefined;
1226
+ this.commissioningServer = undefined;
1227
+ this.matterAggregator = undefined;
1228
+ this.matterServer = undefined;
1229
+ }
1230
+ /**
1231
+ * Retrieves the latest version of a package from the npm registry.
1232
+ * @param packageName - The name of the package.
1233
+ * @returns A Promise that resolves to the latest version of the package.
1234
+ */
1235
+ async getLatestVersion(packageName) {
1236
+ return new Promise((resolve, reject) => {
1237
+ exec(`npm view ${packageName} version`, (error, stdout) => {
1238
+ if (error) {
1239
+ reject(error);
1240
+ }
1241
+ else {
1242
+ resolve(stdout.trim());
1243
+ }
1244
+ });
1245
+ });
1065
1246
  }
1066
1247
  /**
1067
1248
  * Logs the node and system information.
@@ -1122,19 +1303,64 @@ export class Matterbridge {
1122
1303
  const currentFileDirectory = path.dirname(fileURLToPath(import.meta.url));
1123
1304
  this.rootDirectory = path.resolve(currentFileDirectory, '../');
1124
1305
  this.log.debug(`Root Directory: ${this.rootDirectory}`);
1306
+ // Global node_modules directory
1307
+ this.globalModulesDir = execSync('npm root -g').toString().trim();
1308
+ this.log.debug(`Global node_modules Directory: ${this.globalModulesDir}`);
1125
1309
  // Create the data directory .matterbridge in the home directory
1126
1310
  this.matterbridgeDirectory = path.join(this.homeDirectory, '.matterbridge');
1127
1311
  try {
1128
1312
  await fs.access(this.matterbridgeDirectory);
1129
1313
  }
1130
1314
  catch (err) {
1131
- await fs.mkdir(this.matterbridgeDirectory);
1315
+ if (err instanceof Error) {
1316
+ const nodeErr = err;
1317
+ if (nodeErr.code === 'ENOENT') {
1318
+ try {
1319
+ await fs.mkdir(this.matterbridgeDirectory, { recursive: true });
1320
+ this.log.info(`Created Matterbridge Directory: ${this.matterbridgeDirectory}`);
1321
+ }
1322
+ catch (err) {
1323
+ this.log.error(`Error creating directory: ${err}`);
1324
+ }
1325
+ }
1326
+ else {
1327
+ this.log.error(`Error accessing directory: ${err}`);
1328
+ }
1329
+ }
1132
1330
  }
1133
1331
  this.log.debug(`Matterbridge Directory: ${this.matterbridgeDirectory}`);
1332
+ // Create the data directory .matterbridge in the home directory
1333
+ this.matterbridgePluginDirectory = path.join(this.homeDirectory, 'Matterbridge');
1334
+ try {
1335
+ await fs.access(this.matterbridgePluginDirectory);
1336
+ }
1337
+ catch (err) {
1338
+ if (err instanceof Error) {
1339
+ const nodeErr = err;
1340
+ if (nodeErr.code === 'ENOENT') {
1341
+ try {
1342
+ await fs.mkdir(this.matterbridgePluginDirectory, { recursive: true });
1343
+ this.log.info(`Created Matterbridge Plugin Directory: ${this.matterbridgePluginDirectory}`);
1344
+ }
1345
+ catch (err) {
1346
+ this.log.error(`Error creating directory: ${err}`);
1347
+ }
1348
+ }
1349
+ else {
1350
+ this.log.error(`Error accessing directory: ${err}`);
1351
+ }
1352
+ }
1353
+ }
1354
+ this.log.debug(`Matterbridge Plugin Directory: ${this.matterbridgePluginDirectory}`);
1134
1355
  // Matterbridge version
1135
1356
  const packageJson = JSON.parse(await fs.readFile(path.join(this.rootDirectory, 'package.json'), 'utf-8'));
1136
1357
  this.matterbridgeVersion = packageJson.version;
1137
1358
  this.log.debug(`Matterbridge Version: ${this.matterbridgeVersion}`);
1359
+ this.matterbridgeLatestVersion = await this.getLatestVersion('matterbridge');
1360
+ this.log.debug(`Matterbridge Latest Version: ${this.matterbridgeLatestVersion}`);
1361
+ if (this.matterbridgeVersion !== this.matterbridgeLatestVersion) {
1362
+ this.log.warn(`Matterbridge is out of date. Current version: ${this.matterbridgeVersion}, Latest version: ${this.matterbridgeLatestVersion}`);
1363
+ }
1138
1364
  // Current working directory
1139
1365
  const currentDir = process.cwd();
1140
1366
  this.log.debug(`Current Working Directory: ${currentDir}`);
@@ -1157,6 +1383,8 @@ export class Matterbridge {
1157
1383
  paired: plugin.paired,
1158
1384
  connected: plugin.connected,
1159
1385
  registeredDevices: plugin.registeredDevices,
1386
+ qrPairingCode: plugin.qrPairingCode,
1387
+ manualPairingCode: plugin.manualPairingCode,
1160
1388
  }));
1161
1389
  return baseRegisteredPlugins;
1162
1390
  }
@@ -1184,14 +1412,16 @@ export class Matterbridge {
1184
1412
  */
1185
1413
  async initializeFrontend(port = 3000) {
1186
1414
  this.log.debug(`Initializing the frontend on port ${YELLOW}${port}${db} static ${UNDERLINE}${path.join(this.rootDirectory, 'frontend/build')}${UNDERLINEOFF}${rs}`);
1187
- this.app = express();
1415
+ this.expressApp = express();
1188
1416
  // Serve React build directory
1189
- this.app.use(express.static(path.join(this.rootDirectory, 'frontend/build')));
1417
+ this.expressApp.use(express.static(path.join(this.rootDirectory, 'frontend/build')));
1190
1418
  // Endpoint to provide QR pairing code
1191
- this.app.get('/api/qr-code', (req, res) => {
1419
+ this.expressApp.get('/api/qr-code', (req, res) => {
1192
1420
  this.log.debug('The frontend sent /api/qr-code');
1193
- if (!this.matterbridgeContext)
1194
- this.matterbridgeContext = this.createCommissioningServerContext('Matterbridge', 'Matterbridge', DeviceTypes.AGGREGATOR.code, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge aggregator');
1421
+ if (!this.matterbridgeContext) {
1422
+ res.json([]);
1423
+ return;
1424
+ }
1195
1425
  try {
1196
1426
  const qrData = { qrPairingCode: this.matterbridgeContext.get('qrPairingCode'), manualPairingCode: this.matterbridgeContext.get('manualPairingCode') };
1197
1427
  res.json(qrData);
@@ -1203,17 +1433,17 @@ export class Matterbridge {
1203
1433
  }
1204
1434
  });
1205
1435
  // Endpoint to provide system information
1206
- this.app.get('/api/system-info', (req, res) => {
1436
+ this.expressApp.get('/api/system-info', (req, res) => {
1207
1437
  this.log.debug('The frontend sent /api/system-info');
1208
1438
  res.json(this.systemInformation);
1209
1439
  });
1210
1440
  // Endpoint to provide plugins
1211
- this.app.get('/api/plugins', (req, res) => {
1441
+ this.expressApp.get('/api/plugins', (req, res) => {
1212
1442
  this.log.debug('The frontend sent /api/plugins');
1213
1443
  res.json(this.getBaseRegisteredPlugins());
1214
1444
  });
1215
1445
  // Endpoint to provide devices
1216
- this.app.get('/api/devices', (req, res) => {
1446
+ this.expressApp.get('/api/devices', (req, res) => {
1217
1447
  this.log.debug('The frontend sent /api/devices');
1218
1448
  const data = [];
1219
1449
  this.registeredDevices.forEach((registeredDevice) => {
@@ -1240,7 +1470,7 @@ export class Matterbridge {
1240
1470
  res.json(data);
1241
1471
  });
1242
1472
  // Endpoint to provide the cluster servers of the devices
1243
- this.app.get('/api/devices_clusters/:selectedPluginName/:selectedDeviceEndpoint', (req, res) => {
1473
+ this.expressApp.get('/api/devices_clusters/:selectedPluginName/:selectedDeviceEndpoint', (req, res) => {
1244
1474
  const selectedPluginName = req.params.selectedPluginName;
1245
1475
  const selectedDeviceEndpoint = parseInt(req.params.selectedDeviceEndpoint, 10);
1246
1476
  this.log.debug(`The frontend sent /api/devices_clusters plugin:${selectedPluginName} endpoint:${selectedDeviceEndpoint}`);
@@ -1282,12 +1512,49 @@ export class Matterbridge {
1282
1512
  });
1283
1513
  res.json(data);
1284
1514
  });
1515
+ // Endpoint to receive commands
1516
+ this.expressApp.post('/api/command/:command/:param', (req, res) => {
1517
+ const command = req.params.command;
1518
+ const param = req.params.param;
1519
+ this.log.debug(`The frontend sent /api/command/${command}/${param}`);
1520
+ if (!command) {
1521
+ res.status(400).json({ error: 'No command provided' });
1522
+ return;
1523
+ }
1524
+ this.log.info(`***Received command: ${command}:${param}`);
1525
+ // Handle the command debugLevel
1526
+ if (command === 'setloglevel') {
1527
+ if (param === 'Debug') {
1528
+ this.log.setLogDebug(true);
1529
+ this.debugEnabled = true;
1530
+ Logger.defaultLogLevel = Level.DEBUG;
1531
+ }
1532
+ else if (param === 'Info') {
1533
+ this.log.setLogDebug(false);
1534
+ this.debugEnabled = false;
1535
+ Logger.defaultLogLevel = Level.INFO;
1536
+ }
1537
+ else if (param === 'Warn') {
1538
+ this.log.setLogDebug(false);
1539
+ this.debugEnabled = false;
1540
+ Logger.defaultLogLevel = Level.WARN;
1541
+ }
1542
+ this.registeredPlugins.forEach((plugin) => {
1543
+ plugin.platform?.log.setLogDebug(this.debugEnabled);
1544
+ });
1545
+ }
1546
+ // Handle the command debugLevel
1547
+ if (command === 'restart') {
1548
+ this.restartProcess();
1549
+ }
1550
+ res.json({ message: 'Command received' });
1551
+ });
1285
1552
  // Fallback for routing
1286
- this.app.get('*', (req, res) => {
1553
+ this.expressApp.get('*', (req, res) => {
1287
1554
  this.log.warn('The frontend sent *', req.url);
1288
1555
  res.sendFile(path.join(this.rootDirectory, 'frontend/build/index.html'));
1289
1556
  });
1290
- this.app.listen(port, () => {
1557
+ this.expressServer = this.expressApp.listen(port, () => {
1291
1558
  this.log.info(`The frontend is running on ${UNDERLINE}http://localhost:${port}${UNDERLINEOFF}${rs}`);
1292
1559
  });
1293
1560
  this.log.debug(`Frontend initialized on port ${YELLOW}${port}${db} static ${UNDERLINE}${path.join(this.rootDirectory, 'frontend/build')}${UNDERLINEOFF}${rs}`);
@@ -1335,7 +1602,25 @@ export class Matterbridge {
1335
1602
  }
1336
1603
  /*
1337
1604
  TO IMPLEMENT
1605
+
1606
+ import { spawn } from 'child_process';
1607
+
1608
+ function restartProcess() {
1609
+ // Spawn a new process
1610
+ const newProcess = spawn(process.argv[0], process.argv.slice(1), {
1611
+ detached: true,
1612
+ stdio: 'inherit',
1613
+ });
1614
+
1615
+ // Unreference the new process so that the current process can exit
1616
+ newProcess.unref();
1617
+
1618
+ // Exit the current process
1619
+ process.exit();
1620
+ }
1621
+
1338
1622
  import * as WebSocket from 'ws';
1623
+ const globalModulesDir = require('global-modules');
1339
1624
 
1340
1625
  const wss = new WebSocket.Server({ port: 8080 });
1341
1626