Use !input instead of !placeholder (#43820)

* Use !input instead of !placeholder

* Update input name

* Lint

* Move tests around
This commit is contained in:
Paulus Schoutsen 2020-12-01 18:21:36 +01:00 committed by GitHub
parent 7d23ff6511
commit 1c9c99571e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 148 additions and 139 deletions

View file

@ -26,7 +26,7 @@
"files.trimTrailingWhitespace": true, "files.trimTrailingWhitespace": true,
"terminal.integrated.shell.linux": "/bin/bash", "terminal.integrated.shell.linux": "/bin/bash",
"yaml.customTags": [ "yaml.customTags": [
"!placeholder scalar", "!input scalar",
"!secret scalar", "!secret scalar",
"!include_dir_named scalar", "!include_dir_named scalar",
"!include_dir_list scalar", "!include_dir_list scalar",

View file

@ -31,18 +31,18 @@ max_exceeded: silent
trigger: trigger:
platform: state platform: state
entity_id: !placeholder motion_entity entity_id: !input motion_entity
from: "off" from: "off"
to: "on" to: "on"
action: action:
- service: homeassistant.turn_on - service: homeassistant.turn_on
target: !placeholder light_target target: !input light_target
- wait_for_trigger: - wait_for_trigger:
platform: state platform: state
entity_id: !placeholder motion_entity entity_id: !input motion_entity
from: "on" from: "on"
to: "off" to: "off"
- delay: !placeholder no_motion_wait - delay: !input no_motion_wait
- service: homeassistant.turn_off - service: homeassistant.turn_off
target: !placeholder light_target target: !input light_target

View file

@ -18,10 +18,10 @@ blueprint:
trigger: trigger:
platform: state platform: state
entity_id: !placeholder person_entity entity_id: !input person_entity
variables: variables:
zone_entity: !placeholder zone_entity zone_entity: !input zone_entity
zone_state: "{{ states[zone_entity].name }}" zone_state: "{{ states[zone_entity].name }}"
condition: condition:
@ -29,6 +29,6 @@ condition:
value_template: "{{ trigger.from_state.state == zone_state and trigger.to_state.state != zone_state }}" value_template: "{{ trigger.from_state.state == zone_state and trigger.to_state.state != zone_state }}"
action: action:
- service: !placeholder notify_service - service: !input notify_service
data: data:
message: "{{ trigger.to_state.name }} has left {{ zone_state }}" message: "{{ trigger.to_state.name }} has left {{ zone_state }}"

View file

@ -7,7 +7,7 @@ from .errors import ( # noqa
FailedToLoad, FailedToLoad,
InvalidBlueprint, InvalidBlueprint,
InvalidBlueprintInputs, InvalidBlueprintInputs,
MissingPlaceholder, MissingInput,
) )
from .models import Blueprint, BlueprintInputs, DomainBlueprints # noqa from .models import Blueprint, BlueprintInputs, DomainBlueprints # noqa
from .schemas import is_blueprint_instance_config # noqa from .schemas import is_blueprint_instance_config # noqa

View file

@ -66,17 +66,17 @@ class InvalidBlueprintInputs(BlueprintException):
) )
class MissingPlaceholder(BlueprintWithNameException): class MissingInput(BlueprintWithNameException):
"""When we miss a placeholder.""" """When we miss an input."""
def __init__( def __init__(
self, domain: str, blueprint_name: str, placeholder_names: Iterable[str] self, domain: str, blueprint_name: str, input_names: Iterable[str]
) -> None: ) -> None:
"""Initialize blueprint exception.""" """Initialize blueprint exception."""
super().__init__( super().__init__(
domain, domain,
blueprint_name, blueprint_name,
f"Missing placeholder {', '.join(sorted(placeholder_names))}", f"Missing input {', '.join(sorted(input_names))}",
) )

View file

@ -19,7 +19,6 @@ from homeassistant.const import (
) )
from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant, callback from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import placeholder
from homeassistant.util import yaml from homeassistant.util import yaml
from .const import ( from .const import (
@ -38,7 +37,7 @@ from .errors import (
FileAlreadyExists, FileAlreadyExists,
InvalidBlueprint, InvalidBlueprint,
InvalidBlueprintInputs, InvalidBlueprintInputs,
MissingPlaceholder, MissingInput,
) )
from .schemas import BLUEPRINT_INSTANCE_FIELDS, BLUEPRINT_SCHEMA from .schemas import BLUEPRINT_INSTANCE_FIELDS, BLUEPRINT_SCHEMA
@ -59,8 +58,6 @@ class Blueprint:
except vol.Invalid as err: except vol.Invalid as err:
raise InvalidBlueprint(expected_domain, path, data, err) from 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 # In future, we will treat this as "incorrect" and allow to recover from this
data_domain = data[CONF_BLUEPRINT][CONF_DOMAIN] data_domain = data[CONF_BLUEPRINT][CONF_DOMAIN]
if expected_domain is not None and data_domain != expected_domain: if expected_domain is not None and data_domain != expected_domain:
@ -73,7 +70,7 @@ class Blueprint:
self.domain = data_domain 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: if missing:
raise InvalidBlueprint( raise InvalidBlueprint(
@ -143,7 +140,7 @@ class BlueprintInputs:
@property @property
def inputs_with_default(self): def inputs_with_default(self):
"""Return the inputs and fallback to defaults.""" """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) inputs_with_default = dict(self.inputs)
@ -156,12 +153,10 @@ class BlueprintInputs:
def validate(self) -> None: def validate(self) -> None:
"""Validate the inputs.""" """Validate the inputs."""
missing = self.blueprint.placeholders - set(self.inputs_with_default) missing = set(self.blueprint.inputs) - set(self.inputs_with_default)
if missing: if missing:
raise MissingPlaceholder( raise MissingInput(self.blueprint.domain, self.blueprint.name, missing)
self.blueprint.domain, self.blueprint.name, missing
)
# In future we can see if entities are correct domain, areas exist etc # In future we can see if entities are correct domain, areas exist etc
# using the new selector helper. # using the new selector helper.
@ -169,9 +164,7 @@ class BlueprintInputs:
@callback @callback
def async_substitute(self) -> dict: def async_substitute(self) -> dict:
"""Get the blueprint value with the inputs substituted.""" """Get the blueprint value with the inputs substituted."""
processed = placeholder.substitute( processed = yaml.substitute(self.blueprint.data, self.inputs_with_default)
self.blueprint.data, self.inputs_with_default
)
combined = {**processed, **self.config_with_inputs} combined = {**processed, **self.config_with_inputs}
# From config_with_inputs # From config_with_inputs
combined.pop(CONF_USE_BLUEPRINT) combined.pop(CONF_USE_BLUEPRINT)

View file

@ -1,17 +1,21 @@
"""YAML utility functions.""" """YAML utility functions."""
from .const import _SECRET_NAMESPACE, SECRET_YAML from .const import _SECRET_NAMESPACE, SECRET_YAML
from .dumper import dump, save_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 .loader import clear_secret_cache, load_yaml, parse_yaml, secret_yaml
from .objects import Placeholder from .objects import Input
__all__ = [ __all__ = [
"SECRET_YAML", "SECRET_YAML",
"_SECRET_NAMESPACE", "_SECRET_NAMESPACE",
"Placeholder", "Input",
"dump", "dump",
"save_yaml", "save_yaml",
"clear_secret_cache", "clear_secret_cache",
"load_yaml", "load_yaml",
"secret_yaml", "secret_yaml",
"parse_yaml", "parse_yaml",
"UndefinedSubstitution",
"extract_inputs",
"substitute",
] ]

View file

@ -3,7 +3,7 @@ from collections import OrderedDict
import yaml import yaml
from .objects import NodeListClass, Placeholder from .objects import Input, NodeListClass
# mypy: allow-untyped-calls, no-warn-return-any # mypy: allow-untyped-calls, no-warn-return-any
@ -62,6 +62,6 @@ yaml.SafeDumper.add_representer(
) )
yaml.SafeDumper.add_representer( yaml.SafeDumper.add_representer(
Placeholder, Input,
lambda dumper, value: dumper.represent_scalar("!placeholder", value.name), lambda dumper, value: dumper.represent_scalar("!input", value.name),
) )

View file

@ -1,45 +1,46 @@
"""Placeholder helpers.""" """Deal with YAML input."""
from typing import Any, Dict, Set from typing import Any, Dict, Set
from homeassistant.util.yaml import Placeholder from .objects import Input
class UndefinedSubstitution(Exception): class UndefinedSubstitution(Exception):
"""Error raised when we find a substitution that is not defined.""" """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.""" """Initialize the undefined substitution exception."""
super().__init__(f"No substitution found for placeholder {placeholder}") super().__init__(f"No substitution found for input {input_name}")
self.placeholder = placeholder self.input = input
def extract_placeholders(obj: Any) -> Set[str]: def extract_inputs(obj: Any) -> Set[str]:
"""Extract placeholders from a structure.""" """Extract input from a structure."""
found: Set[str] = set() found: Set[str] = set()
_extract_placeholders(obj, found) _extract_inputs(obj, found)
return found return found
def _extract_placeholders(obj: Any, found: Set[str]) -> None: def _extract_inputs(obj: Any, found: Set[str]) -> None:
"""Extract placeholders from a structure.""" """Extract input from a structure."""
if isinstance(obj, Placeholder): if isinstance(obj, Input):
found.add(obj.name) found.add(obj.name)
return return
if isinstance(obj, list): if isinstance(obj, list):
for val in obj: for val in obj:
_extract_placeholders(val, found) _extract_inputs(val, found)
return return
if isinstance(obj, dict): if isinstance(obj, dict):
for val in obj.values(): for val in obj.values():
_extract_placeholders(val, found) _extract_inputs(val, found)
return return
def substitute(obj: Any, substitutions: Dict[str, Any]) -> Any: def substitute(obj: Any, substitutions: Dict[str, Any]) -> Any:
"""Substitute values.""" """Substitute values."""
if isinstance(obj, Placeholder): if isinstance(obj, Input):
if obj.name not in substitutions: if obj.name not in substitutions:
raise UndefinedSubstitution(obj.name) raise UndefinedSubstitution(obj.name)
return substitutions[obj.name] return substitutions[obj.name]

View file

@ -11,7 +11,7 @@ import yaml
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from .const import _SECRET_NAMESPACE, SECRET_YAML from .const import _SECRET_NAMESPACE, SECRET_YAML
from .objects import NodeListClass, NodeStrClass, Placeholder from .objects import Input, NodeListClass, NodeStrClass
try: try:
import keyring import keyring
@ -331,4 +331,4 @@ yaml.SafeLoader.add_constructor("!include_dir_named", _include_dir_named_yaml)
yaml.SafeLoader.add_constructor( yaml.SafeLoader.add_constructor(
"!include_dir_merge_named", _include_dir_merge_named_yaml "!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)

View file

@ -13,12 +13,12 @@ class NodeStrClass(str):
@dataclass(frozen=True) @dataclass(frozen=True)
class Placeholder: class Input:
"""A placeholder that should be substituted.""" """Input that should be substituted."""
name: str name: str
@classmethod @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.""" """Create a new placeholder from a node."""
return cls(node.value) return cls(node.value)

View file

@ -57,9 +57,9 @@ def test_extract_blueprint_from_community_topic(community_post):
) )
assert imported_blueprint is not None assert imported_blueprint is not None
assert imported_blueprint.blueprint.domain == "automation" assert imported_blueprint.blueprint.domain == "automation"
assert imported_blueprint.blueprint.placeholders == { assert imported_blueprint.blueprint.inputs == {
"service_to_call", "service_to_call": None,
"trigger_event", "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 isinstance(imported_blueprint, importer.ImportedBlueprint)
assert imported_blueprint.blueprint.domain == "automation" assert imported_blueprint.blueprint.domain == "automation"
assert imported_blueprint.blueprint.placeholders == { assert imported_blueprint.blueprint.inputs == {
"service_to_call", "service_to_call": None,
"trigger_event", "trigger_event": None,
} }
assert imported_blueprint.suggested_filename == "balloob/test-topic" assert imported_blueprint.suggested_filename == "balloob/test-topic"
assert ( 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) imported_blueprint = await importer.fetch_blueprint_from_url(hass, url)
assert isinstance(imported_blueprint, importer.ImportedBlueprint) assert isinstance(imported_blueprint, importer.ImportedBlueprint)
assert imported_blueprint.blueprint.domain == "automation" assert imported_blueprint.blueprint.domain == "automation"
assert imported_blueprint.blueprint.placeholders == { assert imported_blueprint.blueprint.inputs == {
"service_to_call", "service_to_call": None,
"trigger_event", "trigger_event": None,
} }
assert imported_blueprint.suggested_filename == "balloob/motion_light" assert imported_blueprint.suggested_filename == "balloob/motion_light"
assert imported_blueprint.blueprint.metadata["source_url"] == url 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) imported_blueprint = await importer.fetch_blueprint_from_url(hass, url)
assert isinstance(imported_blueprint, importer.ImportedBlueprint) assert isinstance(imported_blueprint, importer.ImportedBlueprint)
assert imported_blueprint.blueprint.domain == "automation" assert imported_blueprint.blueprint.domain == "automation"
assert imported_blueprint.blueprint.placeholders == { assert imported_blueprint.blueprint.inputs == {
"motion_entity", "motion_entity": {
"light_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.suggested_filename == "balloob/motion_light"
assert imported_blueprint.blueprint.metadata["source_url"] == url assert imported_blueprint.blueprint.metadata["source_url"] == url

View file

@ -4,7 +4,7 @@ import logging
import pytest import pytest
from homeassistant.components.blueprint import errors, models 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 from tests.async_mock import patch
@ -18,18 +18,16 @@ def blueprint_1():
"name": "Hello", "name": "Hello",
"domain": "automation", "domain": "automation",
"source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml",
"input": { "input": {"test-input": {"name": "Name", "description": "Description"}},
"test-placeholder": {"name": "Name", "description": "Description"}
},
}, },
"example": Placeholder("test-placeholder"), "example": Input("test-input"),
} }
) )
@pytest.fixture @pytest.fixture
def blueprint_2(): def blueprint_2():
"""Blueprint fixture with default placeholder.""" """Blueprint fixture with default inputs."""
return models.Blueprint( return models.Blueprint(
{ {
"blueprint": { "blueprint": {
@ -37,12 +35,12 @@ def blueprint_2():
"domain": "automation", "domain": "automation",
"source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml",
"input": { "input": {
"test-placeholder": {"name": "Name", "description": "Description"}, "test-input": {"name": "Name", "description": "Description"},
"test-placeholder-default": {"default": "test"}, "test-input-default": {"default": "test"},
}, },
}, },
"example": Placeholder("test-placeholder"), "example": Input("test-input"),
"example-default": Placeholder("test-placeholder-default"), "example-default": Input("test-input-default"),
} }
) )
@ -72,7 +70,7 @@ def test_blueprint_model_init():
"domain": "automation", "domain": "automation",
"input": {"something": None}, "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", "name": "Hello",
"domain": "automation", "domain": "automation",
"source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", "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.domain == "automation"
assert blueprint_1.name == "Hello" 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(): def test_blueprint_update_metadata():
@ -140,13 +140,13 @@ def test_blueprint_inputs(blueprint_2):
{ {
"use_blueprint": { "use_blueprint": {
"path": "bla", "path": "bla",
"input": {"test-placeholder": 1, "test-placeholder-default": 12}, "input": {"test-input": 1, "test-input-default": 12},
}, },
"example-default": {"overridden": "via-config"}, "example-default": {"overridden": "via-config"},
}, },
) )
inputs.validate() 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() == { assert inputs.async_substitute() == {
"example": 1, "example": 1,
"example-default": {"overridden": "via-config"}, "example-default": {"overridden": "via-config"},
@ -159,7 +159,7 @@ def test_blueprint_inputs_validation(blueprint_1):
blueprint_1, blueprint_1,
{"use_blueprint": {"path": "bla", "input": {"non-existing-placeholder": 1}}}, {"use_blueprint": {"path": "bla", "input": {"non-existing-placeholder": 1}}},
) )
with pytest.raises(errors.MissingPlaceholder): with pytest.raises(errors.MissingInput):
inputs.validate() inputs.validate()
@ -167,13 +167,13 @@ def test_blueprint_inputs_default(blueprint_2):
"""Test blueprint inputs.""" """Test blueprint inputs."""
inputs = models.BlueprintInputs( inputs = models.BlueprintInputs(
blueprint_2, blueprint_2,
{"use_blueprint": {"path": "bla", "input": {"test-placeholder": 1}}}, {"use_blueprint": {"path": "bla", "input": {"test-input": 1}}},
) )
inputs.validate() inputs.validate()
assert inputs.inputs == {"test-placeholder": 1} assert inputs.inputs == {"test-input": 1}
assert inputs.inputs_with_default == { assert inputs.inputs_with_default == {
"test-placeholder": 1, "test-input": 1,
"test-placeholder-default": "test", "test-input-default": "test",
} }
assert inputs.async_substitute() == {"example": 1, "example-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": { "use_blueprint": {
"path": "bla", "path": "bla",
"input": {"test-placeholder": 1, "test-placeholder-default": "custom"}, "input": {"test-input": 1, "test-input-default": "custom"},
} }
}, },
) )
inputs.validate() inputs.validate()
assert inputs.inputs == { assert inputs.inputs == {
"test-placeholder": 1, "test-input": 1,
"test-placeholder-default": "custom", "test-input-default": "custom",
} }
assert inputs.inputs_with_default == { assert inputs.inputs_with_default == {
"test-placeholder": 1, "test-input": 1,
"test-placeholder-default": "custom", "test-input-default": "custom",
} }
assert inputs.async_substitute() == {"example": 1, "example-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): with pytest.raises(errors.InvalidBlueprintInputs):
await domain_bps.async_inputs_from_config({"not-referencing": "use_blueprint"}) 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 domain_bps, "async_get_blueprint", return_value=blueprint_1
): ):
await domain_bps.async_inputs_from_config( 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): with patch.object(domain_bps, "async_get_blueprint", return_value=blueprint_1):
inputs = await domain_bps.async_inputs_from_config( 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.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): async def test_domain_blueprints_add_blueprint(domain_bps, blueprint_1):

View file

@ -124,7 +124,7 @@ async def test_save_blueprint(hass, aioclient_mock, hass_ws_client):
assert msg["success"] assert msg["success"]
assert write_mock.mock_calls assert write_mock.mock_calls
assert write_mock.call_args[0] == ( 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",
) )

View file

@ -7,7 +7,7 @@
"username": "balloob", "username": "balloob",
"avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png", "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png",
"created_at": "2020-10-16T12:20:12.688Z", "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_number": 1,
"post_type": 1, "post_type": 1,
"updated_at": "2020-10-20T08:24:14.189Z", "updated_at": "2020-10-20T08:24:14.189Z",

View file

@ -15,7 +15,7 @@
"raw_url": "https://gist.githubusercontent.com/balloob/e717ce85dd0d2f1bdcdfc884ea25a344/raw/d3cede19ffed75443934325177cd78d26b1700ad/motion_light.yaml", "raw_url": "https://gist.githubusercontent.com/balloob/e717ce85dd0d2f1bdcdfc884ea25a344/raw/d3cede19ffed75443934325177cd78d26b1700ad/motion_light.yaml",
"size": 803, "size": 803,
"truncated": false, "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, "public": false,

View file

@ -159,9 +159,9 @@ blueprint:
service_to_call: service_to_call:
trigger: trigger:
platform: event platform: event
event_type: !placeholder trigger_event event_type: !input trigger_event
action: action:
service: !placeholder service_to_call service: !input service_to_call
""", """,
} }
with patch("os.path.isfile", return_value=True), patch_yaml_files(files): with patch("os.path.isfile", return_value=True), patch_yaml_files(files):

View file

@ -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]}
)

View file

@ -4,5 +4,5 @@ blueprint:
input: input:
trigger: trigger:
action: action:
trigger: !placeholder trigger trigger: !input trigger
action: !placeholder action action: !input action

View file

@ -6,6 +6,6 @@ blueprint:
service_to_call: service_to_call:
trigger: trigger:
platform: event platform: event
event_type: !placeholder trigger_event event_type: !input trigger_event
action: action:
service: !placeholder service_to_call service: !input service_to_call

View file

@ -0,0 +1 @@
"""Tests for YAML util."""

View file

@ -463,18 +463,18 @@ def test_duplicate_key(caplog):
assert "contains duplicate key" in caplog.text assert "contains duplicate key" in caplog.text
def test_placeholder_class(): def test_input_class():
"""Test placeholder class.""" """Test input class."""
placeholder = yaml_loader.Placeholder("hello") input = yaml_loader.Input("hello")
placeholder2 = yaml_loader.Placeholder("hello") input2 = yaml_loader.Input("hello")
assert placeholder.name == "hello" assert input.name == "hello"
assert placeholder == placeholder2 assert input == input2
assert len({placeholder, placeholder2}) == 1 assert len({input, input2}) == 1
def test_placeholder(): def test_input():
"""Test loading placeholders.""" """Test loading inputs."""
data = {"hello": yaml.Placeholder("test_name")} data = {"hello": yaml.Input("test_name")}
assert yaml.parse_yaml(yaml.dump(data)) == data assert yaml.parse_yaml(yaml.dump(data)) == data

View file

@ -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]}
)