diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index be0feb7fa52..59f20a9ed25 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -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 "" for unit in converter.VALID_UNITS) valid_units_str = ", ".join(sorted(valid_units)) diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 77bb6e17f68..04e0a1b7de8 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -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",