diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index ef066b82060..ad948b560bb 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -125,14 +125,14 @@ QUERY_STATISTIC_META = [ STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = { - DistanceConverter.NORMALIZED_UNIT: DistanceConverter, - EnergyConverter.NORMALIZED_UNIT: EnergyConverter, - MassConverter.NORMALIZED_UNIT: MassConverter, - PowerConverter.NORMALIZED_UNIT: PowerConverter, - PressureConverter.NORMALIZED_UNIT: PressureConverter, - SpeedConverter.NORMALIZED_UNIT: SpeedConverter, - TemperatureConverter.NORMALIZED_UNIT: TemperatureConverter, - VolumeConverter.NORMALIZED_UNIT: VolumeConverter, + **{unit: DistanceConverter for unit in DistanceConverter.VALID_UNITS}, + **{unit: EnergyConverter for unit in EnergyConverter.VALID_UNITS}, + **{unit: MassConverter for unit in MassConverter.VALID_UNITS}, + **{unit: PowerConverter for unit in PowerConverter.VALID_UNITS}, + **{unit: PressureConverter for unit in PressureConverter.VALID_UNITS}, + **{unit: SpeedConverter for unit in SpeedConverter.VALID_UNITS}, + **{unit: TemperatureConverter for unit in TemperatureConverter.VALID_UNITS}, + **{unit: VolumeConverter for unit in VolumeConverter.VALID_UNITS}, } @@ -140,7 +140,7 @@ _LOGGER = logging.getLogger(__name__) def _get_unit_class(unit: str | None) -> str | None: - """Get corresponding unit class from from the normalized statistics unit.""" + """Get corresponding unit class from from the statistics unit.""" if converter := STATISTIC_UNIT_TO_UNIT_CONVERTER.get(unit): return converter.UNIT_CLASS return None @@ -151,7 +151,7 @@ def _get_statistic_to_display_unit_converter( state_unit: str | None, requested_units: dict[str, str] | None, ) -> Callable[[float | None], float | None]: - """Prepare a converter from the normalized statistics unit to display unit.""" + """Prepare a converter from the statistics unit to display unit.""" def no_conversion(val: float | None) -> float | None: """Return val.""" @@ -175,21 +175,26 @@ def _get_statistic_to_display_unit_converter( return no_conversion def from_normalized_unit( - val: float | None, conv: type[BaseUnitConverter], to_unit: str + val: float | None, conv: type[BaseUnitConverter], from_unit: str, to_unit: str ) -> float | None: """Return val.""" if val is None: return val - return conv.convert(val, from_unit=conv.NORMALIZED_UNIT, to_unit=to_unit) + return conv.convert(val, from_unit=from_unit, to_unit=to_unit) - return partial(from_normalized_unit, conv=converter, to_unit=display_unit) + return partial( + from_normalized_unit, + conv=converter, + from_unit=statistic_unit, + to_unit=display_unit, + ) def _get_display_to_statistic_unit_converter( display_unit: str | None, statistic_unit: str | None, ) -> Callable[[float], float]: - """Prepare a converter from the display unit to the normalized statistics unit.""" + """Prepare a converter from the display unit to the statistics unit.""" def no_conversion(val: float) -> float: """Return val.""" @@ -201,9 +206,7 @@ def _get_display_to_statistic_unit_converter( if (converter := STATISTIC_UNIT_TO_UNIT_CONVERTER.get(statistic_unit)) is None: return no_conversion - return partial( - converter.convert, from_unit=display_unit, to_unit=converter.NORMALIZED_UNIT - ) + return partial(converter.convert, from_unit=display_unit, to_unit=statistic_unit) def _get_unit_converter( diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 4380efbd2c3..144502dd81a 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -164,10 +164,10 @@ def _normalize_states( if device_class not in UNIT_CONVERTERS or ( old_metadata and old_metadata["unit_of_measurement"] - != UNIT_CONVERTERS[device_class].NORMALIZED_UNIT + not in UNIT_CONVERTERS[device_class].VALID_UNITS ): # We're either not normalizing this device class or this entity is not stored - # normalized, return the states as they are + # in a supported unit, return the states as they are fstates = [] for state in entity_history: try: @@ -205,6 +205,10 @@ def _normalize_states( converter = UNIT_CONVERTERS[device_class] fstates = [] + statistics_unit: str | None = None + if old_metadata: + statistics_unit = old_metadata["unit_of_measurement"] + for state in entity_history: try: fstate = _parse_float(state.state) @@ -224,17 +228,19 @@ def _normalize_states( device_class, ) continue + if statistics_unit is None: + statistics_unit = state_unit fstates.append( ( converter.convert( - fstate, from_unit=state_unit, to_unit=converter.NORMALIZED_UNIT + fstate, from_unit=state_unit, to_unit=statistics_unit ), state, ) ) - return UNIT_CONVERTERS[device_class].NORMALIZED_UNIT, state_unit, fstates + return statistics_unit, state_unit, fstates def _suggest_report_issue(hass: HomeAssistant, entity_id: str) -> str: @@ -423,7 +429,7 @@ def _compile_statistics( # noqa: C901 device_class = _state.attributes.get(ATTR_DEVICE_CLASS) entity_history = history_list[entity_id] - normalized_unit, state_unit, fstates = _normalize_states( + statistics_unit, state_unit, fstates = _normalize_states( hass, session, old_metadatas, @@ -438,7 +444,7 @@ def _compile_statistics( # noqa: C901 state_class = _state.attributes[ATTR_STATE_CLASS] to_process.append( - (entity_id, normalized_unit, state_unit, state_class, fstates) + (entity_id, statistics_unit, state_unit, state_class, fstates) ) if "sum" in wanted_statistics[entity_id]: to_query.append(entity_id) @@ -448,14 +454,14 @@ def _compile_statistics( # noqa: C901 ) for ( # pylint: disable=too-many-nested-blocks entity_id, - normalized_unit, + statistics_unit, state_unit, state_class, fstates, ) in to_process: # Check metadata if old_metadata := old_metadatas.get(entity_id): - if old_metadata[1]["unit_of_measurement"] != normalized_unit: + if old_metadata[1]["unit_of_measurement"] != statistics_unit: if WARN_UNSTABLE_UNIT not in hass.data: hass.data[WARN_UNSTABLE_UNIT] = set() if entity_id not in hass.data[WARN_UNSTABLE_UNIT]: @@ -467,7 +473,7 @@ def _compile_statistics( # noqa: C901 "Go to %s to fix this", "normalized " if device_class in UNIT_CONVERTERS else "", entity_id, - normalized_unit, + statistics_unit, old_metadata[1]["unit_of_measurement"], old_metadata[1]["unit_of_measurement"], LINK_DEV_STATISTICS, @@ -481,7 +487,7 @@ def _compile_statistics( # noqa: C901 "name": None, "source": RECORDER_DOMAIN, "statistic_id": entity_id, - "unit_of_measurement": normalized_unit, + "unit_of_measurement": statistics_unit, } # Make calculations @@ -629,14 +635,13 @@ def list_statistic_ids( if state_unit not in converter.VALID_UNITS: continue - statistics_unit = converter.NORMALIZED_UNIT result[state.entity_id] = { "has_mean": "mean" in provided_statistics, "has_sum": "sum" in provided_statistics, "name": None, "source": RECORDER_DOMAIN, "statistic_id": state.entity_id, - "unit_of_measurement": statistics_unit, + "unit_of_measurement": state_unit, } return result @@ -680,13 +685,13 @@ def validate_statistics( metadata_unit = metadata[1]["unit_of_measurement"] if device_class not in UNIT_CONVERTERS: - issue_type = ( - "units_changed_can_convert" - if statistics.can_convert_units(metadata_unit, state_unit) - else "units_changed" - ) if state_unit != metadata_unit: # The unit has changed + issue_type = ( + "units_changed_can_convert" + if statistics.can_convert_units(metadata_unit, state_unit) + else "units_changed" + ) validation_result[entity_id].append( statistics.ValidationIssue( issue_type, @@ -697,22 +702,19 @@ def validate_statistics( }, ) ) - elif metadata_unit != UNIT_CONVERTERS[device_class].NORMALIZED_UNIT: + elif metadata_unit not in UNIT_CONVERTERS[device_class].VALID_UNITS: # The unit in metadata is not supported for this device class - statistics_unit = UNIT_CONVERTERS[device_class].NORMALIZED_UNIT - issue_type = ( - "unsupported_unit_metadata_can_convert" - if statistics.can_convert_units(metadata_unit, statistics_unit) - else "unsupported_unit_metadata" + valid_units = ", ".join( + sorted(UNIT_CONVERTERS[device_class].VALID_UNITS) ) validation_result[entity_id].append( statistics.ValidationIssue( - issue_type, + "unsupported_unit_metadata", { "statistic_id": entity_id, "device_class": device_class, "metadata_unit": metadata_unit, - "supported_unit": statistics_unit, + "supported_unit": valid_units, }, ) ) diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 58893ee3bb1..6058fd6a2e5 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -591,26 +591,26 @@ async def test_statistics_during_period_bad_end_time( [ (IMPERIAL_SYSTEM, DISTANCE_SENSOR_M_ATTRIBUTES, "m", "m", "distance"), (METRIC_SYSTEM, DISTANCE_SENSOR_M_ATTRIBUTES, "m", "m", "distance"), - (IMPERIAL_SYSTEM, DISTANCE_SENSOR_FT_ATTRIBUTES, "ft", "m", "distance"), - (METRIC_SYSTEM, DISTANCE_SENSOR_FT_ATTRIBUTES, "ft", "m", "distance"), - (IMPERIAL_SYSTEM, ENERGY_SENSOR_WH_ATTRIBUTES, "Wh", "kWh", "energy"), - (METRIC_SYSTEM, ENERGY_SENSOR_WH_ATTRIBUTES, "Wh", "kWh", "energy"), - (IMPERIAL_SYSTEM, GAS_SENSOR_FT3_ATTRIBUTES, "ft³", "m³", "volume"), - (METRIC_SYSTEM, GAS_SENSOR_FT3_ATTRIBUTES, "ft³", "m³", "volume"), - (IMPERIAL_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, "kW", "W", "power"), - (METRIC_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, "kW", "W", "power"), - (IMPERIAL_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, "hPa", "Pa", "pressure"), - (METRIC_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, "hPa", "Pa", "pressure"), - (IMPERIAL_SYSTEM, SPEED_SENSOR_KPH_ATTRIBUTES, "km/h", "m/s", "speed"), - (METRIC_SYSTEM, SPEED_SENSOR_KPH_ATTRIBUTES, "km/h", "m/s", "speed"), + (IMPERIAL_SYSTEM, DISTANCE_SENSOR_FT_ATTRIBUTES, "ft", "ft", "distance"), + (METRIC_SYSTEM, DISTANCE_SENSOR_FT_ATTRIBUTES, "ft", "ft", "distance"), + (IMPERIAL_SYSTEM, ENERGY_SENSOR_WH_ATTRIBUTES, "Wh", "Wh", "energy"), + (METRIC_SYSTEM, ENERGY_SENSOR_WH_ATTRIBUTES, "Wh", "Wh", "energy"), + (IMPERIAL_SYSTEM, GAS_SENSOR_FT3_ATTRIBUTES, "ft³", "ft³", "volume"), + (METRIC_SYSTEM, GAS_SENSOR_FT3_ATTRIBUTES, "ft³", "ft³", "volume"), + (IMPERIAL_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, "kW", "kW", "power"), + (METRIC_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, "kW", "kW", "power"), + (IMPERIAL_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, "hPa", "hPa", "pressure"), + (METRIC_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, "hPa", "hPa", "pressure"), + (IMPERIAL_SYSTEM, SPEED_SENSOR_KPH_ATTRIBUTES, "km/h", "km/h", "speed"), + (METRIC_SYSTEM, SPEED_SENSOR_KPH_ATTRIBUTES, "km/h", "km/h", "speed"), (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_C_ATTRIBUTES, "°C", "°C", "temperature"), (METRIC_SYSTEM, TEMPERATURE_SENSOR_C_ATTRIBUTES, "°C", "°C", "temperature"), - (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, "°F", "°C", "temperature"), - (METRIC_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, "°F", "°C", "temperature"), - (IMPERIAL_SYSTEM, VOLUME_SENSOR_FT3_ATTRIBUTES, "ft³", "m³", "volume"), - (METRIC_SYSTEM, VOLUME_SENSOR_FT3_ATTRIBUTES, "ft³", "m³", "volume"), - (IMPERIAL_SYSTEM, VOLUME_SENSOR_FT3_ATTRIBUTES_TOTAL, "ft³", "m³", "volume"), - (METRIC_SYSTEM, VOLUME_SENSOR_FT3_ATTRIBUTES_TOTAL, "ft³", "m³", "volume"), + (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, "°F", "°F", "temperature"), + (METRIC_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, "°F", "°F", "temperature"), + (IMPERIAL_SYSTEM, VOLUME_SENSOR_FT3_ATTRIBUTES, "ft³", "ft³", "volume"), + (METRIC_SYSTEM, VOLUME_SENSOR_FT3_ATTRIBUTES, "ft³", "ft³", "volume"), + (IMPERIAL_SYSTEM, VOLUME_SENSOR_FT3_ATTRIBUTES_TOTAL, "ft³", "ft³", "volume"), + (METRIC_SYSTEM, VOLUME_SENSOR_FT3_ATTRIBUTES_TOTAL, "ft³", "ft³", "volume"), ], ) async def test_list_statistic_ids( @@ -904,7 +904,7 @@ async def test_update_statistics_metadata( "name": None, "source": "recorder", "statistics_unit_of_measurement": "kW", - "unit_class": None, + "unit_class": "power", } ] @@ -994,7 +994,7 @@ async def test_change_statistics_unit(hass, hass_ws_client, recorder_mock): "name": None, "source": "recorder", "statistics_unit_of_measurement": "kW", - "unit_class": None, + "unit_class": "power", } ] @@ -1101,7 +1101,7 @@ async def test_change_statistics_unit_errors( "name": None, "source": "recorder", "statistics_unit_of_measurement": "kW", - "unit_class": None, + "unit_class": "power", } ] @@ -1504,7 +1504,7 @@ async def test_get_statistics_metadata( "has_sum": has_sum, "name": None, "source": "recorder", - "statistics_unit_of_measurement": unit, + "statistics_unit_of_measurement": attributes["unit_of_measurement"], "unit_class": unit_class, } ] @@ -1531,7 +1531,7 @@ async def test_get_statistics_metadata( "has_sum": has_sum, "name": None, "source": "recorder", - "statistics_unit_of_measurement": unit, + "statistics_unit_of_measurement": attributes["unit_of_measurement"], "unit_class": unit_class, } ] @@ -2160,9 +2160,9 @@ async def test_adjust_sum_statistics_gas( "state_unit, statistic_unit, unit_class, factor, valid_units, invalid_units", ( ("kWh", "kWh", "energy", 1, ("Wh", "kWh", "MWh"), ("ft³", "m³", "cats", None)), - ("MWh", "MWh", None, 1, ("MWh",), ("Wh", "kWh", "ft³", "m³", "cats", None)), + ("MWh", "MWh", "energy", 1, ("Wh", "kWh", "MWh"), ("ft³", "m³", "cats", None)), ("m³", "m³", "volume", 1, ("ft³", "m³"), ("Wh", "kWh", "MWh", "cats", None)), - ("ft³", "ft³", None, 1, ("ft³",), ("m³", "Wh", "kWh", "MWh", "cats", None)), + ("ft³", "ft³", "volume", 1, ("ft³", "m³"), ("Wh", "kWh", "MWh", "cats", None)), ("dogs", "dogs", None, 1, ("dogs",), ("cats", None)), (None, None, None, 1, (None,), ("cats",)), ), @@ -2262,7 +2262,7 @@ async def test_adjust_sum_statistics_errors( "statistic_id": statistic_id, "name": "Total imported energy", "source": source, - "statistics_unit_of_measurement": statistic_unit, + "statistics_unit_of_measurement": state_unit, "unit_class": unit_class, } ] @@ -2276,7 +2276,7 @@ async def test_adjust_sum_statistics_errors( "name": "Total imported energy", "source": source, "statistic_id": statistic_id, - "unit_of_measurement": statistic_unit, + "unit_of_measurement": state_unit, }, ) } diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 637d17e21a8..99aa3a3bf8e 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -86,22 +86,22 @@ def set_time_zone(): ("battery", "%", "%", "%", None, 13.050847, -10, 30), ("battery", None, None, None, None, 13.050847, -10, 30), ("distance", "m", "m", "m", "distance", 13.050847, -10, 30), - ("distance", "mi", "mi", "m", "distance", 13.050847, -10, 30), + ("distance", "mi", "mi", "mi", "distance", 13.050847, -10, 30), ("humidity", "%", "%", "%", None, 13.050847, -10, 30), ("humidity", None, None, None, None, 13.050847, -10, 30), ("pressure", "Pa", "Pa", "Pa", "pressure", 13.050847, -10, 30), - ("pressure", "hPa", "hPa", "Pa", "pressure", 13.050847, -10, 30), - ("pressure", "mbar", "mbar", "Pa", "pressure", 13.050847, -10, 30), - ("pressure", "inHg", "inHg", "Pa", "pressure", 13.050847, -10, 30), - ("pressure", "psi", "psi", "Pa", "pressure", 13.050847, -10, 30), + ("pressure", "hPa", "hPa", "hPa", "pressure", 13.050847, -10, 30), + ("pressure", "mbar", "mbar", "mbar", "pressure", 13.050847, -10, 30), + ("pressure", "inHg", "inHg", "inHg", "pressure", 13.050847, -10, 30), + ("pressure", "psi", "psi", "psi", "pressure", 13.050847, -10, 30), ("speed", "m/s", "m/s", "m/s", "speed", 13.050847, -10, 30), - ("speed", "mph", "mph", "m/s", "speed", 13.050847, -10, 30), + ("speed", "mph", "mph", "mph", "speed", 13.050847, -10, 30), ("temperature", "°C", "°C", "°C", "temperature", 13.050847, -10, 30), - ("temperature", "°F", "°F", "°C", "temperature", 13.050847, -10, 30), + ("temperature", "°F", "°F", "°F", "temperature", 13.050847, -10, 30), ("volume", "m³", "m³", "m³", "volume", 13.050847, -10, 30), - ("volume", "ft³", "ft³", "m³", "volume", 13.050847, -10, 30), + ("volume", "ft³", "ft³", "ft³", "volume", 13.050847, -10, 30), ("weight", "g", "g", "g", "mass", 13.050847, -10, 30), - ("weight", "oz", "oz", "g", "mass", 13.050847, -10, 30), + ("weight", "oz", "oz", "oz", "mass", 13.050847, -10, 30), ], ) def test_compile_hourly_statistics( @@ -355,29 +355,29 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes "units, device_class, state_unit, display_unit, statistics_unit, unit_class, factor", [ (IMPERIAL_SYSTEM, "distance", "m", "m", "m", "distance", 1), - (IMPERIAL_SYSTEM, "distance", "mi", "mi", "m", "distance", 1), + (IMPERIAL_SYSTEM, "distance", "mi", "mi", "mi", "distance", 1), (IMPERIAL_SYSTEM, "energy", "kWh", "kWh", "kWh", "energy", 1), - (IMPERIAL_SYSTEM, "energy", "Wh", "Wh", "kWh", "energy", 1), + (IMPERIAL_SYSTEM, "energy", "Wh", "Wh", "Wh", "energy", 1), (IMPERIAL_SYSTEM, "gas", "m³", "m³", "m³", "volume", 1), - (IMPERIAL_SYSTEM, "gas", "ft³", "ft³", "m³", "volume", 1), + (IMPERIAL_SYSTEM, "gas", "ft³", "ft³", "ft³", "volume", 1), (IMPERIAL_SYSTEM, "monetary", "EUR", "EUR", "EUR", None, 1), (IMPERIAL_SYSTEM, "monetary", "SEK", "SEK", "SEK", None, 1), (IMPERIAL_SYSTEM, "volume", "m³", "m³", "m³", "volume", 1), - (IMPERIAL_SYSTEM, "volume", "ft³", "ft³", "m³", "volume", 1), + (IMPERIAL_SYSTEM, "volume", "ft³", "ft³", "ft³", "volume", 1), (IMPERIAL_SYSTEM, "weight", "g", "g", "g", "mass", 1), - (IMPERIAL_SYSTEM, "weight", "oz", "oz", "g", "mass", 1), + (IMPERIAL_SYSTEM, "weight", "oz", "oz", "oz", "mass", 1), (METRIC_SYSTEM, "distance", "m", "m", "m", "distance", 1), - (METRIC_SYSTEM, "distance", "mi", "mi", "m", "distance", 1), + (METRIC_SYSTEM, "distance", "mi", "mi", "mi", "distance", 1), (METRIC_SYSTEM, "energy", "kWh", "kWh", "kWh", "energy", 1), - (METRIC_SYSTEM, "energy", "Wh", "Wh", "kWh", "energy", 1), + (METRIC_SYSTEM, "energy", "Wh", "Wh", "Wh", "energy", 1), (METRIC_SYSTEM, "gas", "m³", "m³", "m³", "volume", 1), - (METRIC_SYSTEM, "gas", "ft³", "ft³", "m³", "volume", 1), + (METRIC_SYSTEM, "gas", "ft³", "ft³", "ft³", "volume", 1), (METRIC_SYSTEM, "monetary", "EUR", "EUR", "EUR", None, 1), (METRIC_SYSTEM, "monetary", "SEK", "SEK", "SEK", None, 1), (METRIC_SYSTEM, "volume", "m³", "m³", "m³", "volume", 1), - (METRIC_SYSTEM, "volume", "ft³", "ft³", "m³", "volume", 1), + (METRIC_SYSTEM, "volume", "ft³", "ft³", "ft³", "volume", 1), (METRIC_SYSTEM, "weight", "g", "g", "g", "mass", 1), - (METRIC_SYSTEM, "weight", "oz", "oz", "g", "mass", 1), + (METRIC_SYSTEM, "weight", "oz", "oz", "oz", "mass", 1), ], ) async def test_compile_hourly_sum_statistics_amount( @@ -548,11 +548,11 @@ async def test_compile_hourly_sum_statistics_amount( "device_class, state_unit, display_unit, statistics_unit, unit_class, factor", [ ("energy", "kWh", "kWh", "kWh", "energy", 1), - ("energy", "Wh", "Wh", "kWh", "energy", 1), + ("energy", "Wh", "Wh", "Wh", "energy", 1), ("monetary", "EUR", "EUR", "EUR", None, 1), ("monetary", "SEK", "SEK", "SEK", None, 1), ("gas", "m³", "m³", "m³", "volume", 1), - ("gas", "ft³", "ft³", "m³", "volume", 1), + ("gas", "ft³", "ft³", "ft³", "volume", 1), ], ) def test_compile_hourly_sum_statistics_amount_reset_every_state_change( @@ -957,11 +957,11 @@ def test_compile_hourly_sum_statistics_negative_state( "device_class, state_unit, display_unit, statistics_unit, unit_class, factor", [ ("energy", "kWh", "kWh", "kWh", "energy", 1), - ("energy", "Wh", "Wh", "kWh", "energy", 1), + ("energy", "Wh", "Wh", "Wh", "energy", 1), ("monetary", "EUR", "EUR", "EUR", None, 1), ("monetary", "SEK", "SEK", "SEK", None, 1), ("gas", "m³", "m³", "m³", "volume", 1), - ("gas", "ft³", "ft³", "m³", "volume", 1), + ("gas", "ft³", "ft³", "ft³", "volume", 1), ], ) def test_compile_hourly_sum_statistics_total_no_reset( @@ -1061,9 +1061,9 @@ def test_compile_hourly_sum_statistics_total_no_reset( "device_class, state_unit, display_unit, statistics_unit, unit_class, factor", [ ("energy", "kWh", "kWh", "kWh", "energy", 1), - ("energy", "Wh", "Wh", "kWh", "energy", 1), + ("energy", "Wh", "Wh", "Wh", "energy", 1), ("gas", "m³", "m³", "m³", "volume", 1), - ("gas", "ft³", "ft³", "m³", "volume", 1), + ("gas", "ft³", "ft³", "ft³", "volume", 1), ], ) def test_compile_hourly_sum_statistics_total_increasing( @@ -1431,7 +1431,7 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "has_sum": True, "name": None, "source": "recorder", - "statistics_unit_of_measurement": "kWh", + "statistics_unit_of_measurement": "Wh", "unit_class": "energy", }, ] @@ -1728,40 +1728,40 @@ def test_compile_hourly_statistics_fails(hass_recorder, caplog): ("measurement", "battery", "%", "%", "%", None, "mean"), ("measurement", "battery", None, None, None, None, "mean"), ("measurement", "distance", "m", "m", "m", "distance", "mean"), - ("measurement", "distance", "mi", "mi", "m", "distance", "mean"), + ("measurement", "distance", "mi", "mi", "mi", "distance", "mean"), ("total", "distance", "m", "m", "m", "distance", "sum"), - ("total", "distance", "mi", "mi", "m", "distance", "sum"), - ("total", "energy", "Wh", "Wh", "kWh", "energy", "sum"), + ("total", "distance", "mi", "mi", "mi", "distance", "sum"), + ("total", "energy", "Wh", "Wh", "Wh", "energy", "sum"), ("total", "energy", "kWh", "kWh", "kWh", "energy", "sum"), - ("measurement", "energy", "Wh", "Wh", "kWh", "energy", "mean"), + ("measurement", "energy", "Wh", "Wh", "Wh", "energy", "mean"), ("measurement", "energy", "kWh", "kWh", "kWh", "energy", "mean"), ("measurement", "humidity", "%", "%", "%", None, "mean"), ("measurement", "humidity", None, None, None, None, "mean"), ("total", "monetary", "USD", "USD", "USD", None, "sum"), ("total", "monetary", "None", "None", "None", None, "sum"), ("total", "gas", "m³", "m³", "m³", "volume", "sum"), - ("total", "gas", "ft³", "ft³", "m³", "volume", "sum"), + ("total", "gas", "ft³", "ft³", "ft³", "volume", "sum"), ("measurement", "monetary", "USD", "USD", "USD", None, "mean"), ("measurement", "monetary", "None", "None", "None", None, "mean"), ("measurement", "gas", "m³", "m³", "m³", "volume", "mean"), - ("measurement", "gas", "ft³", "ft³", "m³", "volume", "mean"), + ("measurement", "gas", "ft³", "ft³", "ft³", "volume", "mean"), ("measurement", "pressure", "Pa", "Pa", "Pa", "pressure", "mean"), - ("measurement", "pressure", "hPa", "hPa", "Pa", "pressure", "mean"), - ("measurement", "pressure", "mbar", "mbar", "Pa", "pressure", "mean"), - ("measurement", "pressure", "inHg", "inHg", "Pa", "pressure", "mean"), - ("measurement", "pressure", "psi", "psi", "Pa", "pressure", "mean"), + ("measurement", "pressure", "hPa", "hPa", "hPa", "pressure", "mean"), + ("measurement", "pressure", "mbar", "mbar", "mbar", "pressure", "mean"), + ("measurement", "pressure", "inHg", "inHg", "inHg", "pressure", "mean"), + ("measurement", "pressure", "psi", "psi", "psi", "pressure", "mean"), ("measurement", "speed", "m/s", "m/s", "m/s", "speed", "mean"), - ("measurement", "speed", "mph", "mph", "m/s", "speed", "mean"), + ("measurement", "speed", "mph", "mph", "mph", "speed", "mean"), ("measurement", "temperature", "°C", "°C", "°C", "temperature", "mean"), - ("measurement", "temperature", "°F", "°F", "°C", "temperature", "mean"), + ("measurement", "temperature", "°F", "°F", "°F", "temperature", "mean"), ("measurement", "volume", "m³", "m³", "m³", "volume", "mean"), - ("measurement", "volume", "ft³", "ft³", "m³", "volume", "mean"), + ("measurement", "volume", "ft³", "ft³", "ft³", "volume", "mean"), ("total", "volume", "m³", "m³", "m³", "volume", "sum"), - ("total", "volume", "ft³", "ft³", "m³", "volume", "sum"), + ("total", "volume", "ft³", "ft³", "ft³", "volume", "sum"), ("measurement", "weight", "g", "g", "g", "mass", "mean"), - ("measurement", "weight", "oz", "oz", "g", "mass", "mean"), + ("measurement", "weight", "oz", "oz", "oz", "mass", "mean"), ("total", "weight", "g", "g", "g", "mass", "sum"), - ("total", "weight", "oz", "oz", "g", "mass", "sum"), + ("total", "weight", "oz", "oz", "oz", "mass", "sum"), ], ) def test_list_statistic_ids( @@ -2134,7 +2134,7 @@ def test_compile_hourly_statistics_changing_units_3( @pytest.mark.parametrize( "device_class, state_unit, statistic_unit, unit_class, mean1, mean2, min, max", [ - ("power", "kW", "W", None, 13.050847, 13.333333, -10, 30), + ("power", "kW", "kW", "power", 13.050847, 13.333333, -10, 30), ], ) def test_compile_hourly_statistics_changing_device_class_1( @@ -2207,7 +2207,7 @@ def test_compile_hourly_statistics_changing_device_class_1( hist = history.get_significant_states(hass, zero, four) assert dict(states) == dict(hist) - # Run statistics again, we get a warning, and no additional statistics is generated + # Run statistics again, additional statistics is generated do_adhoc_statistics(hass, start=zero + timedelta(minutes=10)) wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) @@ -2265,13 +2265,9 @@ def test_compile_hourly_statistics_changing_device_class_1( hist = history.get_significant_states(hass, zero, four) assert dict(states) == dict(hist) - # Run statistics again, we get a warning, and no additional statistics is generated + # Run statistics again, additional statistics is generated do_adhoc_statistics(hass, start=zero + timedelta(minutes=20)) wait_recording_done(hass) - assert ( - f"The normalized unit of sensor.test1 ({statistic_unit}) does not match the " - f"unit of already compiled statistics ({state_unit})" in caplog.text - ) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { @@ -2311,15 +2307,28 @@ def test_compile_hourly_statistics_changing_device_class_1( "state": None, "sum": None, }, + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat( + zero + timedelta(minutes=20) + ), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=25)), + "mean": approx(mean2), + "min": approx(min), + "max": approx(max), + "last_reset": None, + "state": None, + "sum": None, + }, ] } assert "Error while processing event StatisticsTask" not in caplog.text @pytest.mark.parametrize( - "device_class, state_unit, display_unit, statistic_unit, unit_class, mean, min, max", + "device_class, state_unit, display_unit, statistic_unit, unit_class, mean, mean2, min, max", [ - ("power", "kW", "kW", "W", "power", 13.050847, -10, 30), + ("power", "kW", "kW", "kW", "power", 13.050847, 13.333333, -10, 30), ], ) def test_compile_hourly_statistics_changing_device_class_2( @@ -2331,6 +2340,7 @@ def test_compile_hourly_statistics_changing_device_class_2( statistic_unit, unit_class, mean, + mean2, min, max, ): @@ -2393,13 +2403,9 @@ def test_compile_hourly_statistics_changing_device_class_2( hist = history.get_significant_states(hass, zero, four) assert dict(states) == dict(hist) - # Run statistics again, we get a warning, and no additional statistics is generated + # Run statistics again, additional statistics is generated do_adhoc_statistics(hass, start=zero + timedelta(minutes=10)) wait_recording_done(hass) - assert ( - f"The unit of sensor.test1 ({state_unit}) does not match the " - f"unit of already compiled statistics ({statistic_unit})" in caplog.text - ) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { @@ -2425,7 +2431,20 @@ def test_compile_hourly_statistics_changing_device_class_2( "last_reset": None, "state": None, "sum": None, - } + }, + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat( + zero + timedelta(minutes=10) + ), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=15)), + "mean": approx(mean2), + "min": approx(min), + "max": approx(max), + "last_reset": None, + "state": None, + "sum": None, + }, ] } assert "Error while processing event StatisticsTask" not in caplog.text @@ -3120,13 +3139,13 @@ async def test_validate_statistics_supported_device_class( @pytest.mark.parametrize( - "units, attributes, unit", + "units, attributes, valid_units", [ - (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"), + (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W, kW"), ], ) async def test_validate_statistics_supported_device_class_2( - hass, hass_ws_client, recorder_mock, units, attributes, unit + hass, hass_ws_client, recorder_mock, units, attributes, valid_units ): """Test validate_statistics.""" id = 1 @@ -3172,7 +3191,7 @@ async def test_validate_statistics_supported_device_class_2( "device_class": attributes["device_class"], "metadata_unit": None, "statistic_id": "sensor.test", - "supported_unit": unit, + "supported_unit": valid_units, }, "type": "unsupported_unit_metadata", } @@ -3192,7 +3211,7 @@ async def test_validate_statistics_supported_device_class_2( "device_class": attributes["device_class"], "metadata_unit": None, "statistic_id": "sensor.test", - "supported_unit": unit, + "supported_unit": valid_units, }, "type": "unsupported_unit_metadata", }, @@ -3209,96 +3228,6 @@ async def test_validate_statistics_supported_device_class_2( await assert_validation_result(client, expected) -@pytest.mark.parametrize( - "units, attributes, unit", - [ - (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"), - ], -) -async def test_validate_statistics_supported_device_class_3( - hass, hass_ws_client, recorder_mock, units, attributes, unit -): - """Test validate_statistics.""" - id = 1 - - def next_id(): - nonlocal id - id += 1 - return id - - async def assert_validation_result(client, expected_result): - await client.send_json( - {"id": next_id(), "type": "recorder/validate_statistics"} - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == expected_result - - now = dt_util.utcnow() - - hass.config.units = units - await async_setup_component(hass, "sensor", {}) - await async_recorder_block_till_done(hass) - client = await hass_ws_client() - - # No statistics, no state - empty response - await assert_validation_result(client, {}) - - # No statistics, valid state - empty response - initial_attributes = {"state_class": "measurement", "unit_of_measurement": "kW"} - hass.states.async_set("sensor.test", 10, attributes=initial_attributes) - await hass.async_block_till_done() - await assert_validation_result(client, {}) - - # Statistics has run, device class set - expect error - do_adhoc_statistics(hass, start=now) - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", 12, attributes=attributes) - await hass.async_block_till_done() - expected = { - "sensor.test": [ - { - "data": { - "device_class": attributes["device_class"], - "metadata_unit": "kW", - "statistic_id": "sensor.test", - "supported_unit": unit, - }, - "type": "unsupported_unit_metadata_can_convert", - } - ], - } - await assert_validation_result(client, expected) - - # Invalid state too, expect double errors - hass.states.async_set( - "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": "dogs"}} - ) - await async_recorder_block_till_done(hass) - expected = { - "sensor.test": [ - { - "data": { - "device_class": attributes["device_class"], - "metadata_unit": "kW", - "statistic_id": "sensor.test", - "supported_unit": unit, - }, - "type": "unsupported_unit_metadata_can_convert", - }, - { - "data": { - "device_class": attributes["device_class"], - "state_unit": "dogs", - "statistic_id": "sensor.test", - }, - "type": "unsupported_unit_state", - }, - ], - } - await assert_validation_result(client, expected) - - @pytest.mark.parametrize( "units, attributes, unit", [