mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 14:17:45 +00:00
Do not allow inf
or nan
sensor states in statistics (#55943)
This commit is contained in:
parent
22e6ddf8df
commit
9f1e503784
@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
import datetime
|
import datetime
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
|
import math
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
from homeassistant.components.recorder import history, statistics
|
from homeassistant.components.recorder import history, statistics
|
||||||
@ -175,6 +176,14 @@ def _get_units(fstates: list[tuple[float, State]]) -> set[str | None]:
|
|||||||
return {item[1].attributes.get(ATTR_UNIT_OF_MEASUREMENT) for item in fstates}
|
return {item[1].attributes.get(ATTR_UNIT_OF_MEASUREMENT) for item in fstates}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
def _normalize_states(
|
def _normalize_states(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entity_history: list[State],
|
entity_history: list[State],
|
||||||
@ -189,9 +198,10 @@ def _normalize_states(
|
|||||||
fstates = []
|
fstates = []
|
||||||
for state in entity_history:
|
for state in entity_history:
|
||||||
try:
|
try:
|
||||||
fstates.append((float(state.state), state))
|
fstate = _parse_float(state.state)
|
||||||
except ValueError:
|
except (ValueError, TypeError): # TypeError to guard for NULL state in DB
|
||||||
continue
|
continue
|
||||||
|
fstates.append((fstate, state))
|
||||||
|
|
||||||
if fstates:
|
if fstates:
|
||||||
all_units = _get_units(fstates)
|
all_units = _get_units(fstates)
|
||||||
@ -221,20 +231,20 @@ def _normalize_states(
|
|||||||
|
|
||||||
for state in entity_history:
|
for state in entity_history:
|
||||||
try:
|
try:
|
||||||
fstate = float(state.state)
|
fstate = _parse_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 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))
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
continue
|
continue
|
||||||
|
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))
|
||||||
|
|
||||||
return DEVICE_CLASS_UNITS[device_class], fstates
|
return DEVICE_CLASS_UNITS[device_class], fstates
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""The tests for sensor recorder platform."""
|
"""The tests for sensor recorder platform."""
|
||||||
# pylint: disable=protected-access,invalid-name
|
# pylint: disable=protected-access,invalid-name
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
import math
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -349,6 +350,70 @@ def test_compile_hourly_sum_statistics_amount_reset_every_state_change(
|
|||||||
assert "Error while processing event StatisticsTask" not in caplog.text
|
assert "Error while processing event StatisticsTask" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("state_class", ["measurement"])
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"device_class,unit,native_unit,factor",
|
||||||
|
[
|
||||||
|
("energy", "kWh", "kWh", 1),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_compile_hourly_sum_statistics_nan_inf_state(
|
||||||
|
hass_recorder, caplog, state_class, device_class, unit, native_unit, factor
|
||||||
|
):
|
||||||
|
"""Test compiling hourly statistics with nan and inf states."""
|
||||||
|
zero = dt_util.utcnow()
|
||||||
|
hass = hass_recorder()
|
||||||
|
recorder = hass.data[DATA_INSTANCE]
|
||||||
|
setup_component(hass, "sensor", {})
|
||||||
|
attributes = {
|
||||||
|
"device_class": device_class,
|
||||||
|
"state_class": state_class,
|
||||||
|
"unit_of_measurement": unit,
|
||||||
|
"last_reset": None,
|
||||||
|
}
|
||||||
|
seq = [10, math.nan, 15, 15, 20, math.inf, 20, 10]
|
||||||
|
|
||||||
|
states = {"sensor.test1": []}
|
||||||
|
one = zero
|
||||||
|
for i in range(len(seq)):
|
||||||
|
one = one + timedelta(minutes=1)
|
||||||
|
_states = record_meter_state(
|
||||||
|
hass, one, "sensor.test1", attributes, seq[i : i + 1]
|
||||||
|
)
|
||||||
|
states["sensor.test1"].extend(_states["sensor.test1"])
|
||||||
|
|
||||||
|
hist = history.get_significant_states(
|
||||||
|
hass,
|
||||||
|
zero - timedelta.resolution,
|
||||||
|
one + timedelta.resolution,
|
||||||
|
significant_changes_only=False,
|
||||||
|
)
|
||||||
|
assert dict(states)["sensor.test1"] == dict(hist)["sensor.test1"]
|
||||||
|
|
||||||
|
recorder.do_adhoc_statistics(period="hourly", start=zero)
|
||||||
|
wait_recording_done(hass)
|
||||||
|
statistic_ids = list_statistic_ids(hass)
|
||||||
|
assert statistic_ids == [
|
||||||
|
{"statistic_id": "sensor.test1", "unit_of_measurement": native_unit}
|
||||||
|
]
|
||||||
|
stats = statistics_during_period(hass, zero)
|
||||||
|
assert stats == {
|
||||||
|
"sensor.test1": [
|
||||||
|
{
|
||||||
|
"statistic_id": "sensor.test1",
|
||||||
|
"start": process_timestamp_to_utc_isoformat(zero),
|
||||||
|
"max": None,
|
||||||
|
"mean": None,
|
||||||
|
"min": None,
|
||||||
|
"last_reset": process_timestamp_to_utc_isoformat(one),
|
||||||
|
"state": approx(factor * seq[7]),
|
||||||
|
"sum": approx(factor * (seq[2] + seq[3] + seq[4] + seq[6] + seq[7])),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
assert "Error while processing event StatisticsTask" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"device_class,unit,native_unit,factor",
|
"device_class,unit,native_unit,factor",
|
||||||
[
|
[
|
||||||
|
Loading…
x
Reference in New Issue
Block a user