mirror of
https://github.com/home-assistant/core.git
synced 2025-04-19 14:57:52 +00:00
Add abstract class to trigger based template entities (#139650)
* add abstract class to trigger based template entities * updates after merge of parent PR * add comments * add tests
This commit is contained in:
parent
48865e00b6
commit
aec6868af1
@ -122,9 +122,15 @@ async def _async_resolve_blueprints(
|
||||
raise vol.Invalid("more than one platform defined per blueprint")
|
||||
if len(platforms) == 1:
|
||||
platform = platforms.pop()
|
||||
for prop in (CONF_NAME, CONF_UNIQUE_ID, CONF_VARIABLES):
|
||||
for prop in (CONF_NAME, CONF_UNIQUE_ID):
|
||||
if prop in config:
|
||||
config[platform][prop] = config.pop(prop)
|
||||
# For regular template entities, CONF_VARIABLES should be removed because they just
|
||||
# house input results for template entities. For Trigger based template entities
|
||||
# CONF_VARIABLES should not be removed because the variables are always
|
||||
# executed between the trigger and action.
|
||||
if CONF_TRIGGER not in config and CONF_VARIABLES in config:
|
||||
config[platform][CONF_VARIABLES] = config.pop(CONF_VARIABLES)
|
||||
raw_config = dict(config)
|
||||
|
||||
template_config = TemplateConfig(CONFIG_SECTION_SCHEMA(config))
|
||||
|
@ -2,12 +2,14 @@
|
||||
|
||||
from collections.abc import Callable, Mapping
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.components.blueprint import CONF_USE_BLUEPRINT
|
||||
from homeassistant.const import CONF_PATH, CONF_VARIABLES, EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.core import Context, CoreState, Event, HomeAssistant, callback
|
||||
from homeassistant.helpers import condition, discovery, trigger as trigger_helper
|
||||
from homeassistant.helpers.script import Script
|
||||
from homeassistant.helpers.script_variables import ScriptVariables
|
||||
from homeassistant.helpers.trace import trace_get
|
||||
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
@ -22,7 +24,7 @@ class TriggerUpdateCoordinator(DataUpdateCoordinator):
|
||||
|
||||
REMOVE_TRIGGER = object()
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: dict[str, Any]) -> None:
|
||||
def __init__(self, hass: HomeAssistant, config: ConfigType) -> None:
|
||||
"""Instantiate trigger data."""
|
||||
super().__init__(
|
||||
hass, _LOGGER, config_entry=None, name="Trigger Update Coordinator"
|
||||
@ -32,6 +34,18 @@ class TriggerUpdateCoordinator(DataUpdateCoordinator):
|
||||
self._unsub_start: Callable[[], None] | None = None
|
||||
self._unsub_trigger: Callable[[], None] | None = None
|
||||
self._script: Script | None = None
|
||||
self._run_variables: ScriptVariables | None = None
|
||||
self._blueprint_inputs: dict | None = None
|
||||
if config is not None:
|
||||
self._run_variables = config.get(CONF_VARIABLES)
|
||||
self._blueprint_inputs = getattr(config, "raw_blueprint_inputs", None)
|
||||
|
||||
@property
|
||||
def referenced_blueprint(self) -> str | None:
|
||||
"""Return referenced blueprint or None."""
|
||||
if self._blueprint_inputs is None:
|
||||
return None
|
||||
return cast(str, self._blueprint_inputs[CONF_USE_BLUEPRINT][CONF_PATH])
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str | None:
|
||||
@ -104,6 +118,10 @@ class TriggerUpdateCoordinator(DataUpdateCoordinator):
|
||||
async def _handle_triggered_with_script(
|
||||
self, run_variables: TemplateVarsType, context: Context | None = None
|
||||
) -> None:
|
||||
# Render run variables after the trigger, before checking conditions.
|
||||
if self._run_variables:
|
||||
run_variables = self._run_variables.async_render(self.hass, run_variables)
|
||||
|
||||
if not self._check_condition(run_variables):
|
||||
return
|
||||
# Create a context referring to the trigger context.
|
||||
@ -119,6 +137,9 @@ class TriggerUpdateCoordinator(DataUpdateCoordinator):
|
||||
async def _handle_triggered(
|
||||
self, run_variables: TemplateVarsType, context: Context | None = None
|
||||
) -> None:
|
||||
if self._run_variables:
|
||||
run_variables = self._run_variables.async_render(self.hass, run_variables)
|
||||
|
||||
if not self._check_condition(run_variables):
|
||||
return
|
||||
self._execute_update(run_variables, context)
|
||||
|
@ -9,7 +9,7 @@ from homeassistant.helpers.entity_platform import async_get_platforms
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
|
||||
from .const import DOMAIN, TEMPLATE_BLUEPRINT_SCHEMA
|
||||
from .template_entity import TemplateEntity
|
||||
from .entity import AbstractTemplateEntity
|
||||
|
||||
DATA_BLUEPRINTS = "template_blueprints"
|
||||
|
||||
@ -23,7 +23,7 @@ def templates_with_blueprint(hass: HomeAssistant, blueprint_path: str) -> list[s
|
||||
entity_id
|
||||
for platform in async_get_platforms(hass, DOMAIN)
|
||||
for entity_id, template_entity in platform.entities.items()
|
||||
if isinstance(template_entity, TemplateEntity)
|
||||
if isinstance(template_entity, AbstractTemplateEntity)
|
||||
and template_entity.referenced_blueprint == blueprint_path
|
||||
]
|
||||
|
||||
@ -33,7 +33,8 @@ def blueprint_in_template(hass: HomeAssistant, entity_id: str) -> str | None:
|
||||
"""Return the blueprint the template entity is based on or None."""
|
||||
for platform in async_get_platforms(hass, DOMAIN):
|
||||
if isinstance(
|
||||
(template_entity := platform.entities.get(entity_id)), TemplateEntity
|
||||
(template_entity := platform.entities.get(entity_id)),
|
||||
AbstractTemplateEntity,
|
||||
):
|
||||
return template_entity.referenced_blueprint
|
||||
return None
|
||||
|
@ -31,7 +31,6 @@ from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
AddEntitiesCallback,
|
||||
)
|
||||
from homeassistant.helpers.script import Script
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import TriggerUpdateCoordinator
|
||||
@ -236,12 +235,8 @@ class TriggerNumberEntity(TriggerEntity, NumberEntity):
|
||||
"""Initialize the entity."""
|
||||
super().__init__(hass, coordinator, config)
|
||||
|
||||
self._command_set_value = Script(
|
||||
hass,
|
||||
config[CONF_SET_VALUE],
|
||||
self._rendered.get(CONF_NAME, DEFAULT_NAME),
|
||||
DOMAIN,
|
||||
)
|
||||
name = self._rendered.get(CONF_NAME, DEFAULT_NAME)
|
||||
self.add_script(CONF_SET_VALUE, config[CONF_SET_VALUE], name, DOMAIN)
|
||||
|
||||
self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT)
|
||||
|
||||
@ -276,6 +271,9 @@ class TriggerNumberEntity(TriggerEntity, NumberEntity):
|
||||
if self._config[CONF_OPTIMISTIC]:
|
||||
self._attr_native_value = value
|
||||
self.async_write_ha_state()
|
||||
await self._command_set_value.async_run(
|
||||
{ATTR_VALUE: value}, context=self._context
|
||||
)
|
||||
if set_value := self._action_scripts.get(CONF_SET_VALUE):
|
||||
await self.async_run_script(
|
||||
set_value,
|
||||
run_variables={ATTR_VALUE: value},
|
||||
context=self._context,
|
||||
)
|
||||
|
@ -28,7 +28,6 @@ from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
AddEntitiesCallback,
|
||||
)
|
||||
from homeassistant.helpers.script import Script
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import TriggerUpdateCoordinator
|
||||
@ -198,12 +197,13 @@ class TriggerSelectEntity(TriggerEntity, SelectEntity):
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(hass, coordinator, config)
|
||||
self._command_select_option = Script(
|
||||
hass,
|
||||
config[CONF_SELECT_OPTION],
|
||||
self._rendered.get(CONF_NAME, DEFAULT_NAME),
|
||||
DOMAIN,
|
||||
)
|
||||
if select_option := config.get(CONF_SELECT_OPTION):
|
||||
self.add_script(
|
||||
CONF_SELECT_OPTION,
|
||||
select_option,
|
||||
self._rendered.get(CONF_NAME, DEFAULT_NAME),
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
@ -220,6 +220,9 @@ class TriggerSelectEntity(TriggerEntity, SelectEntity):
|
||||
if self._config[CONF_OPTIMISTIC]:
|
||||
self._attr_current_option = option
|
||||
self.async_write_ha_state()
|
||||
await self._command_select_option.async_run(
|
||||
{ATTR_OPTION: option}, context=self._context
|
||||
)
|
||||
if select_option := self._action_scripts.get(CONF_SELECT_OPTION):
|
||||
await self.async_run_script(
|
||||
select_option,
|
||||
run_variables={ATTR_OPTION: option},
|
||||
context=self._context,
|
||||
)
|
||||
|
@ -8,10 +8,13 @@ from homeassistant.helpers.trigger_template_entity import TriggerBaseEntity
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import TriggerUpdateCoordinator
|
||||
from .entity import AbstractTemplateEntity
|
||||
|
||||
|
||||
class TriggerEntity( # pylint: disable=hass-enforce-class-module
|
||||
TriggerBaseEntity, CoordinatorEntity[TriggerUpdateCoordinator]
|
||||
TriggerBaseEntity,
|
||||
CoordinatorEntity[TriggerUpdateCoordinator],
|
||||
AbstractTemplateEntity,
|
||||
):
|
||||
"""Template entity based on trigger data."""
|
||||
|
||||
@ -24,6 +27,7 @@ class TriggerEntity( # pylint: disable=hass-enforce-class-module
|
||||
"""Initialize the entity."""
|
||||
CoordinatorEntity.__init__(self, coordinator)
|
||||
TriggerBaseEntity.__init__(self, hass, config)
|
||||
AbstractTemplateEntity.__init__(self, hass)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle being added to Home Assistant."""
|
||||
@ -38,6 +42,16 @@ class TriggerEntity( # pylint: disable=hass-enforce-class-module
|
||||
else:
|
||||
self._unique_id = unique_id
|
||||
|
||||
@property
|
||||
def referenced_blueprint(self) -> str | None:
|
||||
"""Return referenced blueprint or None."""
|
||||
return self.coordinator.referenced_blueprint
|
||||
|
||||
@callback
|
||||
def _render_script_variables(self) -> dict:
|
||||
"""Render configured variables."""
|
||||
return self.coordinator.data["run_variables"]
|
||||
|
||||
@callback
|
||||
def _process_data(self) -> None:
|
||||
"""Process new data."""
|
||||
|
@ -16,10 +16,10 @@ from homeassistant.components.blueprint import (
|
||||
DomainBlueprints,
|
||||
)
|
||||
from homeassistant.components.template import DOMAIN, SERVICE_RELOAD
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import Context, HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import yaml as yaml_util
|
||||
from homeassistant.util import dt as dt_util, yaml as yaml_util
|
||||
|
||||
from tests.common import async_mock_service
|
||||
|
||||
@ -212,6 +212,61 @@ async def test_reload_template_when_blueprint_changes(hass: HomeAssistant) -> No
|
||||
assert not_inverted.state == "on"
|
||||
|
||||
|
||||
async def test_trigger_event_sensor(
|
||||
hass: HomeAssistant, device_registry: dr.DeviceRegistry
|
||||
) -> None:
|
||||
"""Test event sensor blueprint."""
|
||||
blueprint = "test_event_sensor.yaml"
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
"template",
|
||||
{
|
||||
"template": [
|
||||
{
|
||||
"use_blueprint": {
|
||||
"path": blueprint,
|
||||
"input": {
|
||||
"event_type": "my_custom_event",
|
||||
"event_data": {"foo": "bar"},
|
||||
},
|
||||
},
|
||||
"name": "My Custom Event",
|
||||
},
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
context = Context()
|
||||
now = dt_util.utcnow()
|
||||
with patch("homeassistant.util.dt.now", return_value=now):
|
||||
hass.bus.async_fire(
|
||||
"my_custom_event", {"foo": "bar", "beer": 2}, context=context
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
date_state = hass.states.get("sensor.my_custom_event")
|
||||
assert date_state is not None
|
||||
assert date_state.state == now.isoformat(timespec="seconds")
|
||||
data = date_state.attributes.get("data")
|
||||
assert data is not None
|
||||
assert data != ""
|
||||
assert data.get("foo") == "bar"
|
||||
assert data.get("beer") == 2
|
||||
|
||||
inverted_foo_template = template.helpers.blueprint_in_template(
|
||||
hass, "sensor.my_custom_event"
|
||||
)
|
||||
assert inverted_foo_template == blueprint
|
||||
|
||||
inverted_binary_sensor_blueprint_entity_ids = (
|
||||
template.helpers.templates_with_blueprint(hass, blueprint)
|
||||
)
|
||||
assert len(inverted_binary_sensor_blueprint_entity_ids) == 1
|
||||
|
||||
with pytest.raises(BlueprintInUse):
|
||||
await template.async_get_blueprints(hass).async_remove_blueprint(blueprint)
|
||||
|
||||
|
||||
async def test_domain_blueprint(hass: HomeAssistant) -> None:
|
||||
"""Test DomainBlueprint services."""
|
||||
reload_handler_calls = async_mock_service(hass, DOMAIN, SERVICE_RELOAD)
|
||||
@ -262,7 +317,8 @@ async def test_invalid_blueprint(
|
||||
)
|
||||
|
||||
assert "more than one platform defined per blueprint" in caplog.text
|
||||
assert await template.async_get_blueprints(hass).async_get_blueprints() == {}
|
||||
blueprints = await template.async_get_blueprints(hass).async_get_blueprints()
|
||||
assert "invalid.yaml" not in blueprints
|
||||
|
||||
|
||||
async def test_no_blueprint(hass: HomeAssistant) -> None:
|
||||
|
@ -330,7 +330,10 @@ async def test_trigger_number(hass: HomeAssistant) -> None:
|
||||
"max": "{{ trigger.event.data.max_beers }}",
|
||||
"step": "{{ trigger.event.data.step }}",
|
||||
"unit_of_measurement": "beer",
|
||||
"set_value": {"event": "test_number_event"},
|
||||
"set_value": {
|
||||
"event": "test_number_event",
|
||||
"event_data": {"entity_id": "{{ this.entity_id }}"},
|
||||
},
|
||||
"optimistic": True,
|
||||
},
|
||||
],
|
||||
@ -379,6 +382,9 @@ async def test_trigger_number(hass: HomeAssistant) -> None:
|
||||
)
|
||||
assert len(events) == 1
|
||||
assert events[0].event_type == "test_number_event"
|
||||
entity_id = events[0].data.get("entity_id")
|
||||
assert entity_id is not None
|
||||
assert entity_id == "number.hello_name"
|
||||
|
||||
|
||||
def _verify(
|
||||
|
@ -264,6 +264,7 @@ async def test_templates_with_entities(
|
||||
async def test_trigger_select(hass: HomeAssistant) -> None:
|
||||
"""Test trigger based template select."""
|
||||
events = async_capture_events(hass, "test_number_event")
|
||||
action_events = async_capture_events(hass, "action_event")
|
||||
assert await setup.async_setup_component(
|
||||
hass,
|
||||
"template",
|
||||
@ -274,13 +275,23 @@ async def test_trigger_select(hass: HomeAssistant) -> None:
|
||||
{
|
||||
"unique_id": "listening-test-event",
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"variables": {"beer": "{{ trigger.event.data.beer }}"},
|
||||
"action": [
|
||||
{"event": "action_event", "event_data": {"beer": "{{ beer }}"}}
|
||||
],
|
||||
"select": [
|
||||
{
|
||||
"name": "Hello Name",
|
||||
"unique_id": "hello_name-id",
|
||||
"state": "{{ trigger.event.data.beer }}",
|
||||
"options": "{{ trigger.event.data.beers }}",
|
||||
"select_option": {"event": "test_number_event"},
|
||||
"select_option": {
|
||||
"event": "test_number_event",
|
||||
"event_data": {
|
||||
"entity_id": "{{ this.entity_id }}",
|
||||
"beer": "{{ beer }}",
|
||||
},
|
||||
},
|
||||
"optimistic": True,
|
||||
},
|
||||
],
|
||||
@ -308,6 +319,12 @@ async def test_trigger_select(hass: HomeAssistant) -> None:
|
||||
assert state.state == "duff"
|
||||
assert state.attributes["options"] == ["duff", "alamo"]
|
||||
|
||||
assert len(action_events) == 1
|
||||
assert action_events[0].event_type == "action_event"
|
||||
beer = action_events[0].data.get("beer")
|
||||
assert beer is not None
|
||||
assert beer == "duff"
|
||||
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SELECT_SERVICE_SELECT_OPTION,
|
||||
@ -316,6 +333,13 @@ async def test_trigger_select(hass: HomeAssistant) -> None:
|
||||
)
|
||||
assert len(events) == 1
|
||||
assert events[0].event_type == "test_number_event"
|
||||
entity_id = events[0].data.get("entity_id")
|
||||
assert entity_id is not None
|
||||
assert entity_id == "select.hello_name"
|
||||
|
||||
beer = events[0].data.get("beer")
|
||||
assert beer is not None
|
||||
assert beer == "duff"
|
||||
|
||||
|
||||
def _verify(
|
||||
|
13
tests/components/template/test_trigger_entity.py
Normal file
13
tests/components/template/test_trigger_entity.py
Normal file
@ -0,0 +1,13 @@
|
||||
"""Test trigger template entity."""
|
||||
|
||||
from homeassistant.components.template import trigger_entity
|
||||
from homeassistant.components.template.coordinator import TriggerUpdateCoordinator
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
||||
async def test_reference_blueprints_is_none(hass: HomeAssistant) -> None:
|
||||
"""Test template entity requires hass to be set before accepting templates."""
|
||||
coordinator = TriggerUpdateCoordinator(hass, {})
|
||||
entity = trigger_entity.TriggerEntity(hass, coordinator, {})
|
||||
|
||||
assert entity.referenced_blueprint is None
|
@ -0,0 +1,27 @@
|
||||
blueprint:
|
||||
name: Create Sensor from Event
|
||||
description: Creates a timestamp sensor from an event
|
||||
domain: template
|
||||
source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/template/blueprints/event_sensor.yaml
|
||||
input:
|
||||
event_type:
|
||||
name: Name of the event_type
|
||||
description: The event_type for the event trigger
|
||||
selector:
|
||||
text:
|
||||
event_data:
|
||||
name: The data for the event
|
||||
description: The event_data for the event trigger
|
||||
selector:
|
||||
object:
|
||||
trigger:
|
||||
- trigger: event
|
||||
event_type: !input event_type
|
||||
event_data: !input event_data
|
||||
variables:
|
||||
event_data: "{{ trigger.event.data }}"
|
||||
sensor:
|
||||
state: "{{ now() }}"
|
||||
device_class: timestamp
|
||||
attributes:
|
||||
data: "{{ event_data }}"
|
Loading…
x
Reference in New Issue
Block a user