diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index f9b6b3b497577..bd19cd40bf2b4 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -2,25 +2,76 @@ import logging from typing import Optional +import voluptuous as vol + from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START from homeassistant.core import CoreState, callback from homeassistant.helpers import ( + config_validation as cv, discovery, + entity_component, trigger as trigger_helper, update_coordinator, ) from homeassistant.helpers.reload import async_setup_reload_service +from homeassistant.helpers.service import async_register_admin_service from .const import CONF_TRIGGER, DOMAIN, PLATFORMS +ATTR_VARIABLES = "variables" +SERVICE_TRIGGER = "trigger" + async def async_setup(hass, config): """Set up the template integration.""" + coordinators = [] + if DOMAIN in config: for conf in config[DOMAIN]: coordinator = TriggerUpdateCoordinator(hass, conf) await coordinator.async_setup(config) + coordinators.append(coordinator) + + async def trigger_coordinator(service_call): + """Trigger an update coordinator.""" + entity_id = service_call.data[ATTR_ENTITY_ID] + + found = None + + for coordinator in coordinators: + if entity_id in coordinator.entity_ids: + found = coordinator + break + + if found is None: + if ATTR_VARIABLES in service_call.data: + raise vol.Invalid( + "Passing variables to state machine based template entities is not allowed" + ) + await entity_component.async_update_entity(hass, entity_id) + return + + found.handle_triggered( + { + **service_call.data.get(ATTR_VARIABLES, {}), + "trigger": {"platform": None}, + }, + context=service_call.context, + ) + + async_register_admin_service( + hass, + DOMAIN, + SERVICE_TRIGGER, + trigger_coordinator, + vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Optional(ATTR_VARIABLES): dict, + } + ), + ) await async_setup_reload_service(hass, DOMAIN, PLATFORMS) @@ -37,6 +88,7 @@ def __init__(self, hass, config): ) self.config = config self._unsub_trigger = None + self.entity_ids = set() @property def unique_id(self) -> Optional[str]: @@ -68,7 +120,7 @@ async def _attach_triggers(self, start_event=None) -> None: self._unsub_trigger = await trigger_helper.async_initialize_triggers( self.hass, self.config[CONF_TRIGGER], - self._handle_triggered, + self.handle_triggered, DOMAIN, self.name, self.logger.log, @@ -76,7 +128,8 @@ async def _attach_triggers(self, start_event=None) -> None: ) @callback - def _handle_triggered(self, run_variables, context=None): + def handle_triggered(self, run_variables, context=None): + """Handle a trigger firing.""" self.async_set_updated_data( {"run_variables": run_variables, "context": context} ) diff --git a/homeassistant/components/template/services.yaml b/homeassistant/components/template/services.yaml index d36111d608e45..10f003c9908ed 100644 --- a/homeassistant/components/template/services.yaml +++ b/homeassistant/components/template/services.yaml @@ -1,3 +1,16 @@ reload: description: Reload all template entities. +trigger: + name: "Trigger updating an entity" + description: Trigger an update of a template entity. If the entity updates based on a trigger, the whole section that configured this entity will be updated. + target: + entity: + integration: template + fields: + variables: + name: Variables + description: Variables to make available to the trigger-based entity. + required: false + selector: + object: diff --git a/homeassistant/components/template/trigger_entity.py b/homeassistant/components/template/trigger_entity.py index 418fa976304d7..b9b8e3cb78ca4 100644 --- a/homeassistant/components/template/trigger_entity.py +++ b/homeassistant/components/template/trigger_entity.py @@ -113,6 +113,8 @@ def extra_state_attributes(self) -> dict[str, Any] | None: async def async_added_to_hass(self) -> None: """Handle being added to Home Assistant.""" template.attach(self.hass, self._config) + self.coordinator.entity_ids.add(self.entity_id) + self.async_on_remove(lambda: self.coordinator.entity_ids.remove(self.entity_id)) await super().async_added_to_hass() if self.coordinator.data is not None: self._handle_coordinator_update() diff --git a/tests/components/template/test_init.py b/tests/components/template/test_init.py index 107c54c710e86..4263d104442a9 100644 --- a/tests/components/template/test_init.py +++ b/tests/components/template/test_init.py @@ -3,8 +3,12 @@ from os import path from unittest.mock import patch +import pytest +import voluptuous as vol + from homeassistant import config from homeassistant.components.template import DOMAIN +from homeassistant.core import Context from homeassistant.helpers.reload import SERVICE_RELOAD from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -305,3 +309,68 @@ async def test_reload_sensors_that_reference_other_template_sensors(hass): def _get_fixtures_base_path(): return path.dirname(path.dirname(path.dirname(__file__))) + + +async def test_manual_trigger_entity(hass): + """Test manually triggering an entity.""" + assert await async_setup_component( + hass, + "template", + { + "template": { + "trigger": [], + "sensors": { + "trigger_based": { + "value_template": "{{ my_var.count }}", + } + }, + }, + "sensor": { + "platform": "template", + "sensors": { + "state_based": { + "value_template": "{{ now() }}", + } + }, + }, + }, + ) + await hass.async_block_till_done() + + orig_state_sensor = hass.states.get("sensor.state_based") + + # Update trigger one + context = Context() + await hass.services.async_call( + "template", + "trigger", + {"entity_id": "sensor.trigger_based", "variables": {"my_var": {"count": 5}}}, + context=context, + blocking=True, + ) + + state = hass.states.get("sensor.trigger_based") + assert state is not None + assert state.state == "5" + assert state.context is context + + # Update state machine one + with pytest.raises(vol.Invalid): + await hass.services.async_call( + "template", + "trigger", + {"entity_id": "sensor.state_based", "variables": {}}, + context=context, + blocking=True, + ) + + await hass.services.async_call( + "template", + "trigger", + {"entity_id": "sensor.state_based"}, + context=context, + blocking=True, + ) + + state = hass.states.get("sensor.state_based") + assert state.state != orig_state_sensor.state