Add support for 'meter_*' services

This commit is contained in:
Adrian Jagielak 2025-07-26 22:44:17 +02:00
parent a39f2d5928
commit a41b75da83
No known key found for this signature in database
GPG Key ID: 0818CF7AF6C62BFB
9 changed files with 1099 additions and 35 deletions

File diff suppressed because one or more lines are too long

View File

@ -4,6 +4,7 @@
- Improved MQTT components interfaces. - Improved MQTT components interfaces.
- Refactored sensors. - Refactored sensors.
- Added support for 'meter_*' services (electricity meter, gas meter, water meter, heating meter, cooling meter).
## 0.1.5 (25.07.2025) ## 0.1.5 (25.07.2025)

File diff suppressed because one or more lines are too long

View File

@ -12,7 +12,7 @@ export interface BaseComponent {
* The [category](https://developers.home-assistant.io/docs/core/entity#generic-properties) of the entity. * The [category](https://developers.home-assistant.io/docs/core/entity#generic-properties) of the entity.
* When set, the entity category must be `diagnostic` for sensors. * When set, the entity category must be `diagnostic` for sensors.
*/ */
entity_category?: string; entity_category?: null | 'config' | 'diagnostic';
/** /**
* Picture URL for the entity. * Picture URL for the entity.

View File

@ -5,6 +5,7 @@ import {
} from '../fimp/vinculum_pd7_device'; } from '../fimp/vinculum_pd7_device';
import { log } from '../logger'; import { log } from '../logger';
import { _alarm__components } from '../services/_alarm'; import { _alarm__components } from '../services/_alarm';
import { _meter__components } from '../services/_meter';
import { _sensor_binary__components } from '../services/_sensor_binary'; import { _sensor_binary__components } from '../services/_sensor_binary';
import { _sensor_numeric__components } from '../services/_sensor_numeric'; import { _sensor_numeric__components } from '../services/_sensor_numeric';
import { barrier_ctrl__components } from '../services/barrier_ctrl'; import { barrier_ctrl__components } from '../services/barrier_ctrl';
@ -150,6 +151,11 @@ const serviceHandlers: {
fan_ctrl: fan_ctrl__components, fan_ctrl: fan_ctrl__components,
indicator_ctrl: indicator_ctrl__components, indicator_ctrl: indicator_ctrl__components,
media_player: media_player__components, media_player: media_player__components,
meter_elec: _meter__components,
meter_gas: _meter__components,
meter_water: _meter__components,
meter_heating: _meter__components,
meter_cooling: _meter__components,
out_bin_switch: out_bin_switch__components, out_bin_switch: out_bin_switch__components,
out_lvl_switch: out_lvl_switch__components, out_lvl_switch: out_lvl_switch__components,
scene_ctrl: scene_ctrl__components, scene_ctrl: scene_ctrl__components,

View File

@ -177,13 +177,32 @@ const haStateCache: Record<
const attributeTypeKeyMap: Record<string, string> = { const attributeTypeKeyMap: Record<string, string> = {
alarm: 'event', alarm: 'event',
meter: 'props.unit',
meter_export: 'props.unit',
}; };
function getNestedValue(obj: any, path: string): any {
if (!obj) return undefined;
return path
.split('.')
.reduce((acc, key) => (acc != null ? acc[key] : undefined), obj);
}
function getTypeKey(attrName: string): string { function getTypeKey(attrName: string): string {
// Default key is 'type', but override for certain attributes
return attributeTypeKeyMap[attrName] || 'type'; return attributeTypeKeyMap[attrName] || 'type';
} }
function extractTypeDiscriminator(
entry: any,
typeKeyPath: string,
): string | undefined {
// Try to read the discriminator from the whole entry first (meter.props.unit),
// then fall back to inside val (e.g. val.type)
return (
getNestedValue(entry, typeKeyPath) ?? getNestedValue(entry.val, typeKeyPath)
);
}
/** /**
* Helper function to process multiple values for an attribute, handling typed values * Helper function to process multiple values for an attribute, handling typed values
*/ */
@ -199,27 +218,28 @@ function processAttributeValues(values: any[], attrName?: string): any {
return tsB - tsA; // Latest first return tsB - tsA; // Latest first
}); });
const typeKey = getTypeKey(attrName || ''); const typeKeyPath = getTypeKey(attrName || '');
const hasTypedValues = sortedValues.some( // Build list of entries that carry a discriminator
(v) => v.val && typeof v.val === 'object' && v.val[typeKey], const entriesWithType = sortedValues
); .map((v) => ({ v, key: extractTypeDiscriminator(v, typeKeyPath) }))
.filter((x) => !!x.key) as Array<{ v: any; key: string }>;
if (!hasTypedValues) { if (entriesWithType.length === 0) {
// No typed values, return the latest value // Not a typed attribute → just return latest value
return sortedValues[0].val; return sortedValues[0].val;
} }
// Group by type, keeping only the latest value for each type // Group by (normalized) discriminator, keeping only the latest per type
const typeMap: Record<string, any> = {}; const typeMap: Record<string, any> = {};
for (const value of sortedValues) { for (const { v, key } of entriesWithType) {
if (value.val && typeof value.val === 'object' && value.val[typeKey]) {
const key = value.val[typeKey];
if (!typeMap[key]) { if (!typeMap[key]) {
const { [typeKey]: _, ...valueWithoutType } = value.val; const payload =
typeMap[key] = valueWithoutType; v && typeof v.val === 'object' && v.val !== null
} ? { ...v.val }
: { val: v.val }; // wrap primitives like meter readings
typeMap[key] = payload;
} }
} }
@ -284,6 +304,14 @@ export function haUpdateStateValueReport(parameters: {
value: any; value: any;
attrName: string; attrName: string;
}) { }) {
if (
parameters.attrName === 'meter' ||
parameters.attrName === 'meter_export'
) {
// Ignore meter readings for now, relying on periodical site state updates
return;
}
// Strip the FIMP envelope so we end up with "/rt:dev/…/ad:x_y" // Strip the FIMP envelope so we end up with "/rt:dev/…/ad:x_y"
const addr = parameters.topic.replace(/^pt:j1\/mt:evt/, ''); const addr = parameters.topic.replace(/^pt:j1\/mt:evt/, '');
const typeKey = getTypeKey(parameters.attrName); const typeKey = getTypeKey(parameters.attrName);
@ -295,13 +323,14 @@ export function haUpdateStateValueReport(parameters: {
if ( if (
parameters.value && parameters.value &&
typeof parameters.value === 'object' && typeof parameters.value === 'object' &&
parameters.value[typeKey] getNestedValue(parameters.value, typeKey)
) { ) {
// Handle typed value update const key = getNestedValue(parameters.value, typeKey);
const key = parameters.value[typeKey]; const valueWithoutType =
const { [typeKey]: _, ...valueWithoutType } = parameters.value; typeof parameters.value === 'object' && parameters.value !== null
? { ...parameters.value }
: { val: parameters.value };
// Get current attribute value
const currentAttrValue = payload[addr][parameters.attrName]; const currentAttrValue = payload[addr][parameters.attrName];
if ( if (

View File

@ -2165,5 +2165,176 @@
}, },
"type": null "type": null
} }
},
{
"client": {
"name": "Smart Electric Meter"
},
"id": 145,
"model": "zb - _TZ3040_bb6xaihh - TS0601",
"modelAlias": "Smart Electric Meter",
"type": {
"subtype": "main_elec",
"type": "meter"
},
"services": {
"meter_elec": {
"name": "meter_elec",
"addr": "/rt:dev/rn:zigbee/ad:1/sv:meter_elec/ad:145_1",
"enabled": true,
"props": {
"sup_units": ["kWh", "W"],
"sup_extended_vals": ["p_import", "u1", "i1", "freq", "p_factor"]
},
"intf": [
"cmd.meter.get_report",
"evt.meter.report",
"cmd.meter_ext.get_report",
"evt.meter_ext.report",
"cmd.meter.reset"
]
}
}
},
{
"client": {
"name": "3-Phase Smart Meter"
},
"id": 246,
"model": "Tibber - Tibber - Pulse Bridge",
"modelAlias": "Tibber Pulse Bridge",
"type": {
"subtype": "main_elec",
"type": "meter"
},
"services": {
"meter_elec": {
"name": "meter_elec",
"addr": "/rt:dev/rn:tibber/ad:1/sv:meter_elec/ad:246",
"enabled": true,
"props": {
"sup_units": ["kWh", "W"],
"sup_export_units": ["kWh", "W"],
"sup_extended_vals": [
"p_import",
"p_export",
"u1",
"u2",
"u3",
"i1",
"i2",
"i3",
"p1",
"p2",
"p3",
"freq",
"p_factor",
"e_import",
"e_export"
]
},
"intf": [
"cmd.meter.get_report",
"evt.meter.report",
"cmd.meter_export.get_report",
"evt.meter_export.report",
"cmd.meter_ext.get_report",
"evt.meter_ext.report",
"cmd.meter.reset"
]
}
}
},
{
"client": {
"name": "Smart Gas Meter"
},
"id": 189,
"model": "zb - _TZ3000_h4yw2xn6 - TS0601",
"modelAlias": "Smart Gas Meter",
"type": {
"subtype": null,
"type": "meter"
},
"services": {
"meter_gas": {
"name": "meter_gas",
"addr": "/rt:dev/rn:zigbee/ad:1/sv:meter_gas/ad:189",
"enabled": true,
"props": {
"sup_units": ["cub_m", "pulse_c"]
},
"intf": ["cmd.meter.get_report", "evt.meter.report", "cmd.meter.reset"]
}
}
},
{
"client": {
"name": "Smart Water Meter"
},
"id": 222,
"model": "zw - Qubino - ZMNHWD1",
"modelAlias": "Qubino Water Meter",
"type": {
"subtype": null,
"type": "meter"
},
"services": {
"meter_water": {
"name": "meter_water",
"addr": "/rt:dev/rn:zw/ad:1/sv:meter_water/ad:222",
"enabled": true,
"props": {
"sup_units": ["cub_m", "gallon"]
},
"intf": ["cmd.meter.get_report", "evt.meter.report", "cmd.meter.reset"]
}
}
},
{
"client": {
"name": "Heat Meter"
},
"id": 298,
"model": "Hoiax - Futurehome - Heat Meter",
"modelAlias": "Futurehome Heat Meter",
"type": {
"subtype": null,
"type": "meter"
},
"services": {
"meter_heat": {
"name": "meter_heat",
"addr": "/rt:dev/rn:hoiax/ad:1/sv:meter_heat/ad:298",
"enabled": true,
"props": {
"sup_units": ["kWh", "W"]
},
"intf": ["cmd.meter.get_report", "evt.meter.report", "cmd.meter.reset"]
}
}
},
{
"client": {
"name": "Cooling Meter"
},
"id": 312,
"model": "Hoiax - Futurehome - Cooling Meter",
"modelAlias": "Futurehome Cooling Meter",
"type": {
"subtype": null,
"type": "meter"
},
"services": {
"meter_cooling": {
"name": "meter_cooling",
"addr": "/rt:dev/rn:hoiax/ad:1/sv:meter_cooling/ad:312",
"enabled": true,
"props": {
"sup_units": ["kWh", "W"]
},
"intf": ["cmd.meter.get_report", "evt.meter.report", "cmd.meter.reset"]
}
}
} }
] ]

View File

@ -1472,5 +1472,260 @@
"name": "alarm_heat" "name": "alarm_heat"
} }
] ]
},
{
"id": 145,
"services": [
{
"addr": "/rt:dev/rn:zigbee/ad:1/sv:meter_elec/ad:145_1",
"attributes": [
{
"name": "meter",
"values": [
{
"props": {
"unit": "kWh"
},
"ts": "2025-01-15 10:30:00 +0100",
"val": 1245.168,
"val_t": "float"
},
{
"props": {
"unit": "W"
},
"ts": "2025-01-15 10:30:05 +0100",
"val": 850,
"val_t": "int"
}
]
},
{
"name": "meter_ext",
"values": [
{
"ts": "2025-01-15 10:30:05 +0100",
"val": {
"p_import": 850,
"u1": 236.5,
"i1": 3.6,
"freq": 50.02,
"p_factor": 0.98
},
"val_t": "float_map"
}
]
}
],
"name": "meter_elec"
}
]
},
{
"id": 246,
"services": [
{
"addr": "/rt:dev/rn:tibber/ad:1/sv:meter_elec/ad:246",
"attributes": [
{
"name": "meter",
"values": [
{
"props": {
"unit": "kWh"
},
"ts": "2025-01-15 10:30:00 +0100",
"val": 15420.891,
"val_t": "float"
},
{
"props": {
"unit": "W"
},
"ts": "2025-01-15 10:30:05 +0100",
"val": 2450,
"val_t": "int"
}
]
},
{
"name": "meter_export",
"values": [
{
"props": {
"unit": "kWh"
},
"ts": "2025-01-15 10:30:00 +0100",
"val": 892.456,
"val_t": "float"
},
{
"props": {
"unit": "W"
},
"ts": "2025-01-15 10:30:05 +0100",
"val": 0,
"val_t": "int"
}
]
},
{
"name": "meter_ext",
"values": [
{
"ts": "2025-01-15 10:30:05 +0100",
"val": {
"p_import": 2450,
"p_export": 0,
"u1": 235.8,
"u2": 236.1,
"u3": 235.4,
"i1": 10.4,
"i2": 10.1,
"i3": 10.8,
"p1": 800,
"p2": 825,
"p3": 825,
"freq": 50.01,
"p_factor": 0.97,
"e_import": 15420891,
"e_export": 892456
},
"val_t": "float_map"
}
]
}
],
"name": "meter_elec"
}
]
},
{
"id": 189,
"services": [
{
"addr": "/rt:dev/rn:zigbee/ad:1/sv:meter_gas/ad:189",
"attributes": [
{
"name": "meter",
"values": [
{
"props": {
"unit": "cub_m"
},
"ts": "2025-01-15 10:30:00 +0100",
"val": 1847.362,
"val_t": "float"
},
{
"props": {
"unit": "pulse_c"
},
"ts": "2025-01-15 10:30:00 +0100",
"val": 184736,
"val_t": "int"
}
]
}
],
"name": "meter_gas"
}
]
},
{
"id": 222,
"services": [
{
"addr": "/rt:dev/rn:zw/ad:1/sv:meter_water/ad:222",
"attributes": [
{
"name": "meter",
"values": [
{
"props": {
"unit": "cub_m"
},
"ts": "2025-01-15 10:30:00 +0100",
"val": 94.127,
"val_t": "float"
},
{
"props": {
"unit": "gallon"
},
"ts": "2025-01-15 10:30:00 +0100",
"val": 24872.5,
"val_t": "float"
}
]
}
],
"name": "meter_water"
}
]
},
{
"id": 298,
"services": [
{
"addr": "/rt:dev/rn:hoiax/ad:1/sv:meter_heat/ad:298",
"attributes": [
{
"name": "meter",
"values": [
{
"props": {
"unit": "kWh"
},
"ts": "2025-01-15 10:30:00 +0100",
"val": 2847.92,
"val_t": "float"
},
{
"props": {
"unit": "W"
},
"ts": "2025-01-15 10:30:05 +0100",
"val": 12500,
"val_t": "int"
}
]
}
],
"name": "meter_heat"
}
]
},
{
"id": 312,
"services": [
{
"addr": "/rt:dev/rn:hoiax/ad:1/sv:meter_cooling/ad:312",
"attributes": [
{
"name": "meter",
"values": [
{
"props": {
"unit": "kWh"
},
"ts": "2025-01-15 10:30:00 +0100",
"val": 1456.73,
"val_t": "float"
},
{
"props": {
"unit": "W"
},
"ts": "2025-01-15 10:30:05 +0100",
"val": 8750,
"val_t": "int"
}
]
}
],
"name": "meter_cooling"
}
]
} }
] ]

View File

@ -0,0 +1,610 @@
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { SensorComponent } from '../ha/mqtt_components/sensor';
import {
SensorDeviceClass,
SensorStateClass,
} from '../ha/mqtt_components/_enums';
import {
CommandHandlers,
ServiceComponentsCreationResult,
} from '../ha/publish_device';
import { sendFimpMsg } from '../fimp/fimp';
import { HaMqttComponent } from '../ha/mqtt_components/_component';
// Define meter value to device class mapping
const METER_VALUE_DEVICE_CLASS_MAP: Record<string, SensorDeviceClass> = {
// Power values
p_import: 'power',
p1: 'power',
p2: 'power',
p3: 'power',
p_export: 'power',
p1_export: 'power',
p2_export: 'power',
p3_export: 'power',
p_import_react: 'reactive_power',
p1_import_react: 'reactive_power',
p2_import_react: 'reactive_power',
p3_import_react: 'reactive_power',
p_export_react: 'reactive_power',
p1_export_react: 'reactive_power',
p2_export_react: 'reactive_power',
p3_export_react: 'reactive_power',
p_import_apparent: 'apparent_power',
p1_import_apparent: 'apparent_power',
p2_import_apparent: 'apparent_power',
p3_import_apparent: 'apparent_power',
p_export_apparent: 'apparent_power',
p1_export_apparent: 'apparent_power',
p2_export_apparent: 'apparent_power',
p3_export_apparent: 'apparent_power',
dc_p: 'power',
// Energy values
e_import: 'energy',
e1_import: 'energy',
e2_import: 'energy',
e3_import: 'energy',
e_export: 'energy',
e1_export: 'energy',
e2_export: 'energy',
e3_export: 'energy',
e_import_react: 'reactive_energy',
e_export_react: 'reactive_energy',
e_import_apparent: 'energy',
e_export_apparent: 'energy',
// Voltage values
u: 'voltage',
u1: 'voltage',
u2: 'voltage',
u3: 'voltage',
u_export: 'voltage',
u1_export: 'voltage',
u2_export: 'voltage',
u3_export: 'voltage',
dc_u: 'voltage',
// Current values
i: 'current',
i1: 'current',
i2: 'current',
i3: 'current',
i_export: 'current',
i1_export: 'current',
i2_export: 'current',
i3_export: 'current',
dc_i: 'current',
// Power factor
p_factor: 'power_factor',
p1_factor: 'power_factor',
p2_factor: 'power_factor',
p3_factor: 'power_factor',
p_factor_export: 'power_factor',
p1_factor_export: 'power_factor',
p2_factor_export: 'power_factor',
p3_factor_export: 'power_factor',
// Frequency
freq: 'frequency',
// Gas meter
// Using 'gas' device class for gas volumes
// Water meter
// Using 'water' device class for water volumes
};
// Define meter value to state class mapping
const METER_VALUE_STATE_CLASS_MAP: Record<string, SensorStateClass> = {
// Energy values are typically total_increasing
e_import: 'total_increasing',
e1_import: 'total_increasing',
e2_import: 'total_increasing',
e3_import: 'total_increasing',
e_export: 'total_increasing',
e1_export: 'total_increasing',
e2_export: 'total_increasing',
e3_export: 'total_increasing',
e_import_react: 'total_increasing',
e_export_react: 'total_increasing',
e_import_apparent: 'total_increasing',
e_export_apparent: 'total_increasing',
// Power values are measurements
p_import: 'measurement',
p1: 'measurement',
p2: 'measurement',
p3: 'measurement',
p_export: 'measurement',
p1_export: 'measurement',
p2_export: 'measurement',
p3_export: 'measurement',
p_import_react: 'measurement',
p1_import_react: 'measurement',
p2_import_react: 'measurement',
p3_import_react: 'measurement',
p_export_react: 'measurement',
p1_export_react: 'measurement',
p2_export_react: 'measurement',
p3_export_react: 'measurement',
p_import_apparent: 'measurement',
p1_import_apparent: 'measurement',
p2_import_apparent: 'measurement',
p3_import_apparent: 'measurement',
p_export_apparent: 'measurement',
p1_export_apparent: 'measurement',
p2_export_apparent: 'measurement',
p3_export_apparent: 'measurement',
dc_p: 'measurement',
// Voltage and current are measurements
u: 'measurement',
u1: 'measurement',
u2: 'measurement',
u3: 'measurement',
u_export: 'measurement',
u1_export: 'measurement',
u2_export: 'measurement',
u3_export: 'measurement',
dc_u: 'measurement',
i: 'measurement',
i1: 'measurement',
i2: 'measurement',
i3: 'measurement',
i_export: 'measurement',
i1_export: 'measurement',
i2_export: 'measurement',
i3_export: 'measurement',
dc_i: 'measurement',
// Power factor is measurement
p_factor: 'measurement',
p1_factor: 'measurement',
p2_factor: 'measurement',
p3_factor: 'measurement',
p_factor_export: 'measurement',
p1_factor_export: 'measurement',
p2_factor_export: 'measurement',
p3_factor_export: 'measurement',
// Frequency is measurement
freq: 'measurement',
// Gas/water volumes are typically total_increasing
// (using generic names as they depend on meter configuration)
};
// Define friendly names for meter values
const METER_VALUE_NAMES: Record<string, string> = {
// Power
p_import: 'Power Import',
p1: 'Power Phase 1',
p2: 'Power Phase 2',
p3: 'Power Phase 3',
p_export: 'Power Export',
p1_export: 'Power Export Phase 1',
p2_export: 'Power Export Phase 2',
p3_export: 'Power Export Phase 3',
p_import_react: 'Reactive Power Import',
p1_import_react: 'Reactive Power Import Phase 1',
p2_import_react: 'Reactive Power Import Phase 2',
p3_import_react: 'Reactive Power Import Phase 3',
p_export_react: 'Reactive Power Export',
p1_export_react: 'Reactive Power Export Phase 1',
p2_export_react: 'Reactive Power Export Phase 2',
p3_export_react: 'Reactive Power Export Phase 3',
p_import_apparent: 'Apparent Power Import',
p1_import_apparent: 'Apparent Power Import Phase 1',
p2_import_apparent: 'Apparent Power Import Phase 2',
p3_import_apparent: 'Apparent Power Import Phase 3',
p_export_apparent: 'Apparent Power Export',
p1_export_apparent: 'Apparent Power Export Phase 1',
p2_export_apparent: 'Apparent Power Export Phase 2',
p3_export_apparent: 'Apparent Power Export Phase 3',
dc_p: 'DC Power',
// Energy
e_import: 'Energy Import',
e1_import: 'Energy Import Phase 1',
e2_import: 'Energy Import Phase 2',
e3_import: 'Energy Import Phase 3',
e_export: 'Energy Export',
e1_export: 'Energy Export Phase 1',
e2_export: 'Energy Export Phase 2',
e3_export: 'Energy Export Phase 3',
e_import_react: 'Reactive Energy Import',
e_export_react: 'Reactive Energy Export',
e_import_apparent: 'Apparent Energy Import',
e_export_apparent: 'Apparent Energy Export',
// Voltage
u: 'Voltage',
u1: 'Voltage Phase 1',
u2: 'Voltage Phase 2',
u3: 'Voltage Phase 3',
u_export: 'Voltage Export',
u1_export: 'Voltage Export Phase 1',
u2_export: 'Voltage Export Phase 2',
u3_export: 'Voltage Export Phase 3',
dc_u: 'DC Voltage',
// Current
i: 'Current',
i1: 'Current Phase 1',
i2: 'Current Phase 2',
i3: 'Current Phase 3',
i_export: 'Current Export',
i1_export: 'Current Export Phase 1',
i2_export: 'Current Export Phase 2',
i3_export: 'Current Export Phase 3',
dc_i: 'DC Current',
// Power Factor
p_factor: 'Power Factor',
p1_factor: 'Power Factor Phase 1',
p2_factor: 'Power Factor Phase 2',
p3_factor: 'Power Factor Phase 3',
p_factor_export: 'Power Factor Export',
p1_factor_export: 'Power Factor Export Phase 1',
p2_factor_export: 'Power Factor Export Phase 2',
p3_factor_export: 'Power Factor Export Phase 3',
// Frequency
freq: 'Frequency',
};
export function _meter__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service,
svcName: string,
): ServiceComponentsCreationResult | undefined {
const components: Record<string, HaMqttComponent> = {};
const commandHandlers: CommandHandlers = {};
// Get supported units and extended values from service properties
const supportedUnits = svc.props?.sup_units || [];
const supportedExportUnits = svc.props?.sup_export_units || [];
const supportedExtendedValues = svc.props?.sup_extended_vals || [];
// Handle regular meter readings (meter.{unit}.val)
// These typically come from evt.meter.report
if (svc.intf?.includes('evt.meter.report')) {
for (const unit of supportedUnits) {
const componentId = `${svc.addr}_${unit}`;
let friendlyName = 'Meter';
let deviceClass: SensorDeviceClass | undefined;
let stateClass: SensorStateClass = 'total_increasing'; // Default for meter readings
// Set appropriate device class and name based on unit
switch (unit) {
case 'kWh':
friendlyName = 'Energy';
deviceClass = 'energy';
break;
case 'W':
friendlyName = 'Power';
deviceClass = 'power';
stateClass = 'measurement';
break;
case 'A':
friendlyName = 'Current';
deviceClass = 'current';
stateClass = 'measurement';
break;
case 'V':
friendlyName = 'Voltage';
deviceClass = 'voltage';
stateClass = 'measurement';
break;
case 'VA':
friendlyName = 'Apparent Power';
deviceClass = 'apparent_power';
stateClass = 'measurement';
break;
case 'kVAh':
friendlyName = 'Apparent Energy';
deviceClass = 'energy';
break;
case 'VAr':
friendlyName = 'Reactive Power';
deviceClass = 'reactive_power';
stateClass = 'measurement';
break;
case 'kVArh':
friendlyName = 'Reactive Energy';
deviceClass = 'reactive_energy';
break;
case 'Hz':
friendlyName = 'Frequency';
deviceClass = 'frequency';
stateClass = 'measurement';
break;
case 'power_factor':
friendlyName = 'Power Factor';
deviceClass = 'power_factor';
stateClass = 'measurement';
break;
case 'pulse_c':
friendlyName = 'Pulse Count';
stateClass = 'total_increasing';
break;
case 'cub_m':
friendlyName = 'Volume';
deviceClass =
svcName === 'meter_gas'
? 'gas'
: svcName === 'meter_water'
? 'water'
: 'volume';
stateClass = 'total_increasing';
break;
case 'cub_f':
friendlyName = 'Volume';
deviceClass =
svcName === 'meter_gas'
? 'gas'
: svcName === 'meter_water'
? 'water'
: 'volume';
stateClass = 'total_increasing';
break;
case 'gallon':
friendlyName = 'Volume';
deviceClass =
svcName === 'meter_gas'
? 'gas'
: svcName === 'meter_water'
? 'water'
: 'volume';
stateClass = 'total_increasing';
break;
default:
friendlyName = `Meter (${unit})`;
break;
}
const component: SensorComponent = {
unique_id: componentId,
platform: 'sensor',
entity_category: 'diagnostic',
name: friendlyName,
unit_of_measurement: unit,
state_class: stateClass,
value_template: `{{ value_json['${svc.addr}'].meter.${unit}.val | default(0) }}`,
};
if (deviceClass) {
component.device_class = deviceClass;
}
// Set suggested display precision based on unit
if (unit === 'kWh' || unit === 'kVAh' || unit === 'kVArh') {
component.suggested_display_precision = 3;
} else if (
unit === 'W' ||
unit === 'VA' ||
unit === 'VAr' ||
unit === 'V' ||
unit === 'A'
) {
component.suggested_display_precision = 1;
} else if (unit === 'Hz') {
component.suggested_display_precision = 2;
} else if (unit === 'power_factor') {
component.suggested_display_precision = 3;
} else if (unit === 'cub_m' || unit === 'cub_f' || unit === 'gallon') {
component.suggested_display_precision = 2;
}
components[componentId] = component;
}
}
// Handle export meter readings (meter.{unit}.val) for bidirectional meters
if (svc.intf?.includes('evt.meter_export.report')) {
for (const unit of supportedExportUnits) {
const componentId = `${svc.addr}_export_${unit}`;
let friendlyName = 'Export Meter';
let deviceClass: SensorDeviceClass | undefined;
let stateClass: SensorStateClass = 'total_increasing'; // Default for meter readings
// Set appropriate device class and name based on unit
switch (unit) {
case 'kWh':
friendlyName = 'Energy Export';
deviceClass = 'energy';
break;
case 'W':
friendlyName = 'Power Export';
deviceClass = 'power';
stateClass = 'measurement';
break;
case 'A':
friendlyName = 'Current Export';
deviceClass = 'current';
stateClass = 'measurement';
break;
case 'V':
friendlyName = 'Voltage Export';
deviceClass = 'voltage';
stateClass = 'measurement';
break;
case 'VA':
friendlyName = 'Apparent Power Export';
deviceClass = 'apparent_power';
stateClass = 'measurement';
break;
case 'kVAh':
friendlyName = 'Apparent Energy Export';
deviceClass = 'energy';
break;
case 'VAr':
friendlyName = 'Reactive Power Export';
deviceClass = 'reactive_power';
stateClass = 'measurement';
break;
case 'kVArh':
friendlyName = 'Reactive Energy Export';
deviceClass = 'reactive_energy';
break;
default:
friendlyName = `Export Meter (${unit})`;
break;
}
const component: SensorComponent = {
unique_id: componentId,
platform: 'sensor',
entity_category: 'diagnostic',
name: friendlyName,
unit_of_measurement: unit,
state_class: stateClass,
value_template: `{{ value_json['${svc.addr}'].meter_export.${unit}.val | default(0) }}`,
};
if (deviceClass) {
component.device_class = deviceClass;
}
// Set suggested display precision
if (unit === 'kWh' || unit === 'kVAh' || unit === 'kVArh') {
component.suggested_display_precision = 3;
} else if (
unit === 'W' ||
unit === 'VA' ||
unit === 'VAr' ||
unit === 'V' ||
unit === 'A'
) {
component.suggested_display_precision = 1;
}
components[componentId] = component;
}
}
// Handle extended meter values (meter_ext.{value_name})
// These typically come from evt.meter_ext.report and include detailed power/voltage/current readings
if (
svc.intf?.includes('evt.meter_ext.report') &&
supportedExtendedValues.length > 0
) {
for (const valueName of supportedExtendedValues) {
const componentId = `${svc.addr}_ext_${valueName}`;
const deviceClass = METER_VALUE_DEVICE_CLASS_MAP[valueName];
const stateClass =
METER_VALUE_STATE_CLASS_MAP[valueName] || 'measurement';
const friendlyName =
METER_VALUE_NAMES[valueName] ||
valueName
.replace(/_/g, ' ')
.replace(/\b\w/g, (l: string) => l.toUpperCase());
// Determine unit based on value name
let unit = '';
if (
valueName.startsWith('p_') ||
valueName.startsWith('p1') ||
valueName.startsWith('p2') ||
valueName.startsWith('p3') ||
valueName === 'dc_p'
) {
unit = 'W';
} else if (
valueName.startsWith('e_') ||
valueName.startsWith('e1') ||
valueName.startsWith('e2') ||
valueName.startsWith('e3')
) {
unit = 'kWh';
} else if (
valueName.startsWith('u_') ||
valueName.startsWith('u1') ||
valueName.startsWith('u2') ||
valueName.startsWith('u3') ||
valueName === 'dc_u'
) {
unit = 'V';
} else if (
valueName.startsWith('i_') ||
valueName.startsWith('i1') ||
valueName.startsWith('i2') ||
valueName.startsWith('i3') ||
valueName === 'dc_i'
) {
unit = 'A';
} else if (valueName.includes('factor')) {
unit = '';
} else if (valueName === 'freq') {
unit = 'Hz';
}
const component: SensorComponent = {
unique_id: componentId,
platform: 'sensor',
entity_category: 'diagnostic',
name: friendlyName,
state_class: stateClass,
value_template: `{{ value_json['${svc.addr}'].meter_ext.${valueName} | default(0) }}`,
};
if (unit) {
component.unit_of_measurement = unit;
}
if (deviceClass) {
component.device_class = deviceClass;
}
// Set suggested display precision
if (unit === 'kWh') {
component.suggested_display_precision = 3;
} else if (unit === 'W' || unit === 'V' || unit === 'A') {
component.suggested_display_precision = 1;
} else if (unit === 'Hz') {
component.suggested_display_precision = 2;
} else if (valueName.includes('factor')) {
component.suggested_display_precision = 3;
}
components[componentId] = component;
}
}
// Handle meter reset button if supported
if (svc.intf?.includes('cmd.meter.reset')) {
const resetCommandTopic = `${topicPrefix}${svc.addr}/reset/command`;
components[`${svc.addr}_reset`] = {
unique_id: `${svc.addr}_reset`,
platform: 'button',
name: 'Reset Meter',
entity_category: 'config',
icon: 'mdi:restart',
command_topic: resetCommandTopic,
};
commandHandlers[resetCommandTopic] = async (_payload: string) => {
await sendFimpMsg({
address: svc.addr,
service: svcName,
cmd: 'cmd.meter.reset',
val_t: 'null',
val: null,
});
};
}
return {
components,
commandHandlers,
};
}