mirror of
https://github.com/home-assistant/core.git
synced 2026-09-06 05:22:44 +01:00
Add conditions: to trigger based template entity options (#174055)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user