Don't create statistics issues when sensor is unavailable or unknown (#127226)
This commit is contained in:
parent
88ff94dd69
commit
df6edd09c0
2 changed files with 87 additions and 3 deletions
|
@ -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))
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Add table
Reference in a new issue