From 7dcef830e4978b563c7b43b99b3d9850eb3a0d16 Mon Sep 17 00:00:00 2001 From: Ingo Fischer Date: Tue, 17 Sep 2024 16:00:44 +0200 Subject: [PATCH] Add Energy and Power clusters to Matter (#98) * Add Energy and Power clusters to Matter * remove logging * address review Feedback --- package-lock.json | 28 +-- package.json | 6 +- src/matter/BridgedDevicesNode.ts | 6 +- src/matter/DeviceNode.ts | 3 +- src/matter/devices/MappingDimmer.ts | 10 +- .../MappingGenericElectricityDataDevice.ts | 227 ++++++++++++++++++ src/matter/devices/MappingLight.ts | 10 +- src/matter/devices/MappingSocket.ts | 10 +- src/matter/devices/SharedStateHandlers.ts | 11 - 9 files changed, 265 insertions(+), 46 deletions(-) create mode 100644 src/matter/devices/MappingGenericElectricityDataDevice.ts diff --git a/package-lock.json b/package-lock.json index 82b72fd2..288ea431 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,8 +12,8 @@ "@iobroker/adapter-core": "^3.1.6", "@iobroker/dm-utils": "^0.3.1", "@iobroker/type-detector": "^4.0.1", - "@project-chip/matter-node.js": "0.10.3", - "@project-chip/matter.js": "0.10.3", + "@project-chip/matter-node.js": "0.10.4", + "@project-chip/matter.js": "0.10.4", "axios": "^1.7.7", "jsonwebtoken": "^9.0.2" }, @@ -49,7 +49,7 @@ "node": ">=16" }, "optionalDependencies": { - "@project-chip/matter-node-ble.js": "0.10.3" + "@project-chip/matter-node-ble.js": "0.10.4" } }, "node_modules/@alcalzone/pak": { @@ -856,13 +856,13 @@ } }, "node_modules/@project-chip/matter-node-ble.js": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@project-chip/matter-node-ble.js/-/matter-node-ble.js-0.10.3.tgz", - "integrity": "sha512-a4YEbd/GhCuqfOnKroc1imhOOQVP/9WNcRHw921H07uakG8r+Cl23r/rO+CNLJg+VQGH4gTmPWDix/pbDivqzw==", + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@project-chip/matter-node-ble.js/-/matter-node-ble.js-0.10.4.tgz", + "integrity": "sha512-cL0DNTU2w7VPSRN9307VQGC3tQ1JP5QksAi6KzoHbQIbf8xB0zrF9WXmxQbsfXcxzDc13KZ4Az1Ltr2Q2YVyVA==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@project-chip/matter.js": "0.10.3" + "@project-chip/matter.js": "0.10.4" }, "engines": { "node": ">=18.0.0" @@ -873,12 +873,12 @@ } }, "node_modules/@project-chip/matter-node.js": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@project-chip/matter-node.js/-/matter-node.js-0.10.3.tgz", - "integrity": "sha512-XYIGRYWk7YsjR/M6t6RWQxUr5EIXUqblAna5s3d/AAUpPgdGh3KYZblUex0mdtuIfcozn8Lcov/M44JgfgkMGw==", + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@project-chip/matter-node.js/-/matter-node.js-0.10.4.tgz", + "integrity": "sha512-uI+6kSStL8cSChzQ5cmkomm5s3CMnKcc0Racmbgi+igOPphlhI2qNDd343Kd2t+Re0EoXsCOIe+i9nR6GlmNRA==", "license": "Apache-2.0", "dependencies": { - "@project-chip/matter.js": "0.10.3", + "@project-chip/matter.js": "0.10.4", "node-localstorage": "^3.0.5" }, "engines": { @@ -886,9 +886,9 @@ } }, "node_modules/@project-chip/matter.js": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@project-chip/matter.js/-/matter.js-0.10.3.tgz", - "integrity": "sha512-4GtBCN4ZHbmzn67oBDnZBxPhemOsWoErp9ZWRKEJNBEIYPMvyG6N01zNO+NU2IdUmMDOj6SWbIfTJUQ/M1Cy3Q==", + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@project-chip/matter.js/-/matter.js-0.10.4.tgz", + "integrity": "sha512-n6HbfieIVrZ52n0U38YjtDTLmvXvWkcc1r7RKlAVWuje+ae2/Fz6be7mxxSkybFJYNkni4nwDa4D9xp/GeIYpQ==", "license": "Apache-2.0", "dependencies": { "@noble/curves": "^1.5.0" diff --git a/package.json b/package.json index bfafef16..a04cf865 100644 --- a/package.json +++ b/package.json @@ -19,14 +19,14 @@ "url": "https://github.com/ioBroker/ioBroker.matter" }, "optionalDependencies": { - "@project-chip/matter-node-ble.js": "0.10.3" + "@project-chip/matter-node-ble.js": "0.10.4" }, "dependencies": { "@iobroker/adapter-core": "^3.1.6", "@iobroker/dm-utils": "^0.3.1", "@iobroker/type-detector": "^4.0.1", - "@project-chip/matter-node.js": "0.10.3", - "@project-chip/matter.js": "0.10.3", + "@project-chip/matter-node.js": "0.10.4", + "@project-chip/matter.js": "0.10.4", "axios": "^1.7.7", "jsonwebtoken": "^9.0.2" }, diff --git a/src/matter/BridgedDevicesNode.ts b/src/matter/BridgedDevicesNode.ts index cd5bc0ff..39ddb150 100644 --- a/src/matter/BridgedDevicesNode.ts +++ b/src/matter/BridgedDevicesNode.ts @@ -1,5 +1,4 @@ import { BridgedDeviceBasicInformationServer } from '@project-chip/matter.js/behavior/definitions/bridged-device-basic-information'; -import { MatterError } from '@project-chip/matter.js/common'; import { VendorId } from '@project-chip/matter.js/datatype'; import { DeviceTypes } from '@project-chip/matter.js/device'; import { Endpoint } from '@project-chip/matter.js/endpoint'; @@ -34,7 +33,6 @@ class BridgedDevices extends BaseServerNode { #parameters: BridgeOptions; #devices: GenericDevice[]; #devicesOptions: BridgeDeviceDescription[]; - #commissioned: boolean | null = null; #started = false; #aggregator?: Endpoint; #deviceEndpoints = new Map(); @@ -74,7 +72,7 @@ class BridgedDevices extends BaseServerNode { await this.#aggregator.add(endpoint); } catch (error) { // MatterErrors might contain nested information so make sure we see all of this - const errorText = error instanceof MatterError ? inspect(error, { depth: 10 }) : error.stack; + const errorText = inspect(error, { depth: 10 }); this.adapter.log.error(`Error adding endpoint ${endpoint.id} to bridge: ${errorText}`); } } @@ -97,7 +95,7 @@ class BridgedDevices extends BaseServerNode { await composedEndpoint.add(endpoint); } catch (error) { // MatterErrors might contain nested information so make sure we see all of this - const errorText = error instanceof MatterError ? inspect(error, { depth: 10 }) : error.stack; + const errorText = inspect(error, { depth: 10 }); this.adapter.log.error(`Error adding endpoint ${endpoint.id} to bridge: ${errorText}`); } } diff --git a/src/matter/DeviceNode.ts b/src/matter/DeviceNode.ts index 6a5ae275..e99ee239 100644 --- a/src/matter/DeviceNode.ts +++ b/src/matter/DeviceNode.ts @@ -1,4 +1,3 @@ -import { MatterError } from '@project-chip/matter.js/common'; import { VendorId } from '@project-chip/matter.js/datatype'; import { ServerNode } from '@project-chip/matter.js/node'; import { inspect } from 'util'; @@ -117,7 +116,7 @@ class Device extends BaseServerNode { await this.serverNode.add(endpoint); } catch (error) { // MatterErrors might contain nested information so make sure we see all of this - const errorText = error instanceof MatterError ? inspect(error, { depth: 10 }) : error.stack; + const errorText = inspect(error, { depth: 10 }); this.adapter.log.error(`Error adding endpoint ${endpoint.id} to bridge: ${errorText}`); } } diff --git a/src/matter/devices/MappingDimmer.ts b/src/matter/devices/MappingDimmer.ts index 51972aa0..2aa55039 100644 --- a/src/matter/devices/MappingDimmer.ts +++ b/src/matter/devices/MappingDimmer.ts @@ -3,11 +3,12 @@ import { Endpoint } from '@project-chip/matter.js/endpoint'; import { GenericDevice } from '../../lib'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; import Dimmer from '../../lib/devices/Dimmer'; -import { IdentifyOptions, MappingGenericDevice } from './MappingGenericDevice'; -import { initializeElectricityStateHandlers, initializeMaintenanceStateHandlers } from './SharedStateHandlers'; +import { IdentifyOptions } from './MappingGenericDevice'; +import { MappingGenericElectricityDataDevice } from './MappingGenericElectricityDataDevice'; +import { initializeMaintenanceStateHandlers } from './SharedStateHandlers'; /** Mapping Logic to map a ioBroker Dimmer device to a Matter DimmableLightDevice. */ -export class MappingDimmer extends MappingGenericDevice { +export class MappingDimmer extends MappingGenericElectricityDataDevice { readonly #ioBrokerDevice: Dimmer; readonly #matterEndpoint: Endpoint; @@ -15,6 +16,7 @@ export class MappingDimmer extends MappingGenericDevice { super(name, uuid); this.#matterEndpoint = new Endpoint(DimmableLightDevice, { id: uuid }); this.#ioBrokerDevice = ioBrokerDevice as Dimmer; + this.addElectricityDataClusters(this.#matterEndpoint, this.#ioBrokerDevice); } // Just change the power state every second @@ -105,6 +107,6 @@ export class MappingDimmer extends MappingGenericDevice { }); await initializeMaintenanceStateHandlers(this.#matterEndpoint, this.#ioBrokerDevice); - await initializeElectricityStateHandlers(this.#matterEndpoint, this.#ioBrokerDevice); + await this.initializeElectricityStateHandlers(this.#matterEndpoint, this.#ioBrokerDevice); } } diff --git a/src/matter/devices/MappingGenericElectricityDataDevice.ts b/src/matter/devices/MappingGenericElectricityDataDevice.ts new file mode 100644 index 00000000..acc7f27e --- /dev/null +++ b/src/matter/devices/MappingGenericElectricityDataDevice.ts @@ -0,0 +1,227 @@ +import { ElectricalEnergyMeasurementServer } from '@project-chip/matter.js/behavior/definitions/electrical-energy-measurement'; +import { ElectricalPowerMeasurementServer } from '@project-chip/matter.js/behavior/definitions/electrical-power-measurement'; +import { PowerTopologyServer } from '@project-chip/matter.js/behavior/definitions/power-topology'; +import { + ElectricalEnergyMeasurement, + ElectricalPowerMeasurement, + MeasurementType, + PowerTopology, +} from '@project-chip/matter.js/cluster'; +import { Endpoint } from '@project-chip/matter.js/endpoint'; +import { PropertyType } from '../../lib/devices/DeviceStateObject'; +import ElectricityDataDevice from '../../lib/devices/ElectricityDataDevice'; +import { MappingGenericDevice } from './MappingGenericDevice'; + +type EnergyValues = { energy: number }; + +type PowerValues = { + activePower: number | null; + activeCurrent: number | null; + voltage: number | null; + frequency: number | null; +}; + +export abstract class MappingGenericElectricityDataDevice extends MappingGenericDevice { + #powerClusterAdded = false; + #energyClusterAdded = false; + + protected addElectricityDataClusters(endpoint: Endpoint, ioBrokerDevice: ElectricityDataDevice): void { + const measuredAccuracies = []; + if (ioBrokerDevice.getPropertyNames().includes(PropertyType.ElectricPower)) { + measuredAccuracies.push({ + measurementType: MeasurementType.ActivePower, + measured: true, + minMeasuredValue: Number.MIN_SAFE_INTEGER, + maxMeasuredValue: Number.MAX_SAFE_INTEGER, + accuracyRanges: [ + { + rangeMin: Number.MIN_SAFE_INTEGER, + rangeMax: Number.MAX_SAFE_INTEGER, + fixedMax: 1, + }, + ], + }); + } + if (ioBrokerDevice.getPropertyNames().includes(PropertyType.Current)) { + measuredAccuracies.push({ + measurementType: MeasurementType.ActiveCurrent, + measured: true, + minMeasuredValue: Number.MIN_SAFE_INTEGER, + maxMeasuredValue: Number.MAX_SAFE_INTEGER, + accuracyRanges: [ + { + rangeMin: Number.MIN_SAFE_INTEGER, + rangeMax: Number.MAX_SAFE_INTEGER, + fixedMax: 1, + }, + ], + }); + } + if (ioBrokerDevice.getPropertyNames().includes(PropertyType.Voltage)) { + measuredAccuracies.push({ + measurementType: MeasurementType.Voltage, + measured: true, + minMeasuredValue: Number.MIN_SAFE_INTEGER, + maxMeasuredValue: Number.MAX_SAFE_INTEGER, + accuracyRanges: [ + { + rangeMin: Number.MIN_SAFE_INTEGER, + rangeMax: Number.MAX_SAFE_INTEGER, + fixedMax: 1, + }, + ], + }); + } + if (ioBrokerDevice.getPropertyNames().includes(PropertyType.Frequency)) { + measuredAccuracies.push({ + measurementType: MeasurementType.Frequency, + measured: true, + minMeasuredValue: Number.MIN_SAFE_INTEGER, + maxMeasuredValue: Number.MAX_SAFE_INTEGER, + accuracyRanges: [ + { + rangeMin: Number.MIN_SAFE_INTEGER, + rangeMax: Number.MAX_SAFE_INTEGER, + fixedMax: 1, + }, + ], + }); + } + + if (measuredAccuracies.length) { + // Adds the ElectricalPowerMeasurement cluster to the endpoint + endpoint.behaviors.require( + ElectricalPowerMeasurementServer.with(ElectricalPowerMeasurement.Feature.AlternatingCurrent), + { + powerMode: ElectricalPowerMeasurement.PowerMode.Ac, + numberOfMeasurementTypes: measuredAccuracies.length, + accuracy: measuredAccuracies, + }, + ); + this.#powerClusterAdded = true; + } + + if (ioBrokerDevice.getPropertyNames().includes(PropertyType.Consumption)) { + // Adds the ElectricalEnergyMeasurement cluster to the endpoint + endpoint.behaviors.require( + ElectricalEnergyMeasurementServer.with( + ElectricalEnergyMeasurement.Feature.ImportedEnergy, + ElectricalEnergyMeasurement.Feature.CumulativeEnergy, + ), + { + accuracy: { + measurementType: MeasurementType.ElectricalEnergy, + measured: true, + minMeasuredValue: Number.MIN_SAFE_INTEGER, + maxMeasuredValue: Number.MAX_SAFE_INTEGER, + accuracyRanges: [ + { + rangeMin: Number.MIN_SAFE_INTEGER, + rangeMax: Number.MAX_SAFE_INTEGER, + fixedMax: 1, + }, + ], + }, + }, + ); + this.#energyClusterAdded = true; + } + + if (this.#powerClusterAdded || this.#energyClusterAdded) { + // Adds PowerTopology cluster to the endpoint + endpoint.behaviors.require(PowerTopologyServer.with(PowerTopology.Feature.TreeTopology)); + } + } + + #getEnergyValues(ioBrokerDevice: ElectricityDataDevice): EnergyValues { + const energy = ioBrokerDevice.getConsumption() ?? 0; + return { + energy: energy * 1000, // mWh + }; + } + + #getPowerValues(ioBrokerDevice: ElectricityDataDevice): PowerValues { + const electricalPower = ioBrokerDevice.getElectricPower(); + const current = ioBrokerDevice.getCurrent(); + const voltage = ioBrokerDevice.getVoltage(); + const frequency = ioBrokerDevice.getFrequency(); + + return { + activePower: typeof electricalPower === 'number' ? electricalPower * 1000 : null, // mW + activeCurrent: typeof current === 'number' ? current * 1000 : null, // mA + voltage: typeof voltage === 'number' ? voltage * 1000 : null, // mV + frequency: typeof frequency === 'number' ? frequency * 1000 : null, // mHz + }; + } + + /** + * Initialize Electricity states for the device and Map it to Matter. + */ + protected async initializeElectricityStateHandlers( + endpoint: Endpoint, + ioBrokerDevice: ElectricityDataDevice, + ): Promise { + if (this.#powerClusterAdded) { + ioBrokerDevice.onChange(async event => { + switch (event.property) { + case PropertyType.ElectricPower: + await endpoint.set({ + electricalPowerMeasurement: { + activePower: typeof event.value === 'number' ? event.value * 1000 : null, + }, + }); + break; + case PropertyType.Current: + await endpoint.set({ + electricalPowerMeasurement: { + activeCurrent: typeof event.value === 'number' ? event.value * 1000 : null, + }, + }); + break; + case PropertyType.Voltage: + await endpoint.set({ + electricalPowerMeasurement: { + voltage: typeof event.value === 'number' ? event.value * 1000 : null, + }, + }); + break; + case PropertyType.Frequency: + await endpoint.set({ + electricalPowerMeasurement: { + frequency: typeof event.value === 'number' ? event.value * 100 : null, + }, + }); + break; + } + }); + + // init current state from ioBroker side + await endpoint.set({ + electricalPowerMeasurement: this.#getPowerValues(ioBrokerDevice), + }); + } + + if (this.#energyClusterAdded) { + ioBrokerDevice.onChange(async event => { + switch (event.property) { + case PropertyType.Consumption: + await endpoint.set({ + electricalEnergyMeasurement: { + cumulativeEnergyImported: { + energy: typeof event.value === 'number' ? event.value * 1000 : 0, + }, + }, + }); + break; + } + }); + + // init current state from ioBroker side + await endpoint.set({ + electricalEnergyMeasurement: { + cumulativeEnergyImported: this.#getEnergyValues(ioBrokerDevice), + }, + }); + } + } +} diff --git a/src/matter/devices/MappingLight.ts b/src/matter/devices/MappingLight.ts index 9935ff64..d96ce0d9 100644 --- a/src/matter/devices/MappingLight.ts +++ b/src/matter/devices/MappingLight.ts @@ -3,11 +3,12 @@ import { Endpoint } from '@project-chip/matter.js/endpoint'; import { GenericDevice } from '../../lib'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; import Light from '../../lib/devices/Light'; -import { IdentifyOptions, MappingGenericDevice } from './MappingGenericDevice'; -import { initializeElectricityStateHandlers, initializeMaintenanceStateHandlers } from './SharedStateHandlers'; +import { IdentifyOptions } from './MappingGenericDevice'; +import { MappingGenericElectricityDataDevice } from './MappingGenericElectricityDataDevice'; +import { initializeMaintenanceStateHandlers } from './SharedStateHandlers'; /** Mapping Logic to map a ioBroker Light device to a Matter OnOffLightDevice. */ -export class MappingLight extends MappingGenericDevice { +export class MappingLight extends MappingGenericElectricityDataDevice { readonly #ioBrokerDevice: Light; readonly #matterEndpoint: Endpoint; @@ -15,6 +16,7 @@ export class MappingLight extends MappingGenericDevice { super(name, uuid); this.#matterEndpoint = new Endpoint(OnOffLightDevice, { id: uuid }); this.#ioBrokerDevice = ioBrokerDevice as Light; + this.addElectricityDataClusters(this.#matterEndpoint, this.#ioBrokerDevice); } // Just change the power state every second @@ -88,6 +90,6 @@ export class MappingLight extends MappingGenericDevice { }); await initializeMaintenanceStateHandlers(this.#matterEndpoint, this.#ioBrokerDevice); - await initializeElectricityStateHandlers(this.#matterEndpoint, this.#ioBrokerDevice); + await this.initializeElectricityStateHandlers(this.#matterEndpoint, this.#ioBrokerDevice); } } diff --git a/src/matter/devices/MappingSocket.ts b/src/matter/devices/MappingSocket.ts index ff0da33f..fe7b777a 100644 --- a/src/matter/devices/MappingSocket.ts +++ b/src/matter/devices/MappingSocket.ts @@ -3,11 +3,12 @@ import { Endpoint } from '@project-chip/matter.js/endpoint'; import { GenericDevice } from '../../lib'; import { PropertyType } from '../../lib/devices/DeviceStateObject'; import Socket from '../../lib/devices/Socket'; -import { IdentifyOptions, MappingGenericDevice } from './MappingGenericDevice'; -import { initializeElectricityStateHandlers, initializeMaintenanceStateHandlers } from './SharedStateHandlers'; +import { IdentifyOptions } from './MappingGenericDevice'; +import { MappingGenericElectricityDataDevice } from './MappingGenericElectricityDataDevice'; +import { initializeMaintenanceStateHandlers } from './SharedStateHandlers'; /** Mapping Logic to map a ioBroker Socket device to a Matter OnOffPlugInUnitDevice. */ -export class MappingSocket extends MappingGenericDevice { +export class MappingSocket extends MappingGenericElectricityDataDevice { readonly #ioBrokerDevice: Socket; readonly #matterEndpoint: Endpoint; @@ -15,6 +16,7 @@ export class MappingSocket extends MappingGenericDevice { super(name, uuid); this.#matterEndpoint = new Endpoint(OnOffPlugInUnitDevice, { id: uuid }); this.#ioBrokerDevice = ioBrokerDevice as Socket; + this.addElectricityDataClusters(this.#matterEndpoint, this.#ioBrokerDevice); } // Just change the power state every second @@ -88,6 +90,6 @@ export class MappingSocket extends MappingGenericDevice { }); await initializeMaintenanceStateHandlers(this.#matterEndpoint, this.#ioBrokerDevice); - await initializeElectricityStateHandlers(this.#matterEndpoint, this.#ioBrokerDevice); + await this.initializeElectricityStateHandlers(this.#matterEndpoint, this.#ioBrokerDevice); } } diff --git a/src/matter/devices/SharedStateHandlers.ts b/src/matter/devices/SharedStateHandlers.ts index d92770b3..25631e8c 100644 --- a/src/matter/devices/SharedStateHandlers.ts +++ b/src/matter/devices/SharedStateHandlers.ts @@ -75,14 +75,3 @@ export async function initializeMaintenanceStateHandlers( ): Promise { // TODO Add more } - -/** - * Initialize Electricity states for the device and Map it to Matter. - * TODO: Implement and add to Energy cluster - */ -export async function initializeElectricityStateHandlers( - _endpoint: Endpoint, - _ioBrokerDevice: GenericDevice, -): Promise { - // TODO ADD extra Energy cluster for energy states later -}