hass.tibber_prices/custom_components/tibber_prices/sensor/attributes/timing.py
Julian Pawlowski c3173a16d6 refactor(attributes): streamline phase type retrieval and attribute building
Consolidate logic for determining current price phase and associated attributes by introducing shared helper functions. This enhances code maintainability and reduces duplication across components.

Impact: Improved clarity and efficiency in price phase handling for users.
2026-04-17 08:52:17 +00:00

110 lines
4 KiB
Python

"""Period timing attribute builders for Tibber Prices sensors."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from custom_components.tibber_prices.entity_utils import add_icon_color_attribute
if TYPE_CHECKING:
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
# Timer #3 triggers every 30 seconds
TIMER_30_SEC_BOUNDARY = 30
# Phase timing sensor keys — allocated once at module level
_PHASE_TIMING_KEYS = frozenset(
{
"current_price_phase_end_time",
"current_price_phase_remaining_minutes",
"current_price_phase_duration",
"current_price_phase_progress",
"next_rising_phase_start_time",
"next_falling_phase_start_time",
"next_flat_phase_start_time",
"next_rising_phase_in_minutes",
"next_falling_phase_in_minutes",
"next_flat_phase_in_minutes",
}
)
def _hours_to_minutes(state_value: Any) -> int | None:
"""Convert hour-based state back to rounded minutes for attributes."""
if state_value is None:
return None
try:
return round(float(state_value) * 60)
except TypeError, ValueError:
return None
def _is_timing_or_volatility_sensor(key: str) -> bool:
"""Check if sensor is a timing or volatility sensor."""
if key.endswith("_volatility"):
return True
# best/peak price timing sensors
if key.startswith(("best_price_", "peak_price_")) and any(
suffix in key for suffix in ["end_time", "remaining_minutes", "progress", "next_start_time", "next_in_minutes"]
):
return True
# price phase timing sensors
return key in _PHASE_TIMING_KEYS
def add_period_timing_attributes(
attributes: dict,
key: str,
state_value: Any = None,
*,
time: TibberPricesTimeService,
) -> None:
"""
Add timestamp and icon_color attributes for best_price/peak_price timing sensors.
The timestamp indicates when the sensor value was calculated:
- Quarter-hour sensors (end_time, next_start_time): Rounded to 15-min boundary (:00, :15, :30, :45)
- 30-second update sensors (remaining_minutes, progress, next_in_minutes): Current time with seconds
Args:
attributes: Dictionary to add attributes to
key: The sensor entity key (e.g., "best_price_end_time")
state_value: Current sensor value for icon_color calculation
time: TibberPricesTimeService instance (required)
"""
# Determine if this is a quarter-hour or 30-second update sensor
# Includes *_start_time to catch next_[type]_phase_start_time keys
is_quarter_hour_sensor = key.endswith(("_end_time", "_next_start_time", "_start_time"))
now = time.now()
if is_quarter_hour_sensor:
# Quarter-hour sensors: Use timestamp of current 15-minute interval
# Round down to the nearest quarter hour (:00, :15, :30, :45)
minute = (now.minute // 15) * 15
timestamp = now.replace(minute=minute, second=0, microsecond=0)
else:
# 30-second update sensors: Round to nearest 30-second boundary (:00 or :30)
# Timer triggers at :00 and :30, so round current time to these boundaries
second = 0 if now.second < TIMER_30_SEC_BOUNDARY else TIMER_30_SEC_BOUNDARY
timestamp = now.replace(second=second, microsecond=0)
attributes["timestamp"] = timestamp
# Add minute-precision attributes for hour-based states to keep automation-friendly values
minute_value = _hours_to_minutes(state_value)
if minute_value is not None:
if key.endswith("period_duration"):
attributes["period_duration_minutes"] = minute_value
elif "phase_duration" in key:
attributes["phase_duration_minutes"] = minute_value
elif key.endswith("remaining_minutes"):
attributes["remaining_minutes"] = minute_value
elif key.endswith("in_minutes"):
attributes["next_in_minutes"] = minute_value
# Add icon_color for dynamic styling
add_icon_color_attribute(attributes, key=key, state_value=state_value)