mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-30 05:13:40 +00:00
Massive refactoring of sensor platform reducing core.py from 2,170 to 909 lines (58% reduction). Extracted business logic into specialized calculators and attribute builders following separation of concerns principles. Changes: - Created sensor/calculators/ package (8 specialized calculators, 1,838 lines): * base.py: Abstract BaseCalculator with coordinator access * interval.py: Single interval calculations (current/next/previous) * rolling_hour.py: 5-interval rolling windows * daily_stat.py: Calendar day min/max/avg statistics * window_24h.py: Trailing/leading 24h windows * volatility.py: Price volatility analysis * trend.py: Complex trend analysis with caching (640 lines) * timing.py: Best/peak price period timing * metadata.py: Home/metering metadata - Created sensor/attributes/ package (8 specialized modules, 1,209 lines): * Modules match calculator types for consistent organization * __init__.py: Routing logic + unified builders * Handles state presentation separately from business logic - Created sensor/chart_data.py (144 lines): * Extracted chart data export functionality from entity class * YAML parsing, service calls, metadata formatting - Created sensor/value_getters.py (276 lines): * Centralized handler mapping for all 80+ sensor types * Single source of truth for sensor routing - Extended sensor/helpers.py (+88 lines): * Added aggregate_window_data() unified aggregator * Added get_hourly_price_value() for backward compatibility * Consolidated sensor-specific helper functions - Refactored sensor/core.py (909 lines, was 2,170): * Instantiates all calculators in __init__ * Delegates value calculations to appropriate calculator * Uses unified handler methods via value_getters mapping * Minimal platform-specific logic remains (icon callbacks, entity lifecycle) - Deleted sensor/attributes.py (1,106 lines): * Functionality split into attributes/ package (8 modules) - Updated AGENTS.md: * Documented Calculator Pattern architecture * Added guidance for adding new sensors with calculation groups * Updated file organization with new package structure Architecture Benefits: - Clear separation: Calculators (business logic) vs Attributes (presentation) - Improved testability: Each calculator independently testable - Better maintainability: 21 focused modules vs monolithic file - Easy extensibility: Add sensors by choosing calculation pattern - Reusable components: Calculators and attribute builders shared across sensors Impact: Significantly improved code organization and maintainability while preserving all functionality. All 80+ sensor types continue working with cleaner, more modular architecture. Developer experience improved with logical file structure and clear separation of concerns.
128 lines
4.6 KiB
Python
128 lines
4.6 KiB
Python
"""Volatility attribute builders for Tibber Prices sensors."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import timedelta
|
|
|
|
from custom_components.tibber_prices.utils.price import calculate_volatility_level
|
|
from homeassistant.util import dt as dt_util
|
|
|
|
|
|
def add_volatility_attributes(
|
|
attributes: dict,
|
|
cached_data: dict,
|
|
) -> None:
|
|
"""
|
|
Add attributes for volatility sensors.
|
|
|
|
Args:
|
|
attributes: Dictionary to add attributes to
|
|
cached_data: Dictionary containing cached sensor data
|
|
|
|
"""
|
|
if cached_data.get("volatility_attributes"):
|
|
attributes.update(cached_data["volatility_attributes"])
|
|
|
|
|
|
def get_prices_for_volatility(
|
|
volatility_type: str,
|
|
price_info: dict,
|
|
) -> list[float]:
|
|
"""
|
|
Get price list for volatility calculation based on type.
|
|
|
|
Args:
|
|
volatility_type: One of "today", "tomorrow", "next_24h", "today_tomorrow"
|
|
price_info: Price information dictionary from coordinator data
|
|
|
|
Returns:
|
|
List of prices to analyze
|
|
|
|
"""
|
|
if volatility_type == "today":
|
|
return [float(p["total"]) for p in price_info.get("today", []) if "total" in p]
|
|
|
|
if volatility_type == "tomorrow":
|
|
return [float(p["total"]) for p in price_info.get("tomorrow", []) if "total" in p]
|
|
|
|
if volatility_type == "next_24h":
|
|
# Rolling 24h from now
|
|
now = dt_util.now()
|
|
end_time = now + timedelta(hours=24)
|
|
prices = []
|
|
|
|
for day_key in ["today", "tomorrow"]:
|
|
for price_data in price_info.get(day_key, []):
|
|
starts_at = dt_util.parse_datetime(price_data.get("startsAt"))
|
|
if starts_at is None:
|
|
continue
|
|
starts_at = dt_util.as_local(starts_at)
|
|
|
|
if now <= starts_at < end_time and "total" in price_data:
|
|
prices.append(float(price_data["total"]))
|
|
return prices
|
|
|
|
if volatility_type == "today_tomorrow":
|
|
# Combined today + tomorrow
|
|
prices = []
|
|
for day_key in ["today", "tomorrow"]:
|
|
for price_data in price_info.get(day_key, []):
|
|
if "total" in price_data:
|
|
prices.append(float(price_data["total"]))
|
|
return prices
|
|
|
|
return []
|
|
|
|
|
|
def add_volatility_type_attributes(
|
|
volatility_attributes: dict,
|
|
volatility_type: str,
|
|
price_info: dict,
|
|
thresholds: dict,
|
|
) -> None:
|
|
"""
|
|
Add type-specific attributes for volatility sensors.
|
|
|
|
Args:
|
|
volatility_attributes: Dictionary to add type-specific attributes to
|
|
volatility_type: Type of volatility calculation
|
|
price_info: Price information dictionary from coordinator data
|
|
thresholds: Volatility thresholds configuration
|
|
|
|
"""
|
|
# Add timestamp for calendar day volatility sensors (midnight of the day)
|
|
if volatility_type == "today":
|
|
today_data = price_info.get("today", [])
|
|
if today_data:
|
|
volatility_attributes["timestamp"] = today_data[0].get("startsAt")
|
|
elif volatility_type == "tomorrow":
|
|
tomorrow_data = price_info.get("tomorrow", [])
|
|
if tomorrow_data:
|
|
volatility_attributes["timestamp"] = tomorrow_data[0].get("startsAt")
|
|
elif volatility_type == "today_tomorrow":
|
|
# For combined today+tomorrow, use today's midnight
|
|
today_data = price_info.get("today", [])
|
|
if today_data:
|
|
volatility_attributes["timestamp"] = today_data[0].get("startsAt")
|
|
|
|
# Add breakdown for today vs tomorrow
|
|
today_prices = [float(p["total"]) for p in price_info.get("today", []) if "total" in p]
|
|
tomorrow_prices = [float(p["total"]) for p in price_info.get("tomorrow", []) if "total" in p]
|
|
|
|
if today_prices:
|
|
today_vol = calculate_volatility_level(today_prices, **thresholds)
|
|
today_spread = (max(today_prices) - min(today_prices)) * 100
|
|
volatility_attributes["today_spread"] = round(today_spread, 2)
|
|
volatility_attributes["today_volatility"] = today_vol
|
|
volatility_attributes["interval_count_today"] = len(today_prices)
|
|
|
|
if tomorrow_prices:
|
|
tomorrow_vol = calculate_volatility_level(tomorrow_prices, **thresholds)
|
|
tomorrow_spread = (max(tomorrow_prices) - min(tomorrow_prices)) * 100
|
|
volatility_attributes["tomorrow_spread"] = round(tomorrow_spread, 2)
|
|
volatility_attributes["tomorrow_volatility"] = tomorrow_vol
|
|
volatility_attributes["interval_count_tomorrow"] = len(tomorrow_prices)
|
|
elif volatility_type == "next_24h":
|
|
# Add time window info
|
|
now = dt_util.now()
|
|
volatility_attributes["timestamp"] = now.isoformat()
|