From a8c794a9753c7560f762ddf11df9952dd6bd6d75 Mon Sep 17 00:00:00 2001 From: Pete Date: Tue, 3 Dec 2024 12:31:14 +0100 Subject: [PATCH 1/5] FIX: make "year" period work in leap year --- homeassistant/components/recorder/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index a59519ef38dfa7..125b354211eb5d 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -902,7 +902,7 @@ def resolve_period( start_time = (start_time + timedelta(days=cal_offset * 366)).replace( month=1, day=1 ) - end_time = (start_time + timedelta(days=365)).replace(day=1) + end_time = (start_time + timedelta(days=366)).replace(day=1) start_time = dt_util.as_utc(start_time) end_time = dt_util.as_utc(end_time) From 4aaba54ddb7040ca0d05c5b518a410244a85ba23 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 4 Dec 2024 10:48:39 +0100 Subject: [PATCH 2/5] Add test --- tests/components/recorder/test_util.py | 92 ++++++++++++++++++-------- 1 file changed, 66 insertions(+), 26 deletions(-) diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 4904bdecc4d9a9..d23f2096aea29f 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -9,6 +9,7 @@ from typing import Any from unittest.mock import MagicMock, Mock, patch +from freezegun.api import FrozenDateTimeFactory import pytest from sqlalchemy import lambda_stmt, text from sqlalchemy.engine.result import ChunkedIteratorResult @@ -1052,55 +1053,94 @@ def all(self): assert rows == ["mock_row"] -@pytest.mark.freeze_time(datetime(2022, 10, 21, 7, 25, tzinfo=UTC)) -async def test_resolve_period(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("start_time", "periods"), + [ + ( + # Test 00:25 local time, during DST + datetime(2022, 10, 21, 7, 25, tzinfo=UTC), + { + "hour": ["2022-10-21T07:00:00+00:00", "2022-10-21T08:00:00+00:00"], + "hour-1": ["2022-10-21T06:00:00+00:00", "2022-10-21T07:00:00+00:00"], + "day": ["2022-10-21T07:00:00+00:00", "2022-10-22T07:00:00+00:00"], + "day-1": ["2022-10-20T07:00:00+00:00", "2022-10-21T07:00:00+00:00"], + "week": ["2022-10-17T07:00:00+00:00", "2022-10-24T07:00:00+00:00"], + "week-1": ["2022-10-10T07:00:00+00:00", "2022-10-17T07:00:00+00:00"], + "month": ["2022-10-01T07:00:00+00:00", "2022-11-01T07:00:00+00:00"], + "month-1": ["2022-09-01T07:00:00+00:00", "2022-10-01T07:00:00+00:00"], + "year": ["2022-01-01T08:00:00+00:00", "2023-01-01T08:00:00+00:00"], + "year-1": ["2021-01-01T08:00:00+00:00", "2022-01-01T08:00:00+00:00"], + }, + ), + ( + # Test 00:25 local time, standard time, February 28th a leap year + datetime(2024, 2, 28, 8, 25, tzinfo=UTC), + { + "hour": ["2024-02-28T08:00:00+00:00", "2024-02-28T09:00:00+00:00"], + "hour-1": ["2024-02-28T07:00:00+00:00", "2024-02-28T08:00:00+00:00"], + "day": ["2024-02-28T08:00:00+00:00", "2024-02-29T08:00:00+00:00"], + "day-1": ["2024-02-27T08:00:00+00:00", "2024-02-28T08:00:00+00:00"], + "week": ["2024-02-26T08:00:00+00:00", "2024-03-04T08:00:00+00:00"], + "week-1": ["2024-02-19T08:00:00+00:00", "2024-02-26T08:00:00+00:00"], + "month": ["2024-02-01T08:00:00+00:00", "2024-03-01T08:00:00+00:00"], + "month-1": ["2024-01-01T08:00:00+00:00", "2024-02-01T08:00:00+00:00"], + "year": ["2024-01-01T08:00:00+00:00", "2025-01-01T08:00:00+00:00"], + "year-1": ["2023-01-01T08:00:00+00:00", "2024-01-01T08:00:00+00:00"], + }, + ), + ], +) +async def test_resolve_period( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + start_time: datetime, + periods: dict[str, tuple[str, str]], +) -> None: """Test statistic_during_period.""" + assert hass.config.time_zone == "US/Pacific" + freezer.move_to(start_time) now = dt_util.utcnow() start_t, end_t = resolve_period({"calendar": {"period": "hour"}}) - assert start_t.isoformat() == "2022-10-21T07:00:00+00:00" - assert end_t.isoformat() == "2022-10-21T08:00:00+00:00" - - start_t, end_t = resolve_period({"calendar": {"period": "hour"}}) - assert start_t.isoformat() == "2022-10-21T07:00:00+00:00" - assert end_t.isoformat() == "2022-10-21T08:00:00+00:00" + assert start_t.isoformat() == periods["hour"][0] + assert end_t.isoformat() == periods["hour"][1] start_t, end_t = resolve_period({"calendar": {"period": "hour", "offset": -1}}) - assert start_t.isoformat() == "2022-10-21T06:00:00+00:00" - assert end_t.isoformat() == "2022-10-21T07:00:00+00:00" + assert start_t.isoformat() == periods["hour-1"][0] + assert end_t.isoformat() == periods["hour-1"][1] start_t, end_t = resolve_period({"calendar": {"period": "day"}}) - assert start_t.isoformat() == "2022-10-21T07:00:00+00:00" - assert end_t.isoformat() == "2022-10-22T07:00:00+00:00" + assert start_t.isoformat() == periods["day"][0] + assert end_t.isoformat() == periods["day"][1] start_t, end_t = resolve_period({"calendar": {"period": "day", "offset": -1}}) - assert start_t.isoformat() == "2022-10-20T07:00:00+00:00" - assert end_t.isoformat() == "2022-10-21T07:00:00+00:00" + assert start_t.isoformat() == periods["day-1"][0] + assert end_t.isoformat() == periods["day-1"][1] start_t, end_t = resolve_period({"calendar": {"period": "week"}}) - assert start_t.isoformat() == "2022-10-17T07:00:00+00:00" - assert end_t.isoformat() == "2022-10-24T07:00:00+00:00" + assert start_t.isoformat() == periods["week"][0] + assert end_t.isoformat() == periods["week"][1] start_t, end_t = resolve_period({"calendar": {"period": "week", "offset": -1}}) - assert start_t.isoformat() == "2022-10-10T07:00:00+00:00" - assert end_t.isoformat() == "2022-10-17T07:00:00+00:00" + assert start_t.isoformat() == periods["week-1"][0] + assert end_t.isoformat() == periods["week-1"][1] start_t, end_t = resolve_period({"calendar": {"period": "month"}}) - assert start_t.isoformat() == "2022-10-01T07:00:00+00:00" - assert end_t.isoformat() == "2022-11-01T07:00:00+00:00" + assert start_t.isoformat() == periods["month"][0] + assert end_t.isoformat() == periods["month"][1] start_t, end_t = resolve_period({"calendar": {"period": "month", "offset": -1}}) - assert start_t.isoformat() == "2022-09-01T07:00:00+00:00" - assert end_t.isoformat() == "2022-10-01T07:00:00+00:00" + assert start_t.isoformat() == periods["month-1"][0] + assert end_t.isoformat() == periods["month-1"][1] start_t, end_t = resolve_period({"calendar": {"period": "year"}}) - assert start_t.isoformat() == "2022-01-01T08:00:00+00:00" - assert end_t.isoformat() == "2023-01-01T08:00:00+00:00" + assert start_t.isoformat() == periods["year"][0] + assert end_t.isoformat() == periods["year"][1] start_t, end_t = resolve_period({"calendar": {"period": "year", "offset": -1}}) - assert start_t.isoformat() == "2021-01-01T08:00:00+00:00" - assert end_t.isoformat() == "2022-01-01T08:00:00+00:00" + assert start_t.isoformat() == periods["year-1"][0] + assert end_t.isoformat() == periods["year-1"][1] # Fixed period assert resolve_period({}) == (None, None) From 542f849532672336454285d4a4e34b4e89f463aa Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 4 Dec 2024 10:58:19 +0100 Subject: [PATCH 3/5] Set second and microsecond to non-zero in test start times --- tests/components/recorder/test_util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index d23f2096aea29f..7b8eef6b16f96a 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -1058,7 +1058,7 @@ def all(self): [ ( # Test 00:25 local time, during DST - datetime(2022, 10, 21, 7, 25, tzinfo=UTC), + datetime(2022, 10, 21, 7, 25, 50, 123, tzinfo=UTC), { "hour": ["2022-10-21T07:00:00+00:00", "2022-10-21T08:00:00+00:00"], "hour-1": ["2022-10-21T06:00:00+00:00", "2022-10-21T07:00:00+00:00"], @@ -1074,7 +1074,7 @@ def all(self): ), ( # Test 00:25 local time, standard time, February 28th a leap year - datetime(2024, 2, 28, 8, 25, tzinfo=UTC), + datetime(2024, 2, 28, 8, 25, 50, 123, tzinfo=UTC), { "hour": ["2024-02-28T08:00:00+00:00", "2024-02-28T09:00:00+00:00"], "hour-1": ["2024-02-28T07:00:00+00:00", "2024-02-28T08:00:00+00:00"], From 06aba46ec6a0a1e944c88fe99d9bc6181a73cc1c Mon Sep 17 00:00:00 2001 From: Pete Date: Wed, 4 Dec 2024 11:59:34 +0100 Subject: [PATCH 4/5] FIX: better fix for leap year problem --- homeassistant/components/recorder/util.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 125b354211eb5d..a99d2d70b8b0c9 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -897,12 +897,8 @@ def resolve_period( start_time = (start_time + timedelta(days=cal_offset * 31)).replace(day=1) end_time = (start_time + timedelta(days=31)).replace(day=1) else: # calendar_period = "year" - start_time = start_of_day.replace(month=12, day=31) - # This works for 100+ years of offset - start_time = (start_time + timedelta(days=cal_offset * 366)).replace( - month=1, day=1 - ) - end_time = (start_time + timedelta(days=366)).replace(day=1) + start_time = start_of_day.replace(month=1, day=1, year=start_of_day.year + cal_offset) + end_time = start_time.replace(month=12, day=31) start_time = dt_util.as_utc(start_time) end_time = dt_util.as_utc(end_time) From d747ab9442028d674bbb46e855d23d1bdc67647b Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 4 Dec 2024 12:41:10 +0100 Subject: [PATCH 5/5] Revert "FIX: better fix for leap year problem" This reverts commit 06aba46ec6a0a1e944c88fe99d9bc6181a73cc1c. --- homeassistant/components/recorder/util.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index a99d2d70b8b0c9..125b354211eb5d 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -897,8 +897,12 @@ def resolve_period( start_time = (start_time + timedelta(days=cal_offset * 31)).replace(day=1) end_time = (start_time + timedelta(days=31)).replace(day=1) else: # calendar_period = "year" - start_time = start_of_day.replace(month=1, day=1, year=start_of_day.year + cal_offset) - end_time = start_time.replace(month=12, day=31) + start_time = start_of_day.replace(month=12, day=31) + # This works for 100+ years of offset + start_time = (start_time + timedelta(days=cal_offset * 366)).replace( + month=1, day=1 + ) + end_time = (start_time + timedelta(days=366)).replace(day=1) start_time = dt_util.as_utc(start_time) end_time = dt_util.as_utc(end_time)