Don't create statistics issues when sensor is unavailable or unknown (#127226)

This commit is contained in:
Erik Montnemery 2024-10-01 22:08:48 +02:00 committed by Franck Nijhof
parent 88ff94dd69
commit df6edd09c0
No known key found for this signature in database
GPG key ID: D62583BA8AB11CA3
2 changed files with 87 additions and 3 deletions

View file

@ -4,6 +4,7 @@ from __future__ import annotations
from collections import defaultdict from collections import defaultdict
from collections.abc import Callable, Iterable from collections.abc import Callable, Iterable
from contextlib import suppress
import datetime import datetime
from functools import partial from functools import partial
import itertools import itertools
@ -179,6 +180,14 @@ def _entity_history_to_float_and_state(
return float_states return float_states
def _is_numeric(state: State) -> bool:
"""Return if the state is numeric."""
with suppress(ValueError, TypeError):
if (num_state := float(state.state)) is not None and math.isfinite(num_state):
return True
return False
def _normalize_states( def _normalize_states(
hass: HomeAssistant, hass: HomeAssistant,
old_metadatas: dict[str, tuple[int, StatisticMetaData]], old_metadatas: dict[str, tuple[int, StatisticMetaData]],
@ -684,13 +693,14 @@ def _update_issues(
"""Update repair issues.""" """Update repair issues."""
for state in sensor_states: for state in sensor_states:
entity_id = state.entity_id entity_id = state.entity_id
numeric = _is_numeric(state)
state_class = try_parse_enum( state_class = try_parse_enum(
SensorStateClass, state.attributes.get(ATTR_STATE_CLASS) SensorStateClass, state.attributes.get(ATTR_STATE_CLASS)
) )
state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
if metadata := metadatas.get(entity_id): if metadata := metadatas.get(entity_id):
if state_class is None: if numeric and state_class is None:
# Sensor no longer has a valid state class # Sensor no longer has a valid state class
report_issue( report_issue(
"state_class_removed", "state_class_removed",
@ -703,7 +713,7 @@ def _update_issues(
metadata_unit = metadata[1]["unit_of_measurement"] metadata_unit = metadata[1]["unit_of_measurement"]
converter = statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER.get(metadata_unit) converter = statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER.get(metadata_unit)
if not converter: if not converter:
if not _equivalent_units({state_unit, metadata_unit}): if numeric and not _equivalent_units({state_unit, metadata_unit}):
# The unit has changed, and it's not possible to convert # The unit has changed, and it's not possible to convert
report_issue( report_issue(
"units_changed", "units_changed",
@ -717,7 +727,7 @@ def _update_issues(
) )
else: else:
clear_issue("units_changed", entity_id) clear_issue("units_changed", entity_id)
elif state_unit not in converter.VALID_UNITS: elif numeric and state_unit not in converter.VALID_UNITS:
# The state unit can't be converted to the unit in metadata # The state unit can't be converted to the unit in metadata
valid_units = (unit or "<None>" for unit in converter.VALID_UNITS) valid_units = (unit or "<None>" for unit in converter.VALID_UNITS)
valid_units_str = ", ".join(sorted(valid_units)) valid_units_str = ", ".join(sorted(valid_units))

View file

@ -4332,6 +4332,26 @@ async def test_validate_unit_change_convertible(
} }
await assert_validation_result(hass, client, expected, {"units_changed"}) await assert_validation_result(hass, client, expected, {"units_changed"})
# Unavailable state - empty response
hass.states.async_set(
"sensor.test",
"unavailable",
attributes={**attributes, "unit_of_measurement": "dogs"},
timestamp=now.timestamp(),
)
await async_recorder_block_till_done(hass)
await assert_validation_result(hass, client, {}, {})
# Unknown state - empty response
hass.states.async_set(
"sensor.test",
"unknown",
attributes={**attributes, "unit_of_measurement": "dogs"},
timestamp=now.timestamp(),
)
await async_recorder_block_till_done(hass)
await assert_validation_result(hass, client, {}, {})
# Valid state - empty response # Valid state - empty response
hass.states.async_set( hass.states.async_set(
"sensor.test", "sensor.test",
@ -4531,6 +4551,26 @@ async def test_validate_statistics_unit_change_no_device_class(
} }
await assert_validation_result(hass, client, expected, {"units_changed"}) await assert_validation_result(hass, client, expected, {"units_changed"})
# Unavailable state - empty response
hass.states.async_set(
"sensor.test",
"unavailable",
attributes={**attributes, "unit_of_measurement": "dogs"},
timestamp=now.timestamp(),
)
await async_recorder_block_till_done(hass)
await assert_validation_result(hass, client, {}, {})
# Unknown state - empty response
hass.states.async_set(
"sensor.test",
"unknown",
attributes={**attributes, "unit_of_measurement": "dogs"},
timestamp=now.timestamp(),
)
await async_recorder_block_till_done(hass)
await assert_validation_result(hass, client, {}, {})
# Valid state - empty response # Valid state - empty response
hass.states.async_set( hass.states.async_set(
"sensor.test", "sensor.test",
@ -4627,6 +4667,20 @@ async def test_validate_statistics_state_class_removed(
} }
await assert_validation_result(hass, client, expected, {"state_class_removed"}) await assert_validation_result(hass, client, expected, {"state_class_removed"})
# Unavailable state - empty response
hass.states.async_set(
"sensor.test", "unavailable", attributes=_attributes, timestamp=now.timestamp()
)
await async_recorder_block_till_done(hass)
await assert_validation_result(hass, client, {}, {})
# Unknown state - empty response
hass.states.async_set(
"sensor.test", "unknown", attributes=_attributes, timestamp=now.timestamp()
)
await async_recorder_block_till_done(hass)
await assert_validation_result(hass, client, {}, {})
@pytest.mark.parametrize( @pytest.mark.parametrize(
("units", "attributes", "unit"), ("units", "attributes", "unit"),
@ -4871,6 +4925,26 @@ async def test_validate_statistics_unit_change_no_conversion(
} }
await assert_validation_result(hass, client, expected, {"units_changed"}) await assert_validation_result(hass, client, expected, {"units_changed"})
# Unavailable state - empty response
hass.states.async_set(
"sensor.test",
"unavailable",
attributes={**attributes, "unit_of_measurement": unit2},
timestamp=now.timestamp(),
)
await async_recorder_block_till_done(hass)
await assert_validation_result(hass, client, {}, {})
# Unknown state - empty response
hass.states.async_set(
"sensor.test",
"unknown",
attributes={**attributes, "unit_of_measurement": unit2},
timestamp=now.timestamp(),
)
await async_recorder_block_till_done(hass)
await assert_validation_result(hass, client, {}, {})
# Original unit - empty response # Original unit - empty response
hass.states.async_set( hass.states.async_set(
"sensor.test", "sensor.test",