mirror of
https://github.com/home-assistant/core.git
synced 2026-04-06 23:47:33 +00:00
Fix section and entity variable resolution for template platforms (#149660)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
@@ -176,7 +176,15 @@ TEMPLATE_BLUEPRINT_SCHEMA = vol.All(
|
||||
)
|
||||
|
||||
|
||||
async def _async_resolve_blueprints(
|
||||
def _merge_section_variables(config: ConfigType, section_variables: ConfigType) -> None:
|
||||
"""Merges a template entity configuration's variables with the section variables."""
|
||||
if (variables := config.pop(CONF_VARIABLES, None)) and isinstance(variables, dict):
|
||||
config[CONF_VARIABLES] = {**section_variables, **variables}
|
||||
else:
|
||||
config[CONF_VARIABLES] = section_variables
|
||||
|
||||
|
||||
async def _async_resolve_template_config(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
) -> TemplateConfig:
|
||||
@@ -187,12 +195,11 @@ async def _async_resolve_blueprints(
|
||||
with suppress(ValueError): # Invalid config
|
||||
raw_config = dict(config)
|
||||
|
||||
config = _backward_compat_schema(config)
|
||||
if is_blueprint_instance_config(config):
|
||||
blueprints = async_get_blueprints(hass)
|
||||
|
||||
blueprint_inputs = await blueprints.async_inputs_from_config(
|
||||
_backward_compat_schema(config)
|
||||
)
|
||||
blueprint_inputs = await blueprints.async_inputs_from_config(config)
|
||||
raw_blueprint_inputs = blueprint_inputs.config_with_inputs
|
||||
|
||||
config = blueprint_inputs.async_substitute()
|
||||
@@ -205,14 +212,32 @@ async def _async_resolve_blueprints(
|
||||
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.
|
||||
# State based template entities remove CONF_VARIABLES because they pass
|
||||
# blueprint inputs to the template entities. Trigger based template entities
|
||||
# retain CONF_VARIABLES because the variables are always executed between
|
||||
# the trigger and action.
|
||||
if CONF_TRIGGERS not in config and CONF_VARIABLES in config:
|
||||
config[platform][CONF_VARIABLES] = config.pop(CONF_VARIABLES)
|
||||
_merge_section_variables(config[platform], config.pop(CONF_VARIABLES))
|
||||
|
||||
raw_config = dict(config)
|
||||
|
||||
# Trigger based template entities retain CONF_VARIABLES because the variables are
|
||||
# always executed between the trigger and action.
|
||||
elif CONF_TRIGGERS not in config and CONF_VARIABLES in config:
|
||||
# State based template entities have 2 layers of variables. Variables at the section level
|
||||
# and variables at the entity level should be merged together at the entity level.
|
||||
section_variables = config.pop(CONF_VARIABLES)
|
||||
platform_config: list[ConfigType] | ConfigType
|
||||
platforms = [platform for platform in PLATFORMS if platform in config]
|
||||
for platform in platforms:
|
||||
platform_config = config[platform]
|
||||
if platform in PLATFORMS:
|
||||
if isinstance(platform_config, dict):
|
||||
platform_config = [platform_config]
|
||||
|
||||
for entity_config in platform_config:
|
||||
_merge_section_variables(entity_config, section_variables)
|
||||
|
||||
template_config = TemplateConfig(CONFIG_SECTION_SCHEMA(config))
|
||||
template_config.raw_blueprint_inputs = raw_blueprint_inputs
|
||||
template_config.raw_config = raw_config
|
||||
@@ -225,7 +250,7 @@ async def async_validate_config_section(
|
||||
) -> TemplateConfig:
|
||||
"""Validate an entire config section for the template integration."""
|
||||
|
||||
validated_config = await _async_resolve_blueprints(hass, config)
|
||||
validated_config = await _async_resolve_template_config(hass, config)
|
||||
|
||||
if CONF_TRIGGERS in validated_config:
|
||||
validated_config[CONF_TRIGGERS] = await async_validate_trigger_config(
|
||||
|
||||
@@ -4,8 +4,9 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.const import CONF_STATE
|
||||
from homeassistant.const import CONF_STATE, CONF_VARIABLES
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.script_variables import ScriptVariables
|
||||
from homeassistant.helpers.template import _SENTINEL
|
||||
from homeassistant.helpers.trigger_template_entity import TriggerBaseEntity
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
@@ -32,6 +33,8 @@ class TriggerEntity( # pylint: disable=hass-enforce-class-module
|
||||
TriggerBaseEntity.__init__(self, hass, config)
|
||||
AbstractTemplateEntity.__init__(self, hass, config)
|
||||
|
||||
self._entity_variables: ScriptVariables | None = config.get(CONF_VARIABLES)
|
||||
self._rendered_entity_variables: dict | None = None
|
||||
self._state_render_error = False
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
@@ -63,9 +66,7 @@ class TriggerEntity( # pylint: disable=hass-enforce-class-module
|
||||
@callback
|
||||
def _render_script_variables(self) -> dict:
|
||||
"""Render configured variables."""
|
||||
if self.coordinator.data is None:
|
||||
return {}
|
||||
return self.coordinator.data["run_variables"] or {}
|
||||
return self._rendered_entity_variables or {}
|
||||
|
||||
def _render_templates(self, variables: dict[str, Any]) -> None:
|
||||
"""Render templates."""
|
||||
@@ -92,7 +93,18 @@ class TriggerEntity( # pylint: disable=hass-enforce-class-module
|
||||
def _process_data(self) -> None:
|
||||
"""Process new data."""
|
||||
|
||||
variables = self._template_variables(self.coordinator.data["run_variables"])
|
||||
coordinator_variables = self.coordinator.data["run_variables"]
|
||||
if self._entity_variables:
|
||||
entity_variables = self._entity_variables.async_simple_render(
|
||||
coordinator_variables
|
||||
)
|
||||
self._rendered_entity_variables = {
|
||||
**coordinator_variables,
|
||||
**entity_variables,
|
||||
}
|
||||
else:
|
||||
self._rendered_entity_variables = coordinator_variables
|
||||
variables = self._template_variables(self._rendered_entity_variables)
|
||||
if self._render_availability_template(variables):
|
||||
self._render_templates(variables)
|
||||
|
||||
|
||||
@@ -5,8 +5,12 @@ from __future__ import annotations
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.template.config import CONFIG_SECTION_SCHEMA
|
||||
from homeassistant.components.template.config import (
|
||||
CONFIG_SECTION_SCHEMA,
|
||||
async_validate_config_section,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.script_variables import ScriptVariables
|
||||
from homeassistant.helpers.template import Template
|
||||
|
||||
|
||||
@@ -93,3 +97,162 @@ async def test_invalid_default_entity_id(
|
||||
}
|
||||
with pytest.raises(vol.Invalid):
|
||||
CONFIG_SECTION_SCHEMA(config)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "expected"),
|
||||
[
|
||||
(
|
||||
{
|
||||
"variables": {"a": 1},
|
||||
"button": {
|
||||
"press": {
|
||||
"service": "test.automation",
|
||||
"data_template": {"caller": "{{ this.entity_id }}"},
|
||||
},
|
||||
"variables": {"b": 2},
|
||||
"device_class": "restart",
|
||||
"unique_id": "test",
|
||||
"name": "test",
|
||||
"icon": "mdi:test",
|
||||
},
|
||||
},
|
||||
{"a": 1, "b": 2},
|
||||
),
|
||||
(
|
||||
{
|
||||
"variables": {"a": 1},
|
||||
"button": [
|
||||
{
|
||||
"press": {
|
||||
"service": "test.automation",
|
||||
"data_template": {"caller": "{{ this.entity_id }}"},
|
||||
},
|
||||
"variables": {"b": 2},
|
||||
"device_class": "restart",
|
||||
"unique_id": "test",
|
||||
"name": "test",
|
||||
"icon": "mdi:test",
|
||||
}
|
||||
],
|
||||
},
|
||||
{"a": 1, "b": 2},
|
||||
),
|
||||
(
|
||||
{
|
||||
"variables": {"a": 1},
|
||||
"button": [
|
||||
{
|
||||
"press": {
|
||||
"service": "test.automation",
|
||||
"data_template": {"caller": "{{ this.entity_id }}"},
|
||||
},
|
||||
"variables": {"a": 2, "b": 2},
|
||||
"device_class": "restart",
|
||||
"unique_id": "test",
|
||||
"name": "test",
|
||||
"icon": "mdi:test",
|
||||
}
|
||||
],
|
||||
},
|
||||
{"a": 2, "b": 2},
|
||||
),
|
||||
(
|
||||
{
|
||||
"variables": {"a": 1},
|
||||
"button": {
|
||||
"press": {
|
||||
"service": "test.automation",
|
||||
"data_template": {"caller": "{{ this.entity_id }}"},
|
||||
},
|
||||
"device_class": "restart",
|
||||
"unique_id": "test",
|
||||
"name": "test",
|
||||
"icon": "mdi:test",
|
||||
},
|
||||
},
|
||||
{"a": 1},
|
||||
),
|
||||
(
|
||||
{
|
||||
"button": {
|
||||
"press": {
|
||||
"service": "test.automation",
|
||||
"data_template": {"caller": "{{ this.entity_id }}"},
|
||||
},
|
||||
"variables": {"b": 2},
|
||||
"device_class": "restart",
|
||||
"unique_id": "test",
|
||||
"name": "test",
|
||||
"icon": "mdi:test",
|
||||
},
|
||||
},
|
||||
{"b": 2},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_combined_state_variables(
|
||||
hass: HomeAssistant, config: dict, expected: dict
|
||||
) -> None:
|
||||
"""Tests combining variables for state based template entities."""
|
||||
validated = await async_validate_config_section(hass, config)
|
||||
assert "variables" not in validated
|
||||
variables: ScriptVariables = validated["button"][0]["variables"]
|
||||
assert variables.as_dict() == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "expected_root", "expected_entity"),
|
||||
[
|
||||
(
|
||||
{
|
||||
"trigger": {"trigger": "event", "event_type": "my_event"},
|
||||
"variables": {"a": 1},
|
||||
"binary_sensor": {
|
||||
"name": "test",
|
||||
"state": "{{ trigger.event.event_type }}",
|
||||
"variables": {"b": 2},
|
||||
},
|
||||
},
|
||||
{"a": 1},
|
||||
{"b": 2},
|
||||
),
|
||||
(
|
||||
{
|
||||
"triggers": {"trigger": "event", "event_type": "my_event"},
|
||||
"variables": {"a": 1},
|
||||
"binary_sensor": {
|
||||
"name": "test",
|
||||
"state": "{{ trigger.event.event_type }}",
|
||||
},
|
||||
},
|
||||
{"a": 1},
|
||||
{},
|
||||
),
|
||||
(
|
||||
{
|
||||
"trigger": {"trigger": "event", "event_type": "my_event"},
|
||||
"binary_sensor": {
|
||||
"name": "test",
|
||||
"state": "{{ trigger.event.event_type }}",
|
||||
"variables": {"b": 2},
|
||||
},
|
||||
},
|
||||
{},
|
||||
{"b": 2},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_combined_trigger_variables(
|
||||
hass: HomeAssistant,
|
||||
config: dict,
|
||||
expected_root: dict,
|
||||
expected_entity: dict,
|
||||
) -> None:
|
||||
"""Tests variable are not combined for trigger based template entities."""
|
||||
empty = ScriptVariables({})
|
||||
validated = await async_validate_config_section(hass, config)
|
||||
root_variables: ScriptVariables = validated.get("variables", empty)
|
||||
assert root_variables.as_dict() == expected_root
|
||||
variables: ScriptVariables = validated["binary_sensor"][0].get("variables", empty)
|
||||
assert variables.as_dict() == expected_entity
|
||||
|
||||
@@ -2298,6 +2298,65 @@ async def test_trigger_action(hass: HomeAssistant) -> None:
|
||||
assert events[0].context.parent_id == context.id
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("count", "domain"), [(1, "template")])
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[
|
||||
{
|
||||
"template": [
|
||||
{
|
||||
"unique_id": "listening-test-event",
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"variables": {"a": "{{ trigger.event.data.a }}"},
|
||||
"action": [
|
||||
{
|
||||
"variables": {"b": "{{ a + 1 }}"},
|
||||
},
|
||||
{"event": "test_event2", "event_data": {"hello": "world"}},
|
||||
],
|
||||
"sensor": [
|
||||
{
|
||||
"name": "Hello Name",
|
||||
"state": "{{ a + b + c }}",
|
||||
"variables": {"c": "{{ b + 1 }}"},
|
||||
"attributes": {
|
||||
"a": "{{ a }}",
|
||||
"b": "{{ b }}",
|
||||
"c": "{{ c }}",
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("start_ha")
|
||||
async def test_trigger_action_variables(hass: HomeAssistant) -> None:
|
||||
"""Test trigger entity with variables in an action works."""
|
||||
event = "test_event2"
|
||||
context = Context()
|
||||
events = async_capture_events(hass, event)
|
||||
|
||||
state = hass.states.get("sensor.hello_name")
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
context = Context()
|
||||
hass.bus.async_fire("test_event", {"a": 1}, context=context)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.hello_name")
|
||||
assert state.state == str(1 + 2 + 3)
|
||||
assert state.context is context
|
||||
assert state.attributes["a"] == 1
|
||||
assert state.attributes["b"] == 2
|
||||
assert state.attributes["c"] == 3
|
||||
|
||||
assert len(events) == 1
|
||||
assert events[0].context.parent_id == context.id
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("count", "domain"), [(1, template.DOMAIN)])
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
|
||||
@@ -7,6 +7,7 @@ from homeassistant.components.template.coordinator import TriggerUpdateCoordinat
|
||||
from homeassistant.const import CONF_ICON, CONF_NAME, CONF_STATE, STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.helpers.script_variables import ScriptVariables
|
||||
from homeassistant.helpers.trigger_template_entity import CONF_PICTURE
|
||||
|
||||
_ICON_TEMPLATE = 'mdi:o{{ "n" if value=="on" else "ff" }}'
|
||||
@@ -123,18 +124,42 @@ async def test_template_state_syntax_error(
|
||||
|
||||
async def test_script_variables_from_coordinator(hass: HomeAssistant) -> None:
|
||||
"""Test script variables."""
|
||||
coordinator = TriggerUpdateCoordinator(hass, {})
|
||||
entity = TestEntity(hass, coordinator, {})
|
||||
|
||||
assert entity._render_script_variables() == {}
|
||||
hass.states.async_set("sensor.test", "1")
|
||||
|
||||
coordinator.data = {"run_variables": None}
|
||||
coordinator = TriggerUpdateCoordinator(
|
||||
hass,
|
||||
{
|
||||
"variables": ScriptVariables(
|
||||
{"a": template.Template("{{ states('sensor.test') }}", hass), "c": 0}
|
||||
)
|
||||
},
|
||||
)
|
||||
entity = TestEntity(
|
||||
hass,
|
||||
coordinator,
|
||||
{
|
||||
"state": template.Template("{{ 'on' }}", hass),
|
||||
"variables": ScriptVariables(
|
||||
{"b": template.Template("{{ a + 1 }}", hass), "c": 1}
|
||||
),
|
||||
},
|
||||
)
|
||||
await coordinator._handle_triggered({})
|
||||
entity._process_data()
|
||||
assert entity._render_script_variables() == {"a": 1, "b": 2, "c": 1}
|
||||
|
||||
assert entity._render_script_variables() == {}
|
||||
hass.states.async_set("sensor.test", "2")
|
||||
|
||||
coordinator._execute_update({"value": STATE_ON})
|
||||
await coordinator._handle_triggered({"value": STATE_ON})
|
||||
entity._process_data()
|
||||
|
||||
assert entity._render_script_variables() == {"value": STATE_ON}
|
||||
assert entity._render_script_variables() == {
|
||||
"value": STATE_ON,
|
||||
"a": 2,
|
||||
"b": 3,
|
||||
"c": 1,
|
||||
}
|
||||
|
||||
|
||||
async def test_default_entity_id(hass: HomeAssistant) -> None:
|
||||
|
||||
Reference in New Issue
Block a user