"""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 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 _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", } ) 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)