diff --git a/.github/workflows/build-matterbridge-plugin.yml b/.github/workflows/build-matterbridge-plugin.yml index 92ad2df..7080f77 100644 --- a/.github/workflows/build-matterbridge-plugin.yml +++ b/.github/workflows/build-matterbridge-plugin.yml @@ -21,6 +21,12 @@ jobs: with: node-version: ${{ matrix.node-version }} + - name: Clean cache + run: npm cache clean --force + + - name: Install latest npm + run: npm install -g npm@latest + - name: Verify Node.js version run: node -v @@ -42,14 +48,3 @@ jobs: - name: Build the project run: npm run build - # - name: List, audit, fix outdated dependencies and build again - # run: | - # npm uninstall matterbridge - # npm list --outdated - # npm audit || true # ignore failures - # npm audit fix || true - # npm list --outdated - # npm install -g matterbridge - # npm install - # npm run build - diff --git a/.github/workflows/publish-matterbridge-plugin.yml b/.github/workflows/publish-matterbridge-plugin.yml index 6b6ab2b..5236b8e 100644 --- a/.github/workflows/publish-matterbridge-plugin.yml +++ b/.github/workflows/publish-matterbridge-plugin.yml @@ -18,6 +18,12 @@ jobs: node-version: '20.x' registry-url: 'https://registry.npmjs.org' + - name: Clean cache + run: npm cache clean --force + + - name: Install latest npm + run: npm install -g npm@latest + - name: Verify Node.js version run: node -v diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cb666b..96843d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,21 @@ If you like this project and find it useful, please consider giving it a star on - Unless you are using docker (in that case all is already updated when you pull the image), please update Matterbridge to 1.5.4 to work with matterbridge-shelly >= 0.9.5. This is a one time issue due to the update to matter.js 0.10.0. +## [0.9.9] - 2024-09-17 + +### Changed + +- [matterbridge]: Removed Matterbridge deprecated method to get the child endpoints. +- [package]: Updated dependencies. + +### Fixed + +- [shelly]: Fixed the bug in configure when postfix is used. + + + Buy me a coffee + + ## [0.9.8] - 2024-09-13 ### Added diff --git a/package-lock.json b/package-lock.json index eb237bb..765c862 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "matterbridge-shelly", - "version": "0.9.8", + "version": "0.9.9-dev.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "matterbridge-shelly", - "version": "0.9.8", + "version": "0.9.9-dev.1", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -21,9 +21,9 @@ "devDependencies": { "@eslint/js": "9.10.0", "@types/eslint__js": "8.42.3", - "@types/jest": "29.5.12", + "@types/jest": "29.5.13", "@types/multicast-dns": "7.2.4", - "@types/node": "22.5.4", + "@types/node": "22.5.5", "@types/ws": "8.5.12", "eslint-config-prettier": "9.1.0", "eslint-plugin-jest": "28.8.3", @@ -1617,9 +1617,9 @@ } }, "node_modules/@types/jest": { - "version": "29.5.12", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", - "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "version": "29.5.13", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.13.tgz", + "integrity": "sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==", "dev": true, "license": "MIT", "dependencies": { @@ -1646,9 +1646,9 @@ } }, "node_modules/@types/node": { - "version": "22.5.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz", - "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", + "version": "22.5.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz", + "integrity": "sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==", "license": "MIT", "dependencies": { "undici-types": "~6.19.2" diff --git a/package.json b/package.json index ef46299..2fc90d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matterbridge-shelly", - "version": "0.9.8", + "version": "0.9.9", "description": "Matterbridge shelly plugin", "author": "https://github.com/Luligu", "license": "Apache-2.0", @@ -64,8 +64,8 @@ "test:utils": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js src/utils.spec.ts --coverage", "test:shellyProperty": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js src/shellyProperty.test.ts --coverage", "test:shellyComponent": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js src/shellyComponent.test.ts --coverage", - "test:mock": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js src/shellyDevice.mock.test.ts --coverage", - "test:real": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js src/shellyDevice.real.test.ts --coverage", + "test:mock": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js src/shellyDevice.mock.test.ts --runInBand --coverage", + "test:real": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js src/shellyDevice.real.test.ts --runInBand --coverage", "test:shelly": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js src/shelly.test.ts --coverage", "lint": "eslint --max-warnings=0 .", "lint:fix": "eslint --fix --max-warnings=0 .", @@ -75,7 +75,7 @@ "cleanBuild": "npm run clean && tsc", "deepClean": "rimraf tsconfig.tsbuildinfo package-lock.json ./dist ./node_modules", "deepCleanRebuild": "npm run deepClean && npm install && npm run build", - "prepublishOnly": "npm run lint && npm run cleanBuild && npm shrinkwrap", + "prepublishOnly": "npm run lint && npm run cleanBuild && npm shrinkwrap --omit=dev", "checkDependencies": "npx npm-check-updates", "updateDependencies": "npx npm-check-updates -u && npm install & npm run cleanBuild", "preversion": "npm run build && npm run lint", @@ -110,9 +110,9 @@ "devDependencies": { "@eslint/js": "9.10.0", "@types/eslint__js": "8.42.3", - "@types/jest": "29.5.12", + "@types/jest": "29.5.13", "@types/multicast-dns": "7.2.4", - "@types/node": "22.5.4", + "@types/node": "22.5.5", "@types/ws": "8.5.12", "eslint-config-prettier": "9.1.0", "eslint-plugin-jest": "28.8.3", diff --git a/src/platform.ts b/src/platform.ts index 8a05dd6..e2ef3eb 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -462,6 +462,9 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { mbDevice.addCommandHandler('off', async (data) => { this.shellyLightCommandHandler(mbDevice, data.endpoint.number, device, 'Off', false); }); + mbDevice.addCommandHandler('toggle', async (data) => { + this.shellyLightCommandHandler(mbDevice, data.endpoint.number, device, 'Toggle', false); + }); // Add event handler switchComponent.on('update', (component: string, property: string, value: ShellyDataType) => { @@ -802,12 +805,20 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { override async onConfigure() { this.log.info(`Configuring platform ${idn}${this.config.name}${rs}${nf}`); this.bridgedDevices.forEach(async (mbDevice) => { - if (!mbDevice.serialNumber) return; - this.log.info(`Configuring device ${dn}${mbDevice.deviceName}${nf} shelly ${hk}${mbDevice.serialNumber}${nf}`); - const shellyDevice = this.shelly.getDevice(mbDevice.serialNumber); - if (!shellyDevice) return; + if (!mbDevice.serialNumber) { + this.log.error(`Shelly device ${dn}${mbDevice.deviceName}${er} has no serial number`); + return; + } + const serial = isValidString(this.config.postfix, 1, 3) ? mbDevice.serialNumber.replace('-' + this.config.postfix, '') : mbDevice.serialNumber; + this.log.info(`Configuring device ${dn}${mbDevice.deviceName}${nf} shelly ${hk}${serial}${nf}`); + const shellyDevice = this.shelly.getDevice(serial); + if (!shellyDevice) { + this.log.error(`Shelly device with serial number ${hk}${serial}${er} not found`); + return; + } mbDevice.getChildEndpoints().forEach(async (childEndpoint) => { - const label = mbDevice.getEndpointLabel(childEndpoint.number); + // const label = mbDevice.getEndpointLabel(childEndpoint.number); + const label = childEndpoint.uniqueStorageKey; // Configure the cluster OnOff attribute onOff if (label?.startsWith('switch') || label?.startsWith('relay') || label?.startsWith('light') || label?.startsWith('rgb')) { const switchComponent = shellyDevice.getComponent(label) as ShellySwitchComponent; @@ -1026,7 +1037,8 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { return false; } // Get the Shelly switch component - const componentName = matterbridgeDevice.getEndpointLabel(endpointNumber); + // const componentName = matterbridgeDevice.getEndpointLabel(endpointNumber); + const componentName = endpoint.uniqueStorageKey; if (!componentName) { shellyDevice.log.error(`shellyCommandHandler error: componentName not found for shelly device ${dn}${shellyDevice?.id}${er}`); return false; @@ -1081,7 +1093,8 @@ export class ShellyPlatform extends MatterbridgeDynamicPlatform { return false; } // Get the Shelly cover component - const componentName = matterbridgeDevice.getEndpointLabel(endpointNumber); + // const componentName = matterbridgeDevice.getEndpointLabel(endpointNumber); + const componentName = endpoint.uniqueStorageKey; if (!componentName) { shellyDevice.log.error(`shellyCoverCommandHandler error: endpointName not found for shelly device ${dn}${shellyDevice?.id}${er}`); return false; diff --git a/src/shellyDevice.real.test.ts b/src/shellyDevice.real.test.ts index b43c4ed..32397a6 100644 --- a/src/shellyDevice.real.test.ts +++ b/src/shellyDevice.real.test.ts @@ -1,24 +1,24 @@ -/* eslint-disable no-console */ -/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable jest/no-conditional-expect */ /* eslint-disable @typescript-eslint/no-unused-vars */ import { Shelly } from './shelly.js'; import { ShellyDevice } from './shellyDevice.js'; // import { ShellyCoverComponent, ShellySwitchComponent } from './shellyComponent'; -import { AnsiLogger, TimestampFormat } from 'matterbridge/logger'; -import { getMacAddress } from 'matterbridge/utils'; +import { AnsiLogger, LogLevel, TimestampFormat } from 'matterbridge/logger'; +import { getMacAddress, wait, waiter } from 'matterbridge/utils'; import { jest } from '@jest/globals'; -import { ShellyCoverComponent, ShellySwitchComponent } from './shellyComponent.js'; +import { isCoverComponent, isLightComponent, isSwitchComponent, ShellyCoverComponent, ShellySwitchComponent } from './shellyComponent.js'; describe('Shellies', () => { let consoleLogSpy: jest.SpiedFunction; - const log = new AnsiLogger({ logName: 'shellyDeviceTest', logTimestampFormat: TimestampFormat.TIME_MILLIS, logDebug: false }); + const log = new AnsiLogger({ logName: 'ShellyDeviceRealTest', logTimestampFormat: TimestampFormat.TIME_MILLIS, logLevel: LogLevel.DEBUG }); const shelly = new Shelly(log, 'admin', 'tango'); - const firmwareGen1 = '1.14.0-gcb84623'; + const firmwareGen1 = 'v1.14.0-gcb84623'; const firmwareGen2 = '1.4.2-gc2639da'; beforeAll(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any consoleLogSpy = jest.spyOn(console, 'log').mockImplementation((...args: any[]) => { // console.error(`Mocked console.log: ${args}`); }); @@ -33,7 +33,7 @@ describe('Shellies', () => { }); afterAll(() => { - // + shelly.destroy(); }); test('Create AnsiLogger and Shelly', () => { @@ -53,44 +53,25 @@ describe('Shellies', () => { }, 300000); }); - describe('create real gen 1 shellydimmer2 219', () => { - if (getMacAddress() !== '30:f6:ef:69:2b:c5') return; - test('create a gen 1 device and update', async () => { - const device = await ShellyDevice.create(shelly, log, '192.168.1.219'); - if (!device) return; - expect(device).not.toBeUndefined(); - expect(device?.gen).toBe(1); - expect(device?.host).toBe('192.168.1.219'); - expect(device?.model).toBe('SHDM-2'); - expect(device?.id).toBe('shellydimmer2-98CDAC0D01BB'); - expect(device?.firmware).toBe('v1.14.0-gcb84623'); - expect(device?.auth).toBe(true); - - await device.fetchUpdate(); - - device.destroy(); - }); - }); - describe('create real gen 2 shellyplus1pm 217', () => { if (getMacAddress() !== '30:f6:ef:69:2b:c5') return; test('create a gen 2 device and update', async () => { const device = await ShellyDevice.create(shelly, log, '192.168.1.217'); if (!device) return; expect(device).not.toBeUndefined(); - expect(device?.gen).toBe(2); - expect(device?.host).toBe('192.168.1.217'); - expect(device?.model).toBe('SNSW-001P16EU'); - expect(device?.id).toBe('shellyplus1pm-441793D69718'); - expect(device?.firmware).toBe(firmwareGen2); - expect(device?.auth).toBe(false); + expect(device.gen).toBe(2); + expect(device.host).toBe('192.168.1.217'); + expect(device.model).toBe('SNSW-001P16EU'); + expect(device.id).toBe('shellyplus1pm-441793D69718'); + expect(device.firmware).toBe(firmwareGen2); + expect(device.auth).toBe(false); await device.fetchUpdate(); device.destroy(); }); - test('send legacy command to a gen 2 device and update', async () => { + test('send legacy command to a gen 2 shellyplus1pm device and update', async () => { const device = await ShellyDevice.create(shelly, log, '192.168.1.217'); expect(device).not.toBeUndefined(); if (!device) return; @@ -110,7 +91,7 @@ describe('Shellies', () => { expect(outputProp).not.toBeUndefined(); const state = stateProp?.value; const output = outputProp?.value; - console.log(`state: ${state} output: ${output}`); + // console.log(`state: ${state} output: ${output}`); expect(state === output).toBeTruthy(); const response = await ShellyDevice.fetch(shelly, log, '192.168.1.217', 'relay/0', { 'turn': 'toggle' }); expect(response).not.toBeUndefined(); @@ -119,15 +100,15 @@ describe('Shellies', () => { expect(stateProp2).not.toBeUndefined(); const outputProp2 = component.getProperty('output'); expect(outputProp2).not.toBeUndefined(); - console.log(`state2: ${stateProp2?.value} output2: ${outputProp2?.value}`); + // console.log(`state2: ${stateProp2?.value} output2: ${outputProp2?.value}`); expect(stateProp2?.value === outputProp2?.value).toBeTruthy(); - console.log(`state: ${state} state2: ${stateProp2?.value}`); + // console.log(`state: ${state} state2: ${stateProp2?.value}`); expect(state === stateProp2?.value).toBeTruthy(); expect(stateProp?.value === stateProp2?.value).toBeTruthy(); device.destroy(); }); - test('send rpc command to a gen 2 device', async () => { + test('send rpc command to a gen 2 shellyplus1pm device', async () => { const device = await ShellyDevice.create(shelly, log, '192.168.1.217'); expect(device).not.toBeUndefined(); if (!device) return; @@ -148,10 +129,12 @@ describe('Shellies', () => { expect(response).not.toBeUndefined(); await device.fetchUpdate(); response = await ShellyDevice.fetch(shelly, log, '192.168.1.217', 'Switch.Set', { 'id': 0, 'on': false }); + expect(response).not.toBeUndefined(); + await device.fetchUpdate(); device.destroy(); }); - test('send legacy command relay to a gen 2 device', async () => { + test('send legacy command relay to a gen 2 shellyplus1pm device', async () => { const device = await ShellyDevice.create(shelly, log, '192.168.1.217'); expect(device).not.toBeUndefined(); if (!device) return; @@ -174,7 +157,7 @@ describe('Shellies', () => { device.destroy(); }); - test('execute On() Off() Toggle() for a gen 2 device', async () => { + test('execute On() Off() Toggle() for a gen 2 shellyplus1pm device', async () => { const device = await ShellyDevice.create(shelly, log, '192.168.1.217'); expect(device).not.toBeUndefined(); if (!device) return; @@ -196,7 +179,7 @@ describe('Shellies', () => { device.destroy(); }); - test('execute Open() CLose() Stop() for a gen 2 device', async () => { + test('execute Open() Close() Stop() for a gen 2 device', async () => { const device = await ShellyDevice.create(shelly, log, '192.168.1.218'); expect(device).not.toBeUndefined(); if (!device) return; @@ -242,16 +225,16 @@ describe('Shellies', () => { const outputP = component.getProperty('output'); const state = stateP?.value; const output = outputP?.value; - console.log(`state: ${state} output: ${output}`); + // console.log(`state: ${state} output: ${output}`); expect(state === output).toBeTruthy(); const res = await ShellyDevice.fetch(shelly, log, '192.168.1.218', 'relay/1', { 'turn': 'toggle' }); expect(res).not.toBeUndefined(); await device.fetchUpdate(); const state2 = component.getProperty('state'); const output2 = component.getProperty('output'); - console.log(`state2: ${state2?.value} output2: ${output2?.value}`); + // console.log(`state2: ${state2?.value} output2: ${output2?.value}`); expect(state2?.value === output2?.value).toBeTruthy(); - console.log(`state: ${state} state2: ${state2?.value}`); + // console.log(`state: ${state} state2: ${state2?.value}`); expect(state === state2?.value).toBeTruthy(); expect(stateP?.value === state2?.value).toBeTruthy(); device.destroy(); @@ -261,25 +244,335 @@ describe('Shellies', () => { const device = await ShellyDevice.create(shelly, log, '192.168.1.218'); expect(device).not.toBeUndefined(); if (!device) return; - console.log('send wrong command to a gen 2 device and update'); + // console.log('send wrong command to a gen 2 device and update'); let res = await ShellyDevice.fetch(shelly, log, '192.168.1.218', 'relay/5', { 'turn': 'toggle' }); expect(res).toBeNull(); - console.log('send wrong command to a gen 2 device and update'); + // console.log('send wrong command to a gen 2 device and update'); res = await ShellyDevice.fetch(shelly, log, '192.168.1.218', 'relay/0', { 'turn': 'toggle' }); expect(res).toBeNull(); device.destroy(); }); }); - describe('create real gen 3 shelly1minig3 221 with auth', () => { + describe('test real gen 1 shellydimmer2 119 with auth', () => { if (getMacAddress() !== '30:f6:ef:69:2b:c5') return; - test('send command to a gen 2 device and update', async () => { + test('Create a gen 1 shellydimmer2 device and update', async () => { + const device = await ShellyDevice.create(shelly, log, '192.168.1.219'); + expect(device).not.toBeUndefined(); + if (!device) return; + expect(device.host).toBe('192.168.1.219'); + expect(device.model).toBe('SHDM-2'); + expect(device.id).toBe('shellydimmer2-98CDAC0D01BB'); + expect(device.firmware).toBe(firmwareGen1); + expect(device.auth).toBe(true); + expect(device.gen).toBe(1); + expect(device.username).toBe('admin'); + expect(device.password).toBe('tango'); + + await device.fetchUpdate(); + + await device.saveDevicePayloads('temp'); + + device.destroy(); + }, 60000); + + test('send command to a gen 1 shellydimmer2 device', async () => { + const device = await ShellyDevice.create(shelly, log, '192.168.1.219'); + expect(device).not.toBeUndefined(); + if (!device) return; + + const component = device.getComponent('light:0'); + expect(component).not.toBeUndefined(); + + if (isLightComponent(component)) { + component.On(); + await waiter( + 'On', + () => { + return component.getValue('state') === true; + }, + false, + 10000, + ); + + component.Level(100); + await waiter( + 'Level(100)', + () => { + return component.getValue('brightness') === 100; + }, + false, + 10000, + ); + + component.Off(); + await waiter( + 'Off', + () => { + return component.getValue('state') === false; + }, + false, + 10000, + ); + + component.Level(50); + await waiter( + 'Level(50)', + () => { + return component.getValue('brightness') === 50; + }, + false, + 10000, + ); + + component.Toggle(); + await waiter( + 'Toggle', + () => { + return component.getValue('state') === true; + }, + false, + 10000, + ); + + component.Off(); + await waiter( + 'Off', + () => { + return component.getValue('state') === false; + }, + false, + 10000, + ); + + component.Level(1); + await waiter( + 'Level(1)', + () => { + return component.getValue('brightness') === 1; + }, + false, + 10000, + ); + } + + device.destroy(); + }, 60000); + }); + + describe('test real gen 2 shellyplus1pm 217 with auth', () => { + if (getMacAddress() !== '30:f6:ef:69:2b:c5') return; + + test('create a gen 2 shellyplus1pm device and update', async () => { + const device = await ShellyDevice.create(shelly, log, '192.168.1.217'); + expect(device).not.toBeUndefined(); + if (!device) return; + expect(device.gen).toBe(2); + expect(device.host).toBe('192.168.1.217'); + expect(device.model).toBe('SNSW-001P16EU'); + expect(device.mac).toBe('441793D69718'); + expect(device.id).toBe('shellyplus1pm-441793D69718'); + expect(device.firmware).toBe(firmwareGen2); + expect(device.auth).toBe(false); + + await device.fetchUpdate(); + + await device.saveDevicePayloads('temp'); + + device.destroy(); + }, 60000); + + test('send command to a gen 2 shellyplus1pm device', async () => { + const device = await ShellyDevice.create(shelly, log, '192.168.1.217'); + expect(device).not.toBeUndefined(); + if (!device) return; + + const component = device.getComponent('switch:0'); + expect(component).not.toBeUndefined(); + + if (isSwitchComponent(component)) { + component.On(); + await waiter( + 'On', + () => { + return component.getValue('state') === true; + }, + false, + 10000, + ); + + component.Off(); + await waiter( + 'Off', + () => { + return component.getValue('state') === false; + }, + false, + 10000, + ); + + component.Toggle(); + await waiter( + 'Toggle', + () => { + return component.getValue('state') === true; + }, + false, + 10000, + ); + + component.Off(); + await waiter( + 'Off', + () => { + return component.getValue('state') === false; + }, + false, + 10000, + ); + } + + device.destroy(); + }, 60000); + }); + + describe('test real gen 2 shellyplus2pm 218 with auth', () => { + if (getMacAddress() !== '30:f6:ef:69:2b:c5') return; + + test('create a gen 2 shellyplus2pm device and update', async () => { + const device = await ShellyDevice.create(shelly, log, '192.168.1.218'); + expect(device).not.toBeUndefined(); + if (!device) return; + expect(device.gen).toBe(2); + expect(device.host).toBe('192.168.1.218'); + expect(device.model).toBe('SNSW-102P16EU'); + expect(device.mac).toBe('5443B23D81F8'); + expect(device.id).toBe('shellyplus2pm-5443B23D81F8'); + expect(device.firmware).toBe(firmwareGen2); + expect(device.auth).toBe(false); + + await device.fetchUpdate(); + + await device.saveDevicePayloads('temp'); + + device.destroy(); + }, 60000); + + test('send command to a gen 2 shellyplus2pm device', async () => { + const device = await ShellyDevice.create(shelly, log, '192.168.1.218'); + expect(device).not.toBeUndefined(); + if (!device) return; + + const cover = device.getComponent('cover:0'); + expect(cover).not.toBeUndefined(); + + if (isCoverComponent(cover)) { + cover.Open(); + await waiter( + 'Open()', + () => { + return cover.getValue('state') === 'opening'; + }, + false, + 20000, + ); + await waiter( + 'Open()', + () => { + return cover.getValue('state') === 'stopped'; + }, + false, + 20000, + ); + await wait(2000); + await device.fetchUpdate(); + expect(cover.getValue('source')).toMatch(/^(limit_switch|timeout)$/); // 'limit_switch' if not stopped for timeout + expect(cover.getValue('state')).toBe('stopped'); // 'open' if not stopped for timeout + expect(cover.getValue('last_direction')).toBe('open'); + expect(cover.getValue('current_pos')).toBe(100); + + cover.Stop(); + await waiter( + 'Stop()', + () => { + return cover.getValue('state') === 'stopped'; + }, + false, + 20000, + ); + await wait(2000); + await device.fetchUpdate(); + expect(cover.getValue('state')).toBe('stopped'); + expect(cover.getValue('last_direction')).toBe('open'); + expect(cover.getValue('current_pos')).toBe(100); + + cover.Close(); + await waiter( + 'Close()', + () => { + return cover.getValue('state') === 'closing'; + }, + false, + 20000, + ); + await waiter( + 'Close()', + () => { + return cover.getValue('state') === 'stopped'; + }, + false, + 20000, + ); + await wait(2000); + await device.fetchUpdate(); + expect(cover.getValue('source')).toBe('timeout'); // 'limit_switch' if not stopped for timeout + expect(cover.getValue('state')).toBe('stopped'); // 'open' if not stopped for timeout + expect(cover.getValue('last_direction')).toBe('close'); + expect(cover.getValue('current_pos')).toBe(0); + + cover.GoToPosition(10); + await waiter( + 'GoToPosition(10)', + () => { + return cover.getValue('state') === 'opening'; + }, + false, + 20000, + ); + await waiter( + 'GoToPosition(10)', + () => { + return cover.getValue('state') === 'stopped'; + }, + false, + 20000, + ); + await wait(2000); + await device.fetchUpdate(); + expect(cover.getValue('source')).toBe('timeout'); + expect(cover.getValue('state')).toBe('stopped'); + expect(cover.getValue('last_direction')).toBe('open'); + expect(cover.getValue('current_pos')).toBe(10); + + // Close for next test + cover.Close(); + await wait(2000); + } + device.destroy(); + }, 120000); + }); + + describe('test real gen 3 shelly1minig3 221 with auth', () => { + if (getMacAddress() !== '30:f6:ef:69:2b:c5') return; + + test('create a gen 3 shelly1minig3 device and update', async () => { const device = await ShellyDevice.create(shelly, log, '192.168.1.221'); expect(device).not.toBeUndefined(); if (!device) return; expect(device.gen).toBe(3); expect(device.host).toBe('192.168.1.221'); expect(device.model).toBe('S3SW-001X8EU'); + expect(device.mac).toBe('543204547478'); expect(device.id).toBe('shelly1minig3-543204547478'); expect(device.firmware).toBe(firmwareGen2); expect(device.auth).toBe(true); @@ -289,6 +582,56 @@ describe('Shellies', () => { await device.saveDevicePayloads('temp'); device.destroy(); - }); + }, 60000); + + test('send command to a gen 3 shelly1minig3 device', async () => { + const device = await ShellyDevice.create(shelly, log, '192.168.1.221'); + expect(device).not.toBeUndefined(); + if (!device) return; + + const component = device.getComponent('switch:0'); + expect(component).not.toBeUndefined(); + + if (isSwitchComponent(component)) { + component.On(); + await waiter( + 'On', + () => { + return component.getValue('state') === true; + }, + false, + 10000, + ); + component.Off(); + await waiter( + 'Off', + () => { + return component.getValue('state') === false; + }, + false, + 10000, + ); + component.Toggle(); + await waiter( + 'Toggle', + () => { + return component.getValue('state') === true; + }, + false, + 10000, + ); + component.Off(); + await waiter( + 'Off', + () => { + return component.getValue('state') === false; + }, + false, + 10000, + ); + } + + device.destroy(); + }, 60000); }); });