mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-29 21:03: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
|
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:
|
async def async_shutdown(self) -> None:
|
||||||
"""
|
"""
|
||||||
Shut down the coordinator and clean up timers.
|
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:
|
if self._last_price_update != old_price_update:
|
||||||
self._api_calls_today += 1
|
self._api_calls_today += 1
|
||||||
self._lifecycle_state = "fresh" # Data just fetched
|
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
|
return result
|
||||||
# Subentries get data from main coordinator (no lifecycle tracking - they don't fetch)
|
# 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
|
# Reset lifecycle state on error
|
||||||
self._is_fetching = False
|
self._is_fetching = False
|
||||||
self._lifecycle_state = "error"
|
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(
|
return await self._data_fetcher.handle_api_error(
|
||||||
err,
|
err,
|
||||||
self._transform_data_for_main_entry,
|
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
|
if next_poll: # None means data is complete, no more polls needed
|
||||||
attributes["next_api_poll"] = next_poll.isoformat()
|
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()
|
next_midnight = lifecycle_calculator.get_next_midnight_turnover_time()
|
||||||
attributes["next_midnight_turnover"] = next_midnight.isoformat()
|
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
|
# Fallback: If we don't know timer offset yet, assume 13:00:00
|
||||||
return tomorrow_13
|
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:
|
def get_next_midnight_turnover_time(self) -> datetime:
|
||||||
"""Calculate when the next midnight turnover will occur."""
|
"""Calculate when the next midnight turnover will occur."""
|
||||||
coordinator = self.coordinator
|
coordinator = self.coordinator
|
||||||
|
|
|
||||||
|
|
@ -110,23 +110,11 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
self._value_getter: Callable | None = self._get_value_getter()
|
self._value_getter: Callable | None = self._get_value_getter()
|
||||||
self._time_sensitive_remove_listener: Callable | None = None
|
self._time_sensitive_remove_listener: Callable | None = None
|
||||||
self._minute_update_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
|
# 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_last_update = None # Track last service call timestamp
|
||||||
self._chart_data_error = None # Track last service call error
|
self._chart_data_error = None # Track last service call error
|
||||||
self._chart_data_response = None # Store service response for attributes
|
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:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""When entity is added to hass."""
|
"""When entity is added to hass."""
|
||||||
await super().async_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()
|
||||||
self._minute_update_remove_listener = None
|
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
|
@callback
|
||||||
def _handle_time_sensitive_update(self, time_service: TibberPricesTimeService) -> None:
|
def _handle_time_sensitive_update(self, time_service: TibberPricesTimeService) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
@ -200,17 +183,6 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
|
|
||||||
self.async_write_ha_state()
|
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
|
@callback
|
||||||
def _handle_coordinator_update(self) -> None:
|
def _handle_coordinator_update(self) -> None:
|
||||||
"""Handle updated data from the coordinator."""
|
"""Handle updated data from the coordinator."""
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue