mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-10-29 09:11:57 +00:00
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
This commit is contained in:
@@ -317,7 +317,11 @@ class ElecPriceTibber(ElecPriceProvider):
|
|||||||
if series.empty:
|
if series.empty:
|
||||||
return series
|
return series
|
||||||
series = series.sort_index()
|
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()
|
series = series.groupby(level=0).mean().sort_index()
|
||||||
return series.dropna()
|
return series.dropna()
|
||||||
|
|
||||||
|
|||||||
@@ -217,6 +217,26 @@ class TestElecPriceTibber:
|
|||||||
assert normalized.iloc[1] == pytest.approx(0.50)
|
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):
|
def test_empty_tomorrow_stores_only_today_and_warns(self, provider):
|
||||||
"""An empty tomorrow list does not create fake values before forecasting."""
|
"""An empty tomorrow list does not create fake values before forecasting."""
|
||||||
response = TibberGraphQLResponse.model_validate(
|
response = TibberGraphQLResponse.model_validate(
|
||||||
|
|||||||
Reference in New Issue
Block a user