Add conditions: to trigger based template entity options (#174055)

This commit is contained in:
Petro31
2026-08-26 14:22:44 +02:00
committed by GitHub
parent 585c6a38d1
commit 8635da2e62
8 changed files with 825 additions and 288 deletions
+71 -18
View File
@@ -44,6 +44,7 @@ from homeassistant.const import (
CONF_TRIGGERS,
CONF_UNIQUE_ID,
CONF_VARIABLES,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, issue_registry as ir
@@ -82,31 +83,82 @@ _LOGGER = logging.getLogger(__name__)
PACKAGE_MERGE_HINT = "list"
_DEFAULT_NAME = "Template Entity"
_DEFAULT_NAMES = {
Platform.ALARM_CONTROL_PANEL: alarm_control_panel_platform.DEFAULT_NAME,
Platform.BINARY_SENSOR: binary_sensor_platform.DEFAULT_NAME,
Platform.BUTTON: button_platform.DEFAULT_NAME,
Platform.COVER: cover_platform.DEFAULT_NAME,
Platform.DEVICE_TRACKER: device_tracker_platform.DEFAULT_NAME,
Platform.EVENT: event_platform.DEFAULT_NAME,
Platform.FAN: fan_platform.DEFAULT_NAME,
Platform.IMAGE: image_platform.DEFAULT_NAME,
Platform.LIGHT: light_platform.DEFAULT_NAME,
Platform.LOCK: lock_platform.DEFAULT_NAME,
Platform.NUMBER: number_platform.DEFAULT_NAME,
Platform.SELECT: select_platform.DEFAULT_NAME,
Platform.SENSOR: sensor_platform.DEFAULT_NAME,
Platform.SWITCH: switch_platform.DEFAULT_NAME,
Platform.UPDATE: update_platform.DEFAULT_NAME,
Platform.VACUUM: vacuum_platform.DEFAULT_NAME,
Platform.WEATHER: weather_platform.DEFAULT_NAME,
}
def _identify_entity_config_requires_trigger(
platform: Platform, option: str, entity_config: ConfigType
) -> None:
"""Raise vol.Invalid if an entity sets an option that requires a trigger."""
if option not in entity_config:
return
_default_name = _DEFAULT_NAMES.get(platform, _DEFAULT_NAME)
identifier = f"{CONF_NAME}: {_default_name}"
if (
(name := entity_config.get(CONF_NAME))
and isinstance(name, Template)
and name.template != _default_name
):
identifier = f"{CONF_NAME}: {name.template}"
elif default_entity_id := entity_config.get(CONF_DEFAULT_ENTITY_ID):
identifier = f"{CONF_DEFAULT_ENTITY_ID}: {default_entity_id}"
elif unique_id := entity_config.get(CONF_UNIQUE_ID):
identifier = f"{CONF_UNIQUE_ID}: {unique_id}"
raise vol.Invalid(
f"The {option} option for template {platform.replace('_', ' ')}: {identifier} "
f"requires a trigger, remove the {option} option or rewrite "
"configuration to use a trigger"
)
def validate_binary_sensor_auto_off_has_trigger(obj: dict) -> dict:
"""Validate that binary sensors with auto_off have triggers."""
if CONF_TRIGGERS not in obj and BINARY_SENSOR_DOMAIN in obj:
binary_sensors: list[ConfigType] = obj[BINARY_SENSOR_DOMAIN]
for binary_sensor in binary_sensors:
if binary_sensor_platform.CONF_AUTO_OFF not in binary_sensor:
_identify_entity_config_requires_trigger(
Platform.BINARY_SENSOR,
binary_sensor_platform.CONF_AUTO_OFF,
binary_sensor,
)
return obj
def validate_entity_config_with_conditions_has_trigger(obj: dict) -> dict:
"""Validate entity condition requires trigger."""
if CONF_TRIGGERS not in obj:
for platform in PLATFORMS:
if platform not in obj:
continue
identifier = f"{CONF_NAME}: {binary_sensor_platform.DEFAULT_NAME}"
if (
(name := binary_sensor.get(CONF_NAME))
and isinstance(name, Template)
and name.template != binary_sensor_platform.DEFAULT_NAME
):
identifier = f"{CONF_NAME}: {name.template}"
elif default_entity_id := binary_sensor.get(CONF_DEFAULT_ENTITY_ID):
identifier = f"{CONF_DEFAULT_ENTITY_ID}: {default_entity_id}"
elif unique_id := binary_sensor.get(CONF_UNIQUE_ID):
identifier = f"{CONF_UNIQUE_ID}: {unique_id}"
raise vol.Invalid(
f"The auto_off option for template binary sensor: {identifier} "
"requires a trigger, remove the auto_off option or rewrite "
"configuration to use a trigger"
)
for entity_config in obj[platform]:
_identify_entity_config_requires_trigger(
platform,
CONF_CONDITIONS,
entity_config,
)
return obj
@@ -252,6 +304,7 @@ CONFIG_SECTION_SCHEMA = vol.All(
BUTTON_DOMAIN,
),
validate_binary_sensor_auto_off_has_trigger,
validate_entity_config_with_conditions_has_trigger,
)
TEMPLATE_BLUEPRINT_SCHEMA = vol.All(
@@ -17,11 +17,11 @@ from homeassistant.core import Context, CoreState, Event, HomeAssistant, callbac
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
from .const import DOMAIN, PLATFORMS
from .validators import check_conditions
_LOGGER = logging.getLogger(__name__)
@@ -135,7 +135,7 @@ class TriggerUpdateCoordinator(DataUpdateCoordinator):
if self._run_variables:
run_variables = self._run_variables.async_render(self.hass, run_variables)
if not self._check_condition(run_variables):
if not check_conditions(self._cond_func, run_variables):
return
# Create a context referring to the trigger context.
trigger_context_id = None if context is None else context.id
@@ -153,22 +153,10 @@ class TriggerUpdateCoordinator(DataUpdateCoordinator):
if self._run_variables:
run_variables = self._run_variables.async_render(self.hass, run_variables)
if not self._check_condition(run_variables):
if not check_conditions(self._cond_func, run_variables):
return
self._execute_update(run_variables, context)
def _check_condition(self, run_variables: TemplateVarsType) -> bool:
if not self._cond_func:
return True
condition_result = self._cond_func.async_check(variables=run_variables)
if condition_result is False:
_LOGGER.debug(
"Conditions not met, aborting template"
" trigger update. Condition summary: %s",
trace_get(clear=False),
)
return condition_result
@callback
def _execute_update(
self, run_variables: TemplateVarsType, context: Context | None = None
+35 -7
View File
@@ -10,6 +10,7 @@ from voluptuous.humanize import humanize_error
from homeassistant.components import blueprint
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_CONDITIONS,
CONF_NAME,
CONF_STATE,
CONF_UNIQUE_ID,
@@ -19,6 +20,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
from homeassistant.helpers import template
from homeassistant.helpers.condition import async_validate_conditions_config
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
@@ -129,14 +131,16 @@ def _get_config_breadcrumbs(config: ConfigType) -> str:
return breadcrumb
async def validate_template_scripts(
async def validate_actions_and_conditions_config(
hass: HomeAssistant,
config: ConfigType,
script_options: tuple[str, ...] | None = None,
) -> bool:
"""Validate template scripts."""
if not script_options:
return True
"""Validate template entity actions and conditions.
Returns True when all conditions and actions validate without error.
When any condition or action fails, return False.
"""
def _humanize(err: Exception, data: Any) -> str:
"""Humanize vol.Invalid, stringify other exceptions."""
@@ -145,6 +149,24 @@ async def validate_template_scripts(
return str(err)
breadcrumb: str | None = None
if (condition_config := config.pop(CONF_CONDITIONS, None)) is not None:
try:
config[CONF_CONDITIONS] = await async_validate_conditions_config(
hass, condition_config
)
except (vol.Invalid, HomeAssistantError) as err:
if not breadcrumb:
breadcrumb = _get_config_breadcrumbs(config)
_LOGGER.error(
"The condition for %s failed to setup: %s",
breadcrumb,
_humanize(err, condition_config),
)
return False
if not script_options:
return True
for script_option in script_options:
if (script_config := config.pop(script_option, None)) is not None:
try:
@@ -205,7 +227,9 @@ async def async_setup_template_platform(
if trigger_entities := [
trigger_entity_cls(hass, discovery_info["coordinator"], entity_config)
for entity_config in discovery_info["entities"]
if await validate_template_scripts(hass, entity_config, script_options)
if await validate_actions_and_conditions_config(
hass, entity_config, script_options
)
]:
async_add_entities(trigger_entities)
else:
@@ -218,7 +242,9 @@ async def async_setup_template_platform(
if state_entities := [
entity_config
for entity_config in discovery_info["entities"]
if await validate_template_scripts(hass, entity_config, script_options)
if await validate_actions_and_conditions_config(
hass, entity_config, script_options
)
]:
async_create_template_tracking_entities(
state_entity_cls,
@@ -249,7 +275,9 @@ async def async_setup_template_entry(
options[CONF_STATE] = options.pop(CONF_VALUE_TEMPLATE)
validated_config = config_schema(options)
if await validate_template_scripts(hass, validated_config, script_options):
if await validate_actions_and_conditions_config(
hass, validated_config, script_options
):
async_add_entities(
[state_entity_cls(hass, validated_config, config_entry.entry_id)]
)
@@ -8,6 +8,7 @@ from typing import TypeVar
import voluptuous as vol
from homeassistant.const import (
CONF_CONDITIONS,
CONF_DEVICE_ID,
CONF_ICON,
CONF_NAME,
@@ -94,6 +95,7 @@ def make_template_entity_common_schema(
vol.Optional(CONF_PICTURE): cv.template,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA,
vol.Optional(CONF_CONDITIONS): cv.CONDITIONS_SCHEMA,
vol.Optional(CONF_ATTRIBUTES): vol.Schema(
vol.All(
{cv.string: cv.template},
@@ -1,11 +1,13 @@
"""Trigger entity."""
from collections.abc import Callable
import logging
from typing import Any, override
from homeassistant.const import CONF_VARIABLES
from homeassistant.const import CONF_CONDITIONS, CONF_VARIABLES
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import condition
from homeassistant.helpers.script_variables import ScriptVariables
from homeassistant.helpers.template import (
_SENTINEL,
@@ -17,8 +19,11 @@ from homeassistant.helpers.trigger_template_entity import (
)
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import TriggerUpdateCoordinator
from .coordinator import TriggerUpdateCoordinator
from .entity import AbstractTemplateEntity
from .validators import check_conditions
_LOGGER = logging.getLogger(__name__)
class TriggerEntity( # pylint: disable=home-assistant-enforce-class-module
@@ -44,6 +49,7 @@ class TriggerEntity( # pylint: disable=home-assistant-enforce-class-module
self._entity_variables: ScriptVariables | None = config.get(CONF_VARIABLES)
self._rendered_entity_variables: dict | None = None
self._state_render_error = False
self._cond_func: condition.ConditionsChecker | None = None
self._skip_rendered_result: list[str] = []
if self.skip_rendered_result is not None:
@@ -52,7 +58,19 @@ class TriggerEntity( # pylint: disable=home-assistant-enforce-class-module
@override
async def async_added_to_hass(self) -> None:
"""Handle being added to Home Assistant."""
# Setup condition before calling async_added_to_hass to ensure
# the condition is available before a trigger can occur
if condition_config := self._config.get(CONF_CONDITIONS):
self._cond_func = await condition.async_conditions_from_config(
self.hass,
condition_config,
_LOGGER,
f"template {self.domain} entity",
)
await super().async_added_to_hass()
if self.coordinator.data is not None:
# The trigger already produced data; rendering it must win over
# restored state, so skip restore entirely to avoid clobbering the
@@ -61,6 +79,13 @@ class TriggerEntity( # pylint: disable=home-assistant-enforce-class-module
else:
await self.async_restore_last_state()
@override
async def async_will_remove_from_hass(self) -> None:
"""Clean up conditions when removing from Home Assistant."""
await super().async_will_remove_from_hass()
if self._cond_func:
self._cond_func.async_unload()
@override
def _set_unique_id(self, unique_id: str | None) -> None:
"""Set unique id."""
@@ -293,6 +318,9 @@ class TriggerEntity( # pylint: disable=home-assistant-enforce-class-module
self._rendered_entity_variables = coordinator_variables
variables = self._template_variables(self._rendered_entity_variables)
if not check_conditions(self._cond_func, variables):
return
self.async_set_context(self.coordinator.data["context"])
if self._render_availability_template(variables):
self._render_templates(variables)
@@ -9,7 +9,10 @@ import voluptuous as vol
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.condition import ConditionsChecker
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.trace import trace_get
from homeassistant.helpers.typing import TemplateVarsType
_LOGGER = logging.getLogger(__name__)
@@ -361,3 +364,20 @@ def string(
return None
return convert
def check_conditions(
condition_func: ConditionsChecker | None, run_variables: TemplateVarsType
) -> bool:
"""Check if conditions have been met using run variables."""
if not condition_func:
return True
condition_result = condition_func.async_check(variables=run_variables)
if condition_result is False:
_LOGGER.debug(
"Conditions not met, aborting template trigger update. Condition summary: %s",
trace_get(clear=False),
)
return condition_result
+512 -246
View File
@@ -26,6 +26,7 @@ from .conftest import (
assert_action,
async_trigger,
make_mock_device_actions,
make_test_trigger,
setup_entity,
setup_mock_devices,
)
@@ -298,6 +299,515 @@ async def test_invalid_binary_sensor_schema_with_auto_off(
) or expected_error in caplog.text
# Button is omitted because it does not support triggers
TRIGGER_PLATFORM_CONFIGURATIONS = [
(
Platform.ALARM_CONTROL_PANEL,
{
"state": "{{ 'disarmed' }}",
},
),
(
Platform.BINARY_SENSOR,
{
"state": "{{ 'on' }}",
},
),
(
Platform.COVER,
{
"state": "{{ 'open' }}",
"open_cover": [],
"close_cover": [],
},
),
(
Platform.DEVICE_TRACKER,
{
"in_zones": "{{ ['zone.home'] }}",
},
),
(
Platform.EVENT,
{
"event_type": "{{ 'single' }}",
"event_types": "{{ ['single'] }}",
},
),
(
Platform.FAN,
{
"state": "{{ 'on' }}",
"turn_on": [],
"turn_off": [],
},
),
(
Platform.IMAGE,
{
"url": "{{ 'http://www.test.com' }}",
},
),
(
Platform.LIGHT,
{
"state": "{{ 'on' }}",
"turn_on": [],
"turn_off": [],
},
),
(
Platform.LOCK,
{
"state": "{{ 'on' }}",
"lock": [],
"unlock": [],
},
),
(
Platform.NUMBER,
{
"state": "{{ 4 }}",
"min": "0",
"max": "100",
"step": "0.1",
"unit_of_measurement": "cm",
"set_value": [],
},
),
(
Platform.SELECT,
{
"state": "{{ 'on' }}",
"options": "{{ ['off', 'on', 'auto'] }}",
"select_option": [],
},
),
(
Platform.SENSOR,
{
"state": "{{ 'yes' }}",
},
),
(
Platform.SWITCH,
{
"state": "{{ 'on' }}",
"turn_on": [],
"turn_off": [],
},
),
(
Platform.UPDATE,
{
"installed_version": "{{ '1.0' }}",
"latest_version": "{{ '2.0' }}",
},
),
(
Platform.VACUUM,
{
"state": "{{ 'docked' }}",
"start": [],
},
),
(
Platform.WEATHER,
{
"condition": "{{ 'cloudy' }}",
"temperature": "{{ 20 }}",
"humidity": "{{ 50 }}",
},
),
]
@pytest.mark.parametrize(("platform", "config"), TRIGGER_PLATFORM_CONFIGURATIONS)
async def test_trigger_schema_with_conditions(
hass: HomeAssistant,
platform: Platform,
config: ConfigType,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test conditions schema with valid and invalid configurations."""
await setup_entity(
hass,
TemplatePlatformSetup(
platform, "test_entity", make_test_trigger("sensor.test_state")
),
ConfigurationStyle.TRIGGER,
1,
{
"conditions": {
"condition": "template",
"value_template": "{{ 1 == 1 }}",
},
**config,
},
)
assert "ERROR" not in caplog.text
@pytest.mark.parametrize(
("platform", "config"),
[
(
Platform.BUTTON,
{
"press": [],
},
),
],
)
async def test_invalid_trigger_schema_with_conditions(
hass: HomeAssistant,
platform: Platform,
config: ConfigType,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test conditions schema with valid and invalid configurations."""
await setup_entity(
hass,
TemplatePlatformSetup(
platform, "test_entity", make_test_trigger("sensor.test_state")
),
ConfigurationStyle.TRIGGER,
0,
{
"conditions": {
"condition": "template",
"value_template": "{{ 1 == 1 }}",
},
**config,
},
)
error = (
"Invalid config for 'template': Unsupported option(s) found for domain button"
)
assert error in caplog.text
@pytest.mark.parametrize(
("platform", "config"),
[
(
Platform.ALARM_CONTROL_PANEL,
{
"state": "{{ 'disarmed' }}",
},
),
(
Platform.BINARY_SENSOR,
{
"state": "{{ 'on' }}",
},
),
(
Platform.COVER,
{
"state": "{{ 'open' }}",
"open_cover": [],
"close_cover": [],
},
),
(
Platform.DEVICE_TRACKER,
{
"in_zones": "{{ ['zone.home'] }}",
},
),
(
Platform.EVENT,
{
"event_type": "{{ 'single' }}",
"event_types": "{{ ['single'] }}",
},
),
(
Platform.FAN,
{
"state": "{{ 'on' }}",
"turn_on": [],
"turn_off": [],
},
),
(
Platform.IMAGE,
{
"url": "{{ 'http://www.test.com' }}",
},
),
(
Platform.LIGHT,
{
"state": "{{ 'on' }}",
"turn_on": [],
"turn_off": [],
},
),
(
Platform.LOCK,
{
"state": "{{ 'on' }}",
"lock": [],
"unlock": [],
},
),
(
Platform.NUMBER,
{
"state": "{{ 4 }}",
"min": "0",
"max": "100",
"step": "0.1",
"unit_of_measurement": "cm",
"set_value": [],
},
),
(
Platform.SELECT,
{
"state": "{{ 'on' }}",
"options": "{{ ['off', 'on', 'auto'] }}",
"select_option": [],
},
),
(
Platform.SENSOR,
{
"state": "{{ 'yes' }}",
},
),
(
Platform.SWITCH,
{
"state": "{{ 'on' }}",
"turn_on": [],
"turn_off": [],
},
),
(
Platform.UPDATE,
{
"installed_version": "{{ '1.0' }}",
"latest_version": "{{ '2.0' }}",
},
),
(
Platform.VACUUM,
{
"state": "{{ 'docked' }}",
"start": [],
},
),
(
Platform.WEATHER,
{
"condition": "{{ 'cloudy' }}",
"temperature": "{{ 20 }}",
"humidity": "{{ 50 }}",
},
),
],
)
async def test_trigger_schema_with_invalid_condition(
hass: HomeAssistant,
platform: Platform,
config: ConfigType,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test conditions schema with valid and invalid configurations."""
await setup_entity(
hass,
TemplatePlatformSetup(
platform, "test_entity", make_test_trigger("sensor.test_state")
),
ConfigurationStyle.TRIGGER,
1,
{
"conditions": {
"condition": "device",
"type": "is_off",
"device_id": "70c5f67ec2f82f9ba128fe6e99eb7dfa",
"entity_id": "c7e6f3753cb18937f2147bbbdccdd949",
"domain": "light",
},
**config,
},
)
assert len(hass.states.async_entity_ids(platform)) == 0
assert (
"The condition for test_entity failed to setup: Unknown device '70c5f67ec2f82f9ba128fe6e99eb7dfa'"
in caplog.text
)
@pytest.mark.parametrize(
("platform", "config"),
[
(
Platform.ALARM_CONTROL_PANEL,
{
"state": "{{ 'disarmed' }}",
},
),
(
Platform.BINARY_SENSOR,
{
"state": "{{ 'on' }}",
},
),
(
Platform.BUTTON,
{
"press": [],
},
),
(
Platform.COVER,
{
"state": "{{ 'open' }}",
"open_cover": [],
"close_cover": [],
},
),
(
Platform.DEVICE_TRACKER,
{
"in_zones": "{{ ['zone.home'] }}",
},
),
(
Platform.EVENT,
{
"event_type": "{{ 'single' }}",
"event_types": "{{ ['single'] }}",
},
),
(
Platform.FAN,
{
"state": "{{ 'on' }}",
"turn_on": [],
"turn_off": [],
},
),
(
Platform.IMAGE,
{
"url": "{{ 'http://www.test.com' }}",
},
),
(
Platform.LIGHT,
{
"state": "{{ 'on' }}",
"turn_on": [],
"turn_off": [],
},
),
(
Platform.LOCK,
{
"state": "{{ 'on' }}",
"lock": [],
"unlock": [],
},
),
(
Platform.NUMBER,
{
"state": "{{ 4 }}",
"min": "0",
"max": "100",
"step": "0.1",
"unit_of_measurement": "cm",
"set_value": [],
},
),
(
Platform.SELECT,
{
"state": "{{ 'on' }}",
"options": "{{ ['off', 'on', 'auto'] }}",
"select_option": [],
},
),
(
Platform.SENSOR,
{
"state": "{{ 'yes' }}",
},
),
(
Platform.SWITCH,
{
"state": "{{ 'on' }}",
"turn_on": [],
"turn_off": [],
},
),
(
Platform.UPDATE,
{
"installed_version": "{{ '1.0' }}",
"latest_version": "{{ '2.0' }}",
},
),
(
Platform.VACUUM,
{
"state": "{{ 'docked' }}",
"start": [],
},
),
(
Platform.WEATHER,
{
"condition": "{{ 'cloudy' }}",
"temperature": "{{ 20 }}",
"humidity": "{{ 50 }}",
},
),
],
)
async def test_invalid_schema_with_conditions(
hass: HomeAssistant,
platform: Platform,
config: ConfigType,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test conditions schema with valid and invalid configurations."""
await setup_entity(
hass,
TemplatePlatformSetup(
platform, "test_entity", make_test_trigger("sensor.test_state")
),
ConfigurationStyle.MODERN,
0,
{
"conditions": {
"condition": "template",
"value_template": "{{ 1 == 1 }}",
},
**config,
},
)
error = (
f"The conditions option for template {platform.replace('_', ' ')}: name: test_entity requires a trigger,"
" remove the conditions option or rewrite configuration to use a trigger"
)
assert error in caplog.text
@pytest.mark.parametrize(
("config", "expected"),
[
@@ -458,129 +968,7 @@ async def test_setup_component_bad_config_logs_error(
assert expected_error in caplog.text
@pytest.mark.parametrize(
("platform", "config"),
[
(
Platform.ALARM_CONTROL_PANEL,
{
"state": "{{ 'disarmed' }}",
},
),
(
Platform.BINARY_SENSOR,
{
"state": "{{ 'on' }}",
},
),
(
Platform.COVER,
{
"state": "{{ 'open' }}",
"open_cover": [],
"close_cover": [],
},
),
(
Platform.DEVICE_TRACKER,
{
"in_zones": "{{ ['zone.home'] }}",
},
),
(
Platform.EVENT,
{
"event_type": "{{ 'single' }}",
"event_types": "{{ ['single'] }}",
},
),
(
Platform.FAN,
{
"state": "{{ 'on' }}",
"turn_on": [],
"turn_off": [],
},
),
(
Platform.IMAGE,
{
"url": "{{ 'http://www.test.com' }}",
},
),
(
Platform.LIGHT,
{
"state": "{{ 'on' }}",
"turn_on": [],
"turn_off": [],
},
),
(
Platform.LOCK,
{
"state": "{{ 'on' }}",
"lock": [],
"unlock": [],
},
),
(
Platform.NUMBER,
{
"state": "{{ 4 }}",
"min": "0",
"max": "100",
"step": "0.1",
"unit_of_measurement": "cm",
"set_value": [],
},
),
(
Platform.SELECT,
{
"state": "{{ 'on' }}",
"options": "{{ ['off', 'on', 'auto'] }}",
"select_option": [],
},
),
(
Platform.SENSOR,
{
"state": "{{ 'yes' }}",
},
),
(
Platform.SWITCH,
{
"state": "{{ 'on' }}",
"turn_on": [],
"turn_off": [],
},
),
(
Platform.UPDATE,
{
"installed_version": "{{ '1.0' }}",
"latest_version": "{{ '2.0' }}",
},
),
(
Platform.VACUUM,
{
"state": "{{ 'docked' }}",
"start": [],
},
),
(
Platform.WEATHER,
{
"condition": "{{ 'cloudy' }}",
"temperature": "{{ 20 }}",
"humidity": "{{ 50 }}",
},
),
],
)
@pytest.mark.parametrize(("platform", "config"), TRIGGER_PLATFORM_CONFIGURATIONS)
@pytest.mark.parametrize(
("extra_section_config", "breadcrumb"),
[
@@ -630,129 +1018,7 @@ async def test_trigger_schema_with_invalid_actions(
)
@pytest.mark.parametrize(
("platform", "config"),
[
(
Platform.ALARM_CONTROL_PANEL,
{
"state": "{{ 'disarmed' }}",
},
),
(
Platform.BINARY_SENSOR,
{
"state": "{{ 'on' }}",
},
),
(
Platform.COVER,
{
"state": "{{ 'open' }}",
"open_cover": [],
"close_cover": [],
},
),
(
Platform.DEVICE_TRACKER,
{
"in_zones": "{{ ['zone.home'] }}",
},
),
(
Platform.EVENT,
{
"event_type": "{{ 'single' }}",
"event_types": "{{ ['single'] }}",
},
),
(
Platform.FAN,
{
"state": "{{ 'on' }}",
"turn_on": [],
"turn_off": [],
},
),
(
Platform.IMAGE,
{
"url": "{{ 'http://www.test.com' }}",
},
),
(
Platform.LIGHT,
{
"state": "{{ 'on' }}",
"turn_on": [],
"turn_off": [],
},
),
(
Platform.LOCK,
{
"state": "{{ 'on' }}",
"lock": [],
"unlock": [],
},
),
(
Platform.NUMBER,
{
"state": "{{ 4 }}",
"min": "0",
"max": "100",
"step": "0.1",
"unit_of_measurement": "cm",
"set_value": [],
},
),
(
Platform.SELECT,
{
"state": "{{ 'on' }}",
"options": "{{ ['off', 'on', 'auto'] }}",
"select_option": [],
},
),
(
Platform.SENSOR,
{
"state": "{{ 'yes' }}",
},
),
(
Platform.SWITCH,
{
"state": "{{ 'on' }}",
"turn_on": [],
"turn_off": [],
},
),
(
Platform.UPDATE,
{
"installed_version": "{{ '1.0' }}",
"latest_version": "{{ '2.0' }}",
},
),
(
Platform.VACUUM,
{
"state": "{{ 'docked' }}",
"start": [],
},
),
(
Platform.WEATHER,
{
"condition": "{{ 'cloudy' }}",
"temperature": "{{ 20 }}",
"humidity": "{{ 50 }}",
},
),
],
)
@pytest.mark.parametrize(("platform", "config"), TRIGGER_PLATFORM_CONFIGURATIONS)
async def test_trigger_schema_with_valid_actions(
hass: HomeAssistant,
platform: Platform,
@@ -15,6 +15,7 @@ from homeassistant.const import (
SERVICE_RELOAD,
STATE_OFF,
STATE_ON,
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import condition, template
@@ -33,6 +34,7 @@ _PICTURE_TEMPLATE = '/local/picture_o{{ "n" if value=="on" else "ff" }}'
class TestEntity(trigger_entity.TriggerEntity):
"""Test entity class."""
domain = "test"
__test__ = False
_entity_id_format = "test.{}"
extra_template_keys = (CONF_STATE,)
@@ -370,3 +372,153 @@ async def test_reload_stops_script_and_unsubscribes_triggers(
# Old trigger should be unsubscribed
listeners = hass.bus.async_listeners()
assert listeners.get("test_event", 0) == 0
async def test_entity_conditions_with_multiple_entities(hass: HomeAssistant) -> None:
"""Test entity conditions with multiple entities."""
with assert_setup_component(1, DOMAIN):
assert await async_setup_component(
hass,
DOMAIN,
{
"template": {
"triggers": {
"trigger": "state",
"entity_id": ["sensor.trigger"],
},
"sensor": [
{
"name": "a",
"state": "{{ states('sensor.trigger') }}",
"conditions": {
"condition": "numeric_state",
"entity_id": "sensor.trigger",
"above": 1,
},
},
{
"name": "b",
"state": "{{ trigger.to_state.state }}",
"conditions": {
"condition": "numeric_state",
"entity_id": "sensor.trigger",
"below": 1,
},
},
],
},
},
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
state = hass.states.get("sensor.a")
assert state
assert state.state == STATE_UNKNOWN
state = hass.states.get("sensor.b")
assert state
assert state.state == STATE_UNKNOWN
await async_trigger(hass, "sensor.trigger", "2")
state = hass.states.get("sensor.a")
assert state
assert state.state == "2"
state = hass.states.get("sensor.b")
assert state
assert state.state == STATE_UNKNOWN
await async_trigger(hass, "sensor.trigger", "0")
state = hass.states.get("sensor.a")
assert state
assert state.state == "2"
state = hass.states.get("sensor.b")
assert state
assert state.state == "0"
async def test_entity_conditions_variables(hass: HomeAssistant) -> None:
"""Test entity conditions variables."""
await async_trigger(hass, "sensor.start", "0")
with assert_setup_component(1, DOMAIN):
assert await async_setup_component(
hass,
DOMAIN,
{
"template": {
"triggers": {
"trigger": "state",
"entity_id": ["sensor.trigger", "sensor.start"],
},
"variables": {"a": "{{ states('sensor.start') }}"},
"sensor": [
{
"name": "test",
"state": "{{ states('sensor.start') }}",
"variables": {"b": "{{ a + 1 }}"},
"conditions": {
"condition": "template",
"value_template": "{{ b > 1 }}",
},
"attributes": {
"a": "{{ a }}",
"b": "{{ b }}",
},
},
],
},
},
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
state = hass.states.get("sensor.test")
assert state
assert state.state == STATE_UNKNOWN
assert "a" not in state.attributes
assert "b" not in state.attributes
await async_trigger(hass, "sensor.trigger", "anything")
state = hass.states.get("sensor.test")
assert state
assert state.state == STATE_UNKNOWN
assert "a" not in state.attributes
assert "b" not in state.attributes
await async_trigger(hass, "sensor.start", "1")
state = hass.states.get("sensor.test")
assert state
assert state.state == "1"
assert state.attributes["a"] == 1
assert state.attributes["b"] == 2
await async_trigger(hass, "sensor.start", "0")
state = hass.states.get("sensor.test")
assert state
assert state.state == "1"
assert state.attributes["a"] == 1
assert state.attributes["b"] == 2
async def test_entity_remove_unloads_condition(
hass: HomeAssistant,
) -> None:
"""Test that removing the entity unloads the condition."""
coordinator = TriggerUpdateCoordinator(hass, {})
mock_cond = Mock(spec=condition.ConditionsChecker)
entity = TestEntity(hass, coordinator, {})
entity._cond_func = mock_cond
await entity.async_will_remove_from_hass()
mock_cond.async_unload.assert_called_once()