Use !input instead of !placeholder (#43820)
* Use !input instead of !placeholder * Update input name * Lint * Move tests around
This commit is contained in:
parent
7d23ff6511
commit
1c9c99571e
23 changed files with 148 additions and 139 deletions
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 }}"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
|
@ -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),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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]
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
2
tests/fixtures/blueprint/community_post.json
vendored
2
tests/fixtures/blueprint/community_post.json
vendored
|
@ -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",
|
||||||
|
|
2
tests/fixtures/blueprint/github_gist.json
vendored
2
tests/fixtures/blueprint/github_gist.json
vendored
|
@ -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,
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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]}
|
|
||||||
)
|
|
|
@ -4,5 +4,5 @@ blueprint:
|
||||||
input:
|
input:
|
||||||
trigger:
|
trigger:
|
||||||
action:
|
action:
|
||||||
trigger: !placeholder trigger
|
trigger: !input trigger
|
||||||
action: !placeholder action
|
action: !input action
|
||||||
|
|
|
@ -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
|
||||||
|
|
1
tests/util/yaml/__init__.py
Normal file
1
tests/util/yaml/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""Tests for YAML util."""
|
|
@ -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
|
34
tests/util/yaml/test_input.py
Normal file
34
tests/util/yaml/test_input.py
Normal 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]}
|
||||||
|
)
|
Loading…
Add table
Add a link
Reference in a new issue