diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 1db5125a8a6..a40df67e2ca 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -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( diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index cc4e9aba7fb..fe74865ca92 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -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, diff --git a/homeassistant/components/automation/const.py b/homeassistant/components/automation/const.py index e6be35494d7..c4ac636282e 100644 --- a/homeassistant/components/automation/const.py +++ b/homeassistant/components/automation/const.py @@ -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" diff --git a/homeassistant/components/automation/helpers.py b/homeassistant/components/automation/helpers.py index 6aefa2b150a..c529fbd504e 100644 --- a/homeassistant/components/automation/helpers.py +++ b/homeassistant/components/automation/helpers.py @@ -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, ) diff --git a/homeassistant/components/blueprint/__init__.py b/homeassistant/components/blueprint/__init__.py index 92d94708e0f..4c7b8e7f4c3 100644 --- a/homeassistant/components/blueprint/__init__.py +++ b/homeassistant/components/blueprint/__init__.py @@ -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) diff --git a/homeassistant/components/blueprint/importer.py b/homeassistant/components/blueprint/importer.py index c231a33991a..c10da532324 100644 --- a/homeassistant/components/blueprint/importer.py +++ b/homeassistant/components/blueprint/importer.py @@ -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\d+)(?:/(?P\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) diff --git a/homeassistant/components/blueprint/models.py b/homeassistant/components/blueprint/models.py index 02a215ca103..f32c3f04989 100644 --- a/homeassistant/components/blueprint/models.py +++ b/homeassistant/components/blueprint/models.py @@ -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]: diff --git a/homeassistant/components/blueprint/websocket_api.py b/homeassistant/components/blueprint/websocket_api.py index 9d3329d8195..3be925c7c8f 100644 --- a/homeassistant/components/blueprint/websocket_api.py +++ b/homeassistant/components/blueprint/websocket_api.py @@ -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: diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 519a40450ed..54af1df8c54 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -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] diff --git a/homeassistant/components/script/helpers.py b/homeassistant/components/script/helpers.py index b070a4d60ce..31aac506b35 100644 --- a/homeassistant/components/script/helpers.py +++ b/homeassistant/components/script/helpers.py @@ -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, ) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index c9347012183..cfa132b71eb 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -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 diff --git a/tests/components/automation/test_blueprint.py b/tests/components/automation/test_blueprint.py index 2c92d7a5242..1095c625fb2 100644 --- a/tests/components/automation/test_blueprint.py +++ b/tests/components/automation/test_blueprint.py @@ -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( diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index d8f04f10458..aaeb4f2e41e 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -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 diff --git a/tests/components/automation/test_recorder.py b/tests/components/automation/test_recorder.py index be354abe9d2..513fee566db 100644 --- a/tests/components/automation/test_recorder.py +++ b/tests/components/automation/test_recorder.py @@ -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"}, } }, ) diff --git a/tests/components/blueprint/test_default_blueprints.py b/tests/components/blueprint/test_default_blueprints.py index 9bd60a7cb6b..f69126a7f25 100644 --- a/tests/components/blueprint/test_default_blueprints.py +++ b/tests/components/blueprint/test_default_blueprints.py @@ -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) diff --git a/tests/components/blueprint/test_models.py b/tests/components/blueprint/test_models.py index 45e35474e4c..0ce8c1f397a 100644 --- a/tests/components/blueprint/test_models.py +++ b/tests/components/blueprint/test_models.py @@ -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"] diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index 13615803569..f8ff0fdd540 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -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", }, diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py index 89113070367..9cd2a25de3a 100644 --- a/tests/components/config/test_automation.py +++ b/tests/components/config/test_automation.py @@ -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( diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 750817f3c41..cb1abecd6ff 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -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 diff --git a/tests/components/script/test_blueprint.py b/tests/components/script/test_blueprint.py index 86567d2f16f..7f03a89c548 100644 --- a/tests/components/script/test_blueprint.py +++ b/tests/components/script/test_blueprint.py @@ -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( diff --git a/tests/components/search/test_init.py b/tests/components/search/test_init.py index 9b2b959e0dd..2c00c3bf6f2 100644 --- a/tests/components/search/test_init.py +++ b/tests/components/search/test_init.py @@ -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": { diff --git a/tests/components/trace/test_websocket_api.py b/tests/components/trace/test_websocket_api.py index 7b292ed39e3..43664c6e7ce 100644 --- a/tests/components/trace/test_websocket_api.py +++ b/tests/components/trace/test_websocket_api.py @@ -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"}, diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 54a87e033dc..9c41bb8ddd2 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -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]", ), diff --git a/tests/testing_config/blueprints/automation/test_event_service.yaml b/tests/testing_config/blueprints/automation/test_event_service.yaml index ba7462ed2e0..035278df258 100644 --- a/tests/testing_config/blueprints/automation/test_event_service.yaml +++ b/tests/testing_config/blueprints/automation/test_event_service.yaml @@ -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 diff --git a/tests/testing_config/blueprints/automation/test_event_service_legacy_schema.yaml b/tests/testing_config/blueprints/automation/test_event_service_legacy_schema.yaml new file mode 100644 index 00000000000..ba7462ed2e0 --- /dev/null +++ b/tests/testing_config/blueprints/automation/test_event_service_legacy_schema.yaml @@ -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