From 32dfcae1f981fe2abac49ad68a693fea3d80328d Mon Sep 17 00:00:00 2001 From: Ingo Fischer Date: Sat, 1 Feb 2025 14:33:49 +0100 Subject: [PATCH] Update 01.02.2025 (#388) * update deps * Update type detection in UI * Update type detection in Backend * Update Device type objects to TypeDetector changes * Optimize font color * CT light is a color light * Optimize Dimmer handling for devices without dimmer state * Adding Boost Switch to Thermostat * Dats display optimizations * Readme * fix tests * make linter happy --- README.md | 8 +- package-lock.json | 126 +++++++++--------- package.json | 10 +- src-admin/src/Tabs/Bridges.tsx | 17 +-- src-admin/src/Tabs/Devices.tsx | 17 +-- src-admin/src/Utils.tsx | 22 ++- src/lib/DeviceManagement.ts | 2 +- src/lib/devices/Blind.ts | 47 +++++++ src/lib/devices/BlindButtons.ts | 34 +++++ src/lib/devices/DeviceStateObject.ts | 24 +++- src/lib/devices/Gate.ts | 39 ++++++ src/lib/devices/GenericDevice.ts | 43 ------ src/lib/devices/Lock.ts | 75 +++++++++++ src/lib/devices/Thermostat.ts | 6 +- src/main.ts | 5 +- src/matter/ControllerNode.ts | 10 +- src/matter/GeneralMatterNode.ts | 8 +- .../ColorTemperatureLightToIoBroker.ts | 5 + .../ExtendedColorLightToIoBroker.ts | 13 +- src/matter/to-iobroker/ioBrokerFactory.ts | 4 +- .../GenericLightingDeviceToMatter.ts | 46 +++++-- src/matter/to-matter/ThermostatToMatter.ts | 55 ++++++-- test/devices.test.js | 8 +- 23 files changed, 451 insertions(+), 173 deletions(-) diff --git a/README.md b/README.md index 13a6da85..ba1e8d3b 100644 --- a/README.md +++ b/README.md @@ -78,12 +78,18 @@ With the ioBroker Matter Adapter, it is possible to map the following use cases: --> ## Changelog + ### **WORK IN PROGRESS** * (@GermanBluefox) Added the "copy to clipboard" button in the debug dialog +* (@Apollon77) Updated matter.js with performance and Memory usage optimizations (and Tasmota pairing workaround) +* (@Apollon77) Reworked Type detection in Backend and for Channel/Device detection type in UI, now multiple devicetypes are offered with most complex one pre-selected +* (@Apollon77) Handle Matter ColorTemperature Lights as a Color capable light to also allow CT-Lights with Hue support +* (@Apollon77) Added BOOST endpoint as switch when exposing Thermostats with Boost state +* (@Apollon77) Optimized some dimmer/level management for light devices without dimmer state ### 0.4.11 (2025-01-28) -* (@Apollon77) Fixed caching issues in device type detection +* (@Apollon77) Fixed caching issues in device type detection in backend * (@Apollon77) Added Debug info icon for Devices and Bridges ### 0.4.10 (2025-01-27) diff --git a/package-lock.json b/package-lock.json index 984edaf4..ca49720b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,9 +13,9 @@ "@iobroker/dm-utils": "^1.0.9", "@iobroker/i18n": "^0.3.1", "@iobroker/type-detector": "^4.2.0", - "@matter/main": "0.12.1", - "@matter/nodejs": "0.12.1", - "@project-chip/matter.js": "0.12.1", + "@matter/main": "0.12.2-alpha.0-20250201-eb5d40a2f", + "@matter/nodejs": "0.12.2-alpha.0-20250201-eb5d40a2f", + "@project-chip/matter.js": "0.12.2-alpha.0-20250201-eb5d40a2f", "axios": "^1.7.9", "jsonwebtoken": "^9.0.2" }, @@ -30,7 +30,7 @@ "@iobroker/testing": "^5.0.3", "@iobroker/types": "^7.0.6", "@types/jsonwebtoken": "^9.0.8", - "@types/node": "^22.12.0", + "@types/node": "^22.13.0", "chai": "^4.5.0", "colorette": "^2.0.20", "mocha": "^11.1.0", @@ -41,7 +41,7 @@ "node": ">=18" }, "optionalDependencies": { - "@matter/nodejs-ble": "0.12.1" + "@matter/nodejs-ble": "0.12.2-alpha.0-20250201-eb5d40a2f" } }, "node_modules/@alcalzone/pak": { @@ -791,64 +791,64 @@ } }, "node_modules/@matter/general": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@matter/general/-/general-0.12.1.tgz", - "integrity": "sha512-26ZnXpcPeKUJ3xGJvJnkM7dMt32IlJ+5ZCoM8KN2FtXC2Z6EUBJMVxjSxXA4Wbg91wZvj/y/z0xkcEI3gcIOAA==", + "version": "0.12.2-alpha.0-20250201-eb5d40a2f", + "resolved": "https://registry.npmjs.org/@matter/general/-/general-0.12.2-alpha.0-20250201-eb5d40a2f.tgz", + "integrity": "sha512-INFvr428z5WGmTBFPSIoEe6rnqLAtybL12QJTV3wuenu0ubXrSBJb+o8+6gJiKx5AR+eJhfl5uZXnboQvGSMWw==", "license": "Apache-2.0", "dependencies": { "@noble/curves": "^1.8.1" } }, "node_modules/@matter/main": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@matter/main/-/main-0.12.1.tgz", - "integrity": "sha512-TQscTaFzgtUy9R+tNDm1aIViW4qDiXVdnYZGTVWjWqPN/7wOtEO2IgwP3xk9c3bHNkEXPux6TfvPoAx7UkYUng==", + "version": "0.12.2-alpha.0-20250201-eb5d40a2f", + "resolved": "https://registry.npmjs.org/@matter/main/-/main-0.12.2-alpha.0-20250201-eb5d40a2f.tgz", + "integrity": "sha512-eyGsbFx1TQztD+57plKQrvVBs9BVXAf+hEYysWCOrHUBRsYPlZw487ySRVWpiP94VspA9K17K721AauFh/uCsg==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.12.1", - "@matter/model": "0.12.1", - "@matter/node": "0.12.1", - "@matter/protocol": "0.12.1", - "@matter/types": "0.12.1", + "@matter/general": "0.12.2-alpha.0-20250201-eb5d40a2f", + "@matter/model": "0.12.2-alpha.0-20250201-eb5d40a2f", + "@matter/node": "0.12.2-alpha.0-20250201-eb5d40a2f", + "@matter/protocol": "0.12.2-alpha.0-20250201-eb5d40a2f", + "@matter/types": "0.12.2-alpha.0-20250201-eb5d40a2f", "@noble/curves": "^1.8.1" }, "optionalDependencies": { - "@matter/nodejs": "0.12.1" + "@matter/nodejs": "0.12.2-alpha.0-20250201-eb5d40a2f" } }, "node_modules/@matter/model": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@matter/model/-/model-0.12.1.tgz", - "integrity": "sha512-UlUR7iRuBRadfuf4Qwo3OYi7WUrXJk4nddqsD2tBq7y1cA5zsnV3E85zVX/8iiiUSqAbx3rcAuWJ+CCjn0ikUA==", + "version": "0.12.2-alpha.0-20250201-eb5d40a2f", + "resolved": "https://registry.npmjs.org/@matter/model/-/model-0.12.2-alpha.0-20250201-eb5d40a2f.tgz", + "integrity": "sha512-roatr/TpFZYzWaq7JQyuKp+UhUWxOqkASTSd2CqvEjuM0TFyKp5NX88HDmoqNDl5bX0MLtdgp4XJAMPNzmYWnw==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.12.1", + "@matter/general": "0.12.2-alpha.0-20250201-eb5d40a2f", "@noble/curves": "^1.8.1" } }, "node_modules/@matter/node": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@matter/node/-/node-0.12.1.tgz", - "integrity": "sha512-PtN1pRmMynPwwxaSZklCDwLt4uCz1o/moXi0PX2DU866JdkHaKPgLi+29PcalcDB2dMZbrcJTaimAhXcjPVkog==", + "version": "0.12.2-alpha.0-20250201-eb5d40a2f", + "resolved": "https://registry.npmjs.org/@matter/node/-/node-0.12.2-alpha.0-20250201-eb5d40a2f.tgz", + "integrity": "sha512-WvbWeXymvJqYGltmnipXiOE3N/MNRUgHbgHxd6r0joazOCj4qy13ZhFno/nRpWQp2IjDmBJW77u1a9s8c4YrZw==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.12.1", - "@matter/model": "0.12.1", - "@matter/protocol": "0.12.1", - "@matter/types": "0.12.1", + "@matter/general": "0.12.2-alpha.0-20250201-eb5d40a2f", + "@matter/model": "0.12.2-alpha.0-20250201-eb5d40a2f", + "@matter/protocol": "0.12.2-alpha.0-20250201-eb5d40a2f", + "@matter/types": "0.12.2-alpha.0-20250201-eb5d40a2f", "@noble/curves": "^1.8.1" } }, "node_modules/@matter/nodejs": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@matter/nodejs/-/nodejs-0.12.1.tgz", - "integrity": "sha512-G4tPCzaCJMuly0yiIf+eOhz1ezSQQarETFtDuImE+9vicVyKKCL1eq5SviV5nx9DikIcalKMbKh6F6t21sslDA==", + "version": "0.12.2-alpha.0-20250201-eb5d40a2f", + "resolved": "https://registry.npmjs.org/@matter/nodejs/-/nodejs-0.12.2-alpha.0-20250201-eb5d40a2f.tgz", + "integrity": "sha512-vwMNomhvVCFOMc1YJlSYnUAvBngjC4Iw9EJUT4fv7bOLyqk1FsuPGJ/s7Uy/MSz4ko1vTp8kUylztQ1F7ClGLw==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.12.1", - "@matter/node": "0.12.1", - "@matter/protocol": "0.12.1", - "@matter/types": "0.12.1", + "@matter/general": "0.12.2-alpha.0-20250201-eb5d40a2f", + "@matter/node": "0.12.2-alpha.0-20250201-eb5d40a2f", + "@matter/protocol": "0.12.2-alpha.0-20250201-eb5d40a2f", + "@matter/types": "0.12.2-alpha.0-20250201-eb5d40a2f", "node-localstorage": "^3.0.5" }, "engines": { @@ -856,15 +856,15 @@ } }, "node_modules/@matter/nodejs-ble": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@matter/nodejs-ble/-/nodejs-ble-0.12.1.tgz", - "integrity": "sha512-AyDLb9cNAp0jiWPSR0vl3UAVRBgQfoO706y490QZ2UXddsotEAWzTkcYSYYi1M6Yp61nCxAQGIuHYbW8XtraAA==", + "version": "0.12.2-alpha.0-20250201-eb5d40a2f", + "resolved": "https://registry.npmjs.org/@matter/nodejs-ble/-/nodejs-ble-0.12.2-alpha.0-20250201-eb5d40a2f.tgz", + "integrity": "sha512-uT67HeQX4XSK/+dHtI2wPf2agwK5kkEMLJh//maVBEjz9n8jjZne9JWoTZGKw8cL2gAjoABj0C6owuL0yDR2Rw==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@matter/general": "0.12.1", - "@matter/protocol": "0.12.1", - "@matter/types": "0.12.1" + "@matter/general": "0.12.2-alpha.0-20250201-eb5d40a2f", + "@matter/protocol": "0.12.2-alpha.0-20250201-eb5d40a2f", + "@matter/types": "0.12.2-alpha.0-20250201-eb5d40a2f" }, "engines": { "node": ">=18.0.0" @@ -875,25 +875,25 @@ } }, "node_modules/@matter/protocol": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@matter/protocol/-/protocol-0.12.1.tgz", - "integrity": "sha512-HOa5OdMsmmSjmtYA+wmBmS2poUeZqlcoA9fUk40kWla6yINJOyHZCgnPKARxXxm1qk95BEoLN3ovUIKoajkstQ==", + "version": "0.12.2-alpha.0-20250201-eb5d40a2f", + "resolved": "https://registry.npmjs.org/@matter/protocol/-/protocol-0.12.2-alpha.0-20250201-eb5d40a2f.tgz", + "integrity": "sha512-Mko5CUNQnOY3yy381Cf6TKeSCTeWzaXt9DRSr2hWmjyT3xNaJYcJlH7N8i4DfXTd4hR1VIM3V3nK3Iqq9DFYWg==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.12.1", - "@matter/model": "0.12.1", - "@matter/types": "0.12.1", + "@matter/general": "0.12.2-alpha.0-20250201-eb5d40a2f", + "@matter/model": "0.12.2-alpha.0-20250201-eb5d40a2f", + "@matter/types": "0.12.2-alpha.0-20250201-eb5d40a2f", "@noble/curves": "^1.8.1" } }, "node_modules/@matter/types": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@matter/types/-/types-0.12.1.tgz", - "integrity": "sha512-O4zyMGLUnoWQZ7P5TJDWrtTpdaG8xSDq1a5xO3e8pmc3I9w6LSoKciEqrjFvq9lT6gvj3hcts/64c0Rxn7NEIw==", + "version": "0.12.2-alpha.0-20250201-eb5d40a2f", + "resolved": "https://registry.npmjs.org/@matter/types/-/types-0.12.2-alpha.0-20250201-eb5d40a2f.tgz", + "integrity": "sha512-Hs4M77cwNTEz+9sR+PDzJn1ixhXoomXJwOM0Q0P1uram7LdnXOA0x8rSABU3pIpajitAGaezJlpuNmt291/uBw==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.12.1", - "@matter/model": "0.12.1", + "@matter/general": "0.12.2-alpha.0-20250201-eb5d40a2f", + "@matter/model": "0.12.2-alpha.0-20250201-eb5d40a2f", "@noble/curves": "^1.8.1" } }, @@ -1056,16 +1056,16 @@ } }, "node_modules/@project-chip/matter.js": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@project-chip/matter.js/-/matter.js-0.12.1.tgz", - "integrity": "sha512-IPNbBxnduNUNoipn4ZHXWE8gEsYM9nryYvWXlLYWwTJ6tDTIR0oZmsUZzhJoDXjShqbXolYJ8bCkFQ7dfMba6w==", + "version": "0.12.2-alpha.0-20250201-eb5d40a2f", + "resolved": "https://registry.npmjs.org/@project-chip/matter.js/-/matter.js-0.12.2-alpha.0-20250201-eb5d40a2f.tgz", + "integrity": "sha512-4GZwIEAZVdX7gNRTWsL7NgiifAaPYiu1jAlB9DjSQNmAPG8qatScSBytqOPmx32CEQbDJTYRY9P5leAjPxSrow==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.12.1", - "@matter/model": "0.12.1", - "@matter/node": "0.12.1", - "@matter/protocol": "0.12.1", - "@matter/types": "0.12.1", + "@matter/general": "0.12.2-alpha.0-20250201-eb5d40a2f", + "@matter/model": "0.12.2-alpha.0-20250201-eb5d40a2f", + "@matter/node": "0.12.2-alpha.0-20250201-eb5d40a2f", + "@matter/protocol": "0.12.2-alpha.0-20250201-eb5d40a2f", + "@matter/types": "0.12.2-alpha.0-20250201-eb5d40a2f", "@noble/curves": "^1.8.1" } }, @@ -1603,9 +1603,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.12.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.12.0.tgz", - "integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==", + "version": "22.13.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.0.tgz", + "integrity": "sha512-ClIbNe36lawluuvq3+YYhnIN2CELi+6q8NpnM7PYp4hBn/TatfboPgVSm2rwKRfnV2M+Ty9GWDFI64KEe+kysA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 67261865..d1f00b8b 100644 --- a/package.json +++ b/package.json @@ -23,16 +23,16 @@ "url": "https://github.com/ioBroker/ioBroker.matter" }, "optionalDependencies": { - "@matter/nodejs-ble": "0.12.1" + "@matter/nodejs-ble": "0.12.2-alpha.0-20250201-eb5d40a2f" }, "dependencies": { "@iobroker/adapter-core": "^3.2.3", "@iobroker/dm-utils": "^1.0.9", "@iobroker/i18n": "^0.3.1", "@iobroker/type-detector": "^4.2.0", - "@matter/main": "0.12.1", - "@matter/nodejs": "0.12.1", - "@project-chip/matter.js": "0.12.1", + "@matter/main": "0.12.2-alpha.0-20250201-eb5d40a2f", + "@matter/nodejs": "0.12.2-alpha.0-20250201-eb5d40a2f", + "@project-chip/matter.js": "0.12.2-alpha.0-20250201-eb5d40a2f", "axios": "^1.7.9", "jsonwebtoken": "^9.0.2" }, @@ -47,7 +47,7 @@ "@iobroker/testing": "^5.0.3", "@iobroker/types": "^7.0.6", "@types/jsonwebtoken": "^9.0.8", - "@types/node": "^22.12.0", + "@types/node": "^22.13.0", "chai": "^4.5.0", "colorette": "^2.0.20", "mocha": "^11.1.0", diff --git a/src-admin/src/Tabs/Bridges.tsx b/src-admin/src/Tabs/Bridges.tsx index 4a69b257..7525c64d 100644 --- a/src-admin/src/Tabs/Bridges.tsx +++ b/src-admin/src/Tabs/Bridges.tsx @@ -55,7 +55,7 @@ import type { DeviceDescription, MatterConfig, } from '../types'; -import { clone, detectDevices, getText } from '../Utils'; +import { clone, detectDevices, getDetectedDeviceTypes, getText } from '../Utils'; import BridgesAndDevices, { STYLES, type BridgesAndDevicesProps, @@ -900,13 +900,13 @@ export class Bridges extends BridgesAndDevices { } // Try to detect ID out of the supported IDs - const controls = + const detectedRooms = (await detectDevices(this.props.socket, I18n.getLanguage(), SUPPORTED_DEVICES, [oid])) ?? []; - if (!controls.length) { - const controls = + if (!detectedRooms.length) { + const detectedRooms = (await detectDevices(this.props.socket, I18n.getLanguage(), undefined, [oid])) ?? []; - const deviceTypes = controls.map(c => c.devices[0].deviceType); + const deviceTypes = getDetectedDeviceTypes(detectedRooms); if (deviceTypes.length) { this.props.showToast( I18n.t('Detected device types "%s" are not supported yet', deviceTypes.join(', ')), @@ -927,7 +927,8 @@ export class Bridges extends BridgesAndDevices { } } else { // Show dialog to select device type but only allow the detected ones - const deviceType = controls[0].devices[0].deviceType; + const detectedDeviceTypes = getDetectedDeviceTypes(detectedRooms); + const deviceType = detectedDeviceTypes[0]; // try to find ON state for dimmer this.setState({ @@ -937,9 +938,9 @@ export class Bridges extends BridgesAndDevices { name: name || '', deviceType, bridgeIndex: this.bridgeIndex as number, - hasOnState: controls[0].devices[0].hasOnState, + hasOnState: detectedRooms[0].devices[0].hasOnState, // TODO: That needs to be more dynamic if we really need it noComposed: false, - detectedDeviceTypes: controls.map(c => c.devices[0].deviceType), + detectedDeviceTypes, }, }); } diff --git a/src-admin/src/Tabs/Devices.tsx b/src-admin/src/Tabs/Devices.tsx index cc02c392..faa51e5e 100644 --- a/src-admin/src/Tabs/Devices.tsx +++ b/src-admin/src/Tabs/Devices.tsx @@ -29,7 +29,7 @@ import { import { I18n, SelectID, IconDeviceType } from '@iobroker/adapter-react-v5'; import DeviceDialog, { SUPPORTED_DEVICES } from '../components/DeviceDialog'; import type { DetectedDevice, DeviceDescription, MatterConfig } from '../types'; -import { clone, detectDevices, getText } from '../Utils'; +import { clone, detectDevices, getDetectedDeviceTypes, getText } from '../Utils'; import InfoBox from '../components/InfoBox'; import BridgesAndDevices, { @@ -608,13 +608,13 @@ class Devices extends BridgesAndDevices { } // Try to detect ID out of the supported IDs - const controls = + const detectedRooms = (await detectDevices(this.props.socket, I18n.getLanguage(), SUPPORTED_DEVICES, [oid])) ?? []; - if (!controls.length) { - const controls = + if (!detectedRooms.length) { + const detectedRooms = (await detectDevices(this.props.socket, I18n.getLanguage(), undefined, [oid])) ?? []; - const deviceTypes = controls.map(c => c.devices[0].deviceType); + const deviceTypes = getDetectedDeviceTypes(detectedRooms); if (deviceTypes.length) { this.props.showToast( I18n.t('Detected device types "%s" are not supported yet', deviceTypes.join(', ')), @@ -636,7 +636,8 @@ class Devices extends BridgesAndDevices { } } else { // Show dialog to select device type but only allow the detected ones - const deviceType = controls[0].devices[0].deviceType; + const detectedDeviceTypes = getDetectedDeviceTypes(detectedRooms); + const deviceType = detectedDeviceTypes[0]; // try to find ON state for dimmer this.setState({ @@ -645,11 +646,11 @@ class Devices extends BridgesAndDevices { oid, name: name || '', deviceType, - hasOnState: controls[0].devices[0].hasOnState, + hasOnState: detectedRooms[0].devices[0].hasOnState, // TODO: That needs to be more dynamic if we really need it vendorID: '0xFFF1', productID: '0x8000', noComposed: false, - detectedDeviceTypes: controls.map(c => c.devices[0].deviceType), + detectedDeviceTypes, }, }); } diff --git a/src-admin/src/Utils.tsx b/src-admin/src/Utils.tsx index 09f15d37..16e52b84 100644 --- a/src-admin/src/Utils.tsx +++ b/src-admin/src/Utils.tsx @@ -158,6 +158,7 @@ export async function detectDevices( _list = list; } + const detectOnSingleObject = list?.length === 1; const options: DetectOptions = { id: '', objects: devicesObject, @@ -166,6 +167,9 @@ export async function detectDevices( ignoreIndicators, allowedTypes, excludedTypes, + // When we only detect for a single object then we try to find anything and ignore enums + detectAllPossibleDevices: detectOnSingleObject, + ignoreEnums: detectOnSingleObject, }; const result: DetectedRoom[] = []; @@ -183,7 +187,7 @@ export async function detectDevices( } const stateId = stateIdObj.id; // if not yet added - if (result.find(item => item.devices.find(st => st._id === stateId))) { + if (!detectOnSingleObject && result.find(item => item.devices.find(st => st._id === stateId))) { return; } const deviceObject: DetectedDevice = { @@ -262,8 +266,8 @@ export async function detectDevices( }); // find names and icons for devices - result.forEach(control => { - control.devices.forEach(dev => { + result.forEach(room => { + room.devices.forEach(dev => { const deviceObj = dev; if (deviceObj.type === 'state' || deviceObj.type === 'channel') { const idArray = deviceObj._id.split('.'); @@ -303,6 +307,18 @@ export async function detectDevices( return result; } +export function getDetectedDeviceTypes(detectedRooms: DetectedRoom[]): Types[] { + const result: Types[] = []; + detectedRooms.forEach(room => { + room.devices.forEach(device => { + if (!result.includes(device.deviceType)) { + result.push(device.deviceType); + } + }); + }); + return result; +} + /** * Get text from object or just a string without trying to translate it */ diff --git a/src/lib/DeviceManagement.ts b/src/lib/DeviceManagement.ts index aa547577..5175e308 100644 --- a/src/lib/DeviceManagement.ts +++ b/src/lib/DeviceManagement.ts @@ -194,7 +194,7 @@ class MatterAdapterDeviceManagement extends DeviceManagement { hasDetails: true, actions: actions.length ? (actions as DeviceAction<'adapter'>[]) : undefined, backgroundColor, - color: '#002346', + color: backgroundColor === 'primary' ? '#164477' : '#57BFFF', group: { key: 'node', name: this.#adapter.getText('Node'), diff --git a/src/lib/devices/Blind.ts b/src/lib/devices/Blind.ts index 1bb0ae59..e9ae22f1 100644 --- a/src/lib/devices/Blind.ts +++ b/src/lib/devices/Blind.ts @@ -2,9 +2,24 @@ import { BlindButtons } from './BlindButtons'; import { type DeviceStateObject, PropertyType, ValueType } from './DeviceStateObject'; import { type DetectedDevice, type DeviceOptions, StateAccessType } from './GenericDevice'; +export enum BlindDirections { + None = 'None', + UpOrOpen = 'Up/Open', + DownOrClose = 'Down/Close', + Unknown = 'Unknown', +} + +export enum BlindDirectionsNumbers { + None = 0, + 'Up/Open' = 1, + 'Down/Close' = 2, + Unknown = 3, +} + export class Blind extends BlindButtons { #setLevelState?: DeviceStateObject; #getLevelState?: DeviceStateObject; + #directionEnumState?: DeviceStateObject; constructor(detectedDevice: DetectedDevice, adapter: ioBroker.Adapter, options?: DeviceOptions) { super(detectedDevice, adapter, options); @@ -25,6 +40,13 @@ export class Blind extends BlindButtons { type: PropertyType.Level, callback: state => (this.#setLevelState = state), }, + { + name: 'DIRECTION', + valueType: ValueType.Enum, + accessType: StateAccessType.Read, + type: PropertyType.DirectionEnum, + callback: state => (this.#directionEnumState = state), + }, ]), ); } @@ -68,4 +90,29 @@ export class Blind extends BlindButtons { hasLiftLevel(): boolean { return !!this.#setLevelState; } + + getDirectionEnum(): BlindDirections | undefined { + if (!this.#directionEnumState) { + throw new Error('Direction state not found'); + } + return this.#directionEnumState.value; + } + + setDirectionENum(value: BlindDirections): Promise { + if (!this.#directionEnumState) { + throw new Error('Direction state not found'); + } + return this.#directionEnumState.setValue(value); + } + + updateDirectionEnum(value: BlindDirections): Promise { + if (!this.#directionEnumState) { + throw new Error('Direction state not found'); + } + return this.#directionEnumState.updateValue(value); + } + + hasDirectionEnum(): boolean { + return !!this.#directionEnumState; + } } diff --git a/src/lib/devices/BlindButtons.ts b/src/lib/devices/BlindButtons.ts index 50cce09d..26f8d523 100644 --- a/src/lib/devices/BlindButtons.ts +++ b/src/lib/devices/BlindButtons.ts @@ -1,5 +1,6 @@ import { type DeviceStateObject, PropertyType, ValueType } from './DeviceStateObject'; import { GenericDevice, type DetectedDevice, type DeviceOptions, StateAccessType } from './GenericDevice'; +import type { BlindDirections } from './Blind'; /* Blinds controlled only by buttons [blindButtons] @@ -29,6 +30,7 @@ export class BlindButtons extends GenericDevice { #setTiltStopState?: DeviceStateObject; #setTiltOpenState?: DeviceStateObject; #setTiltCloseState?: DeviceStateObject; + #directionEnumState?: DeviceStateObject; constructor(detectedDevice: DetectedDevice, adapter: ioBroker.Adapter, options?: DeviceOptions) { super(detectedDevice, adapter, options); @@ -92,6 +94,13 @@ export class BlindButtons extends GenericDevice { type: PropertyType.TiltClose, callback: state => (this.#setTiltCloseState = state), }, + { + name: 'DIRECTION', + valueType: ValueType.Enum, + accessType: StateAccessType.Read, + type: PropertyType.DirectionEnum, + callback: state => (this.#directionEnumState = state), + }, ]), ); } @@ -201,4 +210,29 @@ export class BlindButtons extends GenericDevice { hastTiltStopButton(): boolean { return !!this.#setTiltStopState; } + + getDirectionEnum(): BlindDirections | undefined { + if (!this.#directionEnumState) { + throw new Error('Direction state not found'); + } + return this.#directionEnumState.value; + } + + setDirectionENum(value: BlindDirections): Promise { + if (!this.#directionEnumState) { + throw new Error('Direction state not found'); + } + return this.#directionEnumState.setValue(value); + } + + updateDirectionEnum(value: BlindDirections): Promise { + if (!this.#directionEnumState) { + throw new Error('Direction state not found'); + } + return this.#directionEnumState.updateValue(value); + } + + hasDirectionEnum(): boolean { + return !!this.#directionEnumState; + } } diff --git a/src/lib/devices/DeviceStateObject.ts b/src/lib/devices/DeviceStateObject.ts index e50cb079..b0ff643e 100644 --- a/src/lib/devices/DeviceStateObject.ts +++ b/src/lib/devices/DeviceStateObject.ts @@ -34,6 +34,8 @@ export enum PropertyType { Description = 'description', Dimmer = 'dimmer', Direction = 'direction', + DirectionEnum = 'directionEnum', + DoorState = 'doorState', Duration = 'duration', Elapsed = 'elapsed', ElectricPower = 'electricPower', @@ -352,9 +354,8 @@ export class DeviceStateObject extends EventEmitter { if (!this.object) { throw new Error(`Object not initialized`); } - const obj = this.object; // {'MODE_VALUE': 'MODE_TEXT'} - let modes: { [key: string]: T } = obj?.common?.states; + let modes: { [key: string]: T } | undefined = this.object?.common?.states; if (modes) { // convert ['Auto'] => {'Auto': 'AUTO'} if (Array.isArray(modes)) { @@ -363,8 +364,14 @@ export class DeviceStateObject extends EventEmitter { modes = _m; } this.modes = modes; + this.adapter.log.debug( + `Initialize modes ${Object.entries(modes) + .map(([key, value]) => `(${key}) ${String(value)}`) + .join(',')} for ${this.#id}`, + ); } else { this.modes = {}; + this.adapter.log.debug(`No modes found for ${this.#id}`); } } @@ -519,7 +526,7 @@ export class DeviceStateObject extends EventEmitter { !this.#isIoBrokerState, ); } else if (valueType === 'enum') { - let realValue: string | T = value; + let realValue: number | string | T = value; if (this.modes) { for (const [key, value] of Object.entries(this.modes)) { if (realValue === value) { @@ -527,6 +534,17 @@ export class DeviceStateObject extends EventEmitter { break; } } + this.adapter.log.debug( + `Mapped enum value for ${this.#id}: ${String(value)} --> ${String(realValue)}`, + ); + if ( + this.object.common.type === 'number' && + typeof realValue === 'string' && + realValue.match(/^[0-9]+$/) + ) { + realValue = parseFloat(realValue as string); + this.adapter.log.debug(`Converted enum value to number: ${realValue}`); + } } else { this.adapter.log.info(`Cannot map enum value for ${this.#id} without modes`); } diff --git a/src/lib/devices/Gate.ts b/src/lib/devices/Gate.ts index cadb18cc..7961fdeb 100644 --- a/src/lib/devices/Gate.ts +++ b/src/lib/devices/Gate.ts @@ -1,10 +1,17 @@ import { type DeviceStateObject, PropertyType, ValueType } from './DeviceStateObject'; import { GenericDevice, type DetectedDevice, type DeviceOptions, StateAccessType } from './GenericDevice'; +import { BlindDirections, BlindDirectionsNumbers } from './Blind'; + +export type GateDirections = BlindDirections; +export const GateDirections = BlindDirections; +export type GateDirectionsNumbers = BlindDirectionsNumbers; +export const GateDirectionsNumbers = BlindDirectionsNumbers; export class Gate extends GenericDevice { #setLevelState?: DeviceStateObject; #getLevelState?: DeviceStateObject; #setStopState?: DeviceStateObject; + #directionEnumState?: DeviceStateObject; constructor(detectedDevice: DetectedDevice, adapter: ioBroker.Adapter, options?: DeviceOptions) { super(detectedDevice, adapter, options); @@ -33,6 +40,13 @@ export class Gate extends GenericDevice { type: PropertyType.Stop, callback: state => (this.#setStopState = state), }, + { + name: 'DIRECTION', + valueType: ValueType.Enum, + accessType: StateAccessType.Read, + type: PropertyType.DirectionEnum, + callback: state => (this.#directionEnumState = state), + }, ]), ); } @@ -57,4 +71,29 @@ export class Gate extends GenericDevice { } return this.#setStopState.setValue(true); } + + getDirectionEnum(): BlindDirections | undefined { + if (!this.#directionEnumState) { + throw new Error('Direction state not found'); + } + return this.#directionEnumState.value; + } + + setDirectionENum(value: BlindDirections): Promise { + if (!this.#directionEnumState) { + throw new Error('Direction state not found'); + } + return this.#directionEnumState.setValue(value); + } + + updateDirectionEnum(value: BlindDirections): Promise { + if (!this.#directionEnumState) { + throw new Error('Direction state not found'); + } + return this.#directionEnumState.updateValue(value); + } + + hasDirectionEnum(): boolean { + return !!this.#directionEnumState; + } } diff --git a/src/lib/devices/GenericDevice.ts b/src/lib/devices/GenericDevice.ts index 73a55432..4e3a28aa 100644 --- a/src/lib/devices/GenericDevice.ts +++ b/src/lib/devices/GenericDevice.ts @@ -5,49 +5,6 @@ import type { BridgeDeviceDescription } from '../../ioBrokerStorageTypes'; import { DeviceStateObject, PropertyType, ValueType } from './DeviceStateObject'; import { EventEmitter } from 'events'; -// take here https://github.com/ioBroker/ioBroker.type-detector/blob/master/DEVICES.md#temperature-temperature -// export enum DeviceType { -// AirCondition = 'airCondition', -// Blind = 'blind', -// BlindButtons = 'blindButtons', -// Button = 'button', -// ButtonSensor = 'buttonSensor', -// Camera = 'camera', -// Url = 'url', -// Chart = 'chart', -// Image = 'image', -// Dimmer = 'dimmer', -// Door = 'door', -// FireAlarm = 'fireAlarm', -// FloodAlarm = 'floodAlarm', -// Gate = 'gate', -// Humidity = 'humidity', -// Info = 'info', -// Light = 'light', -// Lock = 'lock', -// Location = 'location', -// Media = 'media', -// Motion = 'motion', -// Rgb = 'rgb', -// Ct = 'ct', -// RgbSingle = 'rgbSingle', -// RgbwSingle = 'rgbwSingle', -// Hue = 'hue', -// Cie = 'cie', -// Slider = 'slider', -// Socket = 'socket', -// Temperature = 'temperature', -// Thermostat = 'thermostat', -// Volume = 'volume', -// VacuumCleaner = 'vacuumCleaner', -// VolumeGroup = 'volumeGroup', -// Window = 'window', -// WindowTilt = 'windowTilt', -// WeatherCurrent = 'weatherCurrent', -// WeatherForecast = 'weatherForecast', -// Warning = 'warning', -// } - export interface DeviceOptions extends BridgeDeviceDescription { additionalStateData?: { [key: string]: Partial }; dimmerOnLevel?: number; diff --git a/src/lib/devices/Lock.ts b/src/lib/devices/Lock.ts index c302b40f..b3c897c8 100644 --- a/src/lib/devices/Lock.ts +++ b/src/lib/devices/Lock.ts @@ -1,10 +1,28 @@ import { type DeviceStateObject, PropertyType, ValueType } from './DeviceStateObject'; import { GenericDevice, type DetectedDevice, type DeviceOptions, StateAccessType } from './GenericDevice'; +export enum LockMovementDirections { + None = 'None', + Unlock = 'Unlock', + Lock = 'Lock', + Open = 'Open', + Unknown = 'Unknown', +} + +export enum LockMovementDirectionsNumbers { + None = 0, + Unlock = 1, + Lock = 2, + Open = 3, + Unknown = 4, +} + export class Lock extends GenericDevice { #setLockState?: DeviceStateObject; #getLockState?: DeviceStateObject; #setOpenState?: DeviceStateObject; + #directionEnumState?: DeviceStateObject; + #doorState?: DeviceStateObject; constructor(detectedDevice: DetectedDevice, adapter: ioBroker.Adapter, options?: DeviceOptions) { super(detectedDevice, adapter, options); @@ -33,6 +51,13 @@ export class Lock extends GenericDevice { type: PropertyType.Open, callback: state => (this.#setOpenState = state), }, + { + name: 'DOOR_STATE', + valueType: ValueType.Boolean, + accessType: StateAccessType.Read, + type: PropertyType.DoorState, + callback: state => (this.#doorState = state), + }, ]), ); } @@ -83,4 +108,54 @@ export class Lock extends GenericDevice { hasOpen(): boolean { return !!this.#setOpenState; } + + getDirectionEnum(): LockMovementDirections | undefined { + if (!this.#directionEnumState) { + throw new Error('Direction state not found'); + } + return this.#directionEnumState.value; + } + + setDirectionENum(value: LockMovementDirections): Promise { + if (!this.#directionEnumState) { + throw new Error('Direction state not found'); + } + return this.#directionEnumState.setValue(value); + } + + updateDirectionEnum(value: LockMovementDirections): Promise { + if (!this.#directionEnumState) { + throw new Error('Direction state not found'); + } + return this.#directionEnumState.updateValue(value); + } + + hasDirectionEnum(): boolean { + return !!this.#directionEnumState; + } + + getDoorState(): boolean | undefined { + if (!this.#doorState) { + throw new Error('Door state not found'); + } + return this.#doorState.value; + } + + async updateDoorState(value: boolean): Promise { + if (!this.#doorState) { + throw new Error('Door state not found'); + } + await this.#doorState.updateValue(value); + } + + setDoorState(value: boolean): Promise { + if (!this.#doorState) { + throw new Error('Door state not found'); + } + return this.#doorState.setValue(value); + } + + hasDoorState(): boolean { + return !!this.#doorState; + } } diff --git a/src/lib/devices/Thermostat.ts b/src/lib/devices/Thermostat.ts index 95a2e5e5..03a65b67 100644 --- a/src/lib/devices/Thermostat.ts +++ b/src/lib/devices/Thermostat.ts @@ -31,7 +31,7 @@ export class Thermostat extends GenericDevice { #getTemperatureState?: DeviceStateObject; #powerState?: DeviceStateObject; #getHumidityState?: DeviceStateObject; - #boostState?: DeviceStateObject; + #boostState?: DeviceStateObject; #partyState?: DeviceStateObject; #modeState?: DeviceStateObject; @@ -234,14 +234,14 @@ export class Thermostat extends GenericDevice { return !!this.#getHumidityState; } - getBoost(): number | undefined { + getBoost(): boolean | number | undefined { if (!this.#boostState) { throw new Error('Boost state not found'); } return this.#boostState.value; } - setBoost(value: number): Promise { + setBoost(value: boolean | number): Promise { if (!this.#boostState) { throw new Error('Boost state not found'); } diff --git a/src/main.ts b/src/main.ts index 3140c673..f2220057 100644 --- a/src/main.ts +++ b/src/main.ts @@ -812,14 +812,15 @@ export class MatterAdapter extends utils.Adapter { ignoreIndicators, excludedTypes: [Types.info], allowedTypes: preferredType ? [preferredType as Types] : undefined, - //ignoreCache: true + ignoreCache: true, + ignoreEnums: true, }; const detector = new ChannelDetector(); let controls = detector.detect(options); if (!controls?.length) { delete options.allowedTypes; - const detector = new ChannelDetector(); + options.detectAllPossibleDevices = true; controls = detector.detect(options); } if (controls?.length) { diff --git a/src/matter/ControllerNode.ts b/src/matter/ControllerNode.ts index 82fc7f18..b045e334 100644 --- a/src/matter/ControllerNode.ts +++ b/src/matter/ControllerNode.ts @@ -210,10 +210,16 @@ class Controller implements GeneralNode { #registerNodeHandlers(node: PairedNode): void { node.events.attributeChanged.on(data => { - this.#nodes.get(node.nodeId.toString())?.handleChangedAttribute(data); + this.#nodes + .get(node.nodeId.toString()) + ?.handleChangedAttribute(data) + .catch(error => this.#adapter.log.error(`Error handling attribute change: ${error}`)); }); node.events.eventTriggered.on(data => { - this.#nodes.get(node.nodeId.toString())?.handleTriggeredEvent(data); + this.#nodes + .get(node.nodeId.toString()) + ?.handleTriggeredEvent(data) + .catch(error => this.#adapter.log.error(`Error handling event: ${error}`)); }); node.events.stateChanged.on(async (info: PairedNodeStates) => { const nodeDetails = (this.#commissioningController?.getCommissionedNodesDetails() ?? []).find( diff --git a/src/matter/GeneralMatterNode.ts b/src/matter/GeneralMatterNode.ts index 07c4a8cd..36486f23 100644 --- a/src/matter/GeneralMatterNode.ts +++ b/src/matter/GeneralMatterNode.ts @@ -184,7 +184,7 @@ export class GeneralMatterNode { if (info !== undefined) { this.#details = { manufacturer: await info.getVendorNameAttribute(), - model: toHex(await info.getProductIdAttribute()), + model: await info.getProductNameAttribute(), }; if (existingObject && existingObject.common.name) { @@ -1091,9 +1091,6 @@ export class GeneralMatterNode { } result.specification = {}; - if (typeof details.dataModelRevision === 'number') { - result.specification.dataModelRevision = details.dataModelRevision; - } if (typeof details.specificationVersion === 'number') { const { major, minor, patch } = SpecificationVersion.decode(details.specificationVersion); result.specification.specificationVersion = `${major}.${minor}.${patch}`; @@ -1108,6 +1105,9 @@ export class GeneralMatterNode { } } } + if (typeof details.dataModelRevision === 'number') { + result.specification.dataModelRevision = details.dataModelRevision; + } if (details.maxPathsPerInvoke) { result.specification.maxPathsPerInvoke = details.maxPathsPerInvoke; } diff --git a/src/matter/to-iobroker/ColorTemperatureLightToIoBroker.ts b/src/matter/to-iobroker/ColorTemperatureLightToIoBroker.ts index c2a947be..20bd685c 100644 --- a/src/matter/to-iobroker/ColorTemperatureLightToIoBroker.ts +++ b/src/matter/to-iobroker/ColorTemperatureLightToIoBroker.ts @@ -7,6 +7,11 @@ import type { DetectedDevice, DeviceOptions } from '../../lib/devices/GenericDev import { GenericElectricityDataDeviceToIoBroker } from './GenericElectricityDataDeviceToIoBroker'; import { kelvinToMireds, miredsToKelvin } from '@matter/main/behaviors'; +/** + * This lass is currently unused and can be removed if the remapping of CT to Extended Color Light works as expected + * after 0.4.12 + */ + export class ColorTemperatureLightToIoBroker extends GenericElectricityDataDeviceToIoBroker { readonly #ioBrokerDevice: Ct; #isLighting = false; diff --git a/src/matter/to-iobroker/ExtendedColorLightToIoBroker.ts b/src/matter/to-iobroker/ExtendedColorLightToIoBroker.ts index 5d18f4ea..f4da93d3 100644 --- a/src/matter/to-iobroker/ExtendedColorLightToIoBroker.ts +++ b/src/matter/to-iobroker/ExtendedColorLightToIoBroker.ts @@ -3,13 +3,14 @@ import { LevelControl, OnOff, ColorControl } from '@matter/main/clusters'; import type { Endpoint, PairedNode } from '@project-chip/matter.js/device'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; import { Cie } from '../../lib/devices/Cie'; +import { Ct } from '../../lib/devices/Ct'; import { Hue } from '../../lib/devices/Hue'; import type { DetectedDevice, DeviceOptions } from '../../lib/devices/GenericDevice'; import { GenericElectricityDataDeviceToIoBroker } from './GenericElectricityDataDeviceToIoBroker'; import { kelvinToMireds, miredsToKelvin } from '@matter/main/behaviors'; export class ExtendedColorLightToIoBroker extends GenericElectricityDataDeviceToIoBroker { - readonly #ioBrokerDevice: Hue | Cie; + readonly #ioBrokerDevice: Hue | Cie | Ct; #hueSaturationTimeout?: ioBroker.Timeout; #minLevel = 1; #maxLevel = 254; @@ -43,12 +44,18 @@ export class ExtendedColorLightToIoBroker extends GenericElectricityDataDeviceTo adapter, this.enableHueDeviceTypeStates(), ); - } else { + } else if (this.appEndpoint.getClusterClient(ColorControl.Complete)?.supportedFeatures.xy) { this.#ioBrokerDevice = new Cie( { ...ChannelDetector.getPatterns().cie, isIoBrokerDevice: false } as DetectedDevice, adapter, this.enableCieDeviceTypeStates(), ); + } else { + this.#ioBrokerDevice = new Ct( + { ...ChannelDetector.getPatterns().ct, isIoBrokerDevice: false } as DetectedDevice, + adapter, + this.enableDeviceTypeStates(), + ); } } @@ -295,7 +302,7 @@ export class ExtendedColorLightToIoBroker extends GenericElectricityDataDeviceTo return super.enableDeviceTypeStates(); } - get ioBrokerDevice(): Hue | Cie { + get ioBrokerDevice(): Hue | Cie | Ct { return this.#ioBrokerDevice; } } diff --git a/src/matter/to-iobroker/ioBrokerFactory.ts b/src/matter/to-iobroker/ioBrokerFactory.ts index b80aa6c1..04d59d7a 100644 --- a/src/matter/to-iobroker/ioBrokerFactory.ts +++ b/src/matter/to-iobroker/ioBrokerFactory.ts @@ -14,7 +14,6 @@ import { OnOffPlugInUnitToIoBroker } from './OnOffPlugInUnitToIoBroker'; import { TemperatureSensorToIoBroker } from './TemperatureSensorToIoBroker'; import { UtilityOnlyToIoBroker } from './UtilityOnlyToIoBroker'; import { WaterLeakDetectorToIoBroker } from './WaterLeakDetectorToIoBroker'; -import { ColorTemperatureLightToIoBroker } from './ColorTemperatureLightToIoBroker'; import { GenericSwitchToIoBroker } from './GenericSwitchToIoBroker'; import { LightSensorToIoBroker } from './LightSensorToIoBroker'; import { ExtendedColorLightToIoBroker } from './ExtendedColorLightToIoBroker'; @@ -71,7 +70,8 @@ async function ioBrokerDeviceFabric( let isSupportedDeviceType = true; switch (primaryDeviceType?.deviceType.id) { case Devices.ColorTemperatureLightDeviceDefinition.deviceType: - DeviceType = ColorTemperatureLightToIoBroker; + //DeviceType = ColorTemperatureLightToIoBroker; + DeviceType = ExtendedColorLightToIoBroker; // Because it could be CT and Hue it is easier top map this way break; case Devices.ContactSensorDeviceDefinition.deviceType: DeviceType = ContactSensorToIoBroker; diff --git a/src/matter/to-matter/GenericLightingDeviceToMatter.ts b/src/matter/to-matter/GenericLightingDeviceToMatter.ts index 05c893ba..863a8577 100644 --- a/src/matter/to-matter/GenericLightingDeviceToMatter.ts +++ b/src/matter/to-matter/GenericLightingDeviceToMatter.ts @@ -54,17 +54,29 @@ export abstract class GenericLightingDeviceToMatter extends GenericElectricityDa protected async initializeOnOffClusterHandlers(): Promise { await this.#matterEndpoint.setStateOf(EventedOnOffLightOnOffServer, { - onOff: this.#ioBrokerDevice.hasPower() ? !!this.#ioBrokerDevice.getPower() : true, + onOff: this.#ioBrokerDevice.hasPower() + ? !!this.#ioBrokerDevice.getPower() + : !(this.#ioBrokerDevice instanceof Light) && this.#ioBrokerDevice.hasDimmer() + ? (this.#ioBrokerDevice.getDimmer() ?? 0) > 0 + : true, }); this.matterEvents.on(this.#matterEndpoint.eventsOf(IoBrokerEvents).onOffControlled, async on => { if (this.#ioBrokerDevice.hasPower()) { await this.#ioBrokerDevice.setPower(on); - } else { - // Report always on when no Power is supported - await this.#matterEndpoint.setStateOf(EventedOnOffLightOnOffServer, { - onOff: true, - }); + } else if (!(this.#ioBrokerDevice instanceof Light) && this.#ioBrokerDevice.hasDimmer()) { + if (on) { + if (this.#ioBrokerDevice.getDimmer() === 0) { + const currentLevel = this.#matterEndpoint.stateOf( + EventedLightingLevelControlServer, + )?.currentLevel; + await this.#ioBrokerDevice.setDimmer( + typeof currentLevel === 'number' ? Math.round((currentLevel / 254) * 100) : 100, + ); + } + } else { + await this.#ioBrokerDevice.setDimmer(0); + } } }); if (!this.#ioBrokerDevice.hasPower()) { @@ -92,7 +104,7 @@ export abstract class GenericLightingDeviceToMatter extends GenericElectricityDa const ioBrokerDevice = this.#ioBrokerDevice; // Pin limited type for the scope of this function const currentLevel = ioBrokerDevice.hasDimmer() - ? ioBrokerDevice.cropValue(ioBrokerDevice.getLevel() ?? 100, 1, 100) + ? ioBrokerDevice.cropValue(ioBrokerDevice.getDimmer() ?? 100, 1, 100) : 100; await this.#matterEndpoint.setStateOf(EventedLightingLevelControlServer, { currentLevel: this.asMatterLevel(currentLevel), @@ -107,7 +119,7 @@ export abstract class GenericLightingDeviceToMatter extends GenericElectricityDa } if (level !== null) { - await ioBrokerDevice.setLevel(Math.round((level / 254) * 100)); + await ioBrokerDevice.setDimmer(Math.round((level / 254) * 100)); } } else { // Report always 100% when no Dimmer is supported @@ -128,11 +140,21 @@ export abstract class GenericLightingDeviceToMatter extends GenericElectricityDa case PropertyType.Level: case PropertyType.LevelActual: case PropertyType.Dimmer: { - const value = ioBrokerDevice.cropValue((event.value as number) ?? 100, 1, 100); + const ioValue = (event.value ?? 100) as number; + if (!this.#ioBrokerDevice.hasPower()) { + // If the device has no power state we still report the onoff state based on level + await this.#matterEndpoint.setStateOf(EventedOnOffLightOnOffServer, { + onOff: ioValue > 0, + }); + } - await this.#matterEndpoint.setStateOf(EventedLightingLevelControlServer, { - currentLevel: this.asMatterLevel(value), - }); + if (ioValue > 0) { + const value = ioBrokerDevice.cropValue(ioValue ?? 100, 1, 100); + + await this.#matterEndpoint.setStateOf(EventedLightingLevelControlServer, { + currentLevel: this.asMatterLevel(value), + }); + } break; } } diff --git a/src/matter/to-matter/ThermostatToMatter.ts b/src/matter/to-matter/ThermostatToMatter.ts index 028064c8..6662509a 100644 --- a/src/matter/to-matter/ThermostatToMatter.ts +++ b/src/matter/to-matter/ThermostatToMatter.ts @@ -1,5 +1,5 @@ import { Endpoint } from '@matter/main'; -import { HumiditySensorDevice, ThermostatDevice } from '@matter/main/devices'; +import { HumiditySensorDevice, ThermostatDevice, OnOffPlugInUnitDevice } from '@matter/main/devices'; import { Thermostat as MatterThermostat } from '@matter/main/clusters'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; import { ThermostatMode, type Thermostat } from '../../lib/devices/Thermostat'; @@ -8,15 +8,27 @@ import { IoThermostatServer } from '../behaviors/ThermostatServer'; import { IoBrokerEvents } from '../behaviors/IoBrokerEvents'; import { IoIdentifyServer } from '../behaviors/IdentifyServer'; import { IoBrokerContext } from '../behaviors/IoBrokerContext'; +import { EventedOnOffPlugInUnitOnOffServer } from '../behaviors/EventedOnOffPlugInUnitOnOffServer'; //const HeatingThermostatServer = IoThermostatServer.with(MatterThermostat.Feature.Heating); //const CoolingThermostatServer = IoThermostatServer.with(MatterThermostat.Feature.Cooling); +const IoThermostatDevice = ThermostatDevice.with(IoThermostatServer, IoBrokerEvents, IoIdentifyServer, IoBrokerContext); +type IoThermostatDevice = typeof IoThermostatDevice; + +const IoOnOffPlugInUnitDevice = OnOffPlugInUnitDevice.with( + EventedOnOffPlugInUnitOnOffServer, + IoBrokerEvents, + IoBrokerContext, +); +type IoOnOffPlugInUnitDevice = typeof IoOnOffPlugInUnitDevice; + /** Mapping Logic to map a ioBroker Temperature device to a Matter TemperatureSensorDevice. */ export class ThermostatToMatter extends GenericDeviceToMatter { readonly #ioBrokerDevice: Thermostat; - readonly #matterEndpointThermostat: Endpoint; + readonly #matterEndpointThermostat: Endpoint; readonly #matterEndpointHumidity?: Endpoint; + readonly #matterEndpointBoost?: Endpoint; #supportedModes = new Array(); #validModes = new Array(); #temperatureDebounceTimeout?: ioBroker.Timeout; @@ -102,12 +114,7 @@ export class ThermostatToMatter extends GenericDeviceToMatter { const hasCooling = clusterModes.includes(MatterThermostat.Feature.Cooling); this.#matterEndpointThermostat = new Endpoint( - ThermostatDevice.with( - IoThermostatServer.with(...clusterModes), - IoBrokerEvents, - IoIdentifyServer, - IoBrokerContext, - ), + IoThermostatDevice.with(IoThermostatServer.with(...clusterModes)), { id: `${uuid}-Thermostat`, ioBrokerContext: { @@ -134,6 +141,19 @@ export class ThermostatToMatter extends GenericDeviceToMatter { if (this.#ioBrokerDevice.hasHumidity()) { this.#matterEndpointHumidity = new Endpoint(HumiditySensorDevice, { id: `${uuid}-Humidity` }); } + if (this.#ioBrokerDevice.hasBoost()) { + this.#matterEndpointBoost = new Endpoint( + OnOffPlugInUnitDevice.with( + EventedOnOffPlugInUnitOnOffServer, + IoBrokerEvents, + IoIdentifyServer, + IoBrokerContext, + ), + { + id: `${uuid}-BoostOnOff`, + }, + ); + } } get matterEndpoints(): Endpoint[] { @@ -141,6 +161,9 @@ export class ThermostatToMatter extends GenericDeviceToMatter { if (this.#matterEndpointHumidity) { endpoints.push(this.#matterEndpointHumidity); } + if (this.#matterEndpointBoost) { + endpoints.push(this.#matterEndpointBoost); + } return endpoints; } @@ -340,6 +363,13 @@ export class ThermostatToMatter extends GenericDeviceToMatter { ); } + if (this.#matterEndpointBoost) { + this.matterEvents.on( + this.#matterEndpointBoost.events.ioBrokerEvents.onOffControlled, + async on => await this.#ioBrokerDevice.setBoost(on), + ); + } + this.#ioBrokerDevice.onChange(async event => { switch (event.property) { case PropertyType.Temperature: @@ -414,6 +444,15 @@ export class ThermostatToMatter extends GenericDeviceToMatter { }); } break; + case PropertyType.Boost: + if (this.#matterEndpointBoost?.owner !== undefined) { + await this.#matterEndpointBoost?.set({ + onOff: { + onOff: event.value as boolean, + }, + }); + } + break; } }); diff --git a/test/devices.test.js b/test/devices.test.js index 6fa4db89..2f78e09b 100644 --- a/test/devices.test.js +++ b/test/devices.test.js @@ -38,6 +38,7 @@ const detectedDevices = { { name: 'TILT_STOP', id: '0_userdata.0.tilt_stop', type: 'boolean' }, { name: 'TILT_OPEN', id: '0_userdata.0.tilt_open', type: 'boolean' }, { name: 'TILT_CLOSE', id: '0_userdata.0.tilt_close', type: 'boolean' }, + { name: 'DOOR_STATE', id: '0_userdata.0.door_state', type: 'boolean' }, { name: 'PRESS', id: '0_userdata.0.press', type: 'boolean' }, { name: 'PRESS_LONG', id: '0_userdata.0.press_long', type: 'boolean' }, @@ -171,6 +172,7 @@ const detectedDevices = { { name: 'LOWBAT', id: '0_userdata.0.lowbat', type: 'boolean' }, { name: 'WORKING', id: '0_userdata.0.working', type: 'boolean' }, { name: 'DIRECTION', id: '0_userdata.0.direction', type: 'boolean' }, + { name: 'DIRECTION_ENUM', id: '0_userdata.0.direction_enum', type: 'enum' }, ], type: 'abstract', }; @@ -210,7 +212,7 @@ class Adapter { max: 200, unit: '°C', type: 'number', - states: entry.type === 'enum' ? { 0: 'Dummy' } : entry.type === 'mixed' ? { null: 'Dummy' } : undefined, + states: entry.type === 'enum' ? { 0: 'Dummy', 1: 'Dummy2' } : entry.type === 'mixed' ? { null: 'Dummy' } : undefined, }, type: 'state', }; @@ -345,7 +347,9 @@ describe('Test Devices', function () { ) { // Try to read value if (deviceObj.getPropertyValue(prop) === undefined) { - throw new Error(`Property "${prop}" (${properties[prop].valueType}) of "${type}" has no value`); + if (prop !== 'directionEnum') { // This enum is special because overlaps name wise with direction + throw new Error(`Property "${prop}" (${properties[prop].valueType}) of "${type}" has no value`); + } } else if (properties[prop].valueType === ValueType.Enum) { if (deviceObj.getPropertyValue(prop) !== 'Dummy') { throw new Error(`Property "${prop}" (Enum) of "${type}" has wrong value`);