mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-29 21:03:40 +00:00
Delete test_flex_filter_bug.py
This commit is contained in:
parent
aae1b9cf4d
commit
a72a9a533e
1 changed files with 0 additions and 277 deletions
|
|
@ -1,277 +0,0 @@
|
|||
"""
|
||||
Test to verify the flex filter bug where prices BELOW the daily minimum are excluded.
|
||||
|
||||
BUG: In level_filtering.py line 160, the condition for best price flex filter is:
|
||||
in_flex = price >= criteria.ref_price and price <= flex_threshold
|
||||
|
||||
This EXCLUDES prices below the daily minimum, which is wrong!
|
||||
|
||||
For best price, we want LOW prices, so we should accept:
|
||||
- Any price from 0 up to (daily_min + flex_amount)
|
||||
|
||||
NOT just:
|
||||
- Prices from daily_min to (daily_min + flex_amount)
|
||||
|
||||
This explains the user's observation: "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."
|
||||
|
||||
If there are intervals with prices LOWER than the daily minimum (due to rounding or
|
||||
floating point precision), they would be EXCLUDED by the current logic!
|
||||
"""
|
||||
|
||||
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_intervals_with_prices_below_minimum() -> list[dict]:
|
||||
"""
|
||||
Create test data where some intervals have prices BELOW the calculated daily minimum.
|
||||
|
||||
This can happen in real data due to:
|
||||
1. Rounding errors in price calculations
|
||||
2. Multiple intervals at the exact same minimum price
|
||||
3. Price data arriving in batches with different precision
|
||||
"""
|
||||
now_local = dt_util.now()
|
||||
base_time = now_local.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
# The "calculated" daily minimum is 0.25 (from the midday interval)
|
||||
# But we'll have an interval at 0.249 which is technically lower
|
||||
daily_min = 0.25
|
||||
daily_avg = 0.30
|
||||
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, # NOTE: This is the "calculated" minimum, not actual
|
||||
"daily_avg": daily_avg,
|
||||
"daily_max": daily_max,
|
||||
}
|
||||
|
||||
intervals = []
|
||||
|
||||
# Early morning: Price 0.249 - BELOW the "daily minimum" of 0.25!
|
||||
# This should be included in best price periods (it's the cheapest!)
|
||||
for hour in range(3):
|
||||
for minute in [0, 15, 30, 45]:
|
||||
intervals.append(_create_interval(hour, minute, 0.249, "VERY_CHEAP", "VERY_LOW"))
|
||||
|
||||
# Mid-morning: Normal prices
|
||||
for hour in range(3, 9):
|
||||
for minute in [0, 15, 30, 45]:
|
||||
intervals.append(_create_interval(hour, minute, 0.32, "NORMAL", "NORMAL"))
|
||||
|
||||
# Midday: Price exactly at "daily minimum"
|
||||
for hour in range(12, 13):
|
||||
for minute in [0, 15, 30, 45]:
|
||||
intervals.append(_create_interval(hour, minute, 0.25, "VERY_CHEAP", "VERY_LOW"))
|
||||
|
||||
# Afternoon: Normal prices
|
||||
for hour in range(13, 18):
|
||||
for minute in [0, 15, 30, 45]:
|
||||
intervals.append(_create_interval(hour, minute, 0.33, "NORMAL", "NORMAL"))
|
||||
|
||||
# Evening: Higher prices
|
||||
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_prices_below_minimum_are_included():
|
||||
"""
|
||||
Test that prices BELOW the daily minimum are included in best price periods.
|
||||
|
||||
BUG REPRODUCTION:
|
||||
With the current logic (price >= daily_min), intervals at 0.249 would be excluded
|
||||
even though they're cheaper than the "minimum" of 0.25!
|
||||
"""
|
||||
mock_coordinator = Mock()
|
||||
mock_coordinator.config_entry = Mock()
|
||||
time_service = TibberPricesTimeService(mock_coordinator)
|
||||
|
||||
test_time = dt_util.now()
|
||||
time_service.now = Mock(return_value=test_time)
|
||||
|
||||
intervals = _create_intervals_with_prices_below_minimum()
|
||||
|
||||
# Very permissive config to ensure periods are found
|
||||
config = TibberPricesPeriodConfig(
|
||||
reverse_sort=False, # Best price
|
||||
flex=0.30, # 30% flex
|
||||
min_distance_from_avg=0.0, # Disable to isolate flex filter
|
||||
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,
|
||||
gap_count=0,
|
||||
)
|
||||
|
||||
result = calculate_periods(intervals, config=config, time=time_service)
|
||||
periods = result["periods"]
|
||||
|
||||
print(f"\nDaily minimum (calculated): 0.25")
|
||||
print(f"Early morning price: 0.249 (BELOW calculated minimum!)")
|
||||
print(f"Midday price: 0.25 (AT calculated minimum)")
|
||||
print(f"Flex allows up to: {0.25 * 1.30:.3f}")
|
||||
print(f"\nWith current buggy logic (price >= daily_min):")
|
||||
print(f" Early morning (0.249): Should be EXCLUDED (0.249 < 0.25)")
|
||||
print(f" Midday (0.25): Should be INCLUDED (0.25 >= 0.25)")
|
||||
print(f"\nWith correct logic (price <= daily_min + flex):")
|
||||
print(f" Early morning (0.249): Should be INCLUDED (0.249 <= 0.325)")
|
||||
print(f" Midday (0.25): Should be INCLUDED (0.25 <= 0.325)")
|
||||
print(f"\nFound {len(periods)} period(s):")
|
||||
for i, period in enumerate(periods):
|
||||
print(f" Period {i+1}: {period['start'].strftime('%H:%M')} to {period['end'].strftime('%H:%M')}")
|
||||
|
||||
# Check if early morning period is found
|
||||
early_morning_found = any(
|
||||
p["start"].hour <= 2
|
||||
for p in periods
|
||||
)
|
||||
|
||||
if not early_morning_found:
|
||||
pytest.fail(
|
||||
"BUG CONFIRMED: Early morning period (price 0.249) was excluded!\n"
|
||||
"The flex filter incorrectly requires price >= daily_min, which excludes\n"
|
||||
"prices below the minimum. This is wrong for best price mode.\n\n"
|
||||
"Fix needed in level_filtering.py line 160:\n"
|
||||
" OLD: in_flex = price >= criteria.ref_price and price <= flex_threshold\n"
|
||||
" NEW: in_flex = price <= flex_threshold"
|
||||
)
|
||||
|
||||
assert early_morning_found, (
|
||||
"Early morning period (0.249) should be included - it's the cheapest price "
|
||||
"and well within the flex threshold."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_flex_filter_accepts_all_prices_below_threshold():
|
||||
"""
|
||||
Test that the flex filter accepts ALL prices below the threshold, not just those >= minimum.
|
||||
|
||||
This is the fundamental expectation for best price mode.
|
||||
"""
|
||||
mock_coordinator = Mock()
|
||||
mock_coordinator.config_entry = Mock()
|
||||
time_service = TibberPricesTimeService(mock_coordinator)
|
||||
|
||||
test_time = dt_util.now()
|
||||
time_service.now = Mock(return_value=test_time)
|
||||
|
||||
now_local = dt_util.now()
|
||||
base_time = now_local.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
# Create a very simple scenario
|
||||
daily_min = 1.00
|
||||
daily_avg = 2.00
|
||||
|
||||
intervals = []
|
||||
|
||||
# Create intervals with prices: 0.80, 0.90, 1.00, 1.10, 1.20
|
||||
# With flex=20%, threshold = 1.00 + 0.20 = 1.20
|
||||
# All of these should be included!
|
||||
prices_to_test = [0.80, 0.90, 1.00, 1.10, 1.20]
|
||||
|
||||
for hour, price in enumerate(prices_to_test):
|
||||
intervals.append({
|
||||
"startsAt": base_time.replace(hour=hour),
|
||||
"total": price,
|
||||
"level": "CHEAP",
|
||||
"rating_level": "LOW",
|
||||
"_original_price": price,
|
||||
"trailing_avg_24h": daily_avg,
|
||||
"daily_min": daily_min,
|
||||
"daily_avg": daily_avg,
|
||||
"daily_max": 3.00,
|
||||
})
|
||||
|
||||
config = TibberPricesPeriodConfig(
|
||||
reverse_sort=False,
|
||||
flex=0.20, # 20% above minimum = 1.20
|
||||
min_distance_from_avg=0.0, # Disable
|
||||
min_period_length=15, # Very short to allow single intervals
|
||||
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,
|
||||
gap_count=0,
|
||||
)
|
||||
|
||||
result = calculate_periods(intervals, config=config, time=time_service)
|
||||
periods = result["periods"]
|
||||
|
||||
# Debug: Check what reference prices were calculated
|
||||
ref_data = result.get("reference_data", {})
|
||||
ref_prices = ref_data.get("ref_prices", {})
|
||||
avg_prices = ref_data.get("avg_prices", {})
|
||||
|
||||
print(f"\nDaily min: {daily_min}, flex: 20%, threshold: {daily_min * 1.20}")
|
||||
print(f"Actual ref_prices from calculation: {ref_prices}")
|
||||
print(f"Actual avg_prices from calculation: {avg_prices}")
|
||||
print(f"Prices tested: {prices_to_test}")
|
||||
print(f"Expected: ALL prices <= calculated threshold should be in periods")
|
||||
print(f"\nFound {len(periods)} period(s):")
|
||||
for i, period in enumerate(periods):
|
||||
print(f" Period {i+1}: {period['start'].strftime('%H:%M')} to {period['end'].strftime('%H:%M')}")
|
||||
|
||||
# Check what actually got included based on the calculated reference price
|
||||
actual_ref_price = list(ref_prices.values())[0] # 0.80
|
||||
actual_threshold = actual_ref_price * 1.20 # 0.96
|
||||
|
||||
print(f"\nCalculated threshold: {actual_threshold}")
|
||||
print(f"Intervals that should qualify (price <= {actual_threshold}):")
|
||||
for price in prices_to_test:
|
||||
qualifies = price <= actual_threshold
|
||||
print(f" Price {price}: {'✓ QUALIFIES' if qualifies else '✗ excluded'}")
|
||||
|
||||
# Intervals 0 (0.80) and 1 (0.90) should qualify
|
||||
# Intervals 2, 3, 4 should NOT qualify (prices > 0.96)
|
||||
# So we expect ONE period from hour 0 to hour 2 (ending after interval 1)
|
||||
|
||||
assert len(periods) == 1, f"Expected 1 continuous period, got {len(periods)}"
|
||||
|
||||
period = periods[0]
|
||||
assert period["start"].hour == 0, "Period should start at hour 0 (price 0.80)"
|
||||
# Period should include intervals at hours 0 and 1, ending at 01:15 + 15min = 01:30?
|
||||
# Actually, let me just check that it includes hour 1
|
||||
period_end_hour = period["end"].hour
|
||||
period_end_minute = period["end"].minute
|
||||
|
||||
# Interval 1 starts at 01:00 and ends at 01:15
|
||||
# So period should end at or after 01:15
|
||||
end_time_minutes = period_end_hour * 60 + period_end_minute
|
||||
expected_min_end = 1 * 60 + 15 # 01:15
|
||||
|
||||
assert end_time_minutes >= expected_min_end, (
|
||||
f"Period should include interval 1 (01:00-01:15). "
|
||||
f"Expected end >= 01:15, got {period_end_hour:02d}:{period_end_minute:02d}"
|
||||
)
|
||||
Loading…
Reference in a new issue