Skip to content
Snippets Groups Projects
Unverified Commit aec6868a authored by Petro31's avatar Petro31 Committed by GitHub
Browse files

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
parent 48865e00
No related branches found
No related tags found
No related merge requests found
......@@ -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(
......
"""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
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 }}"
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment