diff --git a/CHANGELOG.md b/CHANGELOG.md
index d7c7427..1c5fdc5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,19 @@
-# Changelog
+# 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.
+
+
+
+
+
## [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
-
+
## [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();
});
}