From cdbaae45ba90796badb3c49c4d4cb10695843c89 Mon Sep 17 00:00:00 2001 From: sarankumar <120080287+sarankumar1325@users.noreply.github.com> Date: Sun, 25 Oct 2026 10:40:48 +0530 Subject: [PATCH] fix(elecprice): handle mixed UTC offsets in Tibber prices around DST (#1448) Once Tibber publishes prices across a DST change, the series holds timestamps with two UTC offsets and pd.to_datetime() raises "Mixed timezones detected", so every optimization run is canceled. Parse via UTC and convert back to the configured timezone. Fixes #1446 --- .../prediction/elecpricetibber.py | 6 +++++- tests/test_elecpricetibber.py | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/akkudoktoreos/prediction/elecpricetibber.py b/src/akkudoktoreos/prediction/elecpricetibber.py index 8070116c..b7c17d51 100644 --- a/src/akkudoktoreos/prediction/elecpricetibber.py +++ b/src/akkudoktoreos/prediction/elecpricetibber.py @@ -317,7 +317,11 @@ class ElecPriceTibber(ElecPriceProvider): if series.empty: return series series = series.sort_index() - series.index = pd.to_datetime([to_datetime(index).isoformat() for index in series.index]) + # Parse via UTC: around a DST change the series mixes UTC offsets + # (e.g. +02:00 and +01:00), which pd.to_datetime() refuses to combine. + index = pd.to_datetime([to_datetime(index).isoformat() for index in series.index], utc=True) + timezone = self.config.general.timezone + series.index = index.tz_convert(timezone) if timezone else index series = series.groupby(level=0).mean().sort_index() return series.dropna() diff --git a/tests/test_elecpricetibber.py b/tests/test_elecpricetibber.py index 18129168..f963c11e 100644 --- a/tests/test_elecpricetibber.py +++ b/tests/test_elecpricetibber.py @@ -217,6 +217,26 @@ class TestElecPriceTibber: assert normalized.iloc[1] == pytest.approx(0.50) + def test_tibber_normalize_series_handles_dst_change(self, provider, set_other_timezone): + """Prices spanning a DST change (mixed UTC offsets) are normalized, not rejected.""" + set_other_timezone("Europe/Berlin") + index = [ + to_datetime("2026-10-25T02:45:00+02:00", in_timezone="Europe/Berlin"), + to_datetime("2026-10-25T02:00:00+01:00", in_timezone="Europe/Berlin"), + to_datetime("2026-10-25T02:15:00+01:00", in_timezone="Europe/Berlin"), + ] + series = pd.Series([0.10, 0.20, 0.30], index=index) + + normalized = provider._normalize_series(series) + + assert normalized.tolist() == pytest.approx([0.10, 0.20, 0.30]) + assert [ts.timestamp() for ts in normalized.index] == [ + to_datetime("2026-10-25T00:45:00+00:00").timestamp(), + to_datetime("2026-10-25T01:00:00+00:00").timestamp(), + to_datetime("2026-10-25T01:15:00+00:00").timestamp(), + ] + assert provider._resolution_seconds(normalized) == 900 + def test_empty_tomorrow_stores_only_today_and_warns(self, provider): """An empty tomorrow list does not create fake values before forecasting.""" response = TibberGraphQLResponse.model_validate(