mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-29 21:03:40 +00:00
152 lines
5.9 KiB
Python
152 lines
5.9 KiB
Python
"""Test midnight turnover consistency - period visibility before/after midnight."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timedelta
|
|
from unittest.mock import Mock
|
|
from zoneinfo import ZoneInfo
|
|
|
|
import pytest
|
|
|
|
from custom_components.tibber_prices.coordinator.period_handlers.core import (
|
|
calculate_periods,
|
|
)
|
|
from custom_components.tibber_prices.coordinator.period_handlers.types import (
|
|
TibberPricesPeriodConfig,
|
|
)
|
|
from custom_components.tibber_prices.coordinator.time_service import (
|
|
TibberPricesTimeService,
|
|
)
|
|
|
|
|
|
def create_price_interval(dt: datetime, price: float) -> dict:
|
|
"""Create a price interval dict."""
|
|
return {
|
|
"startsAt": dt,
|
|
"total": price,
|
|
"level": "NORMAL",
|
|
"rating_level": "NORMAL",
|
|
}
|
|
|
|
|
|
def create_price_data_scenario() -> tuple[list[dict], list[dict], list[dict], list[dict]]:
|
|
"""Create a realistic price scenario with a period crossing midnight."""
|
|
tz = ZoneInfo("Europe/Berlin")
|
|
base = datetime(2025, 11, 21, 0, 0, 0, tzinfo=tz)
|
|
|
|
# Define cheap hour ranges for each day
|
|
cheap_hours = {
|
|
"yesterday": range(22, 24), # 22:00-23:45
|
|
"today": range(21, 24), # 21:00-23:45 (crosses midnight!)
|
|
"tomorrow": range(1), # 00:00-00:45 (continuation)
|
|
}
|
|
|
|
def generate_day_prices(day_dt: datetime, cheap_range: range) -> list[dict]:
|
|
"""Generate 15-min interval prices for a day."""
|
|
prices = []
|
|
for hour in range(24):
|
|
for minute in [0, 15, 30, 45]:
|
|
dt = day_dt.replace(hour=hour, minute=minute)
|
|
price = 15.0 if hour in cheap_range else 30.0
|
|
prices.append(create_price_interval(dt, price))
|
|
return prices
|
|
|
|
yesterday_prices = generate_day_prices(base - timedelta(days=1), cheap_hours["yesterday"])
|
|
today_prices = generate_day_prices(base, cheap_hours["today"])
|
|
tomorrow_prices = generate_day_prices(base + timedelta(days=1), cheap_hours["tomorrow"])
|
|
day_after_tomorrow_prices = generate_day_prices(base + timedelta(days=2), range(0)) # No cheap hours
|
|
|
|
return yesterday_prices, today_prices, tomorrow_prices, day_after_tomorrow_prices
|
|
|
|
|
|
@pytest.fixture
|
|
def period_config() -> TibberPricesPeriodConfig:
|
|
"""Provide test period configuration."""
|
|
return TibberPricesPeriodConfig(
|
|
reverse_sort=False, # Best price (cheap periods)
|
|
flex=0.50, # 50% flexibility
|
|
min_distance_from_avg=-5.0, # -5% below average
|
|
min_period_length=60, # 60 minutes minimum
|
|
threshold_low=20.0,
|
|
threshold_high=30.0,
|
|
threshold_volatility_moderate=0.3,
|
|
threshold_volatility_high=0.5,
|
|
threshold_volatility_very_high=0.7,
|
|
level_filter=None,
|
|
gap_count=0,
|
|
)
|
|
|
|
|
|
@pytest.mark.integration
|
|
def test_midnight_crossing_period_consistency(period_config: TibberPricesPeriodConfig) -> None:
|
|
"""
|
|
Test that midnight-crossing periods remain visible before and after midnight turnover.
|
|
|
|
This test simulates the real-world scenario where:
|
|
- Before midnight (21st 22:00): Period 21:00→01:00 is visible
|
|
- After midnight (22nd 00:30): Same period should still be visible
|
|
|
|
The period starts on 2025-11-21 (yesterday after turnover) and ends on 2025-11-22 (today).
|
|
"""
|
|
tz = ZoneInfo("Europe/Berlin")
|
|
yesterday_prices, today_prices, tomorrow_prices, day_after_tomorrow_prices = create_price_data_scenario()
|
|
|
|
# Create mock config entry
|
|
mock_config_entry = Mock()
|
|
mock_config_entry.options.get.return_value = "minor"
|
|
|
|
# SCENARIO 1: Before midnight (today = 2025-11-21 22:00)
|
|
current_time_before = datetime(2025, 11, 21, 22, 0, 0, tzinfo=tz)
|
|
time_service_before = TibberPricesTimeService(current_time_before)
|
|
all_prices_before = yesterday_prices + today_prices + tomorrow_prices
|
|
|
|
result_before = calculate_periods(
|
|
all_prices_before,
|
|
config=period_config,
|
|
time=time_service_before,
|
|
)
|
|
periods_before = result_before["periods"]
|
|
|
|
# Find the midnight-crossing period (starts 21st, ends 22nd)
|
|
midnight_period_before = None
|
|
for period in periods_before:
|
|
if period["start"].date().isoformat() == "2025-11-21" and period["end"].date().isoformat() == "2025-11-22":
|
|
midnight_period_before = period
|
|
break
|
|
|
|
assert midnight_period_before is not None, "Expected to find midnight-crossing period before turnover"
|
|
|
|
# SCENARIO 2: After midnight turnover (today = 2025-11-22 00:30)
|
|
current_time_after = datetime(2025, 11, 22, 0, 30, 0, tzinfo=tz)
|
|
time_service_after = TibberPricesTimeService(current_time_after)
|
|
|
|
# Simulate coordinator data shift: yesterday=21st, today=22nd, tomorrow=23rd
|
|
yesterday_after_turnover = today_prices
|
|
today_after_turnover = tomorrow_prices
|
|
tomorrow_after_turnover = day_after_tomorrow_prices
|
|
all_prices_after = yesterday_after_turnover + today_after_turnover + tomorrow_after_turnover
|
|
|
|
result_after = calculate_periods(
|
|
all_prices_after,
|
|
config=period_config,
|
|
time=time_service_after,
|
|
)
|
|
periods_after = result_after["periods"]
|
|
|
|
# Find period that started on 2025-11-21 (now "yesterday")
|
|
period_from_yesterday_after = None
|
|
for period in periods_after:
|
|
if period["start"].date().isoformat() == "2025-11-21":
|
|
period_from_yesterday_after = period
|
|
break
|
|
|
|
assert period_from_yesterday_after is not None, (
|
|
"Expected midnight-crossing period to remain visible after turnover (we're at 00:30, period ends at 01:00)"
|
|
)
|
|
|
|
# Verify consistency: same absolute times
|
|
assert midnight_period_before["start"] == period_from_yesterday_after["start"], "Start time should match"
|
|
assert midnight_period_before["end"] == period_from_yesterday_after["end"], "End time should match"
|
|
assert midnight_period_before["duration_minutes"] == period_from_yesterday_after["duration_minutes"], (
|
|
"Duration should match"
|
|
)
|