mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-05-28 18:43:40 +00:00
Remove the DATA_STATISTICS_REVIEW_REQUIRED flag and all associated persistence logic. The flag approach was over-engineered: we cannot detect whether the Recorder statistics have been fixed, and requiring the user to re-save display settings as acknowledgement is bad UX. New design: show the repair notice once when the mode changes. The user dismisses it when done reviewing. The HA Recorder will independently show its own unit-change dialog — that is sufficient. Changes: - Remove DATA_STATISTICS_REVIEW_REQUIRED constant from const.py - Remove _check_statistics_review_repair() from __init__.py - Remove ir import from __init__.py (no longer needed there) - Remove flag set/clear logic from options_flow.py - Change is_persistent=False (no restart persistence needed) - Update all 5 translations: restore simple "Dismiss this notice" ending
108 lines
4 KiB
Python
108 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
|
|
|
|
|
|
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)
|