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 +6 -4
- package/README-DEV.md +32 -1
- package/README.md +2 -1
- package/dist/deviceManager.js +1 -3
- package/dist/matterbridge.js +49 -78
- package/dist/matterbridgeEndpoint.js +17 -6
- package/dist/pluginManager.js +9 -4
- package/npm-shrinkwrap.json +44 -44
- package/package.json +2 -2
- package/vitest/matterbridge.test.ts +216 -0
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
|
|
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
|
|
25
|
-
- [MatterbridgeEndpoint]: Add MatterbridgeEndpoint
|
|
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
|
-
|
|
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
|
[](https://hub.docker.com/r/luligu/matterbridge)
|
|
6
6
|
[](https://hub.docker.com/r/luligu/matterbridge)
|
|
7
7
|

|
|
8
|
-

|
|
9
|
+
[](https://codecov.io/gh/Luligu/matterbridge)
|
|
9
10
|
|
|
10
11
|
[](https://www.npmjs.com/package/matter-history)
|
|
11
12
|
[](https://www.npmjs.com/package/node-ansi-logger)
|
package/dist/deviceManager.js
CHANGED
|
@@ -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
|
|
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
|
}
|
package/dist/matterbridge.js
CHANGED
|
@@ -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,
|
|
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
|
|
93
|
-
matterbridgeManualPairingCode
|
|
94
|
-
matterbridgeFabricInformations
|
|
95
|
-
matterbridgeSessionInformations
|
|
96
|
-
matterbridgePaired
|
|
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.
|
|
203
|
+
if (device.mode === 'server' && device.serverNode)
|
|
210
204
|
servers.push(device.serverNode);
|
|
211
205
|
}
|
|
212
206
|
}
|
|
213
207
|
await Promise.resolve();
|
|
214
|
-
await
|
|
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,
|
|
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
|
|
284
|
-
|
|
285
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1187
|
-
device.serverNode = await this.createServerNode(
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
1874
|
-
return
|
|
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
|
-
|
|
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.
|
|
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;
|
package/dist/pluginManager.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
});
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "matterbridge",
|
|
3
|
-
"version": "3.0
|
|
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
|
|
9
|
+
"version": "3.1.0-dev-20250627-2b5adba",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@matter/main": "0.15.0
|
|
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
|
|
72
|
-
"resolved": "https://registry.npmjs.org/@matter/general/-/general-0.15.0
|
|
73
|
-
"integrity": "sha512-
|
|
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
|
|
81
|
-
"resolved": "https://registry.npmjs.org/@matter/main/-/main-0.15.0
|
|
82
|
-
"integrity": "sha512
|
|
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
|
|
86
|
-
"@matter/model": "0.15.0
|
|
87
|
-
"@matter/node": "0.15.0
|
|
88
|
-
"@matter/protocol": "0.15.0
|
|
89
|
-
"@matter/types": "0.15.0
|
|
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
|
|
92
|
+
"@matter/nodejs": "0.15.0"
|
|
93
93
|
}
|
|
94
94
|
},
|
|
95
95
|
"node_modules/@matter/model": {
|
|
96
|
-
"version": "0.15.0
|
|
97
|
-
"resolved": "https://registry.npmjs.org/@matter/model/-/model-0.15.0
|
|
98
|
-
"integrity": "sha512-
|
|
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
|
|
101
|
+
"@matter/general": "0.15.0"
|
|
102
102
|
}
|
|
103
103
|
},
|
|
104
104
|
"node_modules/@matter/node": {
|
|
105
|
-
"version": "0.15.0
|
|
106
|
-
"resolved": "https://registry.npmjs.org/@matter/node/-/node-0.15.0
|
|
107
|
-
"integrity": "sha512-
|
|
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
|
|
111
|
-
"@matter/model": "0.15.0
|
|
112
|
-
"@matter/protocol": "0.15.0
|
|
113
|
-
"@matter/types": "0.15.0
|
|
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
|
|
118
|
-
"resolved": "https://registry.npmjs.org/@matter/nodejs/-/nodejs-0.15.0
|
|
119
|
-
"integrity": "sha512-
|
|
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
|
|
124
|
-
"@matter/node": "0.15.0
|
|
125
|
-
"@matter/protocol": "0.15.0
|
|
126
|
-
"@matter/types": "0.15.0
|
|
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
|
|
134
|
-
"resolved": "https://registry.npmjs.org/@matter/protocol/-/protocol-0.15.0
|
|
135
|
-
"integrity": "sha512-
|
|
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
|
|
139
|
-
"@matter/model": "0.15.0
|
|
140
|
-
"@matter/types": "0.15.0
|
|
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
|
|
145
|
-
"resolved": "https://registry.npmjs.org/@matter/types/-/types-0.15.0
|
|
146
|
-
"integrity": "sha512-
|
|
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
|
|
150
|
-
"@matter/model": "0.15.0
|
|
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
|
|
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
|
|
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
|
+
});
|