diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index e33f2e62da2..5132dfc72bb 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import datetime import json import logging -from typing import TypedDict +from typing import TypedDict, overload from sqlalchemy import ( Boolean, @@ -391,6 +391,16 @@ class StatisticsRuns(Base): # type: ignore ) +@overload +def process_timestamp(ts: None) -> None: + ... + + +@overload +def process_timestamp(ts: datetime) -> datetime: + ... + + def process_timestamp(ts): """Process a timestamp into datetime object.""" if ts is None: @@ -401,6 +411,16 @@ def process_timestamp(ts): return dt_util.as_utc(ts) +@overload +def process_timestamp_to_utc_isoformat(ts: None) -> None: + ... + + +@overload +def process_timestamp_to_utc_isoformat(ts: datetime) -> str: + ... + + def process_timestamp_to_utc_isoformat(ts: datetime | None) -> str | None: """Process a timestamp into UTC isotime.""" if ts is None: diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index c8f4e48563c..c1b924ceeec 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -32,6 +32,7 @@ from .models import ( Statistics, StatisticsMeta, StatisticsRuns, + process_timestamp, process_timestamp_to_utc_isoformat, ) from .util import execute, retryable_database_job, session_scope @@ -437,9 +438,6 @@ def _sorted_statistics_to_dict( for stat_id in statistic_ids: result[stat_id] = [] - # Called in a tight loop so cache the function here - _process_timestamp_to_utc_isoformat = process_timestamp_to_utc_isoformat - # Append all statistic entries, and do unit conversion for meta_id, group in groupby(stats, lambda stat: stat.metadata_id): # type: ignore unit = metadata[meta_id]["unit_of_measurement"] @@ -450,21 +448,26 @@ def _sorted_statistics_to_dict( else: convert = no_conversion ent_results = result[meta_id] - ent_results.extend( - { - "statistic_id": statistic_id, - "start": _process_timestamp_to_utc_isoformat(db_state.start), - "mean": convert(db_state.mean, units), - "min": convert(db_state.min, units), - "max": convert(db_state.max, units), - "last_reset": _process_timestamp_to_utc_isoformat(db_state.last_reset), - "state": convert(db_state.state, units), - "sum": (_sum := convert(db_state.sum, units)), - "sum_increase": (inc := convert(db_state.sum_increase, units)), - "sum_decrease": None if _sum is None or inc is None else inc - _sum, - } - for db_state in group - ) + for db_state in group: + start = process_timestamp(db_state.start) + end = start + timedelta(hours=1) + ent_results.append( + { + "statistic_id": statistic_id, + "start": start.isoformat(), + "end": end.isoformat(), + "mean": convert(db_state.mean, units), + "min": convert(db_state.min, units), + "max": convert(db_state.max, units), + "last_reset": process_timestamp_to_utc_isoformat( + db_state.last_reset + ), + "state": convert(db_state.state, units), + "sum": (_sum := convert(db_state.sum, units)), + "sum_increase": (inc := convert(db_state.sum_increase, units)), + "sum_decrease": None if _sum is None or inc is None else inc - _sum, + } + ) # Filter out the empty lists if some states had 0 results. return {metadata[key]["statistic_id"]: val for key, val in result.items() if val} diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 27c2024750c..b237659d528 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -908,6 +908,7 @@ async def test_statistics_during_period( { "statistic_id": "sensor.test", "start": now.isoformat(), + "end": (now + timedelta(hours=1)).isoformat(), "mean": approx(value), "min": approx(value), "max": approx(value), diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 2434f8b4703..1e723e7e2ca 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -45,6 +45,7 @@ def test_compile_hourly_statistics(hass_recorder): expected_1 = { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), "mean": approx(14.915254237288135), "min": approx(10.0), "max": approx(20.0), @@ -57,6 +58,7 @@ def test_compile_hourly_statistics(hass_recorder): expected_2 = { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(four), + "end": process_timestamp_to_utc_isoformat(four + timedelta(hours=1)), "mean": approx(20.0), "min": approx(20.0), "max": approx(20.0), @@ -164,6 +166,7 @@ def test_compile_hourly_statistics_exception( expected_1 = { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(now), + "end": process_timestamp_to_utc_isoformat(now + timedelta(hours=1)), "mean": None, "min": None, "max": None, @@ -176,6 +179,7 @@ def test_compile_hourly_statistics_exception( expected_2 = { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(now + timedelta(hours=1)), + "end": process_timestamp_to_utc_isoformat(now + timedelta(hours=2)), "mean": None, "min": None, "max": None, @@ -235,6 +239,7 @@ def test_rename_entity(hass_recorder): expected_1 = { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), "mean": approx(14.915254237288135), "min": approx(10.0), "max": approx(20.0), diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 6108f4a7ef8..74adf717e5b 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -95,6 +95,7 @@ def test_compile_hourly_statistics( { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), "mean": approx(mean), "min": approx(min), "max": approx(max), @@ -159,6 +160,7 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), "mean": approx(13.050847), "min": approx(-10.0), "max": approx(30.0), @@ -173,6 +175,7 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes { "statistic_id": "sensor.test6", "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), "mean": approx(13.050847), "min": approx(-10.0), "max": approx(30.0), @@ -187,6 +190,7 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes { "statistic_id": "sensor.test7", "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), "mean": approx(13.050847), "min": approx(-10.0), "max": approx(30.0), @@ -260,6 +264,7 @@ def test_compile_hourly_sum_statistics_amount( { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), "max": None, "mean": None, "min": None, @@ -272,6 +277,7 @@ def test_compile_hourly_sum_statistics_amount( { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), "max": None, "mean": None, "min": None, @@ -284,6 +290,7 @@ def test_compile_hourly_sum_statistics_amount( { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=3)), "max": None, "mean": None, "min": None, @@ -360,6 +367,7 @@ def test_compile_hourly_sum_statistics_amount_reset_every_state_change( { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), "max": None, "mean": None, "min": None, @@ -426,6 +434,7 @@ def test_compile_hourly_sum_statistics_nan_inf_state( { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), "max": None, "mean": None, "min": None, @@ -492,6 +501,7 @@ def test_compile_hourly_sum_statistics_total_no_reset( { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), "max": None, "mean": None, "min": None, @@ -504,6 +514,7 @@ def test_compile_hourly_sum_statistics_total_no_reset( { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), "max": None, "mean": None, "min": None, @@ -516,6 +527,7 @@ def test_compile_hourly_sum_statistics_total_no_reset( { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=3)), "max": None, "mean": None, "min": None, @@ -578,6 +590,7 @@ def test_compile_hourly_sum_statistics_total_increasing( { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), "max": None, "mean": None, "min": None, @@ -590,6 +603,7 @@ def test_compile_hourly_sum_statistics_total_increasing( { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), "max": None, "mean": None, "min": None, @@ -602,6 +616,7 @@ def test_compile_hourly_sum_statistics_total_increasing( { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=3)), "max": None, "mean": None, "min": None, @@ -675,6 +690,7 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip( "last_reset": None, "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), "max": None, "mean": None, "min": None, @@ -687,6 +703,7 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip( "last_reset": None, "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), "max": None, "mean": None, "min": None, @@ -699,6 +716,7 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip( "last_reset": None, "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=3)), "max": None, "mean": None, "min": None, @@ -767,6 +785,7 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), "max": None, "mean": None, "min": None, @@ -779,6 +798,7 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), "max": None, "mean": None, "min": None, @@ -791,6 +811,7 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=3)), "max": None, "mean": None, "min": None, @@ -856,6 +877,7 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), "max": None, "mean": None, "min": None, @@ -868,6 +890,7 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), "max": None, "mean": None, "min": None, @@ -880,6 +903,7 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=3)), "max": None, "mean": None, "min": None, @@ -894,6 +918,7 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): { "statistic_id": "sensor.test2", "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), "max": None, "mean": None, "min": None, @@ -906,6 +931,7 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): { "statistic_id": "sensor.test2", "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), "max": None, "mean": None, "min": None, @@ -918,6 +944,7 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): { "statistic_id": "sensor.test2", "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=3)), "max": None, "mean": None, "min": None, @@ -932,6 +959,7 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): { "statistic_id": "sensor.test3", "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), "max": None, "mean": None, "min": None, @@ -944,6 +972,7 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): { "statistic_id": "sensor.test3", "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), "max": None, "mean": None, "min": None, @@ -956,6 +985,7 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): { "statistic_id": "sensor.test3", "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=3)), "max": None, "mean": None, "min": None, @@ -1011,6 +1041,7 @@ def test_compile_hourly_statistics_unchanged( { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(four), + "end": process_timestamp_to_utc_isoformat(four + timedelta(hours=1)), "mean": approx(value), "min": approx(value), "max": approx(value), @@ -1045,6 +1076,7 @@ def test_compile_hourly_statistics_partially_unavailable(hass_recorder, caplog): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), "mean": approx(21.1864406779661), "min": approx(10.0), "max": approx(25.0), @@ -1104,6 +1136,7 @@ def test_compile_hourly_statistics_unavailable( { "statistic_id": "sensor.test2", "start": process_timestamp_to_utc_isoformat(four), + "end": process_timestamp_to_utc_isoformat(four + timedelta(hours=1)), "mean": approx(value), "min": approx(value), "max": approx(value), @@ -1256,6 +1289,7 @@ def test_compile_hourly_statistics_changing_units_1( { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), "mean": approx(mean), "min": approx(min), "max": approx(max), @@ -1284,6 +1318,7 @@ def test_compile_hourly_statistics_changing_units_1( { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), "mean": approx(mean), "min": approx(min), "max": approx(max), @@ -1391,6 +1426,7 @@ def test_compile_hourly_statistics_changing_units_3( { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), "mean": approx(mean), "min": approx(min), "max": approx(max), @@ -1417,6 +1453,7 @@ def test_compile_hourly_statistics_changing_units_3( { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), "mean": approx(mean), "min": approx(min), "max": approx(max), @@ -1497,6 +1534,7 @@ def test_compile_hourly_statistics_changing_statistics( { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), "mean": approx(mean), "min": approx(min), "max": approx(max), @@ -1509,6 +1547,7 @@ def test_compile_hourly_statistics_changing_statistics( { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), "mean": None, "min": None, "max": None,