Do not allow inf or nan sensor states in statistics (#55943)

This commit is contained in:
Erik Montnemery 2021-09-08 17:05:16 +02:00 committed by GitHub
parent 22e6ddf8df
commit 9f1e503784
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 89 additions and 14 deletions

View File

@ -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

View File

@ -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",
[ [