hass.tibber_prices/tests/test_period_overlap.py
Julian Pawlowski bbcfdd4443 fix(periods): stabilize best and peak period outputs
Recompute merged relaxed periods from raw intervals, harden numeric period option normalization, update day-volatility handling for zero or negative averages, and expose day context on period binary sensors.

Add focused regressions for overlap merges, cache invalidation, day statistics, and visible binary sensor attributes.

Impact: Best and peak period entities stay consistent on negative-price days, refresh correctly when same-day prices change, and expose the documented day context attributes.
2026-04-25 22:46:38 +00:00

134 lines
5.4 KiB
Python

"""Regression tests for overlap resolution and merged period summaries."""
from __future__ import annotations
from datetime import timedelta
import pytest
from custom_components.tibber_prices.coordinator.period_handlers import TibberPricesPeriodConfig
from custom_components.tibber_prices.coordinator.period_handlers.period_overlap import resolve_period_overlaps
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
from custom_components.tibber_prices.utils.price import calculate_coefficient_of_variation
from homeassistant.util import dt as dt_util
def _create_interval(base_time, offset: int, price: float, level: str, difference: float, rating: str) -> dict:
"""Create one quarter-hour interval for overlap tests."""
return {
"startsAt": base_time + timedelta(minutes=offset * 15),
"total": price,
"level": level,
"difference": difference,
"rating_level": rating,
}
@pytest.mark.unit
class TestResolvePeriodOverlaps:
"""Validate merged period summaries stay consistent after overlap resolution."""
def test_merge_recomputes_summary_from_raw_intervals(self) -> None:
"""Overlapping periods should be rebuilt from the raw union, not glued summaries."""
base_time = dt_util.parse_datetime("2025-11-22T10:00:00+01:00")
assert base_time is not None
all_prices = [
_create_interval(base_time, 0, 0.10, "CHEAP", -12.0, "LOW"),
_create_interval(base_time, 1, 0.10, "CHEAP", -11.0, "LOW"),
_create_interval(base_time, 2, 0.11, "CHEAP", -10.0, "LOW"),
_create_interval(base_time, 3, 0.11, "CHEAP", -9.0, "NORMAL"),
_create_interval(base_time, 4, 0.12, "NORMAL", -4.0, "NORMAL"),
_create_interval(base_time, 5, 0.13, "NORMAL", 0.0, "NORMAL"),
_create_interval(base_time, 6, 0.13, "NORMAL", 1.0, "NORMAL"),
_create_interval(base_time, 7, 0.14, "NORMAL", 3.0, "NORMAL"),
]
config = TibberPricesPeriodConfig(
reverse_sort=False,
flex=0.15,
min_distance_from_avg=0.0,
min_period_length=60,
threshold_low=-10.0,
threshold_high=10.0,
)
time = TibberPricesTimeService(reference_time=base_time + timedelta(hours=1))
existing_period = {
"start": base_time,
"end": base_time + timedelta(minutes=75),
"duration_minutes": 75,
"level": "cheap",
"rating_level": "low",
"rating_difference_%": -9.2,
"price_mean": 0.108,
"price_median": 0.11,
"price_min": 0.10,
"price_max": 0.12,
"price_spread": 0.02,
"price_coefficient_variation_%": 7.7,
"volatility": "low",
"period_interval_count": 5,
"period_price_diff_from_daily_min": 0.008,
"period_price_diff_from_daily_min_%": 8.0,
"period_position": 1,
"period_count_total": 1,
"period_count_remaining": 0,
}
relaxed_period = {
"start": base_time + timedelta(minutes=60),
"end": base_time + timedelta(minutes=120),
"duration_minutes": 60,
"level": "normal",
"rating_level": "normal",
"rating_difference_%": 0.0,
"price_mean": 0.13,
"price_median": 0.13,
"price_min": 0.12,
"price_max": 0.14,
"price_spread": 0.02,
"price_coefficient_variation_%": 5.8,
"volatility": "low",
"period_interval_count": 4,
"period_price_diff_from_daily_min": 0.03,
"period_price_diff_from_daily_min_%": 30.0,
"period_position": 1,
"period_count_total": 1,
"period_count_remaining": 0,
"relaxation_active": True,
"relaxation_level": "flex=18.0% +level_any",
}
merged_periods, periods_added = resolve_period_overlaps(
existing_periods=[existing_period],
new_relaxed_periods=[relaxed_period],
all_prices=all_prices,
config=config,
time=time,
)
assert periods_added == 1
assert len(merged_periods) == 1
merged = merged_periods[0]
expected_prices = [0.10, 0.10, 0.11, 0.11, 0.12, 0.13, 0.13, 0.14]
expected_cv = calculate_coefficient_of_variation(expected_prices)
assert expected_cv is not None
assert merged["start"] == base_time
assert merged["end"] == base_time + timedelta(minutes=120)
assert merged["period_interval_count"] == 8
assert merged["duration_minutes"] == 120
assert merged["price_mean"] == 0.1175
assert merged["price_median"] == 0.115
assert merged["price_min"] == 0.10
assert merged["price_max"] == 0.14
assert merged["price_spread"] == 0.04
assert merged["price_coefficient_variation_%"] == round(expected_cv, 1)
assert merged["level"] == "normal"
assert merged["rating_level"] == "normal"
assert merged["rating_difference_%"] == -5.25
assert merged["period_price_diff_from_daily_min"] == 0.0175
assert merged["period_price_diff_from_daily_min_%"] == 17.5
assert merged["relaxation_active"] is True
assert merged["relaxation_level"] == "flex=18.0% +level_any"
assert "merged_from" in merged