Don't create statistics issues when sensor is unavailable or unknown (#127226)
parent
f4ab741445
commit
6d65d6bcf6
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
|||
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable, Iterable
|
||||
from contextlib import suppress
|
||||
import datetime
|
||||
from functools import partial
|
||||
import itertools
|
||||
|
@ -179,6 +180,14 @@ def _entity_history_to_float_and_state(
|
|||
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(
|
||||
hass: HomeAssistant,
|
||||
old_metadatas: dict[str, tuple[int, StatisticMetaData]],
|
||||
|
@ -684,13 +693,14 @@ def _update_issues(
|
|||
"""Update repair issues."""
|
||||
for state in sensor_states:
|
||||
entity_id = state.entity_id
|
||||
numeric = _is_numeric(state)
|
||||
state_class = try_parse_enum(
|
||||
SensorStateClass, state.attributes.get(ATTR_STATE_CLASS)
|
||||
)
|
||||
state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
|
||||
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
|
||||
report_issue(
|
||||
"state_class_removed",
|
||||
|
@ -703,7 +713,7 @@ def _update_issues(
|
|||
metadata_unit = metadata[1]["unit_of_measurement"]
|
||||
converter = statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER.get(metadata_unit)
|
||||
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
|
||||
report_issue(
|
||||
"units_changed",
|
||||
|
@ -717,7 +727,7 @@ def _update_issues(
|
|||
)
|
||||
else:
|
||||
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
|
||||
valid_units = (unit or "<None>" for unit in converter.VALID_UNITS)
|
||||
valid_units_str = ", ".join(sorted(valid_units))
|
||||
|
|
|
@ -4332,6 +4332,26 @@ async def test_validate_unit_change_convertible(
|
|||
}
|
||||
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
|
||||
hass.states.async_set(
|
||||
"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"})
|
||||
|
||||
# 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
|
||||
hass.states.async_set(
|
||||
"sensor.test",
|
||||
|
@ -4627,6 +4667,20 @@ async def test_validate_statistics_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(
|
||||
("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"})
|
||||
|
||||
# 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
|
||||
hass.states.async_set(
|
||||
"sensor.test",
|
||||
|
|
Loading…
Reference in New Issue