hass.tibber_prices/custom_components/tibber_prices/sensor/attributes/lifecycle.py
Julian Pawlowski 1d065b11cd fix(services): use injected now in resolve_search_range day offset
_resolve_time_with_day_offset() was calling dt_util.now() internally
instead of using the injected now parameter. This caused incorrect date
calculations in tests and any caller that passes a specific reference time.

Also add missing price_rank_* sensor keys to TIME_SENSITIVE_ENTITY_KEYS
in coordinator/constants.py so quarter-hour refresh is registered for all
11 price rank sensors (current/next/previous interval and hour variants).

Rename dt as dt_utils → dt as dt_util (ICN001) across 11 files to follow
the project-wide import alias convention. Apply ruff auto-fixes for import
ordering and collapsing single-item imports throughout the codebase.

Released-Bug: no
2026-04-14 19:33:24 +00:00

79 lines
3.2 KiB
Python

"""
Attribute builders for lifecycle diagnostic sensor.
This sensor uses event-based updates with state-change filtering to minimize
recorder entries. Only attributes that are relevant to the lifecycle STATE
are included here - attributes that change independently of state belong
in a separate sensor or diagnostics.
Included attributes (update only on state change):
- tomorrow_available: Whether tomorrow's price data is available
- next_api_poll: When the next API poll will occur (builds user trust)
- updates_today: Number of API calls made today
- last_turnover: When the last midnight turnover occurred
- last_error: Details of the last error (if any)
Pool statistics (sensor_intervals_count, cache_fill_percent, etc.) are
intentionally NOT included here because they change independently of
the lifecycle state. With state-change filtering, these would become
stale. Pool statistics are available via diagnostics or could be
exposed as a separate sensor if needed.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from custom_components.tibber_prices.coordinator.core import TibberPricesDataUpdateCoordinator
from custom_components.tibber_prices.sensor.calculators.lifecycle import TibberPricesLifecycleCalculator
def build_lifecycle_attributes(
coordinator: TibberPricesDataUpdateCoordinator,
lifecycle_calculator: TibberPricesLifecycleCalculator,
) -> dict[str, Any]:
"""
Build attributes for data_lifecycle_status sensor.
Event-based updates with state-change filtering - attributes only update
when the lifecycle STATE changes (fresh→cached, cached→turnover_pending, etc.).
Only includes attributes that are directly relevant to the lifecycle state.
Pool statistics are intentionally excluded to avoid stale data.
Returns:
Dict with lifecycle attributes
"""
attributes: dict[str, Any] = {}
# === Tomorrow Data Status ===
# Critical for understanding lifecycle state transitions
attributes["tomorrow_available"] = lifecycle_calculator.has_tomorrow_data()
# === Next API Poll Time ===
# Builds user trust: shows when the integration will check for tomorrow data
# - Before 13:00: Shows today 13:00 (when tomorrow-search begins)
# - After 13:00 without tomorrow data: Shows next Timer #1 execution (active polling)
# - After 13:00 with tomorrow data: Shows tomorrow 13:00 (predictive)
next_poll = lifecycle_calculator.get_next_api_poll_time()
if next_poll:
attributes["next_api_poll"] = next_poll.isoformat()
# === Update Statistics ===
# Shows API activity - resets at midnight with turnover
api_calls = lifecycle_calculator.get_api_calls_today()
attributes["updates_today"] = api_calls
# === Midnight Turnover Info ===
# When was the last successful data rotation
if coordinator._midnight_handler.last_turnover_time: # noqa: SLF001
attributes["last_turnover"] = coordinator._midnight_handler.last_turnover_time.isoformat() # noqa: SLF001
# === Error Status ===
# Present only when there's an active error
if coordinator.last_exception:
attributes["last_error"] = str(coordinator.last_exception)
return attributes