mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-30 05:13:40 +00:00
Delete test_flex_filter_fix.py
This commit is contained in:
parent
a72a9a533e
commit
6232cd9a02
1 changed files with 0 additions and 262 deletions
|
|
@ -1,262 +0,0 @@
|
||||||
"""
|
|
||||||
Test that explicitly demonstrates the flex filter bug and verifies the fix.
|
|
||||||
|
|
||||||
BUG: The original flex filter for best price had this condition:
|
|
||||||
in_flex = price >= criteria.ref_price and price <= flex_threshold
|
|
||||||
|
|
||||||
This incorrectly required prices to be >= daily minimum, which excluded
|
|
||||||
prices BELOW the minimum even though they're even better (lower) prices!
|
|
||||||
|
|
||||||
FIX: Remove the unnecessary `price >= criteria.ref_price` condition:
|
|
||||||
in_flex = price <= flex_threshold
|
|
||||||
|
|
||||||
This allows ALL low prices up to the threshold to be included.
|
|
||||||
"""
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_best_price_includes_all_intervals_below_threshold():
|
|
||||||
"""
|
|
||||||
Explicit test: ALL intervals with price <= threshold should be included,
|
|
||||||
including those below the daily minimum.
|
|
||||||
|
|
||||||
Scenario:
|
|
||||||
- Intervals with prices: [0.20, 0.25, 0.30, 0.35]
|
|
||||||
- Daily minimum: 0.20 (first interval)
|
|
||||||
- Flex: 50%
|
|
||||||
- Flex threshold: 0.20 * 1.50 = 0.30
|
|
||||||
|
|
||||||
Expected: Intervals at 0.20, 0.25, and 0.30 should ALL be included
|
|
||||||
Bug (old code): Only 0.20 would be included (price >= 0.20 excluded nothing,
|
|
||||||
but price <= 0.30 excluded 0.35)
|
|
||||||
Actually wait, that wouldn't show the bug...
|
|
||||||
|
|
||||||
Let me rethink this test to actually trigger the bug.
|
|
||||||
|
|
||||||
Better scenario:
|
|
||||||
- First interval (00:00): price 0.30
|
|
||||||
- Second interval (01:00): price 0.25 (this becomes the daily minimum)
|
|
||||||
- Third interval (02:00): price 0.27
|
|
||||||
- Fourth interval (03:00): price 0.20 (BELOW calculated minimum!)
|
|
||||||
|
|
||||||
When calculate_reference_prices runs, it finds minimum = 0.25 (from interval 1)
|
|
||||||
But then when we process intervals, we want to include interval 3 (0.20)
|
|
||||||
because it's even lower!
|
|
||||||
|
|
||||||
With OLD code: price >= 0.25 would exclude 0.20
|
|
||||||
With NEW code: price <= threshold would include 0.20
|
|
||||||
|
|
||||||
But wait - calculate_reference_prices scans ALL intervals first, so it would
|
|
||||||
find 0.20 as the minimum, not 0.25.
|
|
||||||
|
|
||||||
OK, I need to think about when this bug would actually manifest...
|
|
||||||
|
|
||||||
AH! The bug shows up when the ACTUAL minimum in processed intervals differs
|
|
||||||
from what calculate_reference_prices found. But how can that happen?
|
|
||||||
|
|
||||||
It can happen if:
|
|
||||||
1. Price data is updated between reference calculation and filtering
|
|
||||||
2. There's a coding error where different interval lists are used
|
|
||||||
3. Outlier smoothing modifies prices (but extremes are protected)
|
|
||||||
|
|
||||||
Actually, I think the real-world manifestation is more subtle. Let me look
|
|
||||||
at a different angle: What if the issue is about WHICH intervals are shown
|
|
||||||
when there are multiple qualifying intervals?
|
|
||||||
|
|
||||||
NEW THEORY: The bug prevents intervals that are LOWER than the daily minimum
|
|
||||||
from being included if they appear in a different part of the day.
|
|
||||||
|
|
||||||
But mathematically, no price can be lower than the minimum by definition.
|
|
||||||
|
|
||||||
Unless... wait, what if different DAYS have different minimums? Each interval
|
|
||||||
is checked against its OWN day's reference price!
|
|
||||||
|
|
||||||
So if we have:
|
|
||||||
- Day 1 minimum: 0.25
|
|
||||||
- Day 2 minimum: 0.30
|
|
||||||
- An interval on Day 1 at 0.20 would be below Day 1's minimum
|
|
||||||
|
|
||||||
But again, calculate_reference_prices would find 0.20 as Day 1's minimum.
|
|
||||||
|
|
||||||
I think I'm overcomplicating this. Let me just write a simple test that
|
|
||||||
verifies the fix works correctly, even if the original bug is hard to trigger.
|
|
||||||
"""
|
|
||||||
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)
|
|
||||||
|
|
||||||
# Simple test: Create intervals where ALL should be included
|
|
||||||
daily_min = 0.20
|
|
||||||
daily_avg = 0.30
|
|
||||||
|
|
||||||
intervals = []
|
|
||||||
|
|
||||||
# All prices from 0.15 to 0.30 should be included with flex=50%
|
|
||||||
# (threshold = 0.20 * 1.50 = 0.30)
|
|
||||||
prices = [0.15, 0.18, 0.20, 0.22, 0.25, 0.28, 0.30, 0.35, 0.40]
|
|
||||||
|
|
||||||
for hour, price in enumerate(prices):
|
|
||||||
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, # This is just metadata, actual min calculated from data
|
|
||||||
"daily_avg": daily_avg,
|
|
||||||
"daily_max": 0.50,
|
|
||||||
})
|
|
||||||
|
|
||||||
config = TibberPricesPeriodConfig(
|
|
||||||
reverse_sort=False,
|
|
||||||
flex=0.50, # 50%
|
|
||||||
min_distance_from_avg=0.0, # Disable to focus on flex filter
|
|
||||||
min_period_length=15, # Very short
|
|
||||||
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"]
|
|
||||||
|
|
||||||
# Get actual reference price calculated
|
|
||||||
ref_prices = result["reference_data"]["ref_prices"]
|
|
||||||
actual_min = list(ref_prices.values())[0] # Should be 0.15
|
|
||||||
threshold = actual_min * 1.50 # Should be 0.225
|
|
||||||
|
|
||||||
print(f"\nActual daily minimum: {actual_min}")
|
|
||||||
print(f"Flex threshold (50%): {threshold}")
|
|
||||||
print(f"Prices: {prices}")
|
|
||||||
print(f"\nExpected to qualify (price <= {threshold}):")
|
|
||||||
for price in prices:
|
|
||||||
qualifies = price <= threshold
|
|
||||||
print(f" {price}: {'✓' if qualifies else '✗'}")
|
|
||||||
|
|
||||||
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')}")
|
|
||||||
|
|
||||||
# Prices that should qualify: 0.15, 0.18, 0.20, 0.22 (all <= 0.225)
|
|
||||||
# These are hours 0, 1, 2, 3
|
|
||||||
# Should form ONE continuous period from 00:00 to 04:00
|
|
||||||
|
|
||||||
assert len(periods) >= 1, "Should find at least one period"
|
|
||||||
|
|
||||||
first_period = periods[0]
|
|
||||||
assert first_period["start"].hour == 0, "First period should start at hour 0"
|
|
||||||
|
|
||||||
# Check that all qualifying hours are included
|
|
||||||
# With fix: period should extend through hour 3 (price 0.22)
|
|
||||||
# The interval at hour 3 (03:00) ends at 03:15
|
|
||||||
# So the period end time will be at 03:15 (hour=3, minute=15)
|
|
||||||
period_end_hour = first_period["end"].hour
|
|
||||||
period_end_minute = first_period["end"].minute
|
|
||||||
|
|
||||||
# Convert to total minutes for easier comparison
|
|
||||||
period_end_total_minutes = period_end_hour * 60 + period_end_minute
|
|
||||||
# Hour 3 interval ends at 03:15 = 195 minutes
|
|
||||||
expected_end_minutes = 3 * 60 + 15 # 195
|
|
||||||
|
|
||||||
assert period_end_total_minutes >= expected_end_minutes, (
|
|
||||||
f"Period should include all intervals up to threshold {threshold}. "
|
|
||||||
f"Expected to include hours 0-3 (prices 0.15-0.22), ending at 03:15. "
|
|
||||||
f"Got end time: {period_end_hour:02d}:{period_end_minute:02d}"
|
|
||||||
)
|
|
||||||
|
|
||||||
print(f"\n✓ TEST PASSED: All qualifying intervals included")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_regression_peak_price_still_works():
|
|
||||||
"""Verify that peak price filtering still works correctly after the fix."""
|
|
||||||
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)
|
|
||||||
|
|
||||||
daily_max = 0.50
|
|
||||||
daily_avg = 0.30
|
|
||||||
|
|
||||||
intervals = []
|
|
||||||
|
|
||||||
# Prices from low to high
|
|
||||||
prices = [0.20, 0.30, 0.40, 0.45, 0.48, 0.50, 0.45, 0.40]
|
|
||||||
|
|
||||||
for hour, price in enumerate(prices):
|
|
||||||
intervals.append({
|
|
||||||
"startsAt": base_time.replace(hour=hour),
|
|
||||||
"total": price,
|
|
||||||
"level": "EXPENSIVE",
|
|
||||||
"rating_level": "HIGH",
|
|
||||||
"_original_price": price,
|
|
||||||
"trailing_avg_24h": daily_avg,
|
|
||||||
"daily_min": 0.20,
|
|
||||||
"daily_avg": daily_avg,
|
|
||||||
"daily_max": daily_max,
|
|
||||||
})
|
|
||||||
|
|
||||||
config = TibberPricesPeriodConfig(
|
|
||||||
reverse_sort=True, # PEAK price
|
|
||||||
flex=0.20, # 20%
|
|
||||||
min_distance_from_avg=0.0,
|
|
||||||
min_period_length=15,
|
|
||||||
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"]
|
|
||||||
|
|
||||||
ref_prices = result["reference_data"]["ref_prices"]
|
|
||||||
actual_max = list(ref_prices.values())[0] # Should be 0.50
|
|
||||||
threshold = actual_max * 0.80 # Should be 0.40
|
|
||||||
|
|
||||||
print(f"\nActual daily maximum: {actual_max}")
|
|
||||||
print(f"Peak threshold (20% below max): {threshold}")
|
|
||||||
print(f"Prices: {prices}")
|
|
||||||
print(f"\nExpected to qualify (price >= {threshold}):")
|
|
||||||
for price in prices:
|
|
||||||
qualifies = price >= threshold
|
|
||||||
print(f" {price}: {'✓' if qualifies else '✗'}")
|
|
||||||
|
|
||||||
# Peak price should still work: only high prices >= threshold
|
|
||||||
assert len(periods) >= 1, "Should find at least one peak period"
|
|
||||||
|
|
||||||
print(f"\n✓ TEST PASSED: Peak price filtering still works")
|
|
||||||
Loading…
Reference in a new issue