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.
144 lines
4.5 KiB
Python
144 lines
4.5 KiB
Python
"""Chart data export functionality for Tibber Prices sensors."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
import yaml
|
|
|
|
from custom_components.tibber_prices.const import CONF_CHART_DATA_CONFIG, DOMAIN
|
|
|
|
if TYPE_CHECKING:
|
|
from datetime import datetime
|
|
|
|
from custom_components.tibber_prices.coordinator import TibberPricesDataUpdateCoordinator
|
|
from custom_components.tibber_prices.data import TibberPricesConfigEntry
|
|
from homeassistant.core import HomeAssistant
|
|
|
|
|
|
async def call_chartdata_service_async(
|
|
hass: HomeAssistant,
|
|
coordinator: TibberPricesDataUpdateCoordinator,
|
|
config_entry: TibberPricesConfigEntry,
|
|
) -> tuple[dict | None, str | None]:
|
|
"""
|
|
Call get_chartdata service with user-configured YAML (async).
|
|
|
|
Returns:
|
|
Tuple of (response, error_message).
|
|
If successful: (response_dict, None)
|
|
If failed: (None, error_string)
|
|
|
|
"""
|
|
# Get user-configured YAML
|
|
yaml_config = config_entry.options.get(CONF_CHART_DATA_CONFIG, "")
|
|
|
|
# Parse YAML if provided, otherwise use empty dict (service defaults)
|
|
service_params = {}
|
|
if yaml_config and yaml_config.strip():
|
|
try:
|
|
parsed = yaml.safe_load(yaml_config)
|
|
# Ensure we have a dict (yaml.safe_load can return str, int, etc.)
|
|
if isinstance(parsed, dict):
|
|
service_params = parsed
|
|
else:
|
|
coordinator.logger.warning(
|
|
"YAML configuration must be a dictionary, got %s. Using service defaults.",
|
|
type(parsed).__name__,
|
|
)
|
|
service_params = {}
|
|
except yaml.YAMLError as err:
|
|
coordinator.logger.warning(
|
|
"Invalid chart data YAML configuration: %s. Using service defaults.",
|
|
err,
|
|
)
|
|
service_params = {} # Fall back to service defaults
|
|
|
|
# Add required entry_id parameter
|
|
service_params["entry_id"] = config_entry.entry_id
|
|
|
|
# Call get_chartdata service using official HA service system
|
|
try:
|
|
response = await hass.services.async_call(
|
|
DOMAIN,
|
|
"get_chartdata",
|
|
service_params,
|
|
blocking=True,
|
|
return_response=True,
|
|
)
|
|
except Exception as ex:
|
|
coordinator.logger.exception("Chart data service call failed")
|
|
return None, str(ex)
|
|
else:
|
|
return response, None
|
|
|
|
|
|
def get_chart_data_state(
|
|
chart_data_response: dict | None,
|
|
chart_data_error: str | None,
|
|
) -> str | None:
|
|
"""
|
|
Return state for chart_data_export sensor.
|
|
|
|
Args:
|
|
chart_data_response: Last service response (or None)
|
|
chart_data_error: Last error message (or None)
|
|
|
|
Returns:
|
|
"error" if error occurred
|
|
"ready" if data available
|
|
"pending" if no data yet
|
|
|
|
"""
|
|
if chart_data_error:
|
|
return "error"
|
|
if chart_data_response:
|
|
return "ready"
|
|
return "pending"
|
|
|
|
|
|
def build_chart_data_attributes(
|
|
chart_data_response: dict | None,
|
|
chart_data_last_update: datetime | None,
|
|
chart_data_error: str | None,
|
|
) -> dict[str, object] | None:
|
|
"""
|
|
Return chart data from last service call as attributes with metadata.
|
|
|
|
Attribute order: timestamp, error (if any), service data (at the end).
|
|
|
|
Args:
|
|
chart_data_response: Last service response
|
|
chart_data_last_update: Timestamp of last update
|
|
chart_data_error: Error message if service call failed
|
|
|
|
Returns:
|
|
Dict with timestamp, optional error, and service response data.
|
|
|
|
"""
|
|
# Build base attributes with metadata FIRST
|
|
attributes: dict[str, object] = {
|
|
"timestamp": chart_data_last_update.isoformat() if chart_data_last_update else None,
|
|
}
|
|
|
|
# Add error message if service call failed
|
|
if chart_data_error:
|
|
attributes["error"] = chart_data_error
|
|
|
|
if not chart_data_response:
|
|
# No data - only metadata (timestamp, error)
|
|
return attributes
|
|
|
|
# Service data goes LAST - after metadata
|
|
if isinstance(chart_data_response, dict):
|
|
if len(chart_data_response) > 1:
|
|
# Multiple keys → wrap to prevent collision with metadata
|
|
attributes["data"] = chart_data_response
|
|
else:
|
|
# Single key → safe to merge directly
|
|
attributes.update(chart_data_response)
|
|
else:
|
|
# If response is array/list/primitive, wrap it in "data" key
|
|
attributes["data"] = chart_data_response
|
|
|
|
return attributes
|