diff --git a/CHANGELOG.md b/CHANGELOG.md index b4d2c4d..2e7cb3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ 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. +## [1.0.4] - 2024-10-17 + +### Added + +- [shelly]: Added support for shellyht (Shelly H&T Gen 1) with firmware v1.14.0-gcb84623 and added Jest test (components: Temperature, Humidity and Battery). Verified and tested by Tamer. +- [shelly]: Added mdns Jest test for shellyht (Shelly H&T Gen 1) with firmware v1.14.0-gcb84623. + +### Changed + +- [package]: Updated dependencies. + + + Buy me a coffee + + ## [1.0.3] - 2024-10-15 ### Added @@ -12,7 +27,7 @@ If you like this project and find it useful, please consider giving it a star on ### Changed -- [BTHome]: Changed required version of matterbridge to 1.5.9. +- [plugin]: Changed required version of matterbridge to 1.5.9. - [package]: Updated dependencies. diff --git a/matterbridge-shelly.schema.json b/matterbridge-shelly.schema.json index 70259ba..69f1b60 100644 --- a/matterbridge-shelly.schema.json +++ b/matterbridge-shelly.schema.json @@ -24,11 +24,7 @@ "exposeSwitch": { "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" - ], + "enum": ["switch", "light", "outlet"], "default": "outlet" }, "switchList": { @@ -55,12 +51,7 @@ "exposeInput": { "description": "Choose how to expose the shelly inputs: disabled, contact, momentary or latching switch (you may need to pair again the controller when changed)", "type": "string", - "enum": [ - "disabled", - "contact", - "momentary", - "latching" - ], + "enum": ["disabled", "contact", "momentary", "latching"], "default": "disabled" }, "inputContactList": { @@ -87,10 +78,7 @@ "exposeInputEvent": { "description": "Choose weather to expose the shelly input events: momentary or disabled (you may need to pair again the controller when changed)", "type": "string", - "enum": [ - "momentary", - "disabled" - ], + "enum": ["momentary", "disabled"], "default": "disabled" }, "inputEventList": { @@ -103,10 +91,7 @@ "exposePowerMeter": { "description": "Choose how to expose the shelly power meters: disabled, matter13 (will use Matter 1.3 electricalSensor)", "type": "string", - "enum": [ - "disabled", - "matter13" - ], + "enum": ["disabled", "matter13"], "default": "disabled" }, "blackList": { @@ -202,4 +187,4 @@ "default": false } } -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 281e27d..8ee67cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "matterbridge-shelly", - "version": "1.0.3", + "version": "1.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "matterbridge-shelly", - "version": "1.0.3", + "version": "1.0.4", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -1966,9 +1966,9 @@ } }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz", + "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", "dev": true, "license": "MIT", "peer": true, @@ -2376,9 +2376,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001668", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001668.tgz", - "integrity": "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==", + "version": "1.0.30001669", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz", + "integrity": "sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==", "dev": true, "funding": [ { @@ -2699,9 +2699,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.38", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.38.tgz", - "integrity": "sha512-VbeVexmZ1IFh+5EfrYz1I0HTzHVIlJa112UEWhciPyeOcKJGeTv6N8WnG4wsQB81DGCaVEGhpSb6o6a8WYFXXg==", + "version": "1.5.40", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.40.tgz", + "integrity": "sha512-LYm78o6if4zTasnYclgQzxEcgMoIcybWOhkATWepN95uwVVWV0/IW10v+2sIeHE+bIYWipLneTftVyQm45UY7g==", "dev": true, "license": "ISC" }, @@ -4983,9 +4983,9 @@ } }, "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, @@ -5837,9 +5837,9 @@ } }, "node_modules/tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==", "dev": true, "license": "0BSD" }, diff --git a/package.json b/package.json index cce81b5..43b91c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matterbridge-shelly", - "version": "1.0.3", + "version": "1.0.4", "description": "Matterbridge shelly plugin", "author": "https://github.com/Luligu", "license": "Apache-2.0", @@ -127,4 +127,4 @@ "typescript": "5.6.3", "typescript-eslint": "8.9.0" } -} \ No newline at end of file +} diff --git a/rock-s0/INSTALL.md b/rock-s0/INSTALL.md index 7943fa1..5327168 100644 --- a/rock-s0/INSTALL.md +++ b/rock-s0/INSTALL.md @@ -38,6 +38,7 @@ then Localization, Change Timezone. ``` rsetup ``` + then User Settings, Change Hostname then check /etc/hosts @@ -52,7 +53,7 @@ set the hostname of the device (change rock-s0 with the new hostname you set bef 127.0.1.1 rock-s0 -# Samba without password +# Samba without password (optional) Copy the file smb.conf to /etc/samba/smb.conf @@ -111,6 +112,10 @@ sudo apt install cockpit btop -y sudo apt upgrade ``` +# Install matterbridge cockpit plugin + +copy the four files from cockpit directory to "\usr\share\cockpit\matterbridge" + # Prevent the journal logs to grow ``` @@ -162,6 +167,7 @@ sudo curl https://raw.githubusercontent.com/Luligu/matterbridge-shelly/dev/rock- ``` change twice the hostname with the new hostname you set before + ``` sudo nano /etc/systemd/system/matterbridge.service ``` @@ -189,4 +195,4 @@ sudo systemctl start matterbridge ``` sudo journalctl -u matterbridge.service -f --output cat -``` \ No newline at end of file +``` diff --git a/rock-s0/cockpit/index.html b/rock-s0/cockpit/index.html new file mode 100644 index 0000000..3baa0ae --- /dev/null +++ b/rock-s0/cockpit/index.html @@ -0,0 +1,27 @@ + + + + + + Matterbridge Management + + + + + + + +
+

Matterbridge Dashboard

+ +
Loading Matterbridge status...
+
Loading Matterbridge current version...
+
Loading Matterbridge latest version...
+
Loading Shelly plugin current version...
+
Loading Shelly plugin latest version...
+

System logs:

+
Fetching logs...
+
+ + + \ No newline at end of file diff --git a/rock-s0/cockpit/manifest.json b/rock-s0/cockpit/manifest.json new file mode 100644 index 0000000..c8b1545 --- /dev/null +++ b/rock-s0/cockpit/manifest.json @@ -0,0 +1,11 @@ +{ + "name": "matterbridge", + "title": "Matterbridge Management", + "version": "1.0", + "menu": { + "index": { + "label": "Matterbridge" + } + }, + "content-security-policy": "default-src 'self'; script-src 'self'; style-src 'self';" +} \ No newline at end of file diff --git a/rock-s0/cockpit/matterbridge.css b/rock-s0/cockpit/matterbridge.css new file mode 100644 index 0000000..e67bb56 --- /dev/null +++ b/rock-s0/cockpit/matterbridge.css @@ -0,0 +1,18 @@ +#content { + font-family: Arial, sans-serif; + padding: 20px; +} + +button { + padding: 5px 10px; + margin: 10px 0; +} + +#status, +#matterbridge-current, +#matterbridge-latest, +#shelly-current, +#shelly-latest, +#logs { + margin: 10px 0; +} \ No newline at end of file diff --git a/rock-s0/cockpit/matterbridge.js b/rock-s0/cockpit/matterbridge.js new file mode 100644 index 0000000..7396280 --- /dev/null +++ b/rock-s0/cockpit/matterbridge.js @@ -0,0 +1,109 @@ +/* eslint-disable no-control-regex */ +/* eslint-disable no-console */ +// Wait for Cockpit to fully initialize +cockpit.transport.wait(function () { + console.log('Matterbridge Cockpit extension loaded'); + + // Fetch and display the Matterbridge status + function fetchStatus() { + cockpit + .spawn(['systemctl', 'is-active', 'matterbridge']) + .then(function (status) { + document.getElementById('status').innerText = `Status: ${status.trim().replace('\n', '')}`; + }) + .catch(function (error) { + console.error('Error fetching Matterbridge status:', error); + document.getElementById('status').innerText = 'Error fetching status.'; + }); + } + + // Fetch and display the Matterbridge current version + function fetchMatterbridgeCurrent() { + cockpit + .spawn(['npm', 'list', '-g', 'matterbridge']) + .then(function (status) { + // Extract the version number using a regular expression + const versionMatch = status.match(/matterbridge@(\d+\.\d+\.\d+)/); + const version = versionMatch ? versionMatch[1] : 'Unknown'; + document.getElementById('matterbridge-current').innerText = `Current version: ${version}`; + }) + .catch(function (error) { + console.error('Error fetching Matterbridge current version:', error); + document.getElementById('matterbridge-current').innerText = 'Error fetching Matterbridge current version.'; + }); + } + + // Fetch and display the Matterbridge latest version + function fetchMatterbridgeLatest() { + cockpit + .spawn(['npm', 'show', 'matterbridge', 'version']) + // cockpit.spawn(["whoami"]) + .then(function (status) { + document.getElementById('matterbridge-latest').innerText = `Latest version: ${status.trim()}`; + }) + .catch(function (error) { + console.error('Error fetching Matterbridge latest version:', error); + document.getElementById('matterbridge-latest').innerText = 'Error fetching Matterbridge latest version.'; + }); + } + + // Fetch and display the Shelly plugin current version + function fetchShellyCurrent() { + cockpit + .spawn(['npm', 'list', '-g', 'matterbridge-shelly']) + .then(function (status) { + // Extract the version number using a regular expression + const versionMatch = status.match(/matterbridge-shelly@(\d+\.\d+\.\d+)/); + const version = versionMatch ? versionMatch[1] : 'Unknown'; + document.getElementById('shelly-current').innerText = `Shelly plugin current version: ${version}`; + }) + .catch(function (error) { + console.error('Error fetching Shelly plugin current version:', error); + document.getElementById('shelly-current').innerText = 'Error fetching Shelly plugin current version.'; + }); + } + + // Fetch and display the Shelly plugin latest version + function fetchShellyLatest() { + cockpit + .spawn(['npm', 'show', 'matterbridge-shelly', 'version']) + // cockpit.spawn(["whoami"]) + .then(function (status) { + document.getElementById('shelly-latest').innerText = `Shelly plugin latest version: ${status.trim()}`; + }) + .catch(function (error) { + console.error('Error fetching Shelly plugin latest version:', error); + document.getElementById('shelly-latest').innerText = 'Error fetching Shelly plugin latest version.'; + }); + } + + // Fetch logs + function fetchLogs() { + cockpit + .spawn(['journalctl', '-u', 'matterbridge', '--no-pager', '-n', '20', '-o', 'cat']) + .then(function (logs) { + // const filteredLogs = logs.split('\n').filter(line => !line.includes('matterbridge.service')).join('\n'); + logs = logs.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, ''); + document.getElementById('logs').innerText = logs; + }) + .catch(function (error) { + console.error('Error fetching logs:', error); + document.getElementById('logs').innerText = 'Error fetching logs.'; + }); + } + + // Reload the Matterbridge configuration + document.getElementById('frontend-button').addEventListener('click', function () { + const hostname = window.location.hostname; + const newUrl = `http://${hostname}:8283`; + window.open(newUrl, '_blank'); + }); + + // Initial fetch of status and logs + fetchStatus(); + fetchMatterbridgeCurrent(); + fetchMatterbridgeLatest(); + fetchShellyCurrent(); + fetchShellyLatest(); + fetchLogs(); +}); diff --git a/rock-s0/systemd/matterbridge.service b/rock-s0/systemd/matterbridge.service index 7dd4be4..bac4f76 100644 --- a/rock-s0/systemd/matterbridge.service +++ b/rock-s0/systemd/matterbridge.service @@ -4,7 +4,7 @@ After=network-online.target [Service] Type=simple -ExecStart=matterbridge -service -passcode 20242025 -discriminator 3840 +ExecStart=matterbridge -service -passcode 20242025 -discriminator 3840 -mdnsinterface end0 WorkingDirectory=/home/rock/Matterbridge StandardOutput=inherit StandardError=inherit @@ -12,6 +12,7 @@ Restart=always RestartSec=10s TimeoutStopSec=30s User=rock +Group=rock [Install] WantedBy=multi-user.target diff --git a/src/coapServer.ts b/src/coapServer.ts index 697d558..158d00f 100644 --- a/src/coapServer.ts +++ b/src/coapServer.ts @@ -416,6 +416,11 @@ export class CoapServer extends EventEmitter { if (s.D === 'inputEventCnt' && b.D.startsWith('sensor')) desc.push({ id: s.I, component: b.D.replace('_', ':').replace('sensor', 'input'), property: 'event_cnt', range: s.R }); // SHBTN-2 + // ht component + if (s.D === 'extTemp' && s.U === 'C' && b.D.startsWith('sensor')) desc.push({ id: s.I, component: 'temperature', property: 'tC', range: s.R }); // SHHT-1 + if (s.D === 'extTemp' && s.U === 'F' && b.D.startsWith('sensor')) desc.push({ id: s.I, component: 'temperature', property: 'tF', range: s.R }); // SHHT-1 + if (s.D === 'humidity' && b.D.startsWith('sensor')) desc.push({ id: s.I, component: 'humidity', property: 'value', range: s.R }); // SHHT-1 + // gas_sensor component if (s.D === 'sensorOp' && b.D.startsWith('sensor')) desc.push({ id: s.I, component: 'gas', property: 'sensor_state', range: s.R }); // SHGS-1 if (s.D === 'gas' && b.D.startsWith('sensor')) desc.push({ id: s.I, component: 'gas', property: 'alarm_state', range: s.R }); // SHGS-1 diff --git a/src/mdnsScanner.test.ts b/src/mdnsScanner.test.ts index 489a085..bc3b5d4 100644 --- a/src/mdnsScanner.test.ts +++ b/src/mdnsScanner.test.ts @@ -175,6 +175,25 @@ describe('Shellies MdnsScanner test', () => { expect(mdns.isScanning).toBeFalsy(); }, 10000); + test('Shelly ht', (done) => { + discoveredDeviceListener.mockClear(); + mdns.on('discovered', discoveredDeviceListener); + mdns.start(undefined, undefined, undefined, true); + expect(mdns.isScanning).toBeTruthy(); + setTimeout(() => { + expect(discoveredDeviceListener).toHaveBeenCalledWith({ id: 'shellyht-CA969F', host: '192.168.68.173', port: 80, gen: 1 }); + expect((mdns as any).discoveredDevices.has('shellyht-CA969F')).toBeTruthy(); + done(); + }, 1000); + (mdns as any).discoveredDevices.clear(); + const data = loadResponse('shellyht-CA969F'); + expect(data).not.toBeUndefined(); + if (!data) return; + (mdns as any).scanner.emit('response', data, { address: '192.168.68.173', family: 'IPv4', port: 5353, size: 501 }); + mdns.stop(); + expect(mdns.isScanning).toBeFalsy(); + }, 10000); + test('Shelly bulbduo', (done) => { discoveredDeviceListener.mockClear(); mdns.on('discovered', discoveredDeviceListener); diff --git a/src/mock/SHHT-1-CA969F.coap.citd.json b/src/mock/SHHT-1-CA969F.coap.citd.json new file mode 100644 index 0000000..85ec501 --- /dev/null +++ b/src/mock/SHHT-1-CA969F.coap.citd.json @@ -0,0 +1,65 @@ +{ + "blk": [ + { + "I": 1, + "D": "sensor_0" + }, + { + "I": 2, + "D": "device" + } + ], + "sen": [ + { + "I": 9103, + "T": "EVC", + "D": "cfgChanged", + "R": "U16", + "L": 2 + }, + { + "I": 3101, + "T": "T", + "D": "extTemp", + "U": "C", + "R": ["-55/125", "999"], + "L": 1 + }, + { + "I": 3102, + "T": "T", + "D": "extTemp", + "U": "F", + "R": ["-67/257", "999"], + "L": 1 + }, + { + "I": 3103, + "T": "H", + "D": "humidity", + "R": ["0/100", "999"], + "L": 1 + }, + { + "I": 3115, + "T": "S", + "D": "sensorError", + "R": "0/1", + "L": 1 + }, + { + "I": 3111, + "T": "B", + "D": "battery", + "R": ["0/100", "-1"], + "L": 2 + }, + { + "I": 9102, + "T": "EV", + "D": "wakeupEvent", + "R": ["battery/button/periodic/poweron/sensor/alarm", "unknown"], + "L": 2 + } + ] +} diff --git a/src/mock/SHHT-1-CA969F.coap.cits.json b/src/mock/SHHT-1-CA969F.coap.cits.json new file mode 100644 index 0000000..f42f2de --- /dev/null +++ b/src/mock/SHHT-1-CA969F.coap.cits.json @@ -0,0 +1,11 @@ +{ + "G": [ + [0, 9103, 0], + [0, 3101, 26.25], + [0, 3102, 79.25], + [0, 3103, 55.5], + [0, 3115, 0], + [0, 3111, 100], + [0, 9102, ["button"]] + ] +} diff --git a/src/mock/shellyblugw-B0B21CFAAD18.json b/src/mock/shellyblugw-B0B21CFAAD18.json index 4c29a1e..c947788 100644 --- a/src/mock/shellyblugw-B0B21CFAAD18.json +++ b/src/mock/shellyblugw-B0B21CFAAD18.json @@ -154,4 +154,4 @@ "cfg_rev": 10, "offset": 0, "total": 0 -} \ No newline at end of file +} diff --git a/src/mock/shellyblugw-B0B21CFAAD18.mdns.json b/src/mock/shellyblugw-B0B21CFAAD18.mdns.json index f15c75c..9fe8489 100644 --- a/src/mock/shellyblugw-B0B21CFAAD18.mdns.json +++ b/src/mock/shellyblugw-B0B21CFAAD18.mdns.json @@ -68,9 +68,7 @@ "ttl": 120, "class": "IN", "flush": false, - "data": [ - "gen=2" - ] + "data": ["gen=2"] }, { "name": "ShellyBluGw-B0B21CFAAD18.local", @@ -99,11 +97,7 @@ "ttl": 120, "class": "IN", "flush": false, - "data": [ - "gen=2", - "app=BluGw", - "ver=1.4.2" - ] + "data": ["gen=2", "app=BluGw", "ver=1.4.2"] }, { "name": "ShellyBluGw-B0B21CFAAD18.local", @@ -114,4 +108,4 @@ "data": "192.168.1.168" } ] -} \ No newline at end of file +} diff --git a/src/mock/shellyht-CA969F.json b/src/mock/shellyht-CA969F.json new file mode 100644 index 0000000..269bf9e --- /dev/null +++ b/src/mock/shellyht-CA969F.json @@ -0,0 +1,167 @@ +{ + "shelly": { + "type": "SHHT-1", + "mac": "485519CA969F", + "auth": false, + "fw": "20230913-112531/v1.14.0-gcb84623", + "discoverable": false, + "sleep_mode": true + }, + "settings": { + "device": { + "type": "SHHT-1", + "mac": "485519CA969F", + "hostname": "shellyht-CA969F", + "sleep_mode": true + }, + "wifi_ap": { + "enabled": false, + "ssid": "shellyht-CA969F", + "key": "" + }, + "wifi_sta": { + "enabled": true, + "ssid": "Tamer Umniah", + "ipv4_method": "dhcp", + "ip": null, + "gw": null, + "mask": null, + "dns": null + }, + "wifi_sta1": { + "enabled": false, + "ssid": null, + "ipv4_method": "dhcp", + "ip": null, + "gw": null, + "mask": null, + "dns": null + }, + "mqtt": { + "enable": false, + "server": "192.168.33.3:1883", + "user": "", + "id": "shellyht-CA969F", + "reconnect_timeout_max": 60, + "reconnect_timeout_min": 2, + "clean_session": true, + "keep_alive": 60, + "max_qos": 0, + "retain": false, + "update_period": 30 + }, + "coiot": { + "enabled": true, + "update_period": 15, + "peer": "" + }, + "sntp": { + "server": "time.google.com", + "enabled": true + }, + "login": { + "enabled": false, + "unprotected": false, + "username": "admin" + }, + "pin_code": "", + "name": "HT Gen1", + "fw": "20230913-112531/v1.14.0-gcb84623", + "pon_wifi_reset": false, + "discoverable": false, + "build_info": { + "build_id": "20230913-112531/v1.14.0-gcb84623", + "build_timestamp": "2023-09-13T11:25:31Z", + "build_version": "1.0" + }, + "cloud": { + "enabled": true, + "connected": false + }, + "timezone": null, + "lat": null, + "lng": null, + "tzautodetect": true, + "tz_utc_offset": 10800, + "tz_dst": false, + "tz_dst_auto": true, + "time": "", + "unixtime": 0, + "debug_enable": false, + "allow_cross_origin": false, + "actions": { + "active": false, + "names": ["report_url", "temp_over_url", "temp_under_url", "hum_over_url", "hum_under_url"] + }, + "sensors": { + "temperature_threshold": 1, + "temperature_unit": "C", + "humidity_threshold": 5 + }, + "sleep_mode": { + "period": 12, + "unit": "h" + }, + "external_power": 0, + "temperature_offset": 0, + "humidity_offset": 0 + }, + "status": { + "wifi_sta": { + "connected": true, + "ssid": "Tamer Umniah", + "ip": "192.168.68.173", + "rssi": -37 + }, + "cloud": { + "enabled": true, + "connected": false + }, + "mqtt": { + "connected": false + }, + "time": "", + "unixtime": 0, + "serial": 1, + "has_update": false, + "mac": "485519CA969F", + "cfg_changed_cnt": 0, + "actions_stats": { + "skipped": 0 + }, + "is_valid": true, + "tmp": { + "value": 31.5, + "units": "C", + "tC": 31.5, + "tF": 88.7, + "is_valid": true + }, + "hum": { + "value": 54.5, + "is_valid": true + }, + "bat": { + "value": 100, + "voltage": 2.96 + }, + "act_reasons": ["button"], + "connect_retries": 0, + "sensor_error": 0, + "update": { + "status": "unknown", + "has_update": false, + "new_version": "", + "old_version": "20230913-112531/v1.14.0-gcb84623" + }, + "ram_total": 52384, + "ram_free": 40704, + "fs_size": 233681, + "fs_free": 156122, + "uptime": 2 + }, + "components": [], + "cfg_rev": 0, + "offset": 0, + "total": 0 +} diff --git a/src/mock/shellyht-CA969F.mdns.json b/src/mock/shellyht-CA969F.mdns.json new file mode 100644 index 0000000..06b613d --- /dev/null +++ b/src/mock/shellyht-CA969F.mdns.json @@ -0,0 +1,76 @@ +{ + "id": 0, + "type": "response", + "flags": 1152, + "flag_qr": true, + "opcode": "QUERY", + "flag_aa": true, + "flag_tc": false, + "flag_rd": false, + "flag_ra": true, + "flag_z": false, + "flag_ad": false, + "flag_cd": false, + "rcode": "NOERROR", + "questions": [], + "answers": [ + { + "name": "_services._dns-sd._udp.local", + "type": "PTR", + "ttl": 4500, + "class": "IN", + "flush": false, + "data": "_http._tcp.local" + }, + { + "name": "_http._tcp.local", + "type": "PTR", + "ttl": 4500, + "class": "IN", + "flush": false, + "data": "shellyht-CA969F._http._tcp.local" + }, + { + "name": "shellyht-CA969F._http._tcp.local", + "type": "SRV", + "ttl": 120, + "class": "IN", + "flush": true, + "data": { + "priority": 0, + "weight": 0, + "port": 80, + "target": "shellyht-CA969F.local" + } + }, + { + "name": "shellyht-CA969F._http._tcp.local", + "type": "TXT", + "ttl": 120, + "class": "IN", + "flush": true, + "data": ["id=shellyht-CA969F", "arch=esp8266", "app=ht-sensor", "fw_version=1.0", "fw_id=20230913-112531/v1.14.0-gcb84623", "discoverable=false"] + }, + { + "name": "shellyht-CA969F.local", + "type": "A", + "ttl": 120, + "class": "IN", + "flush": true, + "data": "192.168.68.173" + }, + { + "name": "shellyht-CA969F.local", + "type": "NSEC", + "ttl": 120, + "class": "IN", + "flush": true, + "data": { + "nextDomain": "shellyht-CA969F.local", + "rrtypes": ["A"] + } + } + ], + "authorities": [], + "additionals": [] +} diff --git a/src/mock/shellywalldisplay-00082261E102.json b/src/mock/shellywalldisplay-00082261E102.json index 6f84b24..d97a856 100644 --- a/src/mock/shellywalldisplay-00082261E102.json +++ b/src/mock/shellywalldisplay-00082261E102.json @@ -258,4 +258,4 @@ "cfg_rev": 0, "offset": 0, "total": 0 -} \ No newline at end of file +} diff --git a/src/mock/shellywalldisplay-00082261E102.mdns.json b/src/mock/shellywalldisplay-00082261E102.mdns.json index 52fc3df..3012a14 100644 --- a/src/mock/shellywalldisplay-00082261E102.mdns.json +++ b/src/mock/shellywalldisplay-00082261E102.mdns.json @@ -20,12 +20,7 @@ "ttl": 4500, "class": "IN", "flush": true, - "data": [ - "app=WallDisplay", - "gen=2", - "ver=2.2.1", - "discoverable=true" - ] + "data": ["app=WallDisplay", "gen=2", "ver=2.2.1", "discoverable=true"] }, { "name": "_services._dns-sd._udp.local", @@ -49,12 +44,7 @@ "ttl": 4500, "class": "IN", "flush": true, - "data": [ - "app=WallDisplay", - "gen=2", - "ver=2.2.1", - "discoverable=true" - ] + "data": ["app=WallDisplay", "gen=2", "ver=2.2.1", "discoverable=true"] }, { "name": "_services._dns-sd._udp.local", @@ -173,10 +163,7 @@ "flush": true, "data": { "nextDomain": "ShellyWallDisplay-00082261E102._shelly._tcp.local", - "rrtypes": [ - "TXT", - "SRV" - ] + "rrtypes": ["TXT", "SRV"] } }, { @@ -187,10 +174,7 @@ "flush": true, "data": { "nextDomain": "ShellyWallDisplay-00082261E102._http._tcp.local", - "rrtypes": [ - "TXT", - "SRV" - ] + "rrtypes": ["TXT", "SRV"] } }, { @@ -201,9 +185,7 @@ "flush": true, "data": { "nextDomain": "167.1.168.192.in-addr.arpa", - "rrtypes": [ - "PTR" - ] + "rrtypes": ["PTR"] } }, { @@ -214,9 +196,7 @@ "flush": true, "data": { "nextDomain": "2.0.1.E.1.6.E.F.F.F.2.2.8.0.2.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.E.F.ip6.arpa", - "rrtypes": [ - "PTR" - ] + "rrtypes": ["PTR"] } }, { @@ -227,9 +207,7 @@ "flush": true, "data": { "nextDomain": "2.0.1.E.1.6.E.F.F.F.2.2.8.0.2.0.6.4.7.0.9.3.9.4.8.F.B.C.8.7.D.F.ip6.arpa", - "rrtypes": [ - "PTR" - ] + "rrtypes": ["PTR"] } }, { @@ -240,9 +218,7 @@ "flush": true, "data": { "nextDomain": "A.0.E.1.3.B.9.9.2.0.2.9.C.B.0.2.6.4.7.0.9.3.9.4.8.F.B.C.8.7.D.F.ip6.arpa", - "rrtypes": [ - "PTR" - ] + "rrtypes": ["PTR"] } }, { @@ -253,11 +229,8 @@ "flush": true, "data": { "nextDomain": "ShellyWallDisplay-00082261E102.local", - "rrtypes": [ - "A", - "AAAA" - ] + "rrtypes": ["A", "AAAA"] } } ] -} \ No newline at end of file +} diff --git a/src/mock/shellywalldisplay-00082261E102.thermostat.json b/src/mock/shellywalldisplay-00082261E102.thermostat.json index 23ed7e7..088a1e9 100644 --- a/src/mock/shellywalldisplay-00082261E102.thermostat.json +++ b/src/mock/shellywalldisplay-00082261E102.thermostat.json @@ -280,4 +280,4 @@ "cfg_rev": 0, "offset": 0, "total": 0 -} \ No newline at end of file +} diff --git a/src/platform.ts b/src/platform.ts index debde1c..eed316e 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -845,8 +845,7 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { tempComponent.on('update', (component: string, property: string, value: ShellyDataType) => { this.shellyUpdateHandler(mbDevice, device, component, property, value); }); - } - if (tempComponent?.hasProperty('tC') && isValidNumber(tempComponent.getValue('tC'), -100, 100)) { + } else if (tempComponent?.hasProperty('tC') && isValidNumber(tempComponent.getValue('tC'), -100, 100)) { const child = mbDevice.addChildDeviceTypeWithClusterServer(key, [DeviceTypes.TEMPERATURE_SENSOR], []); const matterTemp = Math.min(Math.max(Math.round((tempComponent.getValue('tC') as number) * 100), -10000), 10000); child.addClusterServer(mbDevice.getDefaultTemperatureMeasurementClusterServer(matterTemp)); @@ -857,9 +856,18 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { } } else if (component.name === 'Humidity' && config.exposeHumidity !== 'disabled') { const humidityComponent = device.getComponent(key); + if (humidityComponent?.hasProperty('value') && isValidNumber(humidityComponent.getValue('value'), 0, 100)) { + const child = mbDevice.addChildDeviceTypeWithClusterServer(key, [DeviceTypes.HUMIDITY_SENSOR], []); + const matterHumidity = Math.min(Math.max(Math.round((humidityComponent.getValue('value') as number) * 100), 0), 10000); + child.addClusterServer(mbDevice.getDefaultRelativeHumidityMeasurementClusterServer(matterHumidity)); + // Add event handler + humidityComponent.on('update', (component: string, property: string, value: ShellyDataType) => { + this.shellyUpdateHandler(mbDevice, device, component, property, value); + }); + } if (humidityComponent?.hasProperty('rh') && isValidNumber(humidityComponent.getValue('rh'), 0, 100)) { const child = mbDevice.addChildDeviceTypeWithClusterServer(key, [DeviceTypes.HUMIDITY_SENSOR], []); - const matterHumidity = Math.min(Math.max(Math.round((humidityComponent.getValue('rh') as number) * 100), -10000), 10000); + const matterHumidity = Math.min(Math.max(Math.round((humidityComponent.getValue('rh') as number) * 100), 0), 10000); child.addClusterServer(mbDevice.getDefaultRelativeHumidityMeasurementClusterServer(matterHumidity)); // Add event handler humidityComponent.on('update', (component: string, property: string, value: ShellyDataType) => { @@ -1707,8 +1715,8 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { if (shellyComponent.name === 'Temperature' && (property === 'value' || property === 'tC') && isValidNumber(value, -100, +100)) { matterbridgeDevice.setAttribute(TemperatureMeasurementCluster.id, 'measuredValue', value * 100, shellyDevice.log, endpoint); } - // Update for Humidity when has rh - if (shellyComponent.name === 'Humidity' && property === 'rh' && isValidNumber(value, 0, 100)) { + // Update for Humidity when has value or rh + if (shellyComponent.name === 'Humidity' && (property === 'value' || property === 'rh') && isValidNumber(value, 0, 100)) { matterbridgeDevice.setAttribute(RelativeHumidityMeasurementCluster.id, 'measuredValue', value * 100, shellyDevice.log, endpoint); } // Update for Illuminance when has lux diff --git a/src/shellyDevice.mock.test.ts b/src/shellyDevice.mock.test.ts index 49b9ab5..06ac9ea 100644 --- a/src/shellyDevice.mock.test.ts +++ b/src/shellyDevice.mock.test.ts @@ -514,6 +514,51 @@ describe('Shelly devices test', () => { if (device) device.destroy(); }); + test('Create a gen 1 shellyht device', async () => { + id = 'shellyht-CA969F'; + log.logName = id; + + device = await ShellyDevice.create(shelly, log, path.join('src', 'mock', id + '.json')); + expect(device).not.toBeUndefined(); + if (!device) return; + expect(device.host).toBe(path.join('src', 'mock', id + '.json')); + expect(device.model).toBe('SHHT-1'); + expect(device.mac).toBe('485519CA969F'); + expect(device.id).toBe(id); + expect(device.firmware).toBe(firmwareGen1); + expect(device.auth).toBe(false); + expect(device.gen).toBe(1); + expect(device.profile).toBe(undefined); + expect(device.name).toBe('HT Gen1'); + expect(device.hasUpdate).toBe(false); + expect(device.lastseen).not.toBe(0); + expect(device.online).toBe(true); + expect(device.cached).toBe(false); + expect(device.sleepMode).toBe(true); + + expect(device.components.length).toBe(10); + expect(device.getComponentNames()).toStrictEqual(['WiFi', 'MQTT', 'CoIoT', 'Sntp', 'Cloud', 'Temperature', 'Humidity', 'Battery']); + expect(device.getComponentIds()).toStrictEqual(['wifi_ap', 'wifi_sta', 'wifi_sta1', 'mqtt', 'coiot', 'sntp', 'cloud', 'temperature', 'humidity', 'battery']); + + expect(device.getComponent('temperature')?.getValue('value')).toBe(31.5); + expect(device.getComponent('temperature')?.getValue('units')).toBe('C'); + expect(device.getComponent('temperature')?.getValue('tC')).toBe(31.5); + expect(device.getComponent('temperature')?.getValue('tF')).toBe(88.7); + expect(device.getComponent('temperature')?.getValue('is_valid')).toBe(true); + expect(device.getComponent('humidity')?.getValue('value')).toBe(54.5); + expect(device.getComponent('humidity')?.getValue('is_valid')).toBe(true); + expect(device.getComponent('battery')?.getValue('level')).toBe(100); + expect(device.getComponent('battery')?.getValue('voltage')).toBe(2.96); + expect(device.getComponent('battery')?.getValue('charging')).toBe(undefined); + + expect(device.getComponent('sys')?.getValue('temperature')).toBe(undefined); + expect(device.getComponent('sys')?.getValue('overtemperature')).toBe(undefined); + + expect(await device.fetchUpdate()).not.toBeNull(); + + if (device) device.destroy(); + }); + test('Create a gen 1 shellygas device', async () => { id = 'shellygas-7C87CEBCECE4'; log.logName = id; diff --git a/src/shellyDevice.realgen1.test.ts b/src/shellyDevice.realgen1.test.ts index 29b60da..8ffca06 100644 --- a/src/shellyDevice.realgen1.test.ts +++ b/src/shellyDevice.realgen1.test.ts @@ -16,7 +16,6 @@ describe('Shellies', () => { let device: ShellyDevice | undefined; const firmwareGen1 = 'v1.14.0-gcb84623'; - const firmwareGen2 = '1.4.2-gc2639da'; const address = '30:f6:ef:69:2b:c5'; beforeAll(async () => { @@ -100,6 +99,54 @@ describe('Shellies', () => { }, 20000); }); + describe('test real gen 1 shelly1l-E8DB84AAD781 241', () => { + if (getMacAddress() !== address) return; + + test('Create a gen 1 shelly1 device and send commands', async () => { + device = await ShellyDevice.create(shelly, log, '192.168.1.241'); + expect(device).not.toBeUndefined(); + if (!device) return; + shelly.addDevice(device); + + expect(device.host).toBe('192.168.1.241'); + expect(device.mac).toBe('E8DB84AAD781'); + expect(device.profile).toBe(undefined); + expect(device.model).toBe('SHSW-L'); + expect(device.id).toBe('shelly1l-E8DB84AAD781'); + expect(device.firmware).toBe(firmwareGen1); + expect(device.auth).toBe(false); + expect(device.gen).toBe(1); + expect(device.hasUpdate).toBe(false); + expect(device.username).toBe('admin'); + expect(device.password).toBe('tango'); + + await device.fetchUpdate(); + + await device.saveDevicePayloads('temp'); + + const component = device.getComponent('relay:0'); + expect(component).not.toBeUndefined(); + + // prettier-ignore + if (isSwitchComponent(component)) { + component.On(); + await waiter('On', () => { return component.getValue('state') === true; }, true); + + component.Off(); + await waiter('Off', () => { return component.getValue('state') === false; }, true); + + component.Toggle(); + await waiter('Toggle', () => { return component.getValue('state') === true; }, true); + + component.Off(); + await waiter('Off', () => { return component.getValue('state') === false; }, true); + } + + shelly.removeDevice(device); + device.destroy(); + }, 20000); + }); + describe('test real gen 1 shellydimmer2 119 with auth', () => { if (getMacAddress() !== address) return; @@ -264,4 +311,328 @@ describe('Shellies', () => { device.destroy(); }, 30000); }); + + describe('test real gen 1 shellyrgbw2-EC64C9D3FFEF mode color 226', () => { + if (getMacAddress() !== address) return; + + test('Create a gen 1 shellyrgbw2 device and send commands', async () => { + device = await ShellyDevice.create(shelly, log, '192.168.1.226'); + expect(device).not.toBeUndefined(); + if (!device) return; + shelly.addDevice(device); + + expect(device.host).toBe('192.168.1.226'); + expect(device.mac).toBe('EC64C9D3FFEF'); + expect(device.profile).toBe('color'); + expect(device.model).toBe('SHRGBW2'); + expect(device.id).toBe('shellyrgbw2-EC64C9D3FFEF'); + expect(device.firmware).toBe(firmwareGen1); + expect(device.auth).toBe(false); + expect(device.gen).toBe(1); + expect(device.hasUpdate).toBe(false); + expect(device.username).toBe('admin'); + expect(device.password).toBe('tango'); + + await device.fetchUpdate(); + + await device.saveDevicePayloads('temp'); + + expect(device.getComponentNames()).toStrictEqual(['WiFi', 'MQTT', 'CoIoT', 'Sntp', 'Cloud', 'Light', 'Sys', 'PowerMeter', 'Input']); + + const component = device.getComponent('light:0'); + expect(component).not.toBeUndefined(); + + // prettier-ignore + if (isLightComponent(component)) { + component.On(); + await waiter('On', () => { return component.getValue('state') === true; }, true); + + component.Level(40); + await waiter('Level(40)', () => { return component.getValue('brightness') === 40; }, true); + + component.ColorRGB(255, 0, 0); + await waiter('ColorRGB(255, 0, 0)', () => { return component.getValue('red') === 255 && component.getValue('green') === 0 && component.getValue('blue') === 0; }, true); + + component.Off(); + await waiter('Off', () => { return component.getValue('state') === false; }, true); + + component.Toggle(); + await waiter('Toggle', () => { return component.getValue('state') === true; }, true); + + component.Level(60); + await waiter('Level(60)', () => { return component.getValue('brightness') === 60; }, true); + + component.ColorRGB(0, 255, 0); + await waiter('ColorRGB(0, 255, 0)', () => { return component.getValue('red') === 0 && component.getValue('green') === 255 && component.getValue('blue') === 0; }, true); + + component.Off(); + await waiter('Off 2', () => { return component.getValue('state') === false; }, true); + } + + shelly.removeDevice(device); + device.destroy(); + }, 30000); + }); + + describe('test real gen 1 shellyrgbw2-EC64C9D199AD mode white 152', () => { + if (getMacAddress() !== address) return; + + test('Create a gen 1 shellyrgbw2 device and send commands', async () => { + device = await ShellyDevice.create(shelly, log, '192.168.1.152'); + expect(device).not.toBeUndefined(); + if (!device) return; + shelly.addDevice(device); + + expect(device.host).toBe('192.168.1.152'); + expect(device.mac).toBe('EC64C9D199AD'); + expect(device.profile).toBe('white'); + expect(device.model).toBe('SHRGBW2'); + expect(device.id).toBe('shellyrgbw2-EC64C9D199AD'); + expect(device.firmware).toBe(firmwareGen1); + expect(device.auth).toBe(false); + expect(device.gen).toBe(1); + expect(device.hasUpdate).toBe(false); + expect(device.username).toBe('admin'); + expect(device.password).toBe('tango'); + + await device.fetchUpdate(); + + await device.saveDevicePayloads('temp'); + + expect(device.getComponentNames()).toStrictEqual(['WiFi', 'MQTT', 'CoIoT', 'Sntp', 'Cloud', 'Light', 'Sys', 'PowerMeter', 'Input']); + expect(device.getComponentIds()).toStrictEqual([ + 'wifi_ap', + 'wifi_sta', + 'wifi_sta1', + 'mqtt', + 'coiot', + 'sntp', + 'cloud', + 'light:0', + 'light:1', + 'light:2', + 'light:3', + 'sys', + 'meter:0', + 'meter:1', + 'meter:2', + 'meter:3', + 'input:0', + ]); + + const component = device.getComponent('light:0'); + expect(component).not.toBeUndefined(); + + // prettier-ignore + if (isLightComponent(component)) { + component.On(); + await waiter('On', () => { return component.getValue('state') === true; }, true); + + component.Level(40); + await waiter('Level(40)', () => { return component.getValue('brightness') === 40; }, true); + + component.Off(); + await waiter('Off', () => { return component.getValue('state') === false; }, true); + + component.Toggle(); + await waiter('Toggle', () => { return component.getValue('state') === true; }, true); + + component.Level(60); + await waiter('Level(60)', () => { return component.getValue('brightness') === 60; }, true); + + component.Off(); + await waiter('Off 2', () => { return component.getValue('state') === false; }, true); + } + + shelly.removeDevice(device); + device.destroy(); + }, 30000); + }); + + describe('test real gen 1 shellyem3-485519D732F4 249', () => { + if (getMacAddress() !== address) return; + + test('Create a gen 1 shellyem3 device and send commands', async () => { + device = await ShellyDevice.create(shelly, log, '192.168.1.249'); + expect(device).not.toBeUndefined(); + if (!device) return; + shelly.addDevice(device); + + expect(device.host).toBe('192.168.1.249'); + expect(device.mac).toBe('485519D732F4'); + expect(device.profile).toBe(undefined); + expect(device.model).toBe('SHEM-3'); + expect(device.id).toBe('shellyem3-485519D732F4'); + expect(device.firmware).toBe(firmwareGen1); + expect(device.auth).toBe(false); + expect(device.gen).toBe(1); + expect(device.hasUpdate).toBe(false); + expect(device.username).toBe('admin'); + expect(device.password).toBe('tango'); + + await device.fetchUpdate(); + + await device.saveDevicePayloads('temp'); + + const component = device.getComponent('relay:0'); + expect(component).not.toBeUndefined(); + + // prettier-ignore + if (isSwitchComponent(component)) { + component.On(); + await waiter('On', () => { return component.getValue('state') === true; }, true); + + component.Off(); + await waiter('Off', () => { return component.getValue('state') === false; }, true); + + component.Toggle(); + await waiter('Toggle', () => { return component.getValue('state') === true; }, true); + + component.Off(); + await waiter('Off', () => { return component.getValue('state') === false; }, true); + } + + shelly.removeDevice(device); + device.destroy(); + }, 20000); + }); + + describe('test real gen 1 shellyswitch25-3494547BF36C 236', () => { + if (getMacAddress() !== address) return; + + test('Create a gen 1 shelly1 device and send commands', async () => { + device = await ShellyDevice.create(shelly, log, '192.168.1.236'); + expect(device).not.toBeUndefined(); + if (!device) return; + shelly.addDevice(device); + + expect(device.host).toBe('192.168.1.236'); + expect(device.mac).toBe('3494547BF36C'); + expect(device.profile).toBe('switch'); + expect(device.model).toBe('SHSW-25'); + expect(device.id).toBe('shellyswitch25-3494547BF36C'); + expect(device.firmware).toBe(firmwareGen1); + expect(device.auth).toBe(false); + expect(device.gen).toBe(1); + expect(device.hasUpdate).toBe(false); + expect(device.username).toBe('admin'); + expect(device.password).toBe('tango'); + + await device.fetchUpdate(); + + await device.saveDevicePayloads('temp'); + + expect(device.getComponentNames()).toStrictEqual(['WiFi', 'MQTT', 'CoIoT', 'Sntp', 'Cloud', 'Relay', 'PowerMeter', 'Input', 'Sys']); + expect(device.getComponentIds()).toStrictEqual([ + 'wifi_ap', + 'wifi_sta', + 'wifi_sta1', + 'mqtt', + 'coiot', + 'sntp', + 'cloud', + 'relay:0', + 'relay:1', + 'meter:0', + 'meter:1', + 'input:0', + 'input:1', + 'sys', + ]); + + const component0 = device.getComponent('relay:0'); + expect(component0).not.toBeUndefined(); + + // prettier-ignore + if (isSwitchComponent(component0)) { + component0.On(); + await waiter('On', () => { return component0.getValue('state') === true; }, true); + + component0.Off(); + await waiter('Off', () => { return component0.getValue('state') === false; }, true); + + component0.Toggle(); + await waiter('Toggle', () => { return component0.getValue('state') === true; }, true); + + component0.Off(); + await waiter('Off', () => { return component0.getValue('state') === false; }, true); + } + + const component1 = device.getComponent('relay:1'); + expect(component1).not.toBeUndefined(); + + // prettier-ignore + if (isSwitchComponent(component1)) { + component1.On(); + await waiter('On', () => { return component1.getValue('state') === true; }, true); + + component1.Off(); + await waiter('Off', () => { return component1.getValue('state') === false; }, true); + + component1.Toggle(); + await waiter('Toggle', () => { return component1.getValue('state') === true; }, true); + + component1.Off(); + await waiter('Off', () => { return component1.getValue('state') === false; }, true); + } + + shelly.removeDevice(device); + device.destroy(); + }, 20000); + }); + + describe('test real gen 1 shellyswitch25-3494546BBF7E 222', () => { + if (getMacAddress() !== address) return; + + test('Create a gen 1 shelly1 device and send commands', async () => { + device = await ShellyDevice.create(shelly, log, '192.168.1.222'); + expect(device).not.toBeUndefined(); + if (!device) return; + shelly.addDevice(device); + + expect(device.host).toBe('192.168.1.222'); + expect(device.mac).toBe('3494546BBF7E'); + expect(device.profile).toBe('cover'); + expect(device.model).toBe('SHSW-25'); + expect(device.id).toBe('shellyswitch25-3494546BBF7E'); + expect(device.firmware).toBe(firmwareGen1); + expect(device.auth).toBe(false); + expect(device.gen).toBe(1); + expect(device.hasUpdate).toBe(false); + expect(device.username).toBe('admin'); + expect(device.password).toBe('tango'); + + await device.fetchUpdate(); + + await device.saveDevicePayloads('temp'); + + expect(device.getComponentNames()).toStrictEqual(['WiFi', 'MQTT', 'CoIoT', 'Sntp', 'Cloud', 'Roller', 'PowerMeter', 'Input', 'Sys']); + expect(device.getComponentIds()).toStrictEqual(['wifi_ap', 'wifi_sta', 'wifi_sta1', 'mqtt', 'coiot', 'sntp', 'cloud', 'roller:0', 'meter:0', 'input:0', 'input:1', 'sys']); + + const component0 = device.getComponent('roller:0'); + expect(component0).not.toBeUndefined(); + + // prettier-ignore + if (isCoverComponent(component0)) { + component0.Open(); + await waiter('Open', () => { return component0.getValue('state') === 'stop'; }, true, 30000); + await waiter('Open', () => { return component0.getValue('current_pos') === 100; }, true, 30000); + + component0.Close(); + await waiter('Close', () => { return component0.getValue('state') === 'stop'; }, true, 30000); + await waiter('Close', () => { return component0.getValue('current_pos') === 0; }, true, 30000); + + component0.GoToPosition(50); + await waiter('GoToPosition(50)', () => { return component0.getValue('state') === 'stop'; }, true, 30000); + await waiter('GoToPosition(50)', () => { return component0.getValue('current_pos') === 50; }, true, 30000); + + component0.Open(); + await waiter('Open', () => { return component0.getValue('state') === 'stop'; }, true, 30000); + await waiter('Open', () => { return component0.getValue('current_pos') === 100; }, true, 30000); + } + + shelly.removeDevice(device); + device.destroy(); + }, 60000); + }); }); diff --git a/src/shellyDevice.ts b/src/shellyDevice.ts index 6d91ce5..b52ebe9 100644 --- a/src/shellyDevice.ts +++ b/src/shellyDevice.ts @@ -21,7 +21,7 @@ * limitations under the License. * */ -import { AnsiLogger, LogLevel, BLUE, CYAN, GREEN, GREY, MAGENTA, RESET, db, debugStringify, er, hk, nf, wr, zb, rs, YELLOW, idn, nt } from 'matterbridge/logger'; +import { AnsiLogger, LogLevel, BLUE, CYAN, GREEN, GREY, MAGENTA, RESET, db, debugStringify, er, hk, nf, wr, zb, rs, YELLOW, idn, nt, rk } from 'matterbridge/logger'; import { getIpv4InterfaceAddress, isValidNumber, isValidObject, isValidString } from 'matterbridge/utils'; import { EventEmitter } from 'events'; import fetch, { RequestInit } from 'node-fetch'; @@ -186,16 +186,6 @@ export class ShellyDevice extends EventEmitter { if (isCoverComponent(component)) return component as unknown as T; return component as T; } - /* - getComponent(id: string): ShellyComponent | ShellyLightComponent | ShellySwitchComponent | ShellyCoverComponent | undefined { - const component = this._components.get(id); - if (!component) return undefined; - else if (component.isSwitchComponent()) return component as ShellySwitchComponent; - else if (component.isLightComponent()) return component as ShellyLightComponent; - else if (component.isCoverComponent()) return component as ShellyCoverComponent; - else return component as ShellyComponent; - } - */ /** * Retrieves an array of component IDs. @@ -553,6 +543,7 @@ export class ShellyDevice extends EventEmitter { if (key === 'tmp' && statusPayload.temperature === undefined && statusPayload.overtemperature === undefined) { device.addComponent(new ShellyComponent(device, 'temperature', 'Temperature')); } + if (key === 'hum') device.addComponent(new ShellyComponent(device, 'humidity', 'Humidity')); if (key === 'voltage') device.addComponent(new ShellyComponent(device, 'sys', 'Sys')); if (key === 'mode') device.addComponent(new ShellyComponent(device, 'sys', 'Sys')); if (key === 'bat') device.addComponent(new ShellyComponent(device, 'battery', 'Battery')); @@ -830,7 +821,7 @@ export class ShellyDevice extends EventEmitter { this.log.debug(`*Unknown bthomesensor ${event.component} with event: ${debugStringify(event)}${rs}`); } } else if (isValidObject(event) && isValidString(event.event) && isValidString(event.component)) { - this.log.debug(`Device ${hk}${this.id}${db} has event ${YELLOW}${event.event}${db} from component ${idn}${event.component}${rs}${db}`); + this.log.debug(`Device ${hk}${this.id}${db} has event ${YELLOW}${event.event}${db} from component ${idn}${event.component}${rs}${db}${rk}`); this.getComponent(event.component)?.emit('event', event.component, event.event); } else { this.log.debug(`*Unknown event:${rs}\n`, event); @@ -972,7 +963,13 @@ export class ShellyDevice extends EventEmitter { if (key === 'tmp') { if (data.temperature === undefined && data.overtemperature === undefined) this.updateComponent('temperature', data[key] as ShellyData); const sensor = data.tmp as ShellyData; - if (sensor.is_valid === true && sensor.value !== undefined) this.getComponent('temperature')?.setValue('value', sensor.value); + if (sensor.is_valid === true && sensor.units === 'C' && isValidNumber(sensor.tC, -55, 125)) this.getComponent('temperature')?.setValue('value', sensor.tC); + if (sensor.is_valid === true && sensor.units === 'F' && isValidNumber(sensor.tF, -67, 257)) this.getComponent('temperature')?.setValue('value', sensor.tF); + } + if (key === 'hum') { + this.updateComponent('humidity', data[key] as ShellyData); + const sensor = data.hum as ShellyData; + if (sensor.is_valid === true && isValidNumber(sensor.value, 0, 100)) this.getComponent('humidity')?.setValue('value', sensor.value); } if (key === 'temperature') { if (data[key] !== null && data[key] !== undefined && typeof data[key] === 'number') this.getComponent('sys')?.setValue('temperature', data[key]); @@ -1207,7 +1204,8 @@ export class ShellyDevice extends EventEmitter { const controller = new AbortController(); const fetchTimeout = setTimeout(() => { controller.abort(); - }, 10000); + log.debug(`***Aborting fetch device ${host}: service ${service} params ${JSON.stringify(params)}`); + }, 20000); const gen = /^[^A-Z]*$/.test(service) ? 1 : 2; const url = gen === 1 ? `http://${host}/${service}` : `http://${host}/rpc`;