diff --git a/README.md b/README.md index afd5be28..93fff5b5 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,11 @@ TODO ## Prerequisites to use this adapter +### Used adapters and apps +If you want to use the iobroker Visu App for device pairing you need: +* ioBroker Visu App at least v1.3.1 +* iot Adapter at least v3.4.4 + ### General prerequisites * One instance of the adapter is bound to one host (aka IP). Multiple instances require a multi-host setup. * Make sure IPv6 is enabled in your network and the host you use this adapter on has an IPv6 address @@ -220,6 +225,17 @@ TBD --> ## Changelog + +### __WORK IN PROGRESS__ +* (@Apollon77) Allows to trigger commands via matter also when state already matches the value +* (@Apollon77) Sets and updates the fabric label for paired devices (default is "ioBroker matter.X") +* (@Apollon77) Detects state deletion for ioBroker devices and updates device in UI to show device state +* (@Apollon77) Several optimizations on commissioning +* (@Apollon77) Do not show commissioning QR codes in ioBroker log +* (@Apollon77) Use Fabric label to try to detect if ioBroker is the controller +* (@Apollon77) Fixes displaying error details for devices and bridges +* (@Apollon77) Fixes the device and type detection logic + ### 0.3.2 (2024-12-21) * (@Apollon77) Fixes several discovery issues diff --git a/io-package.json b/io-package.json index 5d83d4ba..20d9b5cd 100644 --- a/io-package.json +++ b/io-package.json @@ -180,7 +180,8 @@ "debug": false, "login": "", "pass": "", - "defaultBridge": "" + "defaultBridge": "", + "controllerFabricLabel": "" }, "objects": [], "instanceObjects": [ diff --git a/package-lock.json b/package-lock.json index a7865d63..2346938c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,9 +13,9 @@ "@iobroker/dm-utils": "^0.6.11", "@iobroker/i18n": "^0.3.1", "@iobroker/type-detector": "^4.1.1", - "@matter/main": "0.12.0-alpha.0-20241220-755393a73", - "@matter/nodejs": "0.12.0-alpha.0-20241220-755393a73", - "@project-chip/matter.js": "0.12.0-alpha.0-20241220-755393a73", + "@matter/main": "0.12.0-alpha.0-20241227-9e7d81837", + "@matter/nodejs": "0.12.0-alpha.0-20241227-9e7d81837", + "@project-chip/matter.js": "0.12.0-alpha.0-20241227-9e7d81837", "axios": "^1.7.9", "jsonwebtoken": "^9.0.2" }, @@ -40,7 +40,7 @@ "node": ">=18" }, "optionalDependencies": { - "@matter/nodejs-ble": "0.12.0-alpha.0-20241220-755393a73" + "@matter/nodejs-ble": "0.12.0-alpha.0-20241227-9e7d81837" } }, "node_modules/@alcalzone/pak": { @@ -1001,64 +1001,64 @@ } }, "node_modules/@matter/general": { - "version": "0.12.0-alpha.0-20241220-755393a73", - "resolved": "https://registry.npmjs.org/@matter/general/-/general-0.12.0-alpha.0-20241220-755393a73.tgz", - "integrity": "sha512-7UEIrrAaTxS+YFwvq62c42w6O8qsHt5Ktftb4OwV3jADVO0pMdUUTM79wegu/tQp9mCpHiq54dTvWCyV26FwYg==", + "version": "0.12.0-alpha.0-20241227-9e7d81837", + "resolved": "https://registry.npmjs.org/@matter/general/-/general-0.12.0-alpha.0-20241227-9e7d81837.tgz", + "integrity": "sha512-S6uHQlL1UUgXRYl4P51t8NtCMTZSRWrV5bdVfYcheur3LXYiNwnmCp0qasHrDIj7EUFWud3D2EYtH7DvHI2OHQ==", "license": "Apache-2.0", "dependencies": { "@noble/curves": "^1.7.0" } }, "node_modules/@matter/main": { - "version": "0.12.0-alpha.0-20241220-755393a73", - "resolved": "https://registry.npmjs.org/@matter/main/-/main-0.12.0-alpha.0-20241220-755393a73.tgz", - "integrity": "sha512-IubpLp1wAl61LB3VBfzGyJD8B3/FXlDMY9JkoirWXmpvnyIq3J+GiKywqSE6HcGACIy66dRP/6dIVL4BBCilWg==", + "version": "0.12.0-alpha.0-20241227-9e7d81837", + "resolved": "https://registry.npmjs.org/@matter/main/-/main-0.12.0-alpha.0-20241227-9e7d81837.tgz", + "integrity": "sha512-e5tJnDnTXxO2mAz9sdT04C8VosXkTox9RSl2MMhs03SQ0TB3wWCtCKsNo7Q+DMz9RsKmRKsap4/3KsLy2ONMiA==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.12.0-alpha.0-20241220-755393a73", - "@matter/model": "0.12.0-alpha.0-20241220-755393a73", - "@matter/node": "0.12.0-alpha.0-20241220-755393a73", - "@matter/protocol": "0.12.0-alpha.0-20241220-755393a73", - "@matter/types": "0.12.0-alpha.0-20241220-755393a73", + "@matter/general": "0.12.0-alpha.0-20241227-9e7d81837", + "@matter/model": "0.12.0-alpha.0-20241227-9e7d81837", + "@matter/node": "0.12.0-alpha.0-20241227-9e7d81837", + "@matter/protocol": "0.12.0-alpha.0-20241227-9e7d81837", + "@matter/types": "0.12.0-alpha.0-20241227-9e7d81837", "@noble/curves": "^1.7.0" }, "optionalDependencies": { - "@matter/nodejs": "0.12.0-alpha.0-20241220-755393a73" + "@matter/nodejs": "0.12.0-alpha.0-20241227-9e7d81837" } }, "node_modules/@matter/model": { - "version": "0.12.0-alpha.0-20241220-755393a73", - "resolved": "https://registry.npmjs.org/@matter/model/-/model-0.12.0-alpha.0-20241220-755393a73.tgz", - "integrity": "sha512-5ueRMmQbF+/Q1olp0Wg3gFceGfCQYORdHD540FQydxlxtGUsEBteDkhx5jzh/EuOObiXOxyPSF4HPh6jVY2QCg==", + "version": "0.12.0-alpha.0-20241227-9e7d81837", + "resolved": "https://registry.npmjs.org/@matter/model/-/model-0.12.0-alpha.0-20241227-9e7d81837.tgz", + "integrity": "sha512-3XUpAfpuH+ph9loAitL2Rh0GyBnUVNy98TMgO313EcKJGz4wJKJ7Oh27GsM132RU+x4M0vEhyxopzjiTFT+UHQ==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.12.0-alpha.0-20241220-755393a73", + "@matter/general": "0.12.0-alpha.0-20241227-9e7d81837", "@noble/curves": "^1.7.0" } }, "node_modules/@matter/node": { - "version": "0.12.0-alpha.0-20241220-755393a73", - "resolved": "https://registry.npmjs.org/@matter/node/-/node-0.12.0-alpha.0-20241220-755393a73.tgz", - "integrity": "sha512-SvUOMPuo/53tCQDSI/Qb4yvG0pxM7rLpbHVPkRU05HyFwVj3aGj3OKD80k8zEngrQk2Dhs6RjlYTA+tO2GJZBA==", + "version": "0.12.0-alpha.0-20241227-9e7d81837", + "resolved": "https://registry.npmjs.org/@matter/node/-/node-0.12.0-alpha.0-20241227-9e7d81837.tgz", + "integrity": "sha512-A3CerLpIvjXjms4IRxIHZh1skvvmtso7JHpcUFlRbmmfxGZa+wlBkYrpWoh9vYT2eE/Cu250EDqmTCxXqPaY6w==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.12.0-alpha.0-20241220-755393a73", - "@matter/model": "0.12.0-alpha.0-20241220-755393a73", - "@matter/protocol": "0.12.0-alpha.0-20241220-755393a73", - "@matter/types": "0.12.0-alpha.0-20241220-755393a73", + "@matter/general": "0.12.0-alpha.0-20241227-9e7d81837", + "@matter/model": "0.12.0-alpha.0-20241227-9e7d81837", + "@matter/protocol": "0.12.0-alpha.0-20241227-9e7d81837", + "@matter/types": "0.12.0-alpha.0-20241227-9e7d81837", "@noble/curves": "^1.7.0" } }, "node_modules/@matter/nodejs": { - "version": "0.12.0-alpha.0-20241220-755393a73", - "resolved": "https://registry.npmjs.org/@matter/nodejs/-/nodejs-0.12.0-alpha.0-20241220-755393a73.tgz", - "integrity": "sha512-jZc2LwMDr+8F1iWTU1x0TuTYoEnNh15pL+4b7UeLXcdLW2AJEWIJBBw8E8X4Fet6bjPZCI+YsQOU+AxO3PIZ/w==", + "version": "0.12.0-alpha.0-20241227-9e7d81837", + "resolved": "https://registry.npmjs.org/@matter/nodejs/-/nodejs-0.12.0-alpha.0-20241227-9e7d81837.tgz", + "integrity": "sha512-ir096OvJyfIyxio8euD+xQpMUk1RGBTRl450u5CIbEfUglpOU4OR4xqq4b0ZJzLLSSzjACTJX5BDBKoL09Yvzw==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.12.0-alpha.0-20241220-755393a73", - "@matter/node": "0.12.0-alpha.0-20241220-755393a73", - "@matter/protocol": "0.12.0-alpha.0-20241220-755393a73", - "@matter/types": "0.12.0-alpha.0-20241220-755393a73", + "@matter/general": "0.12.0-alpha.0-20241227-9e7d81837", + "@matter/node": "0.12.0-alpha.0-20241227-9e7d81837", + "@matter/protocol": "0.12.0-alpha.0-20241227-9e7d81837", + "@matter/types": "0.12.0-alpha.0-20241227-9e7d81837", "node-localstorage": "^3.0.5" }, "engines": { @@ -1066,44 +1066,44 @@ } }, "node_modules/@matter/nodejs-ble": { - "version": "0.12.0-alpha.0-20241220-755393a73", - "resolved": "https://registry.npmjs.org/@matter/nodejs-ble/-/nodejs-ble-0.12.0-alpha.0-20241220-755393a73.tgz", - "integrity": "sha512-j/C4tDesrZN2jQ6BfPMIUwiLSdhq5CZu75ljz5N9Ao15Dqi3m2V6rpbZsRUQ5GLJTeiUNE647HcLRkcyCfavCw==", + "version": "0.12.0-alpha.0-20241227-9e7d81837", + "resolved": "https://registry.npmjs.org/@matter/nodejs-ble/-/nodejs-ble-0.12.0-alpha.0-20241227-9e7d81837.tgz", + "integrity": "sha512-CONbCL4kTh4qsak5GhC7nGZbrZeoJK7dVnWhz9FtCLuytXe3Pm/Sgf9KLCYyD4oiQE3UNKe9UszP2xnBAITBtA==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@matter/general": "0.12.0-alpha.0-20241220-755393a73", - "@matter/protocol": "0.12.0-alpha.0-20241220-755393a73", - "@matter/types": "0.12.0-alpha.0-20241220-755393a73" + "@matter/general": "0.12.0-alpha.0-20241227-9e7d81837", + "@matter/protocol": "0.12.0-alpha.0-20241227-9e7d81837", + "@matter/types": "0.12.0-alpha.0-20241227-9e7d81837" }, "engines": { "node": ">=18.0.0" }, "optionalDependencies": { "@stoprocent/bleno": "^0.8.1", - "@stoprocent/noble": "^1.15.1" + "@stoprocent/noble": "^1.17.1" } }, "node_modules/@matter/protocol": { - "version": "0.12.0-alpha.0-20241220-755393a73", - "resolved": "https://registry.npmjs.org/@matter/protocol/-/protocol-0.12.0-alpha.0-20241220-755393a73.tgz", - "integrity": "sha512-m2a6klsillM7YL1NVL/3CYDjnBNbnJ6B88cNz4NXJqKURBmkym19lg0h+au093ittG85yKXN3E4/nvCbVXWo2A==", + "version": "0.12.0-alpha.0-20241227-9e7d81837", + "resolved": "https://registry.npmjs.org/@matter/protocol/-/protocol-0.12.0-alpha.0-20241227-9e7d81837.tgz", + "integrity": "sha512-p64NSgQsnOzFBTdJdwEghBtKZR/k2ie6Ocq25PTTLOoj0J05zIiXwgCETMokCa277Qu7hTs40vzzjcZkUAqEkQ==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.12.0-alpha.0-20241220-755393a73", - "@matter/model": "0.12.0-alpha.0-20241220-755393a73", - "@matter/types": "0.12.0-alpha.0-20241220-755393a73", + "@matter/general": "0.12.0-alpha.0-20241227-9e7d81837", + "@matter/model": "0.12.0-alpha.0-20241227-9e7d81837", + "@matter/types": "0.12.0-alpha.0-20241227-9e7d81837", "@noble/curves": "^1.7.0" } }, "node_modules/@matter/types": { - "version": "0.12.0-alpha.0-20241220-755393a73", - "resolved": "https://registry.npmjs.org/@matter/types/-/types-0.12.0-alpha.0-20241220-755393a73.tgz", - "integrity": "sha512-JhmMBXd/bdPleufYb9EZCWhvGEkv9VbBFsutefp5eiRQAyzwzlY5oN2onjNoMd4Cp9HoyCKSPrnjgbo9Njayww==", + "version": "0.12.0-alpha.0-20241227-9e7d81837", + "resolved": "https://registry.npmjs.org/@matter/types/-/types-0.12.0-alpha.0-20241227-9e7d81837.tgz", + "integrity": "sha512-hONAW2FN52Bq7v3SBUWvk8LokREhT+Gv+kND27nN/2bKJorkSoVzoQxsT/0A/e1D91g+ma3KdLuN/Jsutg+GpQ==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.12.0-alpha.0-20241220-755393a73", - "@matter/model": "0.12.0-alpha.0-20241220-755393a73", + "@matter/general": "0.12.0-alpha.0-20241227-9e7d81837", + "@matter/model": "0.12.0-alpha.0-20241227-9e7d81837", "@noble/curves": "^1.7.0" } }, @@ -1266,16 +1266,16 @@ } }, "node_modules/@project-chip/matter.js": { - "version": "0.12.0-alpha.0-20241220-755393a73", - "resolved": "https://registry.npmjs.org/@project-chip/matter.js/-/matter.js-0.12.0-alpha.0-20241220-755393a73.tgz", - "integrity": "sha512-DMLOqcNkRKl8p9SrVGDohTmg8vuoZrTUq7n9Tr2ZnR0HmDkv5XsVwpZJVymcVwQYxcWgLBpC1LWpOGFjMf/i5A==", + "version": "0.12.0-alpha.0-20241227-9e7d81837", + "resolved": "https://registry.npmjs.org/@project-chip/matter.js/-/matter.js-0.12.0-alpha.0-20241227-9e7d81837.tgz", + "integrity": "sha512-3EcDHivSyMe0dPX1jlMQVkCt9kNj7ClhRW02qQQYQxNuJsaqS5ELr+u4nnQdVu4rjeetROQA4SNxghlpLnFD2w==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.12.0-alpha.0-20241220-755393a73", - "@matter/model": "0.12.0-alpha.0-20241220-755393a73", - "@matter/node": "0.12.0-alpha.0-20241220-755393a73", - "@matter/protocol": "0.12.0-alpha.0-20241220-755393a73", - "@matter/types": "0.12.0-alpha.0-20241220-755393a73", + "@matter/general": "0.12.0-alpha.0-20241227-9e7d81837", + "@matter/model": "0.12.0-alpha.0-20241227-9e7d81837", + "@matter/node": "0.12.0-alpha.0-20241227-9e7d81837", + "@matter/protocol": "0.12.0-alpha.0-20241227-9e7d81837", + "@matter/types": "0.12.0-alpha.0-20241227-9e7d81837", "@noble/curves": "^1.7.0" } }, @@ -1696,9 +1696,9 @@ } }, "node_modules/@stoprocent/bluetooth-hci-socket": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@stoprocent/bluetooth-hci-socket/-/bluetooth-hci-socket-1.4.1.tgz", - "integrity": "sha512-p/7G+ydYLIBRX2S8/PJgVU+66CbPucnFOkyDiyrHLtg6wdDCw/Ihm/O2XVVG33Sk77pIsRJsXt7HL6CqjSNDjw==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@stoprocent/bluetooth-hci-socket/-/bluetooth-hci-socket-1.4.3.tgz", + "integrity": "sha512-cBHrMyRdfjWxQkS4fnEm1PdMiVknQfdCB37BP2DH0sHkLvs/BnOrNswqs5wdoZ5rgHvbqlZFZv/o/RYElL9XRg==", "hasInstallScript": true, "license": "MIT", "optional": true, @@ -1722,9 +1722,9 @@ } }, "node_modules/@stoprocent/noble": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@stoprocent/noble/-/noble-1.15.1.tgz", - "integrity": "sha512-e4uG5oQMFDz+94hBtupWvnr7QvmFcNPXZXYB5DS4vqo6/2e6dvReEscRyofv/WxB14O8vc3RQC+QwHDN++vCFw==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@stoprocent/noble/-/noble-1.17.1.tgz", + "integrity": "sha512-pSdSJxZiGx/VFaWPh8DXTe8j5KA+oAGCv8iCfQHV8Rf7T8G2f/SZG4fTiFNfnCe+kxdpHZ/h+GzIIDcZZRHS6Q==", "hasInstallScript": true, "license": "MIT", "optional": true, @@ -1738,7 +1738,7 @@ "node": ">=14" }, "optionalDependencies": { - "@stoprocent/bluetooth-hci-socket": "^1.4.1" + "@stoprocent/bluetooth-hci-socket": "^1.4.3" } }, "node_modules/@tootallnate/once": { diff --git a/package.json b/package.json index 6070fe47..266e50c5 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-20241220-755393a73" + "@matter/nodejs-ble": "0.12.0-alpha.0-20241227-9e7d81837" }, "dependencies": { "@iobroker/adapter-core": "^3.2.3", "@iobroker/i18n": "^0.3.1", "@iobroker/dm-utils": "^0.6.11", "@iobroker/type-detector": "^4.1.1", - "@matter/main": "0.12.0-alpha.0-20241220-755393a73", - "@matter/nodejs": "0.12.0-alpha.0-20241220-755393a73", - "@project-chip/matter.js": "0.12.0-alpha.0-20241220-755393a73", + "@matter/main": "0.12.0-alpha.0-20241227-9e7d81837", + "@matter/nodejs": "0.12.0-alpha.0-20241227-9e7d81837", + "@project-chip/matter.js": "0.12.0-alpha.0-20241227-9e7d81837", "axios": "^1.7.9", "jsonwebtoken": "^9.0.2" }, diff --git a/src/ioBrokerStorageTypes.ts b/src/ioBrokerStorageTypes.ts index 84b648ad..6267a862 100644 --- a/src/ioBrokerStorageTypes.ts +++ b/src/ioBrokerStorageTypes.ts @@ -4,6 +4,7 @@ export interface MatterAdapterConfig extends ioBroker.AdapterConfig { login: string; pass: string; defaultBridge: string; + controllerFabricLabel: string; } export interface BridgeDeviceDescription { diff --git a/src/lib/JsonConfigUtils.ts b/src/lib/JsonConfigUtils.ts index 4cd2de57..d45f4660 100644 --- a/src/lib/JsonConfigUtils.ts +++ b/src/lib/JsonConfigUtils.ts @@ -97,7 +97,7 @@ export function convertDataToJsonConfig(data: StructuredJsonFormData): JsonFormS } if (panelCount === 1) { - return items[`_tab_${Object.keys(data)[0]}`]; + return Object.values(items)[0]; } return { diff --git a/src/lib/SubscribeManager.ts b/src/lib/SubscribeManager.ts index fd822471..873c0295 100644 --- a/src/lib/SubscribeManager.ts +++ b/src/lib/SubscribeManager.ts @@ -1,4 +1,4 @@ -export type SubscribeCallback = (state: ioBroker.State) => Promise; +export type SubscribeCallback = (state: ioBroker.State | null | undefined) => Promise; class SubscribeManager { /** List of all registered subscribed state ids. */ @@ -14,7 +14,7 @@ class SubscribeManager { static async observer(id: string, state: ioBroker.State | null | undefined): Promise { const callbacks = SubscribeManager.subscribes.get(id); - if (callbacks !== undefined && state) { + if (callbacks !== undefined) { for (const callback of callbacks) { await callback(state); } diff --git a/src/lib/devices/DeviceStateObject.ts b/src/lib/devices/DeviceStateObject.ts index 03676359..8212e297 100644 --- a/src/lib/devices/DeviceStateObject.ts +++ b/src/lib/devices/DeviceStateObject.ts @@ -1,5 +1,6 @@ import type { DetectorState, StateType } from '@iobroker/type-detector'; import SubscribeManager from '../SubscribeManager'; +import { EventEmitter } from 'events'; export enum ValueType { String = 'string', @@ -146,7 +147,7 @@ function asCommonType(type: StateType | undefined): ioBroker.CommonType { return 'mixed'; } -export class DeviceStateObject { +export class DeviceStateObject extends EventEmitter { value?: T; updateHandler?: (object: DeviceStateObject) => Promise; isEnum = false; @@ -161,6 +162,7 @@ export class DeviceStateObject { #isIoBrokerState: boolean; #id: string; + #valid: boolean = true; static async create( adapter: ioBroker.Adapter, @@ -183,6 +185,7 @@ export class DeviceStateObject { protected readonly isEnabled: () => boolean, protected readonly unitConversionMap: { [key: string]: (value: number, toDefaultUnit: boolean) => number } = {}, ) { + super(); this.isEnum = valueType === ValueType.Enum; this.#isIoBrokerState = state.isIoBrokerState; this.#id = state.id; @@ -266,6 +269,10 @@ export class DeviceStateObject { return this.object?.common.role ?? 'state'; } + get isValid(): boolean { + return this.#valid; + } + protected parseMinMax(percent = false): void { if (!this.object) { throw new Error(`Object not initialized`); @@ -460,7 +467,22 @@ export class DeviceStateObject { } } - updateState = async (state: ioBroker.State, ignoreEnabledStatus = false): Promise => { + updateState = async (state: ioBroker.State | null | undefined, ignoreEnabledStatus = false): Promise => { + if (!state) { + if (this.#isIoBrokerState) { + // State expired or object got deleted, verify if the object still exists + const obj = await this.adapter.getForeignObjectAsync(this.#id); + if (!obj) { + this.#valid = false; + this.emit('validChanged'); + } + } + return; + } else if (!this.#valid) { + this.#valid = true; + this.emit('validChanged'); + } + if (state.ack !== this.#isIoBrokerState || (!this.isEnabled() && !ignoreEnabledStatus)) { // For Device implementation only acked values are considered to be forwarded to the controllers return; diff --git a/src/lib/devices/GenericDevice.ts b/src/lib/devices/GenericDevice.ts index b25324ef..3d6b3911 100644 --- a/src/lib/devices/GenericDevice.ts +++ b/src/lib/devices/GenericDevice.ts @@ -3,6 +3,7 @@ import type { DetectorState, Types } from '@iobroker/type-detector'; 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 { @@ -88,7 +89,7 @@ interface StateDescription { role?: string; } -abstract class GenericDevice { +abstract class GenericDevice extends EventEmitter { #properties: { [id: string]: StateDescription } = {}; #possibleProperties: { [id: string]: StateDescription } = {}; #adapter: ioBroker.Adapter; @@ -96,6 +97,7 @@ abstract class GenericDevice { #deviceType: Types; #detectedDevice: DetectedDevice; #isIoBrokerDevice: boolean; + #valid = true; #handlers: ((event: { property: PropertyType; value: any; device: GenericDevice }) => Promise)[] = []; protected _construction = new Array<() => Promise>(); @@ -112,6 +114,7 @@ abstract class GenericDevice { adapter: ioBroker.Adapter, protected options?: DeviceOptions, ) { + super(); this.#adapter = adapter; this.#deviceType = detectedDevice.type; this.#detectedDevice = detectedDevice; @@ -185,6 +188,18 @@ abstract class GenericDevice { return !!this.options?.enabled; } + get isValid(): boolean { + return !this.#subscribeObjects.some(obj => !obj.isValid); + } + + #handleValidChange(): void { + const isValid = this.isValid; + if (this.#valid !== isValid) { + this.#valid = isValid; + this.emit('validChanged'); + } + } + isActionAllowedByIdentify(): boolean { return !!this.options?.actionAllowedByIdentify; } @@ -290,6 +305,7 @@ abstract class GenericDevice { if (stateId && !this.#subscribeObjects.find(obj => obj.state.id === stateId)) { await object.subscribe(this.updateState); this.#subscribeObjects.push(object); + object.on('validChanged', () => this.#handleValidChange()); } } callback(object); diff --git a/src/main.ts b/src/main.ts index d2956cf2..c3898ab9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -105,6 +105,7 @@ export class MatterAdapter extends utils.Adapter { #objectProcessQueueTimeout?: NodeJS.Timeout; #currentObjectProcessPromise?: Promise; #controllerActionQueue = new PromiseQueue(); + #blockGuiUpdates = false; public constructor(options: Partial = {}) { super({ @@ -168,16 +169,28 @@ export class MatterAdapter extends utils.Adapter { async shutDownMatterNodes(): Promise { for (const { device } of this.#devices.values()) { - await device?.destroy(); + try { + await device?.destroy(); + } catch (error) { + this.log.warn(`Error while destroying device ${device?.uuid}: ${error.stack}`); + } } this.#devices.clear(); for (const { bridge } of this.#bridges.values()) { - await bridge?.destroy(); + try { + await bridge?.destroy(); + } catch (error) { + this.log.warn(`Error while destroying bridge ${bridge?.uuid}: ${error.stack}`); + } } this.#bridges.clear(); // TODO with next matter.js : if (this.#controllerActionQueue.count) this.#controllerActionQueue.clear(false); - await this.#controller?.stop(); + try { + await this.#controller?.stop(); + } catch (error) { + this.log.warn(`Error while stopping controller: ${error.stack}`); + } this.#controller = undefined; } @@ -250,10 +263,10 @@ export class MatterAdapter extends utils.Adapter { } } } else if (error) { - if (obj.message === 'deviceExtendedInfo') { + if (obj.command === 'deviceExtendedInfo') { const result = this.getGenericErrorDetails('bridge', uuid, error); if (result !== undefined && obj.callback) { - this.sendTo(obj.from, obj.command, result, obj.callback); + this.sendTo(obj.from, obj.command, { result }, obj.callback); } } } @@ -278,10 +291,10 @@ export class MatterAdapter extends utils.Adapter { } } } else if (error) { - if (obj.message === 'deviceExtendedInfo') { + if (obj.command === 'deviceExtendedInfo') { const result = this.getGenericErrorDetails('device', uuid, error); if (result !== undefined && obj.callback) { - this.sendTo(obj.from, obj.command, result, obj.callback); + this.sendTo(obj.from, obj.command, { result }, obj.callback); } } } @@ -377,7 +390,7 @@ export class MatterAdapter extends utils.Adapter { } sendToGui = async (data: any): Promise => { - if (!this.#_guiSubscribes) { + if (!this.#_guiSubscribes || this.#blockGuiUpdates) { return; } if (this.sendToUI) { @@ -572,16 +585,22 @@ export class MatterAdapter extends utils.Adapter { try { // inform GUI about stop await this.sendToGui({ command: 'stopped' }); + } catch { + // ignore + } + this.#blockGuiUpdates = true; - if (this.#deviceManagement) { - await this.#deviceManagement.close(); - } + if (this.#deviceManagement) { + await this.#deviceManagement.close(); + } + try { await this.shutDownMatterNodes(); // close Environment/MDNS? } catch { // ignore } + callback(); } @@ -699,7 +718,9 @@ export class MatterAdapter extends utils.Adapter { } #onStateChange(id: string, state: ioBroker.State | null | undefined): void { - SubscribeManager.observer(id, state).catch(e => this.log.error(`Error while observing state ${id}: ${e}`)); + SubscribeManager.observer(id, state).catch(e => + this.log.error(`Error while observing state ${id}: ${e.stack}`), + ); } async findDeviceFromId(id: string, searchDeviceComingFromLevel?: number): Promise { @@ -783,7 +804,7 @@ export class MatterAdapter extends utils.Adapter { ); if (controlsToCheck.length) { this.log.debug( - `Found ${controlsToCheck?.length} device types for ${id} in ${deviceId}: ${JSON.stringify(controls)}`, + `Found ${controlsToCheck?.length} device types for ${id} in ${deviceId}: ${JSON.stringify(controlsToCheck)}`, ); } else { controlsToCheck = controls; @@ -804,20 +825,20 @@ export class MatterAdapter extends utils.Adapter { this.log.debug( `Found ${controlsWithType?.length} device types for ${deviceId} : ${JSON.stringify(controlsWithType)}`, ); - const mainState = controls[0].states.find((state: DetectorState) => state.id); + const mainState = controlsWithType[0].states.find((state: DetectorState) => state.id); if (mainState) { const id = mainState.id; if (id) { - if (preferredType && controls[0].type !== preferredType) { + if (preferredType && controlsWithType[0].type !== preferredType) { this.log.warn( - `Type detection mismatch for state ${id}: ${controls[0].type} !== ${preferredType}.`, + `Type detection mismatch for state ${id}: ${controlsWithType[0].type} !== ${preferredType}.`, ); } // console.log(`In ${options.id} was detected "${controls[0].type}" with following states:`); - controls[0].states = controls[0].states.filter((state: DetectorState) => state.id); + controlsWithType[0].states = controlsWithType[0].states.filter((state: DetectorState) => state.id); return { - ...controls[0], + ...controlsWithType[0], isIoBrokerDevice: true, } as DetectedDevice; } @@ -1086,11 +1107,12 @@ export class MatterAdapter extends utils.Adapter { return null; } - createMatterController(controllerOptions: MatterControllerConfig): MatterController { + createMatterController(controllerOptions: MatterControllerConfig, fabricLabel: string): MatterController { const matterController = new MatterController({ adapter: this, controllerOptions, updateCallback: () => this.#refreshControllerDevices(), + fabricLabel, }); matterController.init(); // add bridge to server @@ -1325,7 +1347,10 @@ export class MatterAdapter extends utils.Adapter { return { result: true }; } - this.#controller = this.createMatterController(config); + this.#controller = this.createMatterController( + config, + (this.config as MatterAdapterConfig).controllerFabricLabel || `ioBroker ${this.namespace}`, + ); if (handleStart) { await this.#controller.start(); diff --git a/src/matter/BaseServerNode.ts b/src/matter/BaseServerNode.ts index 79baa7fe..e8ca6c1d 100644 --- a/src/matter/BaseServerNode.ts +++ b/src/matter/BaseServerNode.ts @@ -108,7 +108,7 @@ export abstract class BaseServerNode implements GeneralNode { result.connectionInfo = fabrics.map(fabric => ({ vendorId: fabric?.rootVendorId, - vendorName: 'TODO', // TODO: Get vendor name from Clusters + vendorName: fabric.label.toLowerCase().includes('iobroker') ? 'iobroker' : 'unknown', connected: activeSessions .filter(session => session.fabric?.fabricId === fabric.fabricId) .some(({ numberOfActiveSubscriptions }) => !!numberOfActiveSubscriptions), diff --git a/src/matter/BridgedDevicesNode.ts b/src/matter/BridgedDevicesNode.ts index 6d795684..48843094 100644 --- a/src/matter/BridgedDevicesNode.ts +++ b/src/matter/BridgedDevicesNode.ts @@ -11,6 +11,7 @@ import { BaseServerNode } from './BaseServerNode'; import matterDeviceFactory from './to-matter/matterFactory'; import type { GenericDeviceToMatter } from './to-matter/GenericDeviceToMatter'; import type { StructuredJsonFormData } from '../lib/JsonConfigUtils'; +import { IoBrokerCommissioningServer } from './behaviors/IoBrokerCommissioningServer'; export interface BridgeCreateOptions { parameters: BridgeOptions; @@ -154,6 +155,7 @@ class BridgedDevices extends BaseServerNode { this.#deviceEndpoints.set(deviceOptions.uuid, [composedEndpoint]); } await mappingDevice.init(); + mappingDevice.validChanged.on(() => this.updateUiState()); this.#mappingDevices.set(deviceOptions.uuid, mappingDevice); const addedEndpoints = this.#deviceEndpoints.get(deviceOptions.uuid) as Endpoint[]; @@ -199,6 +201,7 @@ class BridgedDevices extends BaseServerNode { this.serverNode = await ServerNode.create( ServerNode.RootEndpoint.with( NetworkCommissioningServer.withFeatures(NetworkCommissioning.Feature.EthernetNetworkInterface), + IoBrokerCommissioningServer, ), { environment: this.adapter.matterEnvironment, @@ -380,10 +383,11 @@ class BridgedDevices extends BaseServerNode { ([ uuid, { + device, error, options: { enabled }, }, - ]) => (error && enabled ? uuid : undefined), + ]) => ((error || !device?.isValid) && enabled ? uuid : undefined), ) .filter(uuid => uuid !== undefined); return errors.length > 0 ? errors : false; diff --git a/src/matter/ControllerNode.ts b/src/matter/ControllerNode.ts index 6fffb00e..986770d2 100644 --- a/src/matter/ControllerNode.ts +++ b/src/matter/ControllerNode.ts @@ -9,7 +9,11 @@ import { import { ManualPairingCodeCodec, QrPairingCodeCodec } from '@matter/main/types'; import { NodeJsBle } from '@matter/nodejs-ble'; import { CommissioningController, type NodeCommissioningOptions } from '@project-chip/matter.js'; -import type { CommissioningControllerNodeOptions, PairedNode } from '@project-chip/matter.js/device'; +import { + NodeStates as PairedNodeStates, + type CommissioningControllerNodeOptions, + type PairedNode, +} from '@project-chip/matter.js/device'; import type { MatterControllerConfig } from '../../src-admin/src/types'; import type { MatterAdapter } from '../main'; import { GeneralMatterNode, type PairedNodeConfig } from './GeneralMatterNode'; @@ -20,6 +24,7 @@ export interface ControllerCreateOptions { adapter: MatterAdapter; controllerOptions: MatterControllerConfig; updateCallback: () => void; + fabricLabel: string; } interface AddDeviceResult { @@ -39,6 +44,7 @@ class Controller implements GeneralNode { #parameters: MatterControllerConfig; readonly #adapter: MatterAdapter; readonly #updateCallback: () => void; + #fabricLabel: string; #commissioningController?: CommissioningController; #nodes = new Map(); #connected: { [nodeId: string]: boolean } = {}; @@ -50,6 +56,7 @@ class Controller implements GeneralNode { this.#adapter = options.adapter; this.#parameters = options.controllerOptions; this.#updateCallback = options.updateCallback; + this.#fabricLabel = options.fabricLabel; } get nodes(): Map { @@ -191,7 +198,7 @@ class Controller implements GeneralNode { node.events.eventTriggered.on(data => { void this.#nodes.get(node.nodeId.toString())?.handleTriggeredEvent(data); }); - node.events.stateChanged.on(async info => { + node.events.stateChanged.on(async (info: PairedNodeStates) => { const nodeDetails = (this.#commissioningController?.getCommissionedNodesDetails() ?? []).find( n => n.nodeId === node.nodeId, ); @@ -210,7 +217,11 @@ class Controller implements GeneralNode { if (deviceNode) { await deviceNode.handleStateChange(info, nodeDetails); } else { - this.#adapter.log.info(`Matter node "${nodeIdStr}" not yet initialized ...`); + if (info !== PairedNodeStates.Disconnected) { + this.#adapter.log.info( + `Matter node "${nodeIdStr}" not initialized ... Got State change to ${info}`, + ); + } } this.#updateCallback(); }); @@ -244,6 +255,7 @@ class Controller implements GeneralNode { environment: this.#adapter.matterEnvironment, id: 'controller', }, + adminFabricLabel: this.#fabricLabel, }); await this.#adapter.extendObjectAsync('controller.info', { @@ -355,12 +367,12 @@ class Controller implements GeneralNode { let productId: number | undefined = undefined; let vendorId: VendorId | undefined = undefined; let knownAddress: ServerAddressIp | undefined = undefined; - if ('manualCode' in data) { + if ('manualCode' in data && data.manualCode.length > 0) { const pairingCodeCodec = ManualPairingCodeCodec.decode(data.manualCode); shortDiscriminator = pairingCodeCodec.shortDiscriminator; longDiscriminator = undefined; passcode = pairingCodeCodec.passcode; - } else if ('qrCode' in data) { + } else if ('qrCode' in data && data.qrCode.length > 0) { const pairingCodeCodec = QrPairingCodeCodec.decode(data.qrCode); // TODO handle the case where multiple devices are included longDiscriminator = pairingCodeCodec[0].discriminator; @@ -476,11 +488,13 @@ class Controller implements GeneralNode { }, device => { this.#adapter.log.debug(`Discovered Device: ${Logger.toJSON(device)}`); - if (!this.#discovering) { - void this.#adapter.sendToGui({ - command: 'discoveredDevice', - device, - }); + if (this.#discovering) { + this.#adapter + .sendToGui({ + command: 'discoveredDevice', + device, + }) + .catch(error => this.#adapter.log.info(`Error sending to GUI: ${error}`)); } }, 60, // timeoutSeconds @@ -536,12 +550,13 @@ class Controller implements GeneralNode { } async stop(): Promise { + this.#adapter.log.info(`Stopping Controller...`); if (this.#discovering) { await this.#discoveryStop(); } - for (const device of this.#nodes.values()) { - await device.destroy(); + for (const node of this.#nodes.values()) { + await node.destroy(); } this.#nodes.clear(); diff --git a/src/matter/DeviceNode.ts b/src/matter/DeviceNode.ts index 344d8b97..b6305588 100644 --- a/src/matter/DeviceNode.ts +++ b/src/matter/DeviceNode.ts @@ -10,6 +10,7 @@ import { BaseServerNode } from './BaseServerNode'; import matterDeviceFactory from './to-matter/matterFactory'; import type { GenericDeviceToMatter } from './to-matter/GenericDeviceToMatter'; import type { StructuredJsonFormData } from '../lib/JsonConfigUtils'; +import { IoBrokerCommissioningServer } from './behaviors/IoBrokerCommissioningServer'; export interface DeviceCreateOptions { parameters: DeviceOptions; @@ -45,6 +46,10 @@ class Device extends BaseServerNode { return this.#parameters.port; } + get error(): boolean { + return !this.#device.isValid; + } + async init(): Promise { this.adapter.log.info(`Adding device ${this.#parameters.uuid} "${this.#parameters.deviceName}"`); @@ -84,6 +89,7 @@ class Device extends BaseServerNode { throw new Error(`ioBroker Device "${this.#device.deviceType}" is not supported`); } + mappingDevice.validChanged.on(() => this.updateUiState()); this.#mappingDevice = mappingDevice; const endpoints = mappingDevice.matterEndpoints; @@ -98,6 +104,7 @@ class Device extends BaseServerNode { this.serverNode = await ServerNode.create( ServerNode.RootEndpoint.with( NetworkCommissioningServer.withFeatures(NetworkCommissioning.Feature.EthernetNetworkInterface), + IoBrokerCommissioningServer, ), { environment: this.adapter.matterEnvironment, diff --git a/src/matter/GeneralMatterNode.ts b/src/matter/GeneralMatterNode.ts index 27e36c16..249bf1ff 100644 --- a/src/matter/GeneralMatterNode.ts +++ b/src/matter/GeneralMatterNode.ts @@ -688,7 +688,7 @@ export class GeneralMatterNode { if (attribute.attribute.writable) { const handler: SubscribeCallback = async state => { - if (state.ack) { + if (!state || state.ack) { return; } // Only controls are processed try { @@ -779,7 +779,7 @@ export class GeneralMatterNode { const handler: SubscribeCallback = async state => { // Only controls are processed - if (state.ack) { + if (!state || state.ack) { return; } @@ -957,7 +957,7 @@ export class GeneralMatterNode { async destroy(): Promise { await this.adapter.setState(this.connectionStateId, false, true); await this.adapter.setState(this.connectionStatusId, NodeStates.Disconnected, true); - return this.clear(); + await this.clear(); } async remove(): Promise { @@ -966,7 +966,7 @@ export class GeneralMatterNode { } await this.adapter.controllerNode.decommissionNode(this.nodeId); await this.clear(); - this.adapter.log.warn(`Node "${this.nodeId}" removed. Removing Storage in ${this.nodeBaseId}`); + this.adapter.log.info(`Node "${this.nodeId}" removed. Removing Storage in ${this.nodeBaseId}`); await this.adapter.delObjectAsync(this.nodeBaseId, { recursive: true }); } diff --git a/src/matter/behaviors/IoBrokerCommissioningServer.ts b/src/matter/behaviors/IoBrokerCommissioningServer.ts new file mode 100644 index 00000000..6209cec5 --- /dev/null +++ b/src/matter/behaviors/IoBrokerCommissioningServer.ts @@ -0,0 +1,7 @@ +import { CommissioningServer } from '@matter/main/node'; + +export class IoBrokerCommissioningServer extends CommissioningServer { + override initiateCommissioning(): void { + // Disable logging of the QR code + } +} diff --git a/src/matter/to-iobroker/GenericDeviceToIoBroker.ts b/src/matter/to-iobroker/GenericDeviceToIoBroker.ts index 5c255b45..3bb971c1 100644 --- a/src/matter/to-iobroker/GenericDeviceToIoBroker.ts +++ b/src/matter/to-iobroker/GenericDeviceToIoBroker.ts @@ -604,6 +604,8 @@ export abstract class GenericDeviceToIoBroker { const powerSource = this.appEndpoint.getClusterClient(PowerSource.Complete); if (powerSource !== undefined) { + states.__header__powersourcedetails = 'Power Source Details'; + if ( powerSource.isAttributeSupportedByName('batQuantity') && powerSource.isAttributeSupportedByName('batReplacementDescription') @@ -619,6 +621,10 @@ export abstract class GenericDeviceToIoBroker { states.batteryVoltage = `${Math.round(percentRemaining / 2)}%`; } } + + if (!states.includedBattery && !states.batteryVoltage) { + delete states.__header__powersourcedetails; + } } return states; @@ -644,7 +650,6 @@ export abstract class GenericDeviceToIoBroker { .map(({ name, code }) => `${name} (${toHex(code)})`) .join(', '), endpoint: this.appEndpoint.number, - __divider__matterdata: true, ...(await this.getMatterStates()), } as Record; diff --git a/src/matter/to-matter/CtToMatter.ts b/src/matter/to-matter/CtToMatter.ts index ce66d193..20640cf6 100644 --- a/src/matter/to-matter/CtToMatter.ts +++ b/src/matter/to-matter/CtToMatter.ts @@ -71,12 +71,9 @@ export class CtToMatter extends GenericElectricityDataDeviceToMatter { registerMatterHandlers(): void { if (this.ioBrokerDevice.hasPower()) { - this.#matterEndpoint.events.ioBrokerEvents.onOffControlled.on(async on => { - const currentValue = !!this.#ioBrokerDevice.getPower(); - if (on !== currentValue) { - await this.#ioBrokerDevice.setPower(on); - } - }); + this.#matterEndpoint.events.ioBrokerEvents.onOffControlled.on( + async on => await this.#ioBrokerDevice.setPower(on), + ); } else { this.#ioBrokerDevice.adapter.log.info( `Device ${this.#ioBrokerDevice.deviceType} (${this.ioBrokerDevice.uuid}) has no mapped power state`, @@ -90,8 +87,7 @@ export class CtToMatter extends GenericElectricityDataDeviceToMatter { await this.#ioBrokerDevice.setTransitionTime(transitionTime * 1000); } - const currentValue = this.#ioBrokerDevice.getDimmer(); - if (level !== currentValue && level !== null) { + if (level !== null) { await this.#ioBrokerDevice.setDimmer(Math.round((level / 254) * 100)); } }); @@ -106,12 +102,8 @@ export class CtToMatter extends GenericElectricityDataDeviceToMatter { await this.#ioBrokerDevice.setTransitionTime(transitionTime * 1000); } - const currentValue = this.#ioBrokerDevice.getTemperature(); - const kelvin = miredsToKelvin(mireds); - if (kelvin !== currentValue) { - await this.#ioBrokerDevice.setTemperature(kelvin); - } + await this.#ioBrokerDevice.setTemperature(kelvin); }); if (this.ioBrokerDevice.hasPower()) { diff --git a/src/matter/to-matter/DimmerToMatter.ts b/src/matter/to-matter/DimmerToMatter.ts index 5e9df562..6442af4a 100644 --- a/src/matter/to-matter/DimmerToMatter.ts +++ b/src/matter/to-matter/DimmerToMatter.ts @@ -50,12 +50,9 @@ export class DimmerToMatter extends GenericElectricityDataDeviceToMatter { registerMatterHandlers(): void { if (this.ioBrokerDevice.hasPower()) { - this.#matterEndpoint.events.ioBrokerEvents.onOffControlled.on(async on => { - const currentValue = !!this.#ioBrokerDevice.getPower(); - if (on !== currentValue) { - await this.#ioBrokerDevice.setPower(on); - } - }); + this.#matterEndpoint.events.ioBrokerEvents.onOffControlled.on( + async on => await this.#ioBrokerDevice.setPower(on), + ); } else { this.#ioBrokerDevice.adapter.log.info( `Device ${this.#ioBrokerDevice.deviceType} (${this.ioBrokerDevice.uuid}) has no mapped power state`, @@ -68,8 +65,7 @@ export class DimmerToMatter extends GenericElectricityDataDeviceToMatter { await this.#ioBrokerDevice.setTransitionTime(transitionTime * 1000); } - const currentValue = this.#ioBrokerDevice.getLevel(); - if (level !== currentValue && level !== null) { + if (level !== null) { await this.#ioBrokerDevice.setLevel(Math.round((level / 254) * 100)); } }); diff --git a/src/matter/to-matter/GenericDeviceToMatter.ts b/src/matter/to-matter/GenericDeviceToMatter.ts index 085cce20..6cf6d33f 100644 --- a/src/matter/to-matter/GenericDeviceToMatter.ts +++ b/src/matter/to-matter/GenericDeviceToMatter.ts @@ -1,4 +1,4 @@ -import { type Endpoint, ObserverGroup, type MaybePromise } from '@matter/main'; +import { capitalize, Observable, type Endpoint, ObserverGroup, type MaybePromise } from '@matter/main'; import type { GenericDevice } from '../../lib'; import type { StructuredJsonFormData } from '../../lib/JsonConfigUtils'; @@ -14,6 +14,8 @@ export abstract class GenericDeviceToMatter { #name: string; #uuid: string; #observers = new ObserverGroup(); + #validChanged = Observable(); + #valid = true; protected constructor(name: string, uuid: string) { this.#name = name; @@ -24,6 +26,10 @@ export abstract class GenericDeviceToMatter { return this.#observers; } + get validChanged(): Observable { + return this.#validChanged; + } + /** * Generic Identify Logic handler. This method uses `doIdentify()` and `resetIdentify()` to handle the identify * logic while an Identification process is running. @@ -83,6 +89,10 @@ export abstract class GenericDeviceToMatter { return this.#uuid; } + get isValid(): boolean { + return this.ioBrokerDevice.isValid; + } + /** * Registers all device Matter change handlers relevant for this device to handle changes by Matter controllers to * control the device. @@ -96,6 +106,14 @@ export abstract class GenericDeviceToMatter { async init(): Promise { this.registerMatterHandlers(); await this.registerIoBrokerHandlersAndInitialize(); + this.ioBrokerDevice.on('validChanged', () => { + const valid = this.ioBrokerDevice.isValid; + if (valid === this.#valid) { + return; + } + this.#valid = valid; + this.#validChanged.emit(); + }); } async destroy(): Promise { @@ -110,10 +128,9 @@ export abstract class GenericDeviceToMatter { details.detectedStates = { __header__device: 'Detected ioBroker Device type', - deviceType: this.ioBrokerDevice.deviceType, + deviceType: capitalize(this.ioBrokerDevice.deviceType ?? 'unknown'), __header__states: 'Detected device states', __text__info: 'The following states were detected for this device:', - __divider__info: true, ...this.ioBrokerDevice.getStates(true, true), }; @@ -122,11 +139,10 @@ export abstract class GenericDeviceToMatter { details.endpoints = { __header__endpoints: 'Device Endpoints', __text__info: 'The following Matter endpoints are mapped for this device.', - __divider__info: true, }; endpoints.forEach(endpoint => { details.endpoints[`__header__endpoint${endpoint.number}`] = `Endpoint ${endpoint.number}`; - details.endpoints[`dt${endpoint.number}__deviceType`] = endpoint.type.name; + details.endpoints[`dt${endpoint.number}__deviceType`] = capitalize(endpoint.type.name); // TODO expose potentially more }); } diff --git a/src/matter/to-matter/LightToMatter.ts b/src/matter/to-matter/LightToMatter.ts index a6aca372..7aa59098 100644 --- a/src/matter/to-matter/LightToMatter.ts +++ b/src/matter/to-matter/LightToMatter.ts @@ -45,12 +45,9 @@ export class LightToMatter extends GenericElectricityDataDeviceToMatter { registerMatterHandlers(): void { // install matter listeners // here we can react on changes from the matter side for onOff - this.#matterEndpoint.events.ioBrokerEvents.onOffControlled.on(async on => { - const currentValue = !!this.#ioBrokerDevice.getPower(); - if (on !== currentValue) { - await this.#ioBrokerDevice.setPower(on); - } - }); + this.#matterEndpoint.events.ioBrokerEvents.onOffControlled.on( + async on => await this.#ioBrokerDevice.setPower(on), + ); let isIdentifying = false; const identifyOptions: IdentifyOptions = {}; diff --git a/src/matter/to-matter/LockToMatter.ts b/src/matter/to-matter/LockToMatter.ts index f2e2c748..130e9c94 100644 --- a/src/matter/to-matter/LockToMatter.ts +++ b/src/matter/to-matter/LockToMatter.ts @@ -55,9 +55,6 @@ export class LockToMatter extends GenericElectricityDataDeviceToMatter { // install matter listeners // here we can react on changes from the matter side for onOff this.#matterEndpoint.events.ioBrokerEvents.doorLockStateControlled.on(async state => { - const currentState = this.#ioBrokerDevice.getLockState() - ? DoorLock.LockState.Locked - : DoorLock.LockState.Unlocked; switch (state) { case null: return; @@ -68,17 +65,11 @@ export class LockToMatter extends GenericElectricityDataDeviceToMatter { if (this.ioBrokerDevice.hasOpen()) { await this.#ioBrokerDevice.setOpen(); } else { - // Adjust state to get it handled by default logic - state = DoorLock.LockState.Unlocked; - if (state !== currentState) { - await this.#ioBrokerDevice.setLockState(state === DoorLock.LockState.Unlocked); - } + await this.#ioBrokerDevice.setLockState(true); // Unlocked } break; default: - if (state !== currentState) { - await this.#ioBrokerDevice.setLockState(state === DoorLock.LockState.Unlocked); - } + await this.#ioBrokerDevice.setLockState(state === DoorLock.LockState.Unlocked); break; } }); diff --git a/src/matter/to-matter/SocketToMatter.ts b/src/matter/to-matter/SocketToMatter.ts index d9b53199..aed5a68e 100644 --- a/src/matter/to-matter/SocketToMatter.ts +++ b/src/matter/to-matter/SocketToMatter.ts @@ -45,12 +45,9 @@ export class SocketToMatter extends GenericElectricityDataDeviceToMatter { registerMatterHandlers(): void { // install matter listeners // here we can react on changes from the matter side for onOff - this.#matterEndpoint.events.ioBrokerEvents.onOffControlled.on(async on => { - const currentValue = !!this.#ioBrokerDevice.getPower(); - if (on !== currentValue) { - await this.#ioBrokerDevice.setPower(on); - } - }); + this.#matterEndpoint.events.ioBrokerEvents.onOffControlled.on( + async on => await this.#ioBrokerDevice.setPower(on), + ); let isIdentifying = false; const identifyOptions: IdentifyOptions = {};