diff --git a/supervisor/homeassistant/core.py b/supervisor/homeassistant/core.py index b1df3ea09..447aade51 100644 --- a/supervisor/homeassistant/core.py +++ b/supervisor/homeassistant/core.py @@ -15,7 +15,8 @@ from awesomeversion import AwesomeVersion from supervisor.utils import remove_colors -from ..const import ATTR_HOMEASSISTANT, BusEvent +from ..bus import EventListener +from ..const import ATTR_HOMEASSISTANT, BusEvent, CoreState from ..coresys import CoreSys from ..docker.const import ContainerState from ..docker.homeassistant import DockerHomeAssistant @@ -75,6 +76,7 @@ class HomeAssistantCore(JobGroup): super().__init__(coresys, JOB_GROUP_HOME_ASSISTANT_CORE) self.instance: DockerHomeAssistant = DockerHomeAssistant(coresys) self._error_state: bool = False + self._watchdog_listener: EventListener | None = None @property def error_state(self) -> bool: @@ -83,9 +85,12 @@ class HomeAssistantCore(JobGroup): async def load(self) -> None: """Prepare Home Assistant object.""" - self.sys_bus.register_event( + self._watchdog_listener = self.sys_bus.register_event( BusEvent.DOCKER_CONTAINER_STATE_CHANGE, self.watchdog_container ) + self.sys_bus.register_event( + BusEvent.SUPERVISOR_STATE_CHANGE, self._supervisor_state_changed + ) try: # Evaluate Version if we lost this information @@ -558,6 +563,16 @@ class HomeAssistantCore(JobGroup): if event.state in [ContainerState.FAILED, ContainerState.UNHEALTHY]: await self._restart_after_problem(event.state) + async def _supervisor_state_changed(self, state: CoreState) -> None: + """Handle supervisor state changes to disable watchdog during shutdown.""" + if state in (CoreState.SHUTDOWN, CoreState.STOPPING, CoreState.CLOSE): + if self._watchdog_listener: + _LOGGER.debug( + "Unregistering Home Assistant watchdog due to system shutdown" + ) + self.sys_bus.remove_listener(self._watchdog_listener) + self._watchdog_listener = None + @Job( name="home_assistant_core_restart_after_problem", throttle_period=WATCHDOG_THROTTLE_PERIOD, diff --git a/tests/homeassistant/test_home_assistant_watchdog.py b/tests/homeassistant/test_home_assistant_watchdog.py index 515fc71be..67d043b99 100644 --- a/tests/homeassistant/test_home_assistant_watchdog.py +++ b/tests/homeassistant/test_home_assistant_watchdog.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, PropertyMock, patch from aiodocker.containers import DockerContainer from awesomeversion import AwesomeVersion -from supervisor.const import BusEvent +from supervisor.const import BusEvent, CoreState from supervisor.coresys import CoreSys from supervisor.docker.const import ContainerState from supervisor.docker.monitor import DockerContainerStateEvent @@ -175,3 +175,73 @@ async def test_home_assistant_watchdog_skip_on_load( events.assert_not_called() restart.assert_not_called() start.assert_not_called() + + +async def test_home_assistant_watchdog_unregisters_on_shutdown( + coresys: CoreSys, +) -> None: + """Test home assistant watchdog unregisters when entering shutdown states.""" + coresys.homeassistant.version = AwesomeVersion("2022.7.3") + with ( + patch( + "supervisor.docker.interface.DockerInterface.version", + new=PropertyMock(return_value=AwesomeVersion("2022.7.3")), + ), + patch.object(type(coresys.homeassistant.core.instance), "attach"), + ): + await coresys.homeassistant.core.load() + + coresys.homeassistant.core.watchdog = True + + # Verify watchdog listener is registered + assert coresys.homeassistant.core._watchdog_listener is not None + watchdog_listener = coresys.homeassistant.core._watchdog_listener + + with ( + patch.object(type(coresys.homeassistant.core), "restart") as restart, + patch.object(type(coresys.homeassistant.core), "start") as start, + patch.object( + type(coresys.homeassistant.core.instance), + "current_state", + return_value=ContainerState.FAILED, + ), + ): + # Watchdog should respond to events before shutdown + coresys.bus.fire_event( + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name="homeassistant", + state=ContainerState.FAILED, + id="abc123", + time=1, + ), + ) + await asyncio.sleep(0) + start.assert_called_once() + start.reset_mock() + + # Test each shutdown state + for shutdown_state in (CoreState.SHUTDOWN, CoreState.STOPPING, CoreState.CLOSE): + # Reload to reset listener + coresys.homeassistant.core._watchdog_listener = watchdog_listener + + # Fire shutdown state change + coresys.bus.fire_event(BusEvent.SUPERVISOR_STATE_CHANGE, shutdown_state) + await asyncio.sleep(0) + + # Verify watchdog listener is unregistered + assert coresys.homeassistant.core._watchdog_listener is None + + # Watchdog should not respond to events after shutdown + coresys.bus.fire_event( + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name="homeassistant", + state=ContainerState.FAILED, + id="abc123", + time=1, + ), + ) + await asyncio.sleep(0) + start.assert_not_called() + restart.assert_not_called()