diff --git a/tests/test_real_dec25_data.py b/tests/test_real_dec25_data.py deleted file mode 100644 index 70f70fc..0000000 --- a/tests/test_real_dec25_data.py +++ /dev/null @@ -1,328 +0,0 @@ -""" -Test with actual December 25, 2025 price data provided by user. - -This test uses the exact price data from the user's comment to verify -that the period calculation works correctly with real-world data. -""" - -from __future__ import annotations - -from datetime import datetime, timezone -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_dec25_intervals() -> list[dict]: - """Create intervals from the actual Dec 25, 2025 data.""" - # Real price data from user's comment - prices_data = [ - ("2025-12-25T00:00:00.000+01:00", 0.295, "NORMAL"), - ("2025-12-25T00:15:00.000+01:00", 0.2893, "NORMAL"), - ("2025-12-25T00:30:00.000+01:00", 0.2893, "NORMAL"), - ("2025-12-25T00:45:00.000+01:00", 0.2926, "NORMAL"), - ("2025-12-25T01:00:00.000+01:00", 0.2878, "NORMAL"), - ("2025-12-25T01:15:00.000+01:00", 0.2865, "NORMAL"), - ("2025-12-25T01:30:00.000+01:00", 0.2847, "NORMAL"), - ("2025-12-25T01:45:00.000+01:00", 0.2845, "NORMAL"), - ("2025-12-25T02:00:00.000+01:00", 0.2852, "NORMAL"), - ("2025-12-25T02:15:00.000+01:00", 0.2837, "NORMAL"), - ("2025-12-25T02:30:00.000+01:00", 0.2837, "NORMAL"), - ("2025-12-25T02:45:00.000+01:00", 0.2833, "NORMAL"), - ("2025-12-25T03:00:00.000+01:00", 0.2841, "NORMAL"), - ("2025-12-25T03:15:00.000+01:00", 0.2828, "NORMAL"), - ("2025-12-25T03:30:00.000+01:00", 0.2824, "NORMAL"), - ("2025-12-25T03:45:00.000+01:00", 0.2818, "NORMAL"), - ("2025-12-25T04:00:00.000+01:00", 0.2814, "NORMAL"), - ("2025-12-25T04:15:00.000+01:00", 0.281, "NORMAL"), - ("2025-12-25T04:30:00.000+01:00", 0.2813, "NORMAL"), - ("2025-12-25T04:45:00.000+01:00", 0.2812, "NORMAL"), - ("2025-12-25T05:00:00.000+01:00", 0.2804, "NORMAL"), - ("2025-12-25T05:15:00.000+01:00", 0.2806, "NORMAL"), - ("2025-12-25T05:30:00.000+01:00", 0.2769, "NORMAL"), - ("2025-12-25T05:45:00.000+01:00", 0.2777, "NORMAL"), - ("2025-12-25T06:00:00.000+01:00", 0.2728, "CHEAP"), - ("2025-12-25T06:15:00.000+01:00", 0.277, "NORMAL"), - ("2025-12-25T06:30:00.000+01:00", 0.277, "NORMAL"), - ("2025-12-25T06:45:00.000+01:00", 0.2724, "CHEAP"), - ("2025-12-25T07:00:00.000+01:00", 0.2717, "CHEAP"), - ("2025-12-25T07:15:00.000+01:00", 0.277, "NORMAL"), - ("2025-12-25T07:30:00.000+01:00", 0.2855, "NORMAL"), - ("2025-12-25T07:45:00.000+01:00", 0.2882, "NORMAL"), - ("2025-12-25T08:00:00.000+01:00", 0.2925, "NORMAL"), - ("2025-12-25T08:15:00.000+01:00", 0.293, "NORMAL"), - ("2025-12-25T08:30:00.000+01:00", 0.2966, "NORMAL"), - ("2025-12-25T08:45:00.000+01:00", 0.2888, "NORMAL"), - ("2025-12-25T09:00:00.000+01:00", 0.2968, "NORMAL"), - ("2025-12-25T09:15:00.000+01:00", 0.2942, "NORMAL"), - ("2025-12-25T09:30:00.000+01:00", 0.2926, "NORMAL"), - ("2025-12-25T09:45:00.000+01:00", 0.2897, "NORMAL"), - ("2025-12-25T10:00:00.000+01:00", 0.2854, "NORMAL"), - ("2025-12-25T10:15:00.000+01:00", 0.28, "NORMAL"), - ("2025-12-25T10:30:00.000+01:00", 0.2752, "NORMAL"), - ("2025-12-25T10:45:00.000+01:00", 0.2806, "NORMAL"), - ("2025-12-25T11:00:00.000+01:00", 0.2758, "NORMAL"), - ("2025-12-25T11:15:00.000+01:00", 0.2713, "CHEAP"), - ("2025-12-25T11:30:00.000+01:00", 0.2743, "NORMAL"), - ("2025-12-25T11:45:00.000+01:00", 0.277, "NORMAL"), - ("2025-12-25T12:00:00.000+01:00", 0.2816, "NORMAL"), - ("2025-12-25T12:15:00.000+01:00", 0.2758, "NORMAL"), - ("2025-12-25T12:30:00.000+01:00", 0.2699, "CHEAP"), - ("2025-12-25T12:45:00.000+01:00", 0.2687, "CHEAP"), - ("2025-12-25T13:00:00.000+01:00", 0.2665, "CHEAP"), # Daily minimum - ("2025-12-25T13:15:00.000+01:00", 0.267, "CHEAP"), - ("2025-12-25T13:30:00.000+01:00", 0.2723, "NORMAL"), - ("2025-12-25T13:45:00.000+01:00", 0.2874, "NORMAL"), - ("2025-12-25T14:00:00.000+01:00", 0.2779, "NORMAL"), - ("2025-12-25T14:15:00.000+01:00", 0.2953, "NORMAL"), - ("2025-12-25T14:30:00.000+01:00", 0.3015, "NORMAL"), - ("2025-12-25T14:45:00.000+01:00", 0.3168, "NORMAL"), - ("2025-12-25T15:00:00.000+01:00", 0.3055, "NORMAL"), - ("2025-12-25T15:15:00.000+01:00", 0.3161, "NORMAL"), - ("2025-12-25T15:30:00.000+01:00", 0.3328, "NORMAL"), - ("2025-12-25T15:45:00.000+01:00", 0.3364, "NORMAL"), - ("2025-12-25T16:00:00.000+01:00", 0.3234, "NORMAL"), - ("2025-12-25T16:15:00.000+01:00", 0.3176, "NORMAL"), - ("2025-12-25T16:30:00.000+01:00", 0.3317, "NORMAL"), - ("2025-12-25T16:45:00.000+01:00", 0.3317, "NORMAL"), - ("2025-12-25T17:00:00.000+01:00", 0.3211, "NORMAL"), - ("2025-12-25T17:15:00.000+01:00", 0.3283, "NORMAL"), - ("2025-12-25T17:30:00.000+01:00", 0.3316, "NORMAL"), - ("2025-12-25T17:45:00.000+01:00", 0.3317, "NORMAL"), - ("2025-12-25T18:00:00.000+01:00", 0.3295, "NORMAL"), - ("2025-12-25T18:15:00.000+01:00", 0.3293, "NORMAL"), - ("2025-12-25T18:30:00.000+01:00", 0.3281, "NORMAL"), - ("2025-12-25T18:45:00.000+01:00", 0.3261, "NORMAL"), - ("2025-12-25T19:00:00.000+01:00", 0.3258, "NORMAL"), - ("2025-12-25T19:15:00.000+01:00", 0.3255, "NORMAL"), - ("2025-12-25T19:30:00.000+01:00", 0.3243, "NORMAL"), - ("2025-12-25T19:45:00.000+01:00", 0.3262, "NORMAL"), - ("2025-12-25T20:00:00.000+01:00", 0.3302, "NORMAL"), - ("2025-12-25T20:15:00.000+01:00", 0.331, "NORMAL"), - ("2025-12-25T20:30:00.000+01:00", 0.3205, "NORMAL"), - ("2025-12-25T20:45:00.000+01:00", 0.3169, "NORMAL"), - ("2025-12-25T21:00:00.000+01:00", 0.3231, "NORMAL"), - ("2025-12-25T21:15:00.000+01:00", 0.3307, "NORMAL"), - ("2025-12-25T21:30:00.000+01:00", 0.3288, "NORMAL"), - ("2025-12-25T21:45:00.000+01:00", 0.3239, "NORMAL"), - ("2025-12-25T22:00:00.000+01:00", 0.3325, "NORMAL"), - ("2025-12-25T22:15:00.000+01:00", 0.3317, "NORMAL"), - ("2025-12-25T22:30:00.000+01:00", 0.3317, "NORMAL"), - ("2025-12-25T22:45:00.000+01:00", 0.3271, "NORMAL"), - ("2025-12-25T23:00:00.000+01:00", 0.3304, "NORMAL"), - ("2025-12-25T23:15:00.000+01:00", 0.3252, "NORMAL"), - ("2025-12-25T23:30:00.000+01:00", 0.324, "NORMAL"), - ("2025-12-25T23:45:00.000+01:00", 0.3211, "NORMAL"), - ] - - # Calculate daily statistics - prices = [p[1] for p in prices_data] - daily_min = min(prices) # 0.2665 - daily_max = max(prices) # 0.3364 - daily_avg = sum(prices) / len(prices) - - intervals = [] - for time_str, price, level in prices_data: - # Parse timestamp - dt = datetime.fromisoformat(time_str) - - intervals.append({ - "startsAt": dt, - "total": price, - "level": level, - "rating_level": "LOW" if level == "CHEAP" else "NORMAL", - "_original_price": price, - "trailing_avg_24h": daily_avg, - "daily_min": daily_min, - "daily_avg": daily_avg, - "daily_max": daily_max, - }) - - return intervals - - -@pytest.mark.asyncio -async def test_dec25_actual_data_analysis(): - """ - Analyze the actual Dec 25, 2025 data to understand price distribution. - - Key observations from the data: - - Daily minimum: 0.2665 (at 13:00) - - Daily maximum: 0.3364 (at 15:45) - - Most prices are NORMAL (only a few CHEAP intervals scattered throughout) - - Early morning (00:00-06:00): prices around 0.28-0.295 - - Late afternoon (13:00-13:15): absolute minimum around 0.2665-0.267 - - User's concern: "only prices before the minimum daily price are considered - while intervals after minimum price should also be included because they are - actually lower than intervals before minimum price" - - This suggests intervals AFTER 13:00 (the minimum) that have lower prices than - intervals BEFORE 13:00 should be included. - """ - mock_coordinator = Mock() - mock_coordinator.config_entry = Mock() - time_service = TibberPricesTimeService(mock_coordinator) - - # Use the actual date from the data - test_time = dt_util.parse_datetime("2025-12-25T12:00:00+01:00") - time_service.now = Mock(return_value=test_time) - - intervals = _create_dec25_intervals() - - # Calculate actual statistics - prices = [i["total"] for i in intervals] - daily_min = min(prices) - daily_max = max(prices) - daily_avg = sum(prices) / len(prices) - - print(f"\n=== December 25, 2025 Price Analysis ===") - print(f"Daily minimum: {daily_min:.4f} (at 13:00)") - print(f"Daily average: {daily_avg:.4f}") - print(f"Daily maximum: {daily_max:.4f} (at 15:45)") - print(f"Price range: {daily_max - daily_min:.4f}") - - # Find CHEAP intervals - cheap_intervals = [(i["startsAt"], i["total"]) for i in intervals if i["level"] == "CHEAP"] - print(f"\nCHEAP intervals ({len(cheap_intervals)}):") - for dt, price in cheap_intervals: - print(f" {dt.strftime('%H:%M')}: {price:.4f}") - - # Test with default config (15% flex) - config = TibberPricesPeriodConfig( - reverse_sort=False, # Best price - flex=0.15, # 15% default - min_distance_from_avg=5.0, # 5% default - 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="cheap", # Only CHEAP intervals - gap_count=0, - ) - - result = calculate_periods(intervals, config=config, time=time_service) - periods = result["periods"] - - # Get calculated reference prices - ref_data = result["reference_data"] - ref_min = list(ref_data["ref_prices"].values())[0] - ref_avg = list(ref_data["avg_prices"].values())[0] - - flex_threshold = ref_min * (1 + config.flex) - distance_threshold = ref_avg * (1 - config.min_distance_from_avg / 100) - - print(f"\n=== Period Calculation (flex={config.flex*100}%, min_distance={config.min_distance_from_avg}%, level=CHEAP) ===") - print(f"Reference minimum: {ref_min:.4f}") - print(f"Reference average: {ref_avg:.4f}") - print(f"Flex threshold (min * 1.{config.flex}): {flex_threshold:.4f}") - print(f"Distance threshold (avg * 0.95): {distance_threshold:.4f}") - print(f"\nIntervals must meet ALL of:") - print(f" 1. price <= {flex_threshold:.4f} (flex filter)") - print(f" 2. price <= {distance_threshold:.4f} (min_distance filter)") - print(f" 3. level = CHEAP (level filter)") - - print(f"\n=== Results ===") - print(f"Found {len(periods)} period(s):") - for i, period in enumerate(periods, 1): - start = period['start'] - end = period['end'] - print(f" Period {i}: {start.strftime('%H:%M')} - {end.strftime('%H:%M')}") - - # Analyze which CHEAP intervals should qualify - print(f"\n=== CHEAP Interval Analysis ===") - for dt, price in cheap_intervals: - in_flex = price <= flex_threshold - in_distance = price <= distance_threshold - qualifies = in_flex and in_distance - status = "✓ QUALIFIES" if qualifies else "✗ excluded" - reasons = [] - if not in_flex: - reasons.append(f"exceeds flex ({price:.4f} > {flex_threshold:.4f})") - if not in_distance: - reasons.append(f"too close to avg ({price:.4f} > {distance_threshold:.4f})") - - reason_str = f" - {', '.join(reasons)}" if reasons else "" - print(f" {dt.strftime('%H:%M')}: {price:.4f} - {status}{reason_str}") - - # Verify the fix works - assert len(periods) > 0, "Should find at least one period with CHEAP intervals" - - -@pytest.mark.asyncio -async def test_dec25_without_level_filter(): - """Test Dec 25 data without level filter to see all qualifying intervals.""" - mock_coordinator = Mock() - mock_coordinator.config_entry = Mock() - time_service = TibberPricesTimeService(mock_coordinator) - - test_time = dt_util.parse_datetime("2025-12-25T12:00:00+01:00") - time_service.now = Mock(return_value=test_time) - - intervals = _create_dec25_intervals() - - # Calculate statistics - prices = [i["total"] for i in intervals] - daily_min = min(prices) - daily_avg = sum(prices) / len(prices) - - # Test without level filter to see ALL qualifying intervals - config = TibberPricesPeriodConfig( - reverse_sort=False, - flex=0.15, # 15% - min_distance_from_avg=5.0, # 5% - 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"] - - ref_min = list(result["reference_data"]["ref_prices"].values())[0] - ref_avg = list(result["reference_data"]["avg_prices"].values())[0] - - flex_threshold = ref_min * (1 + config.flex) - distance_threshold = ref_avg * (1 - config.min_distance_from_avg / 100) - - print(f"\n=== Without Level Filter ===") - print(f"Flex threshold: {flex_threshold:.4f}") - print(f"Distance threshold: {distance_threshold:.4f}") - print(f"\nFound {len(periods)} period(s):") - for i, period in enumerate(periods, 1): - start = period['start'] - end = period['end'] - length_min = (end - start).total_seconds() / 60 - print(f" Period {i}: {start.strftime('%H:%M')} - {end.strftime('%H:%M')} ({length_min:.0f} min)") - - # Check which intervals qualify - qualifying_intervals = [] - for interval in intervals: - price = interval["total"] - dt = interval["startsAt"] - in_flex = price <= flex_threshold - in_distance = price <= distance_threshold - if in_flex and in_distance: - qualifying_intervals.append((dt, price)) - - print(f"\nQualifying intervals ({len(qualifying_intervals)}):") - for dt, price in qualifying_intervals[:20]: # Show first 20 - print(f" {dt.strftime('%H:%M')}: {price:.4f}") - if len(qualifying_intervals) > 20: - print(f" ... and {len(qualifying_intervals) - 20} more")