Normalize energy statistics to kWh (#52238)
This commit is contained in:
parent
b77f2b9e12
commit
c785db4ffa
2 changed files with 75 additions and 14 deletions
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import itertools
|
import itertools
|
||||||
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.recorder import history, statistics
|
from homeassistant.components.recorder import history, statistics
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
|
@ -15,12 +16,19 @@ from homeassistant.components.sensor import (
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
STATE_CLASS_MEASUREMENT,
|
STATE_CLASS_MEASUREMENT,
|
||||||
)
|
)
|
||||||
from homeassistant.const import ATTR_DEVICE_CLASS
|
from homeassistant.const import (
|
||||||
|
ATTR_DEVICE_CLASS,
|
||||||
|
ATTR_UNIT_OF_MEASUREMENT,
|
||||||
|
ENERGY_KILO_WATT_HOUR,
|
||||||
|
ENERGY_WATT_HOUR,
|
||||||
|
)
|
||||||
from homeassistant.core import HomeAssistant, State
|
from homeassistant.core import HomeAssistant, State
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
from . import DOMAIN
|
from . import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEVICE_CLASS_STATISTICS = {
|
DEVICE_CLASS_STATISTICS = {
|
||||||
DEVICE_CLASS_BATTERY: {"mean", "min", "max"},
|
DEVICE_CLASS_BATTERY: {"mean", "min", "max"},
|
||||||
DEVICE_CLASS_ENERGY: {"sum"},
|
DEVICE_CLASS_ENERGY: {"sum"},
|
||||||
|
@ -30,6 +38,15 @@ DEVICE_CLASS_STATISTICS = {
|
||||||
DEVICE_CLASS_MONETARY: {"sum"},
|
DEVICE_CLASS_MONETARY: {"sum"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UNIT_CONVERSIONS = {
|
||||||
|
DEVICE_CLASS_ENERGY: {
|
||||||
|
ENERGY_KILO_WATT_HOUR: lambda x: x,
|
||||||
|
ENERGY_WATT_HOUR: lambda x: x / 1000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WARN_UNSUPPORTED_UNIT = set()
|
||||||
|
|
||||||
|
|
||||||
def _get_entities(hass: HomeAssistant) -> list[tuple[str, str]]:
|
def _get_entities(hass: HomeAssistant) -> list[tuple[str, str]]:
|
||||||
"""Get (entity_id, device_class) of all sensors for which to compile statistics."""
|
"""Get (entity_id, device_class) of all sensors for which to compile statistics."""
|
||||||
|
@ -92,6 +109,36 @@ def _time_weighted_average(
|
||||||
return accumulated / (end - start).total_seconds()
|
return accumulated / (end - start).total_seconds()
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_states(
|
||||||
|
entity_history: list[State], device_class: str, entity_id: str
|
||||||
|
) -> list[tuple[float, State]]:
|
||||||
|
"""Normalize units."""
|
||||||
|
|
||||||
|
if device_class not in UNIT_CONVERSIONS:
|
||||||
|
# We're not normalizing this device class, return the state as they are
|
||||||
|
return [(float(el.state), el) for el in entity_history if _is_number(el.state)]
|
||||||
|
|
||||||
|
fstates = []
|
||||||
|
|
||||||
|
for state in entity_history:
|
||||||
|
# Exclude non numerical states from statistics
|
||||||
|
if not _is_number(state.state):
|
||||||
|
continue
|
||||||
|
|
||||||
|
fstate = float(state.state)
|
||||||
|
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||||
|
# Exclude unsupported units from statistics
|
||||||
|
if unit not in UNIT_CONVERSIONS[device_class]:
|
||||||
|
if entity_id not in WARN_UNSUPPORTED_UNIT:
|
||||||
|
WARN_UNSUPPORTED_UNIT.add(entity_id)
|
||||||
|
_LOGGER.warning("%s has unknown unit %s", entity_id, unit)
|
||||||
|
continue
|
||||||
|
|
||||||
|
fstates.append((UNIT_CONVERSIONS[device_class][unit](fstate), state)) # type: ignore
|
||||||
|
|
||||||
|
return fstates
|
||||||
|
|
||||||
|
|
||||||
def compile_statistics(
|
def compile_statistics(
|
||||||
hass: HomeAssistant, start: datetime.datetime, end: datetime.datetime
|
hass: HomeAssistant, start: datetime.datetime, end: datetime.datetime
|
||||||
) -> dict:
|
) -> dict:
|
||||||
|
@ -115,9 +162,7 @@ def compile_statistics(
|
||||||
continue
|
continue
|
||||||
|
|
||||||
entity_history = history_list[entity_id]
|
entity_history = history_list[entity_id]
|
||||||
fstates = [
|
fstates = _normalize_states(entity_history, device_class, entity_id)
|
||||||
(float(el.state), el) for el in entity_history if _is_number(el.state)
|
|
||||||
]
|
|
||||||
|
|
||||||
if not fstates:
|
if not fstates:
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -49,7 +49,11 @@ def test_compile_hourly_energy_statistics(hass_recorder):
|
||||||
hass = hass_recorder()
|
hass = hass_recorder()
|
||||||
recorder = hass.data[DATA_INSTANCE]
|
recorder = hass.data[DATA_INSTANCE]
|
||||||
setup_component(hass, "sensor", {})
|
setup_component(hass, "sensor", {})
|
||||||
sns1_attr = {"device_class": "energy", "state_class": "measurement"}
|
sns1_attr = {
|
||||||
|
"device_class": "energy",
|
||||||
|
"state_class": "measurement",
|
||||||
|
"unit_of_measurement": "kWh",
|
||||||
|
}
|
||||||
sns2_attr = {"device_class": "energy"}
|
sns2_attr = {"device_class": "energy"}
|
||||||
sns3_attr = {}
|
sns3_attr = {}
|
||||||
|
|
||||||
|
@ -109,9 +113,21 @@ def test_compile_hourly_energy_statistics2(hass_recorder):
|
||||||
hass = hass_recorder()
|
hass = hass_recorder()
|
||||||
recorder = hass.data[DATA_INSTANCE]
|
recorder = hass.data[DATA_INSTANCE]
|
||||||
setup_component(hass, "sensor", {})
|
setup_component(hass, "sensor", {})
|
||||||
sns1_attr = {"device_class": "energy", "state_class": "measurement"}
|
sns1_attr = {
|
||||||
sns2_attr = {"device_class": "energy", "state_class": "measurement"}
|
"device_class": "energy",
|
||||||
sns3_attr = {"device_class": "energy", "state_class": "measurement"}
|
"state_class": "measurement",
|
||||||
|
"unit_of_measurement": "kWh",
|
||||||
|
}
|
||||||
|
sns2_attr = {
|
||||||
|
"device_class": "energy",
|
||||||
|
"state_class": "measurement",
|
||||||
|
"unit_of_measurement": "kWh",
|
||||||
|
}
|
||||||
|
sns3_attr = {
|
||||||
|
"device_class": "energy",
|
||||||
|
"state_class": "measurement",
|
||||||
|
"unit_of_measurement": "Wh",
|
||||||
|
}
|
||||||
|
|
||||||
zero, four, eight, states = record_energy_states(
|
zero, four, eight, states = record_energy_states(
|
||||||
hass, sns1_attr, sns2_attr, sns3_attr
|
hass, sns1_attr, sns2_attr, sns3_attr
|
||||||
|
@ -201,8 +217,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder):
|
||||||
"mean": None,
|
"mean": None,
|
||||||
"min": None,
|
"min": None,
|
||||||
"last_reset": process_timestamp_to_utc_isoformat(zero),
|
"last_reset": process_timestamp_to_utc_isoformat(zero),
|
||||||
"state": approx(5.0),
|
"state": approx(5.0 / 1000),
|
||||||
"sum": approx(5.0),
|
"sum": approx(5.0 / 1000),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"statistic_id": "sensor.test3",
|
"statistic_id": "sensor.test3",
|
||||||
|
@ -211,8 +227,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder):
|
||||||
"mean": None,
|
"mean": None,
|
||||||
"min": None,
|
"min": None,
|
||||||
"last_reset": process_timestamp_to_utc_isoformat(four),
|
"last_reset": process_timestamp_to_utc_isoformat(four),
|
||||||
"state": approx(50.0),
|
"state": approx(50.0 / 1000),
|
||||||
"sum": approx(30.0),
|
"sum": approx(30.0 / 1000),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"statistic_id": "sensor.test3",
|
"statistic_id": "sensor.test3",
|
||||||
|
@ -221,8 +237,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder):
|
||||||
"mean": None,
|
"mean": None,
|
||||||
"min": None,
|
"min": None,
|
||||||
"last_reset": process_timestamp_to_utc_isoformat(four),
|
"last_reset": process_timestamp_to_utc_isoformat(four),
|
||||||
"state": approx(90.0),
|
"state": approx(90.0 / 1000),
|
||||||
"sum": approx(70.0),
|
"sum": approx(70.0 / 1000),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue