matterbridge 3.0.8-dev-20250626-1445fbd → 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
 
@@ -21,8 +21,10 @@ If you like this project and find it useful, please consider giving it a star on
21
21
  - [ESLint]: Added the plugins `eslint-plugin-promise`, `eslint-plugin-jsdoc`, and `@vitest/eslint-plugin`.
22
22
  - [Vitest]: Added Vitest for TypeScript project testing. It will replace Jest, which does not work correctly with ESM module mocks.
23
23
  - [JSDoc]: Added missing JSDoc comments, including `@param` and `@returns` tags.
24
- - [MatterbridgeEndpoint]: Add MatterbridgeEndpoint serverMode='server'
25
- - [MatterbridgeEndpoint]: Add MatterbridgeEndpoint serverMode='matter'
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
+ - [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,7 +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.
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.
34
36
 
35
37
  ### Fixed
36
38
 
package/README-DEV.md CHANGED
@@ -28,6 +28,18 @@ The Matterbridge Plugin Template has an already configured Jest / Vitest test un
28
28
 
29
29
  It also has a workflow configured to run on push and pull request that build, lint and test the plugin on node 20, 22 and 24 with ubuntu, macOS and windows.
30
30
 
31
+ ## Dev Container
32
+
33
+ Using a Dev Container provides a fully isolated, reproducible, and pre-configured development environment. This ensures that all contributors have the same tools, extensions, and dependencies, eliminating "works on my machine" issues. It also makes onboarding new developers fast and hassle-free, as everything needed is set up automatically.
34
+
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
+
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
+
41
+ Since Dev Container doesn't run in network mode 'host', it is not possible to pair Mattebridge running inside the Dev Container.
42
+
31
43
  ## Guidelines on imports/exports
32
44
 
33
45
  Matterbridge exports from:
@@ -239,7 +251,26 @@ It can be useful to call this method from onShutdown() if you don't want to keep
239
251
 
240
252
  ## MatterbridgeEndpoint api
241
253
 
242
- Work in progress...
254
+ You create a Matter device with a new instance of MatterbridgeEndpoint(definition: DeviceTypeDefinition | AtLeastOne<DeviceTypeDefinition>, options: MatterbridgeEndpointOptions = {}, debug: boolean = false).
255
+
256
+ - @param {DeviceTypeDefinition | AtLeastOne<DeviceTypeDefinition>} definition - The DeviceTypeDefinition(s) of the endpoint.
257
+ - @param {MatterbridgeEndpointOptions} [options] - The options for the device.
258
+ - @param {boolean} [debug] - Debug flag.
259
+
260
+ ```typescript
261
+ const device = new MatterbridgeEndpoint([contactSensor, powerSource], { uniqueStorageKey: 'Eve door', mode: 'matter' }, this.config.debug as boolean)
262
+ .createDefaultIdentifyClusterServer()
263
+ .createDefaultBasicInformationClusterServer('My contact sensor', '0123456789')
264
+ .createDefaultBooleanStateClusterServer(true)
265
+ .createDefaultPowerSourceReplaceableBatteryClusterServer(75)
266
+ .addRequiredClusterServers(); // Always better to call it at the end of the chain to add all the not already created but required clusters.
267
+ ```
268
+
269
+ In the above example we create a contact sensor device type with also a power source device type feature replaceble battery.
270
+
271
+ All device types are defined in src\matterbridgeDeviceTypes.ts and taken from the 'Matter-1.4-Device-Library-Specification.pdf'.
272
+
273
+ All default cluster helpers are available as methods of MatterbridgeEndpoint.
243
274
 
244
275
  # Contribution Guidelines
245
276
 
package/README.md CHANGED
@@ -5,7 +5,8 @@
5
5
  [![Docker Version](https://img.shields.io/docker/v/luligu/matterbridge?label=docker%20version&sort=semver)](https://hub.docker.com/r/luligu/matterbridge)
6
6
  [![Docker Pulls](https://img.shields.io/docker/pulls/luligu/matterbridge.svg)](https://hub.docker.com/r/luligu/matterbridge)
7
7
  ![Node.js CI](https://github.com/Luligu/matterbridge/actions/workflows/build.yml/badge.svg)
8
- ![Coverage](https://img.shields.io/badge/Jest%20coverage-89%25-brightgreen)
8
+ ![CodeQL](https://github.com/Luligu/matterbridge/actions/workflows/codeql.yml/badge.svg)
9
+ [![codecov](https://codecov.io/gh/Luligu/matterbridge/branch/main/graph/badge.svg)](https://codecov.io/gh/Luligu/matterbridge)
9
10
 
10
11
  [![power by](https://img.shields.io/badge/powered%20by-matter--history-blue)](https://www.npmjs.com/package/matter-history)
11
12
  [![power by](https://img.shields.io/badge/powered%20by-node--ansi--logger-blue)](https://www.npmjs.com/package/node-ansi-logger)
@@ -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') {
@@ -206,12 +200,15 @@ export class Matterbridge extends EventEmitter {
206
200
  }
207
201
  if (this.devices !== undefined) {
208
202
  for (const device of this.devices.array()) {
209
- if (device.serverMode === 'server' && device.serverNode)
203
+ if (device.mode === 'server' && device.serverNode)
210
204
  servers.push(device.serverNode);
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);
@@ -1082,9 +1052,10 @@ export class Matterbridge extends EventEmitter {
1082
1052
  }
1083
1053
  }
1084
1054
  for (const device of this.devices.array()) {
1085
- if (device.serverMode === 'server' && device.serverNode) {
1055
+ if (device.mode === 'server' && device.serverNode) {
1086
1056
  await this.stopServerNode(device.serverNode);
1087
1057
  device.serverNode = undefined;
1058
+ device.serverContext = undefined;
1088
1059
  }
1089
1060
  }
1090
1061
  this.log.notice('Stopped matter server nodes');
@@ -1181,10 +1152,10 @@ export class Matterbridge extends EventEmitter {
1181
1152
  }
1182
1153
  }
1183
1154
  async createDeviceServerNode(plugin, device) {
1184
- if (device.serverMode === 'server' && !device.serverNode && device.deviceType && device.deviceName && device.vendorId && device.productId && device.vendorName && device.productName) {
1155
+ if (device.mode === 'server' && !device.serverNode && device.deviceType && device.deviceName && device.vendorId && device.productId && device.vendorName && device.productName) {
1185
1156
  this.log.debug(`Creating device ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db} server node...`);
1186
- const storageContext = await this.createServerNodeContext(device.deviceName.replace(/[ .]/g, ''), device.deviceName, DeviceTypeId(device.deviceType), device.vendorId, device.vendorName, device.productId, device.productName);
1187
- device.serverNode = await this.createServerNode(storageContext, this.port ? this.port++ : undefined, this.passcode ? this.passcode++ : undefined, this.discriminator ? this.discriminator++ : undefined);
1157
+ device.serverContext = await this.createServerNodeContext(device.deviceName.replace(/[ .]/g, ''), device.deviceName, DeviceTypeId(device.deviceType), device.vendorId, device.vendorName, device.productId, device.productName);
1158
+ device.serverNode = await this.createServerNode(device.serverContext, this.port ? this.port++ : undefined, this.passcode ? this.passcode++ : undefined, this.discriminator ? this.discriminator++ : undefined);
1188
1159
  this.log.debug(`Adding ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db} to server node...`);
1189
1160
  await device.serverNode.add(device);
1190
1161
  this.log.debug(`Added ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db} to server node`);
@@ -1256,7 +1227,7 @@ export class Matterbridge extends EventEmitter {
1256
1227
  this.log.debug('Cleared startMatterInterval interval for Matterbridge');
1257
1228
  this.startServerNode(this.serverNode);
1258
1229
  for (const device of this.devices.array()) {
1259
- if (device.serverMode === 'server' && device.serverNode) {
1230
+ if (device.mode === 'server' && device.serverNode) {
1260
1231
  this.log.debug(`Starting server node for device ${dev}${device.deviceName}${db} in server mode...`);
1261
1232
  await this.startServerNode(device.serverNode);
1262
1233
  }
@@ -1277,13 +1248,15 @@ export class Matterbridge extends EventEmitter {
1277
1248
  }
1278
1249
  }
1279
1250
  this.frontend.wssSendRefreshRequired('plugins');
1280
- }, 30 * 1000);
1251
+ }, 30 * 1000).unref();
1281
1252
  this.reachabilityTimeout = setTimeout(() => {
1282
1253
  this.log.info(`Setting reachability to true for ${plg}Matterbridge${db}`);
1283
1254
  if (this.aggregatorNode)
1284
1255
  this.setAggregatorReachability(this.aggregatorNode, true);
1285
1256
  this.frontend.wssSendRefreshRequired('reachability');
1286
- }, 60 * 1000);
1257
+ }, 60 * 1000).unref();
1258
+ this.emit('bridge_started');
1259
+ this.log.notice('Matterbridge bridge started successfully');
1287
1260
  }, 1000);
1288
1261
  }
1289
1262
  async startChildbridge() {
@@ -1339,7 +1312,7 @@ export class Matterbridge extends EventEmitter {
1339
1312
  }
1340
1313
  }
1341
1314
  this.frontend.wssSendRefreshRequired('plugins');
1342
- }, 30 * 1000);
1315
+ }, 30 * 1000).unref();
1343
1316
  for (const plugin of this.plugins.array()) {
1344
1317
  if (!plugin.enabled || plugin.error)
1345
1318
  continue;
@@ -1365,14 +1338,16 @@ export class Matterbridge extends EventEmitter {
1365
1338
  if (plugin.type === 'DynamicPlatform' && plugin.aggregatorNode)
1366
1339
  this.setAggregatorReachability(plugin.aggregatorNode, true);
1367
1340
  this.frontend.wssSendRefreshRequired('reachability');
1368
- }, 60 * 1000);
1341
+ }, 60 * 1000).unref();
1369
1342
  }
1370
1343
  for (const device of this.devices.array()) {
1371
- if (device.serverMode === 'server' && device.serverNode) {
1344
+ if (device.mode === 'server' && device.serverNode) {
1372
1345
  this.log.debug(`***Starting server node for device ${plg}${device.deviceName}${db} in server mode...`);
1373
1346
  await this.startServerNode(device.serverNode);
1374
1347
  }
1375
1348
  }
1349
+ this.emit('childbridge_started');
1350
+ this.log.notice('Matterbridge childbridge started successfully');
1376
1351
  }, 1000);
1377
1352
  }
1378
1353
  async startController() {
@@ -1597,10 +1572,7 @@ export class Matterbridge extends EventEmitter {
1597
1572
  this.frontend.wssSendRefreshRequired('fabrics');
1598
1573
  });
1599
1574
  const sanitizeSessions = (sessions) => {
1600
- const sanitizedSessions = this.sanitizeSessionInformation(sessions.map((session) => ({
1601
- ...session,
1602
- secure: session.name.startsWith('secure'),
1603
- })));
1575
+ const sanitizedSessions = this.sanitizeSessionInformation(sessions);
1604
1576
  this.log.debug(`Sessions: ${debugStringify(sanitizedSessions)}`);
1605
1577
  if (this.bridgeMode === 'bridge') {
1606
1578
  this.matterbridgeSessionInformations = sanitizedSessions;
@@ -1702,7 +1674,7 @@ export class Matterbridge extends EventEmitter {
1702
1674
  this.log.error(`Error adding bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.id}${er}) plugin ${plg}${pluginName}${er} not found`);
1703
1675
  return;
1704
1676
  }
1705
- if (device.serverMode === 'server') {
1677
+ if (device.mode === 'server') {
1706
1678
  try {
1707
1679
  this.log.debug(`Creating server node for device ${dev}${device.deviceName}${db} of plugin ${plg}${plugin.name}${db}...`);
1708
1680
  await this.createDeviceServerNode(plugin, device);
@@ -1715,7 +1687,7 @@ export class Matterbridge extends EventEmitter {
1715
1687
  }
1716
1688
  }
1717
1689
  else if (this.bridgeMode === 'bridge') {
1718
- if (device.serverMode === 'matter') {
1690
+ if (device.mode === 'matter') {
1719
1691
  this.log.debug(`Adding matter endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} to Matterbridge server node...`);
1720
1692
  if (!this.serverNode) {
1721
1693
  this.log.error('Server node not found for Matterbridge');
@@ -1774,7 +1746,7 @@ export class Matterbridge extends EventEmitter {
1774
1746
  this.log.error(`Aggregator node not found for plugin ${plg}${plugin.name}${er}`);
1775
1747
  return;
1776
1748
  }
1777
- if (device.serverMode === 'matter')
1749
+ if (device.mode === 'matter')
1778
1750
  await plugin.serverNode?.add(device);
1779
1751
  else
1780
1752
  await plugin.aggregatorNode.add(device);
@@ -1870,8 +1842,8 @@ export class Matterbridge extends EventEmitter {
1870
1842
  };
1871
1843
  });
1872
1844
  }
1873
- sanitizeSessionInformation(sessionInfo) {
1874
- return sessionInfo
1845
+ sanitizeSessionInformation(session) {
1846
+ return session
1875
1847
  .filter((session) => session.isPeerActive)
1876
1848
  .map((session) => {
1877
1849
  return {
@@ -1890,7 +1862,6 @@ export class Matterbridge extends EventEmitter {
1890
1862
  }
1891
1863
  : undefined,
1892
1864
  isPeerActive: session.isPeerActive,
1893
- secure: session.secure,
1894
1865
  lastInteractionTimestamp: session.lastInteractionTimestamp?.toString(),
1895
1866
  lastActiveTimestamp: session.lastActiveTimestamp?.toString(),
1896
1867
  numberOfActiveSubscriptions: session.numberOfActiveSubscriptions,
@@ -1,4 +1,4 @@
1
- import { Endpoint, Lifecycle, MutableEndpoint, NamedHandler, SupportedBehaviors, UINT16_MAX, UINT32_MAX, VendorId } from '@matter/main';
1
+ import { Endpoint, Lifecycle, MutableEndpoint, NamedHandler, SupportedBehaviors, UINT16_MAX, UINT32_MAX, VendorId, } from '@matter/main';
2
2
  import { getClusterNameById, MeasurementType } from '@matter/main/types';
3
3
  import { Descriptor } from '@matter/main/clusters/descriptor';
4
4
  import { PowerSource } from '@matter/main/clusters/power-source';
@@ -68,7 +68,8 @@ import { addClusterServers, addFixedLabel, addOptionalClusterServers, addRequire
68
68
  export class MatterbridgeEndpoint extends Endpoint {
69
69
  static bridgeMode = '';
70
70
  static logLevel = "info";
71
- serverMode = undefined;
71
+ mode = undefined;
72
+ serverContext;
72
73
  serverNode;
73
74
  log;
74
75
  plugin = undefined;
@@ -126,13 +127,23 @@ export class MatterbridgeEndpoint extends Endpoint {
126
127
  if (options.uniqueStorageKey && checkNotLatinCharacters(options.uniqueStorageKey)) {
127
128
  options.uniqueStorageKey = generateUniqueId(options.uniqueStorageKey);
128
129
  }
130
+ if (options.id && checkNotLatinCharacters(options.id)) {
131
+ options.id = generateUniqueId(options.id);
132
+ }
129
133
  const optionsV8 = {
130
134
  id: options.uniqueStorageKey?.replace(/[ .]/g, ''),
131
135
  number: options.endpointId,
132
136
  descriptor: options.tagList ? { tagList: options.tagList, deviceTypeList } : { deviceTypeList },
133
137
  };
138
+ if (options.id !== undefined) {
139
+ optionsV8.id = options.id.replace(/[ .]/g, '');
140
+ }
141
+ if (options.number !== undefined) {
142
+ optionsV8.number = options.number;
143
+ }
134
144
  super(endpointV8, optionsV8);
135
- this.uniqueStorageKey = options.uniqueStorageKey;
145
+ this.mode = options.mode;
146
+ this.uniqueStorageKey = options.id ? options.id : options.uniqueStorageKey;
136
147
  this.name = firstDefinition.name;
137
148
  this.deviceType = firstDefinition.code;
138
149
  this.tagList = options.tagList;
@@ -442,7 +453,7 @@ export class MatterbridgeEndpoint extends Endpoint {
442
453
  });
443
454
  return this;
444
455
  }
445
- createDefaultBasicInformationClusterServer(deviceName, serialNumber, vendorId, vendorName, productId, productName, softwareVersion = 1, softwareVersionString = '1.0.0', hardwareVersion = 1, hardwareVersionString = '1.0.0') {
456
+ createDefaultBasicInformationClusterServer(deviceName, serialNumber, vendorId = 0xfff1, vendorName = 'Matterbridge', productId = 0x8000, productName = 'Matterbridge device', softwareVersion = 1, softwareVersionString = '1.0.0', hardwareVersion = 1, hardwareVersionString = '1.0.0') {
446
457
  this.log.logName = deviceName;
447
458
  this.deviceName = deviceName;
448
459
  this.serialNumber = serialNumber;
@@ -455,7 +466,7 @@ export class MatterbridgeEndpoint extends Endpoint {
455
466
  this.softwareVersionString = softwareVersionString;
456
467
  this.hardwareVersion = hardwareVersion;
457
468
  this.hardwareVersionString = hardwareVersionString;
458
- if (MatterbridgeEndpoint.bridgeMode === 'bridge') {
469
+ if (MatterbridgeEndpoint.bridgeMode === 'bridge' && this.mode === undefined) {
459
470
  const options = this.getClusterServerOptions(Descriptor.Cluster.id);
460
471
  if (options) {
461
472
  const deviceTypeList = options.deviceTypeList;
@@ -465,7 +476,7 @@ export class MatterbridgeEndpoint extends Endpoint {
465
476
  }
466
477
  return this;
467
478
  }
468
- createDefaultBridgedDeviceBasicInformationClusterServer(deviceName, serialNumber, vendorId, vendorName, productName, softwareVersion = 1, softwareVersionString = '1.0.0', hardwareVersion = 1, hardwareVersionString = '1.0.0') {
479
+ createDefaultBridgedDeviceBasicInformationClusterServer(deviceName, serialNumber, vendorId = 0xfff1, vendorName = 'Matterbridge', productName = 'Matterbridge device', softwareVersion = 1, softwareVersionString = '1.0.0', hardwareVersion = 1, hardwareVersionString = '1.0.0') {
469
480
  this.log.logName = deviceName;
470
481
  this.deviceName = deviceName;
471
482
  this.serialNumber = serialNumber;
@@ -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-1445fbd",
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-1445fbd",
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-20250625-c7634df96",
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-20250625-c7634df96",
72
- "resolved": "https://registry.npmjs.org/@matter/general/-/general-0.15.0-alpha.0-20250625-c7634df96.tgz",
73
- "integrity": "sha512-SGs0jerp66b6o8pINI6fpSiPX/S1QlAO8Rr/Rz6usJO3JbS31vnWE5HoDIMMf4EPyyjH+dh5+heASuEBW/K5Hg==",
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-20250625-c7634df96",
81
- "resolved": "https://registry.npmjs.org/@matter/main/-/main-0.15.0-alpha.0-20250625-c7634df96.tgz",
82
- "integrity": "sha512-9x3HeF2MPR31jZVHpzvTN4BcFynFUvuy1lWfwZgMha+BWEQPAQp3DS77pvuIpPMFyWe8kaKtjWTvSsGOHt+vig==",
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-20250625-c7634df96",
86
- "@matter/model": "0.15.0-alpha.0-20250625-c7634df96",
87
- "@matter/node": "0.15.0-alpha.0-20250625-c7634df96",
88
- "@matter/protocol": "0.15.0-alpha.0-20250625-c7634df96",
89
- "@matter/types": "0.15.0-alpha.0-20250625-c7634df96"
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-20250625-c7634df96"
92
+ "@matter/nodejs": "0.15.0"
93
93
  }
94
94
  },
95
95
  "node_modules/@matter/model": {
96
- "version": "0.15.0-alpha.0-20250625-c7634df96",
97
- "resolved": "https://registry.npmjs.org/@matter/model/-/model-0.15.0-alpha.0-20250625-c7634df96.tgz",
98
- "integrity": "sha512-QF+R227USkpJhLzt0R/YxiZm34ag3DhfxDfqcxD9Fghf+vIjGjHfXzNeE8It1tbkgmVVRMdqrxFpw2N5EQ63Nw==",
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-20250625-c7634df96"
101
+ "@matter/general": "0.15.0"
102
102
  }
103
103
  },
104
104
  "node_modules/@matter/node": {
105
- "version": "0.15.0-alpha.0-20250625-c7634df96",
106
- "resolved": "https://registry.npmjs.org/@matter/node/-/node-0.15.0-alpha.0-20250625-c7634df96.tgz",
107
- "integrity": "sha512-CvpjmpcjSOQOKk5YDg4X4WC4+1mkDDBAbI70vI01wJpV2Y8AByPEtLjLdpTEFcXT/AOSj29ga4sMcGR8cm2V3g==",
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-20250625-c7634df96",
111
- "@matter/model": "0.15.0-alpha.0-20250625-c7634df96",
112
- "@matter/protocol": "0.15.0-alpha.0-20250625-c7634df96",
113
- "@matter/types": "0.15.0-alpha.0-20250625-c7634df96"
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-20250625-c7634df96",
118
- "resolved": "https://registry.npmjs.org/@matter/nodejs/-/nodejs-0.15.0-alpha.0-20250625-c7634df96.tgz",
119
- "integrity": "sha512-HZJUh5T04E3zfGw3ZjjkNyMOrXQk5qKZoMS8LI2pwUjHBd1vU2hS28EAXqufELcQ9pvdzUisYbSLWPb2N8B+CA==",
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-20250625-c7634df96",
124
- "@matter/node": "0.15.0-alpha.0-20250625-c7634df96",
125
- "@matter/protocol": "0.15.0-alpha.0-20250625-c7634df96",
126
- "@matter/types": "0.15.0-alpha.0-20250625-c7634df96"
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-20250625-c7634df96",
134
- "resolved": "https://registry.npmjs.org/@matter/protocol/-/protocol-0.15.0-alpha.0-20250625-c7634df96.tgz",
135
- "integrity": "sha512-YRnbd+nYjp9S3Rjm1V+KnmKoxg8KzZ3nO0tH1weLbn397FldHTDhywyYOielIfQoZshfYxsplBG4xIiE1C8mew==",
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-20250625-c7634df96",
139
- "@matter/model": "0.15.0-alpha.0-20250625-c7634df96",
140
- "@matter/types": "0.15.0-alpha.0-20250625-c7634df96"
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-20250625-c7634df96",
145
- "resolved": "https://registry.npmjs.org/@matter/types/-/types-0.15.0-alpha.0-20250625-c7634df96.tgz",
146
- "integrity": "sha512-PXsCCEit+F++aQR63T0Ya+hA5rrjncYjJyi1dNU7TXm8LDsHd0/wbzXSAmgJoaRY9mbMpFg9idi/6JxKGR+7dw==",
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-20250625-c7634df96",
150
- "@matter/model": "0.15.0-alpha.0-20250625-c7634df96"
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-1445fbd",
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-20250625-c7634df96",
101
+ "@matter/main": "0.15.0",
102
102
  "archiver": "7.0.1",
103
103
  "express": "5.1.0",
104
104
  "glob": "11.0.3",
@@ -0,0 +1,216 @@
1
+ import { vi, describe, it, expect, beforeEach, afterEach, MockInstance } from 'vitest';
2
+ import { Matterbridge } from '../src/matterbridge.ts';
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'];
11
+
12
+ // Partial mock for @matter/main, preserving all actual exports except Logger
13
+ vi.mock('@matter/main', async (importOriginal) => {
14
+ const actual = await importOriginal();
15
+ return {
16
+ ...(actual as Record<string, unknown>),
17
+ Logger: {
18
+ log: vi.fn(),
19
+ info: vi.fn(),
20
+ debug: vi.fn(),
21
+ warn: vi.fn(),
22
+ error: vi.fn(),
23
+ fatal: vi.fn(),
24
+ addLogger: vi.fn(),
25
+ setLogger: vi.fn(),
26
+ removeLogger: vi.fn(),
27
+ toJSON: vi.fn(() => ''),
28
+ },
29
+ };
30
+ });
31
+
32
+ let loggerLogSpy: MockInstance<typeof AnsiLogger.prototype.log>;
33
+ let consoleLogSpy: MockInstance<typeof console.log>;
34
+ let consoleDebugSpy: MockInstance<typeof console.debug>;
35
+ let consoleInfoSpy: MockInstance<typeof console.info>;
36
+ let consoleWarnSpy: MockInstance<typeof console.warn>;
37
+ let consoleErrorSpy: MockInstance<typeof console.error>;
38
+ const debug = false; // Set to true to enable debug logging
39
+
40
+ if (!debug) {
41
+ loggerLogSpy = vi.spyOn(AnsiLogger.prototype, 'log').mockImplementation((level: string, message: string, ...parameters: any[]) => {});
42
+ consoleLogSpy = vi.spyOn(console, 'log').mockImplementation((...args: any[]) => {});
43
+ consoleDebugSpy = vi.spyOn(console, 'debug').mockImplementation((...args: any[]) => {});
44
+ consoleInfoSpy = vi.spyOn(console, 'info').mockImplementation((...args: any[]) => {});
45
+ consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation((...args: any[]) => {});
46
+ consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation((...args: any[]) => {});
47
+ } else {
48
+ loggerLogSpy = vi.spyOn(AnsiLogger.prototype, 'log');
49
+ consoleLogSpy = vi.spyOn(console, 'log');
50
+ consoleDebugSpy = vi.spyOn(console, 'debug');
51
+ consoleInfoSpy = vi.spyOn(console, 'info');
52
+ consoleWarnSpy = vi.spyOn(console, 'warn');
53
+ consoleErrorSpy = vi.spyOn(console, 'error');
54
+ }
55
+
56
+ describe('Matterbridge', () => {
57
+ let matterbridge: Matterbridge;
58
+
59
+ beforeEach(async () => {
60
+ matterbridge = await Matterbridge.loadInstance(false);
61
+ // Set up required properties/mocks
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 });
64
+ matterbridge.plugins = { array: () => [], clear: vi.fn(), [Symbol.iterator]: function* () {} } as any;
65
+ matterbridge.devices = { array: () => [], clear: vi.fn(), [Symbol.iterator]: function* () {} } as any;
66
+ matterbridge.frontend = {
67
+ start: vi.fn(),
68
+ stop: vi.fn(),
69
+ wssSendRefreshRequired: vi.fn(),
70
+ wssSendRestartRequired: vi.fn(),
71
+ wssSendSnackbarMessage: vi.fn(),
72
+ wssSendMessage: vi.fn(),
73
+ } as any;
74
+ });
75
+
76
+ afterEach(() => {
77
+ vi.clearAllMocks();
78
+ });
79
+
80
+ it('should initialize system and matterbridge information', () => {
81
+ expect(matterbridge.systemInformation).toBeDefined();
82
+ expect(matterbridge.matterbridgeInformation).toBeDefined();
83
+ });
84
+
85
+ it('should emit initialize_completed after initialize', async () => {
86
+ const spy = vi.fn();
87
+ matterbridge.on('initialize_completed', spy);
88
+ await matterbridge.initialize();
89
+ expect(spy).toHaveBeenCalled();
90
+ expect((matterbridge as any)['initialized']).toBe(true);
91
+ });
92
+
93
+ it('should call parseCommandLine during initialize', async () => {
94
+ const spy = vi.spyOn(matterbridge as any, 'parseCommandLine').mockResolvedValue(undefined);
95
+ await matterbridge.initialize();
96
+ expect(spy).toHaveBeenCalled();
97
+ });
98
+
99
+ it('should log system info in logNodeAndSystemInfo', async () => {
100
+ await (matterbridge as any)['logNodeAndSystemInfo']();
101
+ expect(matterbridge.log.debug).toHaveBeenCalled();
102
+ });
103
+
104
+ it('should register and deregister process handlers', () => {
105
+ (matterbridge as any)['registerProcessHandlers']();
106
+ (matterbridge as any)['deregisterProcessHandlers']();
107
+ expect(matterbridge.log.error).not.toHaveBeenCalled();
108
+ });
109
+
110
+ it('should call cleanup and emit cleanup events', async () => {
111
+ (matterbridge as any)['initialized'] = true;
112
+ (matterbridge as any)['hasCleanupStarted'] = false;
113
+ const spyStart = vi.fn();
114
+ const spyComplete = vi.fn();
115
+ matterbridge.on('cleanup_started', spyStart);
116
+ matterbridge.on('cleanup_completed', spyComplete);
117
+ await (matterbridge as any)['cleanup']('test cleanup');
118
+ expect(spyStart).toHaveBeenCalled();
119
+ expect(spyComplete).toHaveBeenCalled();
120
+ });
121
+
122
+ it('should not run cleanup if already started', async () => {
123
+ (matterbridge as any)['initialized'] = true;
124
+ (matterbridge as any)['hasCleanupStarted'] = true;
125
+ const debugSpy = matterbridge.log.debug as any;
126
+ await (matterbridge as any)['cleanup']('test cleanup');
127
+ expect(debugSpy).toHaveBeenCalledWith('Cleanup already started...');
128
+ });
129
+
130
+ it('should get devices using getDevices()', () => {
131
+ const fakeDevices = [{ id: 1 }, { id: 2 }];
132
+ matterbridge.devices.array = vi.fn(() => fakeDevices) as any;
133
+ expect(matterbridge.getDevices()).toBe(fakeDevices);
134
+ });
135
+
136
+ it('should get plugins using getPlugins()', () => {
137
+ const fakePlugins = [{ name: 'p1' }, { name: 'p2' }];
138
+ matterbridge.plugins.array = vi.fn(() => fakePlugins) as any;
139
+ expect(matterbridge.getPlugins()).toBe(fakePlugins);
140
+ });
141
+
142
+ it('should set log level and propagate to dependencies', async () => {
143
+ const setLevel = vi.fn();
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 };
148
+ matterbridge.log = { debug: vi.fn() } as any;
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);
153
+ expect(matterbridge.log.debug).toHaveBeenCalled();
154
+ });
155
+
156
+ it('should emit and listen to custom events', () => {
157
+ const handler = vi.fn();
158
+ matterbridge.on('online', handler);
159
+ matterbridge.emit('online', 'nodeid');
160
+ expect(handler).toHaveBeenCalledWith('nodeid');
161
+ });
162
+
163
+ it('should call destroyInstance and cleanup', async () => {
164
+ const cleanupSpy = vi.spyOn(matterbridge as any, 'cleanup').mockResolvedValue(undefined);
165
+ matterbridge.log.info = vi.fn();
166
+ await matterbridge.destroyInstance(10);
167
+ expect(cleanupSpy).toHaveBeenCalled();
168
+ expect(matterbridge.log.info).toHaveBeenCalledWith(expect.stringContaining('Destroy instance...'));
169
+ });
170
+
171
+ it('should call restartProcess and cleanup', async () => {
172
+ const cleanupSpy = vi.spyOn(matterbridge as any, 'cleanup').mockResolvedValue(undefined);
173
+ await matterbridge.restartProcess();
174
+ expect(cleanupSpy).toHaveBeenCalledWith('restarting...', true);
175
+ });
176
+
177
+ it('should call shutdownProcess and cleanup', async () => {
178
+ const cleanupSpy = vi.spyOn(matterbridge as any, 'cleanup').mockResolvedValue(undefined);
179
+ await matterbridge.shutdownProcess();
180
+ expect(cleanupSpy).toHaveBeenCalledWith('shutting down...', false);
181
+ });
182
+
183
+ it('should call updateProcess and cleanup', async () => {
184
+ const spawnSpy = vi.spyOn(spawn, 'spawnCommand').mockResolvedValueOnce(true);
185
+ const cleanupSpy = vi.spyOn(matterbridge as any, 'cleanup').mockResolvedValue(undefined);
186
+ matterbridge.log.info = vi.fn();
187
+ await matterbridge.updateProcess();
188
+ expect(spawnSpy).toHaveBeenCalledWith(matterbridge, 'npm', ['install', '-g', 'matterbridge', '--omit=dev', '--verbose']);
189
+ expect(cleanupSpy).toHaveBeenCalledWith('updating...', false);
190
+ expect(matterbridge.log.info).toHaveBeenCalledWith(expect.stringContaining('Updating matterbridge...'));
191
+ expect((matterbridge.frontend as any).wssSendRestartRequired).toHaveBeenCalled();
192
+ });
193
+
194
+ it('should call unregisterAndShutdownProcess and cleanup', async () => {
195
+ const cleanupSpy = vi.spyOn(matterbridge as any, 'cleanup').mockResolvedValue(undefined);
196
+ matterbridge.log.info = vi.fn();
197
+ matterbridge.plugins = [{ name: 'p1' }] as any;
198
+ matterbridge.removeAllBridgedEndpoints = vi.fn().mockResolvedValue(undefined);
199
+ matterbridge.devices.clear = vi.fn();
200
+ await matterbridge.unregisterAndShutdownProcess();
201
+ expect(cleanupSpy).toHaveBeenCalledWith('unregistered all devices and shutting down...', false);
202
+ expect(matterbridge.log.info).toHaveBeenCalledWith(expect.stringContaining('Unregistering all devices and shutting down...'));
203
+ });
204
+
205
+ it('should call shutdownProcessAndReset and cleanup', async () => {
206
+ const cleanupSpy = vi.spyOn(matterbridge as any, 'cleanup').mockResolvedValue(undefined);
207
+ await matterbridge.shutdownProcessAndReset();
208
+ expect(cleanupSpy).toHaveBeenCalledWith('shutting down with reset...', false);
209
+ });
210
+
211
+ it('should call shutdownProcessAndFactoryReset and cleanup', async () => {
212
+ const cleanupSpy = vi.spyOn(matterbridge as any, 'cleanup').mockResolvedValue(undefined);
213
+ await matterbridge.shutdownProcessAndFactoryReset();
214
+ expect(cleanupSpy).toHaveBeenCalledWith('shutting down with factory reset...', false);
215
+ });
216
+ });