mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-30 05:13:40 +00:00
refactor: Enhance period calculations with aggregated levels and ratings
This commit is contained in:
parent
db3299b7a7
commit
f9f4908748
4 changed files with 253 additions and 32 deletions
|
|
@ -347,8 +347,8 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
# Find current or next interval
|
# Find current or next interval
|
||||||
current_interval = self._find_current_or_next_interval(intervals)
|
current_interval = self._find_current_or_next_interval(intervals)
|
||||||
|
|
||||||
# Build periods summary
|
# Build periods summary (merge with original summaries to include level/rating_level)
|
||||||
periods_summary = self._build_periods_summary(intervals)
|
periods_summary = self._build_periods_summary(intervals, period_summaries)
|
||||||
|
|
||||||
# Build final attributes
|
# Build final attributes
|
||||||
return self._build_final_attributes(current_interval, periods_summary, intervals)
|
return self._build_final_attributes(current_interval, periods_summary, intervals)
|
||||||
|
|
@ -369,16 +369,29 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
return interval.copy()
|
return interval.copy()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _build_periods_summary(self, intervals: list[dict]) -> list[dict]:
|
def _build_periods_summary(self, intervals: list[dict], original_summaries: list[dict]) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
Build a summary of periods with consistent attribute structure.
|
Build a summary of periods with consistent attribute structure.
|
||||||
|
|
||||||
Returns a list of period summaries with the same attributes as top-level,
|
Returns a list of period summaries with the same attributes as top-level,
|
||||||
making the structure predictable and easy to use in automations.
|
making the structure predictable and easy to use in automations.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
intervals: List of interval dictionaries with period information
|
||||||
|
original_summaries: Original period summaries from coordinator (with level/rating_level)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not intervals:
|
if not intervals:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
# Build a lookup for original summaries by start time
|
||||||
|
original_lookup: dict[str, dict] = {}
|
||||||
|
for summary in original_summaries:
|
||||||
|
start = summary.get("start")
|
||||||
|
if start:
|
||||||
|
key = start.isoformat() if hasattr(start, "isoformat") else str(start)
|
||||||
|
original_lookup[key] = summary
|
||||||
|
|
||||||
# Group intervals by period (they have the same period_start)
|
# Group intervals by period (they have the same period_start)
|
||||||
periods_dict: dict[str, list[dict]] = {}
|
periods_dict: dict[str, list[dict]] = {}
|
||||||
for interval in intervals:
|
for interval in intervals:
|
||||||
|
|
@ -398,24 +411,41 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
first = period_intervals[0]
|
first = period_intervals[0]
|
||||||
prices = [i["price"] for i in period_intervals if "price" in i]
|
prices = [i["price"] for i in period_intervals if "price" in i]
|
||||||
|
|
||||||
# Use same attribute names as top-level for consistency
|
# Get level and rating_level from original summaries first
|
||||||
|
aggregated_level = None
|
||||||
|
aggregated_rating_level = None
|
||||||
|
period_start = first.get("period_start")
|
||||||
|
if period_start:
|
||||||
|
key = period_start.isoformat() if hasattr(period_start, "isoformat") else str(period_start)
|
||||||
|
original = original_lookup.get(key)
|
||||||
|
if original:
|
||||||
|
aggregated_level = original.get("level")
|
||||||
|
aggregated_rating_level = original.get("rating_level")
|
||||||
|
|
||||||
|
# Optimized attribute order: time → core decisions → prices → details → meta
|
||||||
summary = {
|
summary = {
|
||||||
|
# Time information
|
||||||
"start": first.get("period_start"),
|
"start": first.get("period_start"),
|
||||||
"end": first.get("period_end"),
|
"end": first.get("period_end"),
|
||||||
|
"duration_minutes": first.get("duration_minutes"),
|
||||||
|
# Core decision attributes
|
||||||
|
"level": aggregated_level,
|
||||||
|
"rating_level": aggregated_rating_level,
|
||||||
|
# Price statistics
|
||||||
|
"price_avg": round(sum(prices) / len(prices), 2) if prices else 0,
|
||||||
|
"price_min": round(min(prices), 2) if prices else 0,
|
||||||
|
"price_max": round(max(prices), 2) if prices else 0,
|
||||||
|
# Detail information
|
||||||
"hour": first.get("hour"),
|
"hour": first.get("hour"),
|
||||||
"minute": first.get("minute"),
|
"minute": first.get("minute"),
|
||||||
"time": first.get("time"),
|
"time": first.get("time"),
|
||||||
"duration_minutes": first.get("duration_minutes"),
|
|
||||||
"periods_total": first.get("periods_total"),
|
"periods_total": first.get("periods_total"),
|
||||||
"periods_remaining": first.get("periods_remaining"),
|
"periods_remaining": first.get("periods_remaining"),
|
||||||
"period_position": first.get("period_position"),
|
"period_position": first.get("period_position"),
|
||||||
"intervals_count": len(period_intervals),
|
"intervals_count": len(period_intervals),
|
||||||
"price_avg": round(sum(prices) / len(prices), 2) if prices else 0,
|
|
||||||
"price_min": round(min(prices), 2) if prices else 0,
|
|
||||||
"price_max": round(max(prices), 2) if prices else 0,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add price_diff attributes if present
|
# Add price_diff attributes if present (after details)
|
||||||
self._add_price_diff_for_period(summary, period_intervals, first)
|
self._add_price_diff_for_period(summary, period_intervals, first)
|
||||||
|
|
||||||
summaries.append(summary)
|
summaries.append(summary)
|
||||||
|
|
@ -449,9 +479,47 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
break
|
break
|
||||||
|
|
||||||
if current_period_summary:
|
if current_period_summary:
|
||||||
# Copy all attributes from the period summary
|
# Build attributes with optimized order: time → core decisions → prices → details → meta
|
||||||
attributes = {"timestamp": timestamp}
|
attributes = {
|
||||||
attributes.update(current_period_summary)
|
# Time information
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"start": current_period_summary.get("start"),
|
||||||
|
"end": current_period_summary.get("end"),
|
||||||
|
"duration_minutes": current_period_summary.get("duration_minutes"),
|
||||||
|
# Core decision attributes
|
||||||
|
"level": current_period_summary.get("level"),
|
||||||
|
"rating_level": current_period_summary.get("rating_level"),
|
||||||
|
# Price statistics
|
||||||
|
"price_avg": current_period_summary.get("price_avg"),
|
||||||
|
"price_min": current_period_summary.get("price_min"),
|
||||||
|
"price_max": current_period_summary.get("price_max"),
|
||||||
|
# Detail information
|
||||||
|
"hour": current_period_summary.get("hour"),
|
||||||
|
"minute": current_period_summary.get("minute"),
|
||||||
|
"time": current_period_summary.get("time"),
|
||||||
|
"periods_total": current_period_summary.get("periods_total"),
|
||||||
|
"periods_remaining": current_period_summary.get("periods_remaining"),
|
||||||
|
"period_position": current_period_summary.get("period_position"),
|
||||||
|
"intervals_count": current_period_summary.get("intervals_count"),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add period price_diff attributes if present
|
||||||
|
if "period_price_diff_from_daily_min" in current_period_summary:
|
||||||
|
attributes["period_price_diff_from_daily_min"] = current_period_summary[
|
||||||
|
"period_price_diff_from_daily_min"
|
||||||
|
]
|
||||||
|
if "period_price_diff_from_daily_min_%" in current_period_summary:
|
||||||
|
attributes["period_price_diff_from_daily_min_%"] = current_period_summary[
|
||||||
|
"period_price_diff_from_daily_min_%"
|
||||||
|
]
|
||||||
|
elif "period_price_diff_from_daily_max" in current_period_summary:
|
||||||
|
attributes["period_price_diff_from_daily_max"] = current_period_summary[
|
||||||
|
"period_price_diff_from_daily_max"
|
||||||
|
]
|
||||||
|
if "period_price_diff_from_daily_max_%" in current_period_summary:
|
||||||
|
attributes["period_price_diff_from_daily_max_%"] = current_period_summary[
|
||||||
|
"period_price_diff_from_daily_max_%"
|
||||||
|
]
|
||||||
|
|
||||||
# Add interval-specific price_diff attributes (separate from period average)
|
# Add interval-specific price_diff attributes (separate from period average)
|
||||||
# Shows the reference interval's position vs daily min/max:
|
# Shows the reference interval's position vs daily min/max:
|
||||||
|
|
@ -465,8 +533,8 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
attributes["interval_price_diff_from_daily_max"] = current_interval["price_diff_from_max"]
|
attributes["interval_price_diff_from_daily_max"] = current_interval["price_diff_from_max"]
|
||||||
attributes["interval_price_diff_from_daily_max_%"] = current_interval.get("price_diff_from_max_%")
|
attributes["interval_price_diff_from_daily_max_%"] = current_interval.get("price_diff_from_max_%")
|
||||||
|
|
||||||
|
# Meta information at the end
|
||||||
attributes["periods"] = periods_summary
|
attributes["periods"] = periods_summary
|
||||||
attributes["intervals_count"] = len(filtered_result)
|
|
||||||
return attributes
|
return attributes
|
||||||
|
|
||||||
# Fallback if current period not found in summary
|
# Fallback if current period not found in summary
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ from .const import (
|
||||||
DEFAULT_PRICE_RATING_THRESHOLD_LOW,
|
DEFAULT_PRICE_RATING_THRESHOLD_LOW,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
)
|
)
|
||||||
from .period_utils import calculate_periods
|
from .period_utils import PeriodConfig, calculate_periods
|
||||||
from .price_utils import (
|
from .price_utils import (
|
||||||
enrich_price_info_with_differences,
|
enrich_price_info_with_differences,
|
||||||
find_price_data_for_interval,
|
find_price_data_for_interval,
|
||||||
|
|
@ -738,25 +738,39 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Get rating thresholds from config
|
||||||
|
threshold_low = self.config_entry.options.get(
|
||||||
|
CONF_PRICE_RATING_THRESHOLD_LOW,
|
||||||
|
DEFAULT_PRICE_RATING_THRESHOLD_LOW,
|
||||||
|
)
|
||||||
|
threshold_high = self.config_entry.options.get(
|
||||||
|
CONF_PRICE_RATING_THRESHOLD_HIGH,
|
||||||
|
DEFAULT_PRICE_RATING_THRESHOLD_HIGH,
|
||||||
|
)
|
||||||
|
|
||||||
# Calculate best price periods
|
# Calculate best price periods
|
||||||
best_config = self._get_period_config(reverse_sort=False)
|
best_config = self._get_period_config(reverse_sort=False)
|
||||||
best_periods = calculate_periods(
|
best_period_config = PeriodConfig(
|
||||||
all_prices,
|
|
||||||
reverse_sort=False,
|
reverse_sort=False,
|
||||||
flex=best_config["flex"],
|
flex=best_config["flex"],
|
||||||
min_distance_from_avg=best_config["min_distance_from_avg"],
|
min_distance_from_avg=best_config["min_distance_from_avg"],
|
||||||
min_period_length=best_config["min_period_length"],
|
min_period_length=best_config["min_period_length"],
|
||||||
|
threshold_low=threshold_low,
|
||||||
|
threshold_high=threshold_high,
|
||||||
)
|
)
|
||||||
|
best_periods = calculate_periods(all_prices, config=best_period_config)
|
||||||
|
|
||||||
# Calculate peak price periods
|
# Calculate peak price periods
|
||||||
peak_config = self._get_period_config(reverse_sort=True)
|
peak_config = self._get_period_config(reverse_sort=True)
|
||||||
peak_periods = calculate_periods(
|
peak_period_config = PeriodConfig(
|
||||||
all_prices,
|
|
||||||
reverse_sort=True,
|
reverse_sort=True,
|
||||||
flex=peak_config["flex"],
|
flex=peak_config["flex"],
|
||||||
min_distance_from_avg=peak_config["min_distance_from_avg"],
|
min_distance_from_avg=peak_config["min_distance_from_avg"],
|
||||||
min_period_length=peak_config["min_period_length"],
|
min_period_length=peak_config["min_period_length"],
|
||||||
|
threshold_low=threshold_low,
|
||||||
|
threshold_high=threshold_high,
|
||||||
)
|
)
|
||||||
|
peak_periods = calculate_periods(all_prices, config=peak_period_config)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"best_price": best_periods,
|
"best_price": best_periods,
|
||||||
|
|
|
||||||
|
|
@ -4,22 +4,33 @@ from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
from typing import Any
|
from typing import Any, NamedTuple
|
||||||
|
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
|
from .const import DEFAULT_PRICE_RATING_THRESHOLD_HIGH, DEFAULT_PRICE_RATING_THRESHOLD_LOW
|
||||||
|
from .price_utils import aggregate_period_levels, aggregate_period_ratings
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
MINUTES_PER_INTERVAL = 15
|
MINUTES_PER_INTERVAL = 15
|
||||||
|
|
||||||
|
|
||||||
|
class PeriodConfig(NamedTuple):
|
||||||
|
"""Configuration for period calculation."""
|
||||||
|
|
||||||
|
reverse_sort: bool
|
||||||
|
flex: float
|
||||||
|
min_distance_from_avg: float
|
||||||
|
min_period_length: int
|
||||||
|
threshold_low: float = DEFAULT_PRICE_RATING_THRESHOLD_LOW
|
||||||
|
threshold_high: float = DEFAULT_PRICE_RATING_THRESHOLD_HIGH
|
||||||
|
|
||||||
|
|
||||||
def calculate_periods(
|
def calculate_periods(
|
||||||
all_prices: list[dict],
|
all_prices: list[dict],
|
||||||
*,
|
*,
|
||||||
reverse_sort: bool,
|
config: PeriodConfig,
|
||||||
flex: float,
|
|
||||||
min_distance_from_avg: float,
|
|
||||||
min_period_length: int,
|
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Calculate price periods (best or peak) from price data.
|
Calculate price periods (best or peak) from price data.
|
||||||
|
|
@ -37,10 +48,8 @@ def calculate_periods(
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
all_prices: All price data points from yesterday/today/tomorrow
|
all_prices: All price data points from yesterday/today/tomorrow
|
||||||
reverse_sort: True for peak price (max reference), False for best price (min reference)
|
config: Period configuration containing reverse_sort, flex, min_distance_from_avg,
|
||||||
flex: Flexibility threshold as decimal (e.g., 0.05 = 5%)
|
min_period_length, threshold_low, and threshold_high
|
||||||
min_distance_from_avg: Minimum distance from average as percentage (e.g., 10.0 = 10%)
|
|
||||||
min_period_length: Minimum period length in minutes
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict with:
|
Dict with:
|
||||||
|
|
@ -49,6 +58,14 @@ def calculate_periods(
|
||||||
- reference_data: Daily min/max/avg for on-demand annotation
|
- reference_data: Daily min/max/avg for on-demand annotation
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
# Extract config values
|
||||||
|
reverse_sort = config.reverse_sort
|
||||||
|
flex = config.flex
|
||||||
|
min_distance_from_avg = config.min_distance_from_avg
|
||||||
|
min_period_length = config.min_period_length
|
||||||
|
threshold_low = config.threshold_low
|
||||||
|
threshold_high = config.threshold_high
|
||||||
|
|
||||||
if not all_prices:
|
if not all_prices:
|
||||||
return {
|
return {
|
||||||
"periods": [],
|
"periods": [],
|
||||||
|
|
@ -100,7 +117,12 @@ def calculate_periods(
|
||||||
# Step 8: Extract lightweight period summaries (no full price data)
|
# Step 8: Extract lightweight period summaries (no full price data)
|
||||||
# Note: Filtering for current/future is done here based on end date,
|
# Note: Filtering for current/future is done here based on end date,
|
||||||
# not start date. This preserves periods that started yesterday but end today.
|
# not start date. This preserves periods that started yesterday but end today.
|
||||||
period_summaries = _extract_period_summaries(raw_periods)
|
period_summaries = _extract_period_summaries(
|
||||||
|
raw_periods,
|
||||||
|
all_prices_sorted,
|
||||||
|
threshold_low=threshold_low,
|
||||||
|
threshold_high=threshold_high,
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"periods": period_summaries, # Lightweight summaries only
|
"periods": period_summaries, # Lightweight summaries only
|
||||||
|
|
@ -328,7 +350,13 @@ def _filter_periods_by_end_date(periods: list[list[dict]]) -> list[list[dict]]:
|
||||||
return filtered
|
return filtered
|
||||||
|
|
||||||
|
|
||||||
def _extract_period_summaries(periods: list[list[dict]]) -> list[dict]:
|
def _extract_period_summaries(
|
||||||
|
periods: list[list[dict]],
|
||||||
|
all_prices: list[dict],
|
||||||
|
*,
|
||||||
|
threshold_low: float | None,
|
||||||
|
threshold_high: float | None,
|
||||||
|
) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
Extract lightweight period summaries without storing full price data.
|
Extract lightweight period summaries without storing full price data.
|
||||||
|
|
||||||
|
|
@ -336,9 +364,26 @@ def _extract_period_summaries(periods: list[list[dict]]) -> list[dict]:
|
||||||
- start/end timestamps
|
- start/end timestamps
|
||||||
- interval count
|
- interval count
|
||||||
- duration
|
- duration
|
||||||
|
- aggregated level (from API's "level" field)
|
||||||
|
- aggregated rating_level (from calculated "rating_level" field)
|
||||||
|
|
||||||
Sensors can use these summaries to query the actual price data from priceInfo on demand.
|
Sensors can use these summaries to query the actual price data from priceInfo on demand.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
periods: List of periods, where each period is a list of interval dictionaries
|
||||||
|
all_prices: All price data from the API (enriched with level, difference, rating_level)
|
||||||
|
threshold_low: Low threshold for rating level calculation
|
||||||
|
threshold_high: High threshold for rating level calculation
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
# Build lookup dictionary for full price data by timestamp
|
||||||
|
price_lookup: dict[str, dict] = {}
|
||||||
|
for price_data in all_prices:
|
||||||
|
starts_at = dt_util.parse_datetime(price_data["startsAt"])
|
||||||
|
if starts_at:
|
||||||
|
starts_at = dt_util.as_local(starts_at)
|
||||||
|
price_lookup[starts_at.isoformat()] = price_data
|
||||||
|
|
||||||
summaries = []
|
summaries = []
|
||||||
|
|
||||||
for period in periods:
|
for period in periods:
|
||||||
|
|
@ -354,15 +399,44 @@ def _extract_period_summaries(periods: list[list[dict]]) -> list[dict]:
|
||||||
if not start_time or not end_time:
|
if not start_time or not end_time:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Collect interval timestamps
|
||||||
|
interval_starts = [
|
||||||
|
start.isoformat() for interval in period if (start := interval.get("interval_start")) is not None
|
||||||
|
]
|
||||||
|
|
||||||
|
# Look up full price data for each interval in the period
|
||||||
|
period_price_data: list[dict] = []
|
||||||
|
for start_iso in interval_starts:
|
||||||
|
price_data = price_lookup.get(start_iso)
|
||||||
|
if price_data:
|
||||||
|
period_price_data.append(price_data)
|
||||||
|
|
||||||
|
# Calculate aggregated level and rating_level
|
||||||
|
aggregated_level = None
|
||||||
|
aggregated_rating = None
|
||||||
|
|
||||||
|
if period_price_data:
|
||||||
|
# Aggregate level (from API's "level" field)
|
||||||
|
aggregated_level = aggregate_period_levels(period_price_data)
|
||||||
|
|
||||||
|
# Aggregate rating_level (from calculated "rating_level" and "difference" fields)
|
||||||
|
if threshold_low is not None and threshold_high is not None:
|
||||||
|
aggregated_rating, _ = aggregate_period_ratings(
|
||||||
|
period_price_data,
|
||||||
|
threshold_low,
|
||||||
|
threshold_high,
|
||||||
|
)
|
||||||
|
|
||||||
summary = {
|
summary = {
|
||||||
"start": start_time,
|
"start": start_time,
|
||||||
"end": end_time,
|
"end": end_time,
|
||||||
"interval_count": len(period),
|
"interval_count": len(period),
|
||||||
"duration_minutes": len(period) * MINUTES_PER_INTERVAL,
|
"duration_minutes": len(period) * MINUTES_PER_INTERVAL,
|
||||||
# Store interval timestamps for reference (minimal data)
|
# Store interval timestamps for reference (minimal data)
|
||||||
"interval_starts": [
|
"interval_starts": interval_starts,
|
||||||
start.isoformat() for interval in period if (start := interval.get("interval_start")) is not None
|
# Aggregated attributes
|
||||||
],
|
"level": aggregated_level,
|
||||||
|
"rating_level": aggregated_rating,
|
||||||
}
|
}
|
||||||
|
|
||||||
summaries.append(summary)
|
summaries.append(summary)
|
||||||
|
|
|
||||||
|
|
@ -345,6 +345,71 @@ def aggregate_price_rating(differences: list[float], threshold_low: float, thres
|
||||||
return rating_level or PRICE_RATING_NORMAL, avg_difference
|
return rating_level or PRICE_RATING_NORMAL, avg_difference
|
||||||
|
|
||||||
|
|
||||||
|
def aggregate_period_levels(interval_data_list: list[dict[str, Any]]) -> str | None:
|
||||||
|
"""
|
||||||
|
Aggregate price levels across multiple intervals in a period.
|
||||||
|
|
||||||
|
Extracts "level" from each interval and uses the same logic as
|
||||||
|
aggregate_price_levels() to determine the overall level for the period.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
interval_data_list: List of price interval dictionaries with "level" keys
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The aggregated level string in lowercase (e.g., "very_cheap", "normal", "expensive"),
|
||||||
|
or None if no valid levels found
|
||||||
|
|
||||||
|
"""
|
||||||
|
levels: list[str] = []
|
||||||
|
for interval in interval_data_list:
|
||||||
|
level = interval.get("level")
|
||||||
|
if level is not None and isinstance(level, str):
|
||||||
|
levels.append(level)
|
||||||
|
|
||||||
|
if not levels:
|
||||||
|
return None
|
||||||
|
|
||||||
|
aggregated = aggregate_price_levels(levels)
|
||||||
|
# Convert to lowercase for consistency with other enum sensors
|
||||||
|
return aggregated.lower() if aggregated else None
|
||||||
|
|
||||||
|
|
||||||
|
def aggregate_period_ratings(
|
||||||
|
interval_data_list: list[dict[str, Any]],
|
||||||
|
threshold_low: float,
|
||||||
|
threshold_high: float,
|
||||||
|
) -> tuple[str | None, float | None]:
|
||||||
|
"""
|
||||||
|
Aggregate price ratings across multiple intervals in a period.
|
||||||
|
|
||||||
|
Extracts "difference" from each interval and uses the same logic as
|
||||||
|
aggregate_price_rating() to determine the overall rating for the period.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
interval_data_list: List of price interval dictionaries with "difference" keys
|
||||||
|
threshold_low: The low threshold percentage for LOW rating
|
||||||
|
threshold_high: The high threshold percentage for HIGH rating
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (rating_level, average_difference)
|
||||||
|
rating_level: "low", "normal", "high" (lowercase), or None if no valid data
|
||||||
|
average_difference: The averaged difference percentage, or None if no valid data
|
||||||
|
|
||||||
|
"""
|
||||||
|
differences: list[float] = []
|
||||||
|
for interval in interval_data_list:
|
||||||
|
diff = interval.get("difference")
|
||||||
|
if diff is not None:
|
||||||
|
differences.append(float(diff))
|
||||||
|
|
||||||
|
if not differences:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
rating_level, avg_diff = aggregate_price_rating(differences, threshold_low, threshold_high)
|
||||||
|
# Convert to lowercase for consistency with other enum sensors
|
||||||
|
return rating_level.lower() if rating_level else None, avg_diff
|
||||||
|
|
||||||
|
|
||||||
def calculate_price_trend(
|
def calculate_price_trend(
|
||||||
current_price: float,
|
current_price: float,
|
||||||
future_average: float,
|
future_average: float,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue