Skip to content

Commit db6f46f

Browse files
committed
Handle prices with UTC timestamps in the raw data for non-FI regions
1 parent 4e0bc07 commit db6f46f

2 files changed

Lines changed: 58 additions & 13 deletions

File tree

spothinta_api/models.py

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -257,16 +257,26 @@ def prices_today(self) -> dict[datetime, float]:
257257
The prices for today.
258258
259259
"""
260-
today = self.now_in_timezone().astimezone().date()
260+
today = self.now_in_timezone().date()
261261
prices = {
262262
timestamp: price
263263
for timestamp, price in self.prices.items()
264-
if timestamp.date() == today
264+
if timestamp.astimezone(self.time_zone).date() == today
265265
}
266266

267-
if (self.resolution == timedelta(minutes=15) and len(prices) == 96) or (
268-
self.resolution == timedelta(minutes=60) and len(prices) == 24
269-
):
267+
# Use midnight-to-midnight span to avoid off-by-one from 23:59:59.999999.
268+
day_start = datetime.combine(today, datetime.min.time(), tzinfo=self.time_zone)
269+
next_day_start = datetime.combine(
270+
today + timedelta(days=1),
271+
datetime.min.time(),
272+
tzinfo=self.time_zone,
273+
)
274+
expected_intervals = max(
275+
1,
276+
int((next_day_start - day_start) / self.resolution),
277+
)
278+
279+
if len(prices) == expected_intervals:
270280
return prices
271281

272282
return {}
@@ -279,16 +289,30 @@ def prices_tomorrow(self) -> dict[datetime, float]:
279289
The prices for tomorrow.
280290
281291
"""
282-
tomorrow = (self.now_in_timezone() + timedelta(days=1)).astimezone().date()
292+
tomorrow = (self.now_in_timezone() + timedelta(days=1)).date()
283293
prices = {
284294
timestamp: price
285295
for timestamp, price in self.prices.items()
286-
if timestamp.date() == tomorrow
296+
if timestamp.astimezone(self.time_zone).date() == tomorrow
287297
}
288298

289-
if (self.resolution == timedelta(minutes=15) and len(prices) == 96) or (
290-
self.resolution == timedelta(minutes=60) and len(prices) == 24
291-
):
299+
# Use midnight-to-midnight span to avoid off-by-one from 23:59:59.999999.
300+
day_start = datetime.combine(
301+
tomorrow,
302+
datetime.min.time(),
303+
tzinfo=self.time_zone,
304+
)
305+
next_day_start = datetime.combine(
306+
tomorrow + timedelta(days=1),
307+
datetime.min.time(),
308+
tzinfo=self.time_zone,
309+
)
310+
expected_intervals = max(
311+
1,
312+
int((next_day_start - day_start) / self.resolution),
313+
)
314+
315+
if len(prices) == expected_intervals:
292316
return prices
293317

294318
return {}

tests/test_models.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Test the models."""
22

33
from datetime import datetime, timedelta, timezone
4+
from zoneinfo import ZoneInfo
45

56
import pytest
67
from aiohttp import ClientSession
@@ -140,10 +141,10 @@ async def test_model_se1_15_minute_resolution(aresponses: ResponsesMockServer) -
140141
assert energy.highest_price_tomorrow == 0.0059
141142
assert energy.lowest_price_today == 0.00001
142143
assert energy.lowest_price_tomorrow == 0.00005
143-
assert energy.average_price_today == 0.00113
144-
assert energy.average_price_tomorrow == 0.00249
144+
assert energy.average_price_today == 0.00121
145+
assert energy.average_price_tomorrow == 0.00232
145146
assert energy.current_price == 0.00128
146-
assert energy.intervals_priced_equal_or_lower == 57
147+
assert energy.intervals_priced_equal_or_lower == 51
147148
# The price for another interval
148149
another_interval = datetime(2025, 10, 4, 18, 0, tzinfo=timezone.utc)
149150
assert energy.price_at_time(another_interval) == 0.00242
@@ -166,6 +167,26 @@ async def test_model_se1_15_minute_resolution(aresponses: ResponsesMockServer) -
166167
assert isinstance(energy.timestamp_prices, list)
167168

168169

170+
@pytest.mark.freeze_time("2025-10-04 16:00:00+02:00")
171+
async def test_prices_today_uses_region_local_date_for_utc_timestamps() -> None:
172+
"""Use local date boundaries for SE1 even when source timestamps are UTC."""
173+
start_utc = datetime(2025, 10, 3, 22, 0, tzinfo=timezone.utc)
174+
prices = {start_utc + i * timedelta(minutes=15): i / 100000 for i in range(96)}
175+
energy = Electricity(
176+
prices=prices,
177+
resolution=timedelta(minutes=15),
178+
time_zone=ZoneInfo("Europe/Stockholm"),
179+
)
180+
181+
prices_today = energy.prices_today()
182+
assert len(prices_today) == 96
183+
assert energy.current_price == 0.00064
184+
assert energy.lowest_price_today == 0.0
185+
assert energy.highest_price_today == 0.00095
186+
assert energy.average_price_today == 0.00047
187+
assert energy.prices_tomorrow() == {}
188+
189+
169190
@pytest.mark.freeze_time("2023-05-06 15:00:00+03:00")
170191
async def test_model_no_prices_for_tomorrow(aresponses: ResponsesMockServer) -> None:
171192
"""Test the model for usage at 15:00:00 UTC+3 with no prices for tomorrow."""

0 commit comments

Comments
 (0)