From d9338dff12d5106b5b91c5684170a339a5646b0c Mon Sep 17 00:00:00 2001 From: Luligu Date: Wed, 12 Mar 2025 12:34:27 +0100 Subject: [PATCH 01/11] Update workflows --- .github/workflows/build-matterbridge-plugin.yml | 11 +++++------ .github/workflows/publish-matterbridge-plugin.yml | 10 +++++----- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build-matterbridge-plugin.yml b/.github/workflows/build-matterbridge-plugin.yml index ea4a65d..dd3f9d5 100644 --- a/.github/workflows/build-matterbridge-plugin.yml +++ b/.github/workflows/build-matterbridge-plugin.yml @@ -9,33 +9,33 @@ jobs: strategy: fail-fast: false matrix: - node-version: [18.x, 20.x, 22.x] + node-version: [18.x, 20.x, 22.x, 23.x] os: [ubuntu-latest, windows-latest, macos-latest] steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} + - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - name: Clean cache run: npm cache clean --force - + - name: Verify Node.js version run: node -v - name: Verify Npm version run: npm -v - + - name: Install matterbridge run: npm install -g matterbridge --omit=dev - name: Install dependencies run: npm ci - + - name: Link matterbridge run: npm link matterbridge @@ -47,4 +47,3 @@ jobs: - name: Build the project run: npm run build - diff --git a/.github/workflows/publish-matterbridge-plugin.yml b/.github/workflows/publish-matterbridge-plugin.yml index ac4c71f..0cba041 100644 --- a/.github/workflows/publish-matterbridge-plugin.yml +++ b/.github/workflows/publish-matterbridge-plugin.yml @@ -7,7 +7,7 @@ on: jobs: publish: runs-on: ubuntu-latest - + steps: - name: Checkout repository uses: actions/checkout@v4 @@ -15,24 +15,24 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20.x' + node-version: '22.x' registry-url: 'https://registry.npmjs.org' - name: Clean cache run: npm cache clean --force - + - name: Verify Node.js version run: node -v - name: Verify Npm version run: npm -v - + - name: Install matterbridge run: npm install -g matterbridge --omit=dev - name: Install dependencies run: npm ci - + - name: Link matterbridge run: npm link matterbridge From 0a4f4e8c88bd0fe2a3cba4853bd2a86fefa75faa Mon Sep 17 00:00:00 2001 From: Luligu Date: Wed, 12 Mar 2025 17:04:08 +0100 Subject: [PATCH 02/11] Improved change IP management --- src/mdnsScanner.ts | 4 ++-- src/platform.ts | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/mdnsScanner.ts b/src/mdnsScanner.ts index 34b542a..59bcd9d 100644 --- a/src/mdnsScanner.ts +++ b/src/mdnsScanner.ts @@ -176,7 +176,7 @@ export class MdnsScanner extends EventEmitter { // const [name, mac] = a.name.replace('.local', '').split('-'); // const deviceId = name.toLowerCase() + '-' + mac.toUpperCase(); const deviceId = this.normalizeShellyId(a.name); - if (deviceId && !this.discoveredDevices.has(deviceId)) { + if (deviceId && (!this.discoveredDevices.has(deviceId) || this.discoveredDevices.get(deviceId)?.host !== a.data)) { this.log.debug(`MdnsScanner discovered shelly gen: ${CYAN}${gen}${nf} device id: ${hk}${deviceId}${nf} host: ${zb}${a.data}${nf} port: ${zb}${port}${nf}`); this.discoveredDevices.set(deviceId, { id: deviceId, host: a.data, port, gen }); this.emit('discovered', { id: deviceId, host: a.data, port, gen }); @@ -221,7 +221,7 @@ export class MdnsScanner extends EventEmitter { // const [name, mac] = a.name.replace('.local', '').split('-'); // const deviceId = name.toLowerCase() + '-' + mac.toUpperCase(); const deviceId = this.normalizeShellyId(a.name); - if (deviceId && !this.discoveredDevices.has(deviceId)) { + if (deviceId && (!this.discoveredDevices.has(deviceId) || this.discoveredDevices.get(deviceId)?.host !== a.data)) { this.log.debug(`MdnsScanner discovered shelly gen: ${CYAN}${gen}${nf} device id: ${hk}${deviceId}${nf} host: ${zb}${a.data}${nf} port: ${zb}${port}${nf}`); this.discoveredDevices.set(deviceId, { id: deviceId, host: a.data, port, gen }); this.emit('discovered', { id: deviceId, host: a.data, port, gen }); diff --git a/src/platform.ts b/src/platform.ts index ec6ba1a..6050a23 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -296,7 +296,7 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { const stored = this.storedDevices.get(discoveredDevice.id); if (stored?.host !== discoveredDevice.host) { this.log.warn(`Shelly device ${hk}${discoveredDevice.id}${wr} host ${zb}${discoveredDevice.host}${wr} has been discovered with a different host.`); - this.log.warn(`Set new address for shelly device ${hk}${discoveredDevice.id}${wr} from ${zb}${stored?.host}${wr} to ${zb}${discoveredDevice.host}${wr}`); + this.log.warn(`Setting the new address for shelly device ${hk}${discoveredDevice.id}${wr} from ${zb}${stored?.host}${wr} to ${zb}${discoveredDevice.host}${wr}...`); this.discoveredDevices.set(discoveredDevice.id, discoveredDevice); this.storedDevices.set(discoveredDevice.id, discoveredDevice); this.changedDevices.set(discoveredDevice.id, discoveredDevice.id); @@ -304,7 +304,13 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { if (this.shelly.hasDevice(discoveredDevice.id)) { const device = this.shelly.getDevice(discoveredDevice.id) as ShellyDevice; device.host = discoveredDevice.host; - device.wsClient?.stop(); // It will be restarted by the ShellyDevice interval if gen > 1 + if (device.gen === 1) { + this.shelly.coapServer.registerDevice(device.host, device.id, true); + } else { + device.wsClient?.stop(); + device.wsClient?.setHost(device.host); + device.wsClient?.start(); + } device.log.warn(`Shelly device ${hk}${discoveredDevice.id}${wr} host ${zb}${discoveredDevice.host}${wr} updated`); } else this.log.warn(`Please restart matterbridge for the change to take effect.`); } else { From 22c16073107c19c0ea43c5bd49c716e4387dba24 Mon Sep 17 00:00:00 2001 From: Luligu Date: Mon, 17 Mar 2025 11:59:32 +0100 Subject: [PATCH 03/11] Dev 2.0.5-dev.1 --- CHANGELOG.md | 23 +- matterbridge-shelly.schema.json | 68 ++-- package-lock.json | 624 ++++---------------------------- package.json | 4 +- src/index.test.ts | 2 +- src/platform.test.ts | 4 +- src/platform.ts | 66 +++- 7 files changed, 192 insertions(+), 599 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 305b141..1f6da1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,17 +21,38 @@ Removed options: - exposePowerMeter: electrical sensors are enabled by default on existing setups. You can disable them globally or on a per-device basis adding "PowerMeter" to entityBlackList or deviceEntityBlackList, see [COMPONENTS.md documentation.](https://github.com/Luligu/matterbridge-shelly/blob/main/COMPONENTS.md). On new setups the "PowerMeter" components are already globally disabled by default. +- enableConfigDiscover and deviceIp: replaced by a manually add a device with IP address in expert mode. + New setups: - these components are blacklisted (with entityBlackList) by default: "PowerMeter", "Lux", "Illuminance", "Vibration", "Button". This allows to create simplified devices for the controllers that don't manage correctly composed devices (i.e. Alexa and SmartThings). - all switches are exposed like outlet (matter compliant). - shellyplusi4, shellyi4g3, shellyix3 and shellybutton1 are automatically added to inputMomentaryList when discovered. -- expertMode is disabled. This makes the config showing only username, password and blackList. +- expertMode is disabled. This makes the config showing only username, password, whitelist and blackList. Expert mode: The expertMode option has been added to show an advanced or simplified config. +## [2.0.5] - 2025-03-17 + +### Added + +- [config] Added action: manually add a device with IP address. This allows to add the devices that are not discovered on the network with the mdns. +- [config] Added action: remove a device from the storage with its device id. This allows to remove from the storage a single device when it is no more on the network. +- [config] Added action: scan network. This will send a mdns request on the network. + +### Changed + +- [config] Removed enableConfigDiscover and deviceIp. Replaced by a config action (manually add a device with IP address). +- [package]: Updated package. +- [package]: Updated dependencies. +- [plugin]: Requires Matterbridge 2.2.5. + + + Buy me a coffee + + ## [2.0.4] - 2025-03-13 ### Added diff --git a/matterbridge-shelly.schema.json b/matterbridge-shelly.schema.json index cf39c30..197cf37 100644 --- a/matterbridge-shelly.schema.json +++ b/matterbridge-shelly.schema.json @@ -6,12 +6,14 @@ "name": { "description": "Plugin name", "type": "string", - "readOnly": true + "readOnly": true, + "ui:widget": "hidden" }, "type": { "description": "Plugin type", "type": "string", - "readOnly": true + "readOnly": true, + "ui:widget": "hidden" }, "username": { "description": "Username for password protected shelly devices (used only for gen 1 devices)", @@ -19,7 +21,8 @@ }, "password": { "description": "Password for password protected shelly devices (must be unique for all the devices)", - "type": "string" + "type": "string", + "ui:widget": "password" }, "switchList": { "description": "The devices in the list will be exposed as switches (don't use it for Alexa).", @@ -66,8 +69,8 @@ "uniqueItems": true, "selectFrom": "serial" }, - "blackList": { - "description": "The devices in the list will not be exposed. Use the device id (e.g. shellyplus2pm-5443B23D81F8) or BLU addr (i.e. 7c:c6:b6:65:2d:87)", + "whiteList": { + "description": "Only the devices in the list will be exposed. Use the device id (e.g. shellyplus2pm-5443B23D81F8) or BLU addr (i.e. 7c:c6:b6:65:2d:87).", "type": "array", "items": { "type": "string" @@ -75,8 +78,8 @@ "uniqueItems": true, "selectFrom": "serial" }, - "whiteList": { - "description": "Only the devices in the list will be exposed. Use the device id (e.g. shellyplus2pm-5443B23D81F8) or BLU addr (i.e. 7c:c6:b6:65:2d:87).", + "blackList": { + "description": "The devices in the list will not be exposed. Use the device id (e.g. shellyplus2pm-5443B23D81F8) or BLU addr (i.e. 7c:c6:b6:65:2d:87).", "type": "array", "items": { "type": "string" @@ -85,7 +88,7 @@ "selectFrom": "serial" }, "entityBlackList": { - "description": "The components in the list will not be exposed for all devices. Use the component name (i.e. Temperature)", + "description": "The components in the list will not be exposed for all devices. Use the component name (i.e. Temperature).", "type": "array", "items": { "type": "string" @@ -109,7 +112,7 @@ } }, "nocacheList": { - "description": "The devices in the list will not be loaded from the cache. Use the device id (e.g. shellyplus2pm-5443B23D81F8)", + "description": "The devices in the list will not be loaded from the cache. Use the device id (e.g. shellyplus2pm-5443B23D81F8).", "type": "array", "items": { "type": "string" @@ -117,37 +120,23 @@ "uniqueItems": true, "selectFrom": "serial" }, - "deviceIp": { - "description": "Set the IP address for each device that is not discovered automatically. Enter in the first field the shelly ID of the device and in the second field the IP address. (e.g. shelly1minig3-543204547478: 192.168.1.221). Enable enableConfigDiscover to load the devices from this setting.", - "type": "object", - "uniqueItems": true, - "selectFrom": "serial", - "additionalProperties": { - "type": "string" - } - }, "enableMdnsDiscover": { - "description": "Enable the mdns discovery for shelly devices. Once all the devices are loaded and stored, it is possible to disable this setting to reduce the network traffic.", + "description": "Enable the mdns discovery for shelly devices. Once all the devices are discovered and stored, it is possible to disable this setting to reduce the network traffic.", "type": "boolean", "default": true }, "enableStorageDiscover": { - "description": "Enable storage discovery for shelly devices (it will load from the storage the devices already discovered)", + "description": "Enable storage discovery for shelly devices (it will load from the storage the devices already discovered).", "type": "boolean", "default": true }, "resetStorageDiscover": { - "description": "Reset the storage on the next restart (it will clear the storage and the cache files)", - "type": "boolean", - "default": false - }, - "enableConfigDiscover": { - "description": "Enable config discovery for shelly devices (it will load the devices from deviceIp config setting). It is only needed if a device is not discovered on your network. Once they are loaded and stored, disable this setting.", + "description": "Reset the storage on the next restart (it will clear the storage and the cache files).", "type": "boolean", "default": false }, "enableBleDiscover": { - "description": "Enable ble discovery for shelly BLU devices (it will register the BLU devices paired in each ble gateway, see the readme for more info)", + "description": "Enable ble discovery for shelly BLU devices (it will register the BLU devices paired in each ble gateway, see the readme for more info).", "type": "boolean", "default": true }, @@ -166,6 +155,26 @@ "type": "boolean", "default": false }, + "addDevice": { + "description": "Manually add a device that has not been discovered with mdns:", + "type": "boolean", + "buttonField": "ADD", + "textPlaceholder": "Enter the device IP address", + "default": false + }, + "removeDevice": { + "description": "Remove a device and its cache file from the storage:", + "type": "boolean", + "buttonField": "REMOVE", + "textPlaceholder": "Enter the device id", + "default": false + }, + "scanNetwork": { + "description": "Scan the network with mdns for new devices:", + "type": "boolean", + "buttonText": "SCAN", + "default": false + }, "debug": { "description": "Enable the debug for the plugin (development only)", "type": "boolean", @@ -182,14 +191,15 @@ "default": false }, "debugWs": { - "description": "Enable the debug for the shelly WsClient or WsServer (development only)", + "description": "Enable the debug for the shelly WsClient and WsServer (development only)", "type": "boolean", "default": false }, "unregisterOnShutdown": { "description": "Unregister all devices on shutdown (development only)", "type": "boolean", - "default": false + "default": false, + "ui:widget": "hidden" } } } diff --git a/package-lock.json b/package-lock.json index 26356a1..9012579 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "matterbridge-shelly", - "version": "2.0.4", + "version": "2.0.5-dev.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "matterbridge-shelly", - "version": "2.0.4", + "version": "2.0.5-dev.1", "license": "Apache-2.0", "dependencies": { "coap": "1.4.1", @@ -31,7 +31,7 @@ "prettier": "3.5.3", "ts-jest": "29.2.6", "typescript": "5.8.2", - "typescript-eslint": "8.26.0" + "typescript-eslint": "8.26.1" }, "engines": { "node": ">=18.0.0 <19.0.0 || >=20.0.0 <21.0.0 || >=22.0.0" @@ -81,22 +81,22 @@ } }, "node_modules/@babel/core": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz", - "integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.9", + "@babel/generator": "^7.26.10", "@babel/helper-compilation-targets": "^7.26.5", "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.9", - "@babel/parser": "^7.26.9", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", "@babel/template": "^7.26.9", - "@babel/traverse": "^7.26.9", - "@babel/types": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -122,14 +122,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz", - "integrity": "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", + "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.9", - "@babel/types": "^7.26.9", + "@babel/parser": "^7.26.10", + "@babel/types": "^7.26.10", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -248,27 +248,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz", - "integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", + "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.26.9", - "@babel/types": "^7.26.9" + "@babel/types": "^7.26.10" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", - "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", + "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.26.9" + "@babel/types": "^7.26.10" }, "bin": { "parser": "bin/babel-parser.js" @@ -532,17 +532,17 @@ } }, "node_modules/@babel/traverse": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz", - "integrity": "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.10.tgz", + "integrity": "sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.9", - "@babel/parser": "^7.26.9", + "@babel/generator": "^7.26.10", + "@babel/parser": "^7.26.10", "@babel/template": "^7.26.9", - "@babel/types": "^7.26.9", + "@babel/types": "^7.26.10", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -561,9 +561,9 @@ } }, "node_modules/@babel/types": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", - "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", + "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", "dev": true, "license": "MIT", "dependencies": { @@ -582,9 +582,9 @@ "license": "MIT" }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.0.tgz", - "integrity": "sha512-RoV8Xs9eNwiDvhv7M+xcL4PWyRyIXRY/FLp3buU4h1EYfdF7unWUy3dOjPqb3C7rMUewIcqwW850PgS8h1o1yg==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz", + "integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==", "dev": true, "license": "MIT", "dependencies": { @@ -1526,17 +1526,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.0.tgz", - "integrity": "sha512-cLr1J6pe56zjKYajK6SSSre6nl1Gj6xDp1TY0trpgPzjVbgDwd09v2Ws37LABxzkicmUjhEeg/fAUjPJJB1v5Q==", + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.1.tgz", + "integrity": "sha512-2X3mwqsj9Bd3Ciz508ZUtoQQYpOhU/kWoUqIf49H8Z0+Vbh6UF/y0OEYp0Q0axOGzaBGs7QxRwq0knSQ8khQNA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.26.0", - "@typescript-eslint/type-utils": "8.26.0", - "@typescript-eslint/utils": "8.26.0", - "@typescript-eslint/visitor-keys": "8.26.0", + "@typescript-eslint/scope-manager": "8.26.1", + "@typescript-eslint/type-utils": "8.26.1", + "@typescript-eslint/utils": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1555,144 +1555,17 @@ "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.26.0.tgz", - "integrity": "sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.26.0", - "@typescript-eslint/visitor-keys": "8.26.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.26.0.tgz", - "integrity": "sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.0.tgz", - "integrity": "sha512-tiJ1Hvy/V/oMVRTbEOIeemA2XoylimlDQ03CgPPNaHYZbpsc78Hmngnt+WXZfJX1pjQ711V7g0H7cSJThGYfPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.26.0", - "@typescript-eslint/visitor-keys": "8.26.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.26.0.tgz", - "integrity": "sha512-2L2tU3FVwhvU14LndnQCA2frYC8JnPDVKyQtWFPf8IYFMt/ykEN1bPolNhNbCVgOmdzTlWdusCTKA/9nKrf8Ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.26.0", - "@typescript-eslint/types": "8.26.0", - "@typescript-eslint/typescript-estree": "8.26.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.0.tgz", - "integrity": "sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.26.0", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@typescript-eslint/parser": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.26.0.tgz", - "integrity": "sha512-mNtXP9LTVBy14ZF3o7JG69gRPBK/2QWtQd0j0oH26HcY/foyJJau6pNUez7QrM5UHnSvwlQcJXKsk0I99B9pOA==", + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.26.1.tgz", + "integrity": "sha512-w6HZUV4NWxqd8BdeFf81t07d7/YV9s7TCWrQQbG5uhuvGUAW+fq1usZ1Hmz9UPNLniFnD8GLSsDpjP0hm1S4lQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.26.0", - "@typescript-eslint/types": "8.26.0", - "@typescript-eslint/typescript-estree": "8.26.0", - "@typescript-eslint/visitor-keys": "8.26.0", + "@typescript-eslint/scope-manager": "8.26.1", + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/typescript-estree": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1", "debug": "^4.3.4" }, "engines": { @@ -1707,109 +1580,6 @@ "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.26.0.tgz", - "integrity": "sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.26.0", - "@typescript-eslint/visitor-keys": "8.26.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.26.0.tgz", - "integrity": "sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.0.tgz", - "integrity": "sha512-tiJ1Hvy/V/oMVRTbEOIeemA2XoylimlDQ03CgPPNaHYZbpsc78Hmngnt+WXZfJX1pjQ711V7g0H7cSJThGYfPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.26.0", - "@typescript-eslint/visitor-keys": "8.26.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.0.tgz", - "integrity": "sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.26.0", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@typescript-eslint/scope-manager": { "version": "8.26.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.26.1.tgz", @@ -1829,75 +1599,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.26.0.tgz", - "integrity": "sha512-ruk0RNChLKz3zKGn2LwXuVoeBcUMh+jaqzN461uMMdxy5H9epZqIBtYj7UiPXRuOpaALXGbmRuZQhmwHhaS04Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "8.26.0", - "@typescript-eslint/utils": "8.26.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.26.0.tgz", - "integrity": "sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.26.0", - "@typescript-eslint/visitor-keys": "8.26.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.26.0.tgz", - "integrity": "sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.0.tgz", - "integrity": "sha512-tiJ1Hvy/V/oMVRTbEOIeemA2XoylimlDQ03CgPPNaHYZbpsc78Hmngnt+WXZfJX1pjQ711V7g0H7cSJThGYfPQ==", + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.26.1.tgz", + "integrity": "sha512-Kcj/TagJLwoY/5w9JGEFV0dclQdyqw9+VMndxOJKtoFSjfZhLXhYjzsQEeyza03rwHx2vFEGvrJWJBXKleRvZg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.26.0", - "@typescript-eslint/visitor-keys": "8.26.0", + "@typescript-eslint/typescript-estree": "8.26.1", + "@typescript-eslint/utils": "8.26.1", "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", "ts-api-utils": "^2.0.1" }, "engines": { @@ -1907,78 +1617,11 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.26.0.tgz", - "integrity": "sha512-2L2tU3FVwhvU14LndnQCA2frYC8JnPDVKyQtWFPf8IYFMt/ykEN1bPolNhNbCVgOmdzTlWdusCTKA/9nKrf8Ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.26.0", - "@typescript-eslint/types": "8.26.0", - "@typescript-eslint/typescript-estree": "8.26.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.0.tgz", - "integrity": "sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.26.0", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@typescript-eslint/types": { "version": "8.26.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.26.1.tgz", @@ -2368,9 +2011,9 @@ "license": "MIT" }, "node_modules/bl": { - "version": "6.0.20", - "resolved": "https://registry.npmjs.org/bl/-/bl-6.0.20.tgz", - "integrity": "sha512-JMP0loH6ApbpT4Aa9oU5NqAkdDvcyc8koeuK8i5mYoBCVj3XCXG0uweGNN2m6DqaCO2yRHdm+MjCeTsR5VsmcA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.0.tgz", + "integrity": "sha512-ClDyJGQkc8ZtzdAAbAwBmhMSpwN/sC9HA8jxdYm6nVUbCfZbe2mgza4qh7AuEYyEPB/c4Kznf9s66bnsKMQDjw==", "license": "MIT", "dependencies": { "@types/readable-stream": "^4.0.0", @@ -2511,9 +2154,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001703", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001703.tgz", - "integrity": "sha512-kRlAGTRWgPsOj7oARC9m1okJEXdL/8fekFVcxA8Hl7GH4r/sN4OJn/i6Flde373T50KS7Y37oFbMwlE8+F42kQ==", + "version": "1.0.30001705", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001705.tgz", + "integrity": "sha512-S0uyMMiYvA7CxNgomYBwwwPUnWzFD83f3B1ce5jHUfHTH//QL6hHsreI8RVC5606R4ssqravelYO5TU6t8sEyg==", "dev": true, "funding": [ { @@ -2846,9 +2489,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.114", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.114.tgz", - "integrity": "sha512-DFptFef3iktoKlFQK/afbo274/XNWD00Am0xa7M8FZUepHlHT8PEuiNBoRfFHbH1okqN58AlhbJ4QTkcnXorjA==", + "version": "1.5.119", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.119.tgz", + "integrity": "sha512-Ku4NMzUjz3e3Vweh7PhApPrZSS4fyiCIbcIrG9eKrriYVLmbMepETR/v6SU7xPm98QTqMSYiCwfO89QNjXLkbQ==", "dev": true, "license": "ISC" }, @@ -5991,98 +5634,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.26.0.tgz", - "integrity": "sha512-PtVz9nAnuNJuAVeUFvwztjuUgSnJInODAUx47VDwWPXzd5vismPOtPtt83tzNXyOjVQbPRp786D6WFW/M2koIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.26.0", - "@typescript-eslint/parser": "8.26.0", - "@typescript-eslint/utils": "8.26.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/scope-manager": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.26.0.tgz", - "integrity": "sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.26.0", - "@typescript-eslint/visitor-keys": "8.26.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/types": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.26.0.tgz", - "integrity": "sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.0.tgz", - "integrity": "sha512-tiJ1Hvy/V/oMVRTbEOIeemA2XoylimlDQ03CgPPNaHYZbpsc78Hmngnt+WXZfJX1pjQ711V7g0H7cSJThGYfPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.26.0", - "@typescript-eslint/visitor-keys": "8.26.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/utils": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.26.0.tgz", - "integrity": "sha512-2L2tU3FVwhvU14LndnQCA2frYC8JnPDVKyQtWFPf8IYFMt/ykEN1bPolNhNbCVgOmdzTlWdusCTKA/9nKrf8Ig==", + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.26.1.tgz", + "integrity": "sha512-t/oIs9mYyrwZGRpDv3g+3K6nZ5uhKEMt2oNmAPwaY4/ye0+EH4nXIPYNtkYFS6QHm+1DFg34DbglYBz5P9Xysg==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.26.0", - "@typescript-eslint/types": "8.26.0", - "@typescript-eslint/typescript-estree": "8.26.0" + "@typescript-eslint/eslint-plugin": "8.26.1", + "@typescript-eslint/parser": "8.26.1", + "@typescript-eslint/utils": "8.26.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6096,50 +5656,6 @@ "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.0.tgz", - "integrity": "sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.26.0", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/typescript-eslint/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/typescript-eslint/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/undici-types": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", diff --git a/package.json b/package.json index a1cf032..7dbe696 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matterbridge-shelly", - "version": "2.0.4", + "version": "2.0.5-dev.1", "description": "Matterbridge shelly plugin", "author": "https://github.com/Luligu", "license": "Apache-2.0", @@ -127,6 +127,6 @@ "prettier": "3.5.3", "ts-jest": "29.2.6", "typescript": "5.8.2", - "typescript-eslint": "8.26.0" + "typescript-eslint": "8.26.1" } } diff --git a/src/index.test.ts b/src/index.test.ts index a0ebf8f..8293de3 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -36,7 +36,7 @@ describe('initializePlugin', () => { matterbridgeDirectory: './jest/matterbridge', matterbridgePluginDirectory: './jest/plugins', systemInformation: { ipv4Address: undefined, osRelease: 'xx.xx.xx.xx.xx.xx', nodeVersion: '22.1.10' }, - matterbridgeVersion: '2.2.4', + matterbridgeVersion: '2.2.5', edge: false, log: mockLog, getDevices: jest.fn(() => { diff --git a/src/platform.test.ts b/src/platform.test.ts index 89f42cb..062f569 100644 --- a/src/platform.test.ts +++ b/src/platform.test.ts @@ -98,7 +98,7 @@ describe('ShellyPlatform', () => { matterbridgeDirectory: './jest/matterbridge', matterbridgePluginDirectory: './jest/plugins', systemInformation: { ipv4Address: undefined, osRelease: 'xx.xx.xx.xx.xx.xx', nodeVersion: '22.1.10' }, - matterbridgeVersion: '2.2.4', + matterbridgeVersion: '2.2.5', edge: false, log: mockLog, getDevices: jest.fn(() => { @@ -220,7 +220,7 @@ describe('ShellyPlatform', () => { it('should throw because of version', () => { mockMatterbridge.matterbridgeVersion = '1.5.4'; expect(() => new ShellyPlatform(mockMatterbridge, mockLog, mockConfig as any)).toThrow(); - mockMatterbridge.matterbridgeVersion = '2.2.4'; + mockMatterbridge.matterbridgeVersion = '2.2.5'; }); it('should call onStart with reason and start mDNS', async () => { diff --git a/src/platform.ts b/src/platform.ts index 6050a23..38ef775 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -164,9 +164,9 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { super(matterbridge, log, config as unknown as PlatformConfig); // Verify that Matterbridge is the correct version - if (this.verifyMatterbridgeVersion === undefined || typeof this.verifyMatterbridgeVersion !== 'function' || !this.verifyMatterbridgeVersion('2.2.4')) { + if (this.verifyMatterbridgeVersion === undefined || typeof this.verifyMatterbridgeVersion !== 'function' || !this.verifyMatterbridgeVersion('2.2.5')) { throw new Error( - `This plugin requires Matterbridge version >= "2.2.4". Please update Matterbridge from ${this.matterbridge.matterbridgeVersion} to the latest version in the frontend."`, + `This plugin requires Matterbridge version >= "2.2.5". Please update Matterbridge from ${this.matterbridge.matterbridgeVersion} to the latest version in the frontend."`, ); } @@ -190,6 +190,8 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { delete config.inputEventList; } if (config.exposePowerMeter !== undefined) delete config.exposePowerMeter; + if (config.enableConfigDiscover !== undefined) delete config.enableConfigDiscover; + if (config.deviceIp !== undefined) delete config.deviceIp; } // First config setup @@ -213,7 +215,8 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { delete properties.inputLatchingList; delete properties.inputMomentaryList; delete properties.inputLatchingList; - delete properties.whiteList; + // delete properties.whiteList; + delete properties.addDevice; delete properties.entityBlackList; delete properties.deviceEntityBlackList; delete properties.nocacheList; @@ -312,7 +315,9 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { device.wsClient?.start(); } device.log.warn(`Shelly device ${hk}${discoveredDevice.id}${wr} host ${zb}${discoveredDevice.host}${wr} updated`); - } else this.log.warn(`Please restart matterbridge for the change to take effect.`); + } else { + await this.addDevice(discoveredDevice.id, discoveredDevice.host); + } } else { this.log.info(`Shelly device ${hk}${discoveredDevice.id}${nf} host ${zb}${discoveredDevice.host}${nf} already discovered`); } @@ -343,8 +348,9 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { const shelly = this.matterbridge.plugins.get('matterbridge-shelly'); if (shelly) this.matterbridge.plugins.saveConfigFromJson(shelly, this.config); } + /* // On the first run or after if expertMode is false, add the trv gateway devices to the nocacheList - if ((this.firstRun === true || config.expertMode === false) && discoveredDevice.id.includes('xxxxxxxshellyblugwg3')) { + if ((this.firstRun === true || config.expertMode === false) && discoveredDevice.id.includes('shellyblugwg3')) { if (!config.nocacheList) config.nocacheList = []; if (!config.nocacheList.includes(discoveredDevice.id)) { config.nocacheList.push(discoveredDevice.id); @@ -353,6 +359,7 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { const shelly = this.matterbridge.plugins.get('matterbridge-shelly'); if (shelly) this.matterbridge.plugins.saveConfigFromJson(shelly, this.config); } + */ }); // handle Shelly add event @@ -1485,12 +1492,16 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { this.log.debug( `Loading from storage Shelly device ${hk}${storedDevice.id}${db} host ${zb}${storedDevice.host}${db} port ${CYAN}${storedDevice.port}${db} gen ${CYAN}${storedDevice.gen}${db}`, ); - // this.shelly.emit('discovered', storedDevice); - // add the device to the discoveredDevices map + // Add the device to the discoveredDevices map this.discoveredDevices.set(storedDevice.id, storedDevice); - await this.addDevice(storedDevice.id, storedDevice.host); + const device = await this.addDevice(storedDevice.id, storedDevice.host); + // Update the device generation in the storage + if (device) { + storedDevice.gen = device.gen; + } } this.log.info(`Loaded from storage ${this.storedDevices.size} Shelly devices`); + await this.saveStoredDevices(); } // Add all configured devices @@ -1499,7 +1510,7 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { // eslint-disable-next-line prefer-const for (let [id, host] of Object.entries(this.config.deviceIp as ConfigDeviceIp)) { id = ShellyDevice.normalizeId(id).id; - const configDevice: DiscoveredDevice = { id, host, port: 0, gen: 0 }; + const configDevice: DiscoveredDevice = { id, host, port: 80, gen: 0 }; if (configDevice.id === undefined || configDevice.host === undefined || !isValidIpv4Address(configDevice.host)) { this.log.error(`Config Shelly device id ${hk}${configDevice.id}${er} host ${zb}${configDevice.host}${er} is not valid. Please check the plugin config and restart.`); continue; @@ -1816,6 +1827,40 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { this.bluBridgedDevices.forEach((bluDevice) => (bluDevice.log.logLevel = logLevel)); } + override async onAction(action: string, value?: string) { + if (action === 'addDevice' && value && isValidIpv4Address(value)) { + this.log.info(`Adding device on IP address ${zb}${value}${nf}`); + const device = await ShellyDevice.create(this.shelly, this.log, value); + if (device) { + this.discoveredDevices.set(device.id, { id: device.id, host: device.host, port: 80, gen: device.gen }); + this.storedDevices.set(device.id, { id: device.id, host: device.host, port: 80, gen: device.gen }); + await this.saveStoredDevices(); + await this.addDevice(device.id, device.host); + device.destroy(); + } else { + this.log.error(`Failed to add device on IP address ${zb}${value}${er}`); + } + } + if (action === 'removeDevice' && value && isValidString(value)) { + if (this.shelly.hasDevice(value)) return; + this.log.info(`Removing device with id ${hk}${value}${nf}`); + this.storedDevices.delete(value); + await this.saveStoredDevices(); + const fileName = path.join(this.matterbridge.matterbridgePluginDirectory, 'matterbridge-shelly', `${value}.json`); + try { + this.log.debug(`Deleting cache file: ${fileName}`); + fs.unlinkSync(fileName); + this.log.debug(`Deleted cache file: ${fileName}`); + } catch (error) { + this.log.debug(`****Failed to delete cache for device ${value} file ${fileName} error: ${error}`); + } + } + if (action === 'scanNetwork') { + this.log.info(`Scanning the network for Shelly devices`); + this.shelly.mdnsScanner.sendQuery(); + } + } + private addTagList(component: ShellyComponent): Semtag[] | undefined { if (this.matterbridge.edge) return undefined; // Add the tagList to the descriptor cluster @@ -1876,7 +1921,7 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { } // Called from onStart to add a device from storedDevice or configDevice and from discovered shelly event - private async addDevice(deviceId: string, host: string) { + private async addDevice(deviceId: string, host: string): Promise { if (this.shelly.hasDevice(deviceId) || this.shelly.hasDeviceHost(host)) { this.log.info(`Shelly device ${hk}${deviceId}${nf} host ${zb}${host}${nf} already added`); return; @@ -1930,5 +1975,6 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { log.logName = device.name ?? device.id; await this.shelly.addDevice(device); + return device; } } From 439a12f98d42a255e2c356a5e21bf9634dbc402c Mon Sep 17 00:00:00 2001 From: Luligu Date: Wed, 19 Mar 2025 08:43:24 +0100 Subject: [PATCH 04/11] Update mdnsScanner to version 1.2.3 and enhance debug logging for mDNS queries and responses --- src/mdnsScanner.ts | 62 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/src/mdnsScanner.ts b/src/mdnsScanner.ts index 59bcd9d..0889be7 100644 --- a/src/mdnsScanner.ts +++ b/src/mdnsScanner.ts @@ -4,7 +4,7 @@ * @file src\mdnsScanner.ts * @author Luca Liguori * @date 2024-05-01 - * @version 1.2.2 + * @version 1.2.3 * * Copyright 2024, 2025, 2026 Luca Liguori. * @@ -22,7 +22,7 @@ */ import { AnsiLogger, BLUE, CYAN, LogLevel, TimestampFormat, db, debugStringify, er, hk, idn, ign, nf, rs, zb } from 'matterbridge/logger'; -import mdns, { ResponsePacket } from 'multicast-dns'; +import mdns, { QueryPacket, ResponsePacket } from 'multicast-dns'; import EventEmitter from 'node:events'; import { RemoteInfo, SocketType } from 'node:dgram'; import { promises as fs } from 'node:fs'; @@ -98,6 +98,7 @@ export class MdnsScanner extends EventEmitter { { name: '_shelly._tcp.local', type: 'PTR' }, { name: '_services._dns-sd._udp.local', type: 'PTR' }, ]); + this.scanner?.query([{ name: '_shelly._tcp.local', type: 'PTR', class: 'IN' }]); this.log.debug('Sent mDNS query for shelly devices.'); } @@ -137,7 +138,12 @@ export class MdnsScanner extends EventEmitter { let port = 80; // shellymotionsensor, shellymotion2 send A record before SRV let gen = 1; this.devices.set(rinfo.address, rinfo.address); - if (debug) this.log.debug(`Mdns response from ${ign} ${rinfo.address} family ${rinfo.family} port ${rinfo.port} ${db}`); + if (debug) this.log.debug(`Mdns response from ${ign} ${rinfo.address} family ${rinfo.family} port ${rinfo.port} ${rs}${db} id ${response.id} flags ${response.flags}`); + if (debug) this.log.debug(`--- response.questions[${response.questions.length}] ---`); + for (const q of response.questions) { + if (debug) this.log.debug(`[${idn}${q.type}${rs}${db}] Name: ${CYAN}${q.name}${db} class: ${CYAN}${q.class}${db}`); + } + if (debug) this.log.debug(`--- response.answers[${response.answers.length}] ---`); for (const a of response.answers) { if (a.type === 'SRV' && (a.name.startsWith('shelly') || a.name.startsWith('Shelly'))) { @@ -231,6 +237,19 @@ export class MdnsScanner extends EventEmitter { } } } + if (debug) this.log.debug(`--- response.authorities[${response.authorities.length}] ---`); + if (debug) this.log.debug(`--- end ---\n`); + }); + + this.scanner.on('query', (query: QueryPacket, rinfo: RemoteInfo) => { + if (debug) this.log.debug(`Mdns query from ${idn} ${rinfo.address} family ${rinfo.family} port ${rinfo.port} ${rs}${db} id ${query.id} flags ${query.flags}`); + if (debug) this.log.debug(`--- query.questions[${query.questions.length}] ---`); + for (const q of query.questions) { + if (debug) this.log.debug(`[${ign}${q.type}${rs}${db}] Name: ${CYAN}${q.name}${db} class: ${CYAN}${q.class}${db}`); + } + if (debug) this.log.debug(`--- query.answers[${query.answers.length}] ---`); + if (debug) this.log.debug(`--- query.additionals[${query.additionals.length}] ---`); + if (debug) this.log.debug(`--- query.authorities[${query.authorities.length}] ---`); if (debug) this.log.debug(`--- end ---\n`); }); @@ -398,4 +417,41 @@ if (process.argv.includes('testMdnsScanner')) { mdnsScannerIpv6.stop(); }); } +[12:58:57.941] [ShellyMdnsScanner] Mdns query from 192.168.1.40 family IPv4 port 5353 +[12:58:57.941] [ShellyMdnsScanner] --- query.questions[1] --- +[12:58:57.941] [ShellyMdnsScanner] [PTR] Name: _shelly._tcp.local class: UNKNOWN_32769 +[12:58:57.941] [ShellyMdnsScanner] --- end --- + +[12:58:58.202] [ShellyMdnsScanner] Mdns query from 192.168.1.40 family IPv4 port 5353 +[12:58:58.203] [ShellyMdnsScanner] --- query.questions[1] --- +[12:58:58.203] [ShellyMdnsScanner] [PTR] Name: _shelly._tcp.local class: IN +[12:58:58.203] [ShellyMdnsScanner] --- end --- + +[12:58:59.228] [ShellyMdnsScanner] Mdns query from 192.168.1.40 family IPv4 port 5353 +[12:58:59.228] [ShellyMdnsScanner] --- query.questions[3] --- +[12:58:59.228] [ShellyMdnsScanner] [PTR] Name: _shelly._tcp.local class: IN +[12:58:59.228] [ShellyMdnsScanner] [AAAA] Name: ShellyPlus1PM-CC7B5C0AB624.local class: UNKNOWN_32769 +[12:58:59.229] [ShellyMdnsScanner] [AAAA] Name: ShellyPlus2PM-08F9E0FD173C.local class: UNKNOWN_32769 +[12:58:59.229] [ShellyMdnsScanner] --- end --- + +[12:58:59.231] [ShellyMdnsScanner] Mdns query from 192.168.1.40 family IPv4 port 5353 +[12:58:59.231] [ShellyMdnsScanner] --- query.questions[12] --- +[12:58:59.231] [ShellyMdnsScanner] [AAAA] Name: ShellyPlusRGBWPM-ECC9FF4CEAF0.local class: UNKNOWN_32769 +[12:58:59.231] [ShellyMdnsScanner] [AAAA] Name: ShellyPlus010V-80646FE1FAC4.local class: UNKNOWN_32769 +[12:58:59.231] [ShellyMdnsScanner] [AAAA] Name: ShellyPlus2PM-1C692044F140.local class: UNKNOWN_32769 +[12:58:59.231] [ShellyMdnsScanner] [AAAA] Name: ShellyPlusI4-D48AFC41B6F4.local class: UNKNOWN_32769 +[12:58:59.231] [ShellyMdnsScanner] [AAAA] Name: ShellyBluGw-B0B21CFAAD18.local class: UNKNOWN_32769 +[12:58:59.231] [ShellyMdnsScanner] [AAAA] Name: ShellyPlusPlugS-E86BEAEAA000.local class: UNKNOWN_32769 +[12:58:59.231] [ShellyMdnsScanner] [AAAA] Name: ShellyPlus2PM-5443B23D81F8.local class: UNKNOWN_32769 +[12:58:59.231] [ShellyMdnsScanner] [AAAA] Name: ShellyPlus1PM-441793D69718.local class: UNKNOWN_32769 +[12:58:59.231] [ShellyMdnsScanner] [AAAA] Name: ShellyPlus2PM-30C922810DA0.local class: UNKNOWN_32769 +[12:58:59.231] [ShellyMdnsScanner] [AAAA] Name: Shelly2PMG3-34CDB0770C4C.local class: UNKNOWN_32769 +[12:58:59.231] [ShellyMdnsScanner] [AAAA] Name: Shelly1G3-DCDA0CDEEC20.local class: UNKNOWN_32769 +[12:58:59.232] [ShellyMdnsScanner] [AAAA] Name: ShellyPlus2PM-30C92286CB68.local class: UNKNOWN_32769 +[12:58:59.232] [ShellyMdnsScanner] --- end --- + +[12:58:59.739] [ShellyMdnsScanner] Mdns query from 192.168.1.40 family IPv4 port 5353 +[12:58:59.739] [ShellyMdnsScanner] --- query.questions[2] --- +[12:58:59.739] [ShellyMdnsScanner] [AAAA] Name: ShellyPlus1-E465B8F3028C.local class: UNKNOWN_32769 +[12:58:59.740] [ShellyMdnsScanner] [AAAA] Name: ShellyBluGw-B0B21CFC5080.local class: UNKNOWN_32769 */ From 5e495ddd049b6bd0d60d5a113de80f971ebd4a40 Mon Sep 17 00:00:00 2001 From: Luligu Date: Wed, 19 Mar 2025 08:44:20 +0100 Subject: [PATCH 05/11] Bump ShellyDevice version to 3.1.2 and update bthome_event type to ShellyData --- src/shellyDevice.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/shellyDevice.ts b/src/shellyDevice.ts index 4d1d78b..81eb2de 100644 --- a/src/shellyDevice.ts +++ b/src/shellyDevice.ts @@ -4,7 +4,7 @@ * @file src\shellyDevice.ts * @author Luca Liguori * @date 2024-05-01 - * @version 3.1.1 + * @version 3.1.2 * * Copyright 2024, 2025, 2026 Luca Liguori. * @@ -51,7 +51,7 @@ interface ShellyDeviceEvent { offline: []; awake: []; update: [id: string, key: string, value: ShellyDataType]; - bthome_event: [event: string]; + bthome_event: [event: ShellyData]; bthomedevice_update: [addr: string, rssi: number, packet_id: number, last_updated_ts: number]; bthomesensor_update: [addr: string, sensorName: string, sensorIndex: number, value: ShellyDataType]; bthomedevice_event: [addr: string, event: string]; @@ -863,7 +863,7 @@ export class ShellyDevice extends EventEmitter { for (const event of events) { if (isValidObject(event) && isValidString(event.event) && isValidNumber(event.ts) && isValidString(event.component) && event.component === 'bthome') { this.log.debug(`Device ${hk}${this.id}${db} has event ${YELLOW}${event.event}${db} at ${CYAN}${this.getLocalTimeFromLastUpdated(event.ts as number)}${db}`); - this.emit('bthome_event', event.event); + this.emit('bthome_event', event); } else if (isValidObject(event) && isValidString(event.event) && isValidNumber(event.ts) && isValidString(event.component) && event.component.startsWith('bthomedevice:')) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const device = Array.from(this.bthomeDevices).find(([_addr, _device]) => _device.key === event.component)?.[1]; From 6d58e2d467d42ae8c244eed46940675fdb1e68f8 Mon Sep 17 00:00:00 2001 From: Luligu Date: Wed, 19 Mar 2025 08:45:23 +0100 Subject: [PATCH 06/11] Bump Shelly version to 2.2.3 and add getters/setters for network interface and IP addresses --- src/shelly.ts | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/src/shelly.ts b/src/shelly.ts index 647e163..513cd0f 100644 --- a/src/shelly.ts +++ b/src/shelly.ts @@ -4,7 +4,7 @@ * @file src\shelly.ts * @author Luca Liguori * @date 2024-05-01 - * @version 2.2.2 + * @version 2.2.3 * * Copyright 2024, 2025, 2026 Luca Liguori. * @@ -50,6 +50,9 @@ export class Shelly extends EventEmitter { public username: string | undefined; public password: string | undefined; private _dataPath = ''; + private _interfaceName: string | undefined; + private _ipv4Address: string | undefined; + private _ipv6Address: string | undefined; /** * Creates a new instance of the Shelly class. @@ -226,6 +229,54 @@ export class Shelly extends EventEmitter { return this._dataPath; } + /** + * Gets the network interface name. + * @returns {string | undefined} The network interface name. + */ + get interfaceName(): string | undefined { + return this._interfaceName; + } + + /** + * Sets the network interface name. + * @param {string | undefined} value - The network interface name. + */ + set interfaceName(value: string | undefined) { + this._interfaceName = value; + } + + /** + * Gets the IPv4 address. + * @returns {string | undefined} The IPv4 address. + */ + get ipv4Address(): string | undefined { + return this._ipv4Address; + } + + /** + * Sets the IPv4 address. + * @param {string | undefined} value - The IPv4 address. + */ + set ipv4Address(value: string | undefined) { + this._ipv4Address = value; + } + + /** + * Gets the IPv6 address. + * @returns {string | undefined} The IPv6 address. + */ + get ipv6Address(): string | undefined { + return this._ipv6Address; + } + + /** + * Sets the IPv6 address. + * @param {string | undefined} value - The IPv6 address. + */ + set ipv6Address(value: string | undefined) { + this._ipv6Address = value; + } + /** * Checks if a device with the specified ID exists. * From 135e562b401f8e2326867279847ce0b862d1ac42 Mon Sep 17 00:00:00 2001 From: Luligu Date: Wed, 19 Mar 2025 08:56:53 +0100 Subject: [PATCH 07/11] Release 2.0.5 --- .prettierignore | 4 +- CHANGELOG.md | 15 +- matterbridge-shelly.schema.json | 32 +-- package-lock.json | 16 +- package.json | 2 +- src/platform.ts | 356 +++++++++++++++++--------------- src/shellyDevice.ts | 2 +- 7 files changed, 236 insertions(+), 191 deletions(-) diff --git a/.prettierignore b/.prettierignore index b05a5a5..b7ddddd 100644 --- a/.prettierignore +++ b/.prettierignore @@ -11,4 +11,6 @@ frontend docker # Ignore all HTML files: -**/*.html \ No newline at end of file +**/*.html + +TODO.md \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f6da1f..9a6b914 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ Removed options: - exposePowerMeter: electrical sensors are enabled by default on existing setups. You can disable them globally or on a per-device basis adding "PowerMeter" to entityBlackList or deviceEntityBlackList, see [COMPONENTS.md documentation.](https://github.com/Luligu/matterbridge-shelly/blob/main/COMPONENTS.md). On new setups the "PowerMeter" components are already globally disabled by default. -- enableConfigDiscover and deviceIp: replaced by a manually add a device with IP address in expert mode. +- enableConfigDiscover and deviceIp: replaced by a config action in expert mode. New setups: @@ -34,13 +34,18 @@ Expert mode: The expertMode option has been added to show an advanced or simplified config. -## [2.0.5] - 2025-03-17 +## [2.0.5] - 2025-03-19 ### Added -- [config] Added action: manually add a device with IP address. This allows to add the devices that are not discovered on the network with the mdns. -- [config] Added action: remove a device from the storage with its device id. This allows to remove from the storage a single device when it is no more on the network. -- [config] Added action: scan network. This will send a mdns request on the network. +- [config] Added action: manually add a device with IP address. It allows to add the devices that are not discovered on the network with the mdns. +- [config] Added action: remove a device from the storage with its device id. It allows to remove from the storage a single device when it has been removed from the network. +- [config] Added action: scan network. It will send a mdns request on the network. +- [mdns] Added a fully automatic IP change detection when the device is discovered on a different IP (no need to restart). +- [shelly] Verified AZ Plug Gen3. +- [shelly] Verified PlugSG3 Matter Gen3. +- [BLU]: Verified new BLU firmware 1.0.22 on all BLU devices. +- [TRV]: Verified new BLU TRV firmware 1.2.1. ### Changed diff --git a/matterbridge-shelly.schema.json b/matterbridge-shelly.schema.json index 197cf37..946e60f 100644 --- a/matterbridge-shelly.schema.json +++ b/matterbridge-shelly.schema.json @@ -16,11 +16,11 @@ "ui:widget": "hidden" }, "username": { - "description": "Username for password protected shelly devices (used only for gen 1 devices)", + "description": "Username for password protected shelly devices (used only for gen 1 devices).", "type": "string" }, "password": { - "description": "Password for password protected shelly devices (must be unique for all the devices)", + "description": "Password for password protected shelly devices (must be unique for all the devices).", "type": "string", "ui:widget": "password" }, @@ -69,8 +69,17 @@ "uniqueItems": true, "selectFrom": "serial" }, + "nocacheList": { + "description": "The devices in the list will not be loaded from the cache. Use the device id (e.g. shellyplus2pm-5443B23D81F8). If the list is empty, all the devices will be loaded from the cache.", + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true, + "selectFrom": "serial" + }, "whiteList": { - "description": "Only the devices in the list will be exposed. Use the device id (e.g. shellyplus2pm-5443B23D81F8) or BLU addr (i.e. 7c:c6:b6:65:2d:87).", + "description": "Only the devices in the list will be exposed. Use the device id (e.g. shellyplus2pm-5443B23D81F8) or BLU addr (i.e. 7c:c6:b6:65:2d:87). If the list is empty, all the devices will be exposed.", "type": "array", "items": { "type": "string" @@ -79,7 +88,7 @@ "selectFrom": "serial" }, "blackList": { - "description": "The devices in the list will not be exposed. Use the device id (e.g. shellyplus2pm-5443B23D81F8) or BLU addr (i.e. 7c:c6:b6:65:2d:87).", + "description": "The devices in the list will not be exposed. Use the device id (e.g. shellyplus2pm-5443B23D81F8) or BLU addr (i.e. 7c:c6:b6:65:2d:87). If the list is empty, no devices will be excluded.", "type": "array", "items": { "type": "string" @@ -111,15 +120,6 @@ "selectDeviceEntityFrom": "name" } }, - "nocacheList": { - "description": "The devices in the list will not be loaded from the cache. Use the device id (e.g. shellyplus2pm-5443B23D81F8).", - "type": "array", - "items": { - "type": "string" - }, - "uniqueItems": true, - "selectFrom": "serial" - }, "enableMdnsDiscover": { "description": "Enable the mdns discovery for shelly devices. Once all the devices are discovered and stored, it is possible to disable this setting to reduce the network traffic.", "type": "boolean", @@ -159,6 +159,8 @@ "description": "Manually add a device that has not been discovered with mdns:", "type": "boolean", "buttonField": "ADD", + "buttonClose": false, + "buttonSave": false, "textPlaceholder": "Enter the device IP address", "default": false }, @@ -166,6 +168,8 @@ "description": "Remove a device and its cache file from the storage:", "type": "boolean", "buttonField": "REMOVE", + "buttonClose": false, + "buttonSave": false, "textPlaceholder": "Enter the device id", "default": false }, @@ -173,6 +177,8 @@ "description": "Scan the network with mdns for new devices:", "type": "boolean", "buttonText": "SCAN", + "buttonClose": false, + "buttonSave": false, "default": false }, "debug": { diff --git a/package-lock.json b/package-lock.json index 9012579..a89c171 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "matterbridge-shelly", - "version": "2.0.5-dev.1", + "version": "2.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "matterbridge-shelly", - "version": "2.0.5-dev.1", + "version": "2.0.5", "license": "Apache-2.0", "dependencies": { "coap": "1.4.1", @@ -2154,9 +2154,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001705", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001705.tgz", - "integrity": "sha512-S0uyMMiYvA7CxNgomYBwwwPUnWzFD83f3B1ce5jHUfHTH//QL6hHsreI8RVC5606R4ssqravelYO5TU6t8sEyg==", + "version": "1.0.30001706", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001706.tgz", + "integrity": "sha512-3ZczoTApMAZwPKYWmwVbQMFpXBDds3/0VciVoUwPUbldlYyVLmRVuRs/PcUZtHpbLRpzzDvrvnFuREsGt6lUug==", "dev": true, "funding": [ { @@ -2489,9 +2489,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.119", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.119.tgz", - "integrity": "sha512-Ku4NMzUjz3e3Vweh7PhApPrZSS4fyiCIbcIrG9eKrriYVLmbMepETR/v6SU7xPm98QTqMSYiCwfO89QNjXLkbQ==", + "version": "1.5.120", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.120.tgz", + "integrity": "sha512-oTUp3gfX1gZI+xfD2djr2rzQdHCwHzPQrrK0CD7WpTdF0nPdQ/INcRVjWgLdCT4a9W3jFObR9DAfsuyFQnI8CQ==", "dev": true, "license": "ISC" }, diff --git a/package.json b/package.json index 7dbe696..60e2e80 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matterbridge-shelly", - "version": "2.0.5-dev.1", + "version": "2.0.5", "description": "Matterbridge shelly plugin", "author": "https://github.com/Luligu", "license": "Apache-2.0", diff --git a/src/platform.ts b/src/platform.ts index 38ef775..f095553 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -217,6 +217,8 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { delete properties.inputLatchingList; // delete properties.whiteList; delete properties.addDevice; + delete properties.removeDevice; + delete properties.scanNetwork; delete properties.entityBlackList; delete properties.deviceEntityBlackList; delete properties.nocacheList; @@ -282,6 +284,9 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { this.shelly = new Shelly(log, this.username, this.password); this.shelly.setLogLevel(log.logLevel, this.config.debugMdns as boolean, this.config.debugCoap as boolean, this.config.debugWs as boolean); this.shelly.dataPath = path.join(matterbridge.matterbridgePluginDirectory, 'matterbridge-shelly'); + this.shelly.interfaceName = matterbridge.mdnsInterface; + this.shelly.ipv4Address = matterbridge.ipv4address; + this.shelly.ipv6Address = matterbridge.ipv6address; // handle Shelly discovered event (called from mDNS scanner) this.shelly.on('discovered', async (discoveredDevice: DiscoveredDevice) => { @@ -397,137 +402,16 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { if (device.bthomeDevices.size && device.bthomeSensors.size) { this.log.info(`Shelly device ${hk}${device.id}${nf} host ${zb}${device.host}${nf} is a ble gateway. Adding paired BLU devices...`); this.gatewayDevices.set(device.id, device.id); + // Register the BLU devices - for (const [key, bthomeDevice] of device.bthomeDevices) { + for (const [, bthomeDevice] of device.bthomeDevices) { // Set the device in the selectDevice map for the frontend device selection this.setSelectDevice(bthomeDevice.addr, bthomeDevice.name, 'http://' + device.host, 'ble'); if (!this.validateDevice([bthomeDevice.addr, bthomeDevice.name])) continue; - this.log.info( - `- ${idn}${bthomeDevice.name}${rs}${nf} address ${CYAN}${bthomeDevice.addr}${nf} id ${CYAN}${bthomeDevice.id}${nf} ` + - `model ${CYAN}${bthomeDevice.model}${nf} (${CYAN}${bthomeDevice.type}${nf})`, - ); - let definition: AtLeastOne | undefined; - if (bthomeDevice.model === 'Shelly BLU DoorWindow') definition = [bridgedNode, powerSource]; - else if (bthomeDevice.model === 'Shelly BLU Motion') definition = [bridgedNode, powerSource]; - else if (bthomeDevice.model === 'Shelly BLU Button1') definition = [genericSwitch, bridgedNode, powerSource]; - else if (bthomeDevice.model === 'Shelly BLU HT') definition = [bridgedNode, powerSource]; - else if (bthomeDevice.model === 'Shelly BLU RC Button 4') definition = [bridgedNode, powerSource]; - else if (bthomeDevice.model === 'Shelly BLU Wall Switch 4') definition = [bridgedNode, powerSource]; - else if (bthomeDevice.model === 'Shelly BLU Trv') definition = [thermostatDevice, bridgedNode, powerSource]; - else this.log.error(`Shelly device ${hk}${device.id}${er} host ${zb}${device.host}${er} has an unknown BLU device model ${CYAN}${bthomeDevice.model}${nf}`); - // Check if the BLU device is already registered - this.bluBridgedDevices.forEach((blu) => { - if (blu.serialNumber === bthomeDevice.addr + (this.postfix ? '-' + this.postfix : '')) { - this.log.warn(`BLU device ${idn}${bthomeDevice.name}${rs}${wr} address ${CYAN}${bthomeDevice.addr}${wr} already registered with another ble gateway.`); - definition = undefined; - } - }); - if (definition) { - const mbDevice = new MatterbridgeEndpoint(definition, { uniqueStorageKey: bthomeDevice.name }, config.debug as boolean); - mbDevice.configUrl = `http://${device.host}`; - mbDevice.createDefaultBridgedDeviceBasicInformationClusterServer( - bthomeDevice.name, - bthomeDevice.addr + (this.postfix ? '-' + this.postfix : ''), - 0xfff1, - 'Shelly', - bthomeDevice.model, - ); - if (bthomeDevice.model === 'Shelly BLU DoorWindow') { - mbDevice.createDefaultPowerSourceReplaceableBatteryClusterServer(); - mbDevice.addFixedLabel('composed', 'Sensor'); - mbDevice.addChildDeviceTypeWithClusterServer('Contact', [contactSensor], [], undefined, config.debug as boolean); - if (this.validateEntity(bthomeDevice.addr, 'Illuminance')) - mbDevice.addChildDeviceTypeWithClusterServer('Illuminance', [lightSensor], [], undefined, config.debug as boolean); - } else if (bthomeDevice.model === 'Shelly BLU Motion') { - mbDevice.createDefaultPowerSourceReplaceableBatteryClusterServer(); - mbDevice.addFixedLabel('composed', 'Sensor'); - mbDevice.addChildDeviceTypeWithClusterServer('Motion', [occupancySensor], [], undefined, config.debug as boolean); - if (this.validateEntity(bthomeDevice.addr, 'Illuminance')) - mbDevice.addChildDeviceTypeWithClusterServer('Illuminance', [lightSensor], [], undefined, config.debug as boolean); - if (this.validateEntity(bthomeDevice.addr, 'Button')) - mbDevice.addChildDeviceTypeWithClusterServer('Button', [genericSwitch], [], undefined, config.debug as boolean); - } else if (bthomeDevice.model === 'Shelly BLU Button1') { - mbDevice.createDefaultPowerSourceReplaceableBatteryClusterServer(); - mbDevice.createDefaultSwitchClusterServer(); - } else if (bthomeDevice.model === 'Shelly BLU HT') { - mbDevice.createDefaultPowerSourceReplaceableBatteryClusterServer(); - mbDevice.addFixedLabel('composed', 'Sensor'); - mbDevice.addChildDeviceTypeWithClusterServer('Temperature', [temperatureSensor], [], undefined, config.debug as boolean); - mbDevice.addChildDeviceTypeWithClusterServer('Humidity', [humiditySensor], [], undefined, config.debug as boolean); - if (this.validateEntity(bthomeDevice.addr, 'Button')) - mbDevice.addChildDeviceTypeWithClusterServer('Button', [genericSwitch], [], undefined, config.debug as boolean); - } else if (bthomeDevice.model === 'Shelly BLU RC Button 4') { - mbDevice.createDefaultPowerSourceReplaceableBatteryClusterServer(); - mbDevice.addFixedLabel('composed', 'Input'); - mbDevice.addChildDeviceTypeWithClusterServer('Button0', [genericSwitch], [Switch.Cluster.id], undefined, config.debug as boolean); - mbDevice.addChildDeviceTypeWithClusterServer('Button1', [genericSwitch], [Switch.Cluster.id], undefined, config.debug as boolean); - mbDevice.addChildDeviceTypeWithClusterServer('Button2', [genericSwitch], [Switch.Cluster.id], undefined, config.debug as boolean); - mbDevice.addChildDeviceTypeWithClusterServer('Button3', [genericSwitch], [Switch.Cluster.id], undefined, config.debug as boolean); - } else if (bthomeDevice.model === 'Shelly BLU Wall Switch 4') { - mbDevice.createDefaultPowerSourceReplaceableBatteryClusterServer(); - mbDevice.addFixedLabel('composed', 'Input'); - mbDevice.addChildDeviceTypeWithClusterServer('Button0', [genericSwitch], [Switch.Cluster.id], undefined, config.debug as boolean); - mbDevice.addChildDeviceTypeWithClusterServer('Button1', [genericSwitch], [Switch.Cluster.id], undefined, config.debug as boolean); - mbDevice.addChildDeviceTypeWithClusterServer('Button2', [genericSwitch], [Switch.Cluster.id], undefined, config.debug as boolean); - mbDevice.addChildDeviceTypeWithClusterServer('Button3', [genericSwitch], [Switch.Cluster.id], undefined, config.debug as boolean); - } else if (bthomeDevice.model === 'Shelly BLU Trv') { - mbDevice.createDefaultPowerSourceReplaceableBatteryClusterServer(100, PowerSource.BatChargeLevel.Ok, 3000, 'Type AA', 2); - mbDevice.createDefaultIdentifyClusterServer(); - mbDevice.createDefaultHeatingThermostatClusterServer(undefined, undefined, 4, 30); - mbDevice.subscribeAttribute( - Thermostat.Cluster.id, - 'systemMode', - (newValue: number, oldValue: number) => { - if ( - isValidNumber(newValue, Thermostat.SystemMode.Off, Thermostat.SystemMode.Heat) && - isValidNumber(oldValue, Thermostat.SystemMode.Off, Thermostat.SystemMode.Heat) && - newValue !== oldValue - ) { - mbDevice.log.info(`Thermostat systemMode changed from ${oldValue} to ${newValue}`); - if (oldValue === Thermostat.SystemMode.Heat && newValue === Thermostat.SystemMode.Off) { - if (device.thermostatSystemModeTimeout) clearTimeout(device.thermostatSystemModeTimeout); - device.thermostatSystemModeTimeout = setTimeout(() => { - mbDevice.setAttribute(Thermostat.Cluster.id, 'systemMode', Thermostat.SystemMode.Heat, mbDevice.log); - }, 5000); - } - } - }, - mbDevice.log, - ); - mbDevice.subscribeAttribute( - Thermostat.Cluster.id, - 'occupiedHeatingSetpoint', - (newValue: number, oldValue: number) => { - if (isValidNumber(newValue, 4 * 100, 30 * 100) && isValidNumber(oldValue, 4 * 100, 30 * 100) && newValue !== oldValue) { - mbDevice.log.info(`Thermostat occupiedHeatingSetpoint changed from ${oldValue / 100} to ${newValue / 100}`); - if (device.thermostatSetpointTimeout) clearTimeout(device.thermostatSetpointTimeout); - device.thermostatSetpointTimeout = setTimeout(() => { - mbDevice.log.info(`Setting thermostat occupiedHeatingSetpoint to ${newValue / 100}`); - // http://192.168.1.164/rpc/BluTrv.Call?id=201&method=Trv.SetTarget¶ms={id:0,target_C:19} - ShellyDevice.fetch(this.shelly, mbDevice.log, device.host, 'BluTrv.Call', { - id: bthomeDevice.blutrv_id, - method: 'Trv.SetTarget', - params: { id: 0, target_C: newValue / 100 }, - }); - }, 5000); - } - }, - mbDevice.log, - ); - } - mbDevice.addRequiredClusterServers(); - try { - await this.registerDevice(mbDevice); - this.bluBridgedDevices.set(key, mbDevice); - mbDevice.log.logName = `${bthomeDevice.name}`; - } catch (error) { - this.log.error( - `Shelly device ${hk}${device.id}${er} host ${zb}${device.host}${er} failed to register BLU device ${idn}${bthomeDevice.name}${rs}${er}: ${error instanceof Error ? error.message : error}`, - ); - } - } + await this.addBluDevice(device, bthomeDevice); } - // BLU observer device updates + + // BLU observer bthome device updates device.on('bthomedevice_update', (addr: string, rssi: number, packet_id: number, last_updated_ts: number) => { if (!isValidString(addr, 11) || !isValidNumber(rssi, -100, 0) || !isValidNumber(packet_id, 0) || !isValidNumber(last_updated_ts)) return; const blu = this.bluBridgedDevices.get(addr); @@ -541,7 +425,8 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { `${idn}BLU${rs}${db} observer device update message for BLU device ${idn}${blu.deviceName ?? addr}${rs}${db}: rssi ${YELLOW}${rssi}${db} packet_id ${YELLOW}${packet_id}${db} last_updated ${YELLOW}${device.getLocalTimeFromLastUpdated(last_updated_ts)}${db}`, ); }); - // BLU observer sensor updates + + // BLU observer bthome sensor updates device.on('bthomesensor_update', (addr: string, sensorName: string, sensorIndex: number, value: ShellyDataType) => { if (!isValidString(addr, 11) || !isValidString(sensorName, 6) || !isValidNumber(sensorIndex, 0, 3)) return; const blu = this.bluBridgedDevices.get(addr); @@ -587,22 +472,24 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { } }); - // BLU observer sensor events - device.on('bthome_event', (event: string) => { + // BLU observer bthome events + device.on('bthome_event', (event: ShellyData) => { if (!isValidString(event)) return; - if (event === 'device_discovered') { + if (event.event === 'device_discovered') { this.changedDevices.set(device.id, device.id); device.log.notice(`Shelly device ${idn}${device.name}${rs}${nt} id ${hk}${device.id}${nt} host ${zb}${device.host}${nt} discovered a new BLU device`); } - if (event === 'discovery_done') { + if (event.event === 'discovery_done') { this.changedDevices.set(device.id, device.id); device.log.notice(`Shelly device ${idn}${device.name}${rs}${nt} id ${hk}${device.id}${nt} host ${zb}${device.host}${nt} discovery done`); } - if (event === 'associations_done') { + if (event.event === 'associations_done') { this.changedDevices.set(device.id, device.id); device.log.notice(`Shelly device ${idn}${device.name}${rs}${nt} id ${hk}${device.id}${nt} host ${zb}${device.host}${nt} paired a new BLU device`); } }); + + // BLU observer bthome device events device.on('bthomedevice_event', (addr: string, event: string) => { if (!isValidString(addr, 11) || !isValidString(event, 6)) return; const blu = this.bluBridgedDevices.get(addr); @@ -625,6 +512,8 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { device.log.notice(`Shelly device ${idn}${device.name}${rs}${nt} id ${hk}${device.id}${nt} host ${zb}${device.host}${nt} finished succesfully OTA`); } }); + + // BLU observer bthome sensor events device.on('bthomesensor_event', (addr: string, sensorName: string, sensorIndex: number, event: string) => { if (!isValidString(addr, 11) || !isValidString(sensorName, 6) || !isValidNumber(sensorIndex, 0, 3) || !isValidString(event, 6)) return; const blu = this.bluBridgedDevices.get(addr); @@ -773,6 +662,20 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { // Add event handler from Shelly component.on('event', (component: string, event: string, data: ShellyData) => { this.log.debug(`Received event ${event} from component ${component}`); + // component_added is triggered by a gateway when a component is added + if (event === 'component_added') { + if (!device.sleepMode) this.changedDevices.set(device.id, device.id); + device.log.notice(`Shelly device ${idn}${device.name}${rs}${nt} id ${hk}${device.id}${nt} host ${zb}${device.host}${nt} added a component.`); + device.log.notice(`Please restart matterbridge for the change to take effect.`); + this.matterbridge.frontend.wssSendRestartRequired(); + } + // component_removed is triggered by a gateway when a component is removed + if (event === 'component_removed') { + if (!device.sleepMode) this.changedDevices.set(device.id, device.id); + device.log.notice(`Shelly device ${idn}${device.name}${rs}${nt} id ${hk}${device.id}${nt} host ${zb}${device.host}${nt} removed a component.`); + device.log.notice(`Please restart matterbridge for the change to take effect.`); + this.matterbridge.frontend.wssSendRestartRequired(); + } // scheduled_restart is for restart and for reset if (event === 'scheduled_restart') { if (!device.sleepMode) this.changedDevices.set(device.id, device.id); @@ -1504,32 +1407,6 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { await this.saveStoredDevices(); } - // Add all configured devices - if (this.config.enableConfigDiscover === true && isValidObject(this.config.deviceIp)) { - this.log.info(`Loading from config ${Object.entries(this.config.deviceIp as ConfigDeviceIp).length} Shelly devices`); - // eslint-disable-next-line prefer-const - for (let [id, host] of Object.entries(this.config.deviceIp as ConfigDeviceIp)) { - id = ShellyDevice.normalizeId(id).id; - const configDevice: DiscoveredDevice = { id, host, port: 80, gen: 0 }; - if (configDevice.id === undefined || configDevice.host === undefined || !isValidIpv4Address(configDevice.host)) { - this.log.error(`Config Shelly device id ${hk}${configDevice.id}${er} host ${zb}${configDevice.host}${er} is not valid. Please check the plugin config and restart.`); - continue; - } - if (this.discoveredDevices.has(configDevice.id)) { - this.log.info(`Config Shelly device id ${hk}${configDevice.id}${nf} host ${zb}${configDevice.host}${nf} already loaded from storage. Skipping.`); - continue; - } - this.log.debug(`Loading from config Shelly device ${hk}${configDevice.id}${db} host ${zb}${configDevice.host}${db}`); - // this.shelly.emit('discovered', configDevice); - // add the device to the discoveredDevices map - this.discoveredDevices.set(configDevice.id, configDevice); - this.storedDevices.set(configDevice.id, configDevice); - await this.saveStoredDevices(); - await this.addDevice(configDevice.id, configDevice.host); - } - this.log.info(`Loaded from config ${Object.entries(this.config.deviceIp as ConfigDeviceIp).length} Shelly devices`); - } - // start Shelly mDNS device discoverer if enabled if (this.config.enableMdnsDiscover === true) { this.shelly.mdnsScanner.start(0, 10 * 60 * 1000, this.config.interfaceName as string | undefined, 'udp4', this.config.debugMdns as boolean); @@ -1842,18 +1719,32 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { } } if (action === 'removeDevice' && value && isValidString(value)) { - if (this.shelly.hasDevice(value)) return; - this.log.info(`Removing device with id ${hk}${value}${nf}`); + if (this.shelly.hasDevice(value)) { + this.log.warn(`Removing device id ${hk}${value}${wr} while it is still in use. Please blacklist the device first and restart before removing it.`); + (this.config as unknown as ShellyPlatformConfig).blackList.push(value); + this.shelly.getDevice(value)?.destroy(); + this.shelly.removeDevice(value); + this.matterbridge.frontend.wssSendRestartRequired(); + } + this.log.info(`Removing device id ${hk}${value}${nf}`); this.storedDevices.delete(value); - await this.saveStoredDevices(); + this.changedDevices.delete(value); + this.gatewayDevices.delete(value); + if (this.nodeStorage) { + await this.nodeStorage.set('DeviceIdentifiers', Array.from(this.storedDevices.values())); + await this.nodeStorage.set('ChangedDevices', Array.from(this.changedDevices.values())); + await this.nodeStorage.set('GatewayDevices', Array.from(this.gatewayDevices.values())); + } const fileName = path.join(this.matterbridge.matterbridgePluginDirectory, 'matterbridge-shelly', `${value}.json`); try { this.log.debug(`Deleting cache file: ${fileName}`); fs.unlinkSync(fileName); this.log.debug(`Deleted cache file: ${fileName}`); + this.log.info(`Removed cache file for device id ${hk}${value}${nf}`); } catch (error) { this.log.debug(`****Failed to delete cache for device ${value} file ${fileName} error: ${error}`); } + this.log.info(`Removed device id ${hk}${value}${nf}`); } if (action === 'scanNetwork') { this.log.info(`Scanning the network for Shelly devices`); @@ -1920,7 +1811,7 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { return true; } - // Called from onStart to add a device from storedDevice or configDevice and from discovered shelly event + // Called from onStart to add a device from storedDevice, from discovered shelly event and from onAction() private async addDevice(deviceId: string, host: string): Promise { if (this.shelly.hasDevice(deviceId) || this.shelly.hasDeviceHost(host)) { this.log.info(`Shelly device ${hk}${deviceId}${nf} host ${zb}${host}${nf} already added`); @@ -1965,8 +1856,10 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { ); return; } + // Set the device in the selectDevice map for the frontend device selection this.setSelectDevice(device.id, device.name, 'http://' + host, 'wifi'); + // Destroy the device if it is not a gateway device and it is not validated if (!this.gatewayDevices.has(device.id) && !this.validateDevice([device.id, device.mac, device.name])) { device.destroy(); @@ -1977,4 +1870,143 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { await this.shelly.addDevice(device); return device; } + + private async addBluDevice( + gateway: ShellyDevice, + bthomeDevice: { + id: number; + key: string; + name: string; + addr: string; + model: string; + type: string; + blutrv_id: number; + packet_id: number; + rssi: number; + last_updated_ts: number; + }, + ): Promise { + this.log.info( + `- ${idn}${bthomeDevice.name}${rs}${nf} address ${CYAN}${bthomeDevice.addr}${nf} id ${CYAN}${bthomeDevice.id}${nf} ` + + `model ${CYAN}${bthomeDevice.model}${nf} (${CYAN}${bthomeDevice.type}${nf})`, + ); + let definition: AtLeastOne | undefined; + if (bthomeDevice.model === 'Shelly BLU DoorWindow') definition = [bridgedNode, powerSource]; + else if (bthomeDevice.model === 'Shelly BLU Motion') definition = [bridgedNode, powerSource]; + else if (bthomeDevice.model === 'Shelly BLU Button1') definition = [genericSwitch, bridgedNode, powerSource]; + else if (bthomeDevice.model === 'Shelly BLU HT') definition = [bridgedNode, powerSource]; + else if (bthomeDevice.model === 'Shelly BLU RC Button 4') definition = [bridgedNode, powerSource]; + else if (bthomeDevice.model === 'Shelly BLU Wall Switch 4') definition = [bridgedNode, powerSource]; + else if (bthomeDevice.model === 'Shelly BLU Trv') definition = [thermostatDevice, bridgedNode, powerSource]; + else this.log.error(`Shelly device ${hk}${gateway.id}${er} host ${zb}${gateway.host}${er} has an unknown BLU device model ${CYAN}${bthomeDevice.model}${nf}`); + // Check if the BLU device is already registered + this.bluBridgedDevices.forEach((blu) => { + if (blu.serialNumber === bthomeDevice.addr + (this.postfix ? '-' + this.postfix : '')) { + this.log.warn(`BLU device ${idn}${bthomeDevice.name}${rs}${wr} address ${CYAN}${bthomeDevice.addr}${wr} already registered with another ble gateway.`); + definition = undefined; + } + }); + if (definition) { + const mbDevice = new MatterbridgeEndpoint(definition, { uniqueStorageKey: bthomeDevice.name }, this.config.debug as boolean); + mbDevice.configUrl = `http://${gateway.host}`; + mbDevice.createDefaultBridgedDeviceBasicInformationClusterServer( + bthomeDevice.name, + bthomeDevice.addr + (this.postfix ? '-' + this.postfix : ''), + 0xfff1, + 'Shelly', + bthomeDevice.model, + ); + if (bthomeDevice.model === 'Shelly BLU DoorWindow') { + mbDevice.createDefaultPowerSourceReplaceableBatteryClusterServer(); + mbDevice.addFixedLabel('composed', 'Sensor'); + mbDevice.addChildDeviceTypeWithClusterServer('Contact', [contactSensor], [], undefined, this.config.debug as boolean); + if (this.validateEntity(bthomeDevice.addr, 'Illuminance')) + mbDevice.addChildDeviceTypeWithClusterServer('Illuminance', [lightSensor], [], undefined, this.config.debug as boolean); + } else if (bthomeDevice.model === 'Shelly BLU Motion') { + mbDevice.createDefaultPowerSourceReplaceableBatteryClusterServer(); + mbDevice.addFixedLabel('composed', 'Sensor'); + mbDevice.addChildDeviceTypeWithClusterServer('Motion', [occupancySensor], [], undefined, this.config.debug as boolean); + if (this.validateEntity(bthomeDevice.addr, 'Illuminance')) + mbDevice.addChildDeviceTypeWithClusterServer('Illuminance', [lightSensor], [], undefined, this.config.debug as boolean); + if (this.validateEntity(bthomeDevice.addr, 'Button')) mbDevice.addChildDeviceTypeWithClusterServer('Button', [genericSwitch], [], undefined, this.config.debug as boolean); + } else if (bthomeDevice.model === 'Shelly BLU Button1') { + mbDevice.createDefaultPowerSourceReplaceableBatteryClusterServer(); + mbDevice.createDefaultSwitchClusterServer(); + } else if (bthomeDevice.model === 'Shelly BLU HT') { + mbDevice.createDefaultPowerSourceReplaceableBatteryClusterServer(); + mbDevice.addFixedLabel('composed', 'Sensor'); + mbDevice.addChildDeviceTypeWithClusterServer('Temperature', [temperatureSensor], [], undefined, this.config.debug as boolean); + mbDevice.addChildDeviceTypeWithClusterServer('Humidity', [humiditySensor], [], undefined, this.config.debug as boolean); + if (this.validateEntity(bthomeDevice.addr, 'Button')) mbDevice.addChildDeviceTypeWithClusterServer('Button', [genericSwitch], [], undefined, this.config.debug as boolean); + } else if (bthomeDevice.model === 'Shelly BLU RC Button 4') { + mbDevice.createDefaultPowerSourceReplaceableBatteryClusterServer(); + mbDevice.addFixedLabel('composed', 'Input'); + mbDevice.addChildDeviceTypeWithClusterServer('Button0', [genericSwitch], [Switch.Cluster.id], undefined, this.config.debug as boolean); + mbDevice.addChildDeviceTypeWithClusterServer('Button1', [genericSwitch], [Switch.Cluster.id], undefined, this.config.debug as boolean); + mbDevice.addChildDeviceTypeWithClusterServer('Button2', [genericSwitch], [Switch.Cluster.id], undefined, this.config.debug as boolean); + mbDevice.addChildDeviceTypeWithClusterServer('Button3', [genericSwitch], [Switch.Cluster.id], undefined, this.config.debug as boolean); + } else if (bthomeDevice.model === 'Shelly BLU Wall Switch 4') { + mbDevice.createDefaultPowerSourceReplaceableBatteryClusterServer(); + mbDevice.addFixedLabel('composed', 'Input'); + mbDevice.addChildDeviceTypeWithClusterServer('Button0', [genericSwitch], [Switch.Cluster.id], undefined, this.config.debug as boolean); + mbDevice.addChildDeviceTypeWithClusterServer('Button1', [genericSwitch], [Switch.Cluster.id], undefined, this.config.debug as boolean); + mbDevice.addChildDeviceTypeWithClusterServer('Button2', [genericSwitch], [Switch.Cluster.id], undefined, this.config.debug as boolean); + mbDevice.addChildDeviceTypeWithClusterServer('Button3', [genericSwitch], [Switch.Cluster.id], undefined, this.config.debug as boolean); + } else if (bthomeDevice.model === 'Shelly BLU Trv') { + mbDevice.createDefaultPowerSourceReplaceableBatteryClusterServer(100, PowerSource.BatChargeLevel.Ok, 3000, 'Type AA', 2); + mbDevice.createDefaultIdentifyClusterServer(); + mbDevice.createDefaultHeatingThermostatClusterServer(undefined, undefined, 4, 30); + mbDevice.subscribeAttribute( + Thermostat.Cluster.id, + 'systemMode', + (newValue: number, oldValue: number) => { + if ( + isValidNumber(newValue, Thermostat.SystemMode.Off, Thermostat.SystemMode.Heat) && + isValidNumber(oldValue, Thermostat.SystemMode.Off, Thermostat.SystemMode.Heat) && + newValue !== oldValue + ) { + mbDevice.log.info(`Thermostat systemMode changed from ${oldValue} to ${newValue}`); + if (oldValue === Thermostat.SystemMode.Heat && newValue === Thermostat.SystemMode.Off) { + if (gateway.thermostatSystemModeTimeout) clearTimeout(gateway.thermostatSystemModeTimeout); + gateway.thermostatSystemModeTimeout = setTimeout(() => { + mbDevice.setAttribute(Thermostat.Cluster.id, 'systemMode', Thermostat.SystemMode.Heat, mbDevice.log); + }, 5000); + } + } + }, + mbDevice.log, + ); + mbDevice.subscribeAttribute( + Thermostat.Cluster.id, + 'occupiedHeatingSetpoint', + (newValue: number, oldValue: number) => { + if (isValidNumber(newValue, 4 * 100, 30 * 100) && isValidNumber(oldValue, 4 * 100, 30 * 100) && newValue !== oldValue) { + mbDevice.log.info(`Thermostat occupiedHeatingSetpoint changed from ${oldValue / 100} to ${newValue / 100}`); + if (gateway.thermostatSetpointTimeout) clearTimeout(gateway.thermostatSetpointTimeout); + gateway.thermostatSetpointTimeout = setTimeout(() => { + mbDevice.log.info(`Setting thermostat occupiedHeatingSetpoint to ${newValue / 100}`); + // http://192.168.1.164/rpc/BluTrv.Call?id=201&method=Trv.SetTarget¶ms={id:0,target_C:19} + ShellyDevice.fetch(this.shelly, mbDevice.log, gateway.host, 'BluTrv.Call', { + id: bthomeDevice.blutrv_id, + method: 'Trv.SetTarget', + params: { id: 0, target_C: newValue / 100 }, + }); + }, 5000); + } + }, + mbDevice.log, + ); + } + mbDevice.addRequiredClusterServers(); + try { + await this.registerDevice(mbDevice); + this.bluBridgedDevices.set(bthomeDevice.addr, mbDevice); + mbDevice.log.logName = `${bthomeDevice.name}`; + } catch (error) { + this.log.error( + `Shelly device ${hk}${gateway.id}${er} host ${zb}${gateway.host}${er} failed to register BLU device ${idn}${bthomeDevice.name}${rs}${er}: ${error instanceof Error ? error.message : error}`, + ); + } + } + } } diff --git a/src/shellyDevice.ts b/src/shellyDevice.ts index 81eb2de..51cbd01 100644 --- a/src/shellyDevice.ts +++ b/src/shellyDevice.ts @@ -4,7 +4,7 @@ * @file src\shellyDevice.ts * @author Luca Liguori * @date 2024-05-01 - * @version 3.1.2 + * @version 3.1.3 * * Copyright 2024, 2025, 2026 Luca Liguori. * From 27022ffc7748be7c466ea8a7da3e1ffc690424b0 Mon Sep 17 00:00:00 2001 From: Luligu Date: Wed, 19 Mar 2025 13:53:53 +0100 Subject: [PATCH 08/11] Release 2.0.5 --- src/mdnsScanner.test.ts | 32 +++++++++++++++++++++++++------- src/platform.test.ts | 38 -------------------------------------- src/shellyTypes.ts | 3 +++ 3 files changed, 28 insertions(+), 45 deletions(-) diff --git a/src/mdnsScanner.test.ts b/src/mdnsScanner.test.ts index 975139e..00eaf25 100644 --- a/src/mdnsScanner.test.ts +++ b/src/mdnsScanner.test.ts @@ -3,7 +3,7 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { LogLevel, AnsiLogger, ign, db, hk, CYAN } from 'matterbridge/logger'; +import { LogLevel, AnsiLogger, ign, db, hk, CYAN, rs } from 'matterbridge/logger'; import { MdnsScanner, DiscoveredDeviceListener, DiscoveredDevice } from './mdnsScanner'; import { jest } from '@jest/globals'; import path from 'node:path'; @@ -275,7 +275,10 @@ describe('Shellies MdnsScanner test', () => { expect((mdns as any).devices.get(deviceIp)).toBe(deviceIp); expect((mdns as any).discoveredDevices.has(deviceId)).toBeTruthy(); expect((mdns as any).discoveredDevices.get(deviceId)).toEqual({ id: deviceId, host: deviceIp, port: devicePort, gen: deviceGen }); - expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, `Mdns response from ${ign} ${remoteInfo.address} family ${remoteInfo.family} port ${remoteInfo.port} ${db}`); + expect(loggerLogSpy).toHaveBeenCalledWith( + LogLevel.DEBUG, + expect.stringContaining(`Mdns response from ${ign} ${remoteInfo.address} family ${remoteInfo.family} port ${remoteInfo.port} ${rs}${db}`), + ); expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, `--- response.answers[${responsePacket.answers.length}] ---`); expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, `--- response.additionals[${responsePacket.additionals.length}] ---`); expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, `--- end ---\n`); @@ -352,7 +355,10 @@ describe('Shellies MdnsScanner test', () => { expect((mdns as any).devices.get(deviceIp)).toBe(deviceIp); expect((mdns as any).discoveredDevices.has(deviceId)).toBeTruthy(); expect((mdns as any).discoveredDevices.get(deviceId)).toEqual({ id: deviceId, host: deviceIp, port: devicePort, gen: deviceGen }); - expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, `Mdns response from ${ign} ${remoteInfo.address} family ${remoteInfo.family} port ${remoteInfo.port} ${db}`); + expect(loggerLogSpy).toHaveBeenCalledWith( + LogLevel.DEBUG, + expect.stringContaining(`Mdns response from ${ign} ${remoteInfo.address} family ${remoteInfo.family} port ${remoteInfo.port} ${rs}${db}`), + ); expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, `--- response.answers[${responsePacket.answers.length}] ---`); expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, `--- response.additionals[${responsePacket.additionals.length}] ---`); expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, `--- end ---\n`); @@ -422,7 +428,10 @@ describe('Shellies MdnsScanner test', () => { expect((mdns as any).devices.get(deviceIp)).toBe(deviceIp); expect((mdns as any).discoveredDevices.has(deviceId)).toBeTruthy(); expect((mdns as any).discoveredDevices.get(deviceId)).toEqual({ id: deviceId, host: deviceIp, port: devicePort, gen: deviceGen }); - expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, `Mdns response from ${ign} ${remoteInfo.address} family ${remoteInfo.family} port ${remoteInfo.port} ${db}`); + expect(loggerLogSpy).toHaveBeenCalledWith( + LogLevel.DEBUG, + expect.stringContaining(`Mdns response from ${ign} ${remoteInfo.address} family ${remoteInfo.family} port ${remoteInfo.port} ${rs}${db}`), + ); expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, `--- response.answers[${responsePacket.answers.length}] ---`); expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, `--- response.additionals[${responsePacket.additionals.length}] ---`); expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, `--- end ---\n`); @@ -482,7 +491,10 @@ describe('Shellies MdnsScanner test', () => { expect((mdns as any).devices.get(deviceIp)).toBe(deviceIp); expect((mdns as any).discoveredDevices.has(deviceId)).toBeTruthy(); expect((mdns as any).discoveredDevices.get(deviceId)).toEqual({ id: deviceId, host: deviceIp, port: devicePort, gen: deviceGen }); - expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, `Mdns response from ${ign} ${remoteInfo.address} family ${remoteInfo.family} port ${remoteInfo.port} ${db}`); + expect(loggerLogSpy).toHaveBeenCalledWith( + LogLevel.DEBUG, + expect.stringContaining(`Mdns response from ${ign} ${remoteInfo.address} family ${remoteInfo.family} port ${remoteInfo.port} ${rs}${db}`), + ); expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, `--- response.answers[${responsePacket.answers.length}] ---`); expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, `--- response.additionals[${responsePacket.additionals.length}] ---`); expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, `--- end ---\n`); @@ -547,7 +559,10 @@ describe('Shellies MdnsScanner test', () => { expect((mdns as any).devices.get(deviceIp)).toBe(deviceIp); expect((mdns as any).discoveredDevices.has(deviceId)).toBeTruthy(); expect((mdns as any).discoveredDevices.get(deviceId)).toEqual({ id: deviceId, host: deviceIp, port: devicePort, gen: deviceGen }); - expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, `Mdns response from ${ign} ${remoteInfo.address} family ${remoteInfo.family} port ${remoteInfo.port} ${db}`); + expect(loggerLogSpy).toHaveBeenCalledWith( + LogLevel.DEBUG, + expect.stringContaining(`Mdns response from ${ign} ${remoteInfo.address} family ${remoteInfo.family} port ${remoteInfo.port} ${rs}${db}`), + ); expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, `--- response.answers[${responsePacket.answers.length}] ---`); expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, `--- response.additionals[${responsePacket.additionals.length}] ---`); expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, `--- end ---\n`); @@ -608,7 +623,10 @@ describe('Shellies MdnsScanner test', () => { expect((mdns as any).devices.get(deviceIp)).toBe(deviceIp); expect((mdns as any).discoveredDevices.has(deviceId)).toBeTruthy(); expect((mdns as any).discoveredDevices.get(deviceId)).toEqual({ id: deviceId, host: deviceIp, port: devicePort, gen: deviceGen }); - expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, `Mdns response from ${ign} ${remoteInfo.address} family ${remoteInfo.family} port ${remoteInfo.port} ${db}`); + expect(loggerLogSpy).toHaveBeenCalledWith( + LogLevel.DEBUG, + expect.stringContaining(`Mdns response from ${ign} ${remoteInfo.address} family ${remoteInfo.family} port ${remoteInfo.port} ${rs}${db}`), + ); expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, `--- response.answers[${responsePacket.answers.length}] ---`); expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, `--- response.additionals[${responsePacket.additionals.length}] ---`); expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.DEBUG, `--- end ---\n`); diff --git a/src/platform.test.ts b/src/platform.test.ts index 062f569..e26ab17 100644 --- a/src/platform.test.ts +++ b/src/platform.test.ts @@ -302,44 +302,6 @@ describe('ShellyPlatform', () => { 30 * 1000, ); - it( - 'should call onStart with reason and load from configDiscover', - async () => { - expect(shellyPlatform).toBeDefined(); - expect((shellyPlatform as any).shelly.mdnsScanner.isScanning).toBe(false); - - shellyPlatform.config.enableConfigDiscover = true; - shellyPlatform.config.deviceIp = { - 'shellyemg3-84FCE636582C': 'invalid', - 'shellyplus-34FCE636582C': '192.168.255.1', - }; - - const create = jest.spyOn(ShellyDevice, 'create' as any).mockImplementation(async () => { - return Promise.resolve(undefined); - }); - await shellyPlatform.onStart('Test reason'); - expect(mockLog.info).toHaveBeenCalledWith(`Starting platform ${idn}${mockConfig.name}${rs}${nf}: Test reason`); - expect(mockLog.info).toHaveBeenCalledWith(`Loading from config 2 Shelly devices`); - expect(mockLog.error).toHaveBeenCalledWith( - `Config Shelly device id ${hk}shellyemg3-84FCE636582C${er} host ${zb}invalid${er} is not valid. Please check the plugin config and restart.`, - ); - expect(mockLog.debug).toHaveBeenCalledWith(`Loading from config Shelly device ${hk}shellyplus-34FCE636582C${db} host ${zb}192.168.255.1${db}`); - expect((shellyPlatform as any).nodeStorageManager).toBeDefined(); - expect(shellyPlatform.storedDevices.size).toBe(1); - - shellyPlatform.storedDevices.clear(); - expect(await (shellyPlatform as any).saveStoredDevices()).toBeTruthy(); - expect((shellyPlatform as any).storedDevices.size).toBe(0); - - shellyPlatform.config.enableConfigDiscover = false; - shellyPlatform.config.deviceIp = undefined; - - create.mockRestore(); - cleanup(); - }, - 30 * 1000, - ); - // eslint-disable-next-line jest/no-commented-out-tests /* it( diff --git a/src/shellyTypes.ts b/src/shellyTypes.ts index c5bfe0a..46e5596 100644 --- a/src/shellyTypes.ts +++ b/src/shellyTypes.ts @@ -21,6 +21,9 @@ * limitations under the License. * */ +type AnyPrimitive = string | number | bigint | boolean | symbol | null | undefined; +export type AnyValue = AnyPrimitive | AnyPrimitive[] | Record; + export declare type ParamsTypes = boolean | number | string; export type ShellyDeviceId = string; From 3d48077191fdcd997750c663b0664b9132f13351 Mon Sep 17 00:00:00 2001 From: Luligu Date: Wed, 19 Mar 2025 16:54:42 +0100 Subject: [PATCH 09/11] Bump ShellyDevice version to 3.1.4 and update imports for better organization --- src/shellyDevice.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/shellyDevice.ts b/src/shellyDevice.ts index 51cbd01..a2b1437 100644 --- a/src/shellyDevice.ts +++ b/src/shellyDevice.ts @@ -4,7 +4,7 @@ * @file src\shellyDevice.ts * @author Luca Liguori * @date 2024-05-01 - * @version 3.1.3 + * @version 3.1.4 * * Copyright 2024, 2025, 2026 Luca Liguori. * @@ -21,16 +21,18 @@ * limitations under the License. * */ +// Matterbridge imports import { AnsiLogger, LogLevel, BLUE, CYAN, GREEN, GREY, MAGENTA, RESET, db, debugStringify, er, hk, nf, wr, zb, rs, YELLOW, idn, nt, rk, dn } from 'matterbridge/logger'; import { getIpv4InterfaceAddress, isValidNumber, isValidObject, isValidString } from 'matterbridge/utils'; + +// Node.js imports import { EventEmitter } from 'node:events'; -import fetch, { RequestInit } from 'node-fetch'; import crypto from 'node:crypto'; import { promises as fs } from 'node:fs'; import path from 'node:path'; +// Shellies imports import { parseDigestAuthenticateHeader, createDigestShellyAuth, createBasicShellyAuth, parseBasicAuthenticateHeader, getGen2BodyOptions, getGen1BodyOptions } from './auth.js'; - import { WsClient } from './wsClient.js'; import { Shelly } from './shelly.js'; import { From ffe2358a4b9bd70f42b3e9a448eb88c9b5728acf Mon Sep 17 00:00:00 2001 From: Luligu Date: Wed, 19 Mar 2025 16:55:05 +0100 Subject: [PATCH 10/11] Bump MdnsScanner version to 1.2.4 and add query event to MdnsScanner for enhanced device discovery --- src/mdnsScanner.test.ts | 152 ++++++++++++++++++++++++++++++++++++++++ src/mdnsScanner.ts | 4 +- 2 files changed, 155 insertions(+), 1 deletion(-) diff --git a/src/mdnsScanner.test.ts b/src/mdnsScanner.test.ts index 00eaf25..474b7fb 100644 --- a/src/mdnsScanner.test.ts +++ b/src/mdnsScanner.test.ts @@ -148,6 +148,71 @@ describe('Shellies MdnsScanner test', () => { }, 1000); }, 10000); + test('Generic response', async () => { + // Start the mdns scanner + mdns.start(undefined, 0, '127.0.0.1', 'udp4', true); + expect(mdns.isScanning).toBeTruthy(); + expect(loggerLogSpy).toHaveBeenCalledWith( + LogLevel.INFO, + `Starting MdnsScanner for shelly devices (interface 127.0.0.1 bind 127.0.0.1 type udp4 ip 224.0.0.251) for shelly devices...`, + ); + + // Set up a promise that resolves when the listener is invoked. + const discoveredDeviceListener: jest.MockedFunction = jest.fn(); + const discoveredPromise = new Promise((resolve) => { + discoveredDeviceListener.mockImplementationOnce((device: DiscoveredDevice) => { + console.log(`Shellies MdnsScanner Jest Test: discovered shelly device: ${device.id} at ${device.host} port ${device.port} gen ${device.gen}`); + resolve(device); + }); + }); + mdns.once('discovered', discoveredDeviceListener); + + // Emit the response packet + (mdns as any).scanner.emit('response', generic_ResponsePacket, generic_RemoteInfo); + + // Wait for the discovered event to be processed. + expect(await discoveredPromise).toEqual({ id: 'shellyswitch25-3494546BBF7E', host: '192.168.1.1', port: 80, gen: 1 }); + + // Stop the mdns scanner + mdns.stop(); + expect(mdns.isScanning).toBeFalsy(); + }, 10000); + + test('Generic query', async () => { + // Start the mdns scanner + mdns.start(undefined, 10, '127.0.0.1', 'udp4', true); + expect(mdns.isScanning).toBeTruthy(); + await new Promise((resolve) => setTimeout(resolve, 500)); + expect(loggerLogSpy).toHaveBeenCalledWith( + LogLevel.INFO, + `Starting MdnsScanner for shelly devices (interface 127.0.0.1 bind 127.0.0.1 type udp4 ip 224.0.0.251) for shelly devices...`, + ); + expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.INFO, `Started MdnsScanner for shelly devices.`); + expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.INFO, `Stopped MdnsScanner query service for shelly devices.`); + + // Set up a promise that resolves when the listener is invoked. + const queryDeviceListener: jest.MockedFunction = jest.fn(); + const queryPromise = new Promise((resolve) => { + queryDeviceListener.mockImplementationOnce((device: DiscoveredDevice) => { + console.log(`Shellies MdnsScanner Jest Test: discovered shelly device: ${device.id} at ${device.host} port ${device.port} gen ${device.gen}`); + resolve(device); + }); + }); + mdns.once('query', queryDeviceListener); + + // Emit the response packet + (mdns as any).scanner.emit('query', generic_QueryPacket, generic_RemoteInfo); + + // Wait for the discovered event to be processed. + expect(await queryPromise).toEqual({ 'class': 'IN', 'name': '_http._tcp.local', 'type': 'PTR' }); + + // Stop the mdns scanner + mdns.stop(); + expect(mdns.isScanning).toBeFalsy(); + expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.INFO, `Stopping MdnsScanner for shelly devices...`); + expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.INFO, `Stopped MdnsScanner for shelly devices.`); + }, 10000); + test('Shelly gen 1', async () => { // Start the mdns scanner mdns.start(undefined, 0, '127.0.0.1', 'udp4', true); @@ -687,6 +752,93 @@ describe('Shellies MdnsScanner test', () => { }); }); +const generic_QueryPacket = { + id: 0, + type: 'query', + flags: 1024, + flag_qr: true, + opcode: 'QUERY', + flag_aa: true, + flag_tc: false, + flag_rd: false, + flag_ra: false, + flag_z: false, + flag_ad: false, + flag_cd: false, + rcode: 'NOERROR', + questions: [{ name: '_http._tcp.local', type: 'PTR', class: 'IN' }], + answers: [], + authorities: [], + additionals: [], +}; + +const generic_ResponsePacket = { + id: 0, + type: 'response', + flags: 1024, + flag_qr: true, + opcode: 'QUERY', + flag_aa: true, + flag_tc: false, + flag_rd: false, + flag_ra: false, + flag_z: false, + flag_ad: false, + flag_cd: false, + rcode: 'NOERROR', + questions: [{ name: '_http._tcp.local', type: 'PTR', class: 'IN' }], + answers: [ + { + name: '_http._tcp.local', + type: 'PTR', + ttl: 4500, + class: 'IN', + flush: false, + data: 'shellyswitch25-3494546BBF7E._http._tcp.local', + }, + { + name: 'shellyswitch25-3494546BBF7E._http._tcp.local', + type: 'SRV', + ttl: 120, + class: 'IN', + flush: true, + data: { + priority: 0, + weight: 0, + port: 80, + target: 'shellyswitch25-3494546BBF7E.local', + }, + }, + { + name: 'shellyswitch25-3494546BBF7E._http._tcp.local', + type: 'TXT', + ttl: 120, + class: 'IN', + flush: true, + data: [], + }, + { + name: 'shellyswitch25-3494546BBF7E.local', + type: 'A', + ttl: 120, + class: 'IN', + flush: true, + data: '192.168.1.1', + }, + { + name: 'shellyswitch25-3494546BBF7E.local', + type: 'NSEC', + ttl: 120, + class: 'IN', + flush: true, + data: { nextDomain: 'shellyswitch25-3494546BBF7E.local', rrtypes: ['A'] }, + }, + ], + authorities: [], + additionals: [], +}; +const generic_RemoteInfo = { address: '192.168.1.1', family: 'IPv4', port: 5353, size: 501 }; + const gen1_ResponsePacket = { id: 0, type: 'response', diff --git a/src/mdnsScanner.ts b/src/mdnsScanner.ts index 0889be7..f4f5838 100644 --- a/src/mdnsScanner.ts +++ b/src/mdnsScanner.ts @@ -4,7 +4,7 @@ * @file src\mdnsScanner.ts * @author Luca Liguori * @date 2024-05-01 - * @version 1.2.3 + * @version 1.2.4 * * Copyright 2024, 2025, 2026 Luca Liguori. * @@ -41,6 +41,7 @@ export type DiscoveredDeviceListener = (data: DiscoveredDevice) => void; interface MdnsScannerEvent { discovered: [{ id: ShellyDeviceId; host: string; port: number; gen: number }]; + query: [{ type: string; name: string; class?: string }]; } /** @@ -246,6 +247,7 @@ export class MdnsScanner extends EventEmitter { if (debug) this.log.debug(`--- query.questions[${query.questions.length}] ---`); for (const q of query.questions) { if (debug) this.log.debug(`[${ign}${q.type}${rs}${db}] Name: ${CYAN}${q.name}${db} class: ${CYAN}${q.class}${db}`); + this.emit('query', { type: q.type, name: q.name, class: q.class }); } if (debug) this.log.debug(`--- query.answers[${query.answers.length}] ---`); if (debug) this.log.debug(`--- query.additionals[${query.additionals.length}] ---`); From 3955613aeb384e6ffe33177ed119bf730d82c489 Mon Sep 17 00:00:00 2001 From: Luligu Date: Wed, 19 Mar 2025 16:55:56 +0100 Subject: [PATCH 11/11] Release 2.0.5 --- CHANGELOG.md | 4 +++ package-lock.json | 91 ----------------------------------------------- package.json | 1 - 3 files changed, 4 insertions(+), 92 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a6b914..f3ffe28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,10 @@ The expertMode option has been added to show an advanced or simplified config. - [BLU]: Verified new BLU firmware 1.0.22 on all BLU devices. - [TRV]: Verified new BLU TRV firmware 1.2.1. +### Removed + +- [fetch]: Removed node-fetch package and use the global fetch. + ### Changed - [config] Removed enableConfigDiscover and deviceIp. Replaced by a config action (manually add a device with IP address). diff --git a/package-lock.json b/package-lock.json index a89c171..ab9e894 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "coap": "1.4.1", "multicast-dns": "7.2.5", "node-ansi-logger": "3.0.1", - "node-fetch": "3.3.2", "node-persist-manager": "1.0.8", "ws": "8.18.1" }, @@ -2381,15 +2380,6 @@ "node": ">= 8" } }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -3066,29 +3056,6 @@ "bser": "2.1.1" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -3190,18 +3157,6 @@ "license": "ISC", "peer": true }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4557,43 +4512,6 @@ "url": "https://www.buymeacoffee.com/luligugithub" } }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -5729,15 +5647,6 @@ "makeerror": "1.0.12" } }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 60e2e80..5b1f5c3 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,6 @@ "coap": "1.4.1", "multicast-dns": "7.2.5", "node-ansi-logger": "3.0.1", - "node-fetch": "3.3.2", "node-persist-manager": "1.0.8", "ws": "8.18.1" },