diff --git a/homeassistant/components/template/coordinator.py b/homeassistant/components/template/coordinator.py index 4c90870dac8..730f5615a49 100644 --- a/homeassistant/components/template/coordinator.py +++ b/homeassistant/components/template/coordinator.py @@ -1,8 +1,8 @@ """Data update coordinator for trigger based template entities.""" -from collections.abc import Callable, Mapping +from collections.abc import Callable import logging -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, cast from homeassistant.components.blueprint import CONF_USE_BLUEPRINT from homeassistant.const import ( @@ -37,7 +37,7 @@ class TriggerUpdateCoordinator(DataUpdateCoordinator): hass, _LOGGER, config_entry=None, name="Trigger Update Coordinator" ) self.config = config - self._cond_func: Callable[[Mapping[str, Any] | None], bool] | None = None + self._cond_func: condition.ConditionsChecker | None = None self._unsub_start: Callable[[], None] | None = None self._unsub_trigger: Callable[[], None] | None = None self._script: Script | None = None @@ -69,7 +69,9 @@ class TriggerUpdateCoordinator(DataUpdateCoordinator): self._unsub_trigger() self._unsub_trigger = None if self._script is not None: - await self._script.async_stop() + await self._script.async_unload() + if self._cond_func is not None: + self._cond_func.async_unload() async def async_setup(self, hass_config: ConfigType) -> None: """Set up the trigger and create entities.""" @@ -158,7 +160,7 @@ class TriggerUpdateCoordinator(DataUpdateCoordinator): def _check_condition(self, run_variables: TemplateVarsType) -> bool: if not self._cond_func: return True - condition_result = self._cond_func(run_variables) + condition_result = self._cond_func.async_check(variables=run_variables) if condition_result is False: _LOGGER.debug( "Conditions not met, aborting template trigger update. Condition summary: %s", diff --git a/homeassistant/components/template/entity.py b/homeassistant/components/template/entity.py index 9cd86cc25fc..951e2e19195 100644 --- a/homeassistant/components/template/entity.py +++ b/homeassistant/components/template/entity.py @@ -169,9 +169,15 @@ class AbstractTemplateEntity(Entity): ) async def async_will_remove_from_hass(self) -> None: - """Stop scripts when removing from Home Assistant.""" - for action_script in self._action_scripts.values(): - await action_script.async_stop() + """Clean up scripts when removing from Home Assistant.""" + if not self.registry_entry or self.registry_entry.entity_id == self.entity_id: + # Entity ID not changed, unload scripts as they will not be reused. + for action_script in self._action_scripts.values(): + await action_script.async_unload() + else: + # Entity ID changed, just stop scripts + for action_script in self._action_scripts.values(): + await action_script.async_stop() async def async_run_script( self, diff --git a/tests/components/template/test_entity.py b/tests/components/template/test_entity.py index 5e0a8235742..680db0ec4a1 100644 --- a/tests/components/template/test_entity.py +++ b/tests/components/template/test_entity.py @@ -82,4 +82,5 @@ async def test_reload_stops_entity_action_scripts( await hass.async_block_till_done() assert not turn_on_script.is_running + assert turn_on_script._unloaded assert hass.data["light"].get_entity("light.test_light") is None diff --git a/tests/components/template/test_trigger_entity.py b/tests/components/template/test_trigger_entity.py index ece23f573e5..7b4b9cb4ed0 100644 --- a/tests/components/template/test_trigger_entity.py +++ b/tests/components/template/test_trigger_entity.py @@ -1,7 +1,7 @@ """Test trigger template entity.""" import asyncio -from unittest.mock import patch +from unittest.mock import Mock, patch import pytest @@ -17,7 +17,8 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.helpers import template +from homeassistant.helpers import condition, template +from homeassistant.helpers.script import Script from homeassistant.helpers.trigger_template_entity import CONF_PICTURE from homeassistant.setup import async_setup_component @@ -243,6 +244,23 @@ async def test_multiple_template_validators(hass: HomeAssistant) -> None: assert state.attributes["current_tilt_position"] == 49 +async def test_coordinator_shutdown_unloads_script_and_condition( + hass: HomeAssistant, +) -> None: + """Test that coordinator shutdown stops and unloads script and condition.""" + coordinator = TriggerUpdateCoordinator(hass, {}) + + mock_script = Mock(spec=Script) + mock_cond = Mock(spec=condition.ConditionsChecker) + coordinator._script = mock_script + coordinator._cond_func = mock_cond + + await coordinator.async_shutdown() + + mock_script.async_unload.assert_called_once() + mock_cond.async_unload.assert_called_once() + + async def test_shutdown_stops_script_and_keeps_triggers_subscribed( hass: HomeAssistant, ) -> None: @@ -281,13 +299,15 @@ async def test_shutdown_stops_script_and_keeps_triggers_subscribed( coordinators = hass.data[DATA_COORDINATORS] assert len(coordinators) == 1 assert coordinators[0]._script.is_running + assert not coordinators[0]._script._unloaded # Fire shutdown hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() - # Script should be stopped - this is handled by the script helper + # Script should be stopped but not unloaded - this is handled by the script helper assert not coordinators[0]._script.is_running + assert not coordinators[0]._script._unloaded # Triggers are not unsubscribed on shutdown listeners = hass.bus.async_listeners() @@ -332,6 +352,7 @@ async def test_reload_stops_script_and_unsubscribes_triggers( assert len(coordinators) == 1 coordinator = coordinators[0] assert coordinator._script.is_running + assert not coordinator._script._unloaded # Reload with empty config with patch( @@ -342,8 +363,9 @@ async def test_reload_stops_script_and_unsubscribes_triggers( await hass.services.async_call("template", SERVICE_RELOAD, blocking=True) await hass.async_block_till_done() - # Script should be stopped + # Script should be stopped and unloaded assert not coordinator._script.is_running + assert coordinator._script._unloaded # Old trigger should be unsubscribed listeners = hass.bus.async_listeners()