diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 9db1e532f4c..589eaa21119 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -12,7 +12,7 @@ import logging import os import re from statistics import mean -from typing import TYPE_CHECKING, Any, Literal +from typing import TYPE_CHECKING, Any, Literal, overload from sqlalchemy import bindparam, func from sqlalchemy.exc import SQLAlchemyError, StatementError @@ -123,9 +123,9 @@ QUERY_STATISTIC_META_ID = [ STATISTICS_BAKERY = "recorder_statistics_bakery" -# Convert pressure and temperature statistics from the native unit used for statistics -# to the units configured by the user -UNIT_CONVERSIONS = { +# Convert pressure, temperature and volume statistics from the normalized unit used for +# statistics to the unit configured by the user +STATISTIC_UNIT_TO_DISPLAY_UNIT_CONVERSIONS = { PRESSURE_PA: lambda x, units: pressure_util.convert( x, PRESSURE_PA, units.pressure_unit ) @@ -143,6 +143,17 @@ UNIT_CONVERSIONS = { else None, } +# Convert volume statistics from the display unit configured by the user +# to the normalized unit used for statistics +# This is used to support adjusting statistics in the display unit +DISPLAY_UNIT_TO_STATISTIC_UNIT_CONVERSIONS: dict[ + str, Callable[[float, UnitSystem], float] +] = { + VOLUME_CUBIC_FEET: lambda x, units: volume_util.convert( + x, _configured_unit(VOLUME_CUBIC_METERS, units), VOLUME_CUBIC_METERS + ), +} + _LOGGER = logging.getLogger(__name__) @@ -717,7 +728,17 @@ def get_metadata( ) +@overload +def _configured_unit(unit: None, units: UnitSystem) -> None: + ... + + +@overload def _configured_unit(unit: str, units: UnitSystem) -> str: + ... + + +def _configured_unit(unit: str | None, units: UnitSystem) -> str | None: """Return the pressure and temperature units configured by the user.""" if unit == PRESSURE_PA: return units.pressure_unit @@ -1156,7 +1177,7 @@ def _sorted_statistics_to_dict( statistic_id = metadata[meta_id]["statistic_id"] convert: Callable[[Any, Any], float | None] if convert_units: - convert = UNIT_CONVERSIONS.get(unit, lambda x, units: x) # type: ignore[arg-type,no-any-return] + convert = STATISTIC_UNIT_TO_DISPLAY_UNIT_CONVERSIONS.get(unit, lambda x, units: x) # type: ignore[arg-type,no-any-return] else: convert = no_conversion ent_results = result[meta_id] @@ -1316,6 +1337,12 @@ def adjust_statistics( if statistic_id not in metadata: return True + units = instance.hass.config.units + statistic_unit = metadata[statistic_id][1]["unit_of_measurement"] + display_unit = _configured_unit(statistic_unit, units) + convert = DISPLAY_UNIT_TO_STATISTIC_UNIT_CONVERSIONS.get(display_unit, lambda x, units: x) # type: ignore[arg-type] + sum_adjustment = convert(sum_adjustment, units) + _adjust_sum_statistics( session, StatisticsShortTerm, diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 825aec7904c..f9f1c058626 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -325,20 +325,20 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes @pytest.mark.parametrize("state_class", ["total"]) @pytest.mark.parametrize( - "units,device_class,unit,display_unit,factor,factor2", + "units,device_class,unit,display_unit,factor", [ - (IMPERIAL_SYSTEM, "energy", "kWh", "kWh", 1, 1), - (IMPERIAL_SYSTEM, "energy", "Wh", "kWh", 1 / 1000, 1), - (IMPERIAL_SYSTEM, "monetary", "EUR", "EUR", 1, 1), - (IMPERIAL_SYSTEM, "monetary", "SEK", "SEK", 1, 1), - (IMPERIAL_SYSTEM, "gas", "m³", "ft³", 35.314666711, 35.314666711), - (IMPERIAL_SYSTEM, "gas", "ft³", "ft³", 1, 35.314666711), - (METRIC_SYSTEM, "energy", "kWh", "kWh", 1, 1), - (METRIC_SYSTEM, "energy", "Wh", "kWh", 1 / 1000, 1), - (METRIC_SYSTEM, "monetary", "EUR", "EUR", 1, 1), - (METRIC_SYSTEM, "monetary", "SEK", "SEK", 1, 1), - (METRIC_SYSTEM, "gas", "m³", "m³", 1, 1), - (METRIC_SYSTEM, "gas", "ft³", "m³", 0.0283168466, 1), + (IMPERIAL_SYSTEM, "energy", "kWh", "kWh", 1), + (IMPERIAL_SYSTEM, "energy", "Wh", "kWh", 1 / 1000), + (IMPERIAL_SYSTEM, "monetary", "EUR", "EUR", 1), + (IMPERIAL_SYSTEM, "monetary", "SEK", "SEK", 1), + (IMPERIAL_SYSTEM, "gas", "m³", "ft³", 35.314666711), + (IMPERIAL_SYSTEM, "gas", "ft³", "ft³", 1), + (METRIC_SYSTEM, "energy", "kWh", "kWh", 1), + (METRIC_SYSTEM, "energy", "Wh", "kWh", 1 / 1000), + (METRIC_SYSTEM, "monetary", "EUR", "EUR", 1), + (METRIC_SYSTEM, "monetary", "SEK", "SEK", 1), + (METRIC_SYSTEM, "gas", "m³", "m³", 1), + (METRIC_SYSTEM, "gas", "ft³", "m³", 0.0283168466), ], ) async def test_compile_hourly_sum_statistics_amount( @@ -351,7 +351,6 @@ async def test_compile_hourly_sum_statistics_amount( unit, display_unit, factor, - factor2, ): """Test compiling hourly statistics.""" period0 = dt_util.utcnow() @@ -480,8 +479,8 @@ async def test_compile_hourly_sum_statistics_amount( assert response["success"] await async_wait_recording_done_without_instance(hass) - expected_stats["sensor.test1"][1]["sum"] = approx(factor * 40.0 + factor2 * 100) - expected_stats["sensor.test1"][2]["sum"] = approx(factor * 70.0 + factor2 * 100) + expected_stats["sensor.test1"][1]["sum"] = approx(factor * 40.0 + 100) + expected_stats["sensor.test1"][2]["sum"] = approx(factor * 70.0 + 100) stats = statistics_during_period(hass, period0, period="5minute") assert stats == expected_stats @@ -499,8 +498,8 @@ async def test_compile_hourly_sum_statistics_amount( assert response["success"] await async_wait_recording_done_without_instance(hass) - expected_stats["sensor.test1"][1]["sum"] = approx(factor * 40.0 + factor2 * 100) - expected_stats["sensor.test1"][2]["sum"] = approx(factor * 70.0 - factor2 * 300) + expected_stats["sensor.test1"][1]["sum"] = approx(factor * 40.0 + 100) + expected_stats["sensor.test1"][2]["sum"] = approx(factor * 70.0 - 300) stats = statistics_during_period(hass, period0, period="5minute") assert stats == expected_stats