mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-07-27 17:26:48 +00:00
_calculate_metadata()'s inline calc_stats() computed the median as a naive sorted(data)[len(data) // 2], which returns the upper-middle value for even-length datasets instead of averaging the two middle values. A full day is always an even interval count (96 quarter-hours or 24 hours), so this silently overstated "median" and "median_position" in nearly every price_stats block of the get_chartdata response. The rest of the codebase already has a correct calculate_median() in utils/average.py; this was an isolated inline duplicate with the bug. Impact: get_chartdata's price_stats.combined/dayN.median and median_position now report the statistically correct median for chart annotations and metadata-driven dashboards.
77 lines
2.8 KiB
Python
77 lines
2.8 KiB
Python
"""Tests for get_chartdata metadata statistics calculation."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime, timedelta
|
|
|
|
from custom_components.tibber_prices.services.get_chartdata import _calculate_metadata
|
|
|
|
|
|
def _make_chart_data(prices: list[float], start: datetime | None = None) -> list[dict]:
|
|
"""Build minimal chart_data entries (start_time + price) for metadata calc."""
|
|
base = start or datetime(2026, 1, 1, 0, 0, tzinfo=UTC)
|
|
return [
|
|
{
|
|
"start_time": (base + timedelta(minutes=15 * i)).isoformat(),
|
|
"price_per_kwh": price,
|
|
}
|
|
for i, price in enumerate(prices)
|
|
]
|
|
|
|
|
|
class TestCalculateMetadataMedian:
|
|
"""Regression coverage: median must use proper statistics.median, not naive indexing.
|
|
|
|
A naive `sorted(data)[len(data)//2]` returns the upper-middle value for
|
|
even-length datasets instead of averaging the two middle values. Since a
|
|
full day always has an even interval count (96 quarter-hours, 24 hours),
|
|
this bug silently affected nearly every "combined"/per-day median in the
|
|
chartdata metadata response.
|
|
"""
|
|
|
|
def test_median_for_even_length_dataset_is_averaged(self) -> None:
|
|
"""4 known prices: naive impl would report 30.0, correct median is 25.0."""
|
|
chart_data = _make_chart_data([10.0, 20.0, 30.0, 40.0])
|
|
|
|
metadata = _calculate_metadata(
|
|
chart_data=chart_data,
|
|
price_field="price_per_kwh",
|
|
start_time_field="start_time",
|
|
currency="EUR",
|
|
resolution="interval",
|
|
subunit_currency=False,
|
|
)
|
|
|
|
assert metadata["price_stats"]["combined"]["median"] == 25.0
|
|
|
|
def test_median_for_odd_length_dataset_is_middle_value(self) -> None:
|
|
"""3 known prices: median is simply the middle value."""
|
|
chart_data = _make_chart_data([10.0, 20.0, 30.0])
|
|
|
|
metadata = _calculate_metadata(
|
|
chart_data=chart_data,
|
|
price_field="price_per_kwh",
|
|
start_time_field="start_time",
|
|
currency="EUR",
|
|
resolution="interval",
|
|
subunit_currency=False,
|
|
)
|
|
|
|
assert metadata["price_stats"]["combined"]["median"] == 20.0
|
|
|
|
def test_median_position_reflects_corrected_median(self) -> None:
|
|
"""median_position must be derived from the corrected median, not the naive one."""
|
|
chart_data = _make_chart_data([10.0, 20.0, 30.0, 40.0])
|
|
|
|
metadata = _calculate_metadata(
|
|
chart_data=chart_data,
|
|
price_field="price_per_kwh",
|
|
start_time_field="start_time",
|
|
currency="EUR",
|
|
resolution="interval",
|
|
subunit_currency=False,
|
|
)
|
|
|
|
combined = metadata["price_stats"]["combined"]
|
|
# median=25.0, min=10.0, max=40.0 -> position = (25-10)/(40-10) = 0.5
|
|
assert combined["median_position"] == 0.5
|