From 5cbca30c4585aa54ebd63ccd8ddd3cd1df13b484 Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Fri, 20 Oct 2023 14:37:40 +0300 Subject: [PATCH 1/6] Rename "watts" to "power" to allow for more values to be added Old InfluxDB points should be migrated --- src/eachwatt.ts | 2 +- src/iotawatt.ts | 2 +- src/publisher/console.ts | 2 +- src/publisher/influxdb.ts | 4 +++- src/publisher/mqtt.ts | 2 +- src/sensor.ts | 6 +++--- src/shelly.ts | 14 +++++++------- src/unmetered.ts | 2 +- src/virtual.ts | 2 +- webif/src/routes/Circuits.svelte | 2 +- webif/src/routes/MainsPower.svelte | 2 +- 11 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/eachwatt.ts b/src/eachwatt.ts index 28718a7..85fa275 100644 --- a/src/eachwatt.ts +++ b/src/eachwatt.ts @@ -45,7 +45,7 @@ const mainPollerFunc = async (config: Config) => { // Round all numbers to one decimal point for (const data of sensorData) { - data.watts = Number(data.watts.toFixed(1)) + data.power = Number(data.power.toFixed(1)) } // Publish data diff --git a/src/iotawatt.ts b/src/iotawatt.ts index 2416049..656ffbe 100644 --- a/src/iotawatt.ts +++ b/src/iotawatt.ts @@ -92,7 +92,7 @@ export const getSensorData: PowerSensorPollFunction = async ( return { timestamp: timestamp, circuit: circuit, - watts: getSensorValue(sensor, configuration, status), + power: getSensorValue(sensor, configuration, status), } } catch (e) { console.error((e as Error).message) diff --git a/src/publisher/console.ts b/src/publisher/console.ts index 9f8428d..af58b74 100644 --- a/src/publisher/console.ts +++ b/src/publisher/console.ts @@ -8,7 +8,7 @@ export interface ConsolePublisher extends Publisher { export class ConsolePublisherImpl implements PublisherImpl { publishSensorData(sensorData: PowerSensorData[]): void { for (const data of sensorData) { - console.log(`${data.timestamp}: ${data.circuit.name}: ${data.watts}W`) + console.log(`${data.timestamp}: ${data.circuit.name}: ${data.power}W`) } } diff --git a/src/publisher/influxdb.ts b/src/publisher/influxdb.ts index e2b3748..a35dfa6 100644 --- a/src/publisher/influxdb.ts +++ b/src/publisher/influxdb.ts @@ -34,7 +34,9 @@ export class InfluxDBPublisherImpl implements PublisherImpl { .tag('circuit', data.circuit.name) .tag('circuitType', data.circuit.type as CircuitType) .tag('sensorType', data.circuit.sensor.type) - .floatField('watts', data.watts) + // TODO: Remove "watts", here for backward compatibility + .floatField('watts', data.power) + .floatField('power', data.power) .timestamp(data.timestamp) // Optional tags diff --git a/src/publisher/mqtt.ts b/src/publisher/mqtt.ts index e374b23..01500fd 100644 --- a/src/publisher/mqtt.ts +++ b/src/publisher/mqtt.ts @@ -73,7 +73,7 @@ export class MqttPublisherImpl implements PublisherImpl { for (const data of sensorData) { const topicValueMap: TopicValueMap = new Map( Object.entries({ - [createPowerSensorTopicName(data.circuit, 'power')]: data.watts, + [createPowerSensorTopicName(data.circuit, 'power')]: data.power, }), ) diff --git a/src/sensor.ts b/src/sensor.ts index 7068f6b..996fdab 100644 --- a/src/sensor.ts +++ b/src/sensor.ts @@ -98,7 +98,7 @@ export interface UnmeteredSensor extends PowerSensor { export interface PowerSensorData { timestamp: number circuit: Circuit - watts: number + power: number } export type CharacteristicsSensorData = { @@ -112,7 +112,7 @@ export const emptySensorData = (timestamp: number, circuit: Circuit): PowerSenso return { timestamp, circuit, - watts: 0, + power: 0, } } @@ -129,7 +129,7 @@ export const emptyCharacteristicsSensorData = ( } export const reduceToWatts = (sensorData: PowerSensorData[]): number => { - return sensorData.reduce((acc, data) => acc + data.watts, 0) + return sensorData.reduce((acc, data) => acc + data.power, 0) } export const untangleCircularDeps = (sensorData: PowerSensorData[]): PowerSensorData[] => { diff --git a/src/shelly.ts b/src/shelly.ts index 38d37d1..41507c0 100644 --- a/src/shelly.ts +++ b/src/shelly.ts @@ -60,7 +60,7 @@ const parseGen1Response = (timestamp: number, circuit: Circuit, httpResponse: Ax return { timestamp: timestamp, circuit: circuit, - watts: data.meters[sensor.shelly.meter].power, + power: data.meters[sensor.shelly.meter].power, } } @@ -70,7 +70,7 @@ const parseGen2PMResponse = (timestamp: number, circuit: Circuit, httpResponse: return { timestamp: timestamp, circuit: circuit, - watts: data.apower, + power: data.apower, } } @@ -78,23 +78,23 @@ const parseGen2EMResponse = (timestamp: number, circuit: Circuit, httpResponse: const sensor = circuit.sensor as ShellySensor const data = httpResponse.data as Gen2EMGetStatusResult - let watts = 0 + let power = 0 switch (sensor.shelly.phase) { case 'a': - watts = data.a_act_power + power = data.a_act_power break case 'b': - watts = data.b_act_power + power = data.b_act_power break case 'c': - watts = data.c_act_power + power = data.c_act_power break } return { timestamp: timestamp, circuit: circuit, - watts: watts, + power: power, } } diff --git a/src/unmetered.ts b/src/unmetered.ts index 7799a4e..c015ca9 100644 --- a/src/unmetered.ts +++ b/src/unmetered.ts @@ -19,6 +19,6 @@ export const getSensorData: PowerSensorPollFunction = async ( return { timestamp, circuit, - watts: Math.max(parentWatts - unmeteredWatts, 0), // Don't allow negative values + power: Math.max(parentWatts - unmeteredWatts, 0), // Don't allow negative values } } diff --git a/src/virtual.ts b/src/virtual.ts index 5d03ad8..0a162f4 100644 --- a/src/virtual.ts +++ b/src/virtual.ts @@ -18,6 +18,6 @@ export const getSensorData: PowerSensorPollFunction = async ( return { timestamp: timestamp, circuit: circuit, - watts: childrenSensorData.reduce((acc, data) => acc + data.watts, 0), + power: childrenSensorData.reduce((acc, data) => acc + data.power, 0), } } diff --git a/webif/src/routes/Circuits.svelte b/webif/src/routes/Circuits.svelte index d2c62fe..c8ab40c 100644 --- a/webif/src/routes/Circuits.svelte +++ b/webif/src/routes/Circuits.svelte @@ -20,7 +20,7 @@ {data.circuit.group ?? ''} {data.circuit.type} {data.circuit.sensor.type} - {data.watts}W + {data.power}W {/each} diff --git a/webif/src/routes/MainsPower.svelte b/webif/src/routes/MainsPower.svelte index 10eca3d..533c397 100644 --- a/webif/src/routes/MainsPower.svelte +++ b/webif/src/routes/MainsPower.svelte @@ -6,7 +6,7 @@ {#each sensorData as data}
{data.circuit.name} - {data.watts}W + {data.power}W
{/each}
From 35047b1fc9be714210fbce1e84963f8fc9e9c0e4 Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Fri, 20 Oct 2023 16:01:13 +0300 Subject: [PATCH 2/6] Implement power factor for IotaWatt/Shelly Gen2 sensors and apparent power for Shelly Gen2 Web interface only shows value if present. MQTT only publishes to topic if value is present. --- src/iotawatt.ts | 38 ++++++++++++++++++++++++++---- src/publisher/mqtt.ts | 8 +++++++ src/sensor.ts | 4 ++++ src/shelly.ts | 16 +++++++++++++ webif/src/lib/format.ts | 3 +++ webif/src/routes/Circuits.svelte | 14 +++++++++++ webif/src/routes/MainsPower.svelte | 8 ++++++- 7 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 webif/src/lib/format.ts diff --git a/src/iotawatt.ts b/src/iotawatt.ts index 656ffbe..2bfc023 100644 --- a/src/iotawatt.ts +++ b/src/iotawatt.ts @@ -29,6 +29,7 @@ type IotawattConfiguration = { type IotawattStatusInput = { channel: number Watts: string + Pf?: number } type IotawattStatusOutput = { @@ -51,7 +52,12 @@ const getStatusUrl = (sensor: IotawattSensor): string => { return `http://${sensor.iotawatt.address}/status?state=&inputs=&outputs=` } -const getSensorValue = ( +const parseInputWattValue = (watts: string): number => { + // Can be " 0" or "423"... + return parseInt(watts.trim(), 10) +} + +const getSensorPowerValue = ( sensor: IotawattSensor, configuration: IotawattConfiguration, status: IotawattStatus, @@ -62,8 +68,7 @@ const getSensorValue = ( if (input.name === sensor.iotawatt.name) { const watts = status.inputs[i].Watts - // Can be " 0" or "423"... - return parseInt(watts.trim(), 10) + return parseInputWattValue(watts) } } @@ -74,7 +79,29 @@ const getSensorValue = ( } } - throw new Error(`Failed to find value for sensor ${sensor.iotawatt.name}`) + throw new Error(`Failed to find power value for sensor ${sensor.iotawatt.name}`) +} + +const getSensorPowerFactorValue = ( + sensor: IotawattSensor, + configuration: IotawattConfiguration, + status: IotawattStatus, +): number | undefined => { + // Power factor is only available for inputs + for (let i = 0; i < configuration.inputs.length; i++) { + const input = configuration.inputs[i] + if (input.name === sensor.iotawatt.name) { + // The power factor value cannot be trusted for small loads, and apparently this is done client-side on the + // IotaWatt web interface. Emulate the same logic here. + const watts = parseInputWattValue(status.inputs[i].Watts) + + return watts >= 50 ? status.inputs[i].Pf : undefined + } + } + + // Return undefined if the sensor doesn't have a corresponding input, we don't + // want to fail hard since the value is optional + return undefined } export const getSensorData: PowerSensorPollFunction = async ( @@ -92,7 +119,8 @@ export const getSensorData: PowerSensorPollFunction = async ( return { timestamp: timestamp, circuit: circuit, - power: getSensorValue(sensor, configuration, status), + power: getSensorPowerValue(sensor, configuration, status), + powerFactor: getSensorPowerFactorValue(sensor, configuration, status), } } catch (e) { console.error((e as Error).message) diff --git a/src/publisher/mqtt.ts b/src/publisher/mqtt.ts index 01500fd..ff4d9d4 100644 --- a/src/publisher/mqtt.ts +++ b/src/publisher/mqtt.ts @@ -77,6 +77,14 @@ export class MqttPublisherImpl implements PublisherImpl { }), ) + // Publish optional sensor values too when present + if (data.apparentPower) { + topicValueMap.set(createPowerSensorTopicName(data.circuit, 'apparentPower'), data.apparentPower) + } + if (data.powerFactor) { + topicValueMap.set(createPowerSensorTopicName(data.circuit, 'powerFactor'), data.powerFactor) + } + await this.publishTopicValues(topicValueMap) } diff --git a/src/sensor.ts b/src/sensor.ts index 996fdab..54a114c 100644 --- a/src/sensor.ts +++ b/src/sensor.ts @@ -98,7 +98,11 @@ export interface UnmeteredSensor extends PowerSensor { export interface PowerSensorData { timestamp: number circuit: Circuit + // Mandatory data power: number + // Optional data, not all sensor types support them + apparentPower?: number + powerFactor?: number } export type CharacteristicsSensorData = { diff --git a/src/shelly.ts b/src/shelly.ts index 41507c0..4925ac9 100644 --- a/src/shelly.ts +++ b/src/shelly.ts @@ -28,12 +28,18 @@ type Gen2SwitchGetStatusResult = { type Gen2EMGetStatusResult = { a_act_power: number + a_aprt_power: number + a_pf: number a_voltage: number a_freq: number b_act_power: number + b_aprt_power: number + b_pf: number b_voltage: number b_freq: number c_act_power: number + c_aprt_power: number + c_pf: number c_voltage: number c_freq: number } @@ -79,15 +85,23 @@ const parseGen2EMResponse = (timestamp: number, circuit: Circuit, httpResponse: const data = httpResponse.data as Gen2EMGetStatusResult let power = 0 + let apparentPower = 0 + let powerFactor = 0 switch (sensor.shelly.phase) { case 'a': power = data.a_act_power + apparentPower = data.a_aprt_power + powerFactor = data.a_pf break case 'b': power = data.b_act_power + apparentPower = data.b_aprt_power + powerFactor = data.b_pf break case 'c': power = data.c_act_power + apparentPower = data.b_aprt_power + powerFactor = data.b_pf break } @@ -95,6 +109,8 @@ const parseGen2EMResponse = (timestamp: number, circuit: Circuit, httpResponse: timestamp: timestamp, circuit: circuit, power: power, + apparentPower: apparentPower, + powerFactor: powerFactor, } } diff --git a/webif/src/lib/format.ts b/webif/src/lib/format.ts new file mode 100644 index 0000000..29f844c --- /dev/null +++ b/webif/src/lib/format.ts @@ -0,0 +1,3 @@ +export const formatPf = (pf) => { + return Number(pf).toFixed(2) +} diff --git a/webif/src/routes/Circuits.svelte b/webif/src/routes/Circuits.svelte index c8ab40c..1581a88 100644 --- a/webif/src/routes/Circuits.svelte +++ b/webif/src/routes/Circuits.svelte @@ -1,4 +1,6 @@ @@ -11,6 +13,8 @@ Circuit type Sensor type Power + Apparent power + Power factor @@ -21,6 +25,16 @@ {data.circuit.type} {data.circuit.sensor.type} {data.power}W + + {#if data.apparentPower } + {data.apparentPower}VA + {/if} + + + {#if data.powerFactor } + {formatPf(data.powerFactor)} + {/if} + {/each} diff --git a/webif/src/routes/MainsPower.svelte b/webif/src/routes/MainsPower.svelte index 533c397..0d30d3c 100644 --- a/webif/src/routes/MainsPower.svelte +++ b/webif/src/routes/MainsPower.svelte @@ -5,8 +5,14 @@
{#each sensorData as data}
- {data.circuit.name} + {data.circuit.name} {data.power}W + {#if data.apparentPower } + {data.apparentPower}VA + {/if} + {#if data.powerFactor } + pf {data.powerFactor} + {/if}
{/each}
From 799782792ce0d1a5c4b4d5415862ae8f6b3a9bdf Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Fri, 20 Oct 2023 20:04:51 +0300 Subject: [PATCH 3/6] Publish Home Assistant sensors for apparent power and power factor too We have to scrap the configurable "device identifier" since the topic prefix etc. is already hardcoded. Gotta keep things simple. --- src/publisher/mqtt/homeassistant.ts | 52 ++++++++++++++++++++++++++--- src/sensor.ts | 14 ++++++++ 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/publisher/mqtt/homeassistant.ts b/src/publisher/mqtt/homeassistant.ts index cee28bc..ae56d9f 100644 --- a/src/publisher/mqtt/homeassistant.ts +++ b/src/publisher/mqtt/homeassistant.ts @@ -2,6 +2,7 @@ import { MqttClient } from 'mqtt' import { Config } from '../../config' import { TOPIC_NAME_STATUS } from '../mqtt' import { createPowerSensorTopicName, slugifyName } from './util' +import { supportsApparentPower, supportsPowerFactor } from '../../sensor' export const configureMqttDiscovery = async ( config: Config, @@ -19,18 +20,19 @@ export const configureMqttDiscovery = async ( 'platform': 'mqtt', 'availability_topic': TOPIC_NAME_STATUS, 'device': mqttDeviceInformation, + 'state_class': 'measurement', } const promises = [] for (const circuit of config.circuits) { - // Add power sensors const entityName = slugifyName(circuit.name) - const uniqueId = `${deviceIdentifier}_${entityName}_power` + // Add power sensors + const uniqueId = `${deviceIdentifier}_${entityName}_power` + const configurationTopicName = `homeassistant/sensor/${uniqueId}/config` const configuration = { ...configurationBase, - 'state_class': 'measurement', 'device_class': 'power', 'unit_of_measurement': 'W', 'name': `${circuit.name} power`, @@ -40,12 +42,54 @@ export const configureMqttDiscovery = async ( } // "retain" is used so that the entities will be available immediately after a Home Assistant restart - const configurationTopicName = `homeassistant/sensor/eachwatt/${entityName}/config` promises.push( mqttClient.publishAsync(configurationTopicName, JSON.stringify(configuration), { retain: true, }), ) + + // Add apparent power sensors + if (supportsApparentPower(circuit.sensor)) { + const uniqueId = `${deviceIdentifier}_${entityName}_apparentPower` + const configurationTopicName = `homeassistant/sensor/${uniqueId}/config` + const configuration = { + ...configurationBase, + 'device_class': 'apparent_power', + 'unit_of_measurement': 'VA', + 'name': `${circuit.name} apparent power`, + 'unique_id': uniqueId, + 'object_id': uniqueId, + 'state_topic': createPowerSensorTopicName(circuit, 'apparentPower'), + } + + // "retain" is used so that the entities will be available immediately after a Home Assistant restart + promises.push( + mqttClient.publishAsync(configurationTopicName, JSON.stringify(configuration), { + retain: true, + }), + ) + } + + // Add power factor sensors + if (supportsPowerFactor(circuit.sensor)) { + const uniqueId = `${deviceIdentifier}_${entityName}_powerFactor` + const configurationTopicName = `homeassistant/sensor/${uniqueId}/config` + const configuration = { + ...configurationBase, + 'device_class': 'power_factor', + 'name': `${circuit.name} power factor`, + 'unique_id': uniqueId, + 'object_id': uniqueId, + 'state_topic': createPowerSensorTopicName(circuit, 'powerFactor'), + } + + // "retain" is used so that the entities will be available immediately after a Home Assistant restart + promises.push( + mqttClient.publishAsync(configurationTopicName, JSON.stringify(configuration), { + retain: true, + }), + ) + } } await Promise.all(promises) diff --git a/src/sensor.ts b/src/sensor.ts index 54a114c..e8bf87d 100644 --- a/src/sensor.ts +++ b/src/sensor.ts @@ -142,3 +142,17 @@ export const untangleCircularDeps = (sensorData: PowerSensorData[]): PowerSensor return d }) } + +const isShellyGen2EMSensor = (sensor: PowerSensor): boolean => { + return sensor.type === SensorType.Shelly && (sensor as ShellySensor).shelly.type === ShellyType.Gen2EM +} + +export const supportsApparentPower = (sensor: PowerSensor): boolean => { + // Only EM devices supports this + return isShellyGen2EMSensor(sensor) +} + +export const supportsPowerFactor = (sensor: PowerSensor): boolean => { + // IotaWatt sensors support power factor too, but only for inputs, not outputs + return isShellyGen2EMSensor(sensor) +} From 1c2692ea446e4adb1650f69a1db67e70e28187ce Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Fri, 20 Oct 2023 20:07:06 +0300 Subject: [PATCH 4/6] Scrap the "deviceIdentifier" concept --- src/publisher/mqtt.ts | 7 +------ src/publisher/mqtt/homeassistant.ts | 16 ++++++---------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/publisher/mqtt.ts b/src/publisher/mqtt.ts index ff4d9d4..b456f5c 100644 --- a/src/publisher/mqtt.ts +++ b/src/publisher/mqtt.ts @@ -12,7 +12,6 @@ export type MqttPublisherSettings = { brokerUrl: string homeAssistant?: { autoDiscovery: boolean - deviceIdentifier?: string } } @@ -40,7 +39,7 @@ export class MqttPublisherImpl implements PublisherImpl { // Publish Home Assistant MQTT discovery messages if (this.settings.homeAssistant?.autoDiscovery) { - configureMqttDiscovery(this.config, this.getHomeAssistantDeviceIdentifier(), this.mqttClient) + configureMqttDiscovery(this.config, this.mqttClient) .then(() => { console.log(`Configured Home Assistant MQTT discovery`) }) @@ -108,8 +107,4 @@ export class MqttPublisherImpl implements PublisherImpl { // noinspection TypeScriptValidateTypes await this.mqttClient?.publishAsync(TOPIC_NAME_STATUS, 'online') } - - private getHomeAssistantDeviceIdentifier = (): string => { - return this.settings.homeAssistant?.deviceIdentifier ?? 'eachwatt' - } } diff --git a/src/publisher/mqtt/homeassistant.ts b/src/publisher/mqtt/homeassistant.ts index ae56d9f..e16fa13 100644 --- a/src/publisher/mqtt/homeassistant.ts +++ b/src/publisher/mqtt/homeassistant.ts @@ -4,16 +4,12 @@ import { TOPIC_NAME_STATUS } from '../mqtt' import { createPowerSensorTopicName, slugifyName } from './util' import { supportsApparentPower, supportsPowerFactor } from '../../sensor' -export const configureMqttDiscovery = async ( - config: Config, - deviceIdentifier: string, - mqttClient: MqttClient, -): Promise => { +export const configureMqttDiscovery = async (config: Config, mqttClient: MqttClient): Promise => { // The "device" object that is part of each sensor's configuration payload const mqttDeviceInformation = { - 'name': deviceIdentifier, + 'name': 'eachwatt', 'model': 'Eachwatt', - 'identifiers': deviceIdentifier, + 'identifiers': 'eachwatt', } const configurationBase = { @@ -29,7 +25,7 @@ export const configureMqttDiscovery = async ( const entityName = slugifyName(circuit.name) // Add power sensors - const uniqueId = `${deviceIdentifier}_${entityName}_power` + const uniqueId = `eachwatt_${entityName}_power` const configurationTopicName = `homeassistant/sensor/${uniqueId}/config` const configuration = { ...configurationBase, @@ -50,7 +46,7 @@ export const configureMqttDiscovery = async ( // Add apparent power sensors if (supportsApparentPower(circuit.sensor)) { - const uniqueId = `${deviceIdentifier}_${entityName}_apparentPower` + const uniqueId = `eachwatt_${entityName}_apparentPower` const configurationTopicName = `homeassistant/sensor/${uniqueId}/config` const configuration = { ...configurationBase, @@ -72,7 +68,7 @@ export const configureMqttDiscovery = async ( // Add power factor sensors if (supportsPowerFactor(circuit.sensor)) { - const uniqueId = `${deviceIdentifier}_${entityName}_powerFactor` + const uniqueId = `eachwatt_${entityName}_powerFactor` const configurationTopicName = `homeassistant/sensor/${uniqueId}/config` const configuration = { ...configurationBase, From 8adf23721c92e1fba8e1de418ea8d8aeb77cbda9 Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Fri, 20 Oct 2023 20:22:44 +0300 Subject: [PATCH 5/6] Refactor Home Assistant configuration publishing to be more modular --- src/publisher/mqtt/homeassistant.ts | 143 ++++++++++++++-------------- 1 file changed, 74 insertions(+), 69 deletions(-) diff --git a/src/publisher/mqtt/homeassistant.ts b/src/publisher/mqtt/homeassistant.ts index e16fa13..62ea73a 100644 --- a/src/publisher/mqtt/homeassistant.ts +++ b/src/publisher/mqtt/homeassistant.ts @@ -3,90 +3,95 @@ import { Config } from '../../config' import { TOPIC_NAME_STATUS } from '../mqtt' import { createPowerSensorTopicName, slugifyName } from './util' import { supportsApparentPower, supportsPowerFactor } from '../../sensor' +import { Circuit } from '../../circuit' export const configureMqttDiscovery = async (config: Config, mqttClient: MqttClient): Promise => { - // The "device" object that is part of each sensor's configuration payload - const mqttDeviceInformation = { - 'name': 'eachwatt', - 'model': 'Eachwatt', - 'identifiers': 'eachwatt', - } - - const configurationBase = { - 'platform': 'mqtt', - 'availability_topic': TOPIC_NAME_STATUS, - 'device': mqttDeviceInformation, - 'state_class': 'measurement', - } - const promises = [] for (const circuit of config.circuits) { - const entityName = slugifyName(circuit.name) - // Add power sensors - const uniqueId = `eachwatt_${entityName}_power` - const configurationTopicName = `homeassistant/sensor/${uniqueId}/config` - const configuration = { - ...configurationBase, - 'device_class': 'power', - 'unit_of_measurement': 'W', - 'name': `${circuit.name} power`, - 'unique_id': uniqueId, - 'object_id': uniqueId, - 'state_topic': createPowerSensorTopicName(circuit, 'power'), - } - - // "retain" is used so that the entities will be available immediately after a Home Assistant restart - promises.push( - mqttClient.publishAsync(configurationTopicName, JSON.stringify(configuration), { - retain: true, - }), - ) + promises.push(publishPowerSensorConfiguration(mqttClient, circuit)) // Add apparent power sensors if (supportsApparentPower(circuit.sensor)) { - const uniqueId = `eachwatt_${entityName}_apparentPower` - const configurationTopicName = `homeassistant/sensor/${uniqueId}/config` - const configuration = { - ...configurationBase, - 'device_class': 'apparent_power', - 'unit_of_measurement': 'VA', - 'name': `${circuit.name} apparent power`, - 'unique_id': uniqueId, - 'object_id': uniqueId, - 'state_topic': createPowerSensorTopicName(circuit, 'apparentPower'), - } - - // "retain" is used so that the entities will be available immediately after a Home Assistant restart - promises.push( - mqttClient.publishAsync(configurationTopicName, JSON.stringify(configuration), { - retain: true, - }), - ) + promises.push(publishApparentPowerSensorConfiguration(mqttClient, circuit)) } // Add power factor sensors if (supportsPowerFactor(circuit.sensor)) { - const uniqueId = `eachwatt_${entityName}_powerFactor` - const configurationTopicName = `homeassistant/sensor/${uniqueId}/config` - const configuration = { - ...configurationBase, - 'device_class': 'power_factor', - 'name': `${circuit.name} power factor`, - 'unique_id': uniqueId, - 'object_id': uniqueId, - 'state_topic': createPowerSensorTopicName(circuit, 'powerFactor'), - } - - // "retain" is used so that the entities will be available immediately after a Home Assistant restart - promises.push( - mqttClient.publishAsync(configurationTopicName, JSON.stringify(configuration), { - retain: true, - }), - ) + promises.push(publishPowerFactorSensorConfiguration(mqttClient, circuit)) } } await Promise.all(promises) } + +const getConfigurationBase = (): object => { + return { + 'platform': 'mqtt', + 'availability_topic': TOPIC_NAME_STATUS, + 'state_class': 'measurement', + 'device': { + 'name': 'eachwatt', + 'model': 'Eachwatt', + 'identifiers': 'eachwatt', + }, + } +} + +const publishPowerSensorConfiguration = async (mqttClient: MqttClient, circuit: Circuit) => { + const entityName = slugifyName(circuit.name) + const uniqueId = `eachwatt_${entityName}_power` + const configurationTopicName = `homeassistant/sensor/${uniqueId}/config` + const configuration = { + ...getConfigurationBase(), + 'device_class': 'power', + 'unit_of_measurement': 'W', + 'name': `${circuit.name} power`, + 'unique_id': uniqueId, + 'object_id': uniqueId, + 'state_topic': createPowerSensorTopicName(circuit, 'power'), + } + + return mqttClient.publishAsync(configurationTopicName, JSON.stringify(configuration), { + retain: true, + }) +} + +const publishApparentPowerSensorConfiguration = async (mqttClient: MqttClient, circuit: Circuit) => { + const entityName = slugifyName(circuit.name) + const uniqueId = `eachwatt_${entityName}_apparentPower` + const configurationTopicName = `homeassistant/sensor/${uniqueId}/config` + const configuration = { + ...getConfigurationBase(), + 'device_class': 'apparent_power', + 'unit_of_measurement': 'VA', + 'name': `${circuit.name} apparent power`, + 'unique_id': uniqueId, + 'object_id': uniqueId, + 'state_topic': createPowerSensorTopicName(circuit, 'apparentPower'), + } + + return mqttClient.publishAsync(configurationTopicName, JSON.stringify(configuration), { + retain: true, + }) +} + +const publishPowerFactorSensorConfiguration = async (mqttClient: MqttClient, circuit: Circuit) => { + const entityName = slugifyName(circuit.name) + const uniqueId = `eachwatt_${entityName}_powerFactor` + const configurationTopicName = `homeassistant/sensor/${uniqueId}/config` + const configuration = { + ...getConfigurationBase(), + 'device_class': 'power_factor', + 'name': `${circuit.name} power factor`, + 'unique_id': uniqueId, + 'object_id': uniqueId, + 'state_topic': createPowerSensorTopicName(circuit, 'powerFactor'), + } + + // "retain" is used so that the entities will be available immediately after a Home Assistant restart + return mqttClient.publishAsync(configurationTopicName, JSON.stringify(configuration), { + retain: true, + }) +} From 47424472643825a92a02c98326ebbf500dd78c30 Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Fri, 20 Oct 2023 20:35:37 +0300 Subject: [PATCH 6/6] Don't publish empty sensor data, use undefined instead of 0 Fixes #27 --- src/eachwatt.ts | 4 +++- src/publisher/influxdb.ts | 5 +++++ src/publisher/mqtt.ts | 32 +++++++++++++++++--------------- src/sensor.ts | 14 ++++++-------- src/virtual.ts | 2 +- 5 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/eachwatt.ts b/src/eachwatt.ts index 85fa275..8a50992 100644 --- a/src/eachwatt.ts +++ b/src/eachwatt.ts @@ -45,7 +45,9 @@ const mainPollerFunc = async (config: Config) => { // Round all numbers to one decimal point for (const data of sensorData) { - data.power = Number(data.power.toFixed(1)) + if (data.power !== undefined) { + data.power = Number(data.power.toFixed(1)) + } } // Publish data diff --git a/src/publisher/influxdb.ts b/src/publisher/influxdb.ts index a35dfa6..0ba7599 100644 --- a/src/publisher/influxdb.ts +++ b/src/publisher/influxdb.ts @@ -30,6 +30,11 @@ export class InfluxDBPublisherImpl implements PublisherImpl { async publishSensorData(sensorData: PowerSensorData[]): Promise { for (const data of sensorData) { + // Skip completely if sensor data is missing + if (data.power === undefined) { + continue + } + const power = new Point('power') .tag('circuit', data.circuit.name) .tag('circuitType', data.circuit.type as CircuitType) diff --git a/src/publisher/mqtt.ts b/src/publisher/mqtt.ts index b456f5c..4e92aee 100644 --- a/src/publisher/mqtt.ts +++ b/src/publisher/mqtt.ts @@ -55,12 +55,15 @@ export class MqttPublisherImpl implements PublisherImpl { async publishCharacteristicsSensorData(sensorData: CharacteristicsSensorData[]): Promise { for (const data of sensorData) { - const topicValueMap: TopicValueMap = new Map( - Object.entries({ - [createCharacteristicsSensorTopicName(data.characteristics, 'voltage')]: data.voltage, - [createCharacteristicsSensorTopicName(data.characteristics, 'frequency')]: data.frequency, - }), - ) + const topicValueMap: TopicValueMap = new Map() + + // Only publish when we have data + if (data.voltage !== undefined) { + topicValueMap.set(createCharacteristicsSensorTopicName(data.characteristics, 'voltage'), data.voltage) + } + if (data.frequency !== undefined) { + topicValueMap.set(createCharacteristicsSensorTopicName(data.characteristics, 'frequency'), data.frequency) + } await this.publishTopicValues(topicValueMap) } @@ -70,17 +73,16 @@ export class MqttPublisherImpl implements PublisherImpl { async publishSensorData(sensorData: PowerSensorData[]): Promise { for (const data of sensorData) { - const topicValueMap: TopicValueMap = new Map( - Object.entries({ - [createPowerSensorTopicName(data.circuit, 'power')]: data.power, - }), - ) - - // Publish optional sensor values too when present - if (data.apparentPower) { + const topicValueMap: TopicValueMap = new Map() + + // Only publish when we have data + if (data.power !== undefined) { + topicValueMap.set(createPowerSensorTopicName(data.circuit, 'power'), data.power) + } + if (data.apparentPower !== undefined) { topicValueMap.set(createPowerSensorTopicName(data.circuit, 'apparentPower'), data.apparentPower) } - if (data.powerFactor) { + if (data.powerFactor !== undefined) { topicValueMap.set(createPowerSensorTopicName(data.circuit, 'powerFactor'), data.powerFactor) } diff --git a/src/sensor.ts b/src/sensor.ts index e8bf87d..d02e946 100644 --- a/src/sensor.ts +++ b/src/sensor.ts @@ -98,8 +98,8 @@ export interface UnmeteredSensor extends PowerSensor { export interface PowerSensorData { timestamp: number circuit: Circuit - // Mandatory data - power: number + // Mandatory data. Undefined means the data was not available. + power?: number // Optional data, not all sensor types support them apparentPower?: number powerFactor?: number @@ -108,15 +108,15 @@ export interface PowerSensorData { export type CharacteristicsSensorData = { timestamp: number characteristics: Characteristics - voltage: number - frequency: number + // Mandatory data. Undefined means the data was not available. + voltage?: number + frequency?: number } export const emptySensorData = (timestamp: number, circuit: Circuit): PowerSensorData => { return { timestamp, circuit, - power: 0, } } @@ -127,13 +127,11 @@ export const emptyCharacteristicsSensorData = ( return { timestamp: timestamp, characteristics: characteristics, - voltage: 0, - frequency: 0, } } export const reduceToWatts = (sensorData: PowerSensorData[]): number => { - return sensorData.reduce((acc, data) => acc + data.power, 0) + return sensorData.reduce((acc, data) => acc + (data.power ?? 0), 0) } export const untangleCircularDeps = (sensorData: PowerSensorData[]): PowerSensorData[] => { diff --git a/src/virtual.ts b/src/virtual.ts index 0a162f4..0d925a2 100644 --- a/src/virtual.ts +++ b/src/virtual.ts @@ -18,6 +18,6 @@ export const getSensorData: PowerSensorPollFunction = async ( return { timestamp: timestamp, circuit: circuit, - power: childrenSensorData.reduce((acc, data) => acc + data.power, 0), + power: childrenSensorData.reduce((acc, data) => acc + (data.power ?? 0), 0), } }