diff --git a/CHANGELOG.md b/CHANGELOG.md
index 848990a3..532b8cc7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,16 +21,35 @@ Starting from v. 2.1.0, the legacy old api of matter.js have been completely rem
For this reason there is no compatibility with the old versions of the plugins.
-You need to update all plugins you use and Matterbridge in the same moment.
+You need to update all plugins you use and Matterbridge in the same moment.
I suggest to first update all plugins without restarting and then to update Matterbridge so when it restarts, all versions will be the latest.
+If you use docker, all plugins are already installed in the image so you just need to pull the new image.
+
Compatibility list:
matterbridge-shelly v. 1.1.5
matterbridge-zigbee2mqtt v. 2.4.4
matterbridge-somfy-tahoma v. 1.2.3
matterbridge-hass v. 0.0.8
+## [2.1.4] - 2025-02-07
+
+### Added
+
+- [frontend]: Added memorycheck before cleanup.
+- [platform]: Added a check for not latin characters.
+- [platform]: Added a check for already registered device names.
+
+### Changed
+
+- [package]: Update matter.js to 0.12.3.
+- [matter.js]: Since matter.js storage cannot properly encode non latin names, they are encoded before passing them to matter.js.
+
+
+
+
+
## [2.1.3] - 2025-02-04
### Added
diff --git a/README-DOCKER.md b/README-DOCKER.md
index 5d29e280..77722567 100644
--- a/README-DOCKER.md
+++ b/README-DOCKER.md
@@ -48,11 +48,13 @@ The container must have full access to the host network (needed for mdns).
```
sudo docker run --name matterbridge \
- -v ${HOME}/Matterbridge:/root/Matterbridge \
- -v ${HOME}/.matterbridge:/root/.matterbridge \
+ -v /home//Matterbridge:/root/Matterbridge \
+ -v /home//.matterbridge:/root/.matterbridge \
--network host --restart always -d luligu/matterbridge:latest
```
+Replace USER with your user name (i.e. ubuntu or pi).
+
You may need to adapt the script to your setup.
### Run with docker compose
@@ -63,14 +65,16 @@ The docker-compose.yml file is available in the docker directory of the package
services:
matterbridge:
container_name: matterbridge
- image: luligu/matterbridge:latest # Matterbridge image with the latest tag
- network_mode: host # Ensures the Matter mdns works
- restart: always # Ensures the container always restarts automatically
+ image: luligu/matterbridge:latest # Matterbridge image with the tag latest
+ network_mode: host # Ensures the Matter mdns works
+ restart: always # Ensures the container always restarts automatically
volumes:
- - "${HOME}/Matterbridge:/root/Matterbridge" # Mounts the Matterbridge plugin directory
- - "${HOME}/.matterbridge:/root/.matterbridge" # Mounts the Matterbridge storage directory
+ - "/home//Matterbridge:/root/Matterbridge" # Mounts the Matterbridge plugin directory
+ - "/home//.matterbridge:/root/.matterbridge" # Mounts the Matterbridge storage directory
```
+Replace USER with your user name (i.e. ubuntu or pi).
+
copy it in the home directory or edit the existing one to add the matterbridge service.
Then start docker compose with:
diff --git a/package-lock.json b/package-lock.json
index a9f4770f..68a4cb16 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,15 +1,15 @@
{
"name": "matterbridge",
- "version": "2.1.3",
+ "version": "2.1.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "matterbridge",
- "version": "2.1.3",
+ "version": "2.1.4",
"license": "Apache-2.0",
"dependencies": {
- "@matter/main": "0.12.2",
+ "@matter/main": "0.12.3",
"archiver": "7.0.1",
"express": "4.21.2",
"glob": "11.0.1",
@@ -1319,65 +1319,65 @@
}
},
"node_modules/@matter/general": {
- "version": "0.12.2",
- "resolved": "https://registry.npmjs.org/@matter/general/-/general-0.12.2.tgz",
- "integrity": "sha512-D8S2CQHD7KI8L9lxeRgFCLDziXZRAEHqAJei7e0d5Jqh5thPH6S2HUcSX+je6EZDFTshguMgboW09lhXnUR8lA==",
+ "version": "0.12.3",
+ "resolved": "https://registry.npmjs.org/@matter/general/-/general-0.12.3.tgz",
+ "integrity": "sha512-1dya8bKRAhNkXD5xDWapVSO4Wfug8Qw2RWw74NZQNtGKbyBuNQsCnShtOdYGLnkjf/7XUGsYGmCG8hKepBHnbQ==",
"license": "Apache-2.0",
"dependencies": {
"@noble/curves": "^1.8.1"
}
},
"node_modules/@matter/main": {
- "version": "0.12.2",
- "resolved": "https://registry.npmjs.org/@matter/main/-/main-0.12.2.tgz",
- "integrity": "sha512-bMHBLKmXpLmV/bkOxyhc+j+BhagFg5W6x/TJjfdZv0dZolkD1yT5zGZClmdGaLhhvyuYEKGKlecCM9uQksgxnA==",
+ "version": "0.12.3",
+ "resolved": "https://registry.npmjs.org/@matter/main/-/main-0.12.3.tgz",
+ "integrity": "sha512-PjbSuQfm7pv/opqODgU7JhA5Gj/6TxiFiDr+i4NWFit+/NyVorDpjCKTpCZmj3XD7QhEp0iKeDG7KKBqrQ4TUQ==",
"license": "Apache-2.0",
"dependencies": {
- "@matter/general": "0.12.2",
- "@matter/model": "0.12.2",
- "@matter/node": "0.12.2",
- "@matter/protocol": "0.12.2",
- "@matter/types": "0.12.2",
+ "@matter/general": "0.12.3",
+ "@matter/model": "0.12.3",
+ "@matter/node": "0.12.3",
+ "@matter/protocol": "0.12.3",
+ "@matter/types": "0.12.3",
"@noble/curves": "^1.8.1"
},
"optionalDependencies": {
- "@matter/nodejs": "0.12.2"
+ "@matter/nodejs": "0.12.3"
}
},
"node_modules/@matter/model": {
- "version": "0.12.2",
- "resolved": "https://registry.npmjs.org/@matter/model/-/model-0.12.2.tgz",
- "integrity": "sha512-xM8hwqVepb6YpvjE9jXacA4YypPHJm0latei7YTZystgvh8lUHDk5dkLELyrXzbNYTs//LMPAbj6SIsz9CorPA==",
+ "version": "0.12.3",
+ "resolved": "https://registry.npmjs.org/@matter/model/-/model-0.12.3.tgz",
+ "integrity": "sha512-wuiBuUR45lno2PMx1YPhwZyaG7+20iBlb5SlnzYfI5j6fJv1WyYv22APDDPKDglPj4rP2RABCyCUfd1/K5EfXg==",
"license": "Apache-2.0",
"dependencies": {
- "@matter/general": "0.12.2",
+ "@matter/general": "0.12.3",
"@noble/curves": "^1.8.1"
}
},
"node_modules/@matter/node": {
- "version": "0.12.2",
- "resolved": "https://registry.npmjs.org/@matter/node/-/node-0.12.2.tgz",
- "integrity": "sha512-/VHnBHf6bFf6fVP6SvTrJDtKCzfZ6j5DTuEPRQFbdH1ph7OGyzVXjVRwoLPTzo2XT/VrTPcFNB38UNxTky1zQA==",
+ "version": "0.12.3",
+ "resolved": "https://registry.npmjs.org/@matter/node/-/node-0.12.3.tgz",
+ "integrity": "sha512-+bIYQtjCR053lgCd8mlnIp6ufm+H+zJGlZp8cdMX6IkkvT0vRqF2QxDtHpQn8CtzjnFzg+O5j0R4jdss2SluXg==",
"license": "Apache-2.0",
"dependencies": {
- "@matter/general": "0.12.2",
- "@matter/model": "0.12.2",
- "@matter/protocol": "0.12.2",
- "@matter/types": "0.12.2",
+ "@matter/general": "0.12.3",
+ "@matter/model": "0.12.3",
+ "@matter/protocol": "0.12.3",
+ "@matter/types": "0.12.3",
"@noble/curves": "^1.8.1"
}
},
"node_modules/@matter/nodejs": {
- "version": "0.12.2",
- "resolved": "https://registry.npmjs.org/@matter/nodejs/-/nodejs-0.12.2.tgz",
- "integrity": "sha512-sMnczk+3+7tr5OQDntPP7TuZP83/jC9jG713J1iJxU77cv/GMKIecKfAOrbFl50Ga+4zZTfIV9Mpekn50IT0ig==",
+ "version": "0.12.3",
+ "resolved": "https://registry.npmjs.org/@matter/nodejs/-/nodejs-0.12.3.tgz",
+ "integrity": "sha512-Jx6TIYvl+2WlD9Zh6eMdClsjoPQjIkR3qhYGPbbhHlO7QXZWZz7d83jo1yShHWQrqnnfqqrkXy6iLBphrZ4SSg==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
- "@matter/general": "0.12.2",
- "@matter/node": "0.12.2",
- "@matter/protocol": "0.12.2",
- "@matter/types": "0.12.2",
+ "@matter/general": "0.12.3",
+ "@matter/node": "0.12.3",
+ "@matter/protocol": "0.12.3",
+ "@matter/types": "0.12.3",
"node-localstorage": "^3.0.5"
},
"engines": {
@@ -1385,25 +1385,25 @@
}
},
"node_modules/@matter/protocol": {
- "version": "0.12.2",
- "resolved": "https://registry.npmjs.org/@matter/protocol/-/protocol-0.12.2.tgz",
- "integrity": "sha512-BhfPqRgnNy+Fx6VWffFtA8aGdspvzxp+TY5PJpAqExBLwGB/kc/knagOOfLvSjSyT/gdZmIxep3Iw2ycm7UYYA==",
+ "version": "0.12.3",
+ "resolved": "https://registry.npmjs.org/@matter/protocol/-/protocol-0.12.3.tgz",
+ "integrity": "sha512-mUL6rSXdm0LaS/i6bxjBQlZ/0fi8hXoinBytbKNkztfDOB9I3FeNlgp9FJxZ+ZJtrsPleMg4q+loLU7uNQi30w==",
"license": "Apache-2.0",
"dependencies": {
- "@matter/general": "0.12.2",
- "@matter/model": "0.12.2",
- "@matter/types": "0.12.2",
+ "@matter/general": "0.12.3",
+ "@matter/model": "0.12.3",
+ "@matter/types": "0.12.3",
"@noble/curves": "^1.8.1"
}
},
"node_modules/@matter/types": {
- "version": "0.12.2",
- "resolved": "https://registry.npmjs.org/@matter/types/-/types-0.12.2.tgz",
- "integrity": "sha512-sTujir3YTDrbFnkccCrc9zdSx4AGRHdOeVBLPvVORNieHoKlBeKLBtCA9GsjRbbpnmSrpPe9Fl6z1MkCWblAmw==",
+ "version": "0.12.3",
+ "resolved": "https://registry.npmjs.org/@matter/types/-/types-0.12.3.tgz",
+ "integrity": "sha512-4nlu7nCW+V/Sed1XkJoPvtdRz0wIv8o9YSDj4To3yGR77HDqK9nbLXxpM7J+PQmGQ0/xEX1ylWU9X2dSIsH8WQ==",
"license": "Apache-2.0",
"dependencies": {
- "@matter/general": "0.12.2",
- "@matter/model": "0.12.2",
+ "@matter/general": "0.12.3",
+ "@matter/model": "0.12.3",
"@noble/curves": "^1.8.1"
}
},
@@ -2659,9 +2659,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001697",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001697.tgz",
- "integrity": "sha512-GwNPlWJin8E+d7Gxq96jxM6w0w+VFeyyXRsjU58emtkYqnbwHqXm5uT2uCmO0RQE9htWknOP4xtBlLmM/gWxvQ==",
+ "version": "1.0.30001698",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001698.tgz",
+ "integrity": "sha512-xJ3km2oiG/MbNU8G6zIq6XRZ6HtAOVXsbOrP/blGazi52kc5Yy7b6sDA5O+FbROzRrV7BSTllLHuNvmawYUJjw==",
"dev": true,
"funding": [
{
@@ -3108,9 +3108,9 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.5.91",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.91.tgz",
- "integrity": "sha512-sNSHHyq048PFmZY4S90ax61q+gLCs0X0YmcOII9wG9S2XwbVr+h4VW2wWhnbp/Eys3cCwTxVF292W3qPaxIapQ==",
+ "version": "1.5.95",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.95.tgz",
+ "integrity": "sha512-XNsZaQrgQX+BG37BRQv+E+HcOZlWhqYaDoVVNCws/WrYYdbGrkR1qCDJ2mviBF3flCs6/BTa4O7ANfFTFZk6Dg==",
"dev": true,
"license": "ISC"
},
@@ -5576,9 +5576,9 @@
}
},
"node_modules/object-inspect": {
- "version": "1.13.3",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
- "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==",
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
diff --git a/package.json b/package.json
index b667b73c..27c19259 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "matterbridge",
- "version": "2.1.3",
+ "version": "2.1.4",
"description": "Matterbridge plugin manager for Matter",
"author": "https://github.com/Luligu",
"license": "Apache-2.0",
@@ -147,7 +147,7 @@
"install:jest": "npm install --save-dev jest ts-jest @types/jest eslint-plugin-jest && npm run test"
},
"dependencies": {
- "@matter/main": "0.12.2",
+ "@matter/main": "0.12.3",
"archiver": "7.0.1",
"express": "4.21.2",
"glob": "11.0.1",
diff --git a/src/deviceManager.test.ts b/src/deviceManager.test.ts
index 610f8495..964ae54f 100644
--- a/src/deviceManager.test.ts
+++ b/src/deviceManager.test.ts
@@ -10,7 +10,6 @@ import { MatterbridgeEndpoint } from './matterbridgeEndpoint.js';
import { DeviceManager } from './deviceManager.js';
import { PluginManager } from './pluginManager.js';
import { contactSensor, occupancySensor } from './matterbridgeDeviceTypes.js';
-import { MdnsService } from '@matter/main/protocol';
// Default colors
const plg = '\u001B[38;5;33m';
@@ -38,9 +37,7 @@ describe('DeviceManager with mocked devices', () => {
afterAll(async () => {
// Close the Matterbridge instance
- const server = matterbridge.serverNode;
await matterbridge.destroyInstance();
- await server?.env.get(MdnsService)[Symbol.asyncDispose]();
// Restore the mocked AnsiLogger.log method
loggerLogSpy.mockRestore();
// Restore the mocked console.log
@@ -195,7 +192,6 @@ describe('DeviceManager with real devices', () => {
// Close the Matterbridge instance
const server = matterbridge.serverNode;
await matterbridge.destroyInstance();
- await server?.env.get(MdnsService)[Symbol.asyncDispose]();
// Restore the mocked AnsiLogger.log method
loggerLogSpy.mockRestore();
// Restore the mocked console.log
diff --git a/src/frontend.test.ts b/src/frontend.test.ts
index de7c619a..119e6fac 100644
--- a/src/frontend.test.ts
+++ b/src/frontend.test.ts
@@ -1166,9 +1166,7 @@ describe('Matterbridge frontend', () => {
expect((matterbridge as any).plugins.size).toBe(0);
// Close the Matterbridge instance
- const server = matterbridge.serverNode;
await matterbridge.destroyInstance();
- await server?.env.get(MdnsService)[Symbol.asyncDispose]();
expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, `WebSocket server closed successfully`);
expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.NOTICE, `Cleanup completed. Shutting down...`);
diff --git a/src/frontend.ts b/src/frontend.ts
index 55de2c0d..bc6a5f8b 100644
--- a/src/frontend.ts
+++ b/src/frontend.ts
@@ -924,6 +924,20 @@ export class Frontend {
}
async stop() {
+ // Start the memory check. This will not allow the process to exit but will log the memory usage for 5 minutes.
+ if (hasParameter('memorycheck')) {
+ await new Promise((resolve) => {
+ this.log.debug(`***Memory check started for ${getIntParameter('memorycheck') ?? 5 * 60 * 1000} ms`);
+ setTimeout(
+ () => {
+ this.log.debug(`***Memory check stopped after ${getIntParameter('memorycheck') ?? 5 * 60 * 1000} ms`);
+ resolve();
+ },
+ getIntParameter('memorycheck') ?? 5 * 60 * 1000,
+ );
+ });
+ }
+
// Close the http server
if (this.httpServer) {
this.httpServer.close();
@@ -1015,7 +1029,7 @@ export class Frontend {
};
this.log.debug(
- `***Cpu usage ${CYAN}${cpuUsage.padStart(6, ' ')} %${db} - Memory usage rss ${CYAN}${memoryUsage.rss}${db} heapTotal ${CYAN}${memoryUsage.heapTotal}${db} heapUsed ${CYAN}${memoryUsage.heapUsed}${db} external ${memoryUsage.external} arrayBuffers ${memoryUsage.arrayBuffers}`,
+ `***Cpu usage: ${CYAN}${cpuUsage.padStart(6, ' ')} %${db} - Memory usage: rss ${CYAN}${memoryUsage.rss}${db} heapTotal ${CYAN}${memoryUsage.heapTotal}${db} heapUsed ${CYAN}${memoryUsage.heapUsed}${db} external ${memoryUsage.external} arrayBuffers ${memoryUsage.arrayBuffers}`,
);
};
interval();
@@ -1045,7 +1059,7 @@ export class Frontend {
};
// eslint-disable-next-line no-console
console.log(
- `${YELLOW}Cpu usage${db} ${CYAN}${memory.cpu.padStart(6, ' ')} %${db} - ${YELLOW}Memory usage${db} rss ${CYAN}${memoryUsage.rss}${db} heapTotal ${CYAN}${memoryUsage.heapTotal}${db} heapUsed ${CYAN}${memoryUsage.heapUsed}${db} external ${memoryUsage.external} arrayBuffers ${memoryUsage.arrayBuffers}${rs}`,
+ `${YELLOW}Cpu usage:${db} ${CYAN}${memory.cpu.padStart(6, ' ')} %${db} - ${YELLOW}Memory usage:${db} rss ${CYAN}${memoryUsage.rss}${db} heapTotal ${CYAN}${memoryUsage.heapTotal}${db} heapUsed ${CYAN}${memoryUsage.heapUsed}${db} external ${memoryUsage.external} arrayBuffers ${memoryUsage.arrayBuffers}${rs}`,
);
}
this.memoryData = [];
diff --git a/src/matterbridge.bridge.test.ts b/src/matterbridge.bridge.test.ts
index bc76a939..9eb2d4f3 100644
--- a/src/matterbridge.bridge.test.ts
+++ b/src/matterbridge.bridge.test.ts
@@ -10,7 +10,6 @@ import { jest } from '@jest/globals';
import { AnsiLogger, db, LogLevel, nf, rs, UNDERLINE, UNDERLINEOFF } from 'node-ansi-logger';
import { Matterbridge } from './matterbridge.js';
import { wait, waiter } from './utils/utils.js';
-import { MdnsService } from '@matter/main/protocol';
import { Environment, StorageService } from '@matter/main';
import path from 'path';
import os from 'os';
@@ -83,7 +82,6 @@ describe('Matterbridge loadInstance() and cleanup() -bridge mode', () => {
afterAll(async () => {
// Restore all mocks
jest.restoreAllMocks();
- // console.log('Matterbridge test -bridge mode');
}, 30000);
test('Matterbridge.loadInstance(true) -bridge mode', async () => {
@@ -160,9 +158,7 @@ describe('Matterbridge loadInstance() and cleanup() -bridge mode', () => {
test('Matterbridge.destroyInstance() -bridge mode', async () => {
// Close the Matterbridge instance
- const server = matterbridge.serverNode;
await matterbridge.destroyInstance();
- await server?.env.get(MdnsService)[Symbol.asyncDispose]();
expect((matterbridge as any).log.log).toHaveBeenCalledWith(LogLevel.NOTICE, `Cleanup completed. Shutting down...`);
}, 60000);
diff --git a/src/matterbridge.childbridge.test.ts b/src/matterbridge.childbridge.test.ts
index 038ea2ce..62e5ed07 100644
--- a/src/matterbridge.childbridge.test.ts
+++ b/src/matterbridge.childbridge.test.ts
@@ -9,7 +9,6 @@ import { jest } from '@jest/globals';
import { AnsiLogger, db, LogLevel, nf, rs, UNDERLINE, UNDERLINEOFF } from 'node-ansi-logger';
import { Matterbridge } from './matterbridge.js';
import { wait, waiter } from './utils/utils.js';
-import { MdnsService } from '@matter/main/protocol';
import { Environment, StorageService } from '@matter/main';
import path from 'path';
import os from 'os';
@@ -82,7 +81,6 @@ describe('Matterbridge loadInstance() and cleanup() -childbridge mode', () => {
afterAll(async () => {
// Restore all mocks
jest.restoreAllMocks();
- // console.log('Matterbridge test -childbridge mode');
});
test('Matterbridge.loadInstance(true) -childbridge mode', async () => {
diff --git a/src/matterbridge.test.ts b/src/matterbridge.test.ts
index 9e146c27..750dc4eb 100644
--- a/src/matterbridge.test.ts
+++ b/src/matterbridge.test.ts
@@ -1,13 +1,13 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
-process.argv = ['node', 'matterbridge.test.js', '-frontend', '0', '-profile', 'Jest'];
+process.argv = ['node', 'matterbridge.test.js', '-frontend', '0', '-profile', 'Jest', '-logger', 'debug', '-matterlogger', 'debug'];
import { jest } from '@jest/globals';
// jest.mock('@project-chip/matter-node.js/util');
-import { AnsiLogger, db, LogLevel, nf } from 'node-ansi-logger';
+import { AnsiLogger, db, LogLevel, nf, TimestampFormat } from 'node-ansi-logger';
import { hasParameter } from './utils/utils.js';
import { Matterbridge } from './matterbridge.js';
import { RegisteredPlugin, SessionInformation } from './matterbridgeTypes.js';
@@ -81,7 +81,9 @@ describe('Matterbridge', () => {
let matterbridge: Matterbridge;
test('Matterbridge.loadInstance(false)', async () => {
+ expect((Matterbridge as any).instance).toBeUndefined();
matterbridge = await Matterbridge.loadInstance(false);
+ matterbridge.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: TimestampFormat.TIME_MILLIS, logLevel: LogLevel.DEBUG });
expect(matterbridge).toBeDefined();
expect(matterbridge.profile).toBe('Jest');
expect(matterbridge.nodeStorageName).toBe('storage.Jest');
@@ -89,7 +91,7 @@ describe('Matterbridge', () => {
expect(matterbridge.matterbrideLoggerFile).toBe('matterbridge.Jest.log');
expect(matterbridge.matterLoggerFile).toBe('matter.Jest.log');
expect((matterbridge as any).initialized).toBeFalsy();
- expect((matterbridge as any).log).toBeUndefined();
+ expect((matterbridge as any).log).toBeDefined();
expect((matterbridge as any).homeDirectory).toBe('');
expect((matterbridge as any).matterbridgeDirectory).toBe('');
expect((matterbridge as any).globalModulesDirectory).toBe('');
@@ -111,6 +113,7 @@ describe('Matterbridge', () => {
});
test('Matterbridge.loadInstance(true)', async () => {
+ expect((Matterbridge as any).instance).toBeDefined();
matterbridge = await Matterbridge.loadInstance(true);
expect((matterbridge as any).initialized).toBeFalsy();
if (!(matterbridge as any).initialized) await matterbridge.initialize();
@@ -148,6 +151,7 @@ describe('Matterbridge', () => {
test('Matterbridge.loadInstance(true) with frontend', async () => {
process.argv = ['node', 'matterbridge.test.js', '-frontend', '8081', '-profile', 'Jest'];
+ expect((Matterbridge as any).instance).toBeUndefined();
matterbridge = await Matterbridge.loadInstance(true);
expect((matterbridge as any).initialized).toBeTruthy();
if (!(matterbridge as any).initialized) await matterbridge.initialize();
@@ -179,6 +183,7 @@ describe('Matterbridge', () => {
let matterbridge: Matterbridge;
beforeAll(async () => {
+ process.argv = ['node', 'matterbridge.test.js', '-frontend', '0', '-profile', 'Jest', '-logger', 'debug', '-matterlogger', 'debug'];
matterbridge = await Matterbridge.loadInstance(true);
if (!(matterbridge as any).initialized) await matterbridge.initialize();
});
@@ -191,6 +196,8 @@ describe('Matterbridge', () => {
test('Matterbridge profile', async () => {
expect(matterbridge).toBeDefined();
expect(matterbridge.profile).toBe('Jest');
+ expect((matterbridge as any).initialized).toBe(true);
+ expect((matterbridge as any).hasCleanupStarted).toBe(false);
});
test('should do a partial mock of AnsiLogger', () => {
@@ -273,17 +280,23 @@ describe('Matterbridge', () => {
});
test('matterbridge -help', async () => {
+ expect((matterbridge as any).initialized).toBe(true);
+ expect((matterbridge as any).hasCleanupStarted).toBe(false);
+
const shutdownPromise = new Promise((resolve) => {
matterbridge.on('shutdown', resolve);
});
process.argv = ['node', 'matterbridge.test.js', '-frontend', '0', '-help'];
await (matterbridge as any).parseCommandLine();
- expect(loggerLogSpy).toHaveBeenCalled();
+ expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.INFO, expect.stringContaining('Usage: matterbridge [options]'));
await shutdownPromise;
matterbridge.removeAllListeners('shutdown');
}, 60000);
test('matterbridge -list', async () => {
+ expect((matterbridge as any).initialized).toBe(true);
+ expect((matterbridge as any).hasCleanupStarted).toBe(false);
+
const shutdownPromise = new Promise((resolve) => {
matterbridge.on('shutdown', resolve);
});
@@ -296,6 +309,9 @@ describe('Matterbridge', () => {
}, 60000);
test('matterbridge -logstorage', async () => {
+ expect((matterbridge as any).initialized).toBe(true);
+ expect((matterbridge as any).hasCleanupStarted).toBe(false);
+
const shutdownPromise = new Promise((resolve) => {
matterbridge.on('shutdown', resolve);
});
@@ -307,6 +323,9 @@ describe('Matterbridge', () => {
}, 60000);
test('matterbridge -loginterfaces', async () => {
+ expect((matterbridge as any).initialized).toBe(true);
+ expect((matterbridge as any).hasCleanupStarted).toBe(false);
+
const shutdownPromise = new Promise((resolve) => {
matterbridge.on('shutdown', resolve);
});
@@ -318,6 +337,9 @@ describe('Matterbridge', () => {
}, 60000);
test('matterbridge -reset', async () => {
+ expect((matterbridge as any).initialized).toBe(true);
+ expect((matterbridge as any).hasCleanupStarted).toBe(false);
+
const shutdownPromise = new Promise((resolve) => {
matterbridge.on('shutdown', resolve);
});
@@ -329,6 +351,9 @@ describe('Matterbridge', () => {
}, 60000);
test('matterbridge -reset xxx', async () => {
+ expect((matterbridge as any).initialized).toBe(false);
+ expect((matterbridge as any).hasCleanupStarted).toBe(false);
+
const shutdownPromise = new Promise((resolve) => {
matterbridge.on('shutdown', resolve);
});
@@ -342,6 +367,9 @@ describe('Matterbridge', () => {
}, 60000);
test('matterbridge -add mockPlugin1', async () => {
+ expect((matterbridge as any).initialized).toBe(false);
+ expect((matterbridge as any).hasCleanupStarted).toBe(false);
+
const shutdownPromise = new Promise((resolve) => {
matterbridge.on('shutdown', resolve);
});
@@ -366,6 +394,9 @@ describe('Matterbridge', () => {
}, 60000);
test('matterbridge -disable mockPlugin1', async () => {
+ expect((matterbridge as any).initialized).toBe(false);
+ expect((matterbridge as any).hasCleanupStarted).toBe(false);
+
const shutdownPromise = new Promise((resolve) => {
matterbridge.on('shutdown', resolve);
});
@@ -386,6 +417,9 @@ describe('Matterbridge', () => {
}, 60000);
test('matterbridge -enable mockPlugin1', async () => {
+ expect((matterbridge as any).initialized).toBe(false);
+ expect((matterbridge as any).hasCleanupStarted).toBe(false);
+
const shutdownPromise = new Promise((resolve) => {
matterbridge.on('shutdown', resolve);
});
@@ -406,6 +440,9 @@ describe('Matterbridge', () => {
}, 60000);
test('matterbridge -remove mockPlugin1', async () => {
+ expect((matterbridge as any).initialized).toBe(false);
+ expect((matterbridge as any).hasCleanupStarted).toBe(false);
+
const shutdownPromise = new Promise((resolve) => {
matterbridge.on('shutdown', resolve);
});
@@ -421,6 +458,9 @@ describe('Matterbridge', () => {
}, 60000);
test('matterbridge -add mockPlugin1/2/3', async () => {
+ expect((matterbridge as any).initialized).toBe(false);
+ expect((matterbridge as any).hasCleanupStarted).toBe(false);
+
const shutdownPromise = new Promise((resolve) => {
matterbridge.on('shutdown', resolve);
});
@@ -468,6 +508,9 @@ describe('Matterbridge', () => {
}, 60000);
test('matterbridge start mockPlugin1/2/3', async () => {
+ expect((matterbridge as any).initialized).toBe(false);
+ expect((matterbridge as any).hasCleanupStarted).toBe(false);
+
process.argv = ['node', 'matterbridge.test.js', '-frontend', '0'];
const plugins = (await (matterbridge as any).plugins.array()) as RegisteredPlugin[];
expect(plugins).toHaveLength(3);
@@ -492,15 +535,14 @@ describe('Matterbridge', () => {
}, 10000);
test('matterbridge -factoryreset', async () => {
- const shutdownPromise = new Promise((resolve) => {
- matterbridge.on('shutdown', resolve);
- });
+ expect((matterbridge as any).initialized).toBe(false);
+ expect((matterbridge as any).hasCleanupStarted).toBe(false);
+
process.argv = ['node', 'matterbridge.test.js', '-frontend', '0', '-factoryreset'];
await (matterbridge as any).parseCommandLine();
- expect((matterbridge as any).log.log).toHaveBeenCalledWith(LogLevel.INFO, 'Factory reset done! Remove all paired fabrics from the controllers.');
+ expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.INFO, 'Factory reset done! Remove all paired fabrics from the controllers.');
expect((matterbridge as any).plugins).toHaveLength(0);
- // await shutdownPromise;
matterbridge.removeAllListeners('shutdown');
- }, 60000);
+ }, 10000);
});
});
diff --git a/src/matterbridge.ts b/src/matterbridge.ts
index 0d088d9b..c3572d06 100644
--- a/src/matterbridge.ts
+++ b/src/matterbridge.ts
@@ -45,7 +45,7 @@ import { Frontend } from './frontend.js';
// @matter
import { DeviceTypeId, Endpoint as EndpointNode, Logger, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, VendorId, StorageContext, StorageManager, StorageService, Environment, ServerNode, FabricIndex, SessionsBehavior } from '@matter/main';
-import { DeviceCommissioner, ExposedFabricInformation, FabricAction, PaseClient } from '@matter/main/protocol';
+import { DeviceCommissioner, ExposedFabricInformation, FabricAction, MdnsService, PaseClient } from '@matter/main/protocol';
import { AggregatorEndpoint } from '@matter/main/endpoints';
// Default colors
@@ -222,9 +222,24 @@ export class Matterbridge extends EventEmitter {
*
*/
async destroyInstance() {
+ // Save server nodes to close
+ const servers: ServerNode[] = [];
+ if (this.bridgeMode === 'bridge') {
+ if (this.serverNode) servers.push(this.serverNode);
+ }
+ if (this.bridgeMode === 'childbridge') {
+ for (const plugin of this.plugins.array()) {
+ if (plugin.serverNode) servers.push(plugin.serverNode);
+ }
+ }
+ // Cleanup
await this.cleanup('destroying instance...', false);
- // await matterServerNode.env.get(MdnsService)[Symbol.asyncDispose]();
- // this.log.info(`Closed ${matterServerNode.id} MdnsService`);
+ // Close servers mdns service
+ for (const server of servers) {
+ await server.env.get(MdnsService)[Symbol.asyncDispose]();
+ this.log.info(`Closed ${server.id} MdnsService`);
+ }
+ // Wait for the cleanup to finish
await new Promise((resolve) => {
setTimeout(resolve, 1000);
});
@@ -745,13 +760,13 @@ export class Matterbridge extends EventEmitter {
process.removeAllListeners('unhandledRejection');
this.exceptionHandler = async (error: Error) => {
- this.log.fatal('Unhandled Exception detected at:', error.stack || error, rs);
+ this.log.error('Unhandled Exception detected at:', error.stack || error, rs);
// await this.cleanup('Unhandled Exception detected, cleaning up...');
};
process.on('uncaughtException', this.exceptionHandler);
this.rejectionHandler = async (reason, promise) => {
- this.log.fatal('Unhandled Rejection detected at:', promise, 'reason:', reason instanceof Error ? reason.stack : reason, rs);
+ this.log.error('Unhandled Rejection detected at:', promise, 'reason:', reason instanceof Error ? reason.stack : reason, rs);
// await this.cleanup('Unhandled Rejection detected, cleaning up...');
};
process.on('unhandledRejection', this.rejectionHandler);
@@ -1387,6 +1402,8 @@ export class Matterbridge extends EventEmitter {
}
this.hasCleanupStarted = false;
this.initialized = false;
+ } else {
+ this.log.debug('Cleanup already started...');
}
}
diff --git a/src/matterbridgeAccessoryPlatform.test.ts b/src/matterbridgeAccessoryPlatform.test.ts
index 2dc01451..1f0656a6 100644
--- a/src/matterbridgeAccessoryPlatform.test.ts
+++ b/src/matterbridgeAccessoryPlatform.test.ts
@@ -8,7 +8,6 @@ import { jest } from '@jest/globals';
import { AnsiLogger } from 'node-ansi-logger';
import { Matterbridge } from './matterbridge.js';
import { MatterbridgeAccessoryPlatform } from './matterbridgeAccessoryPlatform.js';
-import { MdnsService } from '@matter/main/protocol';
describe('Matterbridge accessory platform', () => {
beforeAll(async () => {
@@ -41,9 +40,7 @@ describe('Matterbridge accessory platform', () => {
expect(platform.type).toBe('AccessoryPlatform');
// Close the Matterbridge instance
- const server = matterbridge.serverNode;
await matterbridge.destroyInstance();
- await server?.env.get(MdnsService)[Symbol.asyncDispose]();
expect((Matterbridge as any).instance).toBeUndefined();
}, 60000);
});
diff --git a/src/matterbridgeDynamicPlatform.test.ts b/src/matterbridgeDynamicPlatform.test.ts
index 98fcdff9..22233b30 100644
--- a/src/matterbridgeDynamicPlatform.test.ts
+++ b/src/matterbridgeDynamicPlatform.test.ts
@@ -8,7 +8,6 @@ import { jest } from '@jest/globals';
import { AnsiLogger, LogLevel } from 'node-ansi-logger';
import { Matterbridge } from './matterbridge.js';
import { MatterbridgeDynamicPlatform } from './matterbridgeDynamicPlatform.js';
-import { MdnsService } from '@matter/main/protocol';
describe('Matterbridge dynamic platform', () => {
beforeAll(async () => {
@@ -41,9 +40,7 @@ describe('Matterbridge dynamic platform', () => {
expect(platform.type).toBe('DynamicPlatform');
// Close the Matterbridge instance
- const server = matterbridge.serverNode;
await matterbridge.destroyInstance();
- await server?.env.get(MdnsService)[Symbol.asyncDispose]();
expect((Matterbridge as any).instance).toBeUndefined();
}, 60000);
});
diff --git a/src/matterbridgeEndpoint-default.test.ts b/src/matterbridgeEndpoint-default.test.ts
index 509440fc..f5989b61 100644
--- a/src/matterbridgeEndpoint-default.test.ts
+++ b/src/matterbridgeEndpoint-default.test.ts
@@ -83,7 +83,6 @@ import {
TotalVolatileOrganicCompoundsConcentrationMeasurementServer,
} from '@matter/node/behaviors';
import { updateAttribute } from './matterbridgeEndpointHelpers.js';
-import { MdnsService } from '@matter/main/protocol';
describe('MatterbridgeEndpoint class', () => {
let matterbridge: Matterbridge;
@@ -176,9 +175,7 @@ describe('MatterbridgeEndpoint class', () => {
afterAll(async () => {
// Close the Matterbridge instance
- const server = matterbridge.serverNode;
await matterbridge.destroyInstance();
- await server?.env.get(MdnsService)[Symbol.asyncDispose]();
// Restore all mocks
jest.restoreAllMocks();
@@ -922,7 +919,7 @@ describe('MatterbridgeEndpoint class', () => {
// eslint-disable-next-line jest/expect-expect
test('pause before cleanup', async () => {
- await new Promise((resolve) => setTimeout(resolve, 5000)); // Pause for 5 seconds
+ await new Promise((resolve) => setTimeout(resolve, 5000)); // Pause for 5 seconds to allow matter.js promises to settle
}, 60000);
});
});
diff --git a/src/matterbridgeEndpoint-matterjs.test.ts b/src/matterbridgeEndpoint-matterjs.test.ts
index e089ad3b..76d8a160 100644
--- a/src/matterbridgeEndpoint-matterjs.test.ts
+++ b/src/matterbridgeEndpoint-matterjs.test.ts
@@ -345,7 +345,7 @@ describe('MatterbridgeEndpoint class', () => {
test('close server node', async () => {
expect(server).toBeDefined();
await server.close();
- await server.env.get(MdnsService)[Symbol.asyncDispose]();
+ await server.env.get(MdnsService)[Symbol.asyncDispose](); // loadInstance(false) so destroyInstance() does not stop the mDNS service
});
});
});
diff --git a/src/matterbridgeEndpoint.test.ts b/src/matterbridgeEndpoint.test.ts
index b8364b43..18714aaa 100644
--- a/src/matterbridgeEndpoint.test.ts
+++ b/src/matterbridgeEndpoint.test.ts
@@ -61,7 +61,7 @@ import {
ThermostatUserInterfaceConfigurationServer,
TimeSynchronizationServer,
} from '@matter/node/behaviors';
-import { getAttributeId, getClusterId } from './matterbridgeEndpointHelpers.js';
+import { checkNotLatinCharacters, generateUniqueId, getAttributeId, getClusterId } from './matterbridgeEndpointHelpers.js';
describe('MatterbridgeEndpoint class', () => {
let matterbridge: Matterbridge;
@@ -177,6 +177,117 @@ describe('MatterbridgeEndpoint class', () => {
expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.INFO, expect.stringContaining(`\x1B[39mMatterbridge.Matterbridge.${device.uniqueStorageKey.replaceAll(' ', '')} \x1B[0mready`));
}
+ test('conversion of not latin characters', async () => {
+ // Should return false for Latin-based text (including accents)
+ expect(checkNotLatinCharacters('Hello World')).toBe(false);
+ expect(checkNotLatinCharacters('café au lait')).toBe(false);
+ expect(checkNotLatinCharacters('München-2024')).toBe(false);
+ expect(checkNotLatinCharacters('Tōkyō_2024')).toBe(false);
+ expect(checkNotLatinCharacters("L'éclair du matin")).toBe(false);
+ expect(checkNotLatinCharacters('São Paulo')).toBe(false);
+ expect(checkNotLatinCharacters('Résumé and naïve')).toBe(false);
+
+ // Should return false for numbers, dashes, and underscores
+ expect(checkNotLatinCharacters('123456')).toBe(false);
+ expect(checkNotLatinCharacters('Hello_123')).toBe(false);
+ expect(checkNotLatinCharacters('test-underscore_')).toBe(false);
+ expect(checkNotLatinCharacters('text-with-dash')).toBe(false);
+
+ // Special characters should return false
+ expect(checkNotLatinCharacters('.,> {
+ // Sample names
+ const latinNames = ['Hello World', 'café au lait', 'München-2024', 'Tōkyō_2024', "L'éclair du matin", 'São Paulo', 'Résumé and naïve', '123456', 'Hello_123', 'test-underscore_', 'text-with-dash'];
+
+ const specialChars = ['.,>();
+
+ [...latinNames, ...specialChars, ...nonLatinNames].forEach((name) => {
+ const hash = generateUniqueId(name);
+ expect(hash).toHaveLength(32); // MD5 produces 32-char hex strings
+ expect(uniqueIds.has(name)).toBe(false); // Ensure no duplicates
+ uniqueIds.set(name, hash);
+ });
+
+ // The same input should generate the same hash
+ uniqueIds.forEach((hash, name) => {
+ expect(generateUniqueId(name)).toBe(hash);
+ });
+
+ // Different names should generate different hashes
+ const hashes = [...uniqueIds.values()];
+ const uniqueHashes = new Set(hashes);
+ expect(uniqueHashes.size).toBe(hashes.length); // Ensure no hash collisions
+ });
+
+ test('constructor with non latin', async () => {
+ const nonLatinNames = [
+ '버튼 - 작은방 (버튼-작은방)',
+ '거실 공기 관리 - 샤오미 공기 청정기 속도 조절 (거실공기관리-샤오미공기청정기속도조절)',
+ '난방 - 온도관리 방법 설정 (난방-온도관리방법설정)',
+ '단후이 로봇청소기 예약/일정 (단후이로봇청소기예약/일정)',
+ '단후이 로봇청소기 시계/지역시간 설정(mqtt) (단후이로봇청소기시계/지역시간설정(mqtt))',
+ '애드온을 다시 시작(DuckDNS) (애드온을다시시작(DuckDNS))',
+ '외출 중 기기 제어 - 외출 3시간 이상시 끄기 (외출중기기제어-외출3시간이상시끄기)',
+ '메인 등(L1) 조명 자동 컨트롤 (메인등(L1)조명자동컨트롤)',
+ '작은방 공기 관리 - 샤오미 공기 청정기 속도 조절 (작은방공기관리-샤오미공기청정기속도조절)',
+ '알림 - haos 메모이 시용이 90% 이상으로 시스템을 재시작합니다 (알림-haos메모이시용이90%이상으로시스템을재시작합니다)',
+ '알림 - 가스 검침기의 카운트를 다시 설정해 주세요 (알림-가스검침기의카운트를다시설정해주세요)',
+ '알림 - 배달원이 고객님의 음식을 픽업했습니다 (알림-배달원이고객님의음식을픽업했습니다)',
+ '알림 - 조리기기 관리가 수동으로 변경되었습니다 (알림-조리기기관리가수동으로변경되었습니다)',
+ '알림 - 주문이 거의 도착했습니다 (알림-주문이거의도착했습니다)',
+ '알림 - 현재 외부에는 비가 오고 있습니다 (알림-현재외부에는비가오고있습니다)',
+ '알림 - NAS의 보안 위험이 감지 되었습니디. 보안 어드바이저를 확인해 주세요 (알림-NAS의보안위험이감지되었습니디보안어드바이저를확인해주세요)',
+ ];
+ let n = 1000;
+ for (const name of nonLatinNames) {
+ const device = new MatterbridgeEndpoint(onOffOutlet, { uniqueStorageKey: name, endpointId: EndpointNumber(n++) });
+ expect(device).toBeDefined();
+ expect(device.id).toBe(generateUniqueId(name));
+ await add(device);
+ }
+ });
+
test('constructor', async () => {
const deviceType = onOffLight;
const device = new MatterbridgeEndpoint(deviceType, { uniqueStorageKey: 'OnOffLight1' });
diff --git a/src/matterbridgeEndpoint.ts b/src/matterbridgeEndpoint.ts
index 08f4e232..d3e6e871 100644
--- a/src/matterbridgeEndpoint.ts
+++ b/src/matterbridgeEndpoint.ts
@@ -67,6 +67,8 @@ import {
getAttributeId,
setAttribute,
getAttribute,
+ checkNotLatinCharacters,
+ generateUniqueId,
} from './matterbridgeEndpointHelpers.js';
// @matter
@@ -291,6 +293,11 @@ export class MatterbridgeEndpoint extends Endpoint {
};
const endpointV8 = MutableEndpoint(deviceTypeDefinitionV8);
+ // Check if the uniqueStorageKey is valid
+ if (options.uniqueStorageKey && checkNotLatinCharacters(options.uniqueStorageKey)) {
+ options.uniqueStorageKey = generateUniqueId(options.uniqueStorageKey);
+ }
+
// Convert the options to an Endpoint.Options
const optionsV8 = {
id: options.uniqueStorageKey?.replace(/[ .]/g, ''),
diff --git a/src/matterbridgeEndpointHelpers.ts b/src/matterbridgeEndpointHelpers.ts
index 80fac5df..b19ca868 100644
--- a/src/matterbridgeEndpointHelpers.ts
+++ b/src/matterbridgeEndpointHelpers.ts
@@ -111,6 +111,24 @@ export function lowercaseFirstLetter(name: string): string {
return name.charAt(0).toLowerCase() + name.slice(1);
}
+export function checkNotLatinCharacters(deviceName: string): boolean {
+ const nonLatinRegexList = [
+ /[\u0400-\u04FF\u0500-\u052F]/, // Cyrillic
+ /[\u2E80-\u9FFF]/, // CJK (Chinese, Japanese, Korean)
+ /[\uAC00-\uD7AF]/, // Korean Hangul
+ /[\u0600-\u06FF\u0750-\u077F]/, // Arabic, Persian
+ /[\u0590-\u05FF]/, // Hebrew
+ /[\u0900-\u097F]/, // Devanagari (Hindi, Sanskrit)
+ /[\u0E00-\u0E7F]/, // Thai
+ /[\u1200-\u137F]/, // Ethiopic (Amharic, Tigrinya)
+ ];
+ return nonLatinRegexList.some((regex) => regex.test(deviceName));
+}
+
+export function generateUniqueId(deviceName: string): string {
+ return createHash('md5').update(deviceName).digest('hex'); // MD5 hash of the device name
+}
+
export function createUniqueId(param1: string, param2: string, param3: string, param4: string) {
const hash = createHash('md5');
hash.update(param1 + param2 + param3 + param4);
diff --git a/src/matterbridgePlatform.test.ts b/src/matterbridgePlatform.test.ts
index bfd9764e..856eace4 100644
--- a/src/matterbridgePlatform.test.ts
+++ b/src/matterbridgePlatform.test.ts
@@ -2,38 +2,73 @@
/* eslint-disable jest/no-conditional-expect */
/* eslint-disable @typescript-eslint/no-explicit-any */
-process.argv = ['node', 'matterbridge.test.js', '-frontend', '0', '-profile', 'Jest'];
+process.argv = ['node', 'matterbridge.test.js', '-frontend', '0', '-profile', 'JestPlatform'];
import { jest } from '@jest/globals';
-import { AnsiLogger, CYAN, db, LogLevel, pl, wr } from 'node-ansi-logger';
+import { AnsiLogger, CYAN, db, er, LogLevel, pl, wr } from 'node-ansi-logger';
import { NodeStorageManager } from 'node-persist-manager';
import { Matterbridge } from './matterbridge.js';
import { MatterbridgePlatform } from './matterbridgePlatform.js';
import { contactSensor, humiditySensor, powerSource, temperatureSensor } from './matterbridgeDeviceTypes.js';
import { MatterbridgeEndpoint } from './matterbridgeEndpoint.js';
+import { Environment, StorageService } from '@matter/main';
+import path from 'path';
+import os from 'os';
describe('Matterbridge platform', () => {
let matterbridge: Matterbridge;
let platform: MatterbridgePlatform;
- beforeAll(async () => {
- // Mock the AnsiLogger.log method
- jest.spyOn(AnsiLogger.prototype, 'log').mockImplementation((level: string, message: string, ...parameters: any[]) => {
- // console.log(`Mocked log: ${level} - ${message}`, ...parameters);
+ let loggerLogSpy: jest.SpiedFunction;
+ let consoleLogSpy: jest.SpiedFunction;
+ let consoleDebugSpy: jest.SpiedFunction;
+ let consoleInfoSpy: jest.SpiedFunction;
+ let consoleWarnSpy: jest.SpiedFunction;
+ let consoleErrorSpy: jest.SpiedFunction;
+ const debug = false;
+
+ if (!debug) {
+ // Spy on and mock AnsiLogger.log
+ loggerLogSpy = jest.spyOn(AnsiLogger.prototype, 'log').mockImplementation((level: string, message: string, ...parameters: any[]) => {
+ //
+ });
+ // Spy on and mock console.log
+ consoleLogSpy = jest.spyOn(console, 'log').mockImplementation((...args: any[]) => {
+ //
});
- jest.spyOn(AnsiLogger.prototype, 'debug').mockImplementation((message: string, ...parameters: any[]) => {
- // console.log(`Mocked debug: ${message}`, ...parameters);
+ // Spy on and mock console.debug
+ consoleDebugSpy = jest.spyOn(console, 'debug').mockImplementation((...args: any[]) => {
+ //
});
- jest.spyOn(AnsiLogger.prototype, 'info').mockImplementation((message: string, ...parameters: any[]) => {
- // console.log(`Mocked info: ${message}`, ...parameters);
+ // Spy on and mock console.info
+ consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation((...args: any[]) => {
+ //
});
- jest.spyOn(AnsiLogger.prototype, 'warn').mockImplementation((message: string, ...parameters: any[]) => {
- // console.log(`Mocked warn: ${message}`, ...parameters);
+ // Spy on and mock console.warn
+ consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation((...args: any[]) => {
+ //
});
- jest.spyOn(AnsiLogger.prototype, 'error').mockImplementation((message: string, ...parameters: any[]) => {
- // console.log(`Mocked error: ${message}`, ...parameters);
+ // Spy on and mock console.error
+ consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation((...args: any[]) => {
+ //
});
+ } else {
+ // Spy on AnsiLogger.log
+ loggerLogSpy = jest.spyOn(AnsiLogger.prototype, 'log');
+ // Spy on console.log
+ consoleLogSpy = jest.spyOn(console, 'log');
+ // Spy on console.debug
+ consoleDebugSpy = jest.spyOn(console, 'debug');
+ // Spy on console.info
+ consoleInfoSpy = jest.spyOn(console, 'info');
+ // Spy on console.warn
+ consoleWarnSpy = jest.spyOn(console, 'warn');
+ // Spy on console.error
+ consoleErrorSpy = jest.spyOn(console, 'error');
+ }
+
+ beforeAll(async () => {
jest.spyOn(Matterbridge.prototype, 'addBridgedEndpoint').mockImplementation((pluginName: string, device: MatterbridgeEndpoint) => {
// console.log(`Mocked addBridgedEndpoint: ${pluginName} ${device.name}`);
return Promise.resolve();
@@ -56,14 +91,30 @@ describe('Matterbridge platform', () => {
// Destroy the Matterbridge instance
await matterbridge.destroyInstance();
- // Restore the mocked AnsiLogger.log method
- (AnsiLogger.prototype.log as jest.Mock).mockRestore();
+ // Restore all mocks
+ jest.restoreAllMocks();
}, 60000);
beforeEach(() => {
+ // Clear all mocks
jest.clearAllMocks();
});
+ test('should clear JestPlatform', async () => {
+ // Clear all storage contexts
+ const environment = Environment.default;
+ environment.vars.set('path.root', path.join(os.homedir(), '.matterbridge', 'matterstorage.JestPlatform'));
+ const matterStorageService = environment.get(StorageService);
+ expect(matterStorageService).toBeDefined();
+ const matterStorageManager = await matterStorageService.open('Matterbridge');
+ expect(matterStorageManager).toBeDefined();
+ await matterStorageManager?.createContext('persist').clearAll();
+ await matterStorageManager?.createContext('events')?.clearAll();
+ await matterStorageManager?.createContext('fabrics')?.clearAll();
+ await matterStorageManager?.createContext('root')?.clearAll();
+ await matterStorageManager?.createContext('sessions')?.clearAll();
+ });
+
test('should be instance of MattebridgePlatform', () => {
expect(platform).toBeInstanceOf(MatterbridgePlatform);
});
@@ -72,7 +123,7 @@ describe('Matterbridge platform', () => {
platform = new MatterbridgePlatform(matterbridge, new AnsiLogger({ logName: 'Matterbridge platform' }), { name: 'test', type: 'type', debug: false, unregisterOnShutdown: false });
expect(platform.storage).toBeDefined();
expect(platform.storage).toBeInstanceOf(NodeStorageManager);
- expect(platform.log.debug).toHaveBeenCalledWith(expect.stringContaining('Creating storage for plugin test'));
+ expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, expect.stringContaining('Creating storage for plugin test'));
});
test('should do a partial mock of AnsiLogger', () => {
@@ -86,8 +137,8 @@ describe('Matterbridge platform', () => {
});
test('onStart should throw an error if not overridden', async () => {
- (platform.log.debug as jest.Mock).mockClear();
- expect.assertions(2);
+ // (platform.log.debug as jest.Mock).mockClear();
+ // expect.assertions(2);
try {
await platform.onStart('test reason');
} catch (error) {
@@ -283,8 +334,8 @@ describe('Matterbridge platform', () => {
testDevice.number = 100;
(matterbridge as any).devices.set(testDevice);
expect(await platform.checkEndpointNumbers()).toBe(1);
- expect(platform.log.warn).not.toHaveBeenCalled();
- expect(platform.log.debug).toHaveBeenCalledWith(`Setting endpoint number for device ${CYAN}${testDevice.uniqueId}${db} to ${CYAN}${testDevice.maybeNumber}${db}`);
+ expect(loggerLogSpy).not.toHaveBeenCalledWith(LogLevel.WARN, expect.anything());
+ expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, `Setting endpoint number for device ${CYAN}${testDevice.uniqueId}${db} to ${CYAN}${testDevice.maybeNumber}${db}`);
});
test('checkEndpointNumbers should check the testDevice', async () => {
@@ -295,8 +346,8 @@ describe('Matterbridge platform', () => {
testDevice.number = 100;
(matterbridge as any).devices.set(testDevice);
expect(await platform.checkEndpointNumbers()).toBe(1);
- expect(platform.log.warn).not.toHaveBeenCalled();
- expect(platform.log.debug).not.toHaveBeenCalledWith(`Setting endpoint number for device ${CYAN}${testDevice.uniqueId}${db} to ${CYAN}${testDevice.maybeNumber}${db}`);
+ expect(loggerLogSpy).not.toHaveBeenCalledWith(LogLevel.WARN, expect.anything());
+ expect(loggerLogSpy).not.toHaveBeenCalledWith(LogLevel.DEBUG, `Setting endpoint number for device ${CYAN}${testDevice.uniqueId}${db} to ${CYAN}${testDevice.maybeNumber}${db}`);
});
test('checkEndpointNumbers should not check the testDevice', async () => {
@@ -307,8 +358,8 @@ describe('Matterbridge platform', () => {
testDevice.number = 101;
(matterbridge as any).devices.set(testDevice);
expect(await platform.checkEndpointNumbers()).toBe(1);
- expect(platform.log.warn).toHaveBeenCalledWith(`Endpoint number for device ${CYAN}${testDevice.deviceName}${wr} changed from ${CYAN}100${wr} to ${CYAN}101${wr}`);
- expect(platform.log.debug).not.toHaveBeenCalledWith(`Setting endpoint number for device ${CYAN}${testDevice.uniqueId}${db} to ${CYAN}${testDevice.maybeNumber}${db}`);
+ expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.WARN, `Endpoint number for device ${CYAN}${testDevice.deviceName}${wr} changed from ${CYAN}100${wr} to ${CYAN}101${wr}`);
+ expect(loggerLogSpy).not.toHaveBeenCalledWith(LogLevel.DEBUG, `Setting endpoint number for device ${CYAN}${testDevice.uniqueId}${db} to ${CYAN}${testDevice.maybeNumber}${db}`);
});
test('checkEndpointNumbers should not check the testDevice without uniqueId', async () => {
@@ -320,7 +371,7 @@ describe('Matterbridge platform', () => {
(matterbridge as any).devices.set(testDevice);
testDevice.uniqueId = undefined;
expect(await platform.checkEndpointNumbers()).toBe(1);
- expect(platform.log.debug).toHaveBeenCalledWith(`Not checking device ${testDevice.deviceName} without uniqueId or maybeNumber`);
+ expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, `Not checking device ${testDevice.deviceName} without uniqueId or maybeNumber`);
});
test('checkEndpointNumbers should check the testDevice with child endpoints', async () => {
@@ -339,10 +390,10 @@ describe('Matterbridge platform', () => {
expect(await testDevice.getChildEndpoints()).toHaveLength(2);
jest.clearAllMocks();
expect(await platform.checkEndpointNumbers()).toBe(3);
- expect(platform.log.warn).not.toHaveBeenCalledWith(`Endpoint number for device ${CYAN}${testDevice.uniqueId}${wr} changed from ${CYAN}100${wr} to ${CYAN}101${wr}`);
- expect(platform.log.debug).not.toHaveBeenCalledWith(`Setting endpoint number for device ${CYAN}${testDevice.uniqueId}${db} to ${CYAN}${testDevice.maybeNumber}${db}`);
- expect(platform.log.debug).toHaveBeenCalledWith(`Setting child endpoint number for device ${CYAN}${testDevice.uniqueId}${db}.${CYAN}child1${db} to ${CYAN}201${db}`);
- expect(platform.log.debug).toHaveBeenCalledWith(`Setting child endpoint number for device ${CYAN}${testDevice.uniqueId}${db}.${CYAN}child2${db} to ${CYAN}202${db}`);
+ expect(loggerLogSpy).not.toHaveBeenCalledWith(LogLevel.WARN, `Endpoint number for device ${CYAN}${testDevice.uniqueId}${wr} changed from ${CYAN}100${wr} to ${CYAN}101${wr}`);
+ expect(loggerLogSpy).not.toHaveBeenCalledWith(LogLevel.DEBUG, `Setting endpoint number for device ${CYAN}${testDevice.uniqueId}${db} to ${CYAN}${testDevice.maybeNumber}${db}`);
+ expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, `Setting child endpoint number for device ${CYAN}${testDevice.uniqueId}${db}.${CYAN}child1${db} to ${CYAN}201${db}`);
+ expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, `Setting child endpoint number for device ${CYAN}${testDevice.uniqueId}${db}.${CYAN}child2${db} to ${CYAN}202${db}`);
});
test('checkEndpointNumbers should validate the testDevice with child endpoints', async () => {
@@ -361,9 +412,9 @@ describe('Matterbridge platform', () => {
expect(await testDevice.getChildEndpoints()).toHaveLength(2);
jest.clearAllMocks();
expect(await platform.checkEndpointNumbers()).toBe(3);
- expect(platform.log.warn).not.toHaveBeenCalled();
- expect(platform.log.debug).toHaveBeenCalledWith('Checking endpoint numbers...');
- expect(platform.log.debug).toHaveBeenCalledWith('Endpoint numbers check completed.');
+ expect(loggerLogSpy).not.toHaveBeenCalledWith(LogLevel.WARN, expect.anything());
+ expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, 'Checking endpoint numbers...');
+ expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, 'Endpoint numbers check completed.');
});
test('checkEndpointNumbers should not validate the testDevice with child endpoints', async () => {
@@ -382,28 +433,28 @@ describe('Matterbridge platform', () => {
expect(await testDevice.getChildEndpoints()).toHaveLength(2);
jest.clearAllMocks();
expect(await platform.checkEndpointNumbers()).toBe(3);
- expect(platform.log.warn).toHaveBeenCalled();
- expect(platform.log.debug).toHaveBeenCalledTimes(2);
- expect(platform.log.debug).toHaveBeenCalledWith('Checking endpoint numbers...');
- expect(platform.log.debug).toHaveBeenCalledWith('Endpoint numbers check completed.');
+ expect(loggerLogSpy).toHaveBeenCalledTimes(4);
+ expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, 'Checking endpoint numbers...');
+ expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, 'Endpoint numbers check completed.');
});
test('onConfigure should log a message', async () => {
await platform.onConfigure();
- expect(platform.log.debug).toHaveBeenCalledWith('Configuring platform ');
+ expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, 'Configuring platform ');
});
test('onChangeLoggerLevel should log a debug message if not overridden', async () => {
await platform.onChangeLoggerLevel(LogLevel.DEBUG);
- expect(platform.log.debug).toHaveBeenCalledWith("The plugin doesn't override onChangeLoggerLevel. Logger level set to: debug");
+ expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, "The plugin doesn't override onChangeLoggerLevel. Logger level set to: debug");
});
test('onShutdown should log a message', async () => {
await platform.onShutdown('test reason');
- expect(platform.log.debug).toHaveBeenCalledWith('Shutting down platform ', 'test reason');
+ expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, 'Shutting down platform ', 'test reason');
});
test('registerDevice calls matterbridge.addBridgedEndpoint with correct parameters', async () => {
+ await platform.unregisterAllDevices();
const testDevice = new MatterbridgeEndpoint(powerSource);
testDevice.createDefaultBasicInformationClusterServer('test', 'serial01234', 0xfff1, 'Matterbridge', 0x8001, 'Test device');
await platform.registerDevice(testDevice);
@@ -412,6 +463,7 @@ describe('Matterbridge platform', () => {
});
test('unregisterDevice calls matterbridge.removeBridgedEndpoint with correct parameters', async () => {
+ await platform.unregisterAllDevices();
const testDevice = new MatterbridgeEndpoint(powerSource);
testDevice.createDefaultBasicInformationClusterServer('test', 'serial01234', 0xfff1, 'Matterbridge', 0x8001, 'Test device');
await platform.unregisterDevice(testDevice);
@@ -424,4 +476,20 @@ describe('Matterbridge platform', () => {
expect(platform.registeredEndpoints.size).toBe(0);
expect(matterbridge.removeAllBridgedEndpoints).toHaveBeenCalled();
});
+
+ test('registerDevice should log error if the device name already exist', async () => {
+ await platform.unregisterAllDevices();
+ expect(platform.registeredEndpoints.size).toBe(0);
+ expect(platform.registeredEndpointsByName.size).toBe(0);
+ expect(matterbridge.removeAllBridgedEndpoints).toHaveBeenCalled();
+
+ platform.registeredEndpoints.set('test', new MatterbridgeEndpoint(powerSource));
+ platform.registeredEndpointsByName.set('test', new MatterbridgeEndpoint(powerSource));
+ const device = new MatterbridgeEndpoint(powerSource);
+ device.createDefaultBasicInformationClusterServer('test', 'serial01234', 0xfff1, 'Matterbridge', 0x8001, 'Test device');
+ await platform.registerDevice(device);
+ expect(platform.registeredEndpoints.size).toBe(1);
+ expect(platform.registeredEndpointsByName.size).toBe(1);
+ expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.ERROR, `Device with name ${CYAN}${device.deviceName}${er} is already registered. The device will not be added. Please change the device name.`);
+ });
});
diff --git a/src/matterbridgePlatform.ts b/src/matterbridgePlatform.ts
index 176c29e8..9fc84e72 100644
--- a/src/matterbridgePlatform.ts
+++ b/src/matterbridgePlatform.ts
@@ -24,10 +24,11 @@
// Matterbridge
import { Matterbridge } from './matterbridge.js';
import { MatterbridgeEndpoint } from './matterbridgeEndpoint.js';
+import { checkNotLatinCharacters } from './matterbridgeEndpointHelpers.js';
import { isValidArray, isValidObject, isValidString } from './utils/utils.js';
// AnsiLogger module
-import { AnsiLogger, CYAN, db, LogLevel, nf, wr } from './logger/export.js';
+import { AnsiLogger, CYAN, db, er, LogLevel, nf, wr } from './logger/export.js';
// Storage module
import { NodeStorage, NodeStorageManager } from './storage/export.js';
@@ -58,11 +59,18 @@ export class MatterbridgePlatform {
public name = ''; // Will be set by the loadPlugin() method using the package.json value.
public type = ''; // Will be set by the extending classes.
public version = '1.0.0'; // Will be set by the loadPlugin() method using the package.json value.
+
+ // Platform storage
public storage: NodeStorageManager | undefined;
public context: NodeStorage | undefined;
+
+ // Device and entity selection
public selectDevice = new Map();
public selectEntity = new Map();
- public registeredEndpoints = new Map();
+
+ // Registered devices
+ public registeredEndpoints = new Map(); // uniqueId, MatterbridgeEndpoint
+ public registeredEndpointsByName = new Map(); // deviceName, MatterbridgeEndpoint
/**
* Creates an instance of the base MatterbridgePlatform. It is extended by the MatterbridgeAccessoryPlatform and MatterbridgeServicePlatform classes.
@@ -110,7 +118,7 @@ export class MatterbridgePlatform {
}
/**
- * This method can be overridden in the extended class. Call super.onShutdown() to run checkEndpointNumbers().
+ * This method can be overridden in the extended class. Call super.onShutdown() to run checkEndpointNumbers() and cleanup memory.
* It is called when the platform is shutting down.
* Use this method to clean up any resources.
* @param {string} [reason] - The reason for shutting down.
@@ -118,6 +126,14 @@ export class MatterbridgePlatform {
async onShutdown(reason?: string) {
this.log.debug(`Shutting down platform ${this.name}`, reason);
await this.checkEndpointNumbers();
+ this.selectDevice.clear();
+ this.selectEntity.clear();
+ this.registeredEndpoints.clear();
+ this.registeredEndpointsByName.clear();
+ await this.context?.close();
+ this.context = undefined;
+ await this.storage?.close();
+ this.storage = undefined;
}
/**
@@ -128,14 +144,31 @@ export class MatterbridgePlatform {
this.log.debug(`The plugin doesn't override onChangeLoggerLevel. Logger level set to: ${logLevel}`);
}
+ /**
+ * Check if a device with this name is already registered in the platform.
+ * @param {string} deviceName - The device name to check.
+ * @returns {boolean} True if the device is already registered, false otherwise.
+ */
+ hasDeviceName(deviceName: string): boolean {
+ return this.registeredEndpointsByName.has(deviceName);
+ }
+
/**
* Registers a device with the Matterbridge platform.
* @param {MatterbridgeEndpoint} device - The device to register.
*/
async registerDevice(device: MatterbridgeEndpoint) {
device.plugin = this.name;
+ if (device.deviceName && this.registeredEndpointsByName.has(device.deviceName)) {
+ this.log.error(`Device with name ${CYAN}${device.deviceName}${er} is already registered. The device will not be added. Please change the device name.`);
+ return;
+ }
+ if (device.deviceName && checkNotLatinCharacters(device.deviceName)) {
+ this.log.debug(`Device with name ${CYAN}${device.deviceName}${db} has non latin characters.`);
+ }
await this.matterbridge.addBridgedEndpoint(this.name, device);
if (device.uniqueId) this.registeredEndpoints.set(device.uniqueId, device);
+ if (device.deviceName) this.registeredEndpointsByName.set(device.deviceName, device);
}
/**
@@ -145,6 +178,7 @@ export class MatterbridgePlatform {
async unregisterDevice(device: MatterbridgeEndpoint) {
await this.matterbridge.removeBridgedEndpoint(this.name, device);
if (device.uniqueId) this.registeredEndpoints.delete(device.uniqueId);
+ if (device.deviceName) this.registeredEndpointsByName.delete(device.deviceName);
}
/**
@@ -153,6 +187,7 @@ export class MatterbridgePlatform {
async unregisterAllDevices() {
await this.matterbridge.removeAllBridgedEndpoints(this.name);
this.registeredEndpoints.clear();
+ this.registeredEndpointsByName.clear();
}
/**
diff --git a/src/utils/utils.test.ts b/src/utils/utils.test.ts
index acaa578b..c34b60ac 100644
--- a/src/utils/utils.test.ts
+++ b/src/utils/utils.test.ts
@@ -16,6 +16,7 @@ import {
isValidNull,
isValidUndefined,
createZip,
+ getNpmPackageVersion,
} from './utils';
import { promises as fs } from 'fs';
import path from 'path';
@@ -410,4 +411,36 @@ describe('Utils test', () => {
const size = await createZip(path.join('test', 'array.zip'), 'package.json', '*.js', path.join('src', 'utils'));
expect(size).toBeGreaterThan(0);
}, 60000);
+
+ it('should get the latest version', async () => {
+ const version = await getNpmPackageVersion('matterbridge');
+ expect(version).toBeDefined();
+ expect(typeof version).toBe('string');
+ // console.log('Latest version:', version);
+ }, 60000);
+
+ it('should get the latest dev version', async () => {
+ const devVersion = await getNpmPackageVersion('matterbridge', 'dev', 1000);
+ expect(devVersion).toBeDefined();
+ expect(typeof devVersion).toBe('string');
+ // console.log('Latest version tag dev:', devVersion);
+ }, 60000);
+
+ it('should get version for tag latest and dev', async () => {
+ const version = await getNpmPackageVersion('matterbridge', 'latest', 1000);
+ expect(version).toBeDefined();
+ expect(typeof version).toBe('string');
+ // console.log('Latest version:', version);
+
+ const devVersion = await getNpmPackageVersion('matterbridge', 'dev', 1000);
+ expect(devVersion).toBeDefined();
+ expect(typeof devVersion).toBe('string');
+ // console.log('Latest version tag dev:', devVersion);
+
+ expect(devVersion).not.toBe(version);
+ }, 60000);
+
+ it('should not get the latest version of a non existing package', async () => {
+ await expect(getNpmPackageVersion('matterbridge1234567')).rejects.toThrow('Failed to fetch data. Status code: 404');
+ }, 60000);
});
diff --git a/src/utils/utils.ts b/src/utils/utils.ts
index 3efff149..a27b3920 100644
--- a/src/utils/utils.ts
+++ b/src/utils/utils.ts
@@ -23,14 +23,11 @@
// Node.js modules
import os from 'os';
-import { createWriteStream, statSync } from 'fs';
import path from 'path';
-import * as dns from 'dns';
+import { createWriteStream, statSync } from 'fs';
import { promises as fs } from 'fs';
+import * as dns from 'dns';
-// Package modules
-// import archiver, { ArchiverError, EntryData } from 'archiver';
-// import { glob } from 'glob';
import type { ArchiverError, EntryData } from 'archiver';
// AnsiLogger module
@@ -640,3 +637,61 @@ export function getIntParameter(name: string): number | undefined {
if (!isValidNumber(intValue)) return undefined;
return intValue;
}
+
+/**
+ * Retrieves the version of an npm package from the npm registry.
+ *
+ * @param {string} packageName - The name of the npm package.
+ * @param {string} [tag='latest'] - The tag of the package version to retrieve (default is 'latest').
+ * @param {number} [timeout=5000] - The timeout duration in milliseconds (default is 5000ms).
+ * @returns {Promise} A promise that resolves to the version string of the package.
+ * @throws {Error} If the request fails or the tag is not found.
+ */
+export async function getNpmPackageVersion(packageName: string, tag = 'latest', timeout = 5000): Promise {
+ const https = await import('https');
+ return new Promise((resolve, reject) => {
+ const url = `https://registry.npmjs.org/${packageName}`;
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => {
+ controller.abort();
+ reject(new Error(`Request timed out after ${timeout / 1000} seconds`));
+ }, timeout);
+
+ const req = https.get(url, { signal: controller.signal }, (res) => {
+ let data = '';
+
+ if (res.statusCode !== 200) {
+ clearTimeout(timeoutId);
+ res.resume(); // Discard response data to close the socket properly
+ req.destroy(); // Forcefully close the request
+ reject(new Error(`Failed to fetch data. Status code: ${res.statusCode}`));
+ return;
+ }
+
+ res.on('data', (chunk) => {
+ data += chunk;
+ });
+
+ res.on('end', () => {
+ clearTimeout(timeoutId);
+ try {
+ const jsonData = JSON.parse(data);
+ // console.log(`Package ${packageName} tag ${tag}`, jsonData);
+ const version = jsonData['dist-tags']?.[tag];
+ if (version) {
+ resolve(version);
+ } else {
+ reject(new Error(`Tag "${tag}" not found for package "${packageName}"`));
+ }
+ } catch (error) {
+ reject(new Error(`Failed to parse response JSON: ${error instanceof Error ? error.message : error}`));
+ }
+ });
+ });
+
+ req.on('error', (error) => {
+ clearTimeout(timeoutId);
+ reject(new Error(`Request failed: ${error instanceof Error ? error.message : error}`));
+ });
+ });
+}