2021-05-16 17:23:37 +00:00
|
|
|
"""Statistics helper for sensor."""
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
import datetime
|
2021-05-20 11:05:15 +00:00
|
|
|
import itertools
|
2021-06-29 10:20:10 +00:00
|
|
|
import logging
|
2021-09-08 15:05:16 +00:00
|
|
|
import math
|
2021-06-30 12:17:58 +00:00
|
|
|
from typing import Callable
|
2021-05-16 17:23:37 +00:00
|
|
|
|
2021-05-20 11:05:15 +00:00
|
|
|
from homeassistant.components.recorder import history, statistics
|
2021-05-20 16:23:00 +00:00
|
|
|
from homeassistant.components.sensor import (
|
|
|
|
ATTR_STATE_CLASS,
|
|
|
|
DEVICE_CLASS_ENERGY,
|
2021-08-11 16:58:19 +00:00
|
|
|
DEVICE_CLASS_GAS,
|
2021-06-23 13:32:25 +00:00
|
|
|
DEVICE_CLASS_MONETARY,
|
2021-05-21 08:48:11 +00:00
|
|
|
DEVICE_CLASS_PRESSURE,
|
2021-05-20 16:23:00 +00:00
|
|
|
DEVICE_CLASS_TEMPERATURE,
|
|
|
|
STATE_CLASS_MEASUREMENT,
|
2021-09-06 16:28:58 +00:00
|
|
|
STATE_CLASS_TOTAL,
|
2021-08-13 10:35:23 +00:00
|
|
|
STATE_CLASS_TOTAL_INCREASING,
|
|
|
|
STATE_CLASSES,
|
2021-05-20 16:23:00 +00:00
|
|
|
)
|
2021-06-29 10:20:10 +00:00
|
|
|
from homeassistant.const import (
|
|
|
|
ATTR_DEVICE_CLASS,
|
|
|
|
ATTR_UNIT_OF_MEASUREMENT,
|
2021-06-29 12:48:08 +00:00
|
|
|
DEVICE_CLASS_POWER,
|
2021-06-29 10:20:10 +00:00
|
|
|
ENERGY_KILO_WATT_HOUR,
|
|
|
|
ENERGY_WATT_HOUR,
|
2021-06-29 12:48:08 +00:00
|
|
|
POWER_KILO_WATT,
|
|
|
|
POWER_WATT,
|
2021-06-29 21:30:13 +00:00
|
|
|
PRESSURE_BAR,
|
|
|
|
PRESSURE_HPA,
|
|
|
|
PRESSURE_INHG,
|
|
|
|
PRESSURE_MBAR,
|
|
|
|
PRESSURE_PA,
|
|
|
|
PRESSURE_PSI,
|
2021-06-30 11:32:17 +00:00
|
|
|
TEMP_CELSIUS,
|
2021-06-30 12:17:58 +00:00
|
|
|
TEMP_FAHRENHEIT,
|
|
|
|
TEMP_KELVIN,
|
2021-08-11 16:58:19 +00:00
|
|
|
VOLUME_CUBIC_FEET,
|
|
|
|
VOLUME_CUBIC_METERS,
|
2021-06-29 10:20:10 +00:00
|
|
|
)
|
2021-05-28 11:16:52 +00:00
|
|
|
from homeassistant.core import HomeAssistant, State
|
2021-08-25 11:01:55 +00:00
|
|
|
from homeassistant.helpers.entity import entity_sources
|
2021-08-24 15:02:34 +00:00
|
|
|
import homeassistant.util.dt as dt_util
|
2021-06-29 21:30:13 +00:00
|
|
|
import homeassistant.util.pressure as pressure_util
|
2021-06-30 12:17:58 +00:00
|
|
|
import homeassistant.util.temperature as temperature_util
|
2021-08-11 16:58:19 +00:00
|
|
|
import homeassistant.util.volume as volume_util
|
2021-05-16 17:23:37 +00:00
|
|
|
|
2021-07-14 09:54:55 +00:00
|
|
|
from . import ATTR_LAST_RESET, DOMAIN
|
2021-05-16 17:23:37 +00:00
|
|
|
|
2021-06-29 10:20:10 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2021-08-25 11:00:35 +00:00
|
|
|
DEVICE_CLASS_STATISTICS: dict[str, dict[str, set[str]]] = {
|
2021-08-13 10:35:23 +00:00
|
|
|
STATE_CLASS_MEASUREMENT: {
|
2021-08-18 08:03:27 +00:00
|
|
|
# Deprecated, support will be removed in Home Assistant 2021.11
|
2021-08-13 10:35:23 +00:00
|
|
|
DEVICE_CLASS_ENERGY: {"sum"},
|
|
|
|
DEVICE_CLASS_GAS: {"sum"},
|
|
|
|
DEVICE_CLASS_MONETARY: {"sum"},
|
|
|
|
},
|
2021-09-06 16:28:58 +00:00
|
|
|
STATE_CLASS_TOTAL: {},
|
2021-08-25 11:00:35 +00:00
|
|
|
STATE_CLASS_TOTAL_INCREASING: {},
|
|
|
|
}
|
|
|
|
DEFAULT_STATISTICS = {
|
|
|
|
STATE_CLASS_MEASUREMENT: {"mean", "min", "max"},
|
2021-09-06 16:28:58 +00:00
|
|
|
STATE_CLASS_TOTAL: {"sum"},
|
2021-08-25 11:00:35 +00:00
|
|
|
STATE_CLASS_TOTAL_INCREASING: {"sum"},
|
2021-05-20 16:23:00 +00:00
|
|
|
}
|
2021-05-16 17:23:37 +00:00
|
|
|
|
2021-07-02 07:51:47 +00:00
|
|
|
# Normalized units which will be stored in the statistics table
|
2021-06-30 11:32:17 +00:00
|
|
|
DEVICE_CLASS_UNITS = {
|
|
|
|
DEVICE_CLASS_ENERGY: ENERGY_KILO_WATT_HOUR,
|
|
|
|
DEVICE_CLASS_POWER: POWER_WATT,
|
|
|
|
DEVICE_CLASS_PRESSURE: PRESSURE_PA,
|
|
|
|
DEVICE_CLASS_TEMPERATURE: TEMP_CELSIUS,
|
2021-08-11 16:58:19 +00:00
|
|
|
DEVICE_CLASS_GAS: VOLUME_CUBIC_METERS,
|
2021-06-30 11:32:17 +00:00
|
|
|
}
|
|
|
|
|
2021-06-30 12:17:58 +00:00
|
|
|
UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = {
|
2021-07-02 07:51:47 +00:00
|
|
|
# Convert energy to kWh
|
2021-06-29 10:20:10 +00:00
|
|
|
DEVICE_CLASS_ENERGY: {
|
|
|
|
ENERGY_KILO_WATT_HOUR: lambda x: x,
|
|
|
|
ENERGY_WATT_HOUR: lambda x: x / 1000,
|
2021-06-29 12:48:08 +00:00
|
|
|
},
|
2021-07-02 07:51:47 +00:00
|
|
|
# Convert power W
|
2021-06-29 12:48:08 +00:00
|
|
|
DEVICE_CLASS_POWER: {
|
|
|
|
POWER_WATT: lambda x: x,
|
|
|
|
POWER_KILO_WATT: lambda x: x * 1000,
|
|
|
|
},
|
2021-07-02 07:51:47 +00:00
|
|
|
# Convert pressure to Pa
|
|
|
|
# Note: pressure_util.convert is bypassed to avoid redundant error checking
|
2021-06-29 21:30:13 +00:00
|
|
|
DEVICE_CLASS_PRESSURE: {
|
|
|
|
PRESSURE_BAR: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_BAR],
|
|
|
|
PRESSURE_HPA: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_HPA],
|
|
|
|
PRESSURE_INHG: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_INHG],
|
|
|
|
PRESSURE_MBAR: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_MBAR],
|
|
|
|
PRESSURE_PA: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_PA],
|
|
|
|
PRESSURE_PSI: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_PSI],
|
|
|
|
},
|
2021-07-02 07:51:47 +00:00
|
|
|
# Convert temperature to °C
|
|
|
|
# Note: temperature_util.convert is bypassed to avoid redundant error checking
|
2021-06-30 12:17:58 +00:00
|
|
|
DEVICE_CLASS_TEMPERATURE: {
|
|
|
|
TEMP_CELSIUS: lambda x: x,
|
|
|
|
TEMP_FAHRENHEIT: temperature_util.fahrenheit_to_celsius,
|
|
|
|
TEMP_KELVIN: temperature_util.kelvin_to_celsius,
|
|
|
|
},
|
2021-08-11 16:58:19 +00:00
|
|
|
# Convert volume to cubic meter
|
|
|
|
DEVICE_CLASS_GAS: {
|
|
|
|
VOLUME_CUBIC_METERS: lambda x: x,
|
|
|
|
VOLUME_CUBIC_FEET: volume_util.cubic_feet_to_cubic_meter,
|
|
|
|
},
|
2021-06-29 10:20:10 +00:00
|
|
|
}
|
|
|
|
|
2021-08-25 11:01:55 +00:00
|
|
|
# Keep track of entities for which a warning about decreasing value has been logged
|
2021-08-26 12:27:14 +00:00
|
|
|
SEEN_DIP = "sensor_seen_total_increasing_dip"
|
2021-08-25 11:01:55 +00:00
|
|
|
WARN_DIP = "sensor_warn_total_increasing_dip"
|
2021-07-02 07:51:47 +00:00
|
|
|
# Keep track of entities for which a warning about unsupported unit has been logged
|
2021-08-25 11:00:35 +00:00
|
|
|
WARN_UNSUPPORTED_UNIT = "sensor_warn_unsupported_unit"
|
|
|
|
WARN_UNSTABLE_UNIT = "sensor_warn_unstable_unit"
|
2021-05-16 17:23:37 +00:00
|
|
|
|
2021-08-13 10:35:23 +00:00
|
|
|
|
2021-08-25 11:00:35 +00:00
|
|
|
def _get_entities(hass: HomeAssistant) -> list[tuple[str, str, str | None]]:
|
|
|
|
"""Get (entity_id, state_class, device_class) of all sensors for which to compile statistics."""
|
2021-05-16 17:23:37 +00:00
|
|
|
all_sensors = hass.states.all(DOMAIN)
|
|
|
|
entity_ids = []
|
|
|
|
|
|
|
|
for state in all_sensors:
|
2021-08-13 10:35:23 +00:00
|
|
|
if (state_class := state.attributes.get(ATTR_STATE_CLASS)) not in STATE_CLASSES:
|
2021-05-16 17:23:37 +00:00
|
|
|
continue
|
2021-08-25 11:00:35 +00:00
|
|
|
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
|
|
|
|
entity_ids.append((state.entity_id, state_class, device_class))
|
2021-07-27 19:56:34 +00:00
|
|
|
|
2021-05-16 17:23:37 +00:00
|
|
|
return entity_ids
|
|
|
|
|
|
|
|
|
2021-05-28 11:16:52 +00:00
|
|
|
def _time_weighted_average(
|
|
|
|
fstates: list[tuple[float, State]], start: datetime.datetime, end: datetime.datetime
|
|
|
|
) -> float:
|
|
|
|
"""Calculate a time weighted average.
|
|
|
|
|
|
|
|
The average is calculated by, weighting the states by duration in seconds between
|
|
|
|
state changes.
|
|
|
|
Note: there's no interpolation of values between state changes.
|
|
|
|
"""
|
|
|
|
old_fstate: float | None = None
|
|
|
|
old_start_time: datetime.datetime | None = None
|
|
|
|
accumulated = 0.0
|
|
|
|
|
|
|
|
for fstate, state in fstates:
|
|
|
|
# The recorder will give us the last known state, which may be well
|
|
|
|
# before the requested start time for the statistics
|
|
|
|
start_time = start if state.last_updated < start else state.last_updated
|
|
|
|
if old_start_time is None:
|
|
|
|
# Adjust start time, if there was no last known state
|
|
|
|
start = start_time
|
|
|
|
else:
|
|
|
|
duration = start_time - old_start_time
|
|
|
|
# Accumulate the value, weighted by duration until next state change
|
|
|
|
assert old_fstate is not None
|
|
|
|
accumulated += old_fstate * duration.total_seconds()
|
|
|
|
|
|
|
|
old_fstate = fstate
|
|
|
|
old_start_time = start_time
|
|
|
|
|
|
|
|
if old_fstate is not None:
|
|
|
|
# Accumulate the value, weighted by duration until end of the period
|
|
|
|
assert old_start_time is not None
|
|
|
|
duration = end - old_start_time
|
|
|
|
accumulated += old_fstate * duration.total_seconds()
|
|
|
|
|
|
|
|
return accumulated / (end - start).total_seconds()
|
|
|
|
|
|
|
|
|
2021-08-25 11:00:35 +00:00
|
|
|
def _get_units(fstates: list[tuple[float, State]]) -> set[str | None]:
|
|
|
|
"""Return True if all states have the same unit."""
|
|
|
|
return {item[1].attributes.get(ATTR_UNIT_OF_MEASUREMENT) for item in fstates}
|
|
|
|
|
|
|
|
|
2021-09-08 15:05:16 +00:00
|
|
|
def _parse_float(state: str) -> float:
|
|
|
|
"""Parse a float string, throw on inf or nan."""
|
|
|
|
fstate = float(state)
|
|
|
|
if math.isnan(fstate) or math.isinf(fstate):
|
|
|
|
raise ValueError
|
|
|
|
return fstate
|
|
|
|
|
|
|
|
|
2021-06-29 10:20:10 +00:00
|
|
|
def _normalize_states(
|
2021-08-25 11:00:35 +00:00
|
|
|
hass: HomeAssistant,
|
|
|
|
entity_history: list[State],
|
|
|
|
device_class: str | None,
|
|
|
|
entity_id: str,
|
2021-06-30 11:32:17 +00:00
|
|
|
) -> tuple[str | None, list[tuple[float, State]]]:
|
2021-06-29 10:20:10 +00:00
|
|
|
"""Normalize units."""
|
2021-07-01 12:53:03 +00:00
|
|
|
unit = None
|
2021-06-29 10:20:10 +00:00
|
|
|
|
2021-08-25 11:00:35 +00:00
|
|
|
if device_class not in UNIT_CONVERSIONS:
|
2021-06-29 10:20:10 +00:00
|
|
|
# We're not normalizing this device class, return the state as they are
|
2021-09-04 08:47:42 +00:00
|
|
|
fstates = []
|
|
|
|
for state in entity_history:
|
|
|
|
try:
|
2021-09-08 15:05:16 +00:00
|
|
|
fstate = _parse_float(state.state)
|
|
|
|
except (ValueError, TypeError): # TypeError to guard for NULL state in DB
|
2021-09-04 08:47:42 +00:00
|
|
|
continue
|
2021-09-08 15:05:16 +00:00
|
|
|
fstates.append((fstate, state))
|
2021-09-04 08:47:42 +00:00
|
|
|
|
2021-07-01 12:53:03 +00:00
|
|
|
if fstates:
|
2021-08-25 11:00:35 +00:00
|
|
|
all_units = _get_units(fstates)
|
|
|
|
if len(all_units) > 1:
|
|
|
|
if WARN_UNSTABLE_UNIT not in hass.data:
|
|
|
|
hass.data[WARN_UNSTABLE_UNIT] = set()
|
|
|
|
if entity_id not in hass.data[WARN_UNSTABLE_UNIT]:
|
|
|
|
hass.data[WARN_UNSTABLE_UNIT].add(entity_id)
|
2021-08-30 10:51:46 +00:00
|
|
|
extra = ""
|
|
|
|
if old_metadata := statistics.get_metadata(hass, entity_id):
|
|
|
|
extra = (
|
|
|
|
" and matches the unit of already compiled statistics "
|
|
|
|
f"({old_metadata['unit_of_measurement']})"
|
|
|
|
)
|
2021-08-25 11:00:35 +00:00
|
|
|
_LOGGER.warning(
|
2021-08-30 10:51:46 +00:00
|
|
|
"The unit of %s is changing, got multiple %s, generation of long term "
|
|
|
|
"statistics will be suppressed unless the unit is stable%s",
|
2021-08-25 11:00:35 +00:00
|
|
|
entity_id,
|
|
|
|
all_units,
|
2021-08-30 10:51:46 +00:00
|
|
|
extra,
|
2021-08-25 11:00:35 +00:00
|
|
|
)
|
|
|
|
return None, []
|
2021-07-01 12:53:03 +00:00
|
|
|
unit = fstates[0][1].attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
2021-06-30 11:32:17 +00:00
|
|
|
return unit, fstates
|
2021-06-29 10:20:10 +00:00
|
|
|
|
|
|
|
fstates = []
|
|
|
|
|
|
|
|
for state in entity_history:
|
2021-09-04 08:47:42 +00:00
|
|
|
try:
|
2021-09-08 15:05:16 +00:00
|
|
|
fstate = _parse_float(state.state)
|
2021-09-04 08:47:42 +00:00
|
|
|
except ValueError:
|
2021-06-29 10:20:10 +00:00
|
|
|
continue
|
2021-09-08 15:05:16 +00:00
|
|
|
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
|
|
|
# Exclude unsupported units from statistics
|
|
|
|
if unit not in UNIT_CONVERSIONS[device_class]:
|
|
|
|
if WARN_UNSUPPORTED_UNIT not in hass.data:
|
|
|
|
hass.data[WARN_UNSUPPORTED_UNIT] = set()
|
|
|
|
if entity_id not in hass.data[WARN_UNSUPPORTED_UNIT]:
|
|
|
|
hass.data[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))
|
2021-06-29 10:20:10 +00:00
|
|
|
|
2021-08-25 11:00:35 +00:00
|
|
|
return DEVICE_CLASS_UNITS[device_class], fstates
|
2021-06-29 10:20:10 +00:00
|
|
|
|
|
|
|
|
2021-08-25 11:01:55 +00:00
|
|
|
def warn_dip(hass: HomeAssistant, entity_id: str) -> None:
|
2021-08-26 12:27:14 +00:00
|
|
|
"""Log a warning once if a sensor with state_class_total has a decreasing value.
|
|
|
|
|
|
|
|
The log will be suppressed until two dips have been seen to prevent warning due to
|
|
|
|
rounding issues with databases storing the state as a single precision float, which
|
|
|
|
was fixed in recorder DB version 20.
|
|
|
|
"""
|
|
|
|
if SEEN_DIP not in hass.data:
|
|
|
|
hass.data[SEEN_DIP] = set()
|
|
|
|
if entity_id not in hass.data[SEEN_DIP]:
|
|
|
|
hass.data[SEEN_DIP].add(entity_id)
|
|
|
|
return
|
2021-08-25 11:01:55 +00:00
|
|
|
if WARN_DIP not in hass.data:
|
|
|
|
hass.data[WARN_DIP] = set()
|
|
|
|
if entity_id not in hass.data[WARN_DIP]:
|
|
|
|
hass.data[WARN_DIP].add(entity_id)
|
|
|
|
domain = entity_sources(hass).get(entity_id, {}).get("domain")
|
|
|
|
if domain in ["energy", "growatt_server", "solaredge"]:
|
|
|
|
return
|
|
|
|
_LOGGER.warning(
|
|
|
|
"Entity %s %shas state class total_increasing, but its state is "
|
|
|
|
"not strictly increasing. Please create a bug report at %s",
|
|
|
|
entity_id,
|
|
|
|
f"from integration {domain} " if domain else "",
|
|
|
|
"https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue"
|
|
|
|
"+label%3A%22integration%3A+recorder%22",
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def reset_detected(
|
|
|
|
hass: HomeAssistant, entity_id: str, state: float, previous_state: float | None
|
|
|
|
) -> bool:
|
2021-08-24 15:23:55 +00:00
|
|
|
"""Test if a total_increasing sensor has been reset."""
|
2021-08-25 11:01:55 +00:00
|
|
|
if previous_state is None:
|
|
|
|
return False
|
|
|
|
|
|
|
|
if 0.9 * previous_state <= state < previous_state:
|
|
|
|
warn_dip(hass, entity_id)
|
|
|
|
|
|
|
|
return state < 0.9 * previous_state
|
2021-08-24 15:23:55 +00:00
|
|
|
|
|
|
|
|
2021-09-01 04:30:52 +00:00
|
|
|
def _wanted_statistics(
|
|
|
|
entities: list[tuple[str, str, str | None]]
|
|
|
|
) -> dict[str, set[str]]:
|
|
|
|
"""Prepare a dict with wanted statistics for entities."""
|
|
|
|
wanted_statistics = {}
|
|
|
|
for entity_id, state_class, device_class in entities:
|
|
|
|
if device_class in DEVICE_CLASS_STATISTICS[state_class]:
|
|
|
|
wanted_statistics[entity_id] = DEVICE_CLASS_STATISTICS[state_class][
|
|
|
|
device_class
|
|
|
|
]
|
|
|
|
else:
|
|
|
|
wanted_statistics[entity_id] = DEFAULT_STATISTICS[state_class]
|
|
|
|
return wanted_statistics
|
|
|
|
|
|
|
|
|
2021-08-31 17:15:22 +00:00
|
|
|
def compile_statistics( # noqa: C901
|
2021-05-16 17:23:37 +00:00
|
|
|
hass: HomeAssistant, start: datetime.datetime, end: datetime.datetime
|
|
|
|
) -> dict:
|
|
|
|
"""Compile statistics for all entities during start-end.
|
|
|
|
|
|
|
|
Note: This will query the database and must not be run in the event loop
|
|
|
|
"""
|
|
|
|
result: dict = {}
|
|
|
|
|
|
|
|
entities = _get_entities(hass)
|
|
|
|
|
2021-09-01 04:30:52 +00:00
|
|
|
wanted_statistics = _wanted_statistics(entities)
|
|
|
|
|
2021-05-16 17:23:37 +00:00
|
|
|
# Get history between start and end
|
2021-09-01 04:30:52 +00:00
|
|
|
entities_full_history = [i[0] for i in entities if "sum" in wanted_statistics[i[0]]]
|
|
|
|
history_list = {}
|
|
|
|
if entities_full_history:
|
|
|
|
history_list = history.get_significant_states( # type: ignore
|
|
|
|
hass,
|
|
|
|
start - datetime.timedelta.resolution,
|
|
|
|
end,
|
|
|
|
entity_ids=entities_full_history,
|
|
|
|
significant_changes_only=False,
|
|
|
|
)
|
|
|
|
entities_significant_history = [
|
|
|
|
i[0] for i in entities if "sum" not in wanted_statistics[i[0]]
|
|
|
|
]
|
|
|
|
if entities_significant_history:
|
|
|
|
_history_list = history.get_significant_states( # type: ignore
|
|
|
|
hass,
|
|
|
|
start - datetime.timedelta.resolution,
|
|
|
|
end,
|
|
|
|
entity_ids=entities_significant_history,
|
|
|
|
)
|
|
|
|
history_list = {**history_list, **_history_list}
|
2021-05-16 17:23:37 +00:00
|
|
|
|
2021-08-25 11:00:35 +00:00
|
|
|
for entity_id, state_class, device_class in entities:
|
2021-05-16 17:23:37 +00:00
|
|
|
if entity_id not in history_list:
|
|
|
|
continue
|
|
|
|
|
|
|
|
entity_history = history_list[entity_id]
|
2021-08-25 11:00:35 +00:00
|
|
|
unit, fstates = _normalize_states(hass, entity_history, device_class, entity_id)
|
2021-05-16 17:23:37 +00:00
|
|
|
|
|
|
|
if not fstates:
|
|
|
|
continue
|
|
|
|
|
2021-08-25 11:00:35 +00:00
|
|
|
# Check metadata
|
|
|
|
if old_metadata := statistics.get_metadata(hass, entity_id):
|
|
|
|
if old_metadata["unit_of_measurement"] != unit:
|
|
|
|
if WARN_UNSTABLE_UNIT not in hass.data:
|
|
|
|
hass.data[WARN_UNSTABLE_UNIT] = set()
|
|
|
|
if entity_id not in hass.data[WARN_UNSTABLE_UNIT]:
|
|
|
|
hass.data[WARN_UNSTABLE_UNIT].add(entity_id)
|
|
|
|
_LOGGER.warning(
|
|
|
|
"The unit of %s (%s) does not match the unit of already "
|
|
|
|
"compiled statistics (%s). Generation of long term statistics "
|
|
|
|
"will be suppressed unless the unit changes back to %s",
|
|
|
|
entity_id,
|
|
|
|
unit,
|
|
|
|
old_metadata["unit_of_measurement"],
|
2021-08-30 10:51:46 +00:00
|
|
|
old_metadata["unit_of_measurement"],
|
2021-08-25 11:00:35 +00:00
|
|
|
)
|
|
|
|
continue
|
|
|
|
|
2021-05-16 17:23:37 +00:00
|
|
|
result[entity_id] = {}
|
|
|
|
|
2021-06-30 11:32:17 +00:00
|
|
|
# Set meta data
|
2021-07-02 11:17:00 +00:00
|
|
|
result[entity_id]["meta"] = {
|
|
|
|
"unit_of_measurement": unit,
|
2021-09-01 04:30:52 +00:00
|
|
|
"has_mean": "mean" in wanted_statistics[entity_id],
|
|
|
|
"has_sum": "sum" in wanted_statistics[entity_id],
|
2021-07-02 11:17:00 +00:00
|
|
|
}
|
2021-06-30 11:32:17 +00:00
|
|
|
|
2021-05-16 17:23:37 +00:00
|
|
|
# Make calculations
|
2021-06-30 11:32:17 +00:00
|
|
|
stat: dict = {}
|
2021-09-01 04:30:52 +00:00
|
|
|
if "max" in wanted_statistics[entity_id]:
|
2021-06-30 11:32:17 +00:00
|
|
|
stat["max"] = max(*itertools.islice(zip(*fstates), 1))
|
2021-09-01 04:30:52 +00:00
|
|
|
if "min" in wanted_statistics[entity_id]:
|
2021-06-30 11:32:17 +00:00
|
|
|
stat["min"] = min(*itertools.islice(zip(*fstates), 1))
|
2021-05-16 17:23:37 +00:00
|
|
|
|
2021-09-01 04:30:52 +00:00
|
|
|
if "mean" in wanted_statistics[entity_id]:
|
2021-06-30 11:32:17 +00:00
|
|
|
stat["mean"] = _time_weighted_average(fstates, start, end)
|
2021-05-20 11:05:15 +00:00
|
|
|
|
2021-09-01 04:30:52 +00:00
|
|
|
if "sum" in wanted_statistics[entity_id]:
|
2021-08-24 15:02:34 +00:00
|
|
|
last_reset = old_last_reset = None
|
2021-05-20 11:05:15 +00:00
|
|
|
new_state = old_state = None
|
|
|
|
_sum = 0
|
2021-09-08 15:08:48 +00:00
|
|
|
last_stats = statistics.get_last_statistics(hass, 1, entity_id, False)
|
2021-05-20 11:05:15 +00:00
|
|
|
if entity_id in last_stats:
|
|
|
|
# We have compiled history for this sensor before, use that as a starting point
|
2021-08-24 15:02:34 +00:00
|
|
|
last_reset = old_last_reset = last_stats[entity_id][0]["last_reset"]
|
2021-05-20 11:05:15 +00:00
|
|
|
new_state = old_state = last_stats[entity_id][0]["state"]
|
2021-08-27 14:18:49 +00:00
|
|
|
_sum = last_stats[entity_id][0]["sum"] or 0
|
2021-05-20 11:05:15 +00:00
|
|
|
|
|
|
|
for fstate, state in fstates:
|
2021-06-30 12:17:58 +00:00
|
|
|
|
2021-09-06 16:28:58 +00:00
|
|
|
# Deprecated, will be removed in Home Assistant 2021.11
|
2021-08-13 10:35:23 +00:00
|
|
|
if (
|
|
|
|
"last_reset" not in state.attributes
|
|
|
|
and state_class == STATE_CLASS_MEASUREMENT
|
|
|
|
):
|
2021-05-20 11:05:15 +00:00
|
|
|
continue
|
2021-08-13 10:35:23 +00:00
|
|
|
|
|
|
|
reset = False
|
2021-08-24 15:02:34 +00:00
|
|
|
if (
|
|
|
|
state_class != STATE_CLASS_TOTAL_INCREASING
|
|
|
|
and (last_reset := state.attributes.get("last_reset"))
|
|
|
|
!= old_last_reset
|
|
|
|
):
|
2021-08-31 17:15:22 +00:00
|
|
|
if old_state is None:
|
|
|
|
_LOGGER.info(
|
|
|
|
"Compiling initial sum statistics for %s, zero point set to %s",
|
|
|
|
entity_id,
|
|
|
|
fstate,
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
_LOGGER.info(
|
|
|
|
"Detected new cycle for %s, last_reset set to %s (old last_reset %s)",
|
|
|
|
entity_id,
|
|
|
|
last_reset,
|
|
|
|
old_last_reset,
|
|
|
|
)
|
2021-08-24 15:02:34 +00:00
|
|
|
reset = True
|
|
|
|
elif old_state is None and last_reset is None:
|
2021-08-13 10:35:23 +00:00
|
|
|
reset = True
|
2021-08-24 09:18:59 +00:00
|
|
|
_LOGGER.info(
|
|
|
|
"Compiling initial sum statistics for %s, zero point set to %s",
|
|
|
|
entity_id,
|
|
|
|
fstate,
|
|
|
|
)
|
2021-08-13 10:35:23 +00:00
|
|
|
elif state_class == STATE_CLASS_TOTAL_INCREASING and (
|
2021-08-25 11:01:55 +00:00
|
|
|
old_state is None
|
|
|
|
or reset_detected(hass, entity_id, fstate, new_state)
|
2021-08-13 10:35:23 +00:00
|
|
|
):
|
|
|
|
reset = True
|
2021-08-24 09:18:59 +00:00
|
|
|
_LOGGER.info(
|
2021-08-31 17:15:22 +00:00
|
|
|
"Detected new cycle for %s, value dropped from %s to %s",
|
2021-08-24 09:18:59 +00:00
|
|
|
entity_id,
|
|
|
|
fstate,
|
|
|
|
new_state,
|
|
|
|
)
|
2021-08-13 10:35:23 +00:00
|
|
|
|
|
|
|
if reset:
|
2021-05-20 11:05:15 +00:00
|
|
|
# The sensor has been reset, update the sum
|
|
|
|
if old_state is not None:
|
|
|
|
_sum += new_state - old_state
|
|
|
|
# ..and update the starting point
|
|
|
|
new_state = fstate
|
2021-08-24 15:02:34 +00:00
|
|
|
old_last_reset = last_reset
|
2021-08-31 08:45:17 +00:00
|
|
|
# Force a new cycle for an existing sensor to start at 0
|
|
|
|
if old_state is not None:
|
2021-08-18 08:03:27 +00:00
|
|
|
old_state = 0.0
|
2021-08-17 21:05:31 +00:00
|
|
|
else:
|
|
|
|
old_state = new_state
|
2021-05-20 11:05:15 +00:00
|
|
|
else:
|
|
|
|
new_state = fstate
|
|
|
|
|
2021-08-24 15:02:34 +00:00
|
|
|
# Deprecated, will be removed in Home Assistant 2021.11
|
|
|
|
if last_reset is None and state_class == STATE_CLASS_MEASUREMENT:
|
|
|
|
# No valid updates
|
|
|
|
result.pop(entity_id)
|
|
|
|
continue
|
|
|
|
|
2021-08-13 10:35:23 +00:00
|
|
|
if new_state is None or old_state is None:
|
2021-05-20 11:05:15 +00:00
|
|
|
# No valid updates
|
|
|
|
result.pop(entity_id)
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Update the sum with the last state
|
|
|
|
_sum += new_state - old_state
|
2021-08-24 15:02:34 +00:00
|
|
|
if last_reset is not None:
|
|
|
|
stat["last_reset"] = dt_util.parse_datetime(last_reset)
|
2021-06-30 11:32:17 +00:00
|
|
|
stat["sum"] = _sum
|
|
|
|
stat["state"] = new_state
|
|
|
|
|
|
|
|
result[entity_id]["stat"] = stat
|
2021-05-16 17:23:37 +00:00
|
|
|
|
|
|
|
return result
|
2021-07-14 09:54:55 +00:00
|
|
|
|
|
|
|
|
|
|
|
def list_statistic_ids(hass: HomeAssistant, statistic_type: str | None = None) -> dict:
|
|
|
|
"""Return statistic_ids and meta data."""
|
|
|
|
entities = _get_entities(hass)
|
|
|
|
|
|
|
|
statistic_ids = {}
|
|
|
|
|
2021-08-25 11:00:35 +00:00
|
|
|
for entity_id, state_class, device_class in entities:
|
|
|
|
if device_class in DEVICE_CLASS_STATISTICS[state_class]:
|
|
|
|
provided_statistics = DEVICE_CLASS_STATISTICS[state_class][device_class]
|
|
|
|
else:
|
|
|
|
provided_statistics = DEFAULT_STATISTICS[state_class]
|
2021-07-14 09:54:55 +00:00
|
|
|
|
|
|
|
if statistic_type is not None and statistic_type not in provided_statistics:
|
|
|
|
continue
|
|
|
|
|
|
|
|
state = hass.states.get(entity_id)
|
|
|
|
assert state
|
|
|
|
|
2021-08-18 08:03:27 +00:00
|
|
|
if (
|
|
|
|
"sum" in provided_statistics
|
|
|
|
and ATTR_LAST_RESET not in state.attributes
|
|
|
|
and state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
|
|
|
|
):
|
2021-07-14 09:54:55 +00:00
|
|
|
continue
|
|
|
|
|
2021-08-25 11:00:35 +00:00
|
|
|
metadata = statistics.get_metadata(hass, entity_id)
|
|
|
|
if metadata:
|
|
|
|
native_unit: str | None = metadata["unit_of_measurement"]
|
|
|
|
else:
|
|
|
|
native_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
2021-07-14 09:54:55 +00:00
|
|
|
|
2021-08-25 11:00:35 +00:00
|
|
|
if device_class not in UNIT_CONVERSIONS:
|
2021-07-14 09:54:55 +00:00
|
|
|
statistic_ids[entity_id] = native_unit
|
|
|
|
continue
|
|
|
|
|
2021-08-25 11:00:35 +00:00
|
|
|
if native_unit not in UNIT_CONVERSIONS[device_class]:
|
2021-07-14 09:54:55 +00:00
|
|
|
continue
|
|
|
|
|
2021-08-25 11:00:35 +00:00
|
|
|
statistics_unit = DEVICE_CLASS_UNITS[device_class]
|
2021-07-14 09:54:55 +00:00
|
|
|
statistic_ids[entity_id] = statistics_unit
|
|
|
|
|
|
|
|
return statistic_ids
|