mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-29 21:03:40 +00:00
Add two new service actions for intelligent device scheduling: - find_best_start: Find optimal start time for run-once appliances - Considers price, optional energy estimates, optional PV power - Supports flexible time windows (HH:MM, ISO datetime, with/without timezone) - Prefers future candidates over past ones - Includes current interval by default (configurable) - Returns recommended start time with cost analysis - plan_charging: Create optimized charging schedule for energy storage - Supports EV, home battery, balcony battery use cases - Energy target or duration-based planning - Split or continuous charging modes - Efficiency factor support - Includes current interval by default (configurable) - Returns detailed slot-by-slot charging plan Common improvements: - Flexible datetime parsing (ISO 8601, with/without timezone, microseconds) - Time selector in GUI (better UX than text field) - Currency display based on config entry settings - Comprehensive error handling and validation - Detailed response envelopes with warnings/errors Impact: Users can automate appliance scheduling based on electricity prices without external automation rules.
121 lines
3.9 KiB
Python
121 lines
3.9 KiB
Python
"""
|
|
Shared utilities for service handlers.
|
|
|
|
This module provides common helper functions used across multiple service handlers,
|
|
such as entry validation and data extraction.
|
|
|
|
Functions:
|
|
resolve_entry_id: Auto-resolve entry_id when only one config entry exists
|
|
get_entry_and_data: Validate config entry and extract coordinator data
|
|
|
|
Used by:
|
|
- services/chartdata.py: Chart data export service
|
|
- services/apexcharts.py: ApexCharts YAML generation
|
|
- services/refresh_user_data.py: User data refresh
|
|
- services/find_best_start.py: Find best start time
|
|
- services/plan_charging.py: Plan charging
|
|
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from custom_components.tibber_prices.const import DOMAIN
|
|
from custom_components.tibber_prices.coordinator.helpers import get_intervals_for_day_offsets
|
|
from homeassistant.exceptions import ServiceValidationError
|
|
|
|
if TYPE_CHECKING:
|
|
from custom_components.tibber_prices.coordinator import TibberPricesDataUpdateCoordinator
|
|
from homeassistant.core import HomeAssistant
|
|
|
|
|
|
def resolve_entry_id(hass: HomeAssistant, entry_id: str | None) -> str:
|
|
"""
|
|
Resolve entry_id, auto-selecting if only one config entry exists.
|
|
|
|
This provides a user-friendly experience where entry_id is optional
|
|
when only a single Tibber home is configured.
|
|
|
|
Args:
|
|
hass: Home Assistant instance
|
|
entry_id: Config entry ID (optional)
|
|
|
|
Returns:
|
|
Resolved entry_id string
|
|
|
|
Raises:
|
|
ServiceValidationError: If no entries exist or multiple entries exist without entry_id
|
|
|
|
"""
|
|
entries = hass.config_entries.async_entries(DOMAIN)
|
|
|
|
if not entries:
|
|
raise ServiceValidationError(translation_domain=DOMAIN, translation_key="no_entries_configured")
|
|
|
|
# If entry_id provided, use it (will be validated by get_entry_and_data)
|
|
if entry_id:
|
|
return entry_id
|
|
|
|
# Auto-select if only one entry exists
|
|
if len(entries) == 1:
|
|
return entries[0].entry_id
|
|
|
|
# Multiple entries: require explicit entry_id
|
|
# Build a helpful error message listing available entries
|
|
entry_list = ", ".join(f"{e.title} ({e.entry_id})" for e in entries)
|
|
raise ServiceValidationError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="multiple_entries_require_entry_id",
|
|
translation_placeholders={"entries": entry_list},
|
|
)
|
|
|
|
|
|
def get_entry_and_data(hass: HomeAssistant, entry_id: str | None) -> tuple[Any, Any, dict]:
|
|
"""
|
|
Validate entry and extract coordinator and data.
|
|
|
|
If entry_id is None or empty, auto-resolves when only one entry exists.
|
|
|
|
Args:
|
|
hass: Home Assistant instance
|
|
entry_id: Config entry ID to validate (optional if single entry)
|
|
|
|
Returns:
|
|
Tuple of (entry, coordinator, data)
|
|
|
|
Raises:
|
|
ServiceValidationError: If entry_id cannot be resolved or is invalid
|
|
|
|
"""
|
|
# Auto-resolve entry_id if not provided
|
|
resolved_entry_id = resolve_entry_id(hass, entry_id)
|
|
|
|
entry = next(
|
|
(e for e in hass.config_entries.async_entries(DOMAIN) if e.entry_id == resolved_entry_id),
|
|
None,
|
|
)
|
|
if not entry or not hasattr(entry, "runtime_data") or not entry.runtime_data:
|
|
raise ServiceValidationError(translation_domain=DOMAIN, translation_key="invalid_entry_id")
|
|
coordinator = entry.runtime_data.coordinator
|
|
data = coordinator.data or {}
|
|
return entry, coordinator, data
|
|
|
|
|
|
def has_tomorrow_data(coordinator: TibberPricesDataUpdateCoordinator) -> bool:
|
|
"""
|
|
Check if tomorrow's price data is available in coordinator.
|
|
|
|
Uses get_intervals_for_day_offsets() to automatically determine tomorrow
|
|
based on current date.
|
|
|
|
Args:
|
|
coordinator: TibberPricesDataUpdateCoordinator instance
|
|
|
|
Returns:
|
|
True if tomorrow's data exists (at least one interval), False otherwise
|
|
|
|
"""
|
|
coordinator_data = coordinator.data or {}
|
|
tomorrow_intervals = get_intervals_for_day_offsets(coordinator_data, [1])
|
|
return len(tomorrow_intervals) > 0
|