mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-30 05:13:40 +00:00
refactor(coordinator): remove redundant lifecycle callback system
Removed custom lifecycle callback push-update mechanism after confirming it was redundant with Home Assistant's built-in DataUpdateCoordinator pattern. Root cause analysis showed HA's async_update_listeners() is called synchronously (no await) immediately after _async_update_data() returns, making separate lifecycle callbacks unnecessary. Changes: - coordinator/core.py: Removed lifecycle callback methods and notifications - sensor/core.py: Removed lifecycle callback registration and cleanup - sensor/attributes/lifecycle.py: Removed next_tomorrow_check attribute - sensor/calculators/lifecycle.py: Removed get_next_tomorrow_check_time() Impact: Simplified coordinator pattern, no user-visible changes. Standard HA coordinator mechanism provides same immediate update guarantee without custom callback complexity.
This commit is contained in:
parent
f2627a5292
commit
48d6e2580a
4 changed files with 4 additions and 86 deletions
|
|
@ -504,35 +504,6 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||
|
||||
return True
|
||||
|
||||
def register_lifecycle_callback(self, callback: Callable[[], None]) -> Callable[[], None]:
|
||||
"""
|
||||
Register callback for lifecycle state changes (push updates).
|
||||
|
||||
This allows sensors to receive immediate updates when the coordinator's
|
||||
lifecycle state changes, instead of waiting for the next polling cycle.
|
||||
|
||||
Args:
|
||||
callback: Function to call when lifecycle state changes (typically async_write_ha_state)
|
||||
|
||||
Returns:
|
||||
Callable that unregisters the callback when called
|
||||
|
||||
"""
|
||||
if callback not in self._lifecycle_callbacks:
|
||||
self._lifecycle_callbacks.append(callback)
|
||||
|
||||
def unregister() -> None:
|
||||
"""Unregister the lifecycle callback."""
|
||||
if callback in self._lifecycle_callbacks:
|
||||
self._lifecycle_callbacks.remove(callback)
|
||||
|
||||
return unregister
|
||||
|
||||
def _notify_lifecycle_change(self) -> None:
|
||||
"""Notify registered callbacks about lifecycle state change (push update)."""
|
||||
for lifecycle_callback in self._lifecycle_callbacks:
|
||||
lifecycle_callback()
|
||||
|
||||
async def async_shutdown(self) -> None:
|
||||
"""
|
||||
Shut down the coordinator and clean up timers.
|
||||
|
|
@ -661,7 +632,8 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||
if self._last_price_update != old_price_update:
|
||||
self._api_calls_today += 1
|
||||
self._lifecycle_state = "fresh" # Data just fetched
|
||||
self._notify_lifecycle_change() # Push update: fresh data available
|
||||
# No separate lifecycle notification needed - normal async_update_listeners()
|
||||
# will trigger all entities (including lifecycle sensor) after this return
|
||||
|
||||
return result
|
||||
# Subentries get data from main coordinator (no lifecycle tracking - they don't fetch)
|
||||
|
|
@ -675,7 +647,8 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||
# Reset lifecycle state on error
|
||||
self._is_fetching = False
|
||||
self._lifecycle_state = "error"
|
||||
self._notify_lifecycle_change() # Push update: error occurred
|
||||
# No separate lifecycle notification needed - error case returns data
|
||||
# which triggers normal async_update_listeners()
|
||||
return await self._data_fetcher.handle_api_error(
|
||||
err,
|
||||
self._transform_data_for_main_entry,
|
||||
|
|
|
|||
|
|
@ -72,10 +72,6 @@ def build_lifecycle_attributes(
|
|||
if next_poll: # None means data is complete, no more polls needed
|
||||
attributes["next_api_poll"] = next_poll.isoformat()
|
||||
|
||||
next_tomorrow_check = lifecycle_calculator.get_next_tomorrow_check_time()
|
||||
if next_tomorrow_check:
|
||||
attributes["next_tomorrow_check"] = next_tomorrow_check.isoformat()
|
||||
|
||||
next_midnight = lifecycle_calculator.get_next_midnight_turnover_time()
|
||||
attributes["next_midnight_turnover"] = next_midnight.isoformat()
|
||||
|
||||
|
|
|
|||
|
|
@ -181,29 +181,6 @@ class TibberPricesLifecycleCalculator(TibberPricesBaseCalculator):
|
|||
# Fallback: If we don't know timer offset yet, assume 13:00:00
|
||||
return tomorrow_13
|
||||
|
||||
def get_next_tomorrow_check_time(self) -> datetime | None:
|
||||
"""
|
||||
Calculate when the next tomorrow data check will occur.
|
||||
|
||||
Returns None if not applicable (before 13:00 or tomorrow already available).
|
||||
"""
|
||||
coordinator = self.coordinator
|
||||
current_time = coordinator.time.now()
|
||||
now_local = coordinator.time.as_local(current_time)
|
||||
|
||||
# Only relevant after 13:00
|
||||
if now_local.hour < TOMORROW_CHECK_HOUR:
|
||||
return None
|
||||
|
||||
# Only relevant if tomorrow data is missing
|
||||
_, tomorrow_midnight = coordinator.time.get_day_boundaries("today")
|
||||
tomorrow_date = tomorrow_midnight.date()
|
||||
if not coordinator._needs_tomorrow_data(tomorrow_date): # noqa: SLF001 - Internal state access
|
||||
return None
|
||||
|
||||
# Next check = next regular API poll (same as get_next_api_poll_time)
|
||||
return self.get_next_api_poll_time()
|
||||
|
||||
def get_next_midnight_turnover_time(self) -> datetime:
|
||||
"""Calculate when the next midnight turnover will occur."""
|
||||
coordinator = self.coordinator
|
||||
|
|
|
|||
|
|
@ -110,23 +110,11 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
|||
self._value_getter: Callable | None = self._get_value_getter()
|
||||
self._time_sensitive_remove_listener: Callable | None = None
|
||||
self._minute_update_remove_listener: Callable | None = None
|
||||
self._lifecycle_remove_listener: Callable | None = None
|
||||
# Chart data export (for chart_data_export sensor) - from binary_sensor
|
||||
self._chart_data_last_update = None # Track last service call timestamp
|
||||
self._chart_data_error = None # Track last service call error
|
||||
self._chart_data_response = None # Store service response for attributes
|
||||
|
||||
# Register for push updates if this is the lifecycle sensor
|
||||
if entity_description.key == "data_lifecycle_status":
|
||||
self._lifecycle_remove_listener = coordinator.register_lifecycle_callback(self.async_write_ha_state)
|
||||
|
||||
# Register for push updates if this is the chart_data_export sensor
|
||||
# This ensures chart data is refreshed immediately when new price data arrives
|
||||
if entity_description.key == "chart_data_export":
|
||||
self._lifecycle_remove_listener = coordinator.register_lifecycle_callback(
|
||||
self._handle_lifecycle_update_for_chart
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
|
|
@ -161,11 +149,6 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
|||
self._minute_update_remove_listener()
|
||||
self._minute_update_remove_listener = None
|
||||
|
||||
# Remove lifecycle listener if registered
|
||||
if self._lifecycle_remove_listener:
|
||||
self._lifecycle_remove_listener()
|
||||
self._lifecycle_remove_listener = None
|
||||
|
||||
@callback
|
||||
def _handle_time_sensitive_update(self, time_service: TibberPricesTimeService) -> None:
|
||||
"""
|
||||
|
|
@ -200,17 +183,6 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
|||
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _handle_lifecycle_update_for_chart(self) -> None:
|
||||
"""
|
||||
Handle lifecycle state change for chart_data_export sensor.
|
||||
|
||||
When lifecycle state changes (especially to "fresh" after new API data),
|
||||
refresh chart data immediately to ensure charts show latest prices.
|
||||
"""
|
||||
# Schedule async refresh as a task (we're in a callback)
|
||||
self.hass.async_create_task(self._refresh_chart_data())
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
|
|
|
|||
Loading…
Reference in a new issue