From 8175dab7ab7e1b9ecf160fe6b33b93e62e6f999c Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 25 Oct 2022 20:07:28 +0200 Subject: [PATCH] Add week period to recorder statistics api (#80784) * add week period to get statistics api * add test --- .../components/recorder/statistics.py | 36 ++++- .../components/recorder/websocket_api.py | 4 +- tests/components/recorder/test_statistics.py | 140 +++++++++++++++++- 3 files changed, 175 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 88638a6ccc7..2b249aeeb14 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -1014,6 +1014,35 @@ def _reduce_statistics_per_day( return _reduce_statistics(stats, same_day, day_start_end, timedelta(days=1)) +def same_week(time1: datetime, time2: datetime) -> bool: + """Return True if time1 and time2 are in the same year and week.""" + date1 = dt_util.as_local(time1).date() + date2 = dt_util.as_local(time2).date() + return (date1.year, date1.isocalendar().week) == ( + date2.year, + date2.isocalendar().week, + ) + + +def week_start_end(time: datetime) -> tuple[datetime, datetime]: + """Return the start and end of the period (week) time is within.""" + time_local = dt_util.as_local(time) + start_local = time_local.replace( + hour=0, minute=0, second=0, microsecond=0 + ) - timedelta(days=time_local.weekday()) + start = dt_util.as_utc(start_local) + end = dt_util.as_utc(start_local + timedelta(days=7)) + return (start, end) + + +def _reduce_statistics_per_week( + stats: dict[str, list[dict[str, Any]]], +) -> dict[str, list[dict[str, Any]]]: + """Reduce hourly statistics to weekly statistics.""" + + return _reduce_statistics(stats, same_week, week_start_end, timedelta(days=7)) + + def same_month(time1: datetime, time2: datetime) -> bool: """Return True if time1 and time2 are in the same year and month.""" date1 = dt_util.as_local(time1).date() @@ -1089,7 +1118,7 @@ def statistics_during_period( start_time: datetime, end_time: datetime | None = None, statistic_ids: list[str] | None = None, - period: Literal["5minute", "day", "hour", "month"] = "hour", + period: Literal["5minute", "day", "hour", "week", "month"] = "hour", start_time_as_datetime: bool = False, units: dict[str, str] | None = None, ) -> dict[str, list[dict[str, Any]]]: @@ -1122,7 +1151,7 @@ def statistics_during_period( if not stats: return {} # Return statistics combined with metadata - if period not in ("day", "month"): + if period not in ("day", "week", "month"): return _sorted_statistics_to_dict( hass, session, @@ -1152,6 +1181,9 @@ def statistics_during_period( if period == "day": return _reduce_statistics_per_day(result) + if period == "week": + return _reduce_statistics_per_week(result) + return _reduce_statistics_per_month(result) diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 37d117c910e..2079d9537b5 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -62,7 +62,7 @@ def _ws_get_statistics_during_period( start_time: dt, end_time: dt | None, statistic_ids: list[str] | None, - period: Literal["5minute", "day", "hour", "month"], + period: Literal["5minute", "day", "hour", "week", "month"], units: dict[str, str], ) -> str: """Fetch statistics and convert them to json in the executor.""" @@ -118,7 +118,7 @@ async def ws_handle_get_statistics_during_period( vol.Required("start_time"): str, vol.Optional("end_time"): str, vol.Optional("statistic_ids"): [str], - vol.Required("period"): vol.Any("5minute", "hour", "day", "month"), + vol.Required("period"): vol.Any("5minute", "hour", "day", "week", "month"), vol.Optional("units"): vol.Schema( { vol.Optional("distance"): vol.In(DistanceConverter.VALID_UNITS), diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index fb5cf8dba8b..aae6fcf91cb 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -885,10 +885,148 @@ def test_import_statistics_errors(hass_recorder, caplog): assert get_metadata(hass, statistic_ids=("sensor.total_energy_import",)) == {} +@pytest.mark.parametrize("timezone", ["America/Regina", "Europe/Vienna", "UTC"]) +@pytest.mark.freeze_time("2022-10-01 00:00:00+00:00") +def test_weekly_statistics(hass_recorder, caplog, timezone): + """Test weekly statistics.""" + dt_util.set_default_time_zone(dt_util.get_time_zone(timezone)) + + hass = hass_recorder() + wait_recording_done(hass) + assert "Compiling statistics for" not in caplog.text + assert "Statistics already compiled" not in caplog.text + + zero = dt_util.utcnow() + period1 = dt_util.as_utc(dt_util.parse_datetime("2022-10-03 00:00:00")) + period2 = dt_util.as_utc(dt_util.parse_datetime("2022-10-09 23:00:00")) + period3 = dt_util.as_utc(dt_util.parse_datetime("2022-10-10 00:00:00")) + period4 = dt_util.as_utc(dt_util.parse_datetime("2022-10-16 23:00:00")) + + external_statistics = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 2, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 3, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 4, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 5, + }, + ) + external_metadata = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import", + "unit_of_measurement": "kWh", + } + + async_add_external_statistics(hass, external_metadata, external_statistics) + wait_recording_done(hass) + stats = statistics_during_period(hass, zero, period="week") + week1_start = dt_util.as_utc(dt_util.parse_datetime("2022-10-03 00:00:00")) + week1_end = dt_util.as_utc(dt_util.parse_datetime("2022-10-10 00:00:00")) + week2_start = dt_util.as_utc(dt_util.parse_datetime("2022-10-10 00:00:00")) + week2_end = dt_util.as_utc(dt_util.parse_datetime("2022-10-17 00:00:00")) + assert stats == { + "test:total_energy_import": [ + { + "statistic_id": "test:total_energy_import", + "start": week1_start.isoformat(), + "end": week1_end.isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": 1.0, + "sum": 3.0, + }, + { + "statistic_id": "test:total_energy_import", + "start": week2_start.isoformat(), + "end": week2_end.isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": 3.0, + "sum": 5.0, + }, + ] + } + + stats = statistics_during_period( + hass, + start_time=zero, + statistic_ids=["not", "the", "same", "test:total_energy_import"], + period="week", + ) + assert stats == { + "test:total_energy_import": [ + { + "statistic_id": "test:total_energy_import", + "start": week1_start.isoformat(), + "end": week1_end.isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": 1.0, + "sum": 3.0, + }, + { + "statistic_id": "test:total_energy_import", + "start": week2_start.isoformat(), + "end": week2_end.isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": 3.0, + "sum": 5.0, + }, + ] + } + + # Use 5minute to ensure table switch works + stats = statistics_during_period( + hass, + start_time=zero, + statistic_ids=["test:total_energy_import", "with_other"], + period="5minute", + ) + assert stats == {} + + # Ensure future date has not data + future = dt_util.as_utc(dt_util.parse_datetime("2221-11-01 00:00:00")) + stats = statistics_during_period( + hass, start_time=future, end_time=future, period="month" + ) + assert stats == {} + + dt_util.set_default_time_zone(dt_util.get_time_zone("UTC")) + + @pytest.mark.parametrize("timezone", ["America/Regina", "Europe/Vienna", "UTC"]) @pytest.mark.freeze_time("2021-08-01 00:00:00+00:00") def test_monthly_statistics(hass_recorder, caplog, timezone): - """Test inserting external statistics.""" + """Test monthly statistics.""" dt_util.set_default_time_zone(dt_util.get_time_zone(timezone)) hass = hass_recorder()