mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-30 13:23:41 +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.
276 lines
15 KiB
Python
276 lines
15 KiB
Python
"""Value getter mapping for Tibber Prices sensors."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from custom_components.tibber_prices.utils.average import (
|
|
calculate_current_leading_avg,
|
|
calculate_current_leading_max,
|
|
calculate_current_leading_min,
|
|
calculate_current_trailing_avg,
|
|
calculate_current_trailing_max,
|
|
calculate_current_trailing_min,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable
|
|
|
|
from custom_components.tibber_prices.sensor.calculators.daily_stat import DailyStatCalculator
|
|
from custom_components.tibber_prices.sensor.calculators.interval import IntervalCalculator
|
|
from custom_components.tibber_prices.sensor.calculators.metadata import MetadataCalculator
|
|
from custom_components.tibber_prices.sensor.calculators.rolling_hour import RollingHourCalculator
|
|
from custom_components.tibber_prices.sensor.calculators.timing import TimingCalculator
|
|
from custom_components.tibber_prices.sensor.calculators.trend import TrendCalculator
|
|
from custom_components.tibber_prices.sensor.calculators.volatility import VolatilityCalculator
|
|
from custom_components.tibber_prices.sensor.calculators.window_24h import Window24hCalculator
|
|
|
|
|
|
def get_value_getter_mapping( # noqa: PLR0913 - needs all calculators as parameters
|
|
interval_calculator: IntervalCalculator,
|
|
rolling_hour_calculator: RollingHourCalculator,
|
|
daily_stat_calculator: DailyStatCalculator,
|
|
window_24h_calculator: Window24hCalculator,
|
|
trend_calculator: TrendCalculator,
|
|
timing_calculator: TimingCalculator,
|
|
volatility_calculator: VolatilityCalculator,
|
|
metadata_calculator: MetadataCalculator,
|
|
get_next_avg_n_hours_value: Callable[[int], float | None],
|
|
get_price_forecast_value: Callable[[], str | None],
|
|
get_data_timestamp: Callable[[], str | None],
|
|
get_chart_data_export_value: Callable[[], str | None],
|
|
) -> dict[str, Callable]:
|
|
"""
|
|
Build mapping from entity key to value getter callable.
|
|
|
|
This function centralizes the handler mapping logic, making it easier to maintain
|
|
and understand the relationship between sensor types and their calculation methods.
|
|
|
|
Args:
|
|
interval_calculator: Calculator for current/next/previous interval values
|
|
rolling_hour_calculator: Calculator for 5-interval rolling windows
|
|
daily_stat_calculator: Calculator for daily min/max/avg statistics
|
|
window_24h_calculator: Calculator for trailing/leading 24h windows
|
|
trend_calculator: Calculator for price trend analysis
|
|
timing_calculator: Calculator for best/peak price period timing
|
|
volatility_calculator: Calculator for price volatility analysis
|
|
metadata_calculator: Calculator for home/metering metadata
|
|
get_next_avg_n_hours_value: Method for next N-hour average forecasts
|
|
get_price_forecast_value: Method for price forecast sensor
|
|
get_data_timestamp: Method for data timestamp sensor
|
|
get_chart_data_export_value: Method for chart data export sensor
|
|
|
|
Returns:
|
|
Dictionary mapping entity keys to their value getter callables.
|
|
|
|
"""
|
|
return {
|
|
# ================================================================
|
|
# INTERVAL-BASED SENSORS - via IntervalCalculator
|
|
# ================================================================
|
|
# Price level sensors
|
|
"current_interval_price_level": interval_calculator.get_price_level_value,
|
|
"next_interval_price_level": lambda: interval_calculator.get_interval_value(
|
|
interval_offset=1, value_type="level"
|
|
),
|
|
"previous_interval_price_level": lambda: interval_calculator.get_interval_value(
|
|
interval_offset=-1, value_type="level"
|
|
),
|
|
# Price sensors (in cents)
|
|
"current_interval_price": lambda: interval_calculator.get_interval_value(
|
|
interval_offset=0, value_type="price", in_euro=False
|
|
),
|
|
"current_interval_price_major": lambda: interval_calculator.get_interval_value(
|
|
interval_offset=0, value_type="price", in_euro=True
|
|
),
|
|
"next_interval_price": lambda: interval_calculator.get_interval_value(
|
|
interval_offset=1, value_type="price", in_euro=False
|
|
),
|
|
"previous_interval_price": lambda: interval_calculator.get_interval_value(
|
|
interval_offset=-1, value_type="price", in_euro=False
|
|
),
|
|
# Rating sensors
|
|
"current_interval_price_rating": lambda: interval_calculator.get_rating_value(rating_type="current"),
|
|
"next_interval_price_rating": lambda: interval_calculator.get_interval_value(
|
|
interval_offset=1, value_type="rating"
|
|
),
|
|
"previous_interval_price_rating": lambda: interval_calculator.get_interval_value(
|
|
interval_offset=-1, value_type="rating"
|
|
),
|
|
# ================================================================
|
|
# ROLLING HOUR SENSORS (5-interval windows) - via RollingHourCalculator
|
|
# ================================================================
|
|
"current_hour_price_level": lambda: rolling_hour_calculator.get_rolling_hour_value(
|
|
hour_offset=0, value_type="level"
|
|
),
|
|
"next_hour_price_level": lambda: rolling_hour_calculator.get_rolling_hour_value(
|
|
hour_offset=1, value_type="level"
|
|
),
|
|
# Rolling hour average (5 intervals: 2 before + current + 2 after)
|
|
"current_hour_average_price": lambda: rolling_hour_calculator.get_rolling_hour_value(
|
|
hour_offset=0, value_type="price"
|
|
),
|
|
"next_hour_average_price": lambda: rolling_hour_calculator.get_rolling_hour_value(
|
|
hour_offset=1, value_type="price"
|
|
),
|
|
"current_hour_price_rating": lambda: rolling_hour_calculator.get_rolling_hour_value(
|
|
hour_offset=0, value_type="rating"
|
|
),
|
|
"next_hour_price_rating": lambda: rolling_hour_calculator.get_rolling_hour_value(
|
|
hour_offset=1, value_type="rating"
|
|
),
|
|
# ================================================================
|
|
# DAILY STATISTICS SENSORS - via DailyStatCalculator
|
|
# ================================================================
|
|
"lowest_price_today": lambda: daily_stat_calculator.get_daily_stat_value(day="today", stat_func=min),
|
|
"highest_price_today": lambda: daily_stat_calculator.get_daily_stat_value(day="today", stat_func=max),
|
|
"average_price_today": lambda: daily_stat_calculator.get_daily_stat_value(
|
|
day="today",
|
|
stat_func=lambda prices: sum(prices) / len(prices),
|
|
),
|
|
# Tomorrow statistics sensors
|
|
"lowest_price_tomorrow": lambda: daily_stat_calculator.get_daily_stat_value(day="tomorrow", stat_func=min),
|
|
"highest_price_tomorrow": lambda: daily_stat_calculator.get_daily_stat_value(day="tomorrow", stat_func=max),
|
|
"average_price_tomorrow": lambda: daily_stat_calculator.get_daily_stat_value(
|
|
day="tomorrow",
|
|
stat_func=lambda prices: sum(prices) / len(prices),
|
|
),
|
|
# Daily aggregated level sensors
|
|
"yesterday_price_level": lambda: daily_stat_calculator.get_daily_aggregated_value(
|
|
day="yesterday", value_type="level"
|
|
),
|
|
"today_price_level": lambda: daily_stat_calculator.get_daily_aggregated_value(day="today", value_type="level"),
|
|
"tomorrow_price_level": lambda: daily_stat_calculator.get_daily_aggregated_value(
|
|
day="tomorrow", value_type="level"
|
|
),
|
|
# Daily aggregated rating sensors
|
|
"yesterday_price_rating": lambda: daily_stat_calculator.get_daily_aggregated_value(
|
|
day="yesterday", value_type="rating"
|
|
),
|
|
"today_price_rating": lambda: daily_stat_calculator.get_daily_aggregated_value(
|
|
day="today", value_type="rating"
|
|
),
|
|
"tomorrow_price_rating": lambda: daily_stat_calculator.get_daily_aggregated_value(
|
|
day="tomorrow", value_type="rating"
|
|
),
|
|
# ================================================================
|
|
# 24H WINDOW SENSORS (trailing/leading from current) - via Window24hCalculator
|
|
# ================================================================
|
|
# Trailing and leading average sensors
|
|
"trailing_price_average": lambda: window_24h_calculator.get_24h_window_value(
|
|
stat_func=calculate_current_trailing_avg,
|
|
),
|
|
"leading_price_average": lambda: window_24h_calculator.get_24h_window_value(
|
|
stat_func=calculate_current_leading_avg,
|
|
),
|
|
# Trailing and leading min/max sensors
|
|
"trailing_price_min": lambda: window_24h_calculator.get_24h_window_value(
|
|
stat_func=calculate_current_trailing_min,
|
|
),
|
|
"trailing_price_max": lambda: window_24h_calculator.get_24h_window_value(
|
|
stat_func=calculate_current_trailing_max,
|
|
),
|
|
"leading_price_min": lambda: window_24h_calculator.get_24h_window_value(
|
|
stat_func=calculate_current_leading_min,
|
|
),
|
|
"leading_price_max": lambda: window_24h_calculator.get_24h_window_value(
|
|
stat_func=calculate_current_leading_max,
|
|
),
|
|
# ================================================================
|
|
# FUTURE FORECAST SENSORS
|
|
# ================================================================
|
|
# Future average sensors (next N hours from next interval)
|
|
"next_avg_1h": lambda: get_next_avg_n_hours_value(hours=1),
|
|
"next_avg_2h": lambda: get_next_avg_n_hours_value(hours=2),
|
|
"next_avg_3h": lambda: get_next_avg_n_hours_value(hours=3),
|
|
"next_avg_4h": lambda: get_next_avg_n_hours_value(hours=4),
|
|
"next_avg_5h": lambda: get_next_avg_n_hours_value(hours=5),
|
|
"next_avg_6h": lambda: get_next_avg_n_hours_value(hours=6),
|
|
"next_avg_8h": lambda: get_next_avg_n_hours_value(hours=8),
|
|
"next_avg_12h": lambda: get_next_avg_n_hours_value(hours=12),
|
|
# Current and next trend change sensors
|
|
"current_price_trend": trend_calculator.get_current_trend_value,
|
|
"next_price_trend_change": trend_calculator.get_next_trend_change_value,
|
|
# Price trend sensors
|
|
"price_trend_1h": lambda: trend_calculator.get_price_trend_value(hours=1),
|
|
"price_trend_2h": lambda: trend_calculator.get_price_trend_value(hours=2),
|
|
"price_trend_3h": lambda: trend_calculator.get_price_trend_value(hours=3),
|
|
"price_trend_4h": lambda: trend_calculator.get_price_trend_value(hours=4),
|
|
"price_trend_5h": lambda: trend_calculator.get_price_trend_value(hours=5),
|
|
"price_trend_6h": lambda: trend_calculator.get_price_trend_value(hours=6),
|
|
"price_trend_8h": lambda: trend_calculator.get_price_trend_value(hours=8),
|
|
"price_trend_12h": lambda: trend_calculator.get_price_trend_value(hours=12),
|
|
# Diagnostic sensors
|
|
"data_timestamp": get_data_timestamp,
|
|
# Price forecast sensor
|
|
"price_forecast": get_price_forecast_value,
|
|
# Home metadata sensors (via MetadataCalculator)
|
|
"home_type": lambda: metadata_calculator.get_home_metadata_value("type"),
|
|
"home_size": lambda: metadata_calculator.get_home_metadata_value("size"),
|
|
"main_fuse_size": lambda: metadata_calculator.get_home_metadata_value("mainFuseSize"),
|
|
"number_of_residents": lambda: metadata_calculator.get_home_metadata_value("numberOfResidents"),
|
|
"primary_heating_source": lambda: metadata_calculator.get_home_metadata_value("primaryHeatingSource"),
|
|
# Metering point sensors (via MetadataCalculator)
|
|
"grid_company": lambda: metadata_calculator.get_metering_point_value("gridCompany"),
|
|
"grid_area_code": lambda: metadata_calculator.get_metering_point_value("gridAreaCode"),
|
|
"price_area_code": lambda: metadata_calculator.get_metering_point_value("priceAreaCode"),
|
|
"consumption_ean": lambda: metadata_calculator.get_metering_point_value("consumptionEan"),
|
|
"production_ean": lambda: metadata_calculator.get_metering_point_value("productionEan"),
|
|
"energy_tax_type": lambda: metadata_calculator.get_metering_point_value("energyTaxType"),
|
|
"vat_type": lambda: metadata_calculator.get_metering_point_value("vatType"),
|
|
"estimated_annual_consumption": lambda: metadata_calculator.get_metering_point_value(
|
|
"estimatedAnnualConsumption"
|
|
),
|
|
# Subscription sensors (via MetadataCalculator)
|
|
"subscription_status": lambda: metadata_calculator.get_subscription_value("status"),
|
|
# Volatility sensors (via VolatilityCalculator)
|
|
"today_volatility": lambda: volatility_calculator.get_volatility_value(volatility_type="today"),
|
|
"tomorrow_volatility": lambda: volatility_calculator.get_volatility_value(volatility_type="tomorrow"),
|
|
"next_24h_volatility": lambda: volatility_calculator.get_volatility_value(volatility_type="next_24h"),
|
|
"today_tomorrow_volatility": lambda: volatility_calculator.get_volatility_value(
|
|
volatility_type="today_tomorrow"
|
|
),
|
|
# ================================================================
|
|
# BEST/PEAK PRICE TIMING SENSORS - via TimingCalculator
|
|
# ================================================================
|
|
# Best Price timing sensors
|
|
"best_price_end_time": lambda: timing_calculator.get_period_timing_value(
|
|
period_type="best_price", value_type="end_time"
|
|
),
|
|
"best_price_period_duration": lambda: timing_calculator.get_period_timing_value(
|
|
period_type="best_price", value_type="period_duration"
|
|
),
|
|
"best_price_remaining_minutes": lambda: timing_calculator.get_period_timing_value(
|
|
period_type="best_price", value_type="remaining_minutes"
|
|
),
|
|
"best_price_progress": lambda: timing_calculator.get_period_timing_value(
|
|
period_type="best_price", value_type="progress"
|
|
),
|
|
"best_price_next_start_time": lambda: timing_calculator.get_period_timing_value(
|
|
period_type="best_price", value_type="next_start_time"
|
|
),
|
|
"best_price_next_in_minutes": lambda: timing_calculator.get_period_timing_value(
|
|
period_type="best_price", value_type="next_in_minutes"
|
|
),
|
|
# Peak Price timing sensors
|
|
"peak_price_end_time": lambda: timing_calculator.get_period_timing_value(
|
|
period_type="peak_price", value_type="end_time"
|
|
),
|
|
"peak_price_period_duration": lambda: timing_calculator.get_period_timing_value(
|
|
period_type="peak_price", value_type="period_duration"
|
|
),
|
|
"peak_price_remaining_minutes": lambda: timing_calculator.get_period_timing_value(
|
|
period_type="peak_price", value_type="remaining_minutes"
|
|
),
|
|
"peak_price_progress": lambda: timing_calculator.get_period_timing_value(
|
|
period_type="peak_price", value_type="progress"
|
|
),
|
|
"peak_price_next_start_time": lambda: timing_calculator.get_period_timing_value(
|
|
period_type="peak_price", value_type="next_start_time"
|
|
),
|
|
"peak_price_next_in_minutes": lambda: timing_calculator.get_period_timing_value(
|
|
period_type="peak_price", value_type="next_in_minutes"
|
|
),
|
|
# Chart data export sensor
|
|
"chart_data_export": get_chart_data_export_value,
|
|
}
|