mirror of
https://github.com/home-assistant/core.git
synced 2026-09-01 18:24:52 -05:00
Improve template reload (#169480)
This commit is contained in:
committed by
Paulus Schoutsen
parent
4f78bbccc0
commit
4d575e69a4
@@ -206,7 +206,7 @@ async def _process_config(hass: HomeAssistant, hass_config: ConfigType) -> None:
|
||||
# Remove old ones
|
||||
if coordinators:
|
||||
for coordinator in coordinators:
|
||||
coordinator.async_remove()
|
||||
await coordinator.async_shutdown()
|
||||
|
||||
async def init_coordinator(
|
||||
hass: HomeAssistant, conf_section: dict[str, Any]
|
||||
|
||||
@@ -59,13 +59,17 @@ class TriggerUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Return unique ID for the entity."""
|
||||
return self.config.get("unique_id")
|
||||
|
||||
@callback
|
||||
def async_remove(self) -> None:
|
||||
"""Signal that the entities need to remove themselves."""
|
||||
async def async_shutdown(self) -> None:
|
||||
"""Shut down the coordinator and clean up resources."""
|
||||
await super().async_shutdown()
|
||||
if self._unsub_start:
|
||||
self._unsub_start()
|
||||
self._unsub_start = None
|
||||
if self._unsub_trigger:
|
||||
self._unsub_trigger()
|
||||
self._unsub_trigger = None
|
||||
if self._script is not None:
|
||||
await self._script.async_stop()
|
||||
|
||||
async def async_setup(self, hass_config: ConfigType) -> None:
|
||||
"""Set up the trigger and create entities."""
|
||||
|
||||
@@ -168,6 +168,11 @@ class AbstractTemplateEntity(Entity):
|
||||
domain,
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
async def async_run_script(
|
||||
self,
|
||||
script: Script,
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
"""Test abstract template entity."""
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.template import entity as abstract_entity
|
||||
from homeassistant.const import SERVICE_RELOAD
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
|
||||
async def test_template_entity_not_implemented(hass: HomeAssistant) -> None:
|
||||
@@ -11,3 +16,70 @@ async def test_template_entity_not_implemented(hass: HomeAssistant) -> None:
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
_ = abstract_entity.AbstractTemplateEntity(hass, {})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[
|
||||
# State-based template light
|
||||
{
|
||||
"template": {
|
||||
"light": {
|
||||
"name": "test_light",
|
||||
"state": "{{ true }}",
|
||||
"turn_on": [
|
||||
{"delay": {"seconds": 120}},
|
||||
],
|
||||
"turn_off": {"event": "turn_off"},
|
||||
},
|
||||
}
|
||||
},
|
||||
# Trigger-based template light
|
||||
{
|
||||
"template": {
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"light": {
|
||||
"name": "test_light",
|
||||
"state": "{{ true }}",
|
||||
"turn_on": [
|
||||
{"delay": {"seconds": 120}},
|
||||
],
|
||||
"turn_off": {"event": "turn_off"},
|
||||
},
|
||||
}
|
||||
},
|
||||
],
|
||||
ids=["state_based", "trigger_based"],
|
||||
)
|
||||
async def test_reload_stops_entity_action_scripts(
|
||||
hass: HomeAssistant, config: dict
|
||||
) -> None:
|
||||
"""Test that reloading stops template entity action scripts."""
|
||||
assert await async_setup_component(hass, "template", config)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity = hass.data["light"].get_entity("light.test_light")
|
||||
assert entity is not None
|
||||
|
||||
# Call turn_on — script will start and hang on delay
|
||||
hass.async_create_task(
|
||||
hass.services.async_call("light", "turn_on", {"entity_id": "light.test_light"})
|
||||
)
|
||||
await asyncio.sleep(0)
|
||||
|
||||
turn_on_script = entity._action_scripts["turn_on"]
|
||||
assert turn_on_script.is_running
|
||||
|
||||
# Reload with empty config removes the entity and stops scripts
|
||||
with patch(
|
||||
"homeassistant.config.load_yaml_config_file",
|
||||
autospec=True,
|
||||
return_value={"template": []},
|
||||
):
|
||||
await hass.services.async_call("template", SERVICE_RELOAD, blocking=True)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert not turn_on_script.is_running
|
||||
assert hass.data["light"].get_entity("light.test_light") is None
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
"""Test trigger template entity."""
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.template import DOMAIN, trigger_entity
|
||||
from homeassistant.components.template import DATA_COORDINATORS, DOMAIN, trigger_entity
|
||||
from homeassistant.components.template.coordinator import TriggerUpdateCoordinator
|
||||
from homeassistant.const import CONF_ICON, CONF_NAME, CONF_STATE, STATE_OFF, STATE_ON
|
||||
from homeassistant.const import (
|
||||
CONF_ICON,
|
||||
CONF_NAME,
|
||||
CONF_STATE,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
SERVICE_RELOAD,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.helpers.trigger_template_entity import CONF_PICTURE
|
||||
@@ -230,3 +241,110 @@ async def test_multiple_template_validators(hass: HomeAssistant) -> None:
|
||||
assert state.state == "opening"
|
||||
assert state.attributes["current_position"] == 50
|
||||
assert state.attributes["current_tilt_position"] == 49
|
||||
|
||||
|
||||
async def test_shutdown_stops_script_and_keeps_triggers_subscribed(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test that HA shutdown stops coordinator scripts without unsubscribing triggers."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
"template",
|
||||
{
|
||||
"template": {
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": [
|
||||
{"event": "action_event"},
|
||||
{"delay": {"seconds": 120}},
|
||||
],
|
||||
"sensor": {
|
||||
"name": "test",
|
||||
"state": "{{ trigger.event.data.value }}",
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify trigger is active
|
||||
listeners = hass.bus.async_listeners()
|
||||
assert listeners.get("test_event", 0) == 1
|
||||
|
||||
# Fire the trigger to start the action script, then yield without
|
||||
# waiting for the script to finish
|
||||
hass.bus.async_fire("test_event", {"value": "hello"})
|
||||
await asyncio.sleep(0)
|
||||
|
||||
# Script should be running (stuck on delay)
|
||||
coordinators = hass.data[DATA_COORDINATORS]
|
||||
assert len(coordinators) == 1
|
||||
assert coordinators[0]._script.is_running
|
||||
|
||||
# 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
|
||||
assert not coordinators[0]._script.is_running
|
||||
|
||||
# Triggers are not unsubscribed on shutdown
|
||||
listeners = hass.bus.async_listeners()
|
||||
assert listeners.get("test_event", 0) == 1
|
||||
|
||||
|
||||
async def test_reload_stops_script_and_unsubscribes_triggers(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test that reloading stops coordinator scripts and unsubscribes old triggers."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
"template",
|
||||
{
|
||||
"template": {
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": [
|
||||
{"event": "action_event"},
|
||||
{"delay": {"seconds": 120}},
|
||||
],
|
||||
"sensor": {
|
||||
"name": "test",
|
||||
"state": "{{ trigger.event.data.value }}",
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify trigger is active
|
||||
listeners = hass.bus.async_listeners()
|
||||
assert listeners.get("test_event", 0) == 1
|
||||
|
||||
# Fire the trigger to start the action script
|
||||
hass.bus.async_fire("test_event", {"value": "hello"})
|
||||
await asyncio.sleep(0)
|
||||
|
||||
# Script should be running
|
||||
coordinators = hass.data[DATA_COORDINATORS]
|
||||
assert len(coordinators) == 1
|
||||
coordinator = coordinators[0]
|
||||
assert coordinator._script.is_running
|
||||
|
||||
# Reload with empty config
|
||||
with patch(
|
||||
"homeassistant.config.load_yaml_config_file",
|
||||
autospec=True,
|
||||
return_value={"template": []},
|
||||
):
|
||||
await hass.services.async_call("template", SERVICE_RELOAD, blocking=True)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Script should be stopped
|
||||
assert not coordinator._script.is_running
|
||||
|
||||
# Old trigger should be unsubscribed
|
||||
listeners = hass.bus.async_listeners()
|
||||
assert listeners.get("test_event", 0) == 0
|
||||
|
||||
Reference in New Issue
Block a user