From 009e4cd70496f01b4e5bed469f80e8ebf37c0093 Mon Sep 17 00:00:00 2001 From: Luligu <132135057+Luligu@users.noreply.github.com> Date: Wed, 26 Jun 2024 15:16:45 +0200 Subject: [PATCH 1/2] Release 0.6.0 --- CHANGELOG.md | 4 ++-- src/coapServer.ts | 44 ++++++++++++++------------------------------ 2 files changed, 16 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7c7427..8221606 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# Changelog +# Matterbridge Logo   Matterbridge shelly plugin changelog All notable changes to this project will be documented in this file. @@ -16,7 +16,7 @@ If you like this project and find it useful, please consider giving it a star on - [package]: Updated dependencies - Buy me a coffee + Buy me a coffee ## [0.5.1] - 2024-06-25 diff --git a/src/coapServer.ts b/src/coapServer.ts index 9cdbdc3..a33f5e0 100644 --- a/src/coapServer.ts +++ b/src/coapServer.ts @@ -106,13 +106,13 @@ export class CoapServer extends EventEmitter { // agent: this.coapAgent, }) .on('response', (msg: IncomingMessage) => { - this.log.debug(`Coap got device description ("/cit/d") code ${BLUE}${msg.code}${db} url ${BLUE}${msg.url}${db} rsinfo ${debugStringify(msg.rsinfo)}:`); + this.log.debug(`CoIoT (coap) received device description ("/cit/d") code ${BLUE}${msg.code}${db} url ${BLUE}${msg.url}${db} rsinfo ${debugStringify(msg.rsinfo)}:`); msg.url = '/cit/d'; this.parseShellyMessage(msg); resolve(msg); }) .on('error', (err) => { - this.log.error('Coap error getting device description ("/cit/d"):', err); + this.log.error('CoIoT (coap) error getting device description ("/cit/d"):', err); reject(err); }) .end(); @@ -131,12 +131,12 @@ export class CoapServer extends EventEmitter { // agent: this.coapAgent, }) .on('response', (msg: IncomingMessage) => { - this.log.debug(`Coap got device status ("/cit/s") code ${BLUE}${msg.code}${db} url ${BLUE}${msg.url}${db} rsinfo ${debugStringify(msg.rsinfo)}:`); + this.log.debug(`CoIoT (coap) received device status ("/cit/s") code ${BLUE}${msg.code}${db} url ${BLUE}${msg.url}${db} rsinfo ${debugStringify(msg.rsinfo)}:`); this.parseShellyMessage(msg); resolve(msg); }) .on('error', (err) => { - this.log.error('Coap error getting device status ("/cit/s"):', err); + this.log.error('CoIoT (coap) error getting device status ("/cit/s"):', err); reject(err); }) .end(); @@ -144,7 +144,7 @@ export class CoapServer extends EventEmitter { } getMulticastDeviceStatus(timeout = 60): Promise { - this.log.debug('Requesting multicast device status...'); + this.log.debug('Requesting CoIoT (coap) multicast device status...'); return new Promise((resolve, reject) => { const request = coap .request({ @@ -163,7 +163,7 @@ export class CoapServer extends EventEmitter { }) .on('error', (err) => { clearTimeout(timer); - this.log.error('Coap error requesting multicast device status ("/cit/s"):', err); + this.log.error('CoIoT (coap) error requesting multicast device status ("/cit/s"):', err); reject(err); }) .end(); @@ -231,7 +231,7 @@ export class CoapServer extends EventEmitter { } private parseShellyMessage(msg: IncomingMessage) { - this.log.debug(`Parsing device Coap response...`); + this.log.debug(`Parsing device CoIoT (coap) response...`); const host = msg.rsinfo.address; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -370,25 +370,9 @@ export class CoapServer extends EventEmitter { multicastAddress: COAP_MULTICAST_ADDRESS, }); - /* - // 192.168.1.189:5683 - // insert our own middleware right before requests are handled (the last step) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - this.coapServer._middlewares.splice(Math.max(this.coapServer._middlewares.length - 1, 0), 0, (req: any, next: any) => { - this.log.warn(`Server middleware got a messagge code ${req.packet.code} rsinfo ${debugStringify(req.rsinfo)}...`); - // Unicast messages from Shelly devices will have the 2.05 code, which the - // server will silently drop (since its a response code and not a request - // code). To avoid this, we change it to 0.30 here. - if (req.packet.code === '2.05') { - req.packet.code = '0.30'; - } - next(); - }); - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars this.coapServer.on('request', (msg: IncomingMessage, res: OutgoingMessage) => { - this.log.debug(`Coap server got a messagge code ${BLUE}${msg.code}${db} url ${BLUE}${msg.url}${db} rsinfo ${debugStringify(msg.rsinfo)}...`); + this.log.debug(`CoIoT (coap) server got a messagge code ${BLUE}${msg.code}${db} url ${BLUE}${msg.url}${db} rsinfo ${debugStringify(msg.rsinfo)}...`); if (msg.code === '0.30' && msg.url === '/cit/s') { // const coapMessage = this.parseShellyMessage(msg); // this.emit('update', coapMessage); @@ -401,9 +385,9 @@ export class CoapServer extends EventEmitter { this.coapServer.listen((err) => { if (err) { - this.log.warn('Coap server error while listening:', err); + this.log.warn('CoIoT (coap) server error while listening:', err); } else { - this.log.info('Coap server is listening ...'); + this.log.info('CoIoT (coap) server is listening ...'); } }); } @@ -417,21 +401,21 @@ export class CoapServer extends EventEmitter { start(debug = false) { this.log.setLogDebug(debug); if (this._isListening) return; - this.log.info('Starting CoIoT server for shelly devices...'); + this.log.info('Starting CoIoT (coap) server for shelly devices...'); this._isListening = true; this.listenForStatusUpdates(); - this.log.info('Started CoIoT server for shelly devices.'); + this.log.info('Started CoIoT (coap) server for shelly devices.'); } stop() { - this.log.info('Stopping CoIoT server for shelly devices...'); + this.log.info('Stopping CoIoT (coap) server for shelly devices...'); this.removeAllListeners(); this._isListening = false; globalAgent.close(); // this.coapAgent.close(); if (this.coapServer) this.coapServer.close(); this.devices.clear(); - this.log.info('Stopped CoIoT server for shelly devices.'); + this.log.info('Stopped CoIoT (coap) server for shelly devices.'); } } From 7f0b01584d93b9c3313070cf64557e4d177c35ea Mon Sep 17 00:00:00 2001 From: Luligu <132135057+Luligu@users.noreply.github.com> Date: Fri, 28 Jun 2024 22:24:25 +0200 Subject: [PATCH 2/2] Release 0.6.1 --- CHANGELOG.md | 10 +++ README.md | 11 ++- matterbridge-shelly.schema.json | 2 +- package-lock.json | 73 +++++++++-------- package.json | 8 +- src/index.ts | 4 +- src/platform.ts | 138 +++++++++++++++++++++----------- src/shellyComponent.ts | 25 +++--- src/shellyDevice.ts | 7 +- src/wsClient.ts | 18 +++-- 10 files changed, 179 insertions(+), 117 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8221606..1c5fdc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file. If you like this project and find it useful, please consider giving it a star on GitHub at https://github.com/Luligu/matterbridge-shelly and sponsoring it. +## [0.6.1] - 2024-06-28 + +### Changed + +- [firmware]: The recent firmware update for Gen 2 and Gen. 3 devices changed the way data is sent. This fix the electrical readings. + + + Buy me a coffee + + ## [0.6.0] - 2024-06-26 ### Added diff --git a/README.md b/README.md index ed90042..08d3cb5 100644 --- a/README.md +++ b/README.md @@ -114,11 +114,11 @@ Choose how to expose the shelly power meters: disabled, matter13 (use Matter 1.3 ### blackList -If the blackList is defined the devices included in the list will not be exposed to Matter. Use the device id (e.g. shellyplus2pm-5443b23d81f8) +If the blackList is defined the devices included in the list will not be exposed to Matter. Use the device id (e.g. shellyplus2pm-5443B23D81F8) ### whiteList -If the whiteList is defined only the devices included in the list are exposed to Matter. Use the device id (e.g. shellyplus2pm-5443b23d81f8). +If the whiteList is defined only the devices included in the list are exposed to Matter. Use the device id (e.g. shellyplus2pm-5443B23D81F8). ### deviceIp @@ -140,12 +140,16 @@ Reset the storage discovery on the next restart (it will clear the storage of al ### enableConfigDiscover -Should be enabled only if the mdns is not working. It adds the devices defined in deviceIp. +Should be enabled only if the mdns is not working in your network. It adds the devices defined in deviceIp. ### debug Should be enabled only if you want to debug some issue in the log. +### unregisterOnShutdown + +Should be enabled only if you want to remove the devices from the controllers on shutdown. + ### Config file These are the config values: @@ -166,6 +170,7 @@ These are the config values: }, "enableMdnsDiscover": true, "enableStorageDiscover": true, + "resetStorageDiscover": false "enableConfigDiscover": false, "debug": false, "unregisterOnShutdown": false, diff --git a/matterbridge-shelly.schema.json b/matterbridge-shelly.schema.json index 2f36b2f..d2b0e59 100644 --- a/matterbridge-shelly.schema.json +++ b/matterbridge-shelly.schema.json @@ -22,7 +22,7 @@ "type": "string" }, "exposeSwitch": { - "description": "Choose how to expose the shelly switches: as a switch, light or outlet", + "description": "Choose how to expose the shelly switches: as a switch (don't use it for Alexa), light or outlet", "type": "string", "enum": ["switch", "light", "outlet"], "default": "switch" diff --git a/package-lock.json b/package-lock.json index e3f4a0b..46c727c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "matterbridge-shelly", - "version": "0.6.0", + "version": "0.6.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "matterbridge-shelly", - "version": "0.6.0", + "version": "0.6.1", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -19,7 +19,7 @@ "ws": "^8.17.1" }, "devDependencies": { - "@eslint/js": "^9.5.0", + "@eslint/js": "^9.6.0", "@types/eslint__js": "^8.42.3", "@types/jest": "^29.5.12", "@types/multicast-dns": "^7.2.4", @@ -44,7 +44,7 @@ } }, "../matterbridge": { - "version": "1.3.5", + "version": "1.3.6", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -61,16 +61,21 @@ "matterbridge": "dist/cli.js" }, "devDependencies": { - "@tsconfig/node-lts": "^20.1.3", + "@eslint/js": "^9.5.0", + "@types/eslint__js": "^8.42.3", "@types/express": "^4.17.21", - "@types/node": "^20.14.8", + "@types/jest": "^29.5.12", + "@types/node": "^20.14.9", "@types/ws": "^8.5.10", - "@typescript-eslint/eslint-plugin": "^7.13.1", - "@typescript-eslint/parser": "^7.13.1", "eslint-config-prettier": "^9.1.0", + "eslint-plugin-jest": "^28.6.0", "eslint-plugin-prettier": "^5.1.3", + "jest": "^29.7.0", "prettier": "^3.3.2", - "typescript": "^5.5.2" + "rimraf": "^5.0.7", + "ts-jest": "^29.1.5", + "typescript": "^5.5.2", + "typescript-eslint": "^7.14.1" }, "engines": { "node": ">=18.0.0" @@ -771,9 +776,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz", - "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", "dev": true, "license": "MIT", "engines": { @@ -781,16 +786,16 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.16.0.tgz", - "integrity": "sha512-/jmuSd74i4Czf1XXn7wGRWZCuyaUZ330NH1Bek0Pplatt4Sy1S5haN21SCLLdbeKslQ+S0wEJ+++v5YibSi+Lg==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.0.tgz", + "integrity": "sha512-A68TBu6/1mHHuc5YJL0U0VVeGNiklLAL6rRmhTCP2B5XjWLMnrX+HkO+IAXyHvks5cyyY1jjK5ITPQ1HGS2EVA==", "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { "@eslint/object-schema": "^2.1.4", "debug": "^4.3.1", - "minimatch": "^3.0.5" + "minimatch": "^3.1.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -822,9 +827,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.5.0.tgz", - "integrity": "sha512-A7+AOT2ICkodvtsWnxZP4Xxk3NbZ3VMHd8oihydLRGrJgqqdEz1qSeEgXYyT/Cu8h1TWWsQRejIx48mtjZ5y1w==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.6.0.tgz", + "integrity": "sha512-D9B0/3vNg44ZeWbYMpBoXqNP4j6eQD5vNwIlGAuFRRzK/WtT/jvDQW3Bi9kkf3PMDMlM7Yi+73VLUsn5bJcl8A==", "dev": true, "license": "MIT", "engines": { @@ -2394,9 +2399,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001637", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001637.tgz", - "integrity": "sha512-1x0qRI1mD1o9e+7mBI7XtzFAP4XszbHaVWsMiGbSPLYekKTJF7K+FNk6AsXH4sUpc+qrsI3pVgf1Jdl/uGkuSQ==", + "version": "1.0.30001638", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001638.tgz", + "integrity": "sha512-5SuJUJ7cZnhPpeLHaH0c/HPAnAHZvS6ElWyHK9GSIbVOQABLzowiI2pjmpvZ1WEbkyz46iFd4UXlOHR5SqgfMQ==", "dev": true, "funding": [ { @@ -2711,9 +2716,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.4.812", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.812.tgz", - "integrity": "sha512-7L8fC2Ey/b6SePDFKR2zHAy4mbdp1/38Yk5TsARO66W3hC5KEaeKMMHoxwtuH+jcu2AYLSn9QX04i95t6Fl1Hg==", + "version": "1.4.814", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.814.tgz", + "integrity": "sha512-GVulpHjFu1Y9ZvikvbArHmAhZXtm3wHlpjTMcXNGKl4IQ4jMQjlnz8yMQYYqdLHKi/jEL2+CBC2akWVCoIGUdw==", "dev": true, "license": "ISC" }, @@ -2771,18 +2776,18 @@ } }, "node_modules/eslint": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.5.0.tgz", - "integrity": "sha512-+NAOZFrW/jFTS3dASCGBxX1pkFD0/fsO+hfAkJ4TyYKwgsXZbqzrw+seCYFCcPCYXvnD67tAnglU7GQTz6kcVw==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.6.0.tgz", + "integrity": "sha512-ElQkdLMEEqQNM9Njff+2Y4q2afHk7JpkPvrd7Xh7xefwgQynqPxwf55J7di9+MEibWUGdNjFF9ITG9Pck5M84w==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/config-array": "^0.16.0", + "@eslint/config-array": "^0.17.0", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.5.0", + "@eslint/js": "9.6.0", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", @@ -2793,7 +2798,7 @@ "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.0.1", "eslint-visitor-keys": "^4.0.0", - "espree": "^10.0.1", + "espree": "^10.1.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -4959,9 +4964,9 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.3.0.tgz", + "integrity": "sha512-CQl19J/g+Hbjbv4Y3mFNNXFEL/5t/KCg8POCuUqd4rMKjGG+j1ybER83hxV58zL+dFI1PTkt3GNFSHRt+d8qEQ==", "license": "ISC", "engines": { "node": "14 || >=16.14" diff --git a/package.json b/package.json index 3d42054..94763c4 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "matterbridge-shelly", - "version": "0.6.0", + "version": "0.6.1", "description": "Matterbridge shelly plugin", "author": "https://github.com/Luligu", "license": "Apache-2.0", "type": "module", "main": "dist/index.js", - "types": "dist/index.d.js", + "types": "dist/index.d.ts", "repository": { "type": "git", "url": "git+https://github.com/Luligu/matterbridge-shelly.git" @@ -100,7 +100,7 @@ "ws": "^8.17.1" }, "devDependencies": { - "@eslint/js": "^9.5.0", + "@eslint/js": "^9.6.0", "@types/eslint__js": "^8.42.3", "@types/jest": "^29.5.12", "@types/multicast-dns": "^7.2.4", @@ -119,4 +119,4 @@ "overrides": { "eslint": "latest" } -} +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index c2d6b2e..017d010 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,7 +22,7 @@ */ import { Matterbridge, PlatformConfig } from 'matterbridge'; -import { AnsiLogger } from 'node-ansi-logger'; +import { AnsiLogger } from 'matterbridge/logger'; import { ShellyPlatform } from './platform.js'; /** @@ -32,7 +32,7 @@ import { ShellyPlatform } from './platform.js'; * @param {Matterbridge} matterbridge - An instance of MatterBridge. This is the main interface for interacting with the MatterBridge system. * @param {AnsiLogger} log - An instance of AnsiLogger. This is used for logging messages in a format that can be displayed with ANSI color codes. * @param {PlatformConfig} config - The platform configuration. - * @returns {ShellyPlatform} - An instance of the SomfyTahomaPlatform. This is the main interface for interacting with the Somfy Tahoma system. + * @returns {ShellyPlatform} - An instance of the ShellyPlatform. This is the main interface for interacting with the Shellies. * */ diff --git a/src/platform.ts b/src/platform.ts index 592295b..f5b1a8d 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -48,17 +48,20 @@ import { SwitchCluster, Switch, ColorControlCluster, + electricalSensor, + ElectricalPowerMeasurement, + ElectricalEnergyMeasurement, } from 'matterbridge'; -import { AnsiLogger, BLUE, CYAN, GREEN, TimestampFormat, YELLOW, db, debugStringify, dn, er, hk, idn, nf, or, rs, wr, zb } from 'node-ansi-logger'; -import { NodeStorage, NodeStorageManager } from 'node-persist-manager'; +import { AnsiLogger, BLUE, CYAN, GREEN, TimestampFormat, YELLOW, db, debugStringify, dn, er, hk, idn, nf, or, rs, wr, zb } from 'matterbridge/logger'; +import { NodeStorage, NodeStorageManager } from 'matterbridge/storage'; import path from 'path'; import { Shelly } from './shelly.js'; import { DiscoveredDevice } from './mdnsScanner.js'; import { ShellyDevice } from './shellyDevice.js'; -import { ShellyCoverComponent, ShellySwitchComponent } from './shellyComponent.js'; +import { ShellyCoverComponent, ShellyLightComponent, ShellySwitchComponent } from './shellyComponent.js'; import { ShellyData, ShellyDataType } from './shellyTypes.js'; -import { hslColorToRgbColor, rgbColorToHslColor } from './colorUtils.js'; +import { hslColorToRgbColor, rgbColorToHslColor } from './colorUtils.js'; // 'matterbridge/utils/colorUtils'; type ConfigDeviceIp = Record; @@ -240,6 +243,7 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { // Add the electrical EveHistory cluster if ( + config.exposePowerMeter === 'evehistory' && switchComponent.hasProperty('voltage') && switchComponent.hasProperty('current') && switchComponent.hasProperty('apower') && @@ -279,7 +283,13 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { mbDevice.addFixedLabel('composed', component.name); // Add the electrical EveHistory cluster - if (coverComponent.hasProperty('voltage') && coverComponent.hasProperty('current') && coverComponent.hasProperty('apower') && coverComponent.hasProperty('aenergy')) { + if ( + config.exposePowerMeter === 'evehistory' && + coverComponent.hasProperty('voltage') && + coverComponent.hasProperty('current') && + coverComponent.hasProperty('apower') && + coverComponent.hasProperty('aenergy') + ) { child.addClusterServer( mbDevice.getDefaultStaticEveHistoryClusterServer( coverComponent.getValue('voltage') as number, @@ -327,30 +337,50 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { const pmComponent = device.getComponent(key); if (pmComponent) { mbDevice.addFixedLabel('composed', component.name); - // Add the Matter 1.3 device type with the ElectricalPowerMeasurement and ElectricalEnergyMeasurement clusters - // mbDevice.addChildDeviceTypeWithClusterServer('electricalSensor', [electricalSensor], [ElectricalPowerMeasurement.Cluster.id, ElectricalEnergyMeasurement.Cluster.id]); - // Add the custom EveHistory cluster for HA - ClusterRegistry.register(EveHistory.Complete); - const child = mbDevice.addChildDeviceTypeWithClusterServer(key, [powerSource], [EveHistory.Cluster.id]); - // Set the electrical attributes - const voltage = pmComponent.hasProperty('voltage') ? pmComponent.getValue('voltage') : undefined; - if (voltage !== undefined) child.getClusterServer(EveHistoryCluster.with(EveHistory.Feature.EveEnergy))?.setVoltageAttribute(voltage as number); - - const current = pmComponent.hasProperty('current') ? pmComponent.getValue('current') : undefined; - if (current !== undefined) child.getClusterServer(EveHistoryCluster.with(EveHistory.Feature.EveEnergy))?.setCurrentAttribute(current as number); - - const power1 = pmComponent.hasProperty('power') ? pmComponent.getValue('power') : undefined; // Gen 1 devices - if (power1 !== undefined) child.getClusterServer(EveHistoryCluster.with(EveHistory.Feature.EveEnergy))?.setConsumptionAttribute(power1 as number); - const power2 = pmComponent.hasProperty('apower') ? pmComponent.getValue('apower') : undefined; // Gen 2 devices - if (power2 !== undefined) child.getClusterServer(EveHistoryCluster.with(EveHistory.Feature.EveEnergy))?.setConsumptionAttribute(power2 as number); - - const energy1 = pmComponent.hasProperty('total') ? pmComponent.getValue('total') : undefined; // Gen 1 devices in watts - if (energy1 !== undefined && energy1 !== null) - child.getClusterServer(EveHistoryCluster.with(EveHistory.Feature.EveEnergy))?.setTotalConsumptionAttribute((energy1 as number) / 1000); - const energy2 = pmComponent.hasProperty('aenergy') ? pmComponent.getValue('aenergy') : undefined; // Gen 2 devices in watts - if (energy2 !== undefined && energy2 !== null) - child.getClusterServer(EveHistoryCluster.with(EveHistory.Feature.EveEnergy))?.setTotalConsumptionAttribute(((energy2 as ShellyData).total as number) / 1000); - + if (config.exposePowerMeter === 'matter13') { + // Add the Matter 1.3 electricalSensor device type with the ElectricalPowerMeasurement and ElectricalEnergyMeasurement clusters + const child = mbDevice.addChildDeviceTypeWithClusterServer(key, [electricalSensor], [ElectricalPowerMeasurement.Cluster.id, ElectricalEnergyMeasurement.Cluster.id]); + // Set the electrical attributes + const epm = child.getClusterServer(ElectricalPowerMeasurement.Complete); + const voltage = pmComponent.hasProperty('voltage') ? pmComponent.getValue('voltage') : undefined; + if (voltage !== undefined) epm?.setVoltageAttribute(50); + + const current = pmComponent.hasProperty('current') ? pmComponent.getValue('current') : undefined; + if (current !== undefined) epm?.setActiveCurrentAttribute(current as number); + + const power1 = pmComponent.hasProperty('power') ? pmComponent.getValue('power') : undefined; // Gen 1 devices + if (power1 !== undefined) epm?.setActivePowerAttribute(power1 as number); + const power2 = pmComponent.hasProperty('apower') ? pmComponent.getValue('apower') : undefined; // Gen 2 devices + if (power2 !== undefined) epm?.setActivePowerAttribute(power2 as number); + + const eem = child.getClusterServer(ElectricalEnergyMeasurement.Complete); + const energy1 = pmComponent.hasProperty('total') ? pmComponent.getValue('total') : undefined; // Gen 1 devices in watts + if (energy1 !== undefined && energy1 !== null) eem?.setCumulativeEnergyImportedAttribute({ energy: (energy1 as number) / 1000 }); + const energy2 = pmComponent.hasProperty('aenergy') ? pmComponent.getValue('aenergy') : undefined; // Gen 2 devices in watts + if (energy2 !== undefined && energy2 !== null) eem?.setCumulativeEnergyImportedAttribute({ energy: ((energy2 as ShellyData).total as number) / 1000 }); + } else if (config.exposePowerMeter === 'evehistory') { + // Add the powerSource device type with the EveHistory cluster for HA + ClusterRegistry.register(EveHistory.Complete); + const child = mbDevice.addChildDeviceTypeWithClusterServer(key, [powerSource], [EveHistory.Cluster.id]); + // Set the electrical attributes + const voltage = pmComponent.hasProperty('voltage') ? pmComponent.getValue('voltage') : undefined; + if (voltage !== undefined) child.getClusterServer(EveHistoryCluster.with(EveHistory.Feature.EveEnergy))?.setVoltageAttribute(voltage as number); + + const current = pmComponent.hasProperty('current') ? pmComponent.getValue('current') : undefined; + if (current !== undefined) child.getClusterServer(EveHistoryCluster.with(EveHistory.Feature.EveEnergy))?.setCurrentAttribute(current as number); + + const power1 = pmComponent.hasProperty('power') ? pmComponent.getValue('power') : undefined; // Gen 1 devices + if (power1 !== undefined) child.getClusterServer(EveHistoryCluster.with(EveHistory.Feature.EveEnergy))?.setConsumptionAttribute(power1 as number); + const power2 = pmComponent.hasProperty('apower') ? pmComponent.getValue('apower') : undefined; // Gen 2 devices + if (power2 !== undefined) child.getClusterServer(EveHistoryCluster.with(EveHistory.Feature.EveEnergy))?.setConsumptionAttribute(power2 as number); + + const energy1 = pmComponent.hasProperty('total') ? pmComponent.getValue('total') : undefined; // Gen 1 devices in watts + if (energy1 !== undefined && energy1 !== null) + child.getClusterServer(EveHistoryCluster.with(EveHistory.Feature.EveEnergy))?.setTotalConsumptionAttribute((energy1 as number) / 1000); + const energy2 = pmComponent.hasProperty('aenergy') ? pmComponent.getValue('aenergy') : undefined; // Gen 2 devices in watts + if (energy2 !== undefined && energy2 !== null) + child.getClusterServer(EveHistoryCluster.with(EveHistory.Feature.EveEnergy))?.setTotalConsumptionAttribute(((energy2 as ShellyData).total as number) / 1000); + } // Add event handler pmComponent.on('update', (component: string, property: string, value: ShellyDataType) => { this.shellyUpdateHandler(mbDevice, device, component, property, value); @@ -474,8 +504,12 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { } public async saveStoredDevices() { + if (!this.nodeStorage) { + this.log.error('NodeStorage is not initialized'); + return; + } this.log.debug(`Saving ${this.storedDevices.size} discovered Shelly devices to the storage`); - await this.nodeStorage?.set('DeviceIdentifiers', Array.from(this.storedDevices.values())); + await this.nodeStorage.set('DeviceIdentifiers', Array.from(this.storedDevices.values())); } private async loadStoredDevices(): Promise { @@ -484,9 +518,7 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { return false; } const storedDevices = await this.nodeStorage.get('DeviceIdentifiers', []); - for (const device of storedDevices) { - this.storedDevices.set(device.id, device); - } + for (const device of storedDevices) this.storedDevices.set(device.id, device); this.log.debug(`Loaded ${this.storedDevices.size} discovered Shelly devices from the storage`); return true; } @@ -580,22 +612,22 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { shellyDevice.log.error(`shellyCommandHandler error: componentName not found for shelly device ${dn}${shellyDevice?.id}${er}`); return false; } - const switchComponent = shellyDevice?.getComponent(componentName) as ShellySwitchComponent; - if (!switchComponent) { + const lightComponent = shellyDevice?.getComponent(componentName) as ShellyLightComponent; + if (!lightComponent) { shellyDevice.log.error(`shellyCommandHandler error: component ${componentName} not found for shelly device ${dn}${shellyDevice?.id}${er}`); return false; } // Send On() Off() Toggle() command - if (command === 'On') switchComponent.On(); - else if (command === 'Off') switchComponent.Off(); - else if (command === 'Toggle') switchComponent.Toggle(); + if (command === 'On') lightComponent.On(); + else if (command === 'Off') lightComponent.Off(); + else if (command === 'Toggle') lightComponent.Toggle(); if (command === 'On' || command === 'Off' || command === 'Toggle') shellyDevice.log.info(`Command ${hk}${componentName}${nf}:${command}() for shelly device ${idn}${shellyDevice?.id}${rs}${nf}`); // Send Level() command if (command === 'Level' && level !== null && level !== undefined) { const shellyLevel = Math.max(Math.min(Math.round((level / 254) * 100), 100), 1); - switchComponent?.Level(shellyLevel); + lightComponent?.Level(shellyLevel); shellyDevice.log.info(`Command ${hk}${componentName}${nf}:Level(${YELLOW}${shellyLevel}${nf}) for shelly device ${idn}${shellyDevice?.id}${rs}${nf}`); } @@ -604,7 +636,7 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { color.r = Math.max(Math.min(color.r, 255), 0); color.g = Math.max(Math.min(color.g, 255), 0); color.b = Math.max(Math.min(color.b, 255), 0); - switchComponent?.ColorRGB(color.r, color.g, color.b); + lightComponent?.ColorRGB(color.r, color.g, color.b); shellyDevice.log.info( `Command ${hk}${componentName}${nf}:ColorRGB(${YELLOW}${color.r}${nf}, ${YELLOW}${color.g}${nf}, ${YELLOW}${color.b}${nf}) for shelly device ${idn}${shellyDevice?.id}${rs}${nf}`, ); @@ -766,21 +798,24 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { } // Update energy from main components (gen 2 devices send power total inside the component not with meter) if ( - shellyComponent.name === 'Light' || - shellyComponent.name === 'Relay' || - shellyComponent.name === 'Switch' || - shellyComponent.name === 'Cover' || - shellyComponent.name === 'Roller' + this.config.exposePowerMeter === 'evehistory' && + (shellyComponent.name === 'Light' || + shellyComponent.name === 'Relay' || + shellyComponent.name === 'Switch' || + shellyComponent.name === 'Cover' || + shellyComponent.name === 'Roller' || + shellyComponent.name === 'PowerMeter') ) { if (property === 'power' || property === 'apower') { const cluster = endpoint.getClusterServer(EveHistoryCluster.with(EveHistory.Feature.EveEnergy)); cluster?.setConsumptionAttribute(value as number); if (cluster) shellyDevice.log.info(`${db}Update endpoint ${or}${endpoint.number}${db} attribute ${hk}EveHistory-consumption${db} ${YELLOW}${value as number}${db}`); + // Calculate current from power and voltage const voltage = shellyComponent.getValue('voltage') as number; if (voltage) { const current = (value as number) / voltage; cluster?.setCurrentAttribute(current as number); - shellyDevice.log.info(`${db}Update endpoint ${or}${endpoint.number}${db} attribute ${hk}EveHistory-current${db} ${YELLOW}${current as number}${db}`); + if (cluster) shellyDevice.log.info(`${db}Update endpoint ${or}${endpoint.number}${db} attribute ${hk}EveHistory-current${db} ${YELLOW}${current as number}${db}`); } } if (property === 'total') { @@ -806,11 +841,19 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { const cluster = endpoint.getClusterServer(EveHistoryCluster.with(EveHistory.Feature.EveEnergy)); cluster?.setCurrentAttribute(value as number); if (cluster) shellyDevice.log.info(`${db}Update endpoint ${or}${endpoint.number}${db} attribute ${hk}EveHistory-current${db} ${YELLOW}${value as number}${db}`); + // Calculate power from current and voltage + const voltage = shellyComponent.getValue('voltage') as number; + if (voltage) { + const power = (value as number) * voltage; + cluster?.setConsumptionAttribute(power as number); + if (cluster) shellyDevice.log.info(`${db}Update endpoint ${or}${endpoint.number}${db} attribute ${hk}EveHistory-consumption${db} ${YELLOW}${power as number}${db}`); + } } } + /* // Update energy from PowerMeter - if (shellyComponent.name === 'PowerMeter') { + if (this.config.exposePowerMeter === 'evehistory' && shellyComponent.name === 'PowerMeter') { if (property === 'power' || property === 'apower') { const cluster = endpoint.getClusterServer(EveHistoryCluster.with(EveHistory.Feature.EveEnergy)); cluster?.setConsumptionAttribute(value as number); @@ -841,6 +884,7 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { ); } } + */ } private validateWhiteBlackList(entityName: string) { diff --git a/src/shellyComponent.ts b/src/shellyComponent.ts index aaca0d9..dc41f40 100644 --- a/src/shellyComponent.ts +++ b/src/shellyComponent.ts @@ -41,8 +41,6 @@ interface SwitchComponent { On(): void; Off(): void; Toggle(): void; - Level(level: number): void; - ColorRGB(red: number, green: number, blue: number): void; } interface CoverComponent { @@ -58,14 +56,12 @@ export type ShellySwitchComponent = ShellyComponent & SwitchComponent; export type ShellyCoverComponent = ShellyComponent & CoverComponent; -type ShellyComponentType = ShellyComponent & Partial & Partial & Partial; - function isLightComponent(name: string): name is 'Light' { return ['Light'].includes(name); } -function isSwitchComponent(name: string): name is 'Light' | 'Relay' | 'Switch' { - return ['Light', 'Relay', 'Switch'].includes(name); +function isSwitchComponent(name: string): name is 'Relay' | 'Switch' { + return ['Relay', 'Switch'].includes(name); } function isCoverComponent(name: string): name is 'Cover' | 'Roller' { @@ -90,35 +86,38 @@ export class ShellyComponent extends EventEmitter { this.addProperty(new ShellyProperty(this, prop, data[prop] as ShellyDataType)); // Add a state property for Light, Relay, and Switch components - if (isSwitchComponent(name) && (prop === 'ison' || prop === 'output')) this.addProperty(new ShellyProperty(this, 'state', data[prop])); + if (isSwitchComponent(name) || (isLightComponent(name) && (prop === 'ison' || prop === 'output'))) this.addProperty(new ShellyProperty(this, 'state', data[prop])); // Add a brightness property for Light, Relay, and Switch components if (isLightComponent(name) && prop === 'gain') this.addProperty(new ShellyProperty(this, 'brightness', data[prop])); } // Extend the class prototype to include the Switch Relay Light methods dynamically - if (isSwitchComponent(name)) { + if (isSwitchComponent(name) || isLightComponent(name)) { // console.log('Component:', this); - (this as ShellyComponentType).On = function () { + (this as unknown as ShellySwitchComponent).On = function () { this.setValue('state', true); if (device.gen === 1) ShellyDevice.fetch(device.log, device.host, `${id.slice(0, id.indexOf(':'))}/${this.index}`, { turn: 'on' }); if (device.gen !== 1) ShellyDevice.fetch(device.log, device.host, `${this.name}.Set`, { id: this.index, on: true }); }; - (this as ShellyComponentType).Off = function () { + (this as unknown as ShellySwitchComponent).Off = function () { this.setValue('state', false); if (device.gen === 1) ShellyDevice.fetch(device.log, device.host, `${id.slice(0, id.indexOf(':'))}/${this.index}`, { turn: 'off' }); if (device.gen !== 1) ShellyDevice.fetch(device.log, device.host, `${this.name}.Set`, { id: this.index, on: false }); }; - (this as ShellyComponentType).Toggle = function () { + (this as unknown as ShellySwitchComponent).Toggle = function () { const currentState = this.getValue('state'); this.setValue('state', !currentState); if (device.gen === 1) ShellyDevice.fetch(device.log, device.host, `${id.slice(0, id.indexOf(':'))}/${this.index}`, { turn: 'toggle' }); if (device.gen !== 1) ShellyDevice.fetch(device.log, device.host, `${this.name}.Toggle`, { id: this.index }); }; + } - (this as ShellyComponentType).Level = function (level: number) { + // Extend the class prototype to include the Light methods dynamically + if (isLightComponent(name)) { + (this as unknown as ShellyLightComponent).Level = function (level: number) { if (!this.hasProperty('brightness')) return; const adjustedLevel = Math.min(Math.max(Math.round(level), 0), 100); this.setValue('brightness', adjustedLevel); @@ -128,7 +127,7 @@ export class ShellyComponent extends EventEmitter { if (device.gen !== 1) ShellyDevice.fetch(device.log, device.host, `${this.name}.Set`, { id: this.index, brightness: adjustedLevel }); }; - (this as ShellyComponentType).ColorRGB = function (red: number, green: number, blue: number) { + (this as unknown as ShellyLightComponent).ColorRGB = function (red: number, green: number, blue: number) { if (!this.hasProperty('red') || !this.hasProperty('green') || !this.hasProperty('blue')) return; red = Math.min(Math.max(Math.round(red), 0), 255); green = Math.min(Math.max(Math.round(green), 0), 255); diff --git a/src/shellyDevice.ts b/src/shellyDevice.ts index eeced7f..38f5d4a 100644 --- a/src/shellyDevice.ts +++ b/src/shellyDevice.ts @@ -80,7 +80,6 @@ export class ShellyDevice extends EventEmitter { this.colorUpdateTimeout = undefined; if (this.colorCommandTimeout) clearInterval(this.colorCommandTimeout); this.colorCommandTimeout = undefined; - this.lastseen = 0; if (this.lastseenInterval) clearInterval(this.lastseenInterval); this.lastseenInterval = undefined; this.lastseen = 0; @@ -286,7 +285,7 @@ export class ShellyDevice extends EventEmitter { log.info(`Fetching update for device ${hk}${device.id}${nf} host ${zb}${device.host}${nf}.`); device.fetchUpdate(); // We don't await for the update to complete } else { - // log.debug(`Device ${hk}${device.id}${db} host ${zb}${device.host}${db} has been seen the last time: ${CYAN}${lastSeenDateString}${db}.`); + log.debug(`Device ${hk}${device.id}${db} host ${zb}${device.host}${db} has been seen the last time: ${CYAN}${lastSeenDateString}${db}.`); device.online = true; device.emit('online'); } @@ -309,7 +308,6 @@ export class ShellyDevice extends EventEmitter { device.wsClient.on('update', (message) => { if (shelly.debug) log.info(`WebSocket update from device ${hk}${device.id}${nf} host ${zb}${device.host}${nf}`); device.update(message); - device.lastseen = Date.now(); }); } @@ -434,11 +432,10 @@ export class ShellyDevice extends EventEmitter { // http://192.168.1.218/rpc/Switch.Set?id=0&on=false // http://192.168.1.218/rpc/Switch.Toggle?id=0 - // await ShellyDevice.fetch('192.168.1.217', 'rpc/Switch.Toggle', { id: 0 }); static async fetch(log: AnsiLogger, host: string, service: string, params: Record = {}): Promise { // MOCK: Fetch device data from file if host is a json file if (host.endsWith('.json')) { - log.warn(`Fetching device payloads from file ${host}: service ${service} params ${JSON.stringify(params)}`); + log.warn(`Fetching mock device payloads from file ${host}: service ${service} params ${JSON.stringify(params)}`); try { const data = await fs.readFile(host, 'utf8'); const deviceData = JSON.parse(data); diff --git a/src/wsClient.ts b/src/wsClient.ts index 49be49e..5277036 100644 --- a/src/wsClient.ts +++ b/src/wsClient.ts @@ -274,32 +274,34 @@ export class WsClient extends EventEmitter { this.log.debug(`Stopped ws client for Shelly device on address ${this.wsHost}`); } } + /* +// Start the WebSocket client with the following command: node dist/wsClient.js startWsClient if (process.argv.includes('startWsClient')) { - const wsClient = new WsClient('192.168.1.221', 'tango'); - wsClient.start(true); + const wsClient1 = new WsClient('192.168.1.217', 'tango'); + wsClient1.start(true); - const wsClient2 = new WsClient('192.168.1.217', 'tango'); + const wsClient2 = new WsClient('192.168.1.218', 'tango'); wsClient2.start(true); setTimeout(() => { - wsClient.sendRequest('Switch.Set', { id: 0, on: true }); + wsClient1.sendRequest('Switch.Set', { id: 0, on: true }); }, 5000); setTimeout(() => { - wsClient.sendRequest('Switch.Set', { id: 0, on: false }); + wsClient1.sendRequest('Switch.Set', { id: 0, on: false }); }, 10000); setTimeout(() => { - wsClient.sendRequest('Shelly.GetComponents', {}); + wsClient1.sendRequest('Shelly.GetComponents', {}); }, 15000); setTimeout(() => { - wsClient.sendRequest('Shelly.ListMethods', {}); + wsClient1.sendRequest('Shelly.ListMethods', {}); }, 20000); process.on('SIGINT', async function () { - wsClient.stop(); + wsClient1.stop(); wsClient2.stop(); }); }