From cedc4cbe4f2bacd60bb08e8507a1f68895b52ca7 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 30 Sep 2026 16:18:33 +0200 Subject: [PATCH] Cancel coordinator refresh timer on Home Assistant shutdown (#183659) --- homeassistant/helpers/update_coordinator.py | 21 ++++++++++++++++++--- tests/helpers/test_update_coordinator.py | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index cf9223307aa2..2d3df63d3569 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -17,7 +17,14 @@ import requests from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + HassJob, + HassJobType, + HomeAssistant, + callback, +) from homeassistant.exceptions import ( ConfigEntryAuthFailed, ConfigEntryError, @@ -274,12 +281,20 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): self._retry_after = None next_refresh = int(loop.time()) + self._microsecond + update_interval + # Cancelled when Home Assistant stops so a refresh can't fire during + # the close stage, after shared resources like aiohttp sessions are closed + refresh_job = HassJob( + self.__wrap_handle_refresh_interval, + f"{self.name} refresh interval", + job_type=HassJobType.Callback, + cancel_on_shutdown=True, + ) self._unsub_refresh = loop.call_at( - next_refresh, self.__wrap_handle_refresh_interval + next_refresh, self.__wrap_handle_refresh_interval, refresh_job ).cancel @callback - def __wrap_handle_refresh_interval(self) -> None: + def __wrap_handle_refresh_interval(self, _: HassJob) -> None: """Handle a refresh interval occurrence.""" if self.config_entry: self.config_entry.async_create_background_task( diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index d772993f4178..4fc9543b35ee 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -711,6 +711,24 @@ async def test_stop_refresh_on_ha_stop( assert crd.data == 2 +async def test_no_refresh_after_ha_close( + hass: HomeAssistant, crd: update_coordinator.DataUpdateCoordinator[int] +) -> None: + """Test a pending refresh does not fire once Home Assistant has closed.""" + crd.async_add_listener(Mock()) + + async_fire_time_changed(hass, utcnow() + crd.update_interval) + await hass.async_block_till_done() + assert crd.data == 1 + + await hass.async_stop() + + # Shared resources like the aiohttp session are closed at this point + async_fire_time_changed(hass, utcnow() + crd.update_interval * 2) + await hass.async_block_till_done() + assert crd.data == 1 + + @pytest.mark.parametrize( "err_msg", [