Skip to content

Commit 08157f1

Browse files
committed
Return correct value for 60 minute spot prices at all times
1 parent e28159b commit 08157f1

2 files changed

Lines changed: 70 additions & 3 deletions

File tree

spothinta_api/models.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,18 @@
1111
from zoneinfo import ZoneInfo
1212

1313

14-
def _timed_value(moment: datetime, prices: dict[datetime, float]) -> float | None:
14+
def _timed_value(
15+
moment: datetime,
16+
prices: dict[datetime, float],
17+
resolution: timedelta,
18+
) -> float | None:
1519
"""Return a function that returns a value at a specific time.
1620
1721
Args:
1822
----
1923
moment: The time to get the value for.
2024
prices: A dictionary with market prices.
25+
resolution: The time resolution of price intervals (default: 15 minutes).
2126
2227
Returns:
2328
-------
@@ -26,7 +31,7 @@ def _timed_value(moment: datetime, prices: dict[datetime, float]) -> float | Non
2631
"""
2732
value = None
2833
for timestamp, price in prices.items():
29-
future_dt = timestamp + timedelta(minutes=15)
34+
future_dt = timestamp + resolution
3035
if timestamp <= moment < future_dt:
3136
value = round(price, 5)
3237
return value
@@ -373,7 +378,7 @@ def price_at_time(self, moment: datetime) -> float | None:
373378
The price at the specified time.
374379
375380
"""
376-
value = _timed_value(moment, self.prices)
381+
value = _timed_value(moment, self.prices, self.resolution)
377382
if value is not None or value == 0:
378383
return value
379384
return None

tests/test_models.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,68 @@ async def test_model_dst_spring_forward_23h(aresponses: ResponsesMockServer) ->
486486
assert energy.average_price_today is not None
487487

488488

489+
@pytest.mark.freeze_time("2023-05-06 15:30:00+03:00")
490+
async def test_model_60_minute_resolution_non_interval_start(
491+
aresponses: ResponsesMockServer,
492+
) -> None:
493+
"""Test 60-minute resolution when queried at mid-interval time.
494+
495+
Regression test: price_at_time() and current_price should work correctly
496+
even when the queried moment is not at an interval start (e.g., 15:30
497+
within the 15:00-16:00 hour). The interval logic should check if moment
498+
falls within [start, start+resolution), using the actual resolution,
499+
not hardcoded 15 minutes.
500+
"""
501+
aresponses.add(
502+
"api.spot-hinta.fi",
503+
"/TodayAndDayForward",
504+
"GET",
505+
aresponses.Response(
506+
status=200,
507+
headers={"Content-Type": "application/json"},
508+
text=load_fixtures("energy.json"),
509+
),
510+
)
511+
async with ClientSession() as session:
512+
client = SpotHinta(session=session)
513+
energy: Electricity = await client.energy_prices()
514+
assert energy is not None
515+
assert isinstance(energy, Electricity)
516+
# At 15:30, we're in the 15:00-16:00 interval
517+
# Should return the price for 15:00 (0.062)
518+
assert energy.current_price == 0.062
519+
# Also test price_at_time at a non-interval-start time
520+
moment_15_30 = datetime(2023, 5, 6, 12, 30, tzinfo=timezone.utc)
521+
assert energy.price_at_time(moment_15_30) == 0.062
522+
523+
524+
@pytest.mark.freeze_time("2023-05-06 15:58:00+03:00")
525+
async def test_model_60_minute_resolution_late_in_interval(
526+
aresponses: ResponsesMockServer,
527+
) -> None:
528+
"""Test 60-minute resolution at late moment in interval.
529+
530+
Regression test: verifies current_price works correctly at 15:58
531+
(late in the 15:00-16:00 hour), not just at mid-interval times.
532+
"""
533+
aresponses.add(
534+
"api.spot-hinta.fi",
535+
"/TodayAndDayForward",
536+
"GET",
537+
aresponses.Response(
538+
status=200,
539+
headers={"Content-Type": "application/json"},
540+
text=load_fixtures("energy.json"),
541+
),
542+
)
543+
async with ClientSession() as session:
544+
client = SpotHinta(session=session)
545+
energy: Electricity = await client.energy_prices()
546+
assert energy is not None
547+
# At 15:58, still in the 15:00-16:00 interval
548+
assert energy.current_price == 0.062
549+
550+
489551
@pytest.mark.freeze_time("2026-10-25 11:00:00Z")
490552
async def test_model_dst_fall_back_25h(aresponses: ResponsesMockServer) -> None:
491553
"""DST fall back (25-hour day) on 2026-10-25 in Europe/Stockholm.

0 commit comments

Comments
 (0)