Delete test_period_calculation_gap_issue.py

This commit is contained in:
Julian Pawlowski 2025-12-25 09:46:14 +01:00 committed by GitHub
parent 6232cd9a02
commit fd4380161f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,220 +0,0 @@
"""
Test for period calculation issue where qualifying intervals after minimum price are skipped.
Issue: When the daily minimum price occurs in the middle of the day, intervals AFTER
the minimum that qualify (low price) might not be included in best price periods
if there are non-qualifying intervals between them and previous periods.
Example scenario (Dec 25, 2025 type):
- 00:00-06:00: Price 0.30 (CHEAP, qualifies for best price)
- 06:00-12:00: Price 0.35 (NORMAL, doesn't qualify)
- 12:00-13:00: Price 0.28 (CHEAP, MINIMUM - qualifies)
- 13:00-15:00: Price 0.36 (NORMAL, doesn't qualify)
- 15:00-18:00: Price 0.29 (CHEAP, qualifies - should be included!)
- 18:00-24:00: Price 0.38 (EXPENSIVE, doesn't qualify)
Expected: Three separate periods (00:00-06:00, 12:00-13:00, 15:00-18:00)
Actual (bug): Only two periods (00:00-06:00, 12:00-13:00) - third period skipped
Root cause: Sequential processing with temporal continuity requirement.
"""
from __future__ import annotations
from unittest.mock import Mock
import pytest
from custom_components.tibber_prices.coordinator.period_handlers import (
TibberPricesPeriodConfig,
calculate_periods,
)
from custom_components.tibber_prices.coordinator.time_service import (
TibberPricesTimeService,
)
from homeassistant.util import dt as dt_util
def _create_test_intervals_with_gap_issue() -> list[dict]:
"""
Create test data that demonstrates the gap issue.
Pattern simulates a day where qualifying intervals appear in three separate
time windows, separated by non-qualifying intervals.
"""
now_local = dt_util.now()
base_time = now_local.replace(hour=0, minute=0, second=0, microsecond=0)
daily_min = 0.28 # Minimum at 12:00
daily_avg = 0.32
daily_max = 0.38
def _create_interval(hour: int, minute: int, price: float, level: str, rating: str) -> dict:
"""Create a single interval dict."""
return {
"startsAt": base_time.replace(hour=hour, minute=minute),
"total": price,
"level": level,
"rating_level": rating,
"_original_price": price,
"trailing_avg_24h": daily_avg,
"daily_min": daily_min,
"daily_avg": daily_avg,
"daily_max": daily_max,
}
intervals = []
# Period 1: Early morning (00:00-06:00) - CHEAP, qualifies
for hour in range(6):
for minute in [0, 15, 30, 45]:
intervals.append(_create_interval(hour, minute, 0.30, "CHEAP", "LOW"))
# Gap 1: Morning peak (06:00-12:00) - NORMAL, doesn't qualify
for hour in range(6, 12):
for minute in [0, 15, 30, 45]:
intervals.append(_create_interval(hour, minute, 0.35, "NORMAL", "NORMAL"))
# Period 2: Midday minimum (12:00-13:00) - VERY CHEAP, qualifies
for hour in range(12, 13):
for minute in [0, 15, 30, 45]:
intervals.append(_create_interval(hour, minute, 0.28, "VERY_CHEAP", "VERY_LOW"))
# Gap 2: Afternoon rise (13:00-15:00) - NORMAL, doesn't qualify
for hour in range(13, 15):
for minute in [0, 15, 30, 45]:
intervals.append(_create_interval(hour, minute, 0.36, "NORMAL", "NORMAL"))
# Period 3: Late afternoon dip (15:00-18:00) - CHEAP, qualifies (THIS IS THE BUG)
# This period should be included because price 0.29 is:
# - Within flex from daily minimum (0.28)
# - Below average - min_distance
# - Marked as CHEAP level
for hour in range(15, 18):
for minute in [0, 15, 30, 45]:
intervals.append(_create_interval(hour, minute, 0.29, "CHEAP", "LOW"))
# Gap 3: Evening peak (18:00-24:00) - EXPENSIVE, doesn't qualify
for hour in range(18, 24):
for minute in [0, 15, 30, 45]:
intervals.append(_create_interval(hour, minute, 0.38, "EXPENSIVE", "HIGH"))
return intervals
@pytest.mark.asyncio
async def test_best_price_includes_all_qualifying_periods():
"""Test that all qualifying periods are found, not just those before/at minimum."""
# Setup
mock_coordinator = Mock()
mock_coordinator.config_entry = Mock()
time_service = TibberPricesTimeService(mock_coordinator)
# Mock now() to return test date
test_time = dt_util.now()
time_service.now = Mock(return_value=test_time)
intervals = _create_test_intervals_with_gap_issue()
# Daily stats for verification
daily_min = 0.28
daily_avg = 0.32
# Config: 20% flex, 5% min_distance, level filter = CHEAP or better
config = TibberPricesPeriodConfig(
reverse_sort=False, # Best price
flex=0.20, # 20% flex allows prices up to 0.28 * 1.20 = 0.336
min_distance_from_avg=5.0, # Prices must be <= 0.32 * 0.95 = 0.304
min_period_length=60, # 1 hour minimum
threshold_low=0.25,
threshold_high=0.30,
threshold_volatility_moderate=10.0,
threshold_volatility_high=20.0,
threshold_volatility_very_high=30.0,
level_filter="cheap", # Only CHEAP or better
gap_count=0,
)
# Calculate periods
result = calculate_periods(intervals, config=config, time=time_service)
periods = result["periods"]
# Debug output
print(f"\nDaily stats: min={daily_min}, avg={daily_avg}")
print(f"Flex threshold: {daily_min * 1.20:.3f}")
print(f"Min distance threshold: {daily_avg * 0.95:.3f}")
print(f"\nFound {len(periods)} period(s):")
for i, period in enumerate(periods):
print(f" Period {i+1}: {period['start']} to {period['end']} ({period.get('length_minutes')}min)")
# Expected periods:
# 1. 00:00-06:00 (price 0.30: within flex 0.336, below distance 0.304, CHEAP level) ✓
# 2. 12:00-13:00 (price 0.28: within flex 0.336, below distance 0.304, VERY_CHEAP level) ✓
# 3. 15:00-18:00 (price 0.29: within flex 0.336, below distance 0.304, CHEAP level) ✓ THIS ONE MIGHT BE MISSING
# Assertions
assert len(periods) >= 2, f"Expected at least 2 periods, got {len(periods)}"
# Check if third period is found (this is the bug fix target)
# If only 2 periods found, the late afternoon period (15:00-18:00) was skipped
if len(periods) == 2:
pytest.fail(
"BUG CONFIRMED: Only 2 periods found. "
"The late afternoon period (15:00-18:00, price 0.29) was skipped "
"even though it qualifies (CHEAP level, within flex and min_distance). "
"This demonstrates the issue where qualifying intervals AFTER the daily "
"minimum are not included in best price periods."
)
# If fix is working, we should have 3 periods
assert len(periods) == 3, (
f"Expected 3 periods (early morning, midday, late afternoon), got {len(periods)}. "
f"Periods found: {[(p['start'], p['end']) for p in periods]}"
)
# Verify periods are at expected times
period_hours = [(p["start"].hour, p["end"].hour) for p in periods]
expected_ranges = [(0, 6), (12, 13), (15, 18)]
for expected_start, expected_end in expected_ranges:
found = any(start <= expected_start < end or start < expected_end <= end for start, end in period_hours)
assert found, f"Expected period in range {expected_start}:00-{expected_end}:00 not found"
@pytest.mark.asyncio
async def test_best_price_with_relaxed_criteria():
"""Test with more relaxed criteria to ensure at least some periods are found."""
mock_coordinator = Mock()
mock_coordinator.config_entry = Mock()
time_service = TibberPricesTimeService(mock_coordinator)
# Mock now() to return test date
test_time = dt_util.now()
time_service.now = Mock(return_value=test_time)
intervals = _create_test_intervals_with_gap_issue()
# Very relaxed config to ensure periods are found
config = TibberPricesPeriodConfig(
reverse_sort=False,
flex=0.50, # 50% flex - very permissive
min_distance_from_avg=0.0, # Disabled
min_period_length=60,
threshold_low=0.25,
threshold_high=0.30,
threshold_volatility_moderate=10.0,
threshold_volatility_high=20.0,
threshold_volatility_very_high=30.0,
level_filter=None, # No level filter
gap_count=0,
)
result = calculate_periods(intervals, config=config, time=time_service)
periods = result["periods"]
print(f"\nWith relaxed criteria: Found {len(periods)} period(s)")
for i, period in enumerate(periods):
print(f" Period {i+1}: {period['start']} to {period['end']}")
# Even with very relaxed criteria, if we only get 2 periods, something is wrong
assert len(periods) > 0, "No periods found even with very relaxed criteria"