Skip to content

Commit

Permalink
feat: Added gas_meter to modernExtend (#8863)
Browse files Browse the repository at this point in the history
  • Loading branch information
IgnacioHR authored Feb 22, 2025
1 parent 8dc1719 commit 0a35289
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 23 deletions.
37 changes: 37 additions & 0 deletions src/converters/fromZigbee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,43 @@ const converters1 = {
return payload;
},
} satisfies Fz.Converter,
gas_metering: {
cluster: "seMetering",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if (utils.hasAlreadyProcessedMessage(msg, model)) return;
const payload: KeyValueAny = {};
const multiplier = msg.endpoint.getClusterAttributeValue("seMetering", "multiplier") as number;
const divisor = msg.endpoint.getClusterAttributeValue("seMetering", "divisor") as number;
const factor = multiplier && divisor ? multiplier / divisor : null;

if (msg.data.instantaneousDemand !== undefined) {
const power = msg.data.instantaneousDemand;
const property = utils.postfixWithEndpointName("power", msg, model, meta);
payload[property] = utils.precisionRound(power * (factor ?? 1), 2);
}

if (msg.data.currentSummDelivered !== undefined) {
const value = msg.data.currentSummDelivered;
const property = utils.postfixWithEndpointName("energy", msg, model, meta);
payload[property] = utils.precisionRound(value * (factor ?? 1), 2);
}

if (msg.data.status !== undefined) {
const value = msg.data.status;
const property = utils.postfixWithEndpointName("status", msg, model, meta);
payload[property] = value;
}

if (msg.data.extendedStatus !== undefined) {
const value = msg.data.extendedStatus;
const property = utils.postfixWithEndpointName("extended_status", msg, model, meta);
payload[property] = value;
}

return payload;
},
} satisfies Fz.Converter,
on_off: {
cluster: "genOnOff",
type: ["attributeReport", "readResponse"],
Expand Down
14 changes: 14 additions & 0 deletions src/converters/toZigbee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1834,6 +1834,20 @@ const converters2 = {
await ep.read("seMetering", ["instantaneousDemand"]);
},
} satisfies Tz.Converter,
metering_status: {
key: ["status"],
convertGet: async (entity, key, meta) => {
utils.assertEndpoint(entity);
await utils.enforceEndpoint(entity, key, meta).read("seMetering", ["status"]);
},
} satisfies Tz.Converter,
metering_extended_status: {
key: ["extended_status"],
convertGet: async (entity, key, meta) => {
utils.assertEndpoint(entity);
await utils.enforceEndpoint(entity, key, meta).read("seMetering", ["extendedStatus"]);
},
} satisfies Tz.Converter,
currentsummdelivered: {
key: ["energy"],
convertGet: async (entity, key, meta) => {
Expand Down
134 changes: 111 additions & 23 deletions src/lib/modernExtend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1651,35 +1651,30 @@ export function iasWarning(args?: IasWarningArgs): ModernExtend {

// #region Smart Energy

// Uses Electrical Measurement and/or Metering, but for simplicity was put here.
// Uses Electrical Measurement and/or Gas Metering, but for simplicity was put here.
type MultiplierDivisor = {multiplier?: number; divisor?: number};
export interface ElectricityMeterArgs {
type MeterType = "electricity" | "gas"; // water, etc
interface MeterArgs {
type?: MeterType;
cluster?: "both" | "metering" | "electrical";
electricalMeasurementType?: "both" | "ac" | "dc";
current?: false | (MultiplierDivisor & Partial<ReportingConfigWithoutAttribute>);
power?: false | (MultiplierDivisor & Partial<ReportingConfigWithoutAttribute>);
voltage?: false | (MultiplierDivisor & Partial<ReportingConfigWithoutAttribute>);
energy?: false | (MultiplierDivisor & Partial<ReportingConfigWithoutAttribute>);
producedEnergy?: false | true | (MultiplierDivisor & Partial<ReportingConfigWithoutAttribute>);
acFrequency?: false | true | (MultiplierDivisor & Partial<ReportingConfigWithoutAttribute>);
threePhase?: boolean;
status?: boolean;
extendedStatus?: boolean;
configureReporting?: boolean;
powerFactor?: boolean;
endpointNames?: string[];
fzMetering?: Fz.Converter;
// applies only to electrical
electricalMeasurementType?: "both" | "ac" | "dc";
voltage?: false | (MultiplierDivisor & Partial<ReportingConfigWithoutAttribute>);
current?: false | (MultiplierDivisor & Partial<ReportingConfigWithoutAttribute>);
threePhase?: boolean;
producedEnergy?: false | true | (MultiplierDivisor & Partial<ReportingConfigWithoutAttribute>);
acFrequency?: false | true | (MultiplierDivisor & Partial<ReportingConfigWithoutAttribute>);
powerFactor?: boolean;
fzElectricalMeasurement?: Fz.Converter;
}
export function electricityMeter(args?: ElectricityMeterArgs): ModernExtend {
args = {
cluster: "both",
electricalMeasurementType: "ac",
configureReporting: true,
threePhase: false,
producedEnergy: false,
acFrequency: false,
powerFactor: false,
...args,
};
function genericMeter(args?: MeterArgs) {
if (args.cluster !== "electrical") {
const divisors = new Set([
args.cluster === "metering" && isObject(args.power) ? args.power?.divisor : false,
Expand Down Expand Up @@ -1717,6 +1712,17 @@ export function electricityMeter(args?: ElectricityMeterArgs): ModernExtend {
let fromZigbee: Fz.Converter[];
let toZigbee: Tz.Converter[];

const changeLookup: Record<MeterType, {power: number; energy: number}> = {
gas: {
power: 0.01,
energy: 0.1,
},
electricity: {
power: 0.005,
energy: 0.1,
},
};

const configureLookup = {
haElectricalMeasurement: {
// Report change with every 5W change
Expand Down Expand Up @@ -1776,16 +1782,36 @@ export function electricityMeter(args?: ElectricityMeterArgs): ModernExtend {
},
seMetering: {
// Report change with every 5W change
power: {attribute: "instantaneousDemand", divisor: "divisor", multiplier: "multiplier", forced: args.power, change: 0.005},
power: {
attribute: "instantaneousDemand",
divisor: "divisor",
multiplier: "multiplier",
forced: args.power,
change: changeLookup[args.type].power,
},
// Report change with every 0.1kWh change
energy: {attribute: "currentSummDelivered", divisor: "divisor", multiplier: "multiplier", forced: args.energy, change: 0.1},
energy: {
attribute: "currentSummDelivered",
divisor: "divisor",
multiplier: "multiplier",
forced: args.energy,
change: changeLookup[args.type].energy,
},
produced_energy: {
attribute: "currentSummReceived",
divisor: "divisor",
multiplier: "multiplier",
forced: isObject(args.producedEnergy) ? args.producedEnergy : (false as const),
change: 0.1,
},
status: {
attribute: "status",
change: 1,
},
extended_status: {
attribute: "extendedStatus",
change: 1,
},
},
};

Expand Down Expand Up @@ -1849,6 +1875,13 @@ export function electricityMeter(args?: ElectricityMeterArgs): ModernExtend {
delete configureLookup.haElectricalMeasurement.dc_current;
}

if (args.status === false) {
delete configureLookup.seMetering.status;
}
if (args.extendedStatus === false) {
delete configureLookup.seMetering.extended_status;
}

if (args.cluster === "both") {
if (args.power !== false) exposes.push(e.power().withAccess(ea.STATE_GET));
if (args.voltage !== false) exposes.push(e.voltage().withAccess(ea.STATE_GET));
Expand All @@ -1868,13 +1901,34 @@ export function electricityMeter(args?: ElectricityMeterArgs): ModernExtend {
tz.powerfactor,
];
delete configureLookup.seMetering.power;
} else if (args.cluster === "metering") {
} else if (args.cluster === "metering" && args.type === "electricity") {
if (args.power !== false) exposes.push(e.power().withAccess(ea.STATE_GET));
if (args.energy !== false) exposes.push(e.energy().withAccess(ea.STATE_GET));
if (args.producedEnergy !== false) exposes.push(e.produced_energy().withAccess(ea.STATE_GET));
fromZigbee = [args.fzMetering ?? fz.metering];
toZigbee = [tz.metering_power, tz.currentsummdelivered, tz.currentsummreceived];
delete configureLookup.haElectricalMeasurement;
} else if (args.cluster === "metering" && args.type === "gas") {
if (args.power !== false) exposes.push(e.numeric("power", ea.STATE_GET).withUnit("m³/h").withDescription("Instantaneous gas flow in m³/h"));
if (args.energy !== false) exposes.push(e.numeric("energy", ea.ALL).withUnit("m³").withDescription("Total gas consumption in m³"));
fromZigbee = [args.fzMetering ?? fz.gas_metering];
toZigbee = [
{
key: ["energy"],
convertGet: async (entity, key, meta) => {
const ep = determineEndpoint(entity, meta, "seMetering");
await ep.read("seMetering", ["currentSummDelivered"]);
},
convertSet: async (entity, key, value: number, meta) => {
await entity.write("seMetering", {currentSummDelivered: Math.round(value * 100)});
return {state: {energy: value}};
},
} satisfies Tz.Converter,
tz.metering_power,
tz.metering_status,
tz.metering_extended_status,
];
delete configureLookup.haElectricalMeasurement;
} else if (args.cluster === "electrical") {
if (args.power !== false) exposes.push(e.power().withAccess(ea.STATE_GET));
if (args.voltage !== false) exposes.push(e.voltage().withAccess(ea.STATE_GET));
Expand Down Expand Up @@ -1967,7 +2021,41 @@ export function electricityMeter(args?: ElectricityMeterArgs): ModernExtend {

return result;
}
export interface ElectricityMeterArgs extends MeterArgs {
type?: "electricity";
}
export function electricityMeter(args?: ElectricityMeterArgs): ModernExtend {
args = {
type: "electricity",
cluster: "both",
electricalMeasurementType: "ac",
configureReporting: true,
threePhase: false,
producedEnergy: false,
acFrequency: false,
powerFactor: false,
status: false,
extendedStatus: false,
...args,
};
return genericMeter(args);
}

// Uses Metering to measure volume of gas consumed
export interface GasMeterArgs extends MeterArgs {
type?: "gas";
}
export function gasMeter(args?: GasMeterArgs): ModernExtend {
args = {
type: "gas",
cluster: "metering",
configureReporting: true,
status: true,
extendedStatus: true,
...args,
};
return genericMeter(args);
}
// #endregion

// #region Other extends
Expand Down

0 comments on commit 0a35289

Please sign in to comment.