Adjust automation to plural triggers/conditions/actions keys (#123823)

* Adjust automation to plural triggers/conditions/actions keys

* Fix some tests

* Adjust websocket tests

* Fix search tests

* Convert blueprint and blueprint inputs to modern schema

* Pass schema when creating Blueprint object

* Update tests

* Adjust websocket api

---------

Co-authored-by: Joostlek <joostlek@outlook.com>
Co-authored-by: Erik <erik@montnemery.com>
pull/126298/head
Franck Nijhof 2024-09-24 20:03:23 +02:00 committed by GitHub
parent 08bdf797f0
commit 9dfabc3fb7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 488 additions and 183 deletions

View File

@ -19,7 +19,7 @@ from homeassistant.const import (
ATTR_MODE,
ATTR_NAME,
CONF_ALIAS,
CONF_CONDITION,
CONF_CONDITIONS,
CONF_DEVICE_ID,
CONF_ENTITY_ID,
CONF_EVENT_DATA,
@ -98,11 +98,11 @@ from homeassistant.util.hass_dict import HassKey
from .config import AutomationConfig, ValidationStatus
from .const import (
CONF_ACTION,
CONF_ACTIONS,
CONF_INITIAL_STATE,
CONF_TRACE,
CONF_TRIGGER,
CONF_TRIGGER_VARIABLES,
CONF_TRIGGERS,
DEFAULT_INITIAL_STATE,
DOMAIN,
LOGGER,
@ -955,7 +955,7 @@ async def _create_automation_entities(
action_script = Script(
hass,
config_block[CONF_ACTION],
config_block[CONF_ACTIONS],
name,
DOMAIN,
running_description="automation actions",
@ -968,7 +968,7 @@ async def _create_automation_entities(
# and so will pass them on to the script.
)
if CONF_CONDITION in config_block:
if CONF_CONDITIONS in config_block:
cond_func = await _async_process_if(hass, name, config_block)
if cond_func is None:
@ -991,7 +991,7 @@ async def _create_automation_entities(
entity = AutomationEntity(
automation_id,
name,
config_block[CONF_TRIGGER],
config_block[CONF_TRIGGERS],
cond_func,
action_script,
initial_state,
@ -1131,7 +1131,7 @@ async def _async_process_if(
hass: HomeAssistant, name: str, config: dict[str, Any]
) -> IfAction | None:
"""Process if checks."""
if_configs = config[CONF_CONDITION]
if_configs = config[CONF_CONDITIONS]
try:
if_action = await condition.async_conditions_from_config(

View File

@ -16,6 +16,7 @@ from homeassistant.config import config_per_platform, config_without_domain
from homeassistant.const import (
CONF_ALIAS,
CONF_CONDITION,
CONF_CONDITIONS,
CONF_DESCRIPTION,
CONF_ID,
CONF_VARIABLES,
@ -30,11 +31,13 @@ from homeassistant.util.yaml.input import UndefinedSubstitution
from .const import (
CONF_ACTION,
CONF_ACTIONS,
CONF_HIDE_ENTITY,
CONF_INITIAL_STATE,
CONF_TRACE,
CONF_TRIGGER,
CONF_TRIGGER_VARIABLES,
CONF_TRIGGERS,
DOMAIN,
LOGGER,
)
@ -52,7 +55,41 @@ _MINIMAL_PLATFORM_SCHEMA = vol.Schema(
)
def _backward_compat_schema(value: Any | None) -> Any:
"""Backward compatibility for automations."""
if not isinstance(value, dict):
return value
# `trigger` has been renamed to `triggers`
if CONF_TRIGGER in value:
if CONF_TRIGGERS in value:
raise vol.Invalid(
"Cannot specify both 'trigger' and 'triggers'. Please use 'triggers' only."
)
value[CONF_TRIGGERS] = value.pop(CONF_TRIGGER)
# `condition` has been renamed to `conditions`
if CONF_CONDITION in value:
if CONF_CONDITIONS in value:
raise vol.Invalid(
"Cannot specify both 'condition' and 'conditions'. Please use 'conditions' only."
)
value[CONF_CONDITIONS] = value.pop(CONF_CONDITION)
# `action` has been renamed to `actions`
if CONF_ACTION in value:
if CONF_ACTIONS in value:
raise vol.Invalid(
"Cannot specify both 'action' and 'actions'. Please use 'actions' only."
)
value[CONF_ACTIONS] = value.pop(CONF_ACTION)
return value
PLATFORM_SCHEMA = vol.All(
_backward_compat_schema,
cv.deprecated(CONF_HIDE_ENTITY),
script.make_script_schema(
{
@ -63,16 +100,20 @@ PLATFORM_SCHEMA = vol.All(
vol.Optional(CONF_TRACE, default={}): TRACE_CONFIG_SCHEMA,
vol.Optional(CONF_INITIAL_STATE): cv.boolean,
vol.Optional(CONF_HIDE_ENTITY): cv.boolean,
vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA,
vol.Optional(CONF_CONDITION): cv.CONDITIONS_SCHEMA,
vol.Required(CONF_TRIGGERS): cv.TRIGGER_SCHEMA,
vol.Optional(CONF_CONDITIONS): cv.CONDITIONS_SCHEMA,
vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA,
vol.Optional(CONF_TRIGGER_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA,
vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA,
vol.Required(CONF_ACTIONS): cv.SCRIPT_SCHEMA,
},
script.SCRIPT_MODE_SINGLE,
),
)
AUTOMATION_BLUEPRINT_SCHEMA = vol.All(
_backward_compat_schema, blueprint.schemas.BLUEPRINT_SCHEMA
)
async def _async_validate_config_item( # noqa: C901
hass: HomeAssistant,
@ -151,7 +192,9 @@ async def _async_validate_config_item( # noqa: C901
uses_blueprint = True
blueprints = async_get_blueprints(hass)
try:
blueprint_inputs = await blueprints.async_inputs_from_config(config)
blueprint_inputs = await blueprints.async_inputs_from_config(
_backward_compat_schema(config)
)
except blueprint.BlueprintException as err:
if warn_on_errors:
LOGGER.error(
@ -199,8 +242,8 @@ async def _async_validate_config_item( # noqa: C901
automation_config.raw_config = raw_config
try:
automation_config[CONF_TRIGGER] = await async_validate_trigger_config(
hass, validated_config[CONF_TRIGGER]
automation_config[CONF_TRIGGERS] = await async_validate_trigger_config(
hass, validated_config[CONF_TRIGGERS]
)
except (
vol.Invalid,
@ -216,10 +259,10 @@ async def _async_validate_config_item( # noqa: C901
)
return automation_config
if CONF_CONDITION in validated_config:
if CONF_CONDITIONS in validated_config:
try:
automation_config[CONF_CONDITION] = await async_validate_conditions_config(
hass, validated_config[CONF_CONDITION]
automation_config[CONF_CONDITIONS] = await async_validate_conditions_config(
hass, validated_config[CONF_CONDITIONS]
)
except (
vol.Invalid,
@ -239,8 +282,8 @@ async def _async_validate_config_item( # noqa: C901
return automation_config
try:
automation_config[CONF_ACTION] = await script.async_validate_actions_config(
hass, validated_config[CONF_ACTION]
automation_config[CONF_ACTIONS] = await script.async_validate_actions_config(
hass, validated_config[CONF_ACTIONS]
)
except (
vol.Invalid,

View File

@ -3,7 +3,9 @@
import logging
CONF_ACTION = "action"
CONF_ACTIONS = "actions"
CONF_TRIGGER = "trigger"
CONF_TRIGGERS = "triggers"
CONF_TRIGGER_VARIABLES = "trigger_variables"
DOMAIN = "automation"

View File

@ -28,6 +28,14 @@ async def _reload_blueprint_automations(
@callback
def async_get_blueprints(hass: HomeAssistant) -> blueprint.DomainBlueprints:
"""Get automation blueprints."""
# pylint: disable-next=import-outside-toplevel
from .config import AUTOMATION_BLUEPRINT_SCHEMA
return blueprint.DomainBlueprints(
hass, DOMAIN, LOGGER, _blueprint_in_use, _reload_blueprint_automations
hass,
DOMAIN,
LOGGER,
_blueprint_in_use,
_reload_blueprint_automations,
AUTOMATION_BLUEPRINT_SCHEMA,
)

View File

@ -15,7 +15,7 @@ from .errors import ( # noqa: F401
MissingInput,
)
from .models import Blueprint, BlueprintInputs, DomainBlueprints # noqa: F401
from .schemas import is_blueprint_instance_config # noqa: F401
from .schemas import BLUEPRINT_SCHEMA, is_blueprint_instance_config # noqa: F401
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)

View File

@ -16,7 +16,7 @@ from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.util import yaml
from .models import Blueprint
from .schemas import is_blueprint_config
from .schemas import BLUEPRINT_SCHEMA, is_blueprint_config
COMMUNITY_TOPIC_PATTERN = re.compile(
r"^https://community.home-assistant.io/t/[a-z0-9-]+/(?P<topic>\d+)(?:/(?P<post>\d+)|)$"
@ -126,7 +126,7 @@ def _extract_blueprint_from_community_topic(
continue
assert isinstance(data, dict)
blueprint = Blueprint(data)
blueprint = Blueprint(data, schema=BLUEPRINT_SCHEMA)
break
if blueprint is None:
@ -169,7 +169,7 @@ async def fetch_blueprint_from_github_url(
raw_yaml = await resp.text()
data = yaml.parse_yaml(raw_yaml)
assert isinstance(data, dict)
blueprint = Blueprint(data)
blueprint = Blueprint(data, schema=BLUEPRINT_SCHEMA)
parsed_import_url = yarl.URL(import_url)
suggested_filename = f"{parsed_import_url.parts[1]}/{parsed_import_url.parts[-1]}"
@ -211,7 +211,7 @@ async def fetch_blueprint_from_github_gist_url(
continue
assert isinstance(data, dict)
blueprint = Blueprint(data)
blueprint = Blueprint(data, schema=BLUEPRINT_SCHEMA)
break
if blueprint is None:
@ -238,7 +238,7 @@ async def fetch_blueprint_from_website_url(
raw_yaml = await resp.text()
data = yaml.parse_yaml(raw_yaml)
assert isinstance(data, dict)
blueprint = Blueprint(data)
blueprint = Blueprint(data, schema=BLUEPRINT_SCHEMA)
parsed_import_url = yarl.URL(url)
suggested_filename = f"homeassistant/{parsed_import_url.parts[-1][:-5]}"
@ -256,7 +256,7 @@ async def fetch_blueprint_from_generic_url(
data = yaml.parse_yaml(raw_yaml)
assert isinstance(data, dict)
blueprint = Blueprint(data)
blueprint = Blueprint(data, schema=BLUEPRINT_SCHEMA)
parsed_import_url = yarl.URL(url)
suggested_filename = f"{parsed_import_url.host}/{parsed_import_url.parts[-1][:-5]}"
@ -273,7 +273,11 @@ FETCH_FUNCTIONS = (
async def fetch_blueprint_from_url(hass: HomeAssistant, url: str) -> ImportedBlueprint:
"""Get a blueprint from a url."""
"""Get a blueprint from a url.
The returned blueprint will only be validated with BLUEPRINT_SCHEMA, not the domain
specific schema.
"""
for func in FETCH_FUNCTIONS:
with suppress(UnsupportedUrl):
imported_bp = await func(hass, url)

View File

@ -44,7 +44,7 @@ from .errors import (
InvalidBlueprintInputs,
MissingInput,
)
from .schemas import BLUEPRINT_INSTANCE_FIELDS, BLUEPRINT_SCHEMA
from .schemas import BLUEPRINT_INSTANCE_FIELDS
class Blueprint:
@ -56,10 +56,11 @@ class Blueprint:
*,
path: str | None = None,
expected_domain: str | None = None,
schema: Callable[[Any], Any],
) -> None:
"""Initialize a blueprint."""
try:
data = self.data = BLUEPRINT_SCHEMA(data)
data = self.data = schema(data)
except vol.Invalid as err:
raise InvalidBlueprint(expected_domain, path, data, err) from err
@ -197,6 +198,7 @@ class DomainBlueprints:
logger: logging.Logger,
blueprint_in_use: Callable[[HomeAssistant, str], bool],
reload_blueprint_consumers: Callable[[HomeAssistant, str], Awaitable[None]],
blueprint_schema: Callable[[Any], Any],
) -> None:
"""Initialize a domain blueprints instance."""
self.hass = hass
@ -206,6 +208,7 @@ class DomainBlueprints:
self._reload_blueprint_consumers = reload_blueprint_consumers
self._blueprints: dict[str, Blueprint | None] = {}
self._load_lock = asyncio.Lock()
self._blueprint_schema = blueprint_schema
hass.data.setdefault(DOMAIN, {})[domain] = self
@ -233,7 +236,10 @@ class DomainBlueprints:
raise FailedToLoad(self.domain, blueprint_path, err) from err
return Blueprint(
blueprint_data, expected_domain=self.domain, path=blueprint_path
blueprint_data,
expected_domain=self.domain,
path=blueprint_path,
schema=self._blueprint_schema,
)
def _load_blueprints(self) -> dict[str, Blueprint | BlueprintException | None]:

View File

@ -18,6 +18,7 @@ from homeassistant.util import yaml
from . import importer, models
from .const import DOMAIN
from .errors import BlueprintException, FailedToLoad, FileAlreadyExists
from .schemas import BLUEPRINT_SCHEMA
@callback
@ -174,7 +175,9 @@ async def ws_save_blueprint(
try:
yaml_data = cast(dict[str, Any], yaml.parse_yaml(msg["yaml"]))
blueprint = models.Blueprint(yaml_data, expected_domain=domain)
blueprint = models.Blueprint(
yaml_data, expected_domain=domain, schema=BLUEPRINT_SCHEMA
)
if "source_url" in msg:
blueprint.update_metadata(source_url=msg["source_url"])
except HomeAssistantError as err:

View File

@ -70,7 +70,16 @@ class EditAutomationConfigView(EditIdBasedConfigView):
updated_value = {CONF_ID: config_key}
# Iterate through some keys that we want to have ordered in the output
for key in ("alias", "description", "trigger", "condition", "action"):
for key in (
"alias",
"description",
"triggers",
"trigger",
"conditions",
"condition",
"actions",
"action",
):
if key in new_value:
updated_value[key] = new_value[key]

View File

@ -1,6 +1,6 @@
"""Helpers for automation integration."""
from homeassistant.components.blueprint import DomainBlueprints
from homeassistant.components.blueprint import BLUEPRINT_SCHEMA, DomainBlueprints
from homeassistant.const import SERVICE_RELOAD
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.singleton import singleton
@ -27,5 +27,10 @@ async def _reload_blueprint_scripts(hass: HomeAssistant, blueprint_path: str) ->
def async_get_blueprints(hass: HomeAssistant) -> DomainBlueprints:
"""Get script blueprints."""
return DomainBlueprints(
hass, DOMAIN, LOGGER, _blueprint_in_use, _reload_blueprint_scripts
hass,
DOMAIN,
LOGGER,
_blueprint_in_use,
_reload_blueprint_scripts,
BLUEPRINT_SCHEMA,
)

View File

@ -859,9 +859,9 @@ def handle_fire_event(
@decorators.websocket_command(
{
vol.Required("type"): "validate_config",
vol.Optional("trigger"): cv.match_all,
vol.Optional("condition"): cv.match_all,
vol.Optional("action"): cv.match_all,
vol.Optional("triggers"): cv.match_all,
vol.Optional("conditions"): cv.match_all,
vol.Optional("actions"): cv.match_all,
}
)
@decorators.async_response
@ -876,9 +876,13 @@ async def handle_validate_config(
result = {}
for key, schema, validator in (
("trigger", cv.TRIGGER_SCHEMA, trigger.async_validate_trigger_config),
("condition", cv.CONDITIONS_SCHEMA, condition.async_validate_conditions_config),
("action", cv.SCRIPT_SCHEMA, script.async_validate_actions_config),
("triggers", cv.TRIGGER_SCHEMA, trigger.async_validate_trigger_config),
(
"conditions",
cv.CONDITIONS_SCHEMA,
condition.async_validate_conditions_config,
),
("actions", cv.SCRIPT_SCHEMA, script.async_validate_actions_config),
):
if key not in msg:
continue

View File

@ -38,7 +38,10 @@ def patch_blueprint(
return orig_load(self, path)
return models.Blueprint(
yaml.load_yaml(data_path), expected_domain=self.domain, path=path
yaml.load_yaml(data_path),
expected_domain=self.domain,
path=path,
schema=automation.config.AUTOMATION_BLUEPRINT_SCHEMA,
)
with patch(

View File

@ -240,7 +240,7 @@ async def test_trigger_service_ignoring_condition(
automation.DOMAIN: {
"alias": "test",
"trigger": [{"platform": "event", "event_type": "test_event"}],
"condition": {
"conditions": {
"condition": "numeric_state",
"entity_id": "non.existing",
"above": "1",
@ -292,8 +292,8 @@ async def test_two_conditions_with_and(
automation.DOMAIN,
{
automation.DOMAIN: {
"trigger": [{"platform": "event", "event_type": "test_event"}],
"condition": [
"triggers": [{"platform": "event", "event_type": "test_event"}],
"conditions": [
{"condition": "state", "entity_id": entity_id, "state": "100"},
{
"condition": "numeric_state",
@ -301,7 +301,7 @@ async def test_two_conditions_with_and(
"below": 150,
},
],
"action": {"action": "test.automation"},
"actions": {"action": "test.automation"},
}
},
)
@ -331,9 +331,9 @@ async def test_shorthand_conditions_template(
automation.DOMAIN,
{
automation.DOMAIN: {
"trigger": [{"platform": "event", "event_type": "test_event"}],
"condition": "{{ is_state('test.entity', 'hello') }}",
"action": {"action": "test.automation"},
"triggers": [{"platform": "event", "event_type": "test_event"}],
"conditions": "{{ is_state('test.entity', 'hello') }}",
"actions": {"action": "test.automation"},
}
},
)
@ -807,8 +807,8 @@ async def test_reload_unchanged_does_not_stop(
config = {
automation.DOMAIN: {
"alias": "hello",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": [
"triggers": {"platform": "event", "event_type": "test_event"},
"actions": [
{"event": "running"},
{"wait_template": "{{ is_state('test.entity', 'goodbye') }}"},
{"action": "test.automation"},
@ -854,8 +854,8 @@ async def test_reload_single_unchanged_does_not_stop(
automation.DOMAIN: {
"id": "sun",
"alias": "hello",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": [
"triggers": {"platform": "event", "event_type": "test_event"},
"actions": [
{"event": "running"},
{"wait_template": "{{ is_state('test.entity', 'goodbye') }}"},
{"action": "test.automation"},
@ -1092,13 +1092,13 @@ async def test_reload_moved_automation_without_alias(
config = {
automation.DOMAIN: [
{
"trigger": {"platform": "event", "event_type": "test_event"},
"action": [{"action": "test.automation"}],
"triggers": {"platform": "event", "event_type": "test_event"},
"actions": [{"action": "test.automation"}],
},
{
"alias": "automation_with_alias",
"trigger": {"platform": "event", "event_type": "test_event2"},
"action": [{"action": "test.automation"}],
"triggers": {"platform": "event", "event_type": "test_event2"},
"actions": [{"action": "test.automation"}],
},
]
}
@ -1148,18 +1148,18 @@ async def test_reload_identical_automations_without_id(
automation.DOMAIN: [
{
"alias": "dolly",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": [{"action": "test.automation"}],
"triggers": {"platform": "event", "event_type": "test_event"},
"actions": [{"action": "test.automation"}],
},
{
"alias": "dolly",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": [{"action": "test.automation"}],
"triggers": {"platform": "event", "event_type": "test_event"},
"actions": [{"action": "test.automation"}],
},
{
"alias": "dolly",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": [{"action": "test.automation"}],
"triggers": {"platform": "event", "event_type": "test_event"},
"actions": [{"action": "test.automation"}],
},
]
}
@ -1245,13 +1245,13 @@ async def test_reload_identical_automations_without_id(
"automation_config",
[
{
"trigger": {"platform": "event", "event_type": "test_event"},
"action": [{"action": "test.automation"}],
"triggers": {"platform": "event", "event_type": "test_event"},
"actions": [{"action": "test.automation"}],
},
# An automation using templates
{
"trigger": {"platform": "event", "event_type": "test_event"},
"action": [{"action": "{{ 'test.automation' }}"}],
"triggers": {"platform": "event", "event_type": "test_event"},
"actions": [{"action": "{{ 'test.automation' }}"}],
},
# An automation using blueprint
{
@ -1277,14 +1277,14 @@ async def test_reload_identical_automations_without_id(
},
{
"id": "sun",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": [{"action": "test.automation"}],
"triggers": {"platform": "event", "event_type": "test_event"},
"actions": [{"action": "test.automation"}],
},
# An automation using templates
{
"id": "sun",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": [{"action": "{{ 'test.automation' }}"}],
"triggers": {"platform": "event", "event_type": "test_event"},
"actions": [{"action": "{{ 'test.automation' }}"}],
},
# An automation using blueprint
{
@ -1380,8 +1380,8 @@ async def test_reload_automation_when_blueprint_changes(
# Reload the automations without any change, but with updated blueprint
blueprint_path = automation.async_get_blueprints(hass).blueprint_folder
blueprint_config = yaml.load_yaml(blueprint_path / "test_event_service.yaml")
blueprint_config["action"] = [blueprint_config["action"]]
blueprint_config["action"].append(blueprint_config["action"][-1])
blueprint_config["actions"] = [blueprint_config["actions"]]
blueprint_config["actions"].append(blueprint_config["actions"][-1])
with (
patch(
@ -1650,13 +1650,13 @@ async def test_automation_not_trigger_on_bootstrap(hass: HomeAssistant) -> None:
(
{},
"could not be validated",
"required key not provided @ data['action']",
"required key not provided @ data['actions']",
"validation_failed_schema",
),
(
{
"trigger": {"platform": "automation"},
"action": [],
"triggers": {"platform": "automation"},
"actions": [],
},
"failed to setup triggers",
"Integration 'automation' does not provide trigger support.",
@ -1664,14 +1664,14 @@ async def test_automation_not_trigger_on_bootstrap(hass: HomeAssistant) -> None:
),
(
{
"trigger": {"platform": "event", "event_type": "test_event"},
"condition": {
"triggers": {"platform": "event", "event_type": "test_event"},
"conditions": {
"condition": "state",
# The UUID will fail being resolved to en entity_id
"entity_id": "abcdabcdabcdabcdabcdabcdabcdabcd",
"state": "blah",
},
"action": [],
"actions": [],
},
"failed to setup conditions",
"Unknown entity registry entry abcdabcdabcdabcdabcdabcdabcdabcd.",
@ -1679,8 +1679,8 @@ async def test_automation_not_trigger_on_bootstrap(hass: HomeAssistant) -> None:
),
(
{
"trigger": {"platform": "event", "event_type": "test_event"},
"action": {
"triggers": {"platform": "event", "event_type": "test_event"},
"actions": {
"condition": "state",
# The UUID will fail being resolved to en entity_id
"entity_id": "abcdabcdabcdabcdabcdabcdabcdabcd",
@ -1712,8 +1712,8 @@ async def test_automation_bad_config_validation(
{"alias": "bad_automation", **broken_config},
{
"alias": "good_automation",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": {
"triggers": {"platform": "event", "event_type": "test_event"},
"actions": {
"action": "test.automation",
"entity_id": "hello.world",
},
@ -1970,7 +1970,7 @@ async def test_extraction_functions(
DOMAIN: [
{
"alias": "test1",
"trigger": [
"triggers": [
{"platform": "state", "entity_id": "sensor.trigger_state"},
{
"platform": "numeric_state",
@ -2006,12 +2006,12 @@ async def test_extraction_functions(
"event_data": {"entity_id": 123},
},
],
"condition": {
"conditions": {
"condition": "state",
"entity_id": "light.condition_state",
"state": "on",
},
"action": [
"actions": [
{
"action": "test.script",
"data": {"entity_id": "light.in_both"},
@ -2042,7 +2042,7 @@ async def test_extraction_functions(
},
{
"alias": "test2",
"trigger": [
"triggers": [
{
"platform": "device",
"domain": "light",
@ -2078,14 +2078,14 @@ async def test_extraction_functions(
"event_data": {"device_id": 123},
},
],
"condition": {
"conditions": {
"condition": "device",
"device_id": condition_device.id,
"domain": "light",
"type": "is_on",
"entity_id": "light.bla",
},
"action": [
"actions": [
{
"action": "test.script",
"data": {"entity_id": "light.in_both"},
@ -2112,7 +2112,7 @@ async def test_extraction_functions(
},
{
"alias": "test3",
"trigger": [
"triggers": [
{
"platform": "event",
"event_type": "esphome.button_pressed",
@ -2131,14 +2131,14 @@ async def test_extraction_functions(
"event_data": {"area_id": 123},
},
],
"condition": {
"conditions": {
"condition": "device",
"device_id": condition_device.id,
"domain": "light",
"type": "is_on",
"entity_id": "light.bla",
},
"action": [
"actions": [
{
"action": "test.script",
"data": {"entity_id": "light.in_both"},
@ -2287,8 +2287,8 @@ async def test_automation_variables(
"event_type": "{{ trigger.event.event_type }}",
"this_variables": "{{this.entity_id}}",
},
"trigger": {"platform": "event", "event_type": "test_event"},
"action": {
"triggers": {"platform": "event", "event_type": "test_event"},
"actions": {
"action": "test.automation",
"data": {
"value": "{{ test_var }}",
@ -2303,11 +2303,11 @@ async def test_automation_variables(
"test_var": "defined_in_config",
},
"trigger": {"platform": "event", "event_type": "test_event_2"},
"condition": {
"conditions": {
"condition": "template",
"value_template": "{{ trigger.event.data.pass_condition }}",
},
"action": {
"actions": {
"action": "test.automation",
},
},
@ -2315,8 +2315,8 @@ async def test_automation_variables(
"variables": {
"test_var": "{{ trigger.event.data.break + 1 }}",
},
"trigger": {"platform": "event", "event_type": "test_event_3"},
"action": {
"triggers": {"platform": "event", "event_type": "test_event_3"},
"actions": {
"action": "test.automation",
},
},
@ -2517,6 +2517,107 @@ async def test_blueprint_automation(
]
async def test_blueprint_automation_legacy_schema(
hass: HomeAssistant, calls: list[ServiceCall]
) -> None:
"""Test blueprint automation where the blueprint is using legacy schema."""
assert await async_setup_component(
hass,
"automation",
{
"automation": {
"use_blueprint": {
"path": "test_event_service_legacy_schema.yaml",
"input": {
"trigger_event": "blueprint_event",
"service_to_call": "test.automation",
"a_number": 5,
},
}
}
},
)
hass.bus.async_fire("blueprint_event")
await hass.async_block_till_done()
assert len(calls) == 1
assert automation.entities_in_automation(hass, "automation.automation_0") == [
"light.kitchen"
]
assert (
automation.blueprint_in_automation(hass, "automation.automation_0")
== "test_event_service_legacy_schema.yaml"
)
assert automation.automations_with_blueprint(
hass, "test_event_service_legacy_schema.yaml"
) == ["automation.automation_0"]
@pytest.mark.parametrize(
("blueprint", "override"),
[
# Override a blueprint with modern schema with legacy schema
(
"test_event_service.yaml",
{"trigger": {"platform": "event", "event_type": "override"}},
),
# Override a blueprint with modern schema with modern schema
(
"test_event_service.yaml",
{"triggers": {"platform": "event", "event_type": "override"}},
),
# Override a blueprint with legacy schema with legacy schema
(
"test_event_service_legacy_schema.yaml",
{"trigger": {"platform": "event", "event_type": "override"}},
),
# Override a blueprint with legacy schema with modern schema
(
"test_event_service_legacy_schema.yaml",
{"triggers": {"platform": "event", "event_type": "override"}},
),
],
)
async def test_blueprint_automation_override(
hass: HomeAssistant, calls: list[ServiceCall], blueprint: str, override: dict
) -> None:
"""Test blueprint automation where the automation config overrides the blueprint."""
assert await async_setup_component(
hass,
"automation",
{
"automation": {
"use_blueprint": {
"path": blueprint,
"input": {
"trigger_event": "blueprint_event",
"service_to_call": "test.automation",
"a_number": 5,
},
},
}
| override
},
)
hass.bus.async_fire("blueprint_event")
await hass.async_block_till_done()
assert len(calls) == 0
hass.bus.async_fire("override")
await hass.async_block_till_done()
assert len(calls) == 1
assert automation.entities_in_automation(hass, "automation.automation_0") == [
"light.kitchen"
]
assert (
automation.blueprint_in_automation(hass, "automation.automation_0") == blueprint
)
assert automation.automations_with_blueprint(hass, blueprint) == [
"automation.automation_0"
]
@pytest.mark.parametrize(
("blueprint_inputs", "problem", "details"),
[
@ -2542,7 +2643,7 @@ async def test_blueprint_automation(
"Blueprint 'Call service based on event' generated invalid automation",
(
"value should be a string for dictionary value @"
" data['action'][0]['action']"
" data['actions'][0]['action']"
),
),
],
@ -3020,8 +3121,8 @@ async def test_websocket_config(
"""Test config command."""
config = {
"alias": "hello",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": {"action": "test.automation", "data": 100},
"triggers": {"platform": "event", "event_type": "test_event"},
"actions": {"action": "test.automation", "data": 100},
}
assert await async_setup_component(
hass, automation.DOMAIN, {automation.DOMAIN: config}
@ -3303,16 +3404,26 @@ async def test_two_automation_call_restart_script_right_after_each_other(
assert len(events) == 1
async def test_action_service_backward_compatibility(
async def test_action_backward_compatibility(
hass: HomeAssistant, calls: list[ServiceCall]
) -> None:
"""Test we can still use the service call method."""
"""Test we can still use old-style automations.
- Services action using the `service` key instead of `action`
- Singular `trigger` instead of `triggers`
- Singular `condition` instead of `conditions`
- Singular `action` instead of `actions`
"""
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: {
"trigger": {"platform": "event", "event_type": "test_event"},
"condition": {
"condition": "template",
"value_template": "{{ True }}",
},
"action": {
"service": "test.automation",
"entity_id": "hello.world",
@ -3327,3 +3438,48 @@ async def test_action_service_backward_compatibility(
assert len(calls) == 1
assert calls[0].data.get(ATTR_ENTITY_ID) == ["hello.world"]
assert calls[0].data.get("event") == "test_event"
@pytest.mark.parametrize(
("config", "message"),
[
(
{
"trigger": {"platform": "event", "event_type": "test_event"},
"triggers": {"platform": "event", "event_type": "test_event2"},
"actions": [],
},
"Cannot specify both 'trigger' and 'triggers'. Please use 'triggers' only.",
),
(
{
"trigger": {"platform": "event", "event_type": "test_event"},
"condition": {"condition": "template", "value_template": "{{ True }}"},
"conditions": {"condition": "template", "value_template": "{{ True }}"},
},
"Cannot specify both 'condition' and 'conditions'. Please use 'conditions' only.",
),
(
{
"trigger": {"platform": "event", "event_type": "test_event"},
"action": {"service": "test.automation", "entity_id": "hello.world"},
"actions": {"service": "test.automation", "entity_id": "hello.world"},
},
"Cannot specify both 'action' and 'actions'. Please use 'actions' only.",
),
],
)
async def test_invalid_configuration(
hass: HomeAssistant,
config: dict[str, Any],
message: str,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test for invalid automation configurations."""
assert await async_setup_component(
hass,
automation.DOMAIN,
{automation.DOMAIN: config},
)
await hass.async_block_till_done()
assert message in caplog.text

View File

@ -40,7 +40,7 @@ async def test_exclude_attributes(
{
automation.DOMAIN: {
"trigger": {"platform": "event", "event_type": "test_event"},
"action": {"action": "test.automation", "entity_id": "hello.world"},
"actions": {"action": "test.automation", "entity_id": "hello.world"},
}
},
)

View File

@ -6,7 +6,7 @@ import pathlib
import pytest
from homeassistant.components.blueprint import models
from homeassistant.components.blueprint import BLUEPRINT_SCHEMA, models
from homeassistant.components.blueprint.const import BLUEPRINT_FOLDER
from homeassistant.util import yaml
@ -26,4 +26,4 @@ def test_default_blueprints(domain: str) -> None:
LOGGER.info("Processing %s", fil)
assert fil.name.endswith(".yaml")
data = yaml.load_yaml(fil)
models.Blueprint(data, expected_domain=domain)
models.Blueprint(data, expected_domain=domain, schema=BLUEPRINT_SCHEMA)

View File

@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, patch
import pytest
from homeassistant.components.blueprint import errors, models
from homeassistant.components.blueprint import BLUEPRINT_SCHEMA, errors, models
from homeassistant.core import HomeAssistant
from homeassistant.util.yaml import Input
@ -22,7 +22,8 @@ def blueprint_1() -> models.Blueprint:
"input": {"test-input": {"name": "Name", "description": "Description"}},
},
"example": Input("test-input"),
}
},
schema=BLUEPRINT_SCHEMA,
)
@ -57,26 +58,32 @@ def blueprint_2(request: pytest.FixtureRequest) -> models.Blueprint:
}
},
}
return models.Blueprint(blueprint)
return models.Blueprint(blueprint, schema=BLUEPRINT_SCHEMA)
@pytest.fixture
def domain_bps(hass: HomeAssistant) -> models.DomainBlueprints:
"""Domain blueprints fixture."""
return models.DomainBlueprints(
hass, "automation", logging.getLogger(__name__), None, AsyncMock()
hass,
"automation",
logging.getLogger(__name__),
None,
AsyncMock(),
BLUEPRINT_SCHEMA,
)
def test_blueprint_model_init() -> None:
"""Test constructor validation."""
with pytest.raises(errors.InvalidBlueprint):
models.Blueprint({})
models.Blueprint({}, schema=BLUEPRINT_SCHEMA)
with pytest.raises(errors.InvalidBlueprint):
models.Blueprint(
{"blueprint": {"name": "Hello", "domain": "automation"}},
expected_domain="not-automation",
schema=BLUEPRINT_SCHEMA,
)
with pytest.raises(errors.InvalidBlueprint):
@ -88,7 +95,8 @@ def test_blueprint_model_init() -> None:
"input": {"something": None},
},
"trigger": {"platform": Input("non-existing")},
}
},
schema=BLUEPRINT_SCHEMA,
)
@ -115,7 +123,8 @@ def test_blueprint_update_metadata() -> None:
"name": "Hello",
"domain": "automation",
},
}
},
schema=BLUEPRINT_SCHEMA,
)
bp.update_metadata(source_url="http://bla.com")
@ -131,7 +140,8 @@ def test_blueprint_validate() -> None:
"name": "Hello",
"domain": "automation",
},
}
},
schema=BLUEPRINT_SCHEMA,
).validate()
is None
)
@ -143,7 +153,8 @@ def test_blueprint_validate() -> None:
"domain": "automation",
"homeassistant": {"min_version": "100000.0.0"},
},
}
},
schema=BLUEPRINT_SCHEMA,
).validate() == ["Requires at least Home Assistant 100000.0.0"]

View File

@ -64,6 +64,17 @@ async def test_list_blueprints(
"name": "Call service based on event",
},
},
"test_event_service_legacy_schema.yaml": {
"metadata": {
"domain": "automation",
"input": {
"service_to_call": None,
"trigger_event": {"selector": {"text": {}}},
"a_number": {"selector": {"number": {"mode": "box", "step": 1.0}}},
},
"name": "Call service based on event",
},
},
"in_folder/in_folder_blueprint.yaml": {
"metadata": {
"domain": "automation",
@ -212,16 +223,16 @@ async def test_save_blueprint(
" input:\n trigger_event:\n selector:\n text: {}\n "
" service_to_call:\n a_number:\n selector:\n number:\n "
" mode: box\n step: 1.0\n source_url:"
" https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n"
" platform: event\n event_type: !input 'trigger_event'\naction:\n "
" https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntriggers:\n"
" platform: event\n event_type: !input 'trigger_event'\nactions:\n "
" service: !input 'service_to_call'\n entity_id: light.kitchen\n"
# c dumper will not quote the value after !input
"blueprint:\n name: Call service based on event\n domain: automation\n "
" input:\n trigger_event:\n selector:\n text: {}\n "
" service_to_call:\n a_number:\n selector:\n number:\n "
" mode: box\n step: 1.0\n source_url:"
" https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n"
" platform: event\n event_type: !input trigger_event\naction:\n service:"
" https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntriggers:\n"
" platform: event\n event_type: !input trigger_event\nactions:\n service:"
" !input service_to_call\n entity_id: light.kitchen\n"
)
# Make sure ita parsable and does not raise
@ -483,11 +494,11 @@ async def test_substituting_blueprint_inputs(
assert msg["success"]
assert msg["result"]["substituted_config"] == {
"action": {
"actions": {
"entity_id": "light.kitchen",
"service": "test.automation",
},
"trigger": {
"triggers": {
"event_type": "test_event",
"platform": "event",
},

View File

@ -78,7 +78,7 @@ async def test_update_automation_config(
resp = await client.post(
"/api/config/automation/config/moon",
data=json.dumps({"trigger": [], "action": [], "condition": []}),
data=json.dumps({"triggers": [], "actions": [], "conditions": []}),
)
await hass.async_block_till_done()
assert sorted(hass.states.async_entity_ids("automation")) == [
@ -91,8 +91,13 @@ async def test_update_automation_config(
assert result == {"result": "ok"}
new_data = hass_config_store["automations.yaml"]
assert list(new_data[1]) == ["id", "trigger", "condition", "action"]
assert new_data[1] == {"id": "moon", "trigger": [], "condition": [], "action": []}
assert list(new_data[1]) == ["id", "triggers", "conditions", "actions"]
assert new_data[1] == {
"id": "moon",
"triggers": [],
"conditions": [],
"actions": [],
}
@pytest.mark.parametrize("automation_config", [{}])
@ -101,7 +106,7 @@ async def test_update_automation_config(
[
(
{"action": []},
"required key not provided @ data['trigger']",
"required key not provided @ data['triggers']",
),
(
{
@ -254,7 +259,7 @@ async def test_update_remove_key_automation_config(
resp = await client.post(
"/api/config/automation/config/moon",
data=json.dumps({"trigger": [], "action": [], "condition": []}),
data=json.dumps({"triggers": [], "actions": [], "conditions": []}),
)
await hass.async_block_till_done()
assert sorted(hass.states.async_entity_ids("automation")) == [
@ -267,8 +272,13 @@ async def test_update_remove_key_automation_config(
assert result == {"result": "ok"}
new_data = hass_config_store["automations.yaml"]
assert list(new_data[1]) == ["id", "trigger", "condition", "action"]
assert new_data[1] == {"id": "moon", "trigger": [], "condition": [], "action": []}
assert list(new_data[1]) == ["id", "triggers", "conditions", "actions"]
assert new_data[1] == {
"id": "moon",
"triggers": [],
"conditions": [],
"actions": [],
}
@pytest.mark.parametrize("automation_config", [{}])
@ -297,7 +307,7 @@ async def test_bad_formatted_automations(
resp = await client.post(
"/api/config/automation/config/moon",
data=json.dumps({"trigger": [], "action": [], "condition": []}),
data=json.dumps({"triggers": [], "actions": [], "conditions": []}),
)
await hass.async_block_till_done()
assert sorted(hass.states.async_entity_ids("automation")) == [
@ -312,7 +322,12 @@ async def test_bad_formatted_automations(
# Verify ID added
new_data = hass_config_store["automations.yaml"]
assert "id" in new_data[0]
assert new_data[1] == {"id": "moon", "trigger": [], "condition": [], "action": []}
assert new_data[1] == {
"id": "moon",
"triggers": [],
"conditions": [],
"actions": [],
}
@pytest.mark.parametrize(

View File

@ -1307,7 +1307,7 @@ async def test_automation_with_bad_action(
},
)
assert expected_error.format(path="['action'][0]") in caplog.text
assert expected_error.format(path="['actions'][0]") in caplog.text
@patch("homeassistant.helpers.device_registry.DeviceEntry", MockDeviceEntry)
@ -1341,7 +1341,7 @@ async def test_automation_with_bad_condition_action(
},
)
assert expected_error.format(path="['action'][0]") in caplog.text
assert expected_error.format(path="['actions'][0]") in caplog.text
@patch("homeassistant.helpers.device_registry.DeviceEntry", MockDeviceEntry)
@ -1375,7 +1375,7 @@ async def test_automation_with_bad_condition(
},
)
assert expected_error.format(path="['condition'][0]") in caplog.text
assert expected_error.format(path="['conditions'][0]") in caplog.text
async def test_automation_with_sub_condition(
@ -1541,7 +1541,7 @@ async def test_automation_with_bad_sub_condition(
},
)
path = "['condition'][0]['conditions'][0]"
path = "['conditions'][0]['conditions'][0]"
assert expected_error.format(path=path) in caplog.text

View File

@ -9,7 +9,11 @@ from unittest.mock import patch
import pytest
from homeassistant.components import script
from homeassistant.components.blueprint import Blueprint, DomainBlueprints
from homeassistant.components.blueprint import (
BLUEPRINT_SCHEMA,
Blueprint,
DomainBlueprints,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import Context, HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, template
@ -33,7 +37,10 @@ def patch_blueprint(blueprint_path: str, data_path: str) -> Iterator[None]:
return orig_load(self, path)
return Blueprint(
yaml.load_yaml(data_path), expected_domain=self.domain, path=path
yaml.load_yaml(data_path),
expected_domain=self.domain,
path=path,
schema=BLUEPRINT_SCHEMA,
)
with patch(

View File

@ -250,7 +250,7 @@ async def test_search(
{
"id": "unique_id",
"alias": "blueprint_automation_1",
"trigger": {"platform": "template", "value_template": "true"},
"triggers": {"platform": "template", "value_template": "true"},
"use_blueprint": {
"path": "test_event_service.yaml",
"input": {
@ -262,7 +262,7 @@ async def test_search(
},
{
"alias": "blueprint_automation_2",
"trigger": {"platform": "template", "value_template": "true"},
"triggers": {"platform": "template", "value_template": "true"},
"use_blueprint": {
"path": "test_event_service.yaml",
"input": {

View File

@ -47,7 +47,7 @@ async def _setup_automation_or_script(
) -> None:
"""Set up automations or scripts from automation config."""
if domain == "script":
configs = {config["id"]: {"sequence": config["action"]} for config in configs}
configs = {config["id"]: {"sequence": config["actions"]} for config in configs}
if script_config:
if domain == "automation":
@ -85,7 +85,7 @@ async def _run_automation_or_script(
def _assert_raw_config(domain, config, trace):
if domain == "script":
config = {"sequence": config["action"]}
config = {"sequence": config["actions"]}
assert trace["config"] == config
@ -152,20 +152,20 @@ async def test_get_trace(
sun_config = {
"id": "sun",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": {"service": "test.automation"},
"triggers": {"platform": "event", "event_type": "test_event"},
"actions": {"service": "test.automation"},
}
moon_config = {
"id": "moon",
"trigger": [
"triggers": [
{"platform": "event", "event_type": "test_event2"},
{"platform": "event", "event_type": "test_event3"},
],
"condition": {
"conditions": {
"condition": "template",
"value_template": "{{ trigger.event.event_type=='test_event2' }}",
},
"action": {"event": "another_event"},
"actions": {"event": "another_event"},
}
sun_action = {
@ -551,13 +551,13 @@ async def test_trace_overflow(
sun_config = {
"id": "sun",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": {"event": "some_event"},
"triggers": {"platform": "event", "event_type": "test_event"},
"actions": {"event": "some_event"},
}
moon_config = {
"id": "moon",
"trigger": {"platform": "event", "event_type": "test_event2"},
"action": {"event": "another_event"},
"triggers": {"platform": "event", "event_type": "test_event2"},
"actions": {"event": "another_event"},
}
await _setup_automation_or_script(
hass, domain, [sun_config, moon_config], stored_traces=stored_traces
@ -632,13 +632,13 @@ async def test_restore_traces_overflow(
hass_storage["trace.saved_traces"] = saved_traces
sun_config = {
"id": "sun",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": {"event": "some_event"},
"triggers": {"platform": "event", "event_type": "test_event"},
"actions": {"event": "some_event"},
}
moon_config = {
"id": "moon",
"trigger": {"platform": "event", "event_type": "test_event2"},
"action": {"event": "another_event"},
"triggers": {"platform": "event", "event_type": "test_event2"},
"actions": {"event": "another_event"},
}
await _setup_automation_or_script(hass, domain, [sun_config, moon_config])
await hass.async_start()
@ -713,13 +713,13 @@ async def test_restore_traces_late_overflow(
hass_storage["trace.saved_traces"] = saved_traces
sun_config = {
"id": "sun",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": {"event": "some_event"},
"triggers": {"platform": "event", "event_type": "test_event"},
"actions": {"event": "some_event"},
}
moon_config = {
"id": "moon",
"trigger": {"platform": "event", "event_type": "test_event2"},
"action": {"event": "another_event"},
"triggers": {"platform": "event", "event_type": "test_event2"},
"actions": {"event": "another_event"},
}
await _setup_automation_or_script(hass, domain, [sun_config, moon_config])
await hass.async_start()
@ -765,8 +765,8 @@ async def test_trace_no_traces(
sun_config = {
"id": "sun",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": {"event": "some_event"},
"triggers": {"platform": "event", "event_type": "test_event"},
"actions": {"event": "some_event"},
}
await _setup_automation_or_script(hass, domain, [sun_config], stored_traces=0)
@ -832,20 +832,20 @@ async def test_list_traces(
sun_config = {
"id": "sun",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": {"service": "test.automation"},
"triggers": {"platform": "event", "event_type": "test_event"},
"actions": {"service": "test.automation"},
}
moon_config = {
"id": "moon",
"trigger": [
"triggers": [
{"platform": "event", "event_type": "test_event2"},
{"platform": "event", "event_type": "test_event3"},
],
"condition": {
"conditions": {
"condition": "template",
"value_template": "{{ trigger.event.event_type=='test_event2' }}",
},
"action": {"event": "another_event"},
"actions": {"event": "another_event"},
}
await _setup_automation_or_script(hass, domain, [sun_config, moon_config])
@ -965,8 +965,8 @@ async def test_nested_traces(
sun_config = {
"id": "sun",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": {"service": "script.moon"},
"triggers": {"platform": "event", "event_type": "test_event"},
"actions": {"service": "script.moon"},
}
moon_config = {"moon": {"sequence": {"event": "another_event"}}}
await _setup_automation_or_script(hass, domain, [sun_config], moon_config)
@ -1036,8 +1036,8 @@ async def test_breakpoints(
sun_config = {
"id": "sun",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": [
"triggers": {"platform": "event", "event_type": "test_event"},
"actions": [
{"event": "event0"},
{"event": "event1"},
{"event": "event2"},
@ -1206,8 +1206,8 @@ async def test_breakpoints_2(
sun_config = {
"id": "sun",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": [
"triggers": {"platform": "event", "event_type": "test_event"},
"actions": [
{"event": "event0"},
{"event": "event1"},
{"event": "event2"},
@ -1311,8 +1311,8 @@ async def test_breakpoints_3(
sun_config = {
"id": "sun",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": [
"triggers": {"platform": "event", "event_type": "test_event"},
"actions": [
{"event": "event0"},
{"event": "event1"},
{"event": "event2"},

View File

@ -2566,18 +2566,18 @@ async def test_integration_setup_info(
@pytest.mark.parametrize(
("key", "config"),
[
("trigger", {"platform": "event", "event_type": "hello"}),
("trigger", [{"platform": "event", "event_type": "hello"}]),
("triggers", {"platform": "event", "event_type": "hello"}),
("triggers", [{"platform": "event", "event_type": "hello"}]),
(
"condition",
"conditions",
{"condition": "state", "entity_id": "hello.world", "state": "paulus"},
),
(
"condition",
"conditions",
[{"condition": "state", "entity_id": "hello.world", "state": "paulus"}],
),
("action", {"service": "domain_test.test_service"}),
("action", [{"service": "domain_test.test_service"}]),
("actions", {"service": "domain_test.test_service"}),
("actions", [{"service": "domain_test.test_service"}]),
],
)
async def test_validate_config_works(
@ -2599,13 +2599,13 @@ async def test_validate_config_works(
[
# Raises vol.Invalid
(
"trigger",
"triggers",
{"platform": "non_existing", "event_type": "hello"},
"Invalid platform 'non_existing' specified",
),
# Raises vol.Invalid
(
"condition",
"conditions",
{
"condition": "non_existing",
"entity_id": "hello.world",
@ -2619,7 +2619,7 @@ async def test_validate_config_works(
),
# Raises HomeAssistantError
(
"condition",
"conditions",
{
"above": 50,
"condition": "device",
@ -2632,7 +2632,7 @@ async def test_validate_config_works(
),
# Raises vol.Invalid
(
"action",
"actions",
{"non_existing": "domain_test.test_service"},
"Unable to determine action @ data[0]",
),

View File

@ -10,9 +10,9 @@ blueprint:
selector:
number:
mode: "box"
trigger:
triggers:
platform: event
event_type: !input trigger_event
action:
actions:
service: !input service_to_call
entity_id: light.kitchen

View File

@ -0,0 +1,18 @@
blueprint:
name: "Call service based on event"
domain: automation
input:
trigger_event:
selector:
text:
service_to_call:
a_number:
selector:
number:
mode: "box"
trigger:
platform: event
event_type: !input trigger_event
action:
service: !input service_to_call
entity_id: light.kitchen