diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 3dc26ed2a94..e01a97425e1 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -26,7 +26,7 @@ "files.trimTrailingWhitespace": true, "terminal.integrated.shell.linux": "/bin/bash", "yaml.customTags": [ - "!placeholder scalar", + "!input scalar", "!secret scalar", "!include_dir_named scalar", "!include_dir_list scalar", diff --git a/homeassistant/components/automation/blueprints/motion_light.yaml b/homeassistant/components/automation/blueprints/motion_light.yaml index 6cf368f9c0d..cd6e5c7b281 100644 --- a/homeassistant/components/automation/blueprints/motion_light.yaml +++ b/homeassistant/components/automation/blueprints/motion_light.yaml @@ -31,18 +31,18 @@ max_exceeded: silent trigger: platform: state - entity_id: !placeholder motion_entity + entity_id: !input motion_entity from: "off" to: "on" action: - service: homeassistant.turn_on - target: !placeholder light_target + target: !input light_target - wait_for_trigger: platform: state - entity_id: !placeholder motion_entity + entity_id: !input motion_entity from: "on" to: "off" - - delay: !placeholder no_motion_wait + - delay: !input no_motion_wait - service: homeassistant.turn_off - target: !placeholder light_target + target: !input light_target diff --git a/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml b/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml index 7e7dba8bea8..178c3222c0a 100644 --- a/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml +++ b/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml @@ -18,10 +18,10 @@ blueprint: trigger: platform: state - entity_id: !placeholder person_entity + entity_id: !input person_entity variables: - zone_entity: !placeholder zone_entity + zone_entity: !input zone_entity zone_state: "{{ states[zone_entity].name }}" condition: @@ -29,6 +29,6 @@ condition: value_template: "{{ trigger.from_state.state == zone_state and trigger.to_state.state != zone_state }}" action: - - service: !placeholder notify_service + - service: !input notify_service data: message: "{{ trigger.to_state.name }} has left {{ zone_state }}" diff --git a/homeassistant/components/blueprint/__init__.py b/homeassistant/components/blueprint/__init__.py index 12a6782065b..9e8b1260eff 100644 --- a/homeassistant/components/blueprint/__init__.py +++ b/homeassistant/components/blueprint/__init__.py @@ -7,7 +7,7 @@ from .errors import ( # noqa FailedToLoad, InvalidBlueprint, InvalidBlueprintInputs, - MissingPlaceholder, + MissingInput, ) from .models import Blueprint, BlueprintInputs, DomainBlueprints # noqa from .schemas import is_blueprint_instance_config # noqa diff --git a/homeassistant/components/blueprint/errors.py b/homeassistant/components/blueprint/errors.py index 4a12fde1c26..b422b2dcbe3 100644 --- a/homeassistant/components/blueprint/errors.py +++ b/homeassistant/components/blueprint/errors.py @@ -66,17 +66,17 @@ class InvalidBlueprintInputs(BlueprintException): ) -class MissingPlaceholder(BlueprintWithNameException): - """When we miss a placeholder.""" +class MissingInput(BlueprintWithNameException): + """When we miss an input.""" def __init__( - self, domain: str, blueprint_name: str, placeholder_names: Iterable[str] + self, domain: str, blueprint_name: str, input_names: Iterable[str] ) -> None: """Initialize blueprint exception.""" super().__init__( domain, blueprint_name, - f"Missing placeholder {', '.join(sorted(placeholder_names))}", + f"Missing input {', '.join(sorted(input_names))}", ) diff --git a/homeassistant/components/blueprint/models.py b/homeassistant/components/blueprint/models.py index 73417722dcc..32fc30b60b9 100644 --- a/homeassistant/components/blueprint/models.py +++ b/homeassistant/components/blueprint/models.py @@ -19,7 +19,6 @@ from homeassistant.const import ( ) from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import placeholder from homeassistant.util import yaml from .const import ( @@ -38,7 +37,7 @@ from .errors import ( FileAlreadyExists, InvalidBlueprint, InvalidBlueprintInputs, - MissingPlaceholder, + MissingInput, ) from .schemas import BLUEPRINT_INSTANCE_FIELDS, BLUEPRINT_SCHEMA @@ -59,8 +58,6 @@ class Blueprint: except vol.Invalid as err: raise InvalidBlueprint(expected_domain, path, data, err) from err - self.placeholders = placeholder.extract_placeholders(data) - # In future, we will treat this as "incorrect" and allow to recover from this data_domain = data[CONF_BLUEPRINT][CONF_DOMAIN] if expected_domain is not None and data_domain != expected_domain: @@ -73,7 +70,7 @@ class Blueprint: self.domain = data_domain - missing = self.placeholders - set(data[CONF_BLUEPRINT][CONF_INPUT]) + missing = yaml.extract_inputs(data) - set(data[CONF_BLUEPRINT][CONF_INPUT]) if missing: raise InvalidBlueprint( @@ -143,7 +140,7 @@ class BlueprintInputs: @property def inputs_with_default(self): """Return the inputs and fallback to defaults.""" - no_input = self.blueprint.placeholders - set(self.inputs) + no_input = set(self.blueprint.inputs) - set(self.inputs) inputs_with_default = dict(self.inputs) @@ -156,12 +153,10 @@ class BlueprintInputs: def validate(self) -> None: """Validate the inputs.""" - missing = self.blueprint.placeholders - set(self.inputs_with_default) + missing = set(self.blueprint.inputs) - set(self.inputs_with_default) if missing: - raise MissingPlaceholder( - self.blueprint.domain, self.blueprint.name, missing - ) + raise MissingInput(self.blueprint.domain, self.blueprint.name, missing) # In future we can see if entities are correct domain, areas exist etc # using the new selector helper. @@ -169,9 +164,7 @@ class BlueprintInputs: @callback def async_substitute(self) -> dict: """Get the blueprint value with the inputs substituted.""" - processed = placeholder.substitute( - self.blueprint.data, self.inputs_with_default - ) + processed = yaml.substitute(self.blueprint.data, self.inputs_with_default) combined = {**processed, **self.config_with_inputs} # From config_with_inputs combined.pop(CONF_USE_BLUEPRINT) diff --git a/homeassistant/util/yaml/__init__.py b/homeassistant/util/yaml/__init__.py index bb6eb122de5..ac4ac2f9a16 100644 --- a/homeassistant/util/yaml/__init__.py +++ b/homeassistant/util/yaml/__init__.py @@ -1,17 +1,21 @@ """YAML utility functions.""" from .const import _SECRET_NAMESPACE, SECRET_YAML from .dumper import dump, save_yaml +from .input import UndefinedSubstitution, extract_inputs, substitute from .loader import clear_secret_cache, load_yaml, parse_yaml, secret_yaml -from .objects import Placeholder +from .objects import Input __all__ = [ "SECRET_YAML", "_SECRET_NAMESPACE", - "Placeholder", + "Input", "dump", "save_yaml", "clear_secret_cache", "load_yaml", "secret_yaml", "parse_yaml", + "UndefinedSubstitution", + "extract_inputs", + "substitute", ] diff --git a/homeassistant/util/yaml/dumper.py b/homeassistant/util/yaml/dumper.py index 6834323ed72..8e9cb382b6c 100644 --- a/homeassistant/util/yaml/dumper.py +++ b/homeassistant/util/yaml/dumper.py @@ -3,7 +3,7 @@ from collections import OrderedDict import yaml -from .objects import NodeListClass, Placeholder +from .objects import Input, NodeListClass # mypy: allow-untyped-calls, no-warn-return-any @@ -62,6 +62,6 @@ yaml.SafeDumper.add_representer( ) yaml.SafeDumper.add_representer( - Placeholder, - lambda dumper, value: dumper.represent_scalar("!placeholder", value.name), + Input, + lambda dumper, value: dumper.represent_scalar("!input", value.name), ) diff --git a/homeassistant/helpers/placeholder.py b/homeassistant/util/yaml/input.py similarity index 57% rename from homeassistant/helpers/placeholder.py rename to homeassistant/util/yaml/input.py index 3da5eaba76f..6282509fae2 100644 --- a/homeassistant/helpers/placeholder.py +++ b/homeassistant/util/yaml/input.py @@ -1,45 +1,46 @@ -"""Placeholder helpers.""" +"""Deal with YAML input.""" + from typing import Any, Dict, Set -from homeassistant.util.yaml import Placeholder +from .objects import Input class UndefinedSubstitution(Exception): """Error raised when we find a substitution that is not defined.""" - def __init__(self, placeholder: str) -> None: + def __init__(self, input_name: str) -> None: """Initialize the undefined substitution exception.""" - super().__init__(f"No substitution found for placeholder {placeholder}") - self.placeholder = placeholder + super().__init__(f"No substitution found for input {input_name}") + self.input = input -def extract_placeholders(obj: Any) -> Set[str]: - """Extract placeholders from a structure.""" +def extract_inputs(obj: Any) -> Set[str]: + """Extract input from a structure.""" found: Set[str] = set() - _extract_placeholders(obj, found) + _extract_inputs(obj, found) return found -def _extract_placeholders(obj: Any, found: Set[str]) -> None: - """Extract placeholders from a structure.""" - if isinstance(obj, Placeholder): +def _extract_inputs(obj: Any, found: Set[str]) -> None: + """Extract input from a structure.""" + if isinstance(obj, Input): found.add(obj.name) return if isinstance(obj, list): for val in obj: - _extract_placeholders(val, found) + _extract_inputs(val, found) return if isinstance(obj, dict): for val in obj.values(): - _extract_placeholders(val, found) + _extract_inputs(val, found) return def substitute(obj: Any, substitutions: Dict[str, Any]) -> Any: """Substitute values.""" - if isinstance(obj, Placeholder): + if isinstance(obj, Input): if obj.name not in substitutions: raise UndefinedSubstitution(obj.name) return substitutions[obj.name] diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index c9e191db5de..18beb2d4b14 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -11,7 +11,7 @@ import yaml from homeassistant.exceptions import HomeAssistantError from .const import _SECRET_NAMESPACE, SECRET_YAML -from .objects import NodeListClass, NodeStrClass, Placeholder +from .objects import Input, NodeListClass, NodeStrClass try: import keyring @@ -331,4 +331,4 @@ yaml.SafeLoader.add_constructor("!include_dir_named", _include_dir_named_yaml) yaml.SafeLoader.add_constructor( "!include_dir_merge_named", _include_dir_merge_named_yaml ) -yaml.SafeLoader.add_constructor("!placeholder", Placeholder.from_node) +yaml.SafeLoader.add_constructor("!input", Input.from_node) diff --git a/homeassistant/util/yaml/objects.py b/homeassistant/util/yaml/objects.py index c3f4c0ff140..0e46820e0db 100644 --- a/homeassistant/util/yaml/objects.py +++ b/homeassistant/util/yaml/objects.py @@ -13,12 +13,12 @@ class NodeStrClass(str): @dataclass(frozen=True) -class Placeholder: - """A placeholder that should be substituted.""" +class Input: + """Input that should be substituted.""" name: str @classmethod - def from_node(cls, loader: yaml.Loader, node: yaml.nodes.Node) -> "Placeholder": + def from_node(cls, loader: yaml.Loader, node: yaml.nodes.Node) -> "Input": """Create a new placeholder from a node.""" return cls(node.value) diff --git a/tests/components/blueprint/test_importer.py b/tests/components/blueprint/test_importer.py index 9d6bb48bcaf..bb8903459c9 100644 --- a/tests/components/blueprint/test_importer.py +++ b/tests/components/blueprint/test_importer.py @@ -57,9 +57,9 @@ def test_extract_blueprint_from_community_topic(community_post): ) assert imported_blueprint is not None assert imported_blueprint.blueprint.domain == "automation" - assert imported_blueprint.blueprint.placeholders == { - "service_to_call", - "trigger_event", + assert imported_blueprint.blueprint.inputs == { + "service_to_call": None, + "trigger_event": None, } @@ -103,9 +103,9 @@ async def test_fetch_blueprint_from_community_url(hass, aioclient_mock, communit ) assert isinstance(imported_blueprint, importer.ImportedBlueprint) assert imported_blueprint.blueprint.domain == "automation" - assert imported_blueprint.blueprint.placeholders == { - "service_to_call", - "trigger_event", + assert imported_blueprint.blueprint.inputs == { + "service_to_call": None, + "trigger_event": None, } assert imported_blueprint.suggested_filename == "balloob/test-topic" assert ( @@ -133,9 +133,9 @@ async def test_fetch_blueprint_from_github_url(hass, aioclient_mock, url): imported_blueprint = await importer.fetch_blueprint_from_url(hass, url) assert isinstance(imported_blueprint, importer.ImportedBlueprint) assert imported_blueprint.blueprint.domain == "automation" - assert imported_blueprint.blueprint.placeholders == { - "service_to_call", - "trigger_event", + assert imported_blueprint.blueprint.inputs == { + "service_to_call": None, + "trigger_event": None, } assert imported_blueprint.suggested_filename == "balloob/motion_light" assert imported_blueprint.blueprint.metadata["source_url"] == url @@ -152,9 +152,14 @@ async def test_fetch_blueprint_from_github_gist_url(hass, aioclient_mock): imported_blueprint = await importer.fetch_blueprint_from_url(hass, url) assert isinstance(imported_blueprint, importer.ImportedBlueprint) assert imported_blueprint.blueprint.domain == "automation" - assert imported_blueprint.blueprint.placeholders == { - "motion_entity", - "light_entity", + assert imported_blueprint.blueprint.inputs == { + "motion_entity": { + "name": "Motion Sensor", + "selector": { + "entity": {"domain": "binary_sensor", "device_class": "motion"} + }, + }, + "light_entity": {"name": "Light", "selector": {"entity": {"domain": "light"}}}, } assert imported_blueprint.suggested_filename == "balloob/motion_light" assert imported_blueprint.blueprint.metadata["source_url"] == url diff --git a/tests/components/blueprint/test_models.py b/tests/components/blueprint/test_models.py index 3680889e56b..6e15bd952a4 100644 --- a/tests/components/blueprint/test_models.py +++ b/tests/components/blueprint/test_models.py @@ -4,7 +4,7 @@ import logging import pytest from homeassistant.components.blueprint import errors, models -from homeassistant.util.yaml import Placeholder +from homeassistant.util.yaml import Input from tests.async_mock import patch @@ -18,18 +18,16 @@ def blueprint_1(): "name": "Hello", "domain": "automation", "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", - "input": { - "test-placeholder": {"name": "Name", "description": "Description"} - }, + "input": {"test-input": {"name": "Name", "description": "Description"}}, }, - "example": Placeholder("test-placeholder"), + "example": Input("test-input"), } ) @pytest.fixture def blueprint_2(): - """Blueprint fixture with default placeholder.""" + """Blueprint fixture with default inputs.""" return models.Blueprint( { "blueprint": { @@ -37,12 +35,12 @@ def blueprint_2(): "domain": "automation", "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", "input": { - "test-placeholder": {"name": "Name", "description": "Description"}, - "test-placeholder-default": {"default": "test"}, + "test-input": {"name": "Name", "description": "Description"}, + "test-input-default": {"default": "test"}, }, }, - "example": Placeholder("test-placeholder"), - "example-default": Placeholder("test-placeholder-default"), + "example": Input("test-input"), + "example-default": Input("test-input-default"), } ) @@ -72,7 +70,7 @@ def test_blueprint_model_init(): "domain": "automation", "input": {"something": None}, }, - "trigger": {"platform": Placeholder("non-existing")}, + "trigger": {"platform": Input("non-existing")}, } ) @@ -83,11 +81,13 @@ def test_blueprint_properties(blueprint_1): "name": "Hello", "domain": "automation", "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", - "input": {"test-placeholder": {"name": "Name", "description": "Description"}}, + "input": {"test-input": {"name": "Name", "description": "Description"}}, } assert blueprint_1.domain == "automation" assert blueprint_1.name == "Hello" - assert blueprint_1.placeholders == {"test-placeholder"} + assert blueprint_1.inputs == { + "test-input": {"name": "Name", "description": "Description"} + } def test_blueprint_update_metadata(): @@ -140,13 +140,13 @@ def test_blueprint_inputs(blueprint_2): { "use_blueprint": { "path": "bla", - "input": {"test-placeholder": 1, "test-placeholder-default": 12}, + "input": {"test-input": 1, "test-input-default": 12}, }, "example-default": {"overridden": "via-config"}, }, ) inputs.validate() - assert inputs.inputs == {"test-placeholder": 1, "test-placeholder-default": 12} + assert inputs.inputs == {"test-input": 1, "test-input-default": 12} assert inputs.async_substitute() == { "example": 1, "example-default": {"overridden": "via-config"}, @@ -159,7 +159,7 @@ def test_blueprint_inputs_validation(blueprint_1): blueprint_1, {"use_blueprint": {"path": "bla", "input": {"non-existing-placeholder": 1}}}, ) - with pytest.raises(errors.MissingPlaceholder): + with pytest.raises(errors.MissingInput): inputs.validate() @@ -167,13 +167,13 @@ def test_blueprint_inputs_default(blueprint_2): """Test blueprint inputs.""" inputs = models.BlueprintInputs( blueprint_2, - {"use_blueprint": {"path": "bla", "input": {"test-placeholder": 1}}}, + {"use_blueprint": {"path": "bla", "input": {"test-input": 1}}}, ) inputs.validate() - assert inputs.inputs == {"test-placeholder": 1} + assert inputs.inputs == {"test-input": 1} assert inputs.inputs_with_default == { - "test-placeholder": 1, - "test-placeholder-default": "test", + "test-input": 1, + "test-input-default": "test", } assert inputs.async_substitute() == {"example": 1, "example-default": "test"} @@ -185,18 +185,18 @@ def test_blueprint_inputs_override_default(blueprint_2): { "use_blueprint": { "path": "bla", - "input": {"test-placeholder": 1, "test-placeholder-default": "custom"}, + "input": {"test-input": 1, "test-input-default": "custom"}, } }, ) inputs.validate() assert inputs.inputs == { - "test-placeholder": 1, - "test-placeholder-default": "custom", + "test-input": 1, + "test-input-default": "custom", } assert inputs.inputs_with_default == { - "test-placeholder": 1, - "test-placeholder-default": "custom", + "test-input": 1, + "test-input-default": "custom", } assert inputs.async_substitute() == {"example": 1, "example-default": "custom"} @@ -238,7 +238,7 @@ async def test_domain_blueprints_inputs_from_config(domain_bps, blueprint_1): with pytest.raises(errors.InvalidBlueprintInputs): await domain_bps.async_inputs_from_config({"not-referencing": "use_blueprint"}) - with pytest.raises(errors.MissingPlaceholder), patch.object( + with pytest.raises(errors.MissingInput), patch.object( domain_bps, "async_get_blueprint", return_value=blueprint_1 ): await domain_bps.async_inputs_from_config( @@ -247,10 +247,10 @@ async def test_domain_blueprints_inputs_from_config(domain_bps, blueprint_1): with patch.object(domain_bps, "async_get_blueprint", return_value=blueprint_1): inputs = await domain_bps.async_inputs_from_config( - {"use_blueprint": {"path": "bla.yaml", "input": {"test-placeholder": None}}} + {"use_blueprint": {"path": "bla.yaml", "input": {"test-input": None}}} ) assert inputs.blueprint is blueprint_1 - assert inputs.inputs == {"test-placeholder": None} + assert inputs.inputs == {"test-input": None} async def test_domain_blueprints_add_blueprint(domain_bps, blueprint_1): diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index d79463ac3a6..c4f39127d93 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -124,7 +124,7 @@ async def test_save_blueprint(hass, aioclient_mock, hass_ws_client): assert msg["success"] assert write_mock.mock_calls assert write_mock.call_args[0] == ( - "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n service_to_call:\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !placeholder 'trigger_event'\naction:\n service: !placeholder 'service_to_call'\n", + "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n service_to_call:\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: !input 'service_to_call'\n", ) diff --git a/tests/fixtures/blueprint/community_post.json b/tests/fixtures/blueprint/community_post.json index 28684ec65f7..5b9a3dcb9c7 100644 --- a/tests/fixtures/blueprint/community_post.json +++ b/tests/fixtures/blueprint/community_post.json @@ -7,7 +7,7 @@ "username": "balloob", "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png", "created_at": "2020-10-16T12:20:12.688Z", - "cooked": "\u003cp\u003ehere a test topic.\u003cbr\u003e\nhere a test topic.\u003cbr\u003e\nhere a test topic.\u003cbr\u003e\nhere a test topic.\u003c/p\u003e\n\u003ch1\u003eBlock without syntax\u003c/h1\u003e\n\u003cpre\u003e\u003ccode class=\"lang-auto\"\u003eblueprint:\n domain: automation\n name: Example Blueprint from post\n input:\n trigger_event:\n service_to_call:\ntrigger:\n platform: event\n event_type: !placeholder trigger_event\naction:\n service: !placeholder service_to_call\n\u003c/code\u003e\u003c/pre\u003e", + "cooked": "\u003cp\u003ehere a test topic.\u003cbr\u003e\nhere a test topic.\u003cbr\u003e\nhere a test topic.\u003cbr\u003e\nhere a test topic.\u003c/p\u003e\n\u003ch1\u003eBlock without syntax\u003c/h1\u003e\n\u003cpre\u003e\u003ccode class=\"lang-auto\"\u003eblueprint:\n domain: automation\n name: Example Blueprint from post\n input:\n trigger_event:\n service_to_call:\ntrigger:\n platform: event\n event_type: !input trigger_event\naction:\n service: !input service_to_call\n\u003c/code\u003e\u003c/pre\u003e", "post_number": 1, "post_type": 1, "updated_at": "2020-10-20T08:24:14.189Z", diff --git a/tests/fixtures/blueprint/github_gist.json b/tests/fixtures/blueprint/github_gist.json index f25c7ca0238..208e8b54a71 100644 --- a/tests/fixtures/blueprint/github_gist.json +++ b/tests/fixtures/blueprint/github_gist.json @@ -15,7 +15,7 @@ "raw_url": "https://gist.githubusercontent.com/balloob/e717ce85dd0d2f1bdcdfc884ea25a344/raw/d3cede19ffed75443934325177cd78d26b1700ad/motion_light.yaml", "size": 803, "truncated": false, - "content": "blueprint:\n name: Motion-activated Light\n domain: automation\n input:\n motion_entity:\n name: Motion Sensor\n selector:\n entity:\n domain: binary_sensor\n device_class: motion\n light_entity:\n name: Light\n selector:\n entity:\n domain: light\n\n# If motion is detected within the 120s delay,\n# we restart the script.\nmode: restart\nmax_exceeded: silent\n\ntrigger:\n platform: state\n entity_id: !placeholder motion_entity\n from: \"off\"\n to: \"on\"\n\naction:\n - service: homeassistant.turn_on\n entity_id: !placeholder light_entity\n - wait_for_trigger:\n platform: state\n entity_id: !placeholder motion_entity\n from: \"on\"\n to: \"off\"\n - delay: 120\n - service: homeassistant.turn_off\n entity_id: !placeholder light_entity\n" + "content": "blueprint:\n name: Motion-activated Light\n domain: automation\n input:\n motion_entity:\n name: Motion Sensor\n selector:\n entity:\n domain: binary_sensor\n device_class: motion\n light_entity:\n name: Light\n selector:\n entity:\n domain: light\n\n# If motion is detected within the 120s delay,\n# we restart the script.\nmode: restart\nmax_exceeded: silent\n\ntrigger:\n platform: state\n entity_id: !input motion_entity\n from: \"off\"\n to: \"on\"\n\naction:\n - service: homeassistant.turn_on\n entity_id: !input light_entity\n - wait_for_trigger:\n platform: state\n entity_id: !input motion_entity\n from: \"on\"\n to: \"off\"\n - delay: 120\n - service: homeassistant.turn_off\n entity_id: !input light_entity\n" } }, "public": false, diff --git a/tests/helpers/test_check_config.py b/tests/helpers/test_check_config.py index 13ce52a840f..7959cf66403 100644 --- a/tests/helpers/test_check_config.py +++ b/tests/helpers/test_check_config.py @@ -159,9 +159,9 @@ blueprint: service_to_call: trigger: platform: event - event_type: !placeholder trigger_event + event_type: !input trigger_event action: - service: !placeholder service_to_call + service: !input service_to_call """, } with patch("os.path.isfile", return_value=True), patch_yaml_files(files): diff --git a/tests/helpers/test_placeholder.py b/tests/helpers/test_placeholder.py deleted file mode 100644 index d5978cd465a..00000000000 --- a/tests/helpers/test_placeholder.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Test placeholders.""" -import pytest - -from homeassistant.helpers import placeholder -from homeassistant.util.yaml import Placeholder - - -def test_extract_placeholders(): - """Test extracting placeholders from data.""" - assert placeholder.extract_placeholders(Placeholder("hello")) == {"hello"} - assert placeholder.extract_placeholders( - {"info": [1, Placeholder("hello"), 2, Placeholder("world")]} - ) == {"hello", "world"} - - -def test_substitute(): - """Test we can substitute.""" - assert placeholder.substitute(Placeholder("hello"), {"hello": 5}) == 5 - - with pytest.raises(placeholder.UndefinedSubstitution): - placeholder.substitute(Placeholder("hello"), {}) - - assert ( - placeholder.substitute( - {"info": [1, Placeholder("hello"), 2, Placeholder("world")]}, - {"hello": 5, "world": 10}, - ) - == {"info": [1, 5, 2, 10]} - ) diff --git a/tests/testing_config/blueprints/automation/in_folder/in_folder_blueprint.yaml b/tests/testing_config/blueprints/automation/in_folder/in_folder_blueprint.yaml index c869e30c41e..baaaf3df1ea 100644 --- a/tests/testing_config/blueprints/automation/in_folder/in_folder_blueprint.yaml +++ b/tests/testing_config/blueprints/automation/in_folder/in_folder_blueprint.yaml @@ -4,5 +4,5 @@ blueprint: input: trigger: action: -trigger: !placeholder trigger -action: !placeholder action +trigger: !input trigger +action: !input action diff --git a/tests/testing_config/blueprints/automation/test_event_service.yaml b/tests/testing_config/blueprints/automation/test_event_service.yaml index 0e9479cd8c3..eff8b52db16 100644 --- a/tests/testing_config/blueprints/automation/test_event_service.yaml +++ b/tests/testing_config/blueprints/automation/test_event_service.yaml @@ -6,6 +6,6 @@ blueprint: service_to_call: trigger: platform: event - event_type: !placeholder trigger_event + event_type: !input trigger_event action: - service: !placeholder service_to_call + service: !input service_to_call diff --git a/tests/util/yaml/__init__.py b/tests/util/yaml/__init__.py new file mode 100644 index 00000000000..5b5c1b8f15a --- /dev/null +++ b/tests/util/yaml/__init__.py @@ -0,0 +1 @@ +"""Tests for YAML util.""" diff --git a/tests/util/test_yaml.py b/tests/util/yaml/test_init.py similarity index 97% rename from tests/util/test_yaml.py rename to tests/util/yaml/test_init.py index 2e9d1b471ac..1c5b9bd9fd8 100644 --- a/tests/util/test_yaml.py +++ b/tests/util/yaml/test_init.py @@ -463,18 +463,18 @@ def test_duplicate_key(caplog): assert "contains duplicate key" in caplog.text -def test_placeholder_class(): - """Test placeholder class.""" - placeholder = yaml_loader.Placeholder("hello") - placeholder2 = yaml_loader.Placeholder("hello") +def test_input_class(): + """Test input class.""" + input = yaml_loader.Input("hello") + input2 = yaml_loader.Input("hello") - assert placeholder.name == "hello" - assert placeholder == placeholder2 + assert input.name == "hello" + assert input == input2 - assert len({placeholder, placeholder2}) == 1 + assert len({input, input2}) == 1 -def test_placeholder(): - """Test loading placeholders.""" - data = {"hello": yaml.Placeholder("test_name")} +def test_input(): + """Test loading inputs.""" + data = {"hello": yaml.Input("test_name")} assert yaml.parse_yaml(yaml.dump(data)) == data diff --git a/tests/util/yaml/test_input.py b/tests/util/yaml/test_input.py new file mode 100644 index 00000000000..1c13d1b3684 --- /dev/null +++ b/tests/util/yaml/test_input.py @@ -0,0 +1,34 @@ +"""Test inputs.""" +import pytest + +from homeassistant.util.yaml import ( + Input, + UndefinedSubstitution, + extract_inputs, + substitute, +) + + +def test_extract_inputs(): + """Test extracting inputs from data.""" + assert extract_inputs(Input("hello")) == {"hello"} + assert extract_inputs({"info": [1, Input("hello"), 2, Input("world")]}) == { + "hello", + "world", + } + + +def test_substitute(): + """Test we can substitute.""" + assert substitute(Input("hello"), {"hello": 5}) == 5 + + with pytest.raises(UndefinedSubstitution): + substitute(Input("hello"), {}) + + assert ( + substitute( + {"info": [1, Input("hello"), 2, Input("world")]}, + {"hello": 5, "world": 10}, + ) + == {"info": [1, 5, 2, 10]} + )