Skip to content

Commit

Permalink
Add Energy and Power clusters to Matter (#98)
Browse files Browse the repository at this point in the history
* Add Energy and Power clusters to Matter

* remove logging

* address review Feedback
  • Loading branch information
Apollon77 authored Sep 17, 2024
1 parent 31ed32f commit 7dcef83
Show file tree
Hide file tree
Showing 9 changed files with 265 additions and 46 deletions.
28 changes: 14 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
6 changes: 2 additions & 4 deletions src/matter/BridgedDevicesNode.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -34,7 +33,6 @@ class BridgedDevices extends BaseServerNode {
#parameters: BridgeOptions;
#devices: GenericDevice[];
#devicesOptions: BridgeDeviceDescription[];
#commissioned: boolean | null = null;
#started = false;
#aggregator?: Endpoint<AggregatorEndpoint>;
#deviceEndpoints = new Map<string, Endpoint[]>();
Expand Down Expand Up @@ -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}`);
}
}
Expand All @@ -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}`);
}
}
Expand Down
3 changes: 1 addition & 2 deletions src/matter/DeviceNode.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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}`);
}
}
Expand Down
10 changes: 6 additions & 4 deletions src/matter/devices/MappingDimmer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@ 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<DimmableLightDevice>;

constructor(ioBrokerDevice: GenericDevice, name: string, uuid: string) {
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
Expand Down Expand Up @@ -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);
}
}
227 changes: 227 additions & 0 deletions src/matter/devices/MappingGenericElectricityDataDevice.ts
Original file line number Diff line number Diff line change
@@ -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<any>, 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<any>,
ioBrokerDevice: ElectricityDataDevice,
): Promise<void> {
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),
},
});
}
}
}
Loading

0 comments on commit 7dcef83

Please sign in to comment.