matterbridge 3.0.8-dev-20250626-50aa686 → 3.1.0-dev-20250627-2b5adba

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,7 +8,7 @@ If you like this project and find it useful, please consider giving it a star on
8
8
  <img src="bmc-button.svg" alt="Buy me a coffee" width="120">
9
9
  </a>
10
10
 
11
- ## [3.0.8] - 2025-06-??
11
+ ## [3.1.0] - 2025-06-27
12
12
 
13
13
  ### Breaking Changes
14
14
 
@@ -23,6 +23,8 @@ If you like this project and find it useful, please consider giving it a star on
23
23
  - [JSDoc]: Added missing JSDoc comments, including `@param` and `@returns` tags.
24
24
  - [MatterbridgeEndpoint]: Add MatterbridgeEndpoint mode='server'. It allows to advertise a single device like an autonomous device with its server node to be paired.
25
25
  - [MatterbridgeEndpoint]: Add MatterbridgeEndpoint mode='matter'. It allows to add a single device to the Matterbridge server node next to the aggregator. The device is not bridged.
26
+ - [storage]: Improved error handling of corrupted storage.
27
+ - [test]: Improved test units on Matterbridge classes.
26
28
 
27
29
  ### Changed
28
30
 
@@ -30,8 +32,7 @@ If you like this project and find it useful, please consider giving it a star on
30
32
  - [package]: Updated dependencies.
31
33
  - [storage]: Bumped `node-storage-manager` to 2.0.0.
32
34
  - [logger]: Bumped `node-ansi-logger` to 3.1.1.
33
- - [matter.js]: Updated to 0.15.0-alpha.0-20250625-c7634df96.
34
- - [matter.js]: Updated to 0.15.0-alpha.0-20250626-fc3a84ce9.
35
+ - [matter.js]: Updated to 0.15.0-alpha.0-20250625-c7634df96, 0.15.0-alpha.0-20250626-fc3a84ce9, and bumped to 0.15.0.
35
36
 
36
37
  ### Fixed
37
38
 
package/README-DEV.md CHANGED
@@ -34,6 +34,10 @@ Using a Dev Container provides a fully isolated, reproducible, and pre-configure
34
34
 
35
35
  For improved efficiency, the setup uses named Docker volumes for `node_modules`. This means dependencies are installed only once and persist across container rebuilds, making installs and rebuilds much faster than with bind mounts or ephemeral volumes.
36
36
 
37
+ To start the Dev Container, simply open the project folder in [Visual Studio Code](https://code.visualstudio.com/) and, if prompted, click "Reopen in Container". Alternatively, use the Command Palette (`Ctrl+Shift+P` or `Cmd+Shift+P`), search for "Dev Containers: Reopen in Container", and select it. VS Code will automatically build and start the containerized environment for you.
38
+
39
+ > **Note:** The first time you use the Dev Container, it may take a while to download all the required Docker images and set up the environment. Subsequent starts will be much faster.
40
+
37
41
  Since Dev Container doesn't run in network mode 'host', it is not possible to pair Mattebridge running inside the Dev Container.
38
42
 
39
43
  ## Guidelines on imports/exports
@@ -247,7 +251,7 @@ It can be useful to call this method from onShutdown() if you don't want to keep
247
251
 
248
252
  ## MatterbridgeEndpoint api
249
253
 
250
- You create a device with a new instance of MatterbridgeEndpoint(definition: DeviceTypeDefinition | AtLeastOne<DeviceTypeDefinition>, options: MatterbridgeEndpointOptions = {}, debug: boolean = false).
254
+ You create a Matter device with a new instance of MatterbridgeEndpoint(definition: DeviceTypeDefinition | AtLeastOne<DeviceTypeDefinition>, options: MatterbridgeEndpointOptions = {}, debug: boolean = false).
251
255
 
252
256
  - @param {DeviceTypeDefinition | AtLeastOne<DeviceTypeDefinition>} definition - The DeviceTypeDefinition(s) of the endpoint.
253
257
  - @param {MatterbridgeEndpointOptions} [options] - The options for the device.
@@ -3,11 +3,9 @@ import { dev } from './matterbridgeTypes.js';
3
3
  export class DeviceManager {
4
4
  _devices = new Map();
5
5
  matterbridge;
6
- nodeContext;
7
6
  log;
8
- constructor(matterbridge, nodeContext) {
7
+ constructor(matterbridge) {
9
8
  this.matterbridge = matterbridge;
10
- this.nodeContext = nodeContext;
11
9
  this.log = new AnsiLogger({ logName: 'DeviceManager', logTimestampFormat: 4, logLevel: matterbridge.log.logLevel });
12
10
  this.log.debug('Matterbridge device manager starting...');
13
11
  }
@@ -3,7 +3,7 @@ import path from 'node:path';
3
3
  import { promises as fs } from 'node:fs';
4
4
  import EventEmitter from 'node:events';
5
5
  import { inspect } from 'node:util';
6
- import { AnsiLogger, UNDERLINE, UNDERLINEOFF, YELLOW, db, debugStringify, BRIGHT, RESET, er, nf, rs, wr, RED, GREEN, zb, CYAN } from 'node-ansi-logger';
6
+ import { AnsiLogger, UNDERLINE, UNDERLINEOFF, db, debugStringify, BRIGHT, RESET, er, nf, rs, wr, RED, GREEN, zb, CYAN } from 'node-ansi-logger';
7
7
  import { NodeStorageManager } from 'node-persist-manager';
8
8
  import { DeviceTypeId, Endpoint, Logger, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, VendorId, StorageService, Environment, ServerNode, UINT32_MAX, UINT16_MAX, Crypto, } from '@matter/main';
9
9
  import { DeviceCommissioner, FabricAction, MdnsService, PaseClient } from '@matter/main/protocol';
@@ -89,26 +89,26 @@ export class Matterbridge extends EventEmitter {
89
89
  matterbridgeVersion = '';
90
90
  matterbridgeLatestVersion = '';
91
91
  matterbridgeDevVersion = '';
92
- matterbridgeQrPairingCode = undefined;
93
- matterbridgeManualPairingCode = undefined;
94
- matterbridgeFabricInformations = undefined;
95
- matterbridgeSessionInformations = undefined;
96
- matterbridgePaired = undefined;
92
+ matterbridgeQrPairingCode;
93
+ matterbridgeManualPairingCode;
94
+ matterbridgeFabricInformations;
95
+ matterbridgeSessionInformations;
96
+ matterbridgePaired;
97
97
  bridgeMode = '';
98
98
  restartMode = '';
99
99
  profile = getParameter('profile');
100
100
  shutdown = false;
101
101
  edge = true;
102
102
  failCountLimit = hasParameter('shelly') ? 600 : 120;
103
- log;
103
+ log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: 4, logLevel: hasParameter('debug') ? "debug" : "info" });
104
104
  matterbrideLoggerFile = 'matterbridge' + (getParameter('profile') ? '.' + getParameter('profile') : '') + '.log';
105
105
  matterLoggerFile = 'matter' + (getParameter('profile') ? '.' + getParameter('profile') : '') + '.log';
106
106
  plugins;
107
107
  devices;
108
108
  frontend = new Frontend(this);
109
+ nodeStorageName = 'storage' + (getParameter('profile') ? '.' + getParameter('profile') : '');
109
110
  nodeStorage;
110
111
  nodeContext;
111
- nodeStorageName = 'storage' + (getParameter('profile') ? '.' + getParameter('profile') : '');
112
112
  hasCleanupStarted = false;
113
113
  initialized = false;
114
114
  execRunningCount = 0;
@@ -145,12 +145,6 @@ export class Matterbridge extends EventEmitter {
145
145
  constructor() {
146
146
  super();
147
147
  }
148
- emit(eventName, ...args) {
149
- return super.emit(eventName, ...args);
150
- }
151
- on(eventName, listener) {
152
- return super.on(eventName, listener);
153
- }
154
148
  getDevices() {
155
149
  return this.devices.array();
156
150
  }
@@ -191,7 +185,7 @@ export class Matterbridge extends EventEmitter {
191
185
  }
192
186
  return Matterbridge.instance;
193
187
  }
194
- async destroyInstance() {
188
+ async destroyInstance(timeout = 1000, pause = 500) {
195
189
  this.log.info(`Destroy instance...`);
196
190
  const servers = [];
197
191
  if (this.bridgeMode === 'bridge') {
@@ -211,7 +205,10 @@ export class Matterbridge extends EventEmitter {
211
205
  }
212
206
  }
213
207
  await Promise.resolve();
214
- await this.cleanup('destroying instance...', false);
208
+ await new Promise((resolve) => {
209
+ setTimeout(resolve, pause);
210
+ });
211
+ await this.cleanup('destroying instance...', false, timeout);
215
212
  this.log.info(`Dispose ${servers.length} MdnsService...`);
216
213
  for (const server of servers) {
217
214
  await server.env.get(MdnsService)[Symbol.asyncDispose]();
@@ -219,12 +216,11 @@ export class Matterbridge extends EventEmitter {
219
216
  }
220
217
  await Promise.resolve();
221
218
  await new Promise((resolve) => {
222
- setTimeout(resolve, 500);
219
+ setTimeout(resolve, pause);
223
220
  });
224
221
  }
225
222
  async initialize() {
226
223
  this.emit('initialize_started');
227
- this.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: 4, logLevel: hasParameter('debug') ? "debug" : "info" });
228
224
  if (hasParameter('service'))
229
225
  this.restartMode = 'service';
230
226
  if (hasParameter('docker'))
@@ -280,16 +276,15 @@ export class Matterbridge extends EventEmitter {
280
276
  catch (error) {
281
277
  this.log.error(`Error creating node storage manager and context: ${error instanceof Error ? error.message : error}`);
282
278
  if (hasParameter('norestore')) {
283
- this.log.fatal(`The matterbridge node storage is corrupted. Parameter -norestore found: exiting...`);
284
- await this.cleanup('Fatal error creating node storage manager and context for matterbridge');
285
- return;
279
+ this.log.fatal(`The matterbridge storage is corrupted. Found -norestore parameter: exiting...`);
280
+ }
281
+ else {
282
+ this.log.notice(`The matterbridge storage is corrupted. Restoring it with backup...`);
283
+ await copyDirectory(path.join(this.matterbridgeDirectory, this.nodeStorageName + '.backup'), path.join(this.matterbridgeDirectory, this.nodeStorageName));
284
+ this.log.notice(`The matterbridge storage has been restored with backup`);
286
285
  }
287
- this.log.notice(`The matterbridge storage is corrupted. Restoring it with backup...`);
288
- await copyDirectory(path.join(this.matterbridgeDirectory, this.nodeStorageName + '.backup'), path.join(this.matterbridgeDirectory, this.nodeStorageName));
289
- this.log.notice(`The matterbridge storage has been restored with backup`);
290
286
  }
291
287
  if (!this.nodeStorage || !this.nodeContext) {
292
- this.log.fatal('Fatal error creating node storage manager and context for matterbridge');
293
288
  throw new Error('Fatal error creating node storage manager and context for matterbridge');
294
289
  }
295
290
  this.port = getIntParameter('port') ?? (await this.nodeContext.get('matterport', 5540)) ?? 5540;
@@ -305,7 +300,7 @@ export class Matterbridge extends EventEmitter {
305
300
  if (isValidString(pairingFileJson.vendorName, 3))
306
301
  this.aggregatorVendorName = pairingFileJson.vendorName;
307
302
  if (isValidNumber(pairingFileJson.productId))
308
- this.aggregatorProductId = VendorId(pairingFileJson.productId);
303
+ this.aggregatorProductId = pairingFileJson.productId;
309
304
  if (isValidString(pairingFileJson.productName, 3))
310
305
  this.aggregatorProductName = pairingFileJson.productName;
311
306
  if (isValidNumber(pairingFileJson.passcode) && isValidNumber(pairingFileJson.discriminator)) {
@@ -313,19 +308,6 @@ export class Matterbridge extends EventEmitter {
313
308
  this.discriminator = pairingFileJson.discriminator;
314
309
  this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using passcode ${CYAN}${this.passcode}${nf} and discriminator ${CYAN}${this.discriminator}${nf} from pairing file.`);
315
310
  }
316
- if (pairingFileJson.privateKey && pairingFileJson.certificate && pairingFileJson.intermediateCertificate && pairingFileJson.declaration) {
317
- const hexStringToUint8Array = (hexString) => {
318
- const matches = hexString.match(/.{1,2}/g);
319
- return matches ? new Uint8Array(matches.map((byte) => parseInt(byte, 16))) : new Uint8Array();
320
- };
321
- this.certification = {
322
- privateKey: hexStringToUint8Array(pairingFileJson.privateKey),
323
- certificate: hexStringToUint8Array(pairingFileJson.certificate),
324
- intermediateCertificate: hexStringToUint8Array(pairingFileJson.intermediateCertificate),
325
- declaration: hexStringToUint8Array(pairingFileJson.declaration),
326
- };
327
- this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using privateKey, certificate, intermediateCertificate and declaration from pairing file.`);
328
- }
329
311
  }
330
312
  catch (error) {
331
313
  this.log.debug(`Pairing file ${CYAN}${pairingFilePath}${db} not found: ${error instanceof Error ? error.message : error}`);
@@ -433,12 +415,12 @@ export class Matterbridge extends EventEmitter {
433
415
  }
434
416
  if (this.mdnsInterface) {
435
417
  if (!availableInterfaces.includes(this.mdnsInterface)) {
436
- this.log.error(`Invalid mdnsInterface: ${this.mdnsInterface}. Available interfaces are: ${availableInterfaces.join(', ')}. Using all available interfaces.`);
418
+ this.log.error(`Invalid mdnsinterface: ${this.mdnsInterface}. Available interfaces are: ${availableInterfaces.join(', ')}. Using all available interfaces.`);
437
419
  this.mdnsInterface = undefined;
438
420
  await this.nodeContext.remove('mattermdnsinterface');
439
421
  }
440
422
  else {
441
- this.log.info(`Using mdnsInterface ${CYAN}${this.mdnsInterface}${nf} for the Matter MdnsBroadcaster.`);
423
+ this.log.info(`Using mdnsinterface ${CYAN}${this.mdnsInterface}${nf} for the Matter MdnsBroadcaster.`);
442
424
  }
443
425
  }
444
426
  if (this.mdnsInterface)
@@ -505,7 +487,7 @@ export class Matterbridge extends EventEmitter {
505
487
  this.plugins = new PluginManager(this);
506
488
  await this.plugins.loadFromStorage();
507
489
  this.plugins.logLevel = this.log.logLevel;
508
- this.devices = new DeviceManager(this, this.nodeContext);
490
+ this.devices = new DeviceManager(this);
509
491
  this.devices.logLevel = this.log.logLevel;
510
492
  for (const plugin of this.plugins) {
511
493
  const packageJson = await this.plugins.parse(plugin);
@@ -609,18 +591,6 @@ export class Matterbridge extends EventEmitter {
609
591
  }
610
592
  index++;
611
593
  }
612
- const serializedRegisteredDevices = await this.nodeContext?.get('devices', []);
613
- this.log.info(`│ Registered devices (${serializedRegisteredDevices?.length})`);
614
- serializedRegisteredDevices?.forEach((device, index) => {
615
- if (index !== serializedRegisteredDevices.length - 1) {
616
- this.log.info(`├─┬─ plugin ${plg}${device.pluginName}${nf} device: ${dev}${device.deviceName}${nf} uniqueId: ${YELLOW}${device.uniqueId}${nf}`);
617
- this.log.info(`│ └─ endpoint ${RED}${device.endpoint}${nf} ${typ}${device.endpointName}${nf} ${debugStringify(device.clusterServersId)}`);
618
- }
619
- else {
620
- this.log.info(`└─┬─ plugin ${plg}${device.pluginName}${nf} device: ${dev}${device.deviceName}${nf} uniqueId: ${YELLOW}${device.uniqueId}${nf}`);
621
- this.log.info(` └─ endpoint ${RED}${device.endpoint}${nf} ${typ}${device.endpointName}${nf} ${debugStringify(device.clusterServersId)}`);
622
- }
623
- });
624
594
  this.shutdown = true;
625
595
  return;
626
596
  }
@@ -1024,7 +994,7 @@ export class Matterbridge extends EventEmitter {
1024
994
  async shutdownProcessAndFactoryReset() {
1025
995
  await this.cleanup('shutting down with factory reset...', false);
1026
996
  }
1027
- async cleanup(message, restart = false) {
997
+ async cleanup(message, restart = false, timeout = 1000) {
1028
998
  if (this.initialized && !this.hasCleanupStarted) {
1029
999
  this.emit('cleanup_started');
1030
1000
  this.hasCleanupStarted = true;
@@ -1035,7 +1005,7 @@ export class Matterbridge extends EventEmitter {
1035
1005
  this.log.debug('Start matter interval cleared');
1036
1006
  }
1037
1007
  if (this.checkUpdateTimeout) {
1038
- clearInterval(this.checkUpdateTimeout);
1008
+ clearTimeout(this.checkUpdateTimeout);
1039
1009
  this.checkUpdateTimeout = undefined;
1040
1010
  this.log.debug('Check update timeout cleared');
1041
1011
  }
@@ -1066,7 +1036,7 @@ export class Matterbridge extends EventEmitter {
1066
1036
  }
1067
1037
  this.log.notice(`Stopping matter server nodes in ${this.bridgeMode} mode...`);
1068
1038
  this.log.debug('Waiting for the MessageExchange to finish...');
1069
- await new Promise((resolve) => setTimeout(resolve, 1000));
1039
+ await new Promise((resolve) => setTimeout(resolve, timeout));
1070
1040
  if (this.bridgeMode === 'bridge') {
1071
1041
  if (this.serverNode) {
1072
1042
  await this.stopServerNode(this.serverNode);
@@ -297,13 +297,14 @@ export class PluginManager extends EventEmitter {
297
297
  }
298
298
  async enable(nameOrPath) {
299
299
  const { promises } = await import('node:fs');
300
- if (!nameOrPath || nameOrPath === '')
300
+ if (!nameOrPath)
301
301
  return null;
302
302
  if (this._plugins.has(nameOrPath)) {
303
303
  const plugin = this._plugins.get(nameOrPath);
304
304
  plugin.enabled = true;
305
305
  this.log.info(`Enabled plugin ${plg}${plugin.name}${nf}`);
306
306
  await this.saveToStorage();
307
+ this.emit('enabled', plugin.name);
307
308
  return plugin;
308
309
  }
309
310
  const packageJsonPath = await this.resolve(nameOrPath);
@@ -331,13 +332,14 @@ export class PluginManager extends EventEmitter {
331
332
  }
332
333
  async disable(nameOrPath) {
333
334
  const { promises } = await import('node:fs');
334
- if (!nameOrPath || nameOrPath === '')
335
+ if (!nameOrPath)
335
336
  return null;
336
337
  if (this._plugins.has(nameOrPath)) {
337
338
  const plugin = this._plugins.get(nameOrPath);
338
339
  plugin.enabled = false;
339
340
  this.log.info(`Disabled plugin ${plg}${plugin.name}${nf}`);
340
341
  await this.saveToStorage();
342
+ this.emit('disabled', plugin.name);
341
343
  return plugin;
342
344
  }
343
345
  const packageJsonPath = await this.resolve(nameOrPath);
@@ -365,13 +367,14 @@ export class PluginManager extends EventEmitter {
365
367
  }
366
368
  async remove(nameOrPath) {
367
369
  const { promises } = await import('node:fs');
368
- if (!nameOrPath || nameOrPath === '')
370
+ if (!nameOrPath)
369
371
  return null;
370
372
  if (this._plugins.has(nameOrPath)) {
371
373
  const plugin = this._plugins.get(nameOrPath);
372
374
  this._plugins.delete(nameOrPath);
373
375
  this.log.info(`Removed plugin ${plg}${plugin.name}${nf}`);
374
376
  await this.saveToStorage();
377
+ this.emit('removed', plugin.name);
375
378
  return plugin;
376
379
  }
377
380
  const packageJsonPath = await this.resolve(nameOrPath);
@@ -399,7 +402,7 @@ export class PluginManager extends EventEmitter {
399
402
  }
400
403
  async add(nameOrPath) {
401
404
  const { promises } = await import('node:fs');
402
- if (!nameOrPath || nameOrPath === '')
405
+ if (!nameOrPath)
403
406
  return null;
404
407
  const packageJsonPath = await this.resolve(nameOrPath);
405
408
  if (!packageJsonPath) {
@@ -456,6 +459,7 @@ export class PluginManager extends EventEmitter {
456
459
  if (versionLine) {
457
460
  const version = versionLine.split('@')[1].trim();
458
461
  this.log.info(`Installed plugin ${plg}${name}@${version}${nf}`);
462
+ this.emit('installed', name, version);
459
463
  resolve(version);
460
464
  }
461
465
  else {
@@ -479,6 +483,7 @@ export class PluginManager extends EventEmitter {
479
483
  else {
480
484
  this.log.info(`Uninstalled plugin ${plg}${name}${nf}`);
481
485
  this.log.debug(`Uninstalled plugin ${plg}${name}${db}: ${stdout}`);
486
+ this.emit('uninstalled', name);
482
487
  resolve(name);
483
488
  }
484
489
  });
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "matterbridge",
3
- "version": "3.0.8-dev-20250626-50aa686",
3
+ "version": "3.1.0-dev-20250627-2b5adba",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "matterbridge",
9
- "version": "3.0.8-dev-20250626-50aa686",
9
+ "version": "3.1.0-dev-20250627-2b5adba",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
- "@matter/main": "0.15.0-alpha.0-20250626-fc3a84ce9",
12
+ "@matter/main": "0.15.0",
13
13
  "archiver": "7.0.1",
14
14
  "express": "5.1.0",
15
15
  "glob": "11.0.3",
@@ -68,86 +68,86 @@
68
68
  }
69
69
  },
70
70
  "node_modules/@matter/general": {
71
- "version": "0.15.0-alpha.0-20250626-fc3a84ce9",
72
- "resolved": "https://registry.npmjs.org/@matter/general/-/general-0.15.0-alpha.0-20250626-fc3a84ce9.tgz",
73
- "integrity": "sha512-fQwZ8NEEdZIiQXNZXHjmNP7X21OzQw424ukSGWlt+GB7CrfZlF4rsiroeKsRQBFO1L+zCE0LYYe+rlM89IqKKg==",
71
+ "version": "0.15.0",
72
+ "resolved": "https://registry.npmjs.org/@matter/general/-/general-0.15.0.tgz",
73
+ "integrity": "sha512-rZC045EkAiWBWt3nUhvjhW6Tx4L/8RLsZMtdo9VEqPytwYIP9JMuLZt01x4+szM0dRx72k1g0e8zaU8CqobGJQ==",
74
74
  "license": "Apache-2.0",
75
75
  "dependencies": {
76
76
  "@noble/curves": "^1.9.2"
77
77
  }
78
78
  },
79
79
  "node_modules/@matter/main": {
80
- "version": "0.15.0-alpha.0-20250626-fc3a84ce9",
81
- "resolved": "https://registry.npmjs.org/@matter/main/-/main-0.15.0-alpha.0-20250626-fc3a84ce9.tgz",
82
- "integrity": "sha512-x9QYTk521l21/ihZTAWz+wE1flyQhYPY/fvM6YPDF6YTfJgd790FT9Ryqk5qVgrui9eVGF3X6i7DrdFkzpTLVw==",
80
+ "version": "0.15.0",
81
+ "resolved": "https://registry.npmjs.org/@matter/main/-/main-0.15.0.tgz",
82
+ "integrity": "sha512-+mtEs4FRaonPMae38BLIaG6lYbszPfYj2gROAhIHYUuRb/Qf/dk05ZANcCyHuY0+ewi9L51q/nhReSHBQpt3vw==",
83
83
  "license": "Apache-2.0",
84
84
  "dependencies": {
85
- "@matter/general": "0.15.0-alpha.0-20250626-fc3a84ce9",
86
- "@matter/model": "0.15.0-alpha.0-20250626-fc3a84ce9",
87
- "@matter/node": "0.15.0-alpha.0-20250626-fc3a84ce9",
88
- "@matter/protocol": "0.15.0-alpha.0-20250626-fc3a84ce9",
89
- "@matter/types": "0.15.0-alpha.0-20250626-fc3a84ce9"
85
+ "@matter/general": "0.15.0",
86
+ "@matter/model": "0.15.0",
87
+ "@matter/node": "0.15.0",
88
+ "@matter/protocol": "0.15.0",
89
+ "@matter/types": "0.15.0"
90
90
  },
91
91
  "optionalDependencies": {
92
- "@matter/nodejs": "0.15.0-alpha.0-20250626-fc3a84ce9"
92
+ "@matter/nodejs": "0.15.0"
93
93
  }
94
94
  },
95
95
  "node_modules/@matter/model": {
96
- "version": "0.15.0-alpha.0-20250626-fc3a84ce9",
97
- "resolved": "https://registry.npmjs.org/@matter/model/-/model-0.15.0-alpha.0-20250626-fc3a84ce9.tgz",
98
- "integrity": "sha512-MsPhBLOMf+kpzYIHYPp8IO6lf6sbTijzcoCUAq5FLCycuKiqgSQap6mzQKKMMS81FIs/prM5E4dpQQz9aGF6kA==",
96
+ "version": "0.15.0",
97
+ "resolved": "https://registry.npmjs.org/@matter/model/-/model-0.15.0.tgz",
98
+ "integrity": "sha512-H0gxGN6b0bdV9UmozbC36BLeo949DcV1F84xRvRZvwaOzNEAruP4SSG9Ku0ZWU7cNlRX9+pinmWUAxm8CLomaw==",
99
99
  "license": "Apache-2.0",
100
100
  "dependencies": {
101
- "@matter/general": "0.15.0-alpha.0-20250626-fc3a84ce9"
101
+ "@matter/general": "0.15.0"
102
102
  }
103
103
  },
104
104
  "node_modules/@matter/node": {
105
- "version": "0.15.0-alpha.0-20250626-fc3a84ce9",
106
- "resolved": "https://registry.npmjs.org/@matter/node/-/node-0.15.0-alpha.0-20250626-fc3a84ce9.tgz",
107
- "integrity": "sha512-p0ehfBazBnALPKIplEJHibZ+HmszGDszcgq5bsXLlu9MyEfQ6Y3LZK9+hKEH8pavSaJUbMBfQRn5fEzzAMqn+w==",
105
+ "version": "0.15.0",
106
+ "resolved": "https://registry.npmjs.org/@matter/node/-/node-0.15.0.tgz",
107
+ "integrity": "sha512-v15+L1grC+ahHalk28Gf9cKoztxmjHdhkWEkXmYi51YE+XJJYZCC/Ib7QDBHJkk+znf1O6jq19SXoQ2Kylxwug==",
108
108
  "license": "Apache-2.0",
109
109
  "dependencies": {
110
- "@matter/general": "0.15.0-alpha.0-20250626-fc3a84ce9",
111
- "@matter/model": "0.15.0-alpha.0-20250626-fc3a84ce9",
112
- "@matter/protocol": "0.15.0-alpha.0-20250626-fc3a84ce9",
113
- "@matter/types": "0.15.0-alpha.0-20250626-fc3a84ce9"
110
+ "@matter/general": "0.15.0",
111
+ "@matter/model": "0.15.0",
112
+ "@matter/protocol": "0.15.0",
113
+ "@matter/types": "0.15.0"
114
114
  }
115
115
  },
116
116
  "node_modules/@matter/nodejs": {
117
- "version": "0.15.0-alpha.0-20250626-fc3a84ce9",
118
- "resolved": "https://registry.npmjs.org/@matter/nodejs/-/nodejs-0.15.0-alpha.0-20250626-fc3a84ce9.tgz",
119
- "integrity": "sha512-zkCATPiq29m+QRHAeK7+RUS2F2cuVhSQVMqTQjtuQyEVMRAx+8aucJB/3AfOYqLX4DfOLUenxYpt9Mm59bQLYQ==",
117
+ "version": "0.15.0",
118
+ "resolved": "https://registry.npmjs.org/@matter/nodejs/-/nodejs-0.15.0.tgz",
119
+ "integrity": "sha512-CaFEEtNWzUpOzjdoxYu4oiN4GJMEjoZlGv19TAwaSvT7SLjm2B1ojcHBhvD+uUrF1SkGn2zql5mwO+qfYHCJHA==",
120
120
  "license": "Apache-2.0",
121
121
  "optional": true,
122
122
  "dependencies": {
123
- "@matter/general": "0.15.0-alpha.0-20250626-fc3a84ce9",
124
- "@matter/node": "0.15.0-alpha.0-20250626-fc3a84ce9",
125
- "@matter/protocol": "0.15.0-alpha.0-20250626-fc3a84ce9",
126
- "@matter/types": "0.15.0-alpha.0-20250626-fc3a84ce9"
123
+ "@matter/general": "0.15.0",
124
+ "@matter/node": "0.15.0",
125
+ "@matter/protocol": "0.15.0",
126
+ "@matter/types": "0.15.0"
127
127
  },
128
128
  "engines": {
129
129
  "node": ">=18.0.0"
130
130
  }
131
131
  },
132
132
  "node_modules/@matter/protocol": {
133
- "version": "0.15.0-alpha.0-20250626-fc3a84ce9",
134
- "resolved": "https://registry.npmjs.org/@matter/protocol/-/protocol-0.15.0-alpha.0-20250626-fc3a84ce9.tgz",
135
- "integrity": "sha512-82cY1+OXoQQZ7wcApdMipO1n8JBRvryI2+WY0Amy+dPRo7fzd1n0EB2bpoiQ+FZAllrFND31KcaFY5BOR63nvw==",
133
+ "version": "0.15.0",
134
+ "resolved": "https://registry.npmjs.org/@matter/protocol/-/protocol-0.15.0.tgz",
135
+ "integrity": "sha512-TcR3Y+iPDz1N0dcSIQ1qO7uiIbiCwNpcoMSl5Ly8Q9aoyJfYQtIKWMMq6cCrAB9oeMHwLK0SopvSPAbCvWiPkQ==",
136
136
  "license": "Apache-2.0",
137
137
  "dependencies": {
138
- "@matter/general": "0.15.0-alpha.0-20250626-fc3a84ce9",
139
- "@matter/model": "0.15.0-alpha.0-20250626-fc3a84ce9",
140
- "@matter/types": "0.15.0-alpha.0-20250626-fc3a84ce9"
138
+ "@matter/general": "0.15.0",
139
+ "@matter/model": "0.15.0",
140
+ "@matter/types": "0.15.0"
141
141
  }
142
142
  },
143
143
  "node_modules/@matter/types": {
144
- "version": "0.15.0-alpha.0-20250626-fc3a84ce9",
145
- "resolved": "https://registry.npmjs.org/@matter/types/-/types-0.15.0-alpha.0-20250626-fc3a84ce9.tgz",
146
- "integrity": "sha512-cxqOVzCxYk7lH9nuP15sJ2xfF9PfSDMFdv33/gmFTny6y85Yf2ZcTNrVfG5G7vNCL42Sj1edc8BLum8KYM3D8w==",
144
+ "version": "0.15.0",
145
+ "resolved": "https://registry.npmjs.org/@matter/types/-/types-0.15.0.tgz",
146
+ "integrity": "sha512-gLh0AGTUs7XZituhPSfpTc8WbC8lRyJqs5jMaLBZZj+4ptuvI9Y29d9ivitElkwBObG3SJYIgWgSPTmnHuz0IQ==",
147
147
  "license": "Apache-2.0",
148
148
  "dependencies": {
149
- "@matter/general": "0.15.0-alpha.0-20250626-fc3a84ce9",
150
- "@matter/model": "0.15.0-alpha.0-20250626-fc3a84ce9"
149
+ "@matter/general": "0.15.0",
150
+ "@matter/model": "0.15.0"
151
151
  }
152
152
  },
153
153
  "node_modules/@noble/curves": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "matterbridge",
3
- "version": "3.0.8-dev-20250626-50aa686",
3
+ "version": "3.1.0-dev-20250627-2b5adba",
4
4
  "description": "Matterbridge plugin manager for Matter",
5
5
  "author": "https://github.com/Luligu",
6
6
  "license": "Apache-2.0",
@@ -98,7 +98,7 @@
98
98
  }
99
99
  },
100
100
  "dependencies": {
101
- "@matter/main": "0.15.0-alpha.0-20250626-fc3a84ce9",
101
+ "@matter/main": "0.15.0",
102
102
  "archiver": "7.0.1",
103
103
  "express": "5.1.0",
104
104
  "glob": "11.0.3",
@@ -1,6 +1,13 @@
1
1
  import { vi, describe, it, expect, beforeEach, afterEach, MockInstance } from 'vitest';
2
2
  import { Matterbridge } from '../src/matterbridge.ts';
3
- import { AnsiLogger } from 'node-ansi-logger';
3
+ import { AnsiLogger, LogLevel, TimestampFormat } from 'node-ansi-logger';
4
+ import path from 'node:path';
5
+ import spawn from '../src/utils/spawn.ts';
6
+
7
+ const NAME = 'ViMatterbridgeGlobal';
8
+ const HOMEDIR = path.join('jest', NAME);
9
+
10
+ process.argv = ['node', 'matterbridge.test.js', '-novirtual', '-frontend', '0', '-homedir', HOMEDIR, '-logger', 'debug', '-matterlogger', 'debug'];
4
11
 
5
12
  // Partial mock for @matter/main, preserving all actual exports except Logger
6
13
  vi.mock('@matter/main', async (importOriginal) => {
@@ -28,7 +35,7 @@ let consoleDebugSpy: MockInstance<typeof console.debug>;
28
35
  let consoleInfoSpy: MockInstance<typeof console.info>;
29
36
  let consoleWarnSpy: MockInstance<typeof console.warn>;
30
37
  let consoleErrorSpy: MockInstance<typeof console.error>;
31
- const debug = true; // Set to true to enable debug logging
38
+ const debug = false; // Set to true to enable debug logging
32
39
 
33
40
  if (!debug) {
34
41
  loggerLogSpy = vi.spyOn(AnsiLogger.prototype, 'log').mockImplementation((level: string, message: string, ...parameters: any[]) => {});
@@ -52,7 +59,8 @@ describe('Matterbridge', () => {
52
59
  beforeEach(async () => {
53
60
  matterbridge = await Matterbridge.loadInstance(false);
54
61
  // Set up required properties/mocks
55
- matterbridge.log = { debug: vi.fn(), info: vi.fn(), notice: vi.fn(), warn: vi.fn(), error: vi.fn(), fatal: vi.fn() } as any;
62
+ matterbridge.log = { debug: vi.fn(), info: vi.fn(), notice: vi.fn(), warn: vi.fn(), error: vi.fn(), fatal: vi.fn(), now: vi.fn() } as any;
63
+ // matterbridge.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: TimestampFormat.TIME_MILLIS, logLevel: LogLevel.DEBUG });
56
64
  matterbridge.plugins = { array: () => [], clear: vi.fn(), [Symbol.iterator]: function* () {} } as any;
57
65
  matterbridge.devices = { array: () => [], clear: vi.fn(), [Symbol.iterator]: function* () {} } as any;
58
66
  matterbridge.frontend = {
@@ -61,6 +69,7 @@ describe('Matterbridge', () => {
61
69
  wssSendRefreshRequired: vi.fn(),
62
70
  wssSendRestartRequired: vi.fn(),
63
71
  wssSendSnackbarMessage: vi.fn(),
72
+ wssSendMessage: vi.fn(),
64
73
  } as any;
65
74
  });
66
75
 
@@ -120,27 +129,27 @@ describe('Matterbridge', () => {
120
129
 
121
130
  it('should get devices using getDevices()', () => {
122
131
  const fakeDevices = [{ id: 1 }, { id: 2 }];
123
- matterbridge.devices.array = vi.fn(() => fakeDevices);
132
+ matterbridge.devices.array = vi.fn(() => fakeDevices) as any;
124
133
  expect(matterbridge.getDevices()).toBe(fakeDevices);
125
134
  });
126
135
 
127
136
  it('should get plugins using getPlugins()', () => {
128
137
  const fakePlugins = [{ name: 'p1' }, { name: 'p2' }];
129
- matterbridge.plugins.array = vi.fn(() => fakePlugins);
138
+ matterbridge.plugins.array = vi.fn(() => fakePlugins) as any;
130
139
  expect(matterbridge.getPlugins()).toBe(fakePlugins);
131
140
  });
132
141
 
133
142
  it('should set log level and propagate to dependencies', async () => {
134
143
  const setLevel = vi.fn();
135
- matterbridge.frontend.logLevel = undefined;
136
- matterbridge.devices.logLevel = undefined;
137
- matterbridge.plugins.logLevel = undefined;
138
- (global as any).MatterbridgeEndpoint = { logLevel: undefined };
144
+ matterbridge.frontend.logLevel = LogLevel.NONE; // Set to NONE initially
145
+ matterbridge.devices.logLevel = LogLevel.NONE;
146
+ matterbridge.plugins.logLevel = LogLevel.NONE;
147
+ (global as any).MatterbridgeEndpoint = { logLevel: LogLevel.NONE };
139
148
  matterbridge.log = { debug: vi.fn() } as any;
140
- await matterbridge.setLogLevel(1); // 1 = INFO
141
- expect(matterbridge.frontend.logLevel).toBe(1);
142
- expect(matterbridge.devices.logLevel).toBe(1);
143
- expect(matterbridge.plugins.logLevel).toBe(1);
149
+ await matterbridge.setLogLevel(LogLevel.INFO);
150
+ expect(matterbridge.frontend.logLevel).toBe(LogLevel.INFO);
151
+ expect(matterbridge.devices.logLevel).toBe(LogLevel.INFO);
152
+ expect(matterbridge.plugins.logLevel).toBe(LogLevel.INFO);
144
153
  expect(matterbridge.log.debug).toHaveBeenCalled();
145
154
  });
146
155
 
@@ -154,7 +163,7 @@ describe('Matterbridge', () => {
154
163
  it('should call destroyInstance and cleanup', async () => {
155
164
  const cleanupSpy = vi.spyOn(matterbridge as any, 'cleanup').mockResolvedValue(undefined);
156
165
  matterbridge.log.info = vi.fn();
157
- await matterbridge.destroyInstance();
166
+ await matterbridge.destroyInstance(10);
158
167
  expect(cleanupSpy).toHaveBeenCalled();
159
168
  expect(matterbridge.log.info).toHaveBeenCalledWith(expect.stringContaining('Destroy instance...'));
160
169
  });
@@ -172,10 +181,11 @@ describe('Matterbridge', () => {
172
181
  });
173
182
 
174
183
  it('should call updateProcess and cleanup', async () => {
184
+ const spawnSpy = vi.spyOn(spawn, 'spawnCommand').mockResolvedValueOnce(true);
175
185
  const cleanupSpy = vi.spyOn(matterbridge as any, 'cleanup').mockResolvedValue(undefined);
176
186
  matterbridge.log.info = vi.fn();
177
- (matterbridge.frontend as any).wssSendRestartRequired = vi.fn();
178
187
  await matterbridge.updateProcess();
188
+ expect(spawnSpy).toHaveBeenCalledWith(matterbridge, 'npm', ['install', '-g', 'matterbridge', '--omit=dev', '--verbose']);
179
189
  expect(cleanupSpy).toHaveBeenCalledWith('updating...', false);
180
190
  expect(matterbridge.log.info).toHaveBeenCalledWith(expect.stringContaining('Updating matterbridge...'));
181
191
  expect((matterbridge.frontend as any).wssSendRestartRequired).toHaveBeenCalled();
@@ -184,7 +194,7 @@ describe('Matterbridge', () => {
184
194
  it('should call unregisterAndShutdownProcess and cleanup', async () => {
185
195
  const cleanupSpy = vi.spyOn(matterbridge as any, 'cleanup').mockResolvedValue(undefined);
186
196
  matterbridge.log.info = vi.fn();
187
- matterbridge.plugins = [{ name: 'p1' }];
197
+ matterbridge.plugins = [{ name: 'p1' }] as any;
188
198
  matterbridge.removeAllBridgedEndpoints = vi.fn().mockResolvedValue(undefined);
189
199
  matterbridge.devices.clear = vi.fn();
190
200
  await matterbridge.unregisterAndShutdownProcess();
@@ -203,6 +213,4 @@ describe('Matterbridge', () => {
203
213
  await matterbridge.shutdownProcessAndFactoryReset();
204
214
  expect(cleanupSpy).toHaveBeenCalledWith('shutting down with factory reset...', false);
205
215
  });
206
-
207
- // Add more tests for all public/protected methods to reach 100% coverage
208
216
  });