matterbridge 3.0.8-dev-20250626-50aa686 → 3.1.0-dev-20250627-2b5adba
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -3
- package/README-DEV.md +5 -1
- package/dist/deviceManager.js +1 -3
- package/dist/matterbridge.js +27 -57
- package/dist/pluginManager.js +9 -4
- package/npm-shrinkwrap.json +44 -44
- package/package.json +2 -2
- package/vitest/matterbridge.test.ts +26 -18
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
|
|
|
@@ -23,6 +23,8 @@ If you like this project and find it useful, please consider giving it a star on
|
|
|
23
23
|
- [JSDoc]: Added missing JSDoc comments, including `@param` and `@returns` tags.
|
|
24
24
|
- [MatterbridgeEndpoint]: Add MatterbridgeEndpoint mode='server'. It allows to advertise a single device like an autonomous device with its server node to be paired.
|
|
25
25
|
- [MatterbridgeEndpoint]: Add MatterbridgeEndpoint mode='matter'. It allows to add a single device to the Matterbridge server node next to the aggregator. The device is not bridged.
|
|
26
|
+
- [storage]: Improved error handling of corrupted storage.
|
|
27
|
+
- [test]: Improved test units on Matterbridge classes.
|
|
26
28
|
|
|
27
29
|
### Changed
|
|
28
30
|
|
|
@@ -30,8 +32,7 @@ If you like this project and find it useful, please consider giving it a star on
|
|
|
30
32
|
- [package]: Updated dependencies.
|
|
31
33
|
- [storage]: Bumped `node-storage-manager` to 2.0.0.
|
|
32
34
|
- [logger]: Bumped `node-ansi-logger` to 3.1.1.
|
|
33
|
-
- [matter.js]: Updated to 0.15.0-alpha.0-20250625-c7634df96.
|
|
34
|
-
- [matter.js]: Updated to 0.15.0-alpha.0-20250626-fc3a84ce9.
|
|
35
|
+
- [matter.js]: Updated to 0.15.0-alpha.0-20250625-c7634df96, 0.15.0-alpha.0-20250626-fc3a84ce9, and bumped to 0.15.0.
|
|
35
36
|
|
|
36
37
|
### Fixed
|
|
37
38
|
|
package/README-DEV.md
CHANGED
|
@@ -34,6 +34,10 @@ Using a Dev Container provides a fully isolated, reproducible, and pre-configure
|
|
|
34
34
|
|
|
35
35
|
For improved efficiency, the setup uses named Docker volumes for `node_modules`. This means dependencies are installed only once and persist across container rebuilds, making installs and rebuilds much faster than with bind mounts or ephemeral volumes.
|
|
36
36
|
|
|
37
|
+
To start the Dev Container, simply open the project folder in [Visual Studio Code](https://code.visualstudio.com/) and, if prompted, click "Reopen in Container". Alternatively, use the Command Palette (`Ctrl+Shift+P` or `Cmd+Shift+P`), search for "Dev Containers: Reopen in Container", and select it. VS Code will automatically build and start the containerized environment for you.
|
|
38
|
+
|
|
39
|
+
> **Note:** The first time you use the Dev Container, it may take a while to download all the required Docker images and set up the environment. Subsequent starts will be much faster.
|
|
40
|
+
|
|
37
41
|
Since Dev Container doesn't run in network mode 'host', it is not possible to pair Mattebridge running inside the Dev Container.
|
|
38
42
|
|
|
39
43
|
## Guidelines on imports/exports
|
|
@@ -247,7 +251,7 @@ It can be useful to call this method from onShutdown() if you don't want to keep
|
|
|
247
251
|
|
|
248
252
|
## MatterbridgeEndpoint api
|
|
249
253
|
|
|
250
|
-
You create a device with a new instance of MatterbridgeEndpoint(definition: DeviceTypeDefinition | AtLeastOne<DeviceTypeDefinition>, options: MatterbridgeEndpointOptions = {}, debug: boolean = false).
|
|
254
|
+
You create a Matter device with a new instance of MatterbridgeEndpoint(definition: DeviceTypeDefinition | AtLeastOne<DeviceTypeDefinition>, options: MatterbridgeEndpointOptions = {}, debug: boolean = false).
|
|
251
255
|
|
|
252
256
|
- @param {DeviceTypeDefinition | AtLeastOne<DeviceTypeDefinition>} definition - The DeviceTypeDefinition(s) of the endpoint.
|
|
253
257
|
- @param {MatterbridgeEndpointOptions} [options] - The options for the device.
|
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') {
|
|
@@ -211,7 +205,10 @@ export class Matterbridge extends EventEmitter {
|
|
|
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);
|
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",
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { vi, describe, it, expect, beforeEach, afterEach, MockInstance } from 'vitest';
|
|
2
2
|
import { Matterbridge } from '../src/matterbridge.ts';
|
|
3
|
-
import { AnsiLogger } from 'node-ansi-logger';
|
|
3
|
+
import { AnsiLogger, LogLevel, TimestampFormat } from 'node-ansi-logger';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import spawn from '../src/utils/spawn.ts';
|
|
6
|
+
|
|
7
|
+
const NAME = 'ViMatterbridgeGlobal';
|
|
8
|
+
const HOMEDIR = path.join('jest', NAME);
|
|
9
|
+
|
|
10
|
+
process.argv = ['node', 'matterbridge.test.js', '-novirtual', '-frontend', '0', '-homedir', HOMEDIR, '-logger', 'debug', '-matterlogger', 'debug'];
|
|
4
11
|
|
|
5
12
|
// Partial mock for @matter/main, preserving all actual exports except Logger
|
|
6
13
|
vi.mock('@matter/main', async (importOriginal) => {
|
|
@@ -28,7 +35,7 @@ let consoleDebugSpy: MockInstance<typeof console.debug>;
|
|
|
28
35
|
let consoleInfoSpy: MockInstance<typeof console.info>;
|
|
29
36
|
let consoleWarnSpy: MockInstance<typeof console.warn>;
|
|
30
37
|
let consoleErrorSpy: MockInstance<typeof console.error>;
|
|
31
|
-
const debug =
|
|
38
|
+
const debug = false; // Set to true to enable debug logging
|
|
32
39
|
|
|
33
40
|
if (!debug) {
|
|
34
41
|
loggerLogSpy = vi.spyOn(AnsiLogger.prototype, 'log').mockImplementation((level: string, message: string, ...parameters: any[]) => {});
|
|
@@ -52,7 +59,8 @@ describe('Matterbridge', () => {
|
|
|
52
59
|
beforeEach(async () => {
|
|
53
60
|
matterbridge = await Matterbridge.loadInstance(false);
|
|
54
61
|
// Set up required properties/mocks
|
|
55
|
-
matterbridge.log = { debug: vi.fn(), info: vi.fn(), notice: vi.fn(), warn: vi.fn(), error: vi.fn(), fatal: vi.fn() } as any;
|
|
62
|
+
matterbridge.log = { debug: vi.fn(), info: vi.fn(), notice: vi.fn(), warn: vi.fn(), error: vi.fn(), fatal: vi.fn(), now: vi.fn() } as any;
|
|
63
|
+
// matterbridge.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: TimestampFormat.TIME_MILLIS, logLevel: LogLevel.DEBUG });
|
|
56
64
|
matterbridge.plugins = { array: () => [], clear: vi.fn(), [Symbol.iterator]: function* () {} } as any;
|
|
57
65
|
matterbridge.devices = { array: () => [], clear: vi.fn(), [Symbol.iterator]: function* () {} } as any;
|
|
58
66
|
matterbridge.frontend = {
|
|
@@ -61,6 +69,7 @@ describe('Matterbridge', () => {
|
|
|
61
69
|
wssSendRefreshRequired: vi.fn(),
|
|
62
70
|
wssSendRestartRequired: vi.fn(),
|
|
63
71
|
wssSendSnackbarMessage: vi.fn(),
|
|
72
|
+
wssSendMessage: vi.fn(),
|
|
64
73
|
} as any;
|
|
65
74
|
});
|
|
66
75
|
|
|
@@ -120,27 +129,27 @@ describe('Matterbridge', () => {
|
|
|
120
129
|
|
|
121
130
|
it('should get devices using getDevices()', () => {
|
|
122
131
|
const fakeDevices = [{ id: 1 }, { id: 2 }];
|
|
123
|
-
matterbridge.devices.array = vi.fn(() => fakeDevices);
|
|
132
|
+
matterbridge.devices.array = vi.fn(() => fakeDevices) as any;
|
|
124
133
|
expect(matterbridge.getDevices()).toBe(fakeDevices);
|
|
125
134
|
});
|
|
126
135
|
|
|
127
136
|
it('should get plugins using getPlugins()', () => {
|
|
128
137
|
const fakePlugins = [{ name: 'p1' }, { name: 'p2' }];
|
|
129
|
-
matterbridge.plugins.array = vi.fn(() => fakePlugins);
|
|
138
|
+
matterbridge.plugins.array = vi.fn(() => fakePlugins) as any;
|
|
130
139
|
expect(matterbridge.getPlugins()).toBe(fakePlugins);
|
|
131
140
|
});
|
|
132
141
|
|
|
133
142
|
it('should set log level and propagate to dependencies', async () => {
|
|
134
143
|
const setLevel = vi.fn();
|
|
135
|
-
matterbridge.frontend.logLevel =
|
|
136
|
-
matterbridge.devices.logLevel =
|
|
137
|
-
matterbridge.plugins.logLevel =
|
|
138
|
-
(global as any).MatterbridgeEndpoint = { logLevel:
|
|
144
|
+
matterbridge.frontend.logLevel = LogLevel.NONE; // Set to NONE initially
|
|
145
|
+
matterbridge.devices.logLevel = LogLevel.NONE;
|
|
146
|
+
matterbridge.plugins.logLevel = LogLevel.NONE;
|
|
147
|
+
(global as any).MatterbridgeEndpoint = { logLevel: LogLevel.NONE };
|
|
139
148
|
matterbridge.log = { debug: vi.fn() } as any;
|
|
140
|
-
await matterbridge.setLogLevel(
|
|
141
|
-
expect(matterbridge.frontend.logLevel).toBe(
|
|
142
|
-
expect(matterbridge.devices.logLevel).toBe(
|
|
143
|
-
expect(matterbridge.plugins.logLevel).toBe(
|
|
149
|
+
await matterbridge.setLogLevel(LogLevel.INFO);
|
|
150
|
+
expect(matterbridge.frontend.logLevel).toBe(LogLevel.INFO);
|
|
151
|
+
expect(matterbridge.devices.logLevel).toBe(LogLevel.INFO);
|
|
152
|
+
expect(matterbridge.plugins.logLevel).toBe(LogLevel.INFO);
|
|
144
153
|
expect(matterbridge.log.debug).toHaveBeenCalled();
|
|
145
154
|
});
|
|
146
155
|
|
|
@@ -154,7 +163,7 @@ describe('Matterbridge', () => {
|
|
|
154
163
|
it('should call destroyInstance and cleanup', async () => {
|
|
155
164
|
const cleanupSpy = vi.spyOn(matterbridge as any, 'cleanup').mockResolvedValue(undefined);
|
|
156
165
|
matterbridge.log.info = vi.fn();
|
|
157
|
-
await matterbridge.destroyInstance();
|
|
166
|
+
await matterbridge.destroyInstance(10);
|
|
158
167
|
expect(cleanupSpy).toHaveBeenCalled();
|
|
159
168
|
expect(matterbridge.log.info).toHaveBeenCalledWith(expect.stringContaining('Destroy instance...'));
|
|
160
169
|
});
|
|
@@ -172,10 +181,11 @@ describe('Matterbridge', () => {
|
|
|
172
181
|
});
|
|
173
182
|
|
|
174
183
|
it('should call updateProcess and cleanup', async () => {
|
|
184
|
+
const spawnSpy = vi.spyOn(spawn, 'spawnCommand').mockResolvedValueOnce(true);
|
|
175
185
|
const cleanupSpy = vi.spyOn(matterbridge as any, 'cleanup').mockResolvedValue(undefined);
|
|
176
186
|
matterbridge.log.info = vi.fn();
|
|
177
|
-
(matterbridge.frontend as any).wssSendRestartRequired = vi.fn();
|
|
178
187
|
await matterbridge.updateProcess();
|
|
188
|
+
expect(spawnSpy).toHaveBeenCalledWith(matterbridge, 'npm', ['install', '-g', 'matterbridge', '--omit=dev', '--verbose']);
|
|
179
189
|
expect(cleanupSpy).toHaveBeenCalledWith('updating...', false);
|
|
180
190
|
expect(matterbridge.log.info).toHaveBeenCalledWith(expect.stringContaining('Updating matterbridge...'));
|
|
181
191
|
expect((matterbridge.frontend as any).wssSendRestartRequired).toHaveBeenCalled();
|
|
@@ -184,7 +194,7 @@ describe('Matterbridge', () => {
|
|
|
184
194
|
it('should call unregisterAndShutdownProcess and cleanup', async () => {
|
|
185
195
|
const cleanupSpy = vi.spyOn(matterbridge as any, 'cleanup').mockResolvedValue(undefined);
|
|
186
196
|
matterbridge.log.info = vi.fn();
|
|
187
|
-
matterbridge.plugins = [{ name: 'p1' }];
|
|
197
|
+
matterbridge.plugins = [{ name: 'p1' }] as any;
|
|
188
198
|
matterbridge.removeAllBridgedEndpoints = vi.fn().mockResolvedValue(undefined);
|
|
189
199
|
matterbridge.devices.clear = vi.fn();
|
|
190
200
|
await matterbridge.unregisterAndShutdownProcess();
|
|
@@ -203,6 +213,4 @@ describe('Matterbridge', () => {
|
|
|
203
213
|
await matterbridge.shutdownProcessAndFactoryReset();
|
|
204
214
|
expect(cleanupSpy).toHaveBeenCalledWith('shutting down with factory reset...', false);
|
|
205
215
|
});
|
|
206
|
-
|
|
207
|
-
// Add more tests for all public/protected methods to reach 100% coverage
|
|
208
216
|
});
|