diff --git a/CHANGELOG.md b/CHANGELOG.md index d7c7427..1c5fdc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,19 @@ -# Changelog +# Matterbridge Logo   Matterbridge shelly plugin changelog 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 @@ -16,7 +26,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/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/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.'); } } 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(); }); }