mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-30 05:13:40 +00:00
Delete test_real_dec25_data.py
This commit is contained in:
parent
fd4380161f
commit
3eb6b835fc
1 changed files with 0 additions and 328 deletions
|
|
@ -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")
|
|
||||||
Loading…
Reference in a new issue