diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 78c5719510aa3..8499b610659e3 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -94,6 +94,8 @@ class _StatisticsConfig: # Keep track of entities for which a warning about unsupported unit has been logged WARN_UNSUPPORTED_UNIT: HassKey[set[str]] = HassKey(f"{DOMAIN}_warn_unsupported_unit") WARN_UNSTABLE_UNIT: HassKey[set[str]] = HassKey(f"{DOMAIN}_warn_unstable_unit") +# Keep track of entities for which a change in unit has been observed +SEEN_CHANGED_UNIT: HassKey[set[str]] = HassKey(f"{DOMAIN}_seen_changed_unit") # Keep track of entities for which a warning about statistics mean algorithm change has been logged WARN_STATISTICS_MEAN_CHANGED: HassKey[set[str]] = HassKey( f"{DOMAIN}_warn_statistics_mean_change" @@ -279,11 +281,12 @@ def _normalize_states( """Normalize units.""" state_unit: str | None = None statistics_unit: str | None - state_unit = fstates[0][1].attributes.get(ATTR_UNIT_OF_MEASUREMENT) - device_class = fstates[0][1].attributes.get(ATTR_DEVICE_CLASS) + state_unit = fstates[-1][1].attributes.get(ATTR_UNIT_OF_MEASUREMENT) + all_units = _get_units(fstates) + device_class = fstates[-1][1].attributes.get(ATTR_DEVICE_CLASS) old_metadata = old_metadatas[entity_id][1] if entity_id in old_metadatas else None if not old_metadata: - # We've not seen this sensor before, the first valid state determines the unit + # We've not seen this sensor before, the most recent valid state determines the unit # used for statistics statistics_unit = state_unit unit_class = _get_unit_class(device_class, state_unit) @@ -305,7 +308,6 @@ def _normalize_states( if not (converter := _get_unit_converter(unit_class)): # The unit used by this sensor doesn't support unit conversion - all_units = _get_units(fstates) if not _equivalent_units(all_units): if WARN_UNSTABLE_UNIT not in hass.data: hass.data[WARN_UNSTABLE_UNIT] = set() @@ -332,20 +334,34 @@ def _normalize_states( if state_unit != statistics_unit: unit_class = _get_unit_class( - fstates[0][1].attributes.get(ATTR_DEVICE_CLASS), + fstates[-1][1].attributes.get(ATTR_DEVICE_CLASS), state_unit, ) return unit_class, state_unit, fstates - valid_fstates: list[tuple[float, State]] = [] - convert: Callable[[float], float] | None = None - last_unit: str | None | UndefinedType = UNDEFINED valid_units = converter.VALID_UNITS - for fstate, state in fstates: - state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - # Exclude states with unsupported unit from statistics - if state_unit not in valid_units: + if any(unit not in valid_units for unit in all_units): + states_by_unit: list[list[tuple[float, State]]] = [ + list(states) + for _, states in itertools.groupby( + fstates, key=lambda x: x[1].attributes.get(ATTR_UNIT_OF_MEASUREMENT) + ) + ] + + if SEEN_CHANGED_UNIT not in hass.data: + hass.data[SEEN_CHANGED_UNIT] = set() + + if ( + len(states_by_unit) == 2 + and state_unit == statistics_unit + and entity_id not in hass.data[SEEN_CHANGED_UNIT] + ): + # Potential unit migration, silently drop states with unexpected units once + hass.data[SEEN_CHANGED_UNIT].add(entity_id) + fstates = states_by_unit[1] + else: + # Exclude states with unsupported unit from statistics if WARN_UNSUPPORTED_UNIT not in hass.data: hass.data[WARN_UNSUPPORTED_UNIT] = set() if entity_id not in hass.data[WARN_UNSUPPORTED_UNIT]: @@ -363,8 +379,18 @@ def _normalize_states( statistics_unit, LINK_DEV_STATISTICS, ) - continue + fstates = [ + (fstate, state) + for fstate, state in fstates + if state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) in valid_units + ] + valid_fstates: list[tuple[float, State]] = [] + convert: Callable[[float], float] | None = None + last_unit: str | None | UndefinedType = UNDEFINED + + for fstate, state in fstates: + state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state_unit != last_unit: # The unit of measurement has changed since the last state change # recreate the converter factory