From d8ce89ac10ddacdc7ff69b642cfb6d5b942e3f23 Mon Sep 17 00:00:00 2001 From: Ingo Fischer Date: Wed, 22 Jan 2025 18:44:18 +0100 Subject: [PATCH] Identify and PowerSource and UI optimizations (#322) * deps * power source opts * better return duplicate pairing error * refactor identify logic * add logging * remove existing devie check in UI * readme * adjust logging * ui update --- README.md | 6 + package-lock.json | 146 +++++++-------- package.json | 8 +- src-admin/package-lock.json | 48 ++--- src-admin/package.json | 6 +- src-admin/src/Tabs/Controller.tsx | 2 +- .../components/DiscoveredDevicesDialog.tsx | 16 -- src/lib/devices/Ct.ts | 4 +- src/lib/devices/GenericDevice.ts | 2 +- src/lib/devices/GenericLightingDevice.ts | 11 ++ src/lib/devices/Light.ts | 4 +- src/matter/ControllerNode.ts | 14 +- src/matter/DeviceNode.ts | 2 +- src/matter/IoBrokerObjectStorage.ts | 9 +- src/matter/behaviors/IdentifyServer.ts | 169 ++++++++++++++++- src/matter/behaviors/IoBrokerContext.ts | 17 +- .../to-iobroker/GenericDeviceToIoBroker.ts | 173 ++++++++++-------- src/matter/to-matter/BlindsToMatter.ts | 18 +- src/matter/to-matter/ButtonSensorToMatter.ts | 15 +- src/matter/to-matter/ButtonToMatter.ts | 13 +- src/matter/to-matter/CieToMatter.ts | 8 + src/matter/to-matter/CtToMatter.ts | 8 + src/matter/to-matter/DimmerToMatter.ts | 12 +- src/matter/to-matter/DoorToMatter.ts | 13 +- src/matter/to-matter/FloodAlarmToMatter.ts | 13 +- src/matter/to-matter/GenericDeviceToMatter.ts | 52 ------ .../GenericLightingDeviceToMatter.ts | 43 ----- src/matter/to-matter/HueAndRgbToMatter.ts | 8 + src/matter/to-matter/HumidityToMatter.ts | 15 +- src/matter/to-matter/IlluminanceToMatter.ts | 15 +- src/matter/to-matter/LightToMatter.ts | 17 +- src/matter/to-matter/LockToMatter.ts | 24 +-- src/matter/to-matter/MotionToMatter.ts | 13 +- src/matter/to-matter/SocketToMatter.ts | 47 ++--- src/matter/to-matter/TemperatureToMatter.ts | 20 +- src/matter/to-matter/ThermostatToMatter.ts | 18 +- src/matter/to-matter/WindowToMatter.ts | 13 +- 37 files changed, 598 insertions(+), 424 deletions(-) create mode 100644 src/lib/devices/GenericLightingDevice.ts diff --git a/README.md b/README.md index f8e3a271..f40a8e63 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,12 @@ With the ioBroker Matter Adapter it is possible to map the following use cases: --> ## Changelog + +### __WORK IN PROGRESS__ +* (@bluefox) Optimized UI +* (@Apollon77) Improved handling for Power Source cluster on root endpoint +* (@Apollon77) Changed Identify handling - Light will be turned on/off, others just logged + ### 0.4.0 (2025-01-20) * (@Apollon77) "SET" states are no longer updated when Actual states are present and get updated! * (@Apollon77) Initializes states also with "ack=false" states because better than no initial values diff --git a/package-lock.json b/package-lock.json index ea783446..7802f828 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,9 +13,9 @@ "@iobroker/dm-utils": "^1.0.6", "@iobroker/i18n": "^0.3.1", "@iobroker/type-detector": "^4.1.1", - "@matter/main": "0.12.0-alpha.0-20250118-678537a5f", - "@matter/nodejs": "0.12.0-alpha.0-20250118-678537a5f", - "@project-chip/matter.js": "0.12.0-alpha.0-20250118-678537a5f", + "@matter/main": "0.12.0-alpha.0-20250121-0ab1b29a1", + "@matter/nodejs": "0.12.0-alpha.0-20250121-0ab1b29a1", + "@project-chip/matter.js": "0.12.0-alpha.0-20250121-0ab1b29a1", "axios": "^1.7.9", "jsonwebtoken": "^9.0.2" }, @@ -40,7 +40,7 @@ "node": ">=18" }, "optionalDependencies": { - "@matter/nodejs-ble": "0.12.0-alpha.0-20250118-678537a5f" + "@matter/nodejs-ble": "0.12.0-alpha.0-20250121-0ab1b29a1" } }, "node_modules/@alcalzone/pak": { @@ -874,64 +874,64 @@ } }, "node_modules/@matter/general": { - "version": "0.12.0-alpha.0-20250118-678537a5f", - "resolved": "https://registry.npmjs.org/@matter/general/-/general-0.12.0-alpha.0-20250118-678537a5f.tgz", - "integrity": "sha512-zBL6aFa6NdRdj2z1jgsldkmsd4+8Z67W01JBc8Xfc0aifKevgfn7EEzEwrZ2a0Zos50UzBUSiXVygBylFI3fiQ==", + "version": "0.12.0-alpha.0-20250121-0ab1b29a1", + "resolved": "https://registry.npmjs.org/@matter/general/-/general-0.12.0-alpha.0-20250121-0ab1b29a1.tgz", + "integrity": "sha512-NflxKytbLdnPFlN+mUh2U9/bAit+WtraKBcaUoowlDHRtTlxEfPXXwIanj8dCxNd7XtFJ2vaOPeFGLd0Ep37KQ==", "license": "Apache-2.0", "dependencies": { - "@noble/curves": "^1.8.0" + "@noble/curves": "^1.8.1" } }, "node_modules/@matter/main": { - "version": "0.12.0-alpha.0-20250118-678537a5f", - "resolved": "https://registry.npmjs.org/@matter/main/-/main-0.12.0-alpha.0-20250118-678537a5f.tgz", - "integrity": "sha512-AVurLqe6TbNgHFDSsLKR6guCcVzlspARGdPPwmxz318vdo31bsDs4sA25XKA21HnjvFCO8x/btV9edWCXorIYw==", + "version": "0.12.0-alpha.0-20250121-0ab1b29a1", + "resolved": "https://registry.npmjs.org/@matter/main/-/main-0.12.0-alpha.0-20250121-0ab1b29a1.tgz", + "integrity": "sha512-frK2xqrpzHIO5fzbIyj5QQm5mpNuyta01kCK0gdID7xJNWLEIctq9tvUJZnGVbXRn7SsSOsDo2n6BVIu0wZnSg==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.12.0-alpha.0-20250118-678537a5f", - "@matter/model": "0.12.0-alpha.0-20250118-678537a5f", - "@matter/node": "0.12.0-alpha.0-20250118-678537a5f", - "@matter/protocol": "0.12.0-alpha.0-20250118-678537a5f", - "@matter/types": "0.12.0-alpha.0-20250118-678537a5f", - "@noble/curves": "^1.7.0" + "@matter/general": "0.12.0-alpha.0-20250121-0ab1b29a1", + "@matter/model": "0.12.0-alpha.0-20250121-0ab1b29a1", + "@matter/node": "0.12.0-alpha.0-20250121-0ab1b29a1", + "@matter/protocol": "0.12.0-alpha.0-20250121-0ab1b29a1", + "@matter/types": "0.12.0-alpha.0-20250121-0ab1b29a1", + "@noble/curves": "^1.8.1" }, "optionalDependencies": { - "@matter/nodejs": "0.12.0-alpha.0-20250118-678537a5f" + "@matter/nodejs": "0.12.0-alpha.0-20250121-0ab1b29a1" } }, "node_modules/@matter/model": { - "version": "0.12.0-alpha.0-20250118-678537a5f", - "resolved": "https://registry.npmjs.org/@matter/model/-/model-0.12.0-alpha.0-20250118-678537a5f.tgz", - "integrity": "sha512-FPuPcXKsOy40OYVvdt/vLZ0yuhaSMPx/z6KJREKKLwxh+32yIrx8rcQ4NwgMR9ibcaU0musAcHUGbeQWZhVBpw==", + "version": "0.12.0-alpha.0-20250121-0ab1b29a1", + "resolved": "https://registry.npmjs.org/@matter/model/-/model-0.12.0-alpha.0-20250121-0ab1b29a1.tgz", + "integrity": "sha512-/+u3lfQqaezt0hgeu2bMqttvYfTca1rmWUuKxjwqNmMkSNu95kh5WLlWHJUNY5jz3ylgGvF33Kfh7rSyGySCtg==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.12.0-alpha.0-20250118-678537a5f", - "@noble/curves": "^1.7.0" + "@matter/general": "0.12.0-alpha.0-20250121-0ab1b29a1", + "@noble/curves": "^1.8.1" } }, "node_modules/@matter/node": { - "version": "0.12.0-alpha.0-20250118-678537a5f", - "resolved": "https://registry.npmjs.org/@matter/node/-/node-0.12.0-alpha.0-20250118-678537a5f.tgz", - "integrity": "sha512-KEqFTPb7Ua+g+KakHqK7BvVtMUnaVWvZV2hrHIKfAfJsBaO6LkrUSWaKuHKA6szeex5UTODX26W4W9CaDpHTVQ==", + "version": "0.12.0-alpha.0-20250121-0ab1b29a1", + "resolved": "https://registry.npmjs.org/@matter/node/-/node-0.12.0-alpha.0-20250121-0ab1b29a1.tgz", + "integrity": "sha512-TNDvRQKyX+mxkVneUq3IMJm8r5GKJvEeVVqCEZLvMnbkFAsh97zA2jWtUGdvDG+/efuF7/w2LCGxc3nBE4db/A==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.12.0-alpha.0-20250118-678537a5f", - "@matter/model": "0.12.0-alpha.0-20250118-678537a5f", - "@matter/protocol": "0.12.0-alpha.0-20250118-678537a5f", - "@matter/types": "0.12.0-alpha.0-20250118-678537a5f", - "@noble/curves": "^1.7.0" + "@matter/general": "0.12.0-alpha.0-20250121-0ab1b29a1", + "@matter/model": "0.12.0-alpha.0-20250121-0ab1b29a1", + "@matter/protocol": "0.12.0-alpha.0-20250121-0ab1b29a1", + "@matter/types": "0.12.0-alpha.0-20250121-0ab1b29a1", + "@noble/curves": "^1.8.1" } }, "node_modules/@matter/nodejs": { - "version": "0.12.0-alpha.0-20250118-678537a5f", - "resolved": "https://registry.npmjs.org/@matter/nodejs/-/nodejs-0.12.0-alpha.0-20250118-678537a5f.tgz", - "integrity": "sha512-QLjQAXwe8dWyDcW+pS7ugIJKG3r1HuXgOYZNjUXF4EGugozcttSDcWwuD/Qn1z1kDv5kpKHyv+DUabWN1XINmQ==", + "version": "0.12.0-alpha.0-20250121-0ab1b29a1", + "resolved": "https://registry.npmjs.org/@matter/nodejs/-/nodejs-0.12.0-alpha.0-20250121-0ab1b29a1.tgz", + "integrity": "sha512-6hjM22O2xg8jDhIXA2GhoANs7R2RhblGr4ITB4wbf8IPw/JisISydOs6QSxENCfP9zR7fpQ1CfTcVj4c4SMKrQ==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.12.0-alpha.0-20250118-678537a5f", - "@matter/node": "0.12.0-alpha.0-20250118-678537a5f", - "@matter/protocol": "0.12.0-alpha.0-20250118-678537a5f", - "@matter/types": "0.12.0-alpha.0-20250118-678537a5f", + "@matter/general": "0.12.0-alpha.0-20250121-0ab1b29a1", + "@matter/node": "0.12.0-alpha.0-20250121-0ab1b29a1", + "@matter/protocol": "0.12.0-alpha.0-20250121-0ab1b29a1", + "@matter/types": "0.12.0-alpha.0-20250121-0ab1b29a1", "node-localstorage": "^3.0.5" }, "engines": { @@ -939,15 +939,15 @@ } }, "node_modules/@matter/nodejs-ble": { - "version": "0.12.0-alpha.0-20250118-678537a5f", - "resolved": "https://registry.npmjs.org/@matter/nodejs-ble/-/nodejs-ble-0.12.0-alpha.0-20250118-678537a5f.tgz", - "integrity": "sha512-N+bOOnvBCYfZYcGDIQE4Ykl7M8DcKbPuHbOqypAhZebUE/MJvOIjohFQ+A1LxyZa3RaCqbx48uS1EJ4cCWf1YQ==", + "version": "0.12.0-alpha.0-20250121-0ab1b29a1", + "resolved": "https://registry.npmjs.org/@matter/nodejs-ble/-/nodejs-ble-0.12.0-alpha.0-20250121-0ab1b29a1.tgz", + "integrity": "sha512-1Ppz2pJwyLyc6zPjjpN4MUSDnhnAizu9kk6JXsnqwo48+lxcy2siBZd7JxFO3OsFgiayqvqAhMxZ7pQ7ffkFGQ==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@matter/general": "0.12.0-alpha.0-20250118-678537a5f", - "@matter/protocol": "0.12.0-alpha.0-20250118-678537a5f", - "@matter/types": "0.12.0-alpha.0-20250118-678537a5f" + "@matter/general": "0.12.0-alpha.0-20250121-0ab1b29a1", + "@matter/protocol": "0.12.0-alpha.0-20250121-0ab1b29a1", + "@matter/types": "0.12.0-alpha.0-20250121-0ab1b29a1" }, "engines": { "node": ">=18.0.0" @@ -958,35 +958,35 @@ } }, "node_modules/@matter/protocol": { - "version": "0.12.0-alpha.0-20250118-678537a5f", - "resolved": "https://registry.npmjs.org/@matter/protocol/-/protocol-0.12.0-alpha.0-20250118-678537a5f.tgz", - "integrity": "sha512-Vyht6Ai+6D8ZEyWNqqlKyz1OeljbjZJmY3OUs6gVar5D8LJ8kcFLqyercVwomOBzaR00dgJYj+RITFYMb7l1mw==", + "version": "0.12.0-alpha.0-20250121-0ab1b29a1", + "resolved": "https://registry.npmjs.org/@matter/protocol/-/protocol-0.12.0-alpha.0-20250121-0ab1b29a1.tgz", + "integrity": "sha512-y2nX5obGB5xpFDu+JRUeTu4GJnjq3fQPNjWTUxpW1P6UOa4kd0vOjkw7WIHl+KIitulVcU5PNqzrUGS6z9G0Vw==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.12.0-alpha.0-20250118-678537a5f", - "@matter/model": "0.12.0-alpha.0-20250118-678537a5f", - "@matter/types": "0.12.0-alpha.0-20250118-678537a5f", - "@noble/curves": "^1.8.0" + "@matter/general": "0.12.0-alpha.0-20250121-0ab1b29a1", + "@matter/model": "0.12.0-alpha.0-20250121-0ab1b29a1", + "@matter/types": "0.12.0-alpha.0-20250121-0ab1b29a1", + "@noble/curves": "^1.8.1" } }, "node_modules/@matter/types": { - "version": "0.12.0-alpha.0-20250118-678537a5f", - "resolved": "https://registry.npmjs.org/@matter/types/-/types-0.12.0-alpha.0-20250118-678537a5f.tgz", - "integrity": "sha512-mp8Pq5UMImgv0qt6K7pY3rQVdNXrdo2VC7Re5WAZfLwjVFJaQOwELfpknl495NV9SCAu5KqMfc5iN84reC3k5g==", + "version": "0.12.0-alpha.0-20250121-0ab1b29a1", + "resolved": "https://registry.npmjs.org/@matter/types/-/types-0.12.0-alpha.0-20250121-0ab1b29a1.tgz", + "integrity": "sha512-Cqnrfd0ZXg43AJIWR6a+6HvQsLnSFi0JLJLezLNqelCa+6woYd4m61s/bscZK0xdp3FxcTFCE8jaCJ/tMlndAw==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.12.0-alpha.0-20250118-678537a5f", - "@matter/model": "0.12.0-alpha.0-20250118-678537a5f", - "@noble/curves": "^1.7.0" + "@matter/general": "0.12.0-alpha.0-20250121-0ab1b29a1", + "@matter/model": "0.12.0-alpha.0-20250121-0ab1b29a1", + "@noble/curves": "^1.8.1" } }, "node_modules/@noble/curves": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.0.tgz", - "integrity": "sha512-j84kjAbzEnQHaSIhRPUmB3/eVXu2k3dKPl2LOrR8fSOIL+89U+7lV117EWHtq/GHM3ReGHM46iRBdZfpc4HRUQ==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz", + "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", "license": "MIT", "dependencies": { - "@noble/hashes": "1.7.0" + "@noble/hashes": "1.7.1" }, "engines": { "node": "^14.21.3 || >=16" @@ -996,9 +996,9 @@ } }, "node_modules/@noble/hashes": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.0.tgz", - "integrity": "sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", + "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", "license": "MIT", "engines": { "node": "^14.21.3 || >=16" @@ -1139,17 +1139,17 @@ } }, "node_modules/@project-chip/matter.js": { - "version": "0.12.0-alpha.0-20250118-678537a5f", - "resolved": "https://registry.npmjs.org/@project-chip/matter.js/-/matter.js-0.12.0-alpha.0-20250118-678537a5f.tgz", - "integrity": "sha512-+3jOaQa8XZ88Vod+AfRLvc21Gae4I/5J/eO/0jvLNOb/FNTgfbSazqc3QsKoHxoFZU6qsgn8EoBM22CtnsfM5w==", + "version": "0.12.0-alpha.0-20250121-0ab1b29a1", + "resolved": "https://registry.npmjs.org/@project-chip/matter.js/-/matter.js-0.12.0-alpha.0-20250121-0ab1b29a1.tgz", + "integrity": "sha512-Y5fg5wikxGzjCCm6BjxFZgwEu9B/NXi5qOvV3virHJE9tvT+tDwtPYyTLKeLhciiMO2avUzqC7gJkc2SzqfFyQ==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.12.0-alpha.0-20250118-678537a5f", - "@matter/model": "0.12.0-alpha.0-20250118-678537a5f", - "@matter/node": "0.12.0-alpha.0-20250118-678537a5f", - "@matter/protocol": "0.12.0-alpha.0-20250118-678537a5f", - "@matter/types": "0.12.0-alpha.0-20250118-678537a5f", - "@noble/curves": "^1.7.0" + "@matter/general": "0.12.0-alpha.0-20250121-0ab1b29a1", + "@matter/model": "0.12.0-alpha.0-20250121-0ab1b29a1", + "@matter/node": "0.12.0-alpha.0-20250121-0ab1b29a1", + "@matter/protocol": "0.12.0-alpha.0-20250121-0ab1b29a1", + "@matter/types": "0.12.0-alpha.0-20250121-0ab1b29a1", + "@noble/curves": "^1.8.1" } }, "node_modules/@puppeteer/browsers": { diff --git a/package.json b/package.json index 22e3d1e8..464955db 100644 --- a/package.json +++ b/package.json @@ -23,16 +23,16 @@ "url": "https://github.com/ioBroker/ioBroker.matter" }, "optionalDependencies": { - "@matter/nodejs-ble": "0.12.0-alpha.0-20250118-678537a5f" + "@matter/nodejs-ble": "0.12.0-alpha.0-20250121-0ab1b29a1" }, "dependencies": { "@iobroker/adapter-core": "^3.2.3", "@iobroker/i18n": "^0.3.1", "@iobroker/dm-utils": "^1.0.6", "@iobroker/type-detector": "^4.1.1", - "@matter/main": "0.12.0-alpha.0-20250118-678537a5f", - "@matter/nodejs": "0.12.0-alpha.0-20250118-678537a5f", - "@project-chip/matter.js": "0.12.0-alpha.0-20250118-678537a5f", + "@matter/main": "0.12.0-alpha.0-20250121-0ab1b29a1", + "@matter/nodejs": "0.12.0-alpha.0-20250121-0ab1b29a1", + "@project-chip/matter.js": "0.12.0-alpha.0-20250121-0ab1b29a1", "axios": "^1.7.9", "jsonwebtoken": "^9.0.2" }, diff --git a/src-admin/package-lock.json b/src-admin/package-lock.json index 51534b82..58e574c9 100644 --- a/src-admin/package-lock.json +++ b/src-admin/package-lock.json @@ -9,8 +9,8 @@ "version": "0.4.0", "dependencies": { "@foxriver76/iob-component-lib": "^0.2.0", - "@iobroker/adapter-react-v5": "^7.4.14", - "@iobroker/dm-gui-components": "^7.4.14", + "@iobroker/adapter-react-v5": "^7.4.15", + "@iobroker/dm-gui-components": "^7.4.15", "@iobroker/type-detector": "^4.1.1", "@types/react-dom": "^18.3.5", "@types/uuid": "^10.0.0", @@ -988,9 +988,9 @@ } }, "node_modules/@iobroker/adapter-react-v5": { - "version": "7.4.14", - "resolved": "https://registry.npmjs.org/@iobroker/adapter-react-v5/-/adapter-react-v5-7.4.14.tgz", - "integrity": "sha512-4iDWeUVRf10ofBn+QqaIoPZQqd9vadumKIZdwBgSrQvdyrawk9Q+lf6BLkt0+Ln//rqfbVZG51CJrz47ZP+Xmg==", + "version": "7.4.15", + "resolved": "https://registry.npmjs.org/@iobroker/adapter-react-v5/-/adapter-react-v5-7.4.15.tgz", + "integrity": "sha512-La0e90iYGynJ3/fosSCI3NZOwsCcYmirDVRA2b24RCzlfkBp+rhqFb4s4w2m3CFOeipdQ1mkBhkjSFFp5DwnTQ==", "license": "MIT", "dependencies": { "@emotion/react": "^11.13.5", @@ -1119,13 +1119,13 @@ } }, "node_modules/@iobroker/dm-gui-components": { - "version": "7.4.14", - "resolved": "https://registry.npmjs.org/@iobroker/dm-gui-components/-/dm-gui-components-7.4.14.tgz", - "integrity": "sha512-RTGfx5BlpzkibkJxxZKTSCk71nsO96Iwf1B1Se0SgQ1F1l/bALDwCgiPVXRY2p37KhG1j/EtDPCicTlQOGtZ1g==", + "version": "7.4.15", + "resolved": "https://registry.npmjs.org/@iobroker/dm-gui-components/-/dm-gui-components-7.4.15.tgz", + "integrity": "sha512-4wQns8df7hXeJdNHqWIDl1ItcnPfitcU+spVS9NiZNRubCLo9fTsSaYHhAVrCmiEoV34n5VGJFBK6XhDKaP6mA==", "license": "MIT", "dependencies": { - "@iobroker/adapter-react-v5": "7.4.14", - "@iobroker/json-config": "7.4.14" + "@iobroker/adapter-react-v5": "7.4.15", + "@iobroker/json-config": "7.4.15" } }, "node_modules/@iobroker/js-controller-common": { @@ -1185,11 +1185,11 @@ } }, "node_modules/@iobroker/json-config": { - "version": "7.4.14", - "resolved": "https://registry.npmjs.org/@iobroker/json-config/-/json-config-7.4.14.tgz", - "integrity": "sha512-VT58k0NzriZZD21FDUXU3IYRyu/lfd3wCdeEpEMyf0eGr6+WQU5KUnBeCWLDU6m8BgGsqatJAmMtz31Je8od+w==", + "version": "7.4.15", + "resolved": "https://registry.npmjs.org/@iobroker/json-config/-/json-config-7.4.15.tgz", + "integrity": "sha512-PkujtS8pJw8EBTMoY9TO38iPjdBWoP14kKJPXwcieRo5ZW1+uaTA/e6/oAUqU2imRuO4GiSjlNsEdPF5W5rZcg==", "dependencies": { - "@iobroker/adapter-react-v5": "7.4.14", + "@iobroker/adapter-react-v5": "7.4.15", "@mui/x-date-pickers": "^7.23.0", "crypto-js": "^4.2.0", "react-ace": "^13.0.0", @@ -1508,14 +1508,14 @@ } }, "node_modules/@mui/x-date-pickers": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.23.6.tgz", - "integrity": "sha512-jt6rEAYLju3NZe3y2S+I5KcTiSHV79FW0jeNUEUTceg1qsPzseHbND66k3zVF0hO3N2oZtLtPywof6vN5Doe+Q==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.24.0.tgz", + "integrity": "sha512-oBM9Yp2H3tJ7qoHB4APQJYxZG4rz6JD4CwLzbzD9o3r+E1HGpGSLhwK3rDEz9VEjbOq8893Z2TGYLLWoyjeFXQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.7", "@mui/utils": "^5.16.6 || ^6.0.0", - "@mui/x-internals": "7.23.6", + "@mui/x-internals": "7.24.0", "@types/react-transition-group": "^4.4.11", "clsx": "^2.1.1", "prop-types": "^15.8.1", @@ -1574,9 +1574,9 @@ } }, "node_modules/@mui/x-internals": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.23.6.tgz", - "integrity": "sha512-hT1Pa4PNCnxwiauPbYMC3p4DiEF1x05Iu4C1MtC/jMJ1LtthymLmTuQ6ZQ53/R9FeqK6sYd6A6noR+vNMjp5DA==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.24.0.tgz", + "integrity": "sha512-lYa/XLltxNMY8YAFDopIHrXda2EAoqMCilyGMuPMz+WTG+b+StlUKqtj8cgFPQ/sa5dQ2fR7R3KJdjLREKUrlQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.7", @@ -2087,9 +2087,9 @@ } }, "node_modules/ace-builds": { - "version": "1.37.4", - "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.37.4.tgz", - "integrity": "sha512-niaXM/b7Nm6l/GKpc/jtG0jjExBOkqRN1pZbyV/ngb3GrQQF5fCB2032n5qaaAr7hWSGbc+PGfZ3C0LsmYQptA==", + "version": "1.37.5", + "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.37.5.tgz", + "integrity": "sha512-VMJ4Cnhq6L9dwvOCyuyyvQuiVTSwdZC7zDKJBBBJJax0wGQ7MvzQZFoi0gMmCm2I4Zuv/ZbtwU/dlglIhCNLhw==", "license": "BSD-3-Clause" }, "node_modules/acorn": { diff --git a/src-admin/package.json b/src-admin/package.json index 01a62f14..29702da9 100644 --- a/src-admin/package.json +++ b/src-admin/package.json @@ -7,8 +7,8 @@ }, "dependencies": { "@foxriver76/iob-component-lib": "^0.2.0", - "@iobroker/adapter-react-v5": "^7.4.14", - "@iobroker/dm-gui-components": "^7.4.14", + "@iobroker/adapter-react-v5": "^7.4.15", + "@iobroker/dm-gui-components": "^7.4.15", "@iobroker/type-detector": "^4.1.1", "@types/react-dom": "^18.3.5", "@types/uuid": "^10.0.0", @@ -51,4 +51,4 @@ } ] ] -} \ No newline at end of file +} diff --git a/src-admin/src/Tabs/Controller.tsx b/src-admin/src/Tabs/Controller.tsx index 3ba150c0..85fdcdd4 100644 --- a/src-admin/src/Tabs/Controller.tsx +++ b/src-admin/src/Tabs/Controller.tsx @@ -536,7 +536,7 @@ class Controller extends Component { this.setState({ backendProcessingActive: false }); if (result.error || !result.result) { - window.alert(`Cannot connect: ${result.error || 'Unknown error'}`); + window.alert(`Cannot pair device: ${result.error || 'Unknown error'}`); } else { window.alert(I18n.t('Connected')); this.refDeviceManager.current?.loadData(); diff --git a/src-admin/src/components/DiscoveredDevicesDialog.tsx b/src-admin/src/components/DiscoveredDevicesDialog.tsx index 60ee5981..d7cdb1eb 100644 --- a/src-admin/src/components/DiscoveredDevicesDialog.tsx +++ b/src-admin/src/components/DiscoveredDevicesDialog.tsx @@ -33,7 +33,6 @@ interface DiscoveredDevicesDialogProps { } interface DiscoveredDevicesDialogState { - existing: string[]; /** Was a discovery result received which means that the dialog should stay open also after discovery ended */ discoveryDone: boolean; /** The discovery process is active in the backend */ @@ -50,7 +49,6 @@ export default class DiscoveredDevicesDialog extends Component< constructor(props: DiscoveredDevicesDialogProps) { super(props); this.state = { - existing: [], discoveryRunning: false, discoveryDone: false, discovered: [], @@ -129,19 +127,6 @@ export default class DiscoveredDevicesDialog extends Component< } async componentDidMount(): Promise { - // Read existing devices - const folders = await this.props.socket.getObjectViewSystem( - 'folder', - `matter.${this.props.instance}.`, - `matter.${this.props.instance}.\u9999`, - ); - - this.setState({ - existing: Object.keys(folders) - .map(id => id.split('.').pop() || '') - .filter(a => a), - }); - await this.props.socket.subscribeState( `matter.${this.props.instance}.controller.info.discovering`, this.onStateChange, @@ -243,7 +228,6 @@ export default class DiscoveredDevicesDialog extends Component< this.setState({ showQrCodeDialog: device })} /> diff --git a/src/lib/devices/Ct.ts b/src/lib/devices/Ct.ts index aa38d73d..730fce06 100644 --- a/src/lib/devices/Ct.ts +++ b/src/lib/devices/Ct.ts @@ -1,8 +1,8 @@ import { type DeviceStateObject, PropertyType, ValueType } from './DeviceStateObject'; -import { ElectricityDataDevice } from './ElectricityDataDevice'; import { type DetectedDevice, type DeviceOptions, StateAccessType } from './GenericDevice'; +import { GenericLightingDevice } from './GenericLightingDevice'; -export class Ct extends ElectricityDataDevice { +export class Ct extends GenericLightingDevice { #dimmerState?: DeviceStateObject; #brightnessState?: DeviceStateObject; #temperatureState?: DeviceStateObject; diff --git a/src/lib/devices/GenericDevice.ts b/src/lib/devices/GenericDevice.ts index a9568a21..4a7a774f 100644 --- a/src/lib/devices/GenericDevice.ts +++ b/src/lib/devices/GenericDevice.ts @@ -201,7 +201,7 @@ export abstract class GenericDevice extends EventEmitter { } } - isActionAllowedByIdentify(): boolean { + get isActionAllowedByIdentify(): boolean { return !!this.options?.actionAllowedByIdentify; } diff --git a/src/lib/devices/GenericLightingDevice.ts b/src/lib/devices/GenericLightingDevice.ts new file mode 100644 index 00000000..cf6f87f0 --- /dev/null +++ b/src/lib/devices/GenericLightingDevice.ts @@ -0,0 +1,11 @@ +import { ElectricityDataDevice } from './ElectricityDataDevice'; + +export abstract class GenericLightingDevice extends ElectricityDataDevice { + abstract hasPower(): boolean; + + abstract getPower(): boolean | undefined; + + abstract setPower(value: boolean): Promise; + + abstract updatePower(value: boolean): Promise; +} diff --git a/src/lib/devices/Light.ts b/src/lib/devices/Light.ts index 1af616d1..dab57bc9 100644 --- a/src/lib/devices/Light.ts +++ b/src/lib/devices/Light.ts @@ -1,8 +1,8 @@ import { type DeviceStateObject, PropertyType, ValueType } from './DeviceStateObject'; -import { ElectricityDataDevice } from './ElectricityDataDevice'; import { type DetectedDevice, type DeviceOptions, StateAccessType } from './GenericDevice'; +import { GenericLightingDevice } from './GenericLightingDevice'; -export class Light extends ElectricityDataDevice { +export class Light extends GenericLightingDevice { #setPowerState?: DeviceStateObject; #getPowerState?: DeviceStateObject; diff --git a/src/matter/ControllerNode.ts b/src/matter/ControllerNode.ts index bf3bf869..5ac29a08 100644 --- a/src/matter/ControllerNode.ts +++ b/src/matter/ControllerNode.ts @@ -5,6 +5,7 @@ import { type CommissionableDevice, type ControllerCommissioningFlowOptions, type DiscoveryData, + CommissioningError, } from '@matter/main/protocol'; import { ManualPairingCodeCodec, QrPairingCodeCodec } from '@matter/main/types'; import { NodeJsBle } from '@matter/nodejs-ble'; @@ -146,12 +147,19 @@ class Controller implements GeneralNode { // We return the pollingId and execute the commissioning async this.commissionDevice(options) .then(result => this.#commissioningStatus.set(pollingId, { status: 'finished', result })) - .catch(error => + .catch(error => { + if (error instanceof CommissioningError) { + // TODO Remove after next matter.js update + if (error.message.startsWith('Commission error for "addNoc": 9,')) { + error.message = + 'This device is already paired to this Controller! You can not pair it again.'; + } + } this.#commissioningStatus.set(pollingId, { status: 'error', result: { error: error.message }, - }), - ) + }); + }) .finally(() => setTimeout(() => this.#commissioningStatus.delete(pollingId), 60 * 60_000)); return { result: { pollingId } }; } diff --git a/src/matter/DeviceNode.ts b/src/matter/DeviceNode.ts index 7cf3190c..ad83b93d 100644 --- a/src/matter/DeviceNode.ts +++ b/src/matter/DeviceNode.ts @@ -188,7 +188,7 @@ class Device extends BaseServerNode { return; } if (this.serverNode.lifecycle.isCommissioned) { - this.adapter.log.error('Device is already commissioned ... what should change? Ignoring changes'); + this.adapter.log.debug('Device is already commissioned ... Ignoring changes because non allowed'); return; } diff --git a/src/matter/IoBrokerObjectStorage.ts b/src/matter/IoBrokerObjectStorage.ts index 0d74340e..67bc23e1 100644 --- a/src/matter/IoBrokerObjectStorage.ts +++ b/src/matter/IoBrokerObjectStorage.ts @@ -128,8 +128,9 @@ export class IoBrokerObjectStorage implements MaybeAsyncStorage { if (this.#localStorageManager && this.#isLocallyStored(contexts)) { return this.#localStorageManager.get(contexts, key); } + const oid = this.buildKey(contexts, key); try { - const valueState = await this.#adapter.getStateAsync(this.buildKey(contexts, key)); + const valueState = await this.#adapter.getStateAsync(oid); if (valueState === null || valueState === undefined) { return undefined; } @@ -141,7 +142,7 @@ export class IoBrokerObjectStorage implements MaybeAsyncStorage { } return fromJson(valueState.val) as T; } catch (error) { - this.#adapter.log.error(`[STORAGE] Cannot read state: ${error.message}`); + this.#adapter.log.error(`[STORAGE] Cannot read state ${oid}: ${error.message}`); } } @@ -219,7 +220,7 @@ export class IoBrokerObjectStorage implements MaybeAsyncStorage { } await this.#adapter.setState(oid, toJson(value), true); } catch (error) { - this.#adapter.log.error(`[STORAGE] Cannot save state: ${error.message}`); + this.#adapter.log.error(`[STORAGE] Cannot save state ${oid}: ${error.message}`); } } @@ -254,7 +255,7 @@ export class IoBrokerObjectStorage implements MaybeAsyncStorage { try { await this.#adapter.delObjectAsync(oid); } catch (error) { - this.#adapter.log.error(`[STORAGE] Cannot delete state: ${error.message}`); + this.#adapter.log.error(`[STORAGE] Cannot delete state ${oid}: ${error.message}`); } this.#existingObjectIds.delete(oid); } diff --git a/src/matter/behaviors/IdentifyServer.ts b/src/matter/behaviors/IdentifyServer.ts index a1103d6f..0a4f79a4 100644 --- a/src/matter/behaviors/IdentifyServer.ts +++ b/src/matter/behaviors/IdentifyServer.ts @@ -1,9 +1,170 @@ import { IdentifyServer } from '@matter/main/behaviors'; +import { Identify } from '@matter/main/clusters'; +import { Time, MaybePromise, type Timer } from '@matter/main'; +import { IoBrokerContext } from './IoBrokerContext'; +import { GenericLightingDevice } from '../../lib/devices/GenericLightingDevice'; +import type { MatterAdapter } from '../../main'; +import type { GenericDevice } from '../../lib/devices/GenericDevice'; -export class LoggingIdentifyServer extends IdentifyServer {} +export interface IdentifyState { + currentState?: boolean; + initialState?: boolean; +} + +export class IoIdentifyServer extends IdentifyServer { + protected declare internal: IoIdentifyServer.Internal; + + override async initialize(): Promise { + if (this.state.identifyType === undefined) { + this.state.identifyType = Identify.IdentifyType.Display; + } + super.initialize(); + + // Init from context + const context = await this.agent.load(IoBrokerContext); + this.internal.adapter = context.state.adapter as MatterAdapter; + this.internal.device = context.state.device; + + // Register events + this.reactTo(this.events.startIdentifying, this.startIdentifying); + this.reactTo(this.events.stopIdentifying, this.stopIdentifying); + } + + startIdentifying(): void { + this.internal.identifyHandlerTimeout = Time.getPeriodicTimer('Identify', 1000, () => + MaybePromise.then( + () => this.handleIoIdentify(false), + () => {}, + error => { + this.internal.adapter.log.warn(`Error during identify: ${error.message}`); + }, + ), + ).start(); + MaybePromise.then( + () => this.handleIoIdentify(true), + () => {}, + error => { + this.internal.adapter.log.warn(`Error during identify: ${error.message}`); + }, + ); + } + + stopIdentifying(): void { + this.internal.identifyHandlerTimeout?.stop(); + this.internal.identifyHandlerTimeout = undefined; + } + + handleIoIdentify(startToIdentify: boolean): MaybePromise { + if (startToIdentify) { + // TODO push info to UI and show popup, ideally get the name from the device + // or when using uuid be aware it could also be a bridged uuid + this.internal.adapter.log.info(`Identify started for ${this.internal.device.uuid}`); + } else { + this.internal.adapter.log.info(`Identify continues for ${this.internal.device.uuid}`); + } + } +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace IoIdentifyServer { + export class Internal extends IdentifyServer.Internal { + identifyHandlerTimeout?: Timer; + adapter!: MatterAdapter; + device!: GenericDevice; + } +} + +export class IoLightingIdentifyServer extends IoIdentifyServer { + protected declare internal: IoLightingIdentifyServer.Internal; + + override async initialize(): Promise { + this.state.identifyType = Identify.IdentifyType.LightOutput; + return super.initialize(); + } + + stopIdentifying(): void { + super.stopIdentifying(); + + if (this.internal.identifyState === undefined) { + return; + } + const initialState = this.internal.identifyState.initialState; + this.internal.identifyState = undefined; + if (initialState !== undefined) { + if ( + initialState && + this.internal.device.isActionAllowedByIdentify && + this.internal.device instanceof GenericLightingDevice && + this.internal.device.hasPower() + ) { + this.internal.device + .setPower(initialState) + .catch(error => + this.internal.adapter.log.info(`Can not reset state after identify: ${error.message}`), + ); + } + } + } + + async handleIoIdentify(startToIdentify: boolean): Promise { + if (startToIdentify) { + let currentState = false; + if ( + this.internal.device.isActionAllowedByIdentify && + this.internal.device instanceof GenericLightingDevice && + this.internal.device.hasPower() + ) { + currentState = !!this.internal.device.getPower(); + } + this.internal.identifyState = { currentState, initialState: currentState }; + } + + if (!this.internal.identifyState) { + return; + } + + if ( + this.internal.device.isActionAllowedByIdentify && + this.internal.device instanceof GenericLightingDevice && + this.internal.device.hasPower() + ) { + this.internal.identifyState.currentState = !this.internal.identifyState.currentState; + await this.internal.device.setPower(this.internal.identifyState.currentState); + if (startToIdentify) { + super.handleIoIdentify(startToIdentify); + } + } else { + super.handleIoIdentify(startToIdentify); + } + } + + override async triggerEffect(): Promise { + if (this.internal.identifyTimer?.isRunning) { + return; + } + if ( + this.internal.device.isActionAllowedByIdentify && + this.internal.device instanceof GenericLightingDevice && + this.internal.device.hasPower() + ) { + const currentState = !!this.internal.device.getPower(); + await this.internal.device.setPower(!currentState); + this.internal.identifyTimer = Time.getTimer('Identify effect', 1000, () => + (this.internal.device as GenericLightingDevice) + .setPower(currentState) + .catch(error => + this.internal.adapter.log.info(`Can not reset state after identify: ${error.message}`), + ), + ).start(); + } else { + this.internal.adapter.log.info(`Triggered identify effect for ${this.internal.device.uuid}`); + } + } +} -export class LightingIdentifyServer extends IdentifyServer { - override triggerEffect(): void { - // TODO +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace IoLightingIdentifyServer { + export class Internal extends IoIdentifyServer.Internal { + identifyState?: IdentifyState; } } diff --git a/src/matter/behaviors/IoBrokerContext.ts b/src/matter/behaviors/IoBrokerContext.ts index 5aaf21de..dcdc6d1f 100644 --- a/src/matter/behaviors/IoBrokerContext.ts +++ b/src/matter/behaviors/IoBrokerContext.ts @@ -1,25 +1,26 @@ import { Behavior } from '@matter/main'; import type { GenericDevice } from '../../lib/devices/GenericDevice'; -import type { MatterAdapter } from '../../main'; -export class IoBrokerContextBehavior extends Behavior { +export class IoBrokerContext extends Behavior { static override readonly id = 'ioBrokerContext'; - declare state: IoBrokerContextBehavior.State; + static override readonly early = true; + + declare state: IoBrokerContext.State; override initialize(): void { - if (!this.state.adapter) { - throw new Error('Adapter not set'); - } if (!this.state.device) { throw new Error('Device not set'); } + if (!this.state.adapter) { + throw new Error('Adapter not set'); + } } } // eslint-disable-next-line @typescript-eslint/no-namespace -export namespace IoBrokerContextBehavior { +export namespace IoBrokerContext { export class State { - adapter!: MatterAdapter; + adapter!: ioBroker.Adapter; device!: GenericDevice; } } diff --git a/src/matter/to-iobroker/GenericDeviceToIoBroker.ts b/src/matter/to-iobroker/GenericDeviceToIoBroker.ts index e92b82e7..c3d7da27 100644 --- a/src/matter/to-iobroker/GenericDeviceToIoBroker.ts +++ b/src/matter/to-iobroker/GenericDeviceToIoBroker.ts @@ -155,38 +155,30 @@ export abstract class GenericDeviceToIoBroker { return this.#deviceOptions; } + #enablePowerSourceStatesForEndpoint(endpoint: Endpoint): boolean { + const powerSource = endpoint.getClusterClient(PowerSource.Complete); + if (powerSource === undefined) { + return false; + } + const endpointId = endpoint.getNumber(); + this.enableDeviceTypeStateForAttribute(PropertyType.LowBattery, { + endpointId, + clusterId: PowerSource.Cluster.id, + attributeName: 'batChargeLevel', + convertValue: value => value !== PowerSource.BatChargeLevel.Ok, + }); + this.enableDeviceTypeStateForAttribute(PropertyType.Battery, { + endpointId, + clusterId: PowerSource.Cluster.id, + attributeName: 'batPercentRemaining', + convertValue: value => Math.round(value / 2), + }); + return true; + } + #enablePowerSourceStates(): void { - const powerSource = this.appEndpoint.getClusterClient(PowerSource.Complete); - if (powerSource !== undefined) { - const endpointId = this.appEndpoint.getNumber(); - this.enableDeviceTypeStateForAttribute(PropertyType.LowBattery, { - endpointId, - clusterId: PowerSource.Cluster.id, - attributeName: 'batChargeLevel', - convertValue: value => value !== PowerSource.BatChargeLevel.Ok, - }); - this.enableDeviceTypeStateForAttribute(PropertyType.Battery, { - endpointId, - clusterId: PowerSource.Cluster.id, - attributeName: 'batPercentRemaining', - convertValue: value => Math.round(value / 2), - }); - } else { - const rootPowerSource = this.#rootEndpoint.getClusterClient(PowerSource.Complete); - if (rootPowerSource !== undefined && rootPowerSource.supportedFeatures.battery) { - this.enableDeviceTypeStateForAttribute(PropertyType.LowBattery, { - endpointId: EndpointNumber(0), - clusterId: PowerSource.Cluster.id, - attributeName: 'batChargeLevel', - convertValue: value => value !== PowerSource.BatChargeLevel.Ok, - }); - this.enableDeviceTypeStateForAttribute(PropertyType.Battery, { - endpointId: EndpointNumber(0), - clusterId: PowerSource.Cluster.id, - attributeName: 'batPercentRemaining', - convertValue: value => Math.round(value / 2), - }); - } + if (!this.#enablePowerSourceStatesForEndpoint(this.appEndpoint)) { + this.#enablePowerSourceStatesForEndpoint(this.#rootEndpoint); } } @@ -656,7 +648,7 @@ export abstract class GenericDeviceToIoBroker { return this.appEndpoint.hasClusterClient(Identify.Cluster); } - async identify(identifyTime = 30): Promise { + async identify(identifyTime = 10): Promise { await this.appEndpoint.getClusterClient(Identify.Cluster)?.identify({ identifyTime }); } @@ -665,40 +657,54 @@ export abstract class GenericDeviceToIoBroker { await this.#adapter.extendObjectAsync(this.baseId, { common: { name } }); } - async getMatterStates(): Promise> { + async #addPowerSourceStates(endpoint: Endpoint): Promise | undefined> { const states: Record = {}; - const powerSource = this.appEndpoint.getClusterClient(PowerSource.Complete); - if (powerSource !== undefined) { - states.__header__powersourcedetails = 'Power Source Details'; - - if ( - powerSource.isAttributeSupportedByName('batQuantity') && - powerSource.isAttributeSupportedByName('batReplacementDescription') - ) { - states.includedBattery = `${await powerSource.getBatQuantityAttribute(false)} x ${await powerSource.getBatReplacementDescriptionAttribute(false)}`; - } - const voltage = powerSource.isAttributeSupportedByName('batVoltage') - ? await powerSource.getBatVoltageAttribute(false) - : undefined; - const percentRemaining = powerSource.isAttributeSupportedByName('batPercentRemaining') - ? await powerSource.getBatPercentRemainingAttribute(false) - : undefined; - - if (typeof voltage === 'number') { - states.batteryVoltage = `${(voltage / 1_000).toFixed(2)} V${typeof percentRemaining === 'number' ? ` (${Math.round(percentRemaining / 2)}%)` : ''}`; - } else if (typeof percentRemaining === 'number') { - states.batteryVoltage = `${Math.round(percentRemaining / 2)}%`; - } - - if (!states.includedBattery && !states.batteryVoltage) { - delete states.__header__powersourcedetails; + const powerSource = endpoint.getClusterClient(PowerSource.Complete); + if (powerSource === undefined) { + return undefined; + } + states.__header__powersourcedetails = 'Power Source Details'; + + if ( + powerSource.isAttributeSupportedByName('batQuantity') && + powerSource.isAttributeSupportedByName('batReplacementDescription') + ) { + states.includedBattery = `${await powerSource.getBatQuantityAttribute(false)} x ${await powerSource.getBatReplacementDescriptionAttribute(false)}`; + } + const voltage = powerSource.isAttributeSupportedByName('batVoltage') + ? await powerSource.getBatVoltageAttribute(false) + : undefined; + const percentRemaining = powerSource.isAttributeSupportedByName('batPercentRemaining') + ? await powerSource.getBatPercentRemainingAttribute(false) + : undefined; + + if (typeof voltage === 'number') { + states.batteryVoltage = `${(voltage / 1_000).toFixed(2)} V${typeof percentRemaining === 'number' ? ` (${Math.round(percentRemaining / 2)}%)` : ''}`; + } else if (typeof percentRemaining === 'number') { + states.batteryVoltage = `${Math.round(percentRemaining / 2)}%`; + } + + if (!states.includedBattery && !states.batteryVoltage) { + delete states.__header__powersourcedetails; + } else if (powerSource.isAttributeSupportedByName('endpointList')) { + const endpointList = await powerSource.getEndpointListAttribute(false); + if (endpointList) { + states.__header__powersourcedetails = 'Power Source Details'; + states.providesPowerForTheseEndpoints = endpointList.join(', '); } } - return states; } + async getMatterStates(): Promise> { + return ( + (await this.#addPowerSourceStates(this.appEndpoint)) ?? + (await this.#addPowerSourceStates(this.#rootEndpoint)) ?? + {} + ); + } + async getDeviceDetails(nodeConnected: boolean): Promise { const result: StructuredJsonFormData = {}; @@ -759,34 +765,39 @@ export abstract class GenericDeviceToIoBroker { } } + async #getBatteryStatus(endpoint: Endpoint): Promise { + const powerSource = endpoint.getClusterClient(PowerSource.Complete); + if (powerSource === undefined) { + return undefined; + } + if ( + powerSource.isAttributeSupportedByName('BatChargeState') && + (await powerSource.getBatChargeStateAttribute(false)) === PowerSource.BatChargeState.IsCharging + ) { + return 'charging'; + } + const voltage = powerSource.isAttributeSupportedByName('batVoltage') + ? await powerSource.getBatVoltageAttribute(false) + : undefined; + const percentRemaining = powerSource.isAttributeSupportedByName('batPercentRemaining') + ? await powerSource.getBatPercentRemainingAttribute(false) + : undefined; + + if (typeof percentRemaining === 'number') { + return Math.round(percentRemaining / 2); + } else if (typeof voltage === 'number') { + return `${voltage}mV`; + } + } + async getStatus(nodeStatus: DeviceStatus): Promise { const status: DeviceStatus = { connection: typeof nodeStatus === 'object' ? nodeStatus.connection : nodeStatus, }; if (status.connection === 'connected') { - const powerSource = this.appEndpoint.getClusterClient(PowerSource.Complete); - if (powerSource !== undefined) { - if ( - powerSource.isAttributeSupportedByName('BatChargeState') && - (await powerSource.getBatChargeStateAttribute(false)) === PowerSource.BatChargeState.IsCharging - ) { - status.battery = 'charging'; - } else { - const voltage = powerSource.isAttributeSupportedByName('batVoltage') - ? await powerSource.getBatVoltageAttribute(false) - : undefined; - const percentRemaining = powerSource.isAttributeSupportedByName('batPercentRemaining') - ? await powerSource.getBatPercentRemainingAttribute(false) - : undefined; - - if (typeof percentRemaining === 'number') { - status.battery = Math.round(percentRemaining / 2); - } else if (typeof voltage === 'number') { - status.battery = `${voltage}mV`; - } - } - } + status.battery = + (await this.#getBatteryStatus(this.appEndpoint)) ?? (await this.#getBatteryStatus(this.#rootEndpoint)); } return status; } diff --git a/src/matter/to-matter/BlindsToMatter.ts b/src/matter/to-matter/BlindsToMatter.ts index d7301b70..9e9635ae 100644 --- a/src/matter/to-matter/BlindsToMatter.ts +++ b/src/matter/to-matter/BlindsToMatter.ts @@ -6,8 +6,10 @@ import { WindowCovering } from '@matter/main/clusters'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; import type { BlindButtons } from '../../lib/devices/BlindButtons'; import type { Blind } from '../../lib/devices/Blind'; -import { GenericDeviceToMatter, type IdentifyOptions } from './GenericDeviceToMatter'; +import { GenericDeviceToMatter } from './GenericDeviceToMatter'; import { IoBrokerEvents } from '../behaviors/IoBrokerEvents'; +import { IoIdentifyServer } from '../behaviors/IdentifyServer'; +import { IoBrokerContext } from '../behaviors/IoBrokerContext'; /*const PosAwareLiftWindowCoveringServer = EventedWindowCoveringServer.with( WindowCovering.Feature.Lift, @@ -17,7 +19,12 @@ const PosAwareTiltWindowCoveringServer = EventedWindowCoveringServer.with( WindowCovering.Feature.Tilt, WindowCovering.Feature.PositionAwareTilt, );*/ -const IoBrokerWindowCoveringDevice = WindowCoveringDevice.with(EventedWindowCoveringServer, IoBrokerEvents); +const IoBrokerWindowCoveringDevice = WindowCoveringDevice.with( + EventedWindowCoveringServer, + IoBrokerEvents, + IoIdentifyServer, + IoBrokerContext, +); type IoBrokerWindowCoveringDevice = typeof IoBrokerWindowCoveringDevice; export class BlindsToMatter extends GenericDeviceToMatter { @@ -46,6 +53,10 @@ export class BlindsToMatter extends GenericDeviceToMatter { IoBrokerWindowCoveringDevice.with(EventedWindowCoveringServer.with(...this.#supportedFeatures)), { id: uuid, + ioBrokerContext: { + device: ioBrokerDevice, + adapter: ioBrokerDevice.adapter, + }, windowCovering: { type: this.#supportedFeatures.includes(WindowCovering.Feature.Lift) && @@ -64,9 +75,6 @@ export class BlindsToMatter extends GenericDeviceToMatter { ); } - async doIdentify(_identifyOptions: IdentifyOptions): Promise {} - async resetIdentify(_identifyOptions: IdentifyOptions): Promise {} - get matterEndpoints(): Endpoint[] { return [this.#matterEndpoint]; } diff --git a/src/matter/to-matter/ButtonSensorToMatter.ts b/src/matter/to-matter/ButtonSensorToMatter.ts index 957b5c04..6ae2310d 100644 --- a/src/matter/to-matter/ButtonSensorToMatter.ts +++ b/src/matter/to-matter/ButtonSensorToMatter.ts @@ -4,7 +4,9 @@ import { SwitchServer } from '@matter/main/behaviors'; import { Switch } from '@matter/main/clusters'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; import type { ButtonSensor } from '../../lib/devices/ButtonSensor'; -import { GenericDeviceToMatter, type IdentifyOptions } from './GenericDeviceToMatter'; +import { GenericDeviceToMatter } from './GenericDeviceToMatter'; +import { IoIdentifyServer } from '../behaviors/IdentifyServer'; +import { IoBrokerContext } from '../behaviors/IoBrokerContext'; const SwitchDevice = GenericSwitchDevice.with( SwitchServer.withFeatures( @@ -12,6 +14,8 @@ const SwitchDevice = GenericSwitchDevice.with( Switch.Feature.MomentarySwitchRelease, Switch.Feature.MomentarySwitchMultiPress, ), + IoIdentifyServer, + IoBrokerContext, ); const SwithDeviceWithLongPress = GenericSwitchDevice.with( @@ -21,6 +25,8 @@ const SwithDeviceWithLongPress = GenericSwitchDevice.with( Switch.Feature.MomentarySwitchMultiPress, Switch.Feature.MomentarySwitchLongPress, ), + IoIdentifyServer, + IoBrokerContext, ); export class ButtonSensorToMatter extends GenericDeviceToMatter { @@ -36,6 +42,10 @@ export class ButtonSensorToMatter extends GenericDeviceToMatter { this.#matterEndpoint = new Endpoint(Type, { id: uuid, + ioBrokerContext: { + device: ioBrokerDevice, + adapter: ioBrokerDevice.adapter, + }, switch: { numberOfPositions: 2, currentPosition: 0, @@ -46,9 +56,6 @@ export class ButtonSensorToMatter extends GenericDeviceToMatter { }); } - async doIdentify(_identifyOptions: IdentifyOptions): Promise {} - async resetIdentify(_identifyOptions: IdentifyOptions): Promise {} - get matterEndpoints(): Endpoint[] { return [this.#matterEndpoint]; } diff --git a/src/matter/to-matter/ButtonToMatter.ts b/src/matter/to-matter/ButtonToMatter.ts index cc4c3588..4e9f9c09 100644 --- a/src/matter/to-matter/ButtonToMatter.ts +++ b/src/matter/to-matter/ButtonToMatter.ts @@ -4,7 +4,9 @@ import { SwitchServer } from '@matter/main/behaviors'; import { Switch } from '@matter/main/clusters'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; import type { Button } from '../../lib/devices/Button'; -import { GenericDeviceToMatter, type IdentifyOptions } from './GenericDeviceToMatter'; +import { GenericDeviceToMatter } from './GenericDeviceToMatter'; +import { IoIdentifyServer } from '../behaviors/IdentifyServer'; +import { IoBrokerContext } from '../behaviors/IoBrokerContext'; const SwitchDevice = GenericSwitchDevice.with( SwitchServer.withFeatures( @@ -12,6 +14,8 @@ const SwitchDevice = GenericSwitchDevice.with( Switch.Feature.MomentarySwitchRelease, Switch.Feature.MomentarySwitchMultiPress, ), + IoIdentifyServer, + IoBrokerContext, ); export class ButtonToMatter extends GenericDeviceToMatter { @@ -22,6 +26,10 @@ export class ButtonToMatter extends GenericDeviceToMatter { super(name, uuid); this.#matterEndpoint = new Endpoint(SwitchDevice, { id: uuid, + ioBrokerContext: { + device: ioBrokerDevice, + adapter: ioBrokerDevice.adapter, + }, switch: { numberOfPositions: 2, currentPosition: 0, @@ -32,9 +40,6 @@ export class ButtonToMatter extends GenericDeviceToMatter { this.#ioBrokerDevice = ioBrokerDevice; } - async doIdentify(_identifyOptions: IdentifyOptions): Promise {} - async resetIdentify(_identifyOptions: IdentifyOptions): Promise {} - get matterEndpoints(): Endpoint[] { return [this.#matterEndpoint]; } diff --git a/src/matter/to-matter/CieToMatter.ts b/src/matter/to-matter/CieToMatter.ts index 53f7b88e..71d91cf7 100644 --- a/src/matter/to-matter/CieToMatter.ts +++ b/src/matter/to-matter/CieToMatter.ts @@ -9,12 +9,16 @@ import { ColorControl } from '@matter/main/clusters'; import { kelvinToMireds, miredsToKelvin } from '@matter/main/behaviors'; import { GenericLightingDeviceToMatter } from './GenericLightingDeviceToMatter'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; +import { IoLightingIdentifyServer } from '../behaviors/IdentifyServer'; +import { IoBrokerContext } from '../behaviors/IoBrokerContext'; const IoBrokerExtendedColorLightDevice = ExtendedColorLightDevice.with( EventedOnOffLightOnOffServer, EventedLightingLevelControlServer, EventedExtendedColorXyColorControlServer, IoBrokerEvents, + IoLightingIdentifyServer, + IoBrokerContext, ); type IoBrokerExtendedColorLightDevice = typeof IoBrokerExtendedColorLightDevice; @@ -25,6 +29,10 @@ export class CieToMatter extends GenericLightingDeviceToMatter { constructor(ioBrokerDevice: Cie, name: string, uuid: string) { const matterEndpoint = new Endpoint(IoBrokerExtendedColorLightDevice, { id: uuid, + ioBrokerContext: { + device: ioBrokerDevice, + adapter: ioBrokerDevice.adapter, + }, colorControl: { remainingTime: 0, colorMode: ColorControl.ColorMode.CurrentXAndCurrentY, diff --git a/src/matter/to-matter/CtToMatter.ts b/src/matter/to-matter/CtToMatter.ts index 4576b352..b8400bc7 100644 --- a/src/matter/to-matter/CtToMatter.ts +++ b/src/matter/to-matter/CtToMatter.ts @@ -9,12 +9,16 @@ import { ColorControl } from '@matter/main/clusters'; import { kelvinToMireds, miredsToKelvin } from '@matter/main/behaviors'; import { GenericLightingDeviceToMatter } from './GenericLightingDeviceToMatter'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; +import { IoBrokerContext } from '../behaviors/IoBrokerContext'; +import { IoLightingIdentifyServer } from '../behaviors/IdentifyServer'; const IoBrokerColorTemperatureLightDevice = ColorTemperatureLightDevice.with( EventedOnOffLightOnOffServer, EventedLightingLevelControlServer, EventedColorTemperatureColorControlServer, IoBrokerEvents, + IoLightingIdentifyServer, + IoBrokerContext, ); type IoBrokerColorTemperatureLightDevice = typeof IoBrokerColorTemperatureLightDevice; @@ -25,6 +29,10 @@ export class CtToMatter extends GenericLightingDeviceToMatter { constructor(ioBrokerDevice: Ct, name: string, uuid: string) { const matterEndpoint = new Endpoint(IoBrokerColorTemperatureLightDevice, { id: uuid, + ioBrokerContext: { + device: ioBrokerDevice, + adapter: ioBrokerDevice.adapter, + }, colorControl: { remainingTime: 0, colorMode: ColorControl.ColorMode.ColorTemperatureMireds, diff --git a/src/matter/to-matter/DimmerToMatter.ts b/src/matter/to-matter/DimmerToMatter.ts index cbdbb7c7..d05c5e9b 100644 --- a/src/matter/to-matter/DimmerToMatter.ts +++ b/src/matter/to-matter/DimmerToMatter.ts @@ -5,16 +5,26 @@ import { EventedOnOffLightOnOffServer } from '../behaviors/EventedOnOffLightOnOf import { IoBrokerEvents } from '../behaviors/IoBrokerEvents'; import { EventedLightingLevelControlServer } from '../behaviors/EventedLightingLevelControlServer'; import { GenericLightingDeviceToMatter } from './GenericLightingDeviceToMatter'; +import { IoLightingIdentifyServer } from '../behaviors/IdentifyServer'; +import { IoBrokerContext } from '../behaviors/IoBrokerContext'; const IoBrokerDimmableLightDevice = DimmableLightDevice.with( EventedOnOffLightOnOffServer, EventedLightingLevelControlServer, IoBrokerEvents, + IoLightingIdentifyServer, + IoBrokerContext, ); export class DimmerToMatter extends GenericLightingDeviceToMatter { constructor(ioBrokerDevice: Dimmer, name: string, uuid: string) { - const matterEndpoint = new Endpoint(IoBrokerDimmableLightDevice, { id: uuid }); + const matterEndpoint = new Endpoint(IoBrokerDimmableLightDevice, { + id: uuid, + ioBrokerContext: { + device: ioBrokerDevice, + adapter: ioBrokerDevice.adapter, + }, + }); super(ioBrokerDevice, matterEndpoint, name, uuid); } diff --git a/src/matter/to-matter/DoorToMatter.ts b/src/matter/to-matter/DoorToMatter.ts index e9c886be..3cce6017 100644 --- a/src/matter/to-matter/DoorToMatter.ts +++ b/src/matter/to-matter/DoorToMatter.ts @@ -2,7 +2,9 @@ import { Endpoint } from '@matter/main'; import { ContactSensorDevice } from '@matter/main/devices'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; import type { Door } from '../../lib/devices/Door'; -import { GenericDeviceToMatter, type IdentifyOptions } from './GenericDeviceToMatter'; +import { GenericDeviceToMatter } from './GenericDeviceToMatter'; +import { IoIdentifyServer } from '../behaviors/IdentifyServer'; +import { IoBrokerContext } from '../behaviors/IoBrokerContext'; /** Mapping Logic to map a ioBroker Temperature device to a Matter TemperatureSensorDevice. */ export class DoorToMatter extends GenericDeviceToMatter { @@ -11,8 +13,12 @@ export class DoorToMatter extends GenericDeviceToMatter { constructor(ioBrokerDevice: Door, name: string, uuid: string) { super(name, uuid); - this.#matterEndpoint = new Endpoint(ContactSensorDevice, { + this.#matterEndpoint = new Endpoint(ContactSensorDevice.with(IoIdentifyServer, IoBrokerContext), { id: uuid, + ioBrokerContext: { + device: ioBrokerDevice, + adapter: ioBrokerDevice.adapter, + }, booleanState: { stateValue: false, // Will be corrected in registerHandlersAndInitialize }, @@ -20,9 +26,6 @@ export class DoorToMatter extends GenericDeviceToMatter { this.#ioBrokerDevice = ioBrokerDevice; } - async doIdentify(_identifyOptions: IdentifyOptions): Promise {} - async resetIdentify(_identifyOptions: IdentifyOptions): Promise {} - get matterEndpoints(): Endpoint[] { return [this.#matterEndpoint]; } diff --git a/src/matter/to-matter/FloodAlarmToMatter.ts b/src/matter/to-matter/FloodAlarmToMatter.ts index 31eb5f75..1afa4336 100644 --- a/src/matter/to-matter/FloodAlarmToMatter.ts +++ b/src/matter/to-matter/FloodAlarmToMatter.ts @@ -2,7 +2,9 @@ import { Endpoint } from '@matter/main'; import { WaterLeakDetectorDevice } from '@matter/main/devices'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; import type { FloodAlarm } from '../../lib/devices/FloodAlarm'; -import { GenericDeviceToMatter, type IdentifyOptions } from './GenericDeviceToMatter'; +import { GenericDeviceToMatter } from './GenericDeviceToMatter'; +import { IoIdentifyServer } from '../behaviors/IdentifyServer'; +import { IoBrokerContext } from '../behaviors/IoBrokerContext'; /** Mapping Logic to map a ioBroker Temperature device to a Matter TemperatureSensorDevice. */ export class FloodAlarmToMatter extends GenericDeviceToMatter { @@ -11,8 +13,12 @@ export class FloodAlarmToMatter extends GenericDeviceToMatter { constructor(ioBrokerDevice: FloodAlarm, name: string, uuid: string) { super(name, uuid); - this.#matterEndpoint = new Endpoint(WaterLeakDetectorDevice, { + this.#matterEndpoint = new Endpoint(WaterLeakDetectorDevice.with(IoIdentifyServer, IoBrokerContext), { id: uuid, + ioBrokerContext: { + device: ioBrokerDevice, + adapter: ioBrokerDevice.adapter, + }, booleanState: { stateValue: false, // Will be corrected in registerHandlersAndInitialize }, @@ -20,9 +26,6 @@ export class FloodAlarmToMatter extends GenericDeviceToMatter { this.#ioBrokerDevice = ioBrokerDevice; } - async doIdentify(_identifyOptions: IdentifyOptions): Promise {} - async resetIdentify(_identifyOptions: IdentifyOptions): Promise {} - get matterEndpoints(): Endpoint[] { return [this.#matterEndpoint]; } diff --git a/src/matter/to-matter/GenericDeviceToMatter.ts b/src/matter/to-matter/GenericDeviceToMatter.ts index 3fa697d3..86b9bc43 100644 --- a/src/matter/to-matter/GenericDeviceToMatter.ts +++ b/src/matter/to-matter/GenericDeviceToMatter.ts @@ -2,15 +2,8 @@ import { capitalize, Observable, type Endpoint, ObserverGroup, type MaybePromise import type { GenericDevice } from '../../lib'; import type { StructuredJsonFormData } from '../../lib/JsonConfigUtils'; -export interface IdentifyOptions { - currentState?: any; - initialState?: any; -} - /** Base class to map an ioBroker device to a matter device. */ export abstract class GenericDeviceToMatter { - #identifyTimeout?: NodeJS.Timeout; - #identifyHighlightState = false; #name: string; #uuid: string; #observers = new ObserverGroup(); @@ -30,51 +23,6 @@ export abstract class GenericDeviceToMatter { return this.#validChanged; } - /** - * Generic Identify Logic handler. This method uses `doIdentify()` and `resetIdentify()` to handle the identify - * logic while an Identification process is running. - */ - handleIdentify(identifyOptions: IdentifyOptions): void { - clearTimeout(this.#identifyTimeout); - this.#identifyTimeout = setTimeout(async () => { - this.#identifyTimeout = undefined; - const highlightState = !this.#identifyHighlightState; - if (highlightState) { - if (this.ioBrokerDevice.isActionAllowedByIdentify()) { - await this.doIdentify(identifyOptions); - } - } else { - if (this.ioBrokerDevice.isActionAllowedByIdentify()) { - await this.resetIdentify(identifyOptions); - } - } - this.handleIdentify(identifyOptions); - }, 1000); - } - - /** - * This method is called when the Identification process is stopped. - */ - async stopIdentify(identifyOptions: IdentifyOptions): Promise { - clearTimeout(this.#identifyTimeout); - this.#identifyTimeout = undefined; - // set to initial state - if (this.ioBrokerDevice.isActionAllowedByIdentify()) { - await this.resetIdentify(identifyOptions); - } - } - - /** - * This method gets triggered ever second when Identification is active and should implements device specific logic. - */ - abstract doIdentify(identifyOptions: IdentifyOptions): Promise; - - /** - * This method is called at the end of the Identification and should restore the state from before the identification - * started. - */ - abstract resetIdentify(identifyOptions: IdentifyOptions): Promise; - /** Return the created Matter Endpoints of this device. */ abstract get matterEndpoints(): Endpoint[]; diff --git a/src/matter/to-matter/GenericLightingDeviceToMatter.ts b/src/matter/to-matter/GenericLightingDeviceToMatter.ts index 47f853fe..05c893ba 100644 --- a/src/matter/to-matter/GenericLightingDeviceToMatter.ts +++ b/src/matter/to-matter/GenericLightingDeviceToMatter.ts @@ -7,10 +7,8 @@ import type { RgbSingle } from '../../lib/devices/RgbSingle'; import type { RgbwSingle } from '../../lib/devices/RgbwSingle'; import { GenericElectricityDataDeviceToMatter } from './GenericElectricityDataDeviceToMatter'; import type { Endpoint } from '@matter/main'; -import { IdentifyServer } from '@matter/main/behaviors'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; import { IoBrokerEvents } from '../behaviors/IoBrokerEvents'; -import type { IdentifyOptions } from './GenericDeviceToMatter'; import { EventedLightingLevelControlServer } from '../behaviors/EventedLightingLevelControlServer'; import { EventedOnOffLightOnOffServer } from '../behaviors/EventedOnOffLightOnOffServer'; @@ -44,28 +42,6 @@ export abstract class GenericLightingDeviceToMatter extends GenericElectricityDa super.registerHandlersAndInitialize(); await this.initializeElectricityStateHandlers(this.#matterEndpoint, this.#ioBrokerDevice); - - let isIdentifying = false; - const identifyOptions: IdentifyOptions = {}; - this.matterEvents.on(this.#matterEndpoint.eventsOf(IdentifyServer).identifyTime$Changed, async value => { - if (!this.#ioBrokerDevice.hasPower()) { - return; // TODO - } - - // identifyTime is set when an identify command is called and then decreased every second while identify logic runs. - if (value > 0 && !isIdentifying) { - isIdentifying = true; - const identifyInitialState = !!this.#ioBrokerDevice.getPower(); - - identifyOptions.currentState = identifyInitialState; - identifyOptions.initialState = identifyInitialState; - - this.handleIdentify(identifyOptions); - } else if (value === 0) { - isIdentifying = false; - await this.stopIdentify(identifyOptions); - } - }); } get matterEndpoints(): Endpoint[] { @@ -76,25 +52,6 @@ export abstract class GenericLightingDeviceToMatter extends GenericElectricityDa return this.#ioBrokerDevice; } - // Just change the power state every second - async doIdentify(identifyOptions: IdentifyOptions): Promise { - if (!this.#ioBrokerDevice.hasPower()) { - return; - } - - identifyOptions.currentState = !identifyOptions.currentState; - await this.#ioBrokerDevice.setPower(identifyOptions.currentState as boolean); - } - - // Restore the given initial state after the identity process is over - async resetIdentify(identifyOptions: IdentifyOptions): Promise { - if (!this.#ioBrokerDevice.hasPower()) { - return; - } - - await this.#ioBrokerDevice.setPower(identifyOptions.initialState as boolean); - } - protected async initializeOnOffClusterHandlers(): Promise { await this.#matterEndpoint.setStateOf(EventedOnOffLightOnOffServer, { onOff: this.#ioBrokerDevice.hasPower() ? !!this.#ioBrokerDevice.getPower() : true, diff --git a/src/matter/to-matter/HueAndRgbToMatter.ts b/src/matter/to-matter/HueAndRgbToMatter.ts index 7d51ea98..99e2512b 100644 --- a/src/matter/to-matter/HueAndRgbToMatter.ts +++ b/src/matter/to-matter/HueAndRgbToMatter.ts @@ -12,12 +12,16 @@ import { ColorControl } from '@matter/main/clusters'; import { rgbToXy, rgbToHsv, hsvToRgb, xyToRgb, kelvinToMireds, miredsToKelvin } from '@matter/main/behaviors'; import { GenericLightingDeviceToMatter } from './GenericLightingDeviceToMatter'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; +import { IoLightingIdentifyServer } from '../behaviors/IdentifyServer'; +import { IoBrokerContext } from '../behaviors/IoBrokerContext'; const IoBrokerExtendedColorLightDevice = ExtendedColorLightDevice.with( EventedOnOffLightOnOffServer, EventedLightingLevelControlServer, EventedExtendedColorHueSaturationColorControlServer, IoBrokerEvents, + IoLightingIdentifyServer, + IoBrokerContext, ); type IoBrokerExtendedColorLightDevice = typeof IoBrokerExtendedColorLightDevice; @@ -33,6 +37,10 @@ export class HueAndRgbToMatter extends GenericLightingDeviceToMatter { constructor(ioBrokerDevice: Hue | Rgb | RgbSingle | RgbwSingle, name: string, uuid: string) { const matterEndpoint = new Endpoint(IoBrokerExtendedColorLightDevice, { id: uuid, + ioBrokerContext: { + device: ioBrokerDevice, + adapter: ioBrokerDevice.adapter, + }, colorControl: { remainingTime: 0, colorMode: ColorControl.ColorMode.CurrentHueAndCurrentSaturation, diff --git a/src/matter/to-matter/HumidityToMatter.ts b/src/matter/to-matter/HumidityToMatter.ts index 4562ee20..b6ef4b61 100644 --- a/src/matter/to-matter/HumidityToMatter.ts +++ b/src/matter/to-matter/HumidityToMatter.ts @@ -2,7 +2,9 @@ import { Endpoint } from '@matter/main'; import { HumiditySensorDevice } from '@matter/main/devices'; import type { Humidity } from '../../lib/devices/Humidity'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; -import { GenericDeviceToMatter, type IdentifyOptions } from './GenericDeviceToMatter'; +import { GenericDeviceToMatter } from './GenericDeviceToMatter'; +import { IoIdentifyServer } from '../behaviors/IdentifyServer'; +import { IoBrokerContext } from '../behaviors/IoBrokerContext'; /** Mapping Logic to map a ioBroker Humidity device to a Matter HumiditySensorDevice. */ export class HumidityToMatter extends GenericDeviceToMatter { @@ -11,13 +13,16 @@ export class HumidityToMatter extends GenericDeviceToMatter { constructor(ioBrokerDevice: Humidity, name: string, uuid: string) { super(name, uuid); - this.#matterEndpoint = new Endpoint(HumiditySensorDevice, { id: uuid }); + this.#matterEndpoint = new Endpoint(HumiditySensorDevice.with(IoIdentifyServer, IoBrokerContext), { + id: uuid, + ioBrokerContext: { + device: ioBrokerDevice, + adapter: ioBrokerDevice.adapter, + }, + }); this.#ioBrokerDevice = ioBrokerDevice; } - async doIdentify(_identifyOptions: IdentifyOptions): Promise {} - async resetIdentify(_identifyOptions: IdentifyOptions): Promise {} - get matterEndpoints(): Endpoint[] { return [this.#matterEndpoint]; } diff --git a/src/matter/to-matter/IlluminanceToMatter.ts b/src/matter/to-matter/IlluminanceToMatter.ts index 390306f3..38ea5a38 100644 --- a/src/matter/to-matter/IlluminanceToMatter.ts +++ b/src/matter/to-matter/IlluminanceToMatter.ts @@ -2,7 +2,9 @@ import { Endpoint } from '@matter/main'; import { LightSensorDevice } from '@matter/main/devices'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; import type { Illuminance } from '../../lib/devices/Illuminance'; -import { GenericDeviceToMatter, type IdentifyOptions } from './GenericDeviceToMatter'; +import { GenericDeviceToMatter } from './GenericDeviceToMatter'; +import { IoIdentifyServer } from '../behaviors/IdentifyServer'; +import { IoBrokerContext } from '../behaviors/IoBrokerContext'; /** Mapping Logic to map a ioBroker Temperature device to a Matter TemperatureSensorDevice. */ export class IlluminanceToMatter extends GenericDeviceToMatter { @@ -12,12 +14,15 @@ export class IlluminanceToMatter extends GenericDeviceToMatter { constructor(ioBrokerDevice: Illuminance, name: string, uuid: string) { super(name, uuid); this.#ioBrokerDevice = ioBrokerDevice; - this.#matterEndpoint = new Endpoint(LightSensorDevice, { id: uuid }); + this.#matterEndpoint = new Endpoint(LightSensorDevice.with(IoIdentifyServer, IoBrokerContext), { + id: uuid, + ioBrokerContext: { + device: ioBrokerDevice, + adapter: ioBrokerDevice.adapter, + }, + }); } - async doIdentify(_identifyOptions: IdentifyOptions): Promise {} - async resetIdentify(_identifyOptions: IdentifyOptions): Promise {} - get matterEndpoints(): Endpoint[] { return [this.#matterEndpoint]; } diff --git a/src/matter/to-matter/LightToMatter.ts b/src/matter/to-matter/LightToMatter.ts index 037a2066..4b78d7cc 100644 --- a/src/matter/to-matter/LightToMatter.ts +++ b/src/matter/to-matter/LightToMatter.ts @@ -4,13 +4,26 @@ import { GenericLightingDeviceToMatter } from './GenericLightingDeviceToMatter'; import { EventedOnOffLightOnOffServer } from '../behaviors/EventedOnOffLightOnOffServer'; import { IoBrokerEvents } from '../behaviors/IoBrokerEvents'; import type { Light } from '../../lib/devices/Light'; +import { IoLightingIdentifyServer } from '../behaviors/IdentifyServer'; +import { IoBrokerContext } from '../behaviors/IoBrokerContext'; -const IoBrokerOnOffLightDevice = OnOffLightDevice.with(EventedOnOffLightOnOffServer, IoBrokerEvents); +const IoBrokerOnOffLightDevice = OnOffLightDevice.with( + EventedOnOffLightOnOffServer, + IoBrokerEvents, + IoLightingIdentifyServer, + IoBrokerContext, +); /** Mapping Logic to map a ioBroker Light device to a Matter OnOffLightDevice. */ export class LightToMatter extends GenericLightingDeviceToMatter { constructor(ioBrokerDevice: Light, name: string, uuid: string) { - const matterEndpoint = new Endpoint(IoBrokerOnOffLightDevice, { id: uuid }); + const matterEndpoint = new Endpoint(IoBrokerOnOffLightDevice, { + id: uuid, + ioBrokerContext: { + device: ioBrokerDevice, + adapter: ioBrokerDevice.adapter, + }, + }); super(ioBrokerDevice, matterEndpoint, name, uuid); } diff --git a/src/matter/to-matter/LockToMatter.ts b/src/matter/to-matter/LockToMatter.ts index fa167896..96939001 100644 --- a/src/matter/to-matter/LockToMatter.ts +++ b/src/matter/to-matter/LockToMatter.ts @@ -3,12 +3,18 @@ import { DoorLock } from '@matter/main/clusters'; import { DoorLockDevice } from '@matter/main/devices'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; import type { Lock } from '../../lib/devices/Lock'; -import type { IdentifyOptions } from './GenericDeviceToMatter'; import { GenericElectricityDataDeviceToMatter } from './GenericElectricityDataDeviceToMatter'; import { IoBrokerEvents } from '../behaviors/IoBrokerEvents'; import { EventedDoorLockServer } from '../behaviors/EventedDoorLockServer'; +import { IoIdentifyServer } from '../behaviors/IdentifyServer'; +import { IoBrokerContext } from '../behaviors/IoBrokerContext'; -const IoBrokerDoorLockDevice = DoorLockDevice.with(EventedDoorLockServer, IoBrokerEvents); +const IoBrokerDoorLockDevice = DoorLockDevice.with( + EventedDoorLockServer, + IoBrokerEvents, + IoIdentifyServer, + IoBrokerContext, +); type IoBrokerDoorLockDevice = typeof IoBrokerDoorLockDevice; // TODO Add Latching support when "Open" is there! @@ -23,6 +29,10 @@ export class LockToMatter extends GenericElectricityDataDeviceToMatter { this.#matterEndpoint = new Endpoint(IoBrokerDoorLockDevice, { id: uuid, + ioBrokerContext: { + device: ioBrokerDevice, + adapter: ioBrokerDevice.adapter, + }, doorLock: { lockType: DoorLock.LockType.Other, actuatorEnabled: true, @@ -32,16 +42,6 @@ export class LockToMatter extends GenericElectricityDataDeviceToMatter { this.#ioBrokerDevice = ioBrokerDevice; } - // Just change the power state every second - async doIdentify(_identifyOptions: IdentifyOptions): Promise { - // TODO - } - - // Restore the given initial state after the identity process is over - async resetIdentify(_identifyOptions: IdentifyOptions): Promise { - // TODO - } - get matterEndpoints(): Endpoint[] { return [this.#matterEndpoint]; } diff --git a/src/matter/to-matter/MotionToMatter.ts b/src/matter/to-matter/MotionToMatter.ts index fbbb61b7..c778e9d4 100644 --- a/src/matter/to-matter/MotionToMatter.ts +++ b/src/matter/to-matter/MotionToMatter.ts @@ -4,7 +4,9 @@ import { LightSensorDevice, OccupancySensorDevice } from '@matter/main/devices'; import type { TypeFromBitSchema } from '@matter/main/types'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; import type { Motion } from '../../lib/devices/Motion'; -import { GenericDeviceToMatter, type IdentifyOptions } from './GenericDeviceToMatter'; +import { GenericDeviceToMatter } from './GenericDeviceToMatter'; +import { IoIdentifyServer } from '../behaviors/IdentifyServer'; +import { IoBrokerContext } from '../behaviors/IoBrokerContext'; /** Mapping Logic to map a ioBroker Temperature device to a Matter TemperatureSensorDevice. */ export class MotionToMatter extends GenericDeviceToMatter { @@ -14,8 +16,12 @@ export class MotionToMatter extends GenericDeviceToMatter { constructor(ioBrokerDevice: Motion, name: string, uuid: string) { super(name, uuid); - this.#matterEndpointOccupancy = new Endpoint(OccupancySensorDevice, { + this.#matterEndpointOccupancy = new Endpoint(OccupancySensorDevice.with(IoIdentifyServer, IoBrokerContext), { id: `${uuid}-Occupancy`, + ioBrokerContext: { + device: ioBrokerDevice, + adapter: ioBrokerDevice.adapter, + }, occupancySensing: { // Deprecated fields but mandatory, so et PIR for now occupancySensorType: OccupancySensing.OccupancySensorType.Pir, @@ -28,9 +34,6 @@ export class MotionToMatter extends GenericDeviceToMatter { } } - async doIdentify(_identifyOptions: IdentifyOptions): Promise {} - async resetIdentify(_identifyOptions: IdentifyOptions): Promise {} - get matterEndpoints(): Endpoint[] { const endpoints: Endpoint[] = [this.#matterEndpointOccupancy]; if (this.#matterEndpointLightSensor) { diff --git a/src/matter/to-matter/SocketToMatter.ts b/src/matter/to-matter/SocketToMatter.ts index a137cb58..159759f8 100644 --- a/src/matter/to-matter/SocketToMatter.ts +++ b/src/matter/to-matter/SocketToMatter.ts @@ -2,12 +2,18 @@ import { Endpoint } from '@matter/main'; import { OnOffPlugInUnitDevice } from '@matter/main/devices'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; import type { Socket } from '../../lib/devices/Socket'; -import type { IdentifyOptions } from './GenericDeviceToMatter'; import { GenericElectricityDataDeviceToMatter } from './GenericElectricityDataDeviceToMatter'; import { EventedOnOffPlugInUnitOnOffServer } from '../behaviors/EventedOnOffPlugInUnitOnOffServer'; import { IoBrokerEvents } from '../behaviors/IoBrokerEvents'; +import { IoIdentifyServer } from '../behaviors/IdentifyServer'; +import { IoBrokerContext } from '../behaviors/IoBrokerContext'; -const IoBrokerOnOffPlugInUnitDevice = OnOffPlugInUnitDevice.with(EventedOnOffPlugInUnitOnOffServer, IoBrokerEvents); +const IoBrokerOnOffPlugInUnitDevice = OnOffPlugInUnitDevice.with( + EventedOnOffPlugInUnitOnOffServer, + IoBrokerEvents, + IoIdentifyServer, + IoBrokerContext, +); type IoBrokerOnOffPlugInUnitDevice = typeof IoBrokerOnOffPlugInUnitDevice; /** Mapping Logic to map a ioBroker Socket device to a Matter OnOffPlugInUnitDevice. */ @@ -17,22 +23,17 @@ export class SocketToMatter extends GenericElectricityDataDeviceToMatter { constructor(ioBrokerDevice: Socket, name: string, uuid: string) { super(name, uuid); - this.#matterEndpoint = new Endpoint(IoBrokerOnOffPlugInUnitDevice, { id: uuid }); + this.#matterEndpoint = new Endpoint(IoBrokerOnOffPlugInUnitDevice, { + id: uuid, + ioBrokerContext: { + device: ioBrokerDevice, + adapter: ioBrokerDevice.adapter, + }, + }); this.#ioBrokerDevice = ioBrokerDevice; this.addElectricityDataClusters(this.#matterEndpoint, this.#ioBrokerDevice); } - // Just change the power state every second - doIdentify(identifyOptions: IdentifyOptions): Promise { - identifyOptions.currentState = !identifyOptions.currentState; - return this.#ioBrokerDevice.setPower(!!identifyOptions.currentState); - } - - // Restore the given initial state after the identity process is over - resetIdentify(identifyOptions: IdentifyOptions): Promise { - return this.#ioBrokerDevice.setPower(!!identifyOptions.initialState); - } - get matterEndpoints(): Endpoint[] { return [this.#matterEndpoint]; } @@ -57,24 +58,6 @@ export class SocketToMatter extends GenericElectricityDataDeviceToMatter { async on => await this.#ioBrokerDevice.setPower(on), ); - let isIdentifying = false; - const identifyOptions: IdentifyOptions = {}; - this.matterEvents.on(this.#matterEndpoint.events.identify.identifyTime$Changed, async value => { - // identifyTime is set when an identify command is called and then decreased every second while identify logic runs. - if (value > 0 && !isIdentifying) { - isIdentifying = true; - const identifyInitialState = !!this.#ioBrokerDevice.getPower(); - - identifyOptions.currentState = identifyInitialState; - identifyOptions.initialState = identifyInitialState; - - this.handleIdentify(identifyOptions); - } else if (value === 0) { - isIdentifying = false; - await this.stopIdentify(identifyOptions); - } - }); - this.#ioBrokerDevice.onChange(async event => { switch (event.property) { case PropertyType.Power: diff --git a/src/matter/to-matter/TemperatureToMatter.ts b/src/matter/to-matter/TemperatureToMatter.ts index 0b65b824..4241b433 100644 --- a/src/matter/to-matter/TemperatureToMatter.ts +++ b/src/matter/to-matter/TemperatureToMatter.ts @@ -2,7 +2,9 @@ import { Endpoint } from '@matter/main'; import { HumiditySensorDevice, TemperatureSensorDevice } from '@matter/main/devices'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; import type { Temperature } from '../../lib/devices/Temperature'; -import { GenericDeviceToMatter, type IdentifyOptions } from './GenericDeviceToMatter'; +import { GenericDeviceToMatter } from './GenericDeviceToMatter'; +import { IoIdentifyServer } from '../behaviors/IdentifyServer'; +import { IoBrokerContext } from '../behaviors/IoBrokerContext'; /** Mapping Logic to map a ioBroker Temperature device to a Matter TemperatureSensorDevice. */ export class TemperatureToMatter extends GenericDeviceToMatter { @@ -12,18 +14,22 @@ export class TemperatureToMatter extends GenericDeviceToMatter { constructor(ioBrokerDevice: Temperature, name: string, uuid: string) { super(name, uuid); - this.#matterEndpointTemperature = new Endpoint(TemperatureSensorDevice, { - id: `${uuid}-Temperature`, - }); + this.#matterEndpointTemperature = new Endpoint( + TemperatureSensorDevice.with(IoIdentifyServer, IoBrokerContext), + { + id: `${uuid}-Temperature`, + ioBrokerContext: { + device: ioBrokerDevice, + adapter: ioBrokerDevice.adapter, + }, + }, + ); this.#ioBrokerDevice = ioBrokerDevice; if (this.#ioBrokerDevice.hasHumidity()) { this.#matterEndpointHumidity = new Endpoint(HumiditySensorDevice, { id: `${uuid}-Humidity` }); } } - async doIdentify(_identifyOptions: IdentifyOptions): Promise {} - async resetIdentify(_identifyOptions: IdentifyOptions): Promise {} - get matterEndpoints(): Endpoint[] { const endpoints: Endpoint[] = [this.#matterEndpointTemperature]; if (this.#matterEndpointHumidity) { diff --git a/src/matter/to-matter/ThermostatToMatter.ts b/src/matter/to-matter/ThermostatToMatter.ts index 6dcc55a4..2226c37e 100644 --- a/src/matter/to-matter/ThermostatToMatter.ts +++ b/src/matter/to-matter/ThermostatToMatter.ts @@ -3,9 +3,11 @@ import { HumiditySensorDevice, ThermostatDevice } 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'; -import { GenericDeviceToMatter, type IdentifyOptions } from './GenericDeviceToMatter'; +import { GenericDeviceToMatter } from './GenericDeviceToMatter'; import { IoThermostatServer } from '../behaviors/ThermostatServer'; import { IoBrokerEvents } from '../behaviors/IoBrokerEvents'; +import { IoIdentifyServer } from '../behaviors/IdentifyServer'; +import { IoBrokerContext } from '../behaviors/IoBrokerContext'; //const HeatingThermostatServer = IoThermostatServer.with(MatterThermostat.Feature.Heating); //const CoolingThermostatServer = IoThermostatServer.with(MatterThermostat.Feature.Cooling); @@ -85,9 +87,18 @@ export class ThermostatToMatter extends GenericDeviceToMatter { const hasCooling = clusterModes.includes(MatterThermostat.Feature.Cooling); this.#matterEndpointThermostat = new Endpoint( - ThermostatDevice.with(IoThermostatServer.with(...clusterModes), IoBrokerEvents), + ThermostatDevice.with( + IoThermostatServer.with(...clusterModes), + IoBrokerEvents, + IoIdentifyServer, + IoBrokerContext, + ), { id: `${uuid}-Thermostat`, + ioBrokerContext: { + device: ioBrokerDevice, + adapter: ioBrokerDevice.adapter, + }, thermostat: { // Values are potentially corrected later again systemMode: hasHeating ? MatterThermostat.SystemMode.Heat : MatterThermostat.SystemMode.Cool, @@ -110,9 +121,6 @@ export class ThermostatToMatter extends GenericDeviceToMatter { } } - async doIdentify(_identifyOptions: IdentifyOptions): Promise {} - async resetIdentify(_identifyOptions: IdentifyOptions): Promise {} - get matterEndpoints(): Endpoint[] { const endpoints: Endpoint[] = [this.#matterEndpointThermostat]; if (this.#matterEndpointHumidity) { diff --git a/src/matter/to-matter/WindowToMatter.ts b/src/matter/to-matter/WindowToMatter.ts index 62085407..c1c7ec1d 100644 --- a/src/matter/to-matter/WindowToMatter.ts +++ b/src/matter/to-matter/WindowToMatter.ts @@ -2,7 +2,9 @@ import { Endpoint } from '@matter/main'; import { ContactSensorDevice } from '@matter/main/devices'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; import type { Window } from '../../lib/devices/Window'; -import { GenericDeviceToMatter, type IdentifyOptions } from './GenericDeviceToMatter'; +import { GenericDeviceToMatter } from './GenericDeviceToMatter'; +import { IoIdentifyServer } from '../behaviors/IdentifyServer'; +import { IoBrokerContext } from '../behaviors/IoBrokerContext'; /** Mapping Logic to map a ioBroker Temperature device to a Matter TemperatureSensorDevice. */ export class WindowToMatter extends GenericDeviceToMatter { @@ -11,8 +13,12 @@ export class WindowToMatter extends GenericDeviceToMatter { constructor(ioBrokerDevice: Window, name: string, uuid: string) { super(name, uuid); - this.#matterEndpoint = new Endpoint(ContactSensorDevice, { + this.#matterEndpoint = new Endpoint(ContactSensorDevice.with(IoIdentifyServer, IoBrokerContext), { id: uuid, + ioBrokerContext: { + device: ioBrokerDevice, + adapter: ioBrokerDevice.adapter, + }, booleanState: { stateValue: false, // Will be corrected in registerHandlersAndInitialize }, @@ -20,9 +26,6 @@ export class WindowToMatter extends GenericDeviceToMatter { this.#ioBrokerDevice = ioBrokerDevice; } - async doIdentify(_identifyOptions: IdentifyOptions): Promise {} - async resetIdentify(_identifyOptions: IdentifyOptions): Promise {} - get matterEndpoints(): Endpoint[] { return [this.#matterEndpoint]; }