matterbridge 3.0.8-dev-20250626-1445fbd → 3.0.8-dev-20250626-50aa686

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
@@ -21,8 +21,8 @@ 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
26
 
27
27
  ### Changed
28
28
 
@@ -31,6 +31,7 @@ If you like this project and find it useful, please consider giving it a star on
31
31
  - [storage]: Bumped `node-storage-manager` to 2.0.0.
32
32
  - [logger]: Bumped `node-ansi-logger` to 3.1.1.
33
33
  - [matter.js]: Updated to 0.15.0-alpha.0-20250625-c7634df96.
34
+ - [matter.js]: Updated to 0.15.0-alpha.0-20250626-fc3a84ce9.
34
35
 
35
36
  ### Fixed
36
37
 
package/README-DEV.md CHANGED
@@ -28,6 +28,14 @@ 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
+ Since Dev Container doesn't run in network mode 'host', it is not possible to pair Mattebridge running inside the Dev Container.
38
+
31
39
  ## Guidelines on imports/exports
32
40
 
33
41
  Matterbridge exports from:
@@ -239,7 +247,26 @@ It can be useful to call this method from onShutdown() if you don't want to keep
239
247
 
240
248
  ## MatterbridgeEndpoint api
241
249
 
242
- Work in progress...
250
+ You create a device with a new instance of MatterbridgeEndpoint(definition: DeviceTypeDefinition | AtLeastOne<DeviceTypeDefinition>, options: MatterbridgeEndpointOptions = {}, debug: boolean = false).
251
+
252
+ - @param {DeviceTypeDefinition | AtLeastOne<DeviceTypeDefinition>} definition - The DeviceTypeDefinition(s) of the endpoint.
253
+ - @param {MatterbridgeEndpointOptions} [options] - The options for the device.
254
+ - @param {boolean} [debug] - Debug flag.
255
+
256
+ ```typescript
257
+ const device = new MatterbridgeEndpoint([contactSensor, powerSource], { uniqueStorageKey: 'Eve door', mode: 'matter' }, this.config.debug as boolean)
258
+ .createDefaultIdentifyClusterServer()
259
+ .createDefaultBasicInformationClusterServer('My contact sensor', '0123456789')
260
+ .createDefaultBooleanStateClusterServer(true)
261
+ .createDefaultPowerSourceReplaceableBatteryClusterServer(75)
262
+ .addRequiredClusterServers(); // Always better to call it at the end of the chain to add all the not already created but required clusters.
263
+ ```
264
+
265
+ In the above example we create a contact sensor device type with also a power source device type feature replaceble battery.
266
+
267
+ All device types are defined in src\matterbridgeDeviceTypes.ts and taken from the 'Matter-1.4-Device-Library-Specification.pdf'.
268
+
269
+ All default cluster helpers are available as methods of MatterbridgeEndpoint.
243
270
 
244
271
  # Contribution Guidelines
245
272
 
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)
@@ -206,7 +206,7 @@ export class Matterbridge extends EventEmitter {
206
206
  }
207
207
  if (this.devices !== undefined) {
208
208
  for (const device of this.devices.array()) {
209
- if (device.serverMode === 'server' && device.serverNode)
209
+ if (device.mode === 'server' && device.serverNode)
210
210
  servers.push(device.serverNode);
211
211
  }
212
212
  }
@@ -1082,9 +1082,10 @@ export class Matterbridge extends EventEmitter {
1082
1082
  }
1083
1083
  }
1084
1084
  for (const device of this.devices.array()) {
1085
- if (device.serverMode === 'server' && device.serverNode) {
1085
+ if (device.mode === 'server' && device.serverNode) {
1086
1086
  await this.stopServerNode(device.serverNode);
1087
1087
  device.serverNode = undefined;
1088
+ device.serverContext = undefined;
1088
1089
  }
1089
1090
  }
1090
1091
  this.log.notice('Stopped matter server nodes');
@@ -1181,10 +1182,10 @@ export class Matterbridge extends EventEmitter {
1181
1182
  }
1182
1183
  }
1183
1184
  async createDeviceServerNode(plugin, device) {
1184
- if (device.serverMode === 'server' && !device.serverNode && device.deviceType && device.deviceName && device.vendorId && device.productId && device.vendorName && device.productName) {
1185
+ if (device.mode === 'server' && !device.serverNode && device.deviceType && device.deviceName && device.vendorId && device.productId && device.vendorName && device.productName) {
1185
1186
  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);
1187
+ device.serverContext = await this.createServerNodeContext(device.deviceName.replace(/[ .]/g, ''), device.deviceName, DeviceTypeId(device.deviceType), device.vendorId, device.vendorName, device.productId, device.productName);
1188
+ device.serverNode = await this.createServerNode(device.serverContext, this.port ? this.port++ : undefined, this.passcode ? this.passcode++ : undefined, this.discriminator ? this.discriminator++ : undefined);
1188
1189
  this.log.debug(`Adding ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db} to server node...`);
1189
1190
  await device.serverNode.add(device);
1190
1191
  this.log.debug(`Added ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db} to server node`);
@@ -1256,7 +1257,7 @@ export class Matterbridge extends EventEmitter {
1256
1257
  this.log.debug('Cleared startMatterInterval interval for Matterbridge');
1257
1258
  this.startServerNode(this.serverNode);
1258
1259
  for (const device of this.devices.array()) {
1259
- if (device.serverMode === 'server' && device.serverNode) {
1260
+ if (device.mode === 'server' && device.serverNode) {
1260
1261
  this.log.debug(`Starting server node for device ${dev}${device.deviceName}${db} in server mode...`);
1261
1262
  await this.startServerNode(device.serverNode);
1262
1263
  }
@@ -1277,13 +1278,15 @@ export class Matterbridge extends EventEmitter {
1277
1278
  }
1278
1279
  }
1279
1280
  this.frontend.wssSendRefreshRequired('plugins');
1280
- }, 30 * 1000);
1281
+ }, 30 * 1000).unref();
1281
1282
  this.reachabilityTimeout = setTimeout(() => {
1282
1283
  this.log.info(`Setting reachability to true for ${plg}Matterbridge${db}`);
1283
1284
  if (this.aggregatorNode)
1284
1285
  this.setAggregatorReachability(this.aggregatorNode, true);
1285
1286
  this.frontend.wssSendRefreshRequired('reachability');
1286
- }, 60 * 1000);
1287
+ }, 60 * 1000).unref();
1288
+ this.emit('bridge_started');
1289
+ this.log.notice('Matterbridge bridge started successfully');
1287
1290
  }, 1000);
1288
1291
  }
1289
1292
  async startChildbridge() {
@@ -1339,7 +1342,7 @@ export class Matterbridge extends EventEmitter {
1339
1342
  }
1340
1343
  }
1341
1344
  this.frontend.wssSendRefreshRequired('plugins');
1342
- }, 30 * 1000);
1345
+ }, 30 * 1000).unref();
1343
1346
  for (const plugin of this.plugins.array()) {
1344
1347
  if (!plugin.enabled || plugin.error)
1345
1348
  continue;
@@ -1365,14 +1368,16 @@ export class Matterbridge extends EventEmitter {
1365
1368
  if (plugin.type === 'DynamicPlatform' && plugin.aggregatorNode)
1366
1369
  this.setAggregatorReachability(plugin.aggregatorNode, true);
1367
1370
  this.frontend.wssSendRefreshRequired('reachability');
1368
- }, 60 * 1000);
1371
+ }, 60 * 1000).unref();
1369
1372
  }
1370
1373
  for (const device of this.devices.array()) {
1371
- if (device.serverMode === 'server' && device.serverNode) {
1374
+ if (device.mode === 'server' && device.serverNode) {
1372
1375
  this.log.debug(`***Starting server node for device ${plg}${device.deviceName}${db} in server mode...`);
1373
1376
  await this.startServerNode(device.serverNode);
1374
1377
  }
1375
1378
  }
1379
+ this.emit('childbridge_started');
1380
+ this.log.notice('Matterbridge childbridge started successfully');
1376
1381
  }, 1000);
1377
1382
  }
1378
1383
  async startController() {
@@ -1597,10 +1602,7 @@ export class Matterbridge extends EventEmitter {
1597
1602
  this.frontend.wssSendRefreshRequired('fabrics');
1598
1603
  });
1599
1604
  const sanitizeSessions = (sessions) => {
1600
- const sanitizedSessions = this.sanitizeSessionInformation(sessions.map((session) => ({
1601
- ...session,
1602
- secure: session.name.startsWith('secure'),
1603
- })));
1605
+ const sanitizedSessions = this.sanitizeSessionInformation(sessions);
1604
1606
  this.log.debug(`Sessions: ${debugStringify(sanitizedSessions)}`);
1605
1607
  if (this.bridgeMode === 'bridge') {
1606
1608
  this.matterbridgeSessionInformations = sanitizedSessions;
@@ -1702,7 +1704,7 @@ export class Matterbridge extends EventEmitter {
1702
1704
  this.log.error(`Error adding bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.id}${er}) plugin ${plg}${pluginName}${er} not found`);
1703
1705
  return;
1704
1706
  }
1705
- if (device.serverMode === 'server') {
1707
+ if (device.mode === 'server') {
1706
1708
  try {
1707
1709
  this.log.debug(`Creating server node for device ${dev}${device.deviceName}${db} of plugin ${plg}${plugin.name}${db}...`);
1708
1710
  await this.createDeviceServerNode(plugin, device);
@@ -1715,7 +1717,7 @@ export class Matterbridge extends EventEmitter {
1715
1717
  }
1716
1718
  }
1717
1719
  else if (this.bridgeMode === 'bridge') {
1718
- if (device.serverMode === 'matter') {
1720
+ if (device.mode === 'matter') {
1719
1721
  this.log.debug(`Adding matter endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} to Matterbridge server node...`);
1720
1722
  if (!this.serverNode) {
1721
1723
  this.log.error('Server node not found for Matterbridge');
@@ -1774,7 +1776,7 @@ export class Matterbridge extends EventEmitter {
1774
1776
  this.log.error(`Aggregator node not found for plugin ${plg}${plugin.name}${er}`);
1775
1777
  return;
1776
1778
  }
1777
- if (device.serverMode === 'matter')
1779
+ if (device.mode === 'matter')
1778
1780
  await plugin.serverNode?.add(device);
1779
1781
  else
1780
1782
  await plugin.aggregatorNode.add(device);
@@ -1870,8 +1872,8 @@ export class Matterbridge extends EventEmitter {
1870
1872
  };
1871
1873
  });
1872
1874
  }
1873
- sanitizeSessionInformation(sessionInfo) {
1874
- return sessionInfo
1875
+ sanitizeSessionInformation(session) {
1876
+ return session
1875
1877
  .filter((session) => session.isPeerActive)
1876
1878
  .map((session) => {
1877
1879
  return {
@@ -1890,7 +1892,6 @@ export class Matterbridge extends EventEmitter {
1890
1892
  }
1891
1893
  : undefined,
1892
1894
  isPeerActive: session.isPeerActive,
1893
- secure: session.secure,
1894
1895
  lastInteractionTimestamp: session.lastInteractionTimestamp?.toString(),
1895
1896
  lastActiveTimestamp: session.lastActiveTimestamp?.toString(),
1896
1897
  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;
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "matterbridge",
3
- "version": "3.0.8-dev-20250626-1445fbd",
3
+ "version": "3.0.8-dev-20250626-50aa686",
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.0.8-dev-20250626-50aa686",
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-alpha.0-20250626-fc3a84ce9",
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-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==",
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-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==",
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-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"
90
90
  },
91
91
  "optionalDependencies": {
92
- "@matter/nodejs": "0.15.0-alpha.0-20250625-c7634df96"
92
+ "@matter/nodejs": "0.15.0-alpha.0-20250626-fc3a84ce9"
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-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==",
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-alpha.0-20250626-fc3a84ce9"
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-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==",
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-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"
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-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==",
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-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"
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-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==",
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-alpha.0-20250626-fc3a84ce9",
139
+ "@matter/model": "0.15.0-alpha.0-20250626-fc3a84ce9",
140
+ "@matter/types": "0.15.0-alpha.0-20250626-fc3a84ce9"
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-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==",
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-alpha.0-20250626-fc3a84ce9",
150
+ "@matter/model": "0.15.0-alpha.0-20250626-fc3a84ce9"
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.0.8-dev-20250626-50aa686",
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-alpha.0-20250626-fc3a84ce9",
102
102
  "archiver": "7.0.1",
103
103
  "express": "5.1.0",
104
104
  "glob": "11.0.3",
@@ -0,0 +1,208 @@
1
+ import { vi, describe, it, expect, beforeEach, afterEach, MockInstance } from 'vitest';
2
+ import { Matterbridge } from '../src/matterbridge.ts';
3
+ import { AnsiLogger } from 'node-ansi-logger';
4
+
5
+ // Partial mock for @matter/main, preserving all actual exports except Logger
6
+ vi.mock('@matter/main', async (importOriginal) => {
7
+ const actual = await importOriginal();
8
+ return {
9
+ ...(actual as Record<string, unknown>),
10
+ Logger: {
11
+ log: vi.fn(),
12
+ info: vi.fn(),
13
+ debug: vi.fn(),
14
+ warn: vi.fn(),
15
+ error: vi.fn(),
16
+ fatal: vi.fn(),
17
+ addLogger: vi.fn(),
18
+ setLogger: vi.fn(),
19
+ removeLogger: vi.fn(),
20
+ toJSON: vi.fn(() => ''),
21
+ },
22
+ };
23
+ });
24
+
25
+ let loggerLogSpy: MockInstance<typeof AnsiLogger.prototype.log>;
26
+ let consoleLogSpy: MockInstance<typeof console.log>;
27
+ let consoleDebugSpy: MockInstance<typeof console.debug>;
28
+ let consoleInfoSpy: MockInstance<typeof console.info>;
29
+ let consoleWarnSpy: MockInstance<typeof console.warn>;
30
+ let consoleErrorSpy: MockInstance<typeof console.error>;
31
+ const debug = true; // Set to true to enable debug logging
32
+
33
+ if (!debug) {
34
+ loggerLogSpy = vi.spyOn(AnsiLogger.prototype, 'log').mockImplementation((level: string, message: string, ...parameters: any[]) => {});
35
+ consoleLogSpy = vi.spyOn(console, 'log').mockImplementation((...args: any[]) => {});
36
+ consoleDebugSpy = vi.spyOn(console, 'debug').mockImplementation((...args: any[]) => {});
37
+ consoleInfoSpy = vi.spyOn(console, 'info').mockImplementation((...args: any[]) => {});
38
+ consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation((...args: any[]) => {});
39
+ consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation((...args: any[]) => {});
40
+ } else {
41
+ loggerLogSpy = vi.spyOn(AnsiLogger.prototype, 'log');
42
+ consoleLogSpy = vi.spyOn(console, 'log');
43
+ consoleDebugSpy = vi.spyOn(console, 'debug');
44
+ consoleInfoSpy = vi.spyOn(console, 'info');
45
+ consoleWarnSpy = vi.spyOn(console, 'warn');
46
+ consoleErrorSpy = vi.spyOn(console, 'error');
47
+ }
48
+
49
+ describe('Matterbridge', () => {
50
+ let matterbridge: Matterbridge;
51
+
52
+ beforeEach(async () => {
53
+ matterbridge = await Matterbridge.loadInstance(false);
54
+ // 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;
56
+ matterbridge.plugins = { array: () => [], clear: vi.fn(), [Symbol.iterator]: function* () {} } as any;
57
+ matterbridge.devices = { array: () => [], clear: vi.fn(), [Symbol.iterator]: function* () {} } as any;
58
+ matterbridge.frontend = {
59
+ start: vi.fn(),
60
+ stop: vi.fn(),
61
+ wssSendRefreshRequired: vi.fn(),
62
+ wssSendRestartRequired: vi.fn(),
63
+ wssSendSnackbarMessage: vi.fn(),
64
+ } as any;
65
+ });
66
+
67
+ afterEach(() => {
68
+ vi.clearAllMocks();
69
+ });
70
+
71
+ it('should initialize system and matterbridge information', () => {
72
+ expect(matterbridge.systemInformation).toBeDefined();
73
+ expect(matterbridge.matterbridgeInformation).toBeDefined();
74
+ });
75
+
76
+ it('should emit initialize_completed after initialize', async () => {
77
+ const spy = vi.fn();
78
+ matterbridge.on('initialize_completed', spy);
79
+ await matterbridge.initialize();
80
+ expect(spy).toHaveBeenCalled();
81
+ expect((matterbridge as any)['initialized']).toBe(true);
82
+ });
83
+
84
+ it('should call parseCommandLine during initialize', async () => {
85
+ const spy = vi.spyOn(matterbridge as any, 'parseCommandLine').mockResolvedValue(undefined);
86
+ await matterbridge.initialize();
87
+ expect(spy).toHaveBeenCalled();
88
+ });
89
+
90
+ it('should log system info in logNodeAndSystemInfo', async () => {
91
+ await (matterbridge as any)['logNodeAndSystemInfo']();
92
+ expect(matterbridge.log.debug).toHaveBeenCalled();
93
+ });
94
+
95
+ it('should register and deregister process handlers', () => {
96
+ (matterbridge as any)['registerProcessHandlers']();
97
+ (matterbridge as any)['deregisterProcessHandlers']();
98
+ expect(matterbridge.log.error).not.toHaveBeenCalled();
99
+ });
100
+
101
+ it('should call cleanup and emit cleanup events', async () => {
102
+ (matterbridge as any)['initialized'] = true;
103
+ (matterbridge as any)['hasCleanupStarted'] = false;
104
+ const spyStart = vi.fn();
105
+ const spyComplete = vi.fn();
106
+ matterbridge.on('cleanup_started', spyStart);
107
+ matterbridge.on('cleanup_completed', spyComplete);
108
+ await (matterbridge as any)['cleanup']('test cleanup');
109
+ expect(spyStart).toHaveBeenCalled();
110
+ expect(spyComplete).toHaveBeenCalled();
111
+ });
112
+
113
+ it('should not run cleanup if already started', async () => {
114
+ (matterbridge as any)['initialized'] = true;
115
+ (matterbridge as any)['hasCleanupStarted'] = true;
116
+ const debugSpy = matterbridge.log.debug as any;
117
+ await (matterbridge as any)['cleanup']('test cleanup');
118
+ expect(debugSpy).toHaveBeenCalledWith('Cleanup already started...');
119
+ });
120
+
121
+ it('should get devices using getDevices()', () => {
122
+ const fakeDevices = [{ id: 1 }, { id: 2 }];
123
+ matterbridge.devices.array = vi.fn(() => fakeDevices);
124
+ expect(matterbridge.getDevices()).toBe(fakeDevices);
125
+ });
126
+
127
+ it('should get plugins using getPlugins()', () => {
128
+ const fakePlugins = [{ name: 'p1' }, { name: 'p2' }];
129
+ matterbridge.plugins.array = vi.fn(() => fakePlugins);
130
+ expect(matterbridge.getPlugins()).toBe(fakePlugins);
131
+ });
132
+
133
+ it('should set log level and propagate to dependencies', async () => {
134
+ 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 };
139
+ 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);
144
+ expect(matterbridge.log.debug).toHaveBeenCalled();
145
+ });
146
+
147
+ it('should emit and listen to custom events', () => {
148
+ const handler = vi.fn();
149
+ matterbridge.on('online', handler);
150
+ matterbridge.emit('online', 'nodeid');
151
+ expect(handler).toHaveBeenCalledWith('nodeid');
152
+ });
153
+
154
+ it('should call destroyInstance and cleanup', async () => {
155
+ const cleanupSpy = vi.spyOn(matterbridge as any, 'cleanup').mockResolvedValue(undefined);
156
+ matterbridge.log.info = vi.fn();
157
+ await matterbridge.destroyInstance();
158
+ expect(cleanupSpy).toHaveBeenCalled();
159
+ expect(matterbridge.log.info).toHaveBeenCalledWith(expect.stringContaining('Destroy instance...'));
160
+ });
161
+
162
+ it('should call restartProcess and cleanup', async () => {
163
+ const cleanupSpy = vi.spyOn(matterbridge as any, 'cleanup').mockResolvedValue(undefined);
164
+ await matterbridge.restartProcess();
165
+ expect(cleanupSpy).toHaveBeenCalledWith('restarting...', true);
166
+ });
167
+
168
+ it('should call shutdownProcess and cleanup', async () => {
169
+ const cleanupSpy = vi.spyOn(matterbridge as any, 'cleanup').mockResolvedValue(undefined);
170
+ await matterbridge.shutdownProcess();
171
+ expect(cleanupSpy).toHaveBeenCalledWith('shutting down...', false);
172
+ });
173
+
174
+ it('should call updateProcess and cleanup', async () => {
175
+ const cleanupSpy = vi.spyOn(matterbridge as any, 'cleanup').mockResolvedValue(undefined);
176
+ matterbridge.log.info = vi.fn();
177
+ (matterbridge.frontend as any).wssSendRestartRequired = vi.fn();
178
+ await matterbridge.updateProcess();
179
+ expect(cleanupSpy).toHaveBeenCalledWith('updating...', false);
180
+ expect(matterbridge.log.info).toHaveBeenCalledWith(expect.stringContaining('Updating matterbridge...'));
181
+ expect((matterbridge.frontend as any).wssSendRestartRequired).toHaveBeenCalled();
182
+ });
183
+
184
+ it('should call unregisterAndShutdownProcess and cleanup', async () => {
185
+ const cleanupSpy = vi.spyOn(matterbridge as any, 'cleanup').mockResolvedValue(undefined);
186
+ matterbridge.log.info = vi.fn();
187
+ matterbridge.plugins = [{ name: 'p1' }];
188
+ matterbridge.removeAllBridgedEndpoints = vi.fn().mockResolvedValue(undefined);
189
+ matterbridge.devices.clear = vi.fn();
190
+ await matterbridge.unregisterAndShutdownProcess();
191
+ expect(cleanupSpy).toHaveBeenCalledWith('unregistered all devices and shutting down...', false);
192
+ expect(matterbridge.log.info).toHaveBeenCalledWith(expect.stringContaining('Unregistering all devices and shutting down...'));
193
+ });
194
+
195
+ it('should call shutdownProcessAndReset and cleanup', async () => {
196
+ const cleanupSpy = vi.spyOn(matterbridge as any, 'cleanup').mockResolvedValue(undefined);
197
+ await matterbridge.shutdownProcessAndReset();
198
+ expect(cleanupSpy).toHaveBeenCalledWith('shutting down with reset...', false);
199
+ });
200
+
201
+ it('should call shutdownProcessAndFactoryReset and cleanup', async () => {
202
+ const cleanupSpy = vi.spyOn(matterbridge as any, 'cleanup').mockResolvedValue(undefined);
203
+ await matterbridge.shutdownProcessAndFactoryReset();
204
+ expect(cleanupSpy).toHaveBeenCalledWith('shutting down with factory reset...', false);
205
+ });
206
+
207
+ // Add more tests for all public/protected methods to reach 100% coverage
208
+ });