Adjust automation to plural triggers/conditions/actions keys (#123823)
* Adjust automation to plural triggers/conditions/actions keys * Fix some tests * Adjust websocket tests * Fix search tests * Convert blueprint and blueprint inputs to modern schema * Pass schema when creating Blueprint object * Update tests * Adjust websocket api --------- Co-authored-by: Joostlek <joostlek@outlook.com> Co-authored-by: Erik <erik@montnemery.com>pull/126298/head
parent
08bdf797f0
commit
9dfabc3fb7
|
@ -19,7 +19,7 @@ from homeassistant.const import (
|
|||
ATTR_MODE,
|
||||
ATTR_NAME,
|
||||
CONF_ALIAS,
|
||||
CONF_CONDITION,
|
||||
CONF_CONDITIONS,
|
||||
CONF_DEVICE_ID,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_EVENT_DATA,
|
||||
|
@ -98,11 +98,11 @@ from homeassistant.util.hass_dict import HassKey
|
|||
|
||||
from .config import AutomationConfig, ValidationStatus
|
||||
from .const import (
|
||||
CONF_ACTION,
|
||||
CONF_ACTIONS,
|
||||
CONF_INITIAL_STATE,
|
||||
CONF_TRACE,
|
||||
CONF_TRIGGER,
|
||||
CONF_TRIGGER_VARIABLES,
|
||||
CONF_TRIGGERS,
|
||||
DEFAULT_INITIAL_STATE,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
|
@ -955,7 +955,7 @@ async def _create_automation_entities(
|
|||
|
||||
action_script = Script(
|
||||
hass,
|
||||
config_block[CONF_ACTION],
|
||||
config_block[CONF_ACTIONS],
|
||||
name,
|
||||
DOMAIN,
|
||||
running_description="automation actions",
|
||||
|
@ -968,7 +968,7 @@ async def _create_automation_entities(
|
|||
# and so will pass them on to the script.
|
||||
)
|
||||
|
||||
if CONF_CONDITION in config_block:
|
||||
if CONF_CONDITIONS in config_block:
|
||||
cond_func = await _async_process_if(hass, name, config_block)
|
||||
|
||||
if cond_func is None:
|
||||
|
@ -991,7 +991,7 @@ async def _create_automation_entities(
|
|||
entity = AutomationEntity(
|
||||
automation_id,
|
||||
name,
|
||||
config_block[CONF_TRIGGER],
|
||||
config_block[CONF_TRIGGERS],
|
||||
cond_func,
|
||||
action_script,
|
||||
initial_state,
|
||||
|
@ -1131,7 +1131,7 @@ async def _async_process_if(
|
|||
hass: HomeAssistant, name: str, config: dict[str, Any]
|
||||
) -> IfAction | None:
|
||||
"""Process if checks."""
|
||||
if_configs = config[CONF_CONDITION]
|
||||
if_configs = config[CONF_CONDITIONS]
|
||||
|
||||
try:
|
||||
if_action = await condition.async_conditions_from_config(
|
||||
|
|
|
@ -16,6 +16,7 @@ from homeassistant.config import config_per_platform, config_without_domain
|
|||
from homeassistant.const import (
|
||||
CONF_ALIAS,
|
||||
CONF_CONDITION,
|
||||
CONF_CONDITIONS,
|
||||
CONF_DESCRIPTION,
|
||||
CONF_ID,
|
||||
CONF_VARIABLES,
|
||||
|
@ -30,11 +31,13 @@ from homeassistant.util.yaml.input import UndefinedSubstitution
|
|||
|
||||
from .const import (
|
||||
CONF_ACTION,
|
||||
CONF_ACTIONS,
|
||||
CONF_HIDE_ENTITY,
|
||||
CONF_INITIAL_STATE,
|
||||
CONF_TRACE,
|
||||
CONF_TRIGGER,
|
||||
CONF_TRIGGER_VARIABLES,
|
||||
CONF_TRIGGERS,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
)
|
||||
|
@ -52,7 +55,41 @@ _MINIMAL_PLATFORM_SCHEMA = vol.Schema(
|
|||
)
|
||||
|
||||
|
||||
def _backward_compat_schema(value: Any | None) -> Any:
|
||||
"""Backward compatibility for automations."""
|
||||
|
||||
if not isinstance(value, dict):
|
||||
return value
|
||||
|
||||
# `trigger` has been renamed to `triggers`
|
||||
if CONF_TRIGGER in value:
|
||||
if CONF_TRIGGERS in value:
|
||||
raise vol.Invalid(
|
||||
"Cannot specify both 'trigger' and 'triggers'. Please use 'triggers' only."
|
||||
)
|
||||
value[CONF_TRIGGERS] = value.pop(CONF_TRIGGER)
|
||||
|
||||
# `condition` has been renamed to `conditions`
|
||||
if CONF_CONDITION in value:
|
||||
if CONF_CONDITIONS in value:
|
||||
raise vol.Invalid(
|
||||
"Cannot specify both 'condition' and 'conditions'. Please use 'conditions' only."
|
||||
)
|
||||
value[CONF_CONDITIONS] = value.pop(CONF_CONDITION)
|
||||
|
||||
# `action` has been renamed to `actions`
|
||||
if CONF_ACTION in value:
|
||||
if CONF_ACTIONS in value:
|
||||
raise vol.Invalid(
|
||||
"Cannot specify both 'action' and 'actions'. Please use 'actions' only."
|
||||
)
|
||||
value[CONF_ACTIONS] = value.pop(CONF_ACTION)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = vol.All(
|
||||
_backward_compat_schema,
|
||||
cv.deprecated(CONF_HIDE_ENTITY),
|
||||
script.make_script_schema(
|
||||
{
|
||||
|
@ -63,16 +100,20 @@ PLATFORM_SCHEMA = vol.All(
|
|||
vol.Optional(CONF_TRACE, default={}): TRACE_CONFIG_SCHEMA,
|
||||
vol.Optional(CONF_INITIAL_STATE): cv.boolean,
|
||||
vol.Optional(CONF_HIDE_ENTITY): cv.boolean,
|
||||
vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA,
|
||||
vol.Optional(CONF_CONDITION): cv.CONDITIONS_SCHEMA,
|
||||
vol.Required(CONF_TRIGGERS): cv.TRIGGER_SCHEMA,
|
||||
vol.Optional(CONF_CONDITIONS): cv.CONDITIONS_SCHEMA,
|
||||
vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA,
|
||||
vol.Optional(CONF_TRIGGER_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA,
|
||||
vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_ACTIONS): cv.SCRIPT_SCHEMA,
|
||||
},
|
||||
script.SCRIPT_MODE_SINGLE,
|
||||
),
|
||||
)
|
||||
|
||||
AUTOMATION_BLUEPRINT_SCHEMA = vol.All(
|
||||
_backward_compat_schema, blueprint.schemas.BLUEPRINT_SCHEMA
|
||||
)
|
||||
|
||||
|
||||
async def _async_validate_config_item( # noqa: C901
|
||||
hass: HomeAssistant,
|
||||
|
@ -151,7 +192,9 @@ async def _async_validate_config_item( # noqa: C901
|
|||
uses_blueprint = True
|
||||
blueprints = async_get_blueprints(hass)
|
||||
try:
|
||||
blueprint_inputs = await blueprints.async_inputs_from_config(config)
|
||||
blueprint_inputs = await blueprints.async_inputs_from_config(
|
||||
_backward_compat_schema(config)
|
||||
)
|
||||
except blueprint.BlueprintException as err:
|
||||
if warn_on_errors:
|
||||
LOGGER.error(
|
||||
|
@ -199,8 +242,8 @@ async def _async_validate_config_item( # noqa: C901
|
|||
automation_config.raw_config = raw_config
|
||||
|
||||
try:
|
||||
automation_config[CONF_TRIGGER] = await async_validate_trigger_config(
|
||||
hass, validated_config[CONF_TRIGGER]
|
||||
automation_config[CONF_TRIGGERS] = await async_validate_trigger_config(
|
||||
hass, validated_config[CONF_TRIGGERS]
|
||||
)
|
||||
except (
|
||||
vol.Invalid,
|
||||
|
@ -216,10 +259,10 @@ async def _async_validate_config_item( # noqa: C901
|
|||
)
|
||||
return automation_config
|
||||
|
||||
if CONF_CONDITION in validated_config:
|
||||
if CONF_CONDITIONS in validated_config:
|
||||
try:
|
||||
automation_config[CONF_CONDITION] = await async_validate_conditions_config(
|
||||
hass, validated_config[CONF_CONDITION]
|
||||
automation_config[CONF_CONDITIONS] = await async_validate_conditions_config(
|
||||
hass, validated_config[CONF_CONDITIONS]
|
||||
)
|
||||
except (
|
||||
vol.Invalid,
|
||||
|
@ -239,8 +282,8 @@ async def _async_validate_config_item( # noqa: C901
|
|||
return automation_config
|
||||
|
||||
try:
|
||||
automation_config[CONF_ACTION] = await script.async_validate_actions_config(
|
||||
hass, validated_config[CONF_ACTION]
|
||||
automation_config[CONF_ACTIONS] = await script.async_validate_actions_config(
|
||||
hass, validated_config[CONF_ACTIONS]
|
||||
)
|
||||
except (
|
||||
vol.Invalid,
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
import logging
|
||||
|
||||
CONF_ACTION = "action"
|
||||
CONF_ACTIONS = "actions"
|
||||
CONF_TRIGGER = "trigger"
|
||||
CONF_TRIGGERS = "triggers"
|
||||
CONF_TRIGGER_VARIABLES = "trigger_variables"
|
||||
DOMAIN = "automation"
|
||||
|
||||
|
|
|
@ -28,6 +28,14 @@ async def _reload_blueprint_automations(
|
|||
@callback
|
||||
def async_get_blueprints(hass: HomeAssistant) -> blueprint.DomainBlueprints:
|
||||
"""Get automation blueprints."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from .config import AUTOMATION_BLUEPRINT_SCHEMA
|
||||
|
||||
return blueprint.DomainBlueprints(
|
||||
hass, DOMAIN, LOGGER, _blueprint_in_use, _reload_blueprint_automations
|
||||
hass,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
_blueprint_in_use,
|
||||
_reload_blueprint_automations,
|
||||
AUTOMATION_BLUEPRINT_SCHEMA,
|
||||
)
|
||||
|
|
|
@ -15,7 +15,7 @@ from .errors import ( # noqa: F401
|
|||
MissingInput,
|
||||
)
|
||||
from .models import Blueprint, BlueprintInputs, DomainBlueprints # noqa: F401
|
||||
from .schemas import is_blueprint_instance_config # noqa: F401
|
||||
from .schemas import BLUEPRINT_SCHEMA, is_blueprint_instance_config # noqa: F401
|
||||
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ from homeassistant.helpers import aiohttp_client, config_validation as cv
|
|||
from homeassistant.util import yaml
|
||||
|
||||
from .models import Blueprint
|
||||
from .schemas import is_blueprint_config
|
||||
from .schemas import BLUEPRINT_SCHEMA, is_blueprint_config
|
||||
|
||||
COMMUNITY_TOPIC_PATTERN = re.compile(
|
||||
r"^https://community.home-assistant.io/t/[a-z0-9-]+/(?P<topic>\d+)(?:/(?P<post>\d+)|)$"
|
||||
|
@ -126,7 +126,7 @@ def _extract_blueprint_from_community_topic(
|
|||
continue
|
||||
assert isinstance(data, dict)
|
||||
|
||||
blueprint = Blueprint(data)
|
||||
blueprint = Blueprint(data, schema=BLUEPRINT_SCHEMA)
|
||||
break
|
||||
|
||||
if blueprint is None:
|
||||
|
@ -169,7 +169,7 @@ async def fetch_blueprint_from_github_url(
|
|||
raw_yaml = await resp.text()
|
||||
data = yaml.parse_yaml(raw_yaml)
|
||||
assert isinstance(data, dict)
|
||||
blueprint = Blueprint(data)
|
||||
blueprint = Blueprint(data, schema=BLUEPRINT_SCHEMA)
|
||||
|
||||
parsed_import_url = yarl.URL(import_url)
|
||||
suggested_filename = f"{parsed_import_url.parts[1]}/{parsed_import_url.parts[-1]}"
|
||||
|
@ -211,7 +211,7 @@ async def fetch_blueprint_from_github_gist_url(
|
|||
continue
|
||||
assert isinstance(data, dict)
|
||||
|
||||
blueprint = Blueprint(data)
|
||||
blueprint = Blueprint(data, schema=BLUEPRINT_SCHEMA)
|
||||
break
|
||||
|
||||
if blueprint is None:
|
||||
|
@ -238,7 +238,7 @@ async def fetch_blueprint_from_website_url(
|
|||
raw_yaml = await resp.text()
|
||||
data = yaml.parse_yaml(raw_yaml)
|
||||
assert isinstance(data, dict)
|
||||
blueprint = Blueprint(data)
|
||||
blueprint = Blueprint(data, schema=BLUEPRINT_SCHEMA)
|
||||
|
||||
parsed_import_url = yarl.URL(url)
|
||||
suggested_filename = f"homeassistant/{parsed_import_url.parts[-1][:-5]}"
|
||||
|
@ -256,7 +256,7 @@ async def fetch_blueprint_from_generic_url(
|
|||
data = yaml.parse_yaml(raw_yaml)
|
||||
|
||||
assert isinstance(data, dict)
|
||||
blueprint = Blueprint(data)
|
||||
blueprint = Blueprint(data, schema=BLUEPRINT_SCHEMA)
|
||||
|
||||
parsed_import_url = yarl.URL(url)
|
||||
suggested_filename = f"{parsed_import_url.host}/{parsed_import_url.parts[-1][:-5]}"
|
||||
|
@ -273,7 +273,11 @@ FETCH_FUNCTIONS = (
|
|||
|
||||
|
||||
async def fetch_blueprint_from_url(hass: HomeAssistant, url: str) -> ImportedBlueprint:
|
||||
"""Get a blueprint from a url."""
|
||||
"""Get a blueprint from a url.
|
||||
|
||||
The returned blueprint will only be validated with BLUEPRINT_SCHEMA, not the domain
|
||||
specific schema.
|
||||
"""
|
||||
for func in FETCH_FUNCTIONS:
|
||||
with suppress(UnsupportedUrl):
|
||||
imported_bp = await func(hass, url)
|
||||
|
|
|
@ -44,7 +44,7 @@ from .errors import (
|
|||
InvalidBlueprintInputs,
|
||||
MissingInput,
|
||||
)
|
||||
from .schemas import BLUEPRINT_INSTANCE_FIELDS, BLUEPRINT_SCHEMA
|
||||
from .schemas import BLUEPRINT_INSTANCE_FIELDS
|
||||
|
||||
|
||||
class Blueprint:
|
||||
|
@ -56,10 +56,11 @@ class Blueprint:
|
|||
*,
|
||||
path: str | None = None,
|
||||
expected_domain: str | None = None,
|
||||
schema: Callable[[Any], Any],
|
||||
) -> None:
|
||||
"""Initialize a blueprint."""
|
||||
try:
|
||||
data = self.data = BLUEPRINT_SCHEMA(data)
|
||||
data = self.data = schema(data)
|
||||
except vol.Invalid as err:
|
||||
raise InvalidBlueprint(expected_domain, path, data, err) from err
|
||||
|
||||
|
@ -197,6 +198,7 @@ class DomainBlueprints:
|
|||
logger: logging.Logger,
|
||||
blueprint_in_use: Callable[[HomeAssistant, str], bool],
|
||||
reload_blueprint_consumers: Callable[[HomeAssistant, str], Awaitable[None]],
|
||||
blueprint_schema: Callable[[Any], Any],
|
||||
) -> None:
|
||||
"""Initialize a domain blueprints instance."""
|
||||
self.hass = hass
|
||||
|
@ -206,6 +208,7 @@ class DomainBlueprints:
|
|||
self._reload_blueprint_consumers = reload_blueprint_consumers
|
||||
self._blueprints: dict[str, Blueprint | None] = {}
|
||||
self._load_lock = asyncio.Lock()
|
||||
self._blueprint_schema = blueprint_schema
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[domain] = self
|
||||
|
||||
|
@ -233,7 +236,10 @@ class DomainBlueprints:
|
|||
raise FailedToLoad(self.domain, blueprint_path, err) from err
|
||||
|
||||
return Blueprint(
|
||||
blueprint_data, expected_domain=self.domain, path=blueprint_path
|
||||
blueprint_data,
|
||||
expected_domain=self.domain,
|
||||
path=blueprint_path,
|
||||
schema=self._blueprint_schema,
|
||||
)
|
||||
|
||||
def _load_blueprints(self) -> dict[str, Blueprint | BlueprintException | None]:
|
||||
|
|
|
@ -18,6 +18,7 @@ from homeassistant.util import yaml
|
|||
from . import importer, models
|
||||
from .const import DOMAIN
|
||||
from .errors import BlueprintException, FailedToLoad, FileAlreadyExists
|
||||
from .schemas import BLUEPRINT_SCHEMA
|
||||
|
||||
|
||||
@callback
|
||||
|
@ -174,7 +175,9 @@ async def ws_save_blueprint(
|
|||
|
||||
try:
|
||||
yaml_data = cast(dict[str, Any], yaml.parse_yaml(msg["yaml"]))
|
||||
blueprint = models.Blueprint(yaml_data, expected_domain=domain)
|
||||
blueprint = models.Blueprint(
|
||||
yaml_data, expected_domain=domain, schema=BLUEPRINT_SCHEMA
|
||||
)
|
||||
if "source_url" in msg:
|
||||
blueprint.update_metadata(source_url=msg["source_url"])
|
||||
except HomeAssistantError as err:
|
||||
|
|
|
@ -70,7 +70,16 @@ class EditAutomationConfigView(EditIdBasedConfigView):
|
|||
updated_value = {CONF_ID: config_key}
|
||||
|
||||
# Iterate through some keys that we want to have ordered in the output
|
||||
for key in ("alias", "description", "trigger", "condition", "action"):
|
||||
for key in (
|
||||
"alias",
|
||||
"description",
|
||||
"triggers",
|
||||
"trigger",
|
||||
"conditions",
|
||||
"condition",
|
||||
"actions",
|
||||
"action",
|
||||
):
|
||||
if key in new_value:
|
||||
updated_value[key] = new_value[key]
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"""Helpers for automation integration."""
|
||||
|
||||
from homeassistant.components.blueprint import DomainBlueprints
|
||||
from homeassistant.components.blueprint import BLUEPRINT_SCHEMA, DomainBlueprints
|
||||
from homeassistant.const import SERVICE_RELOAD
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
|
@ -27,5 +27,10 @@ async def _reload_blueprint_scripts(hass: HomeAssistant, blueprint_path: str) ->
|
|||
def async_get_blueprints(hass: HomeAssistant) -> DomainBlueprints:
|
||||
"""Get script blueprints."""
|
||||
return DomainBlueprints(
|
||||
hass, DOMAIN, LOGGER, _blueprint_in_use, _reload_blueprint_scripts
|
||||
hass,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
_blueprint_in_use,
|
||||
_reload_blueprint_scripts,
|
||||
BLUEPRINT_SCHEMA,
|
||||
)
|
||||
|
|
|
@ -859,9 +859,9 @@ def handle_fire_event(
|
|||
@decorators.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "validate_config",
|
||||
vol.Optional("trigger"): cv.match_all,
|
||||
vol.Optional("condition"): cv.match_all,
|
||||
vol.Optional("action"): cv.match_all,
|
||||
vol.Optional("triggers"): cv.match_all,
|
||||
vol.Optional("conditions"): cv.match_all,
|
||||
vol.Optional("actions"): cv.match_all,
|
||||
}
|
||||
)
|
||||
@decorators.async_response
|
||||
|
@ -876,9 +876,13 @@ async def handle_validate_config(
|
|||
result = {}
|
||||
|
||||
for key, schema, validator in (
|
||||
("trigger", cv.TRIGGER_SCHEMA, trigger.async_validate_trigger_config),
|
||||
("condition", cv.CONDITIONS_SCHEMA, condition.async_validate_conditions_config),
|
||||
("action", cv.SCRIPT_SCHEMA, script.async_validate_actions_config),
|
||||
("triggers", cv.TRIGGER_SCHEMA, trigger.async_validate_trigger_config),
|
||||
(
|
||||
"conditions",
|
||||
cv.CONDITIONS_SCHEMA,
|
||||
condition.async_validate_conditions_config,
|
||||
),
|
||||
("actions", cv.SCRIPT_SCHEMA, script.async_validate_actions_config),
|
||||
):
|
||||
if key not in msg:
|
||||
continue
|
||||
|
|
|
@ -38,7 +38,10 @@ def patch_blueprint(
|
|||
return orig_load(self, path)
|
||||
|
||||
return models.Blueprint(
|
||||
yaml.load_yaml(data_path), expected_domain=self.domain, path=path
|
||||
yaml.load_yaml(data_path),
|
||||
expected_domain=self.domain,
|
||||
path=path,
|
||||
schema=automation.config.AUTOMATION_BLUEPRINT_SCHEMA,
|
||||
)
|
||||
|
||||
with patch(
|
||||
|
|
|
@ -240,7 +240,7 @@ async def test_trigger_service_ignoring_condition(
|
|||
automation.DOMAIN: {
|
||||
"alias": "test",
|
||||
"trigger": [{"platform": "event", "event_type": "test_event"}],
|
||||
"condition": {
|
||||
"conditions": {
|
||||
"condition": "numeric_state",
|
||||
"entity_id": "non.existing",
|
||||
"above": "1",
|
||||
|
@ -292,8 +292,8 @@ async def test_two_conditions_with_and(
|
|||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": [{"platform": "event", "event_type": "test_event"}],
|
||||
"condition": [
|
||||
"triggers": [{"platform": "event", "event_type": "test_event"}],
|
||||
"conditions": [
|
||||
{"condition": "state", "entity_id": entity_id, "state": "100"},
|
||||
{
|
||||
"condition": "numeric_state",
|
||||
|
@ -301,7 +301,7 @@ async def test_two_conditions_with_and(
|
|||
"below": 150,
|
||||
},
|
||||
],
|
||||
"action": {"action": "test.automation"},
|
||||
"actions": {"action": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
@ -331,9 +331,9 @@ async def test_shorthand_conditions_template(
|
|||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": [{"platform": "event", "event_type": "test_event"}],
|
||||
"condition": "{{ is_state('test.entity', 'hello') }}",
|
||||
"action": {"action": "test.automation"},
|
||||
"triggers": [{"platform": "event", "event_type": "test_event"}],
|
||||
"conditions": "{{ is_state('test.entity', 'hello') }}",
|
||||
"actions": {"action": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
@ -807,8 +807,8 @@ async def test_reload_unchanged_does_not_stop(
|
|||
config = {
|
||||
automation.DOMAIN: {
|
||||
"alias": "hello",
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": [
|
||||
"triggers": {"platform": "event", "event_type": "test_event"},
|
||||
"actions": [
|
||||
{"event": "running"},
|
||||
{"wait_template": "{{ is_state('test.entity', 'goodbye') }}"},
|
||||
{"action": "test.automation"},
|
||||
|
@ -854,8 +854,8 @@ async def test_reload_single_unchanged_does_not_stop(
|
|||
automation.DOMAIN: {
|
||||
"id": "sun",
|
||||
"alias": "hello",
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": [
|
||||
"triggers": {"platform": "event", "event_type": "test_event"},
|
||||
"actions": [
|
||||
{"event": "running"},
|
||||
{"wait_template": "{{ is_state('test.entity', 'goodbye') }}"},
|
||||
{"action": "test.automation"},
|
||||
|
@ -1092,13 +1092,13 @@ async def test_reload_moved_automation_without_alias(
|
|||
config = {
|
||||
automation.DOMAIN: [
|
||||
{
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": [{"action": "test.automation"}],
|
||||
"triggers": {"platform": "event", "event_type": "test_event"},
|
||||
"actions": [{"action": "test.automation"}],
|
||||
},
|
||||
{
|
||||
"alias": "automation_with_alias",
|
||||
"trigger": {"platform": "event", "event_type": "test_event2"},
|
||||
"action": [{"action": "test.automation"}],
|
||||
"triggers": {"platform": "event", "event_type": "test_event2"},
|
||||
"actions": [{"action": "test.automation"}],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
@ -1148,18 +1148,18 @@ async def test_reload_identical_automations_without_id(
|
|||
automation.DOMAIN: [
|
||||
{
|
||||
"alias": "dolly",
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": [{"action": "test.automation"}],
|
||||
"triggers": {"platform": "event", "event_type": "test_event"},
|
||||
"actions": [{"action": "test.automation"}],
|
||||
},
|
||||
{
|
||||
"alias": "dolly",
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": [{"action": "test.automation"}],
|
||||
"triggers": {"platform": "event", "event_type": "test_event"},
|
||||
"actions": [{"action": "test.automation"}],
|
||||
},
|
||||
{
|
||||
"alias": "dolly",
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": [{"action": "test.automation"}],
|
||||
"triggers": {"platform": "event", "event_type": "test_event"},
|
||||
"actions": [{"action": "test.automation"}],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
@ -1245,13 +1245,13 @@ async def test_reload_identical_automations_without_id(
|
|||
"automation_config",
|
||||
[
|
||||
{
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": [{"action": "test.automation"}],
|
||||
"triggers": {"platform": "event", "event_type": "test_event"},
|
||||
"actions": [{"action": "test.automation"}],
|
||||
},
|
||||
# An automation using templates
|
||||
{
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": [{"action": "{{ 'test.automation' }}"}],
|
||||
"triggers": {"platform": "event", "event_type": "test_event"},
|
||||
"actions": [{"action": "{{ 'test.automation' }}"}],
|
||||
},
|
||||
# An automation using blueprint
|
||||
{
|
||||
|
@ -1277,14 +1277,14 @@ async def test_reload_identical_automations_without_id(
|
|||
},
|
||||
{
|
||||
"id": "sun",
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": [{"action": "test.automation"}],
|
||||
"triggers": {"platform": "event", "event_type": "test_event"},
|
||||
"actions": [{"action": "test.automation"}],
|
||||
},
|
||||
# An automation using templates
|
||||
{
|
||||
"id": "sun",
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": [{"action": "{{ 'test.automation' }}"}],
|
||||
"triggers": {"platform": "event", "event_type": "test_event"},
|
||||
"actions": [{"action": "{{ 'test.automation' }}"}],
|
||||
},
|
||||
# An automation using blueprint
|
||||
{
|
||||
|
@ -1380,8 +1380,8 @@ async def test_reload_automation_when_blueprint_changes(
|
|||
# Reload the automations without any change, but with updated blueprint
|
||||
blueprint_path = automation.async_get_blueprints(hass).blueprint_folder
|
||||
blueprint_config = yaml.load_yaml(blueprint_path / "test_event_service.yaml")
|
||||
blueprint_config["action"] = [blueprint_config["action"]]
|
||||
blueprint_config["action"].append(blueprint_config["action"][-1])
|
||||
blueprint_config["actions"] = [blueprint_config["actions"]]
|
||||
blueprint_config["actions"].append(blueprint_config["actions"][-1])
|
||||
|
||||
with (
|
||||
patch(
|
||||
|
@ -1650,13 +1650,13 @@ async def test_automation_not_trigger_on_bootstrap(hass: HomeAssistant) -> None:
|
|||
(
|
||||
{},
|
||||
"could not be validated",
|
||||
"required key not provided @ data['action']",
|
||||
"required key not provided @ data['actions']",
|
||||
"validation_failed_schema",
|
||||
),
|
||||
(
|
||||
{
|
||||
"trigger": {"platform": "automation"},
|
||||
"action": [],
|
||||
"triggers": {"platform": "automation"},
|
||||
"actions": [],
|
||||
},
|
||||
"failed to setup triggers",
|
||||
"Integration 'automation' does not provide trigger support.",
|
||||
|
@ -1664,14 +1664,14 @@ async def test_automation_not_trigger_on_bootstrap(hass: HomeAssistant) -> None:
|
|||
),
|
||||
(
|
||||
{
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"condition": {
|
||||
"triggers": {"platform": "event", "event_type": "test_event"},
|
||||
"conditions": {
|
||||
"condition": "state",
|
||||
# The UUID will fail being resolved to en entity_id
|
||||
"entity_id": "abcdabcdabcdabcdabcdabcdabcdabcd",
|
||||
"state": "blah",
|
||||
},
|
||||
"action": [],
|
||||
"actions": [],
|
||||
},
|
||||
"failed to setup conditions",
|
||||
"Unknown entity registry entry abcdabcdabcdabcdabcdabcdabcdabcd.",
|
||||
|
@ -1679,8 +1679,8 @@ async def test_automation_not_trigger_on_bootstrap(hass: HomeAssistant) -> None:
|
|||
),
|
||||
(
|
||||
{
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": {
|
||||
"triggers": {"platform": "event", "event_type": "test_event"},
|
||||
"actions": {
|
||||
"condition": "state",
|
||||
# The UUID will fail being resolved to en entity_id
|
||||
"entity_id": "abcdabcdabcdabcdabcdabcdabcdabcd",
|
||||
|
@ -1712,8 +1712,8 @@ async def test_automation_bad_config_validation(
|
|||
{"alias": "bad_automation", **broken_config},
|
||||
{
|
||||
"alias": "good_automation",
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": {
|
||||
"triggers": {"platform": "event", "event_type": "test_event"},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"entity_id": "hello.world",
|
||||
},
|
||||
|
@ -1970,7 +1970,7 @@ async def test_extraction_functions(
|
|||
DOMAIN: [
|
||||
{
|
||||
"alias": "test1",
|
||||
"trigger": [
|
||||
"triggers": [
|
||||
{"platform": "state", "entity_id": "sensor.trigger_state"},
|
||||
{
|
||||
"platform": "numeric_state",
|
||||
|
@ -2006,12 +2006,12 @@ async def test_extraction_functions(
|
|||
"event_data": {"entity_id": 123},
|
||||
},
|
||||
],
|
||||
"condition": {
|
||||
"conditions": {
|
||||
"condition": "state",
|
||||
"entity_id": "light.condition_state",
|
||||
"state": "on",
|
||||
},
|
||||
"action": [
|
||||
"actions": [
|
||||
{
|
||||
"action": "test.script",
|
||||
"data": {"entity_id": "light.in_both"},
|
||||
|
@ -2042,7 +2042,7 @@ async def test_extraction_functions(
|
|||
},
|
||||
{
|
||||
"alias": "test2",
|
||||
"trigger": [
|
||||
"triggers": [
|
||||
{
|
||||
"platform": "device",
|
||||
"domain": "light",
|
||||
|
@ -2078,14 +2078,14 @@ async def test_extraction_functions(
|
|||
"event_data": {"device_id": 123},
|
||||
},
|
||||
],
|
||||
"condition": {
|
||||
"conditions": {
|
||||
"condition": "device",
|
||||
"device_id": condition_device.id,
|
||||
"domain": "light",
|
||||
"type": "is_on",
|
||||
"entity_id": "light.bla",
|
||||
},
|
||||
"action": [
|
||||
"actions": [
|
||||
{
|
||||
"action": "test.script",
|
||||
"data": {"entity_id": "light.in_both"},
|
||||
|
@ -2112,7 +2112,7 @@ async def test_extraction_functions(
|
|||
},
|
||||
{
|
||||
"alias": "test3",
|
||||
"trigger": [
|
||||
"triggers": [
|
||||
{
|
||||
"platform": "event",
|
||||
"event_type": "esphome.button_pressed",
|
||||
|
@ -2131,14 +2131,14 @@ async def test_extraction_functions(
|
|||
"event_data": {"area_id": 123},
|
||||
},
|
||||
],
|
||||
"condition": {
|
||||
"conditions": {
|
||||
"condition": "device",
|
||||
"device_id": condition_device.id,
|
||||
"domain": "light",
|
||||
"type": "is_on",
|
||||
"entity_id": "light.bla",
|
||||
},
|
||||
"action": [
|
||||
"actions": [
|
||||
{
|
||||
"action": "test.script",
|
||||
"data": {"entity_id": "light.in_both"},
|
||||
|
@ -2287,8 +2287,8 @@ async def test_automation_variables(
|
|||
"event_type": "{{ trigger.event.event_type }}",
|
||||
"this_variables": "{{this.entity_id}}",
|
||||
},
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": {
|
||||
"triggers": {"platform": "event", "event_type": "test_event"},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
"value": "{{ test_var }}",
|
||||
|
@ -2303,11 +2303,11 @@ async def test_automation_variables(
|
|||
"test_var": "defined_in_config",
|
||||
},
|
||||
"trigger": {"platform": "event", "event_type": "test_event_2"},
|
||||
"condition": {
|
||||
"conditions": {
|
||||
"condition": "template",
|
||||
"value_template": "{{ trigger.event.data.pass_condition }}",
|
||||
},
|
||||
"action": {
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
},
|
||||
},
|
||||
|
@ -2315,8 +2315,8 @@ async def test_automation_variables(
|
|||
"variables": {
|
||||
"test_var": "{{ trigger.event.data.break + 1 }}",
|
||||
},
|
||||
"trigger": {"platform": "event", "event_type": "test_event_3"},
|
||||
"action": {
|
||||
"triggers": {"platform": "event", "event_type": "test_event_3"},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
},
|
||||
},
|
||||
|
@ -2517,6 +2517,107 @@ async def test_blueprint_automation(
|
|||
]
|
||||
|
||||
|
||||
async def test_blueprint_automation_legacy_schema(
|
||||
hass: HomeAssistant, calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test blueprint automation where the blueprint is using legacy schema."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
"automation",
|
||||
{
|
||||
"automation": {
|
||||
"use_blueprint": {
|
||||
"path": "test_event_service_legacy_schema.yaml",
|
||||
"input": {
|
||||
"trigger_event": "blueprint_event",
|
||||
"service_to_call": "test.automation",
|
||||
"a_number": 5,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
hass.bus.async_fire("blueprint_event")
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 1
|
||||
assert automation.entities_in_automation(hass, "automation.automation_0") == [
|
||||
"light.kitchen"
|
||||
]
|
||||
assert (
|
||||
automation.blueprint_in_automation(hass, "automation.automation_0")
|
||||
== "test_event_service_legacy_schema.yaml"
|
||||
)
|
||||
assert automation.automations_with_blueprint(
|
||||
hass, "test_event_service_legacy_schema.yaml"
|
||||
) == ["automation.automation_0"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("blueprint", "override"),
|
||||
[
|
||||
# Override a blueprint with modern schema with legacy schema
|
||||
(
|
||||
"test_event_service.yaml",
|
||||
{"trigger": {"platform": "event", "event_type": "override"}},
|
||||
),
|
||||
# Override a blueprint with modern schema with modern schema
|
||||
(
|
||||
"test_event_service.yaml",
|
||||
{"triggers": {"platform": "event", "event_type": "override"}},
|
||||
),
|
||||
# Override a blueprint with legacy schema with legacy schema
|
||||
(
|
||||
"test_event_service_legacy_schema.yaml",
|
||||
{"trigger": {"platform": "event", "event_type": "override"}},
|
||||
),
|
||||
# Override a blueprint with legacy schema with modern schema
|
||||
(
|
||||
"test_event_service_legacy_schema.yaml",
|
||||
{"triggers": {"platform": "event", "event_type": "override"}},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_blueprint_automation_override(
|
||||
hass: HomeAssistant, calls: list[ServiceCall], blueprint: str, override: dict
|
||||
) -> None:
|
||||
"""Test blueprint automation where the automation config overrides the blueprint."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
"automation",
|
||||
{
|
||||
"automation": {
|
||||
"use_blueprint": {
|
||||
"path": blueprint,
|
||||
"input": {
|
||||
"trigger_event": "blueprint_event",
|
||||
"service_to_call": "test.automation",
|
||||
"a_number": 5,
|
||||
},
|
||||
},
|
||||
}
|
||||
| override
|
||||
},
|
||||
)
|
||||
|
||||
hass.bus.async_fire("blueprint_event")
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 0
|
||||
|
||||
hass.bus.async_fire("override")
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 1
|
||||
|
||||
assert automation.entities_in_automation(hass, "automation.automation_0") == [
|
||||
"light.kitchen"
|
||||
]
|
||||
assert (
|
||||
automation.blueprint_in_automation(hass, "automation.automation_0") == blueprint
|
||||
)
|
||||
assert automation.automations_with_blueprint(hass, blueprint) == [
|
||||
"automation.automation_0"
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("blueprint_inputs", "problem", "details"),
|
||||
[
|
||||
|
@ -2542,7 +2643,7 @@ async def test_blueprint_automation(
|
|||
"Blueprint 'Call service based on event' generated invalid automation",
|
||||
(
|
||||
"value should be a string for dictionary value @"
|
||||
" data['action'][0]['action']"
|
||||
" data['actions'][0]['action']"
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -3020,8 +3121,8 @@ async def test_websocket_config(
|
|||
"""Test config command."""
|
||||
config = {
|
||||
"alias": "hello",
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": {"action": "test.automation", "data": 100},
|
||||
"triggers": {"platform": "event", "event_type": "test_event"},
|
||||
"actions": {"action": "test.automation", "data": 100},
|
||||
}
|
||||
assert await async_setup_component(
|
||||
hass, automation.DOMAIN, {automation.DOMAIN: config}
|
||||
|
@ -3303,16 +3404,26 @@ async def test_two_automation_call_restart_script_right_after_each_other(
|
|||
assert len(events) == 1
|
||||
|
||||
|
||||
async def test_action_service_backward_compatibility(
|
||||
async def test_action_backward_compatibility(
|
||||
hass: HomeAssistant, calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test we can still use the service call method."""
|
||||
"""Test we can still use old-style automations.
|
||||
|
||||
- Services action using the `service` key instead of `action`
|
||||
- Singular `trigger` instead of `triggers`
|
||||
- Singular `condition` instead of `conditions`
|
||||
- Singular `action` instead of `actions`
|
||||
"""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"condition": {
|
||||
"condition": "template",
|
||||
"value_template": "{{ True }}",
|
||||
},
|
||||
"action": {
|
||||
"service": "test.automation",
|
||||
"entity_id": "hello.world",
|
||||
|
@ -3327,3 +3438,48 @@ async def test_action_service_backward_compatibility(
|
|||
assert len(calls) == 1
|
||||
assert calls[0].data.get(ATTR_ENTITY_ID) == ["hello.world"]
|
||||
assert calls[0].data.get("event") == "test_event"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "message"),
|
||||
[
|
||||
(
|
||||
{
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"triggers": {"platform": "event", "event_type": "test_event2"},
|
||||
"actions": [],
|
||||
},
|
||||
"Cannot specify both 'trigger' and 'triggers'. Please use 'triggers' only.",
|
||||
),
|
||||
(
|
||||
{
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"condition": {"condition": "template", "value_template": "{{ True }}"},
|
||||
"conditions": {"condition": "template", "value_template": "{{ True }}"},
|
||||
},
|
||||
"Cannot specify both 'condition' and 'conditions'. Please use 'conditions' only.",
|
||||
),
|
||||
(
|
||||
{
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": {"service": "test.automation", "entity_id": "hello.world"},
|
||||
"actions": {"service": "test.automation", "entity_id": "hello.world"},
|
||||
},
|
||||
"Cannot specify both 'action' and 'actions'. Please use 'actions' only.",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_invalid_configuration(
|
||||
hass: HomeAssistant,
|
||||
config: dict[str, Any],
|
||||
message: str,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test for invalid automation configurations."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{automation.DOMAIN: config},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert message in caplog.text
|
||||
|
|
|
@ -40,7 +40,7 @@ async def test_exclude_attributes(
|
|||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": {"action": "test.automation", "entity_id": "hello.world"},
|
||||
"actions": {"action": "test.automation", "entity_id": "hello.world"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
@ -6,7 +6,7 @@ import pathlib
|
|||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.blueprint import models
|
||||
from homeassistant.components.blueprint import BLUEPRINT_SCHEMA, models
|
||||
from homeassistant.components.blueprint.const import BLUEPRINT_FOLDER
|
||||
from homeassistant.util import yaml
|
||||
|
||||
|
@ -26,4 +26,4 @@ def test_default_blueprints(domain: str) -> None:
|
|||
LOGGER.info("Processing %s", fil)
|
||||
assert fil.name.endswith(".yaml")
|
||||
data = yaml.load_yaml(fil)
|
||||
models.Blueprint(data, expected_domain=domain)
|
||||
models.Blueprint(data, expected_domain=domain, schema=BLUEPRINT_SCHEMA)
|
||||
|
|
|
@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, patch
|
|||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.blueprint import errors, models
|
||||
from homeassistant.components.blueprint import BLUEPRINT_SCHEMA, errors, models
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util.yaml import Input
|
||||
|
||||
|
@ -22,7 +22,8 @@ def blueprint_1() -> models.Blueprint:
|
|||
"input": {"test-input": {"name": "Name", "description": "Description"}},
|
||||
},
|
||||
"example": Input("test-input"),
|
||||
}
|
||||
},
|
||||
schema=BLUEPRINT_SCHEMA,
|
||||
)
|
||||
|
||||
|
||||
|
@ -57,26 +58,32 @@ def blueprint_2(request: pytest.FixtureRequest) -> models.Blueprint:
|
|||
}
|
||||
},
|
||||
}
|
||||
return models.Blueprint(blueprint)
|
||||
return models.Blueprint(blueprint, schema=BLUEPRINT_SCHEMA)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def domain_bps(hass: HomeAssistant) -> models.DomainBlueprints:
|
||||
"""Domain blueprints fixture."""
|
||||
return models.DomainBlueprints(
|
||||
hass, "automation", logging.getLogger(__name__), None, AsyncMock()
|
||||
hass,
|
||||
"automation",
|
||||
logging.getLogger(__name__),
|
||||
None,
|
||||
AsyncMock(),
|
||||
BLUEPRINT_SCHEMA,
|
||||
)
|
||||
|
||||
|
||||
def test_blueprint_model_init() -> None:
|
||||
"""Test constructor validation."""
|
||||
with pytest.raises(errors.InvalidBlueprint):
|
||||
models.Blueprint({})
|
||||
models.Blueprint({}, schema=BLUEPRINT_SCHEMA)
|
||||
|
||||
with pytest.raises(errors.InvalidBlueprint):
|
||||
models.Blueprint(
|
||||
{"blueprint": {"name": "Hello", "domain": "automation"}},
|
||||
expected_domain="not-automation",
|
||||
schema=BLUEPRINT_SCHEMA,
|
||||
)
|
||||
|
||||
with pytest.raises(errors.InvalidBlueprint):
|
||||
|
@ -88,7 +95,8 @@ def test_blueprint_model_init() -> None:
|
|||
"input": {"something": None},
|
||||
},
|
||||
"trigger": {"platform": Input("non-existing")},
|
||||
}
|
||||
},
|
||||
schema=BLUEPRINT_SCHEMA,
|
||||
)
|
||||
|
||||
|
||||
|
@ -115,7 +123,8 @@ def test_blueprint_update_metadata() -> None:
|
|||
"name": "Hello",
|
||||
"domain": "automation",
|
||||
},
|
||||
}
|
||||
},
|
||||
schema=BLUEPRINT_SCHEMA,
|
||||
)
|
||||
|
||||
bp.update_metadata(source_url="http://bla.com")
|
||||
|
@ -131,7 +140,8 @@ def test_blueprint_validate() -> None:
|
|||
"name": "Hello",
|
||||
"domain": "automation",
|
||||
},
|
||||
}
|
||||
},
|
||||
schema=BLUEPRINT_SCHEMA,
|
||||
).validate()
|
||||
is None
|
||||
)
|
||||
|
@ -143,7 +153,8 @@ def test_blueprint_validate() -> None:
|
|||
"domain": "automation",
|
||||
"homeassistant": {"min_version": "100000.0.0"},
|
||||
},
|
||||
}
|
||||
},
|
||||
schema=BLUEPRINT_SCHEMA,
|
||||
).validate() == ["Requires at least Home Assistant 100000.0.0"]
|
||||
|
||||
|
||||
|
|
|
@ -64,6 +64,17 @@ async def test_list_blueprints(
|
|||
"name": "Call service based on event",
|
||||
},
|
||||
},
|
||||
"test_event_service_legacy_schema.yaml": {
|
||||
"metadata": {
|
||||
"domain": "automation",
|
||||
"input": {
|
||||
"service_to_call": None,
|
||||
"trigger_event": {"selector": {"text": {}}},
|
||||
"a_number": {"selector": {"number": {"mode": "box", "step": 1.0}}},
|
||||
},
|
||||
"name": "Call service based on event",
|
||||
},
|
||||
},
|
||||
"in_folder/in_folder_blueprint.yaml": {
|
||||
"metadata": {
|
||||
"domain": "automation",
|
||||
|
@ -212,16 +223,16 @@ async def test_save_blueprint(
|
|||
" input:\n trigger_event:\n selector:\n text: {}\n "
|
||||
" service_to_call:\n a_number:\n selector:\n number:\n "
|
||||
" mode: box\n step: 1.0\n source_url:"
|
||||
" https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n"
|
||||
" platform: event\n event_type: !input 'trigger_event'\naction:\n "
|
||||
" https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntriggers:\n"
|
||||
" platform: event\n event_type: !input 'trigger_event'\nactions:\n "
|
||||
" service: !input 'service_to_call'\n entity_id: light.kitchen\n"
|
||||
# c dumper will not quote the value after !input
|
||||
"blueprint:\n name: Call service based on event\n domain: automation\n "
|
||||
" input:\n trigger_event:\n selector:\n text: {}\n "
|
||||
" service_to_call:\n a_number:\n selector:\n number:\n "
|
||||
" mode: box\n step: 1.0\n source_url:"
|
||||
" https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n"
|
||||
" platform: event\n event_type: !input trigger_event\naction:\n service:"
|
||||
" https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntriggers:\n"
|
||||
" platform: event\n event_type: !input trigger_event\nactions:\n service:"
|
||||
" !input service_to_call\n entity_id: light.kitchen\n"
|
||||
)
|
||||
# Make sure ita parsable and does not raise
|
||||
|
@ -483,11 +494,11 @@ async def test_substituting_blueprint_inputs(
|
|||
|
||||
assert msg["success"]
|
||||
assert msg["result"]["substituted_config"] == {
|
||||
"action": {
|
||||
"actions": {
|
||||
"entity_id": "light.kitchen",
|
||||
"service": "test.automation",
|
||||
},
|
||||
"trigger": {
|
||||
"triggers": {
|
||||
"event_type": "test_event",
|
||||
"platform": "event",
|
||||
},
|
||||
|
|
|
@ -78,7 +78,7 @@ async def test_update_automation_config(
|
|||
|
||||
resp = await client.post(
|
||||
"/api/config/automation/config/moon",
|
||||
data=json.dumps({"trigger": [], "action": [], "condition": []}),
|
||||
data=json.dumps({"triggers": [], "actions": [], "conditions": []}),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert sorted(hass.states.async_entity_ids("automation")) == [
|
||||
|
@ -91,8 +91,13 @@ async def test_update_automation_config(
|
|||
assert result == {"result": "ok"}
|
||||
|
||||
new_data = hass_config_store["automations.yaml"]
|
||||
assert list(new_data[1]) == ["id", "trigger", "condition", "action"]
|
||||
assert new_data[1] == {"id": "moon", "trigger": [], "condition": [], "action": []}
|
||||
assert list(new_data[1]) == ["id", "triggers", "conditions", "actions"]
|
||||
assert new_data[1] == {
|
||||
"id": "moon",
|
||||
"triggers": [],
|
||||
"conditions": [],
|
||||
"actions": [],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("automation_config", [{}])
|
||||
|
@ -101,7 +106,7 @@ async def test_update_automation_config(
|
|||
[
|
||||
(
|
||||
{"action": []},
|
||||
"required key not provided @ data['trigger']",
|
||||
"required key not provided @ data['triggers']",
|
||||
),
|
||||
(
|
||||
{
|
||||
|
@ -254,7 +259,7 @@ async def test_update_remove_key_automation_config(
|
|||
|
||||
resp = await client.post(
|
||||
"/api/config/automation/config/moon",
|
||||
data=json.dumps({"trigger": [], "action": [], "condition": []}),
|
||||
data=json.dumps({"triggers": [], "actions": [], "conditions": []}),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert sorted(hass.states.async_entity_ids("automation")) == [
|
||||
|
@ -267,8 +272,13 @@ async def test_update_remove_key_automation_config(
|
|||
assert result == {"result": "ok"}
|
||||
|
||||
new_data = hass_config_store["automations.yaml"]
|
||||
assert list(new_data[1]) == ["id", "trigger", "condition", "action"]
|
||||
assert new_data[1] == {"id": "moon", "trigger": [], "condition": [], "action": []}
|
||||
assert list(new_data[1]) == ["id", "triggers", "conditions", "actions"]
|
||||
assert new_data[1] == {
|
||||
"id": "moon",
|
||||
"triggers": [],
|
||||
"conditions": [],
|
||||
"actions": [],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("automation_config", [{}])
|
||||
|
@ -297,7 +307,7 @@ async def test_bad_formatted_automations(
|
|||
|
||||
resp = await client.post(
|
||||
"/api/config/automation/config/moon",
|
||||
data=json.dumps({"trigger": [], "action": [], "condition": []}),
|
||||
data=json.dumps({"triggers": [], "actions": [], "conditions": []}),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert sorted(hass.states.async_entity_ids("automation")) == [
|
||||
|
@ -312,7 +322,12 @@ async def test_bad_formatted_automations(
|
|||
# Verify ID added
|
||||
new_data = hass_config_store["automations.yaml"]
|
||||
assert "id" in new_data[0]
|
||||
assert new_data[1] == {"id": "moon", "trigger": [], "condition": [], "action": []}
|
||||
assert new_data[1] == {
|
||||
"id": "moon",
|
||||
"triggers": [],
|
||||
"conditions": [],
|
||||
"actions": [],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
|
@ -1307,7 +1307,7 @@ async def test_automation_with_bad_action(
|
|||
},
|
||||
)
|
||||
|
||||
assert expected_error.format(path="['action'][0]") in caplog.text
|
||||
assert expected_error.format(path="['actions'][0]") in caplog.text
|
||||
|
||||
|
||||
@patch("homeassistant.helpers.device_registry.DeviceEntry", MockDeviceEntry)
|
||||
|
@ -1341,7 +1341,7 @@ async def test_automation_with_bad_condition_action(
|
|||
},
|
||||
)
|
||||
|
||||
assert expected_error.format(path="['action'][0]") in caplog.text
|
||||
assert expected_error.format(path="['actions'][0]") in caplog.text
|
||||
|
||||
|
||||
@patch("homeassistant.helpers.device_registry.DeviceEntry", MockDeviceEntry)
|
||||
|
@ -1375,7 +1375,7 @@ async def test_automation_with_bad_condition(
|
|||
},
|
||||
)
|
||||
|
||||
assert expected_error.format(path="['condition'][0]") in caplog.text
|
||||
assert expected_error.format(path="['conditions'][0]") in caplog.text
|
||||
|
||||
|
||||
async def test_automation_with_sub_condition(
|
||||
|
@ -1541,7 +1541,7 @@ async def test_automation_with_bad_sub_condition(
|
|||
},
|
||||
)
|
||||
|
||||
path = "['condition'][0]['conditions'][0]"
|
||||
path = "['conditions'][0]['conditions'][0]"
|
||||
assert expected_error.format(path=path) in caplog.text
|
||||
|
||||
|
||||
|
|
|
@ -9,7 +9,11 @@ from unittest.mock import patch
|
|||
import pytest
|
||||
|
||||
from homeassistant.components import script
|
||||
from homeassistant.components.blueprint import Blueprint, DomainBlueprints
|
||||
from homeassistant.components.blueprint import (
|
||||
BLUEPRINT_SCHEMA,
|
||||
Blueprint,
|
||||
DomainBlueprints,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import Context, HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr, template
|
||||
|
@ -33,7 +37,10 @@ def patch_blueprint(blueprint_path: str, data_path: str) -> Iterator[None]:
|
|||
return orig_load(self, path)
|
||||
|
||||
return Blueprint(
|
||||
yaml.load_yaml(data_path), expected_domain=self.domain, path=path
|
||||
yaml.load_yaml(data_path),
|
||||
expected_domain=self.domain,
|
||||
path=path,
|
||||
schema=BLUEPRINT_SCHEMA,
|
||||
)
|
||||
|
||||
with patch(
|
||||
|
|
|
@ -250,7 +250,7 @@ async def test_search(
|
|||
{
|
||||
"id": "unique_id",
|
||||
"alias": "blueprint_automation_1",
|
||||
"trigger": {"platform": "template", "value_template": "true"},
|
||||
"triggers": {"platform": "template", "value_template": "true"},
|
||||
"use_blueprint": {
|
||||
"path": "test_event_service.yaml",
|
||||
"input": {
|
||||
|
@ -262,7 +262,7 @@ async def test_search(
|
|||
},
|
||||
{
|
||||
"alias": "blueprint_automation_2",
|
||||
"trigger": {"platform": "template", "value_template": "true"},
|
||||
"triggers": {"platform": "template", "value_template": "true"},
|
||||
"use_blueprint": {
|
||||
"path": "test_event_service.yaml",
|
||||
"input": {
|
||||
|
|
|
@ -47,7 +47,7 @@ async def _setup_automation_or_script(
|
|||
) -> None:
|
||||
"""Set up automations or scripts from automation config."""
|
||||
if domain == "script":
|
||||
configs = {config["id"]: {"sequence": config["action"]} for config in configs}
|
||||
configs = {config["id"]: {"sequence": config["actions"]} for config in configs}
|
||||
|
||||
if script_config:
|
||||
if domain == "automation":
|
||||
|
@ -85,7 +85,7 @@ async def _run_automation_or_script(
|
|||
|
||||
def _assert_raw_config(domain, config, trace):
|
||||
if domain == "script":
|
||||
config = {"sequence": config["action"]}
|
||||
config = {"sequence": config["actions"]}
|
||||
assert trace["config"] == config
|
||||
|
||||
|
||||
|
@ -152,20 +152,20 @@ async def test_get_trace(
|
|||
|
||||
sun_config = {
|
||||
"id": "sun",
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": {"service": "test.automation"},
|
||||
"triggers": {"platform": "event", "event_type": "test_event"},
|
||||
"actions": {"service": "test.automation"},
|
||||
}
|
||||
moon_config = {
|
||||
"id": "moon",
|
||||
"trigger": [
|
||||
"triggers": [
|
||||
{"platform": "event", "event_type": "test_event2"},
|
||||
{"platform": "event", "event_type": "test_event3"},
|
||||
],
|
||||
"condition": {
|
||||
"conditions": {
|
||||
"condition": "template",
|
||||
"value_template": "{{ trigger.event.event_type=='test_event2' }}",
|
||||
},
|
||||
"action": {"event": "another_event"},
|
||||
"actions": {"event": "another_event"},
|
||||
}
|
||||
|
||||
sun_action = {
|
||||
|
@ -551,13 +551,13 @@ async def test_trace_overflow(
|
|||
|
||||
sun_config = {
|
||||
"id": "sun",
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": {"event": "some_event"},
|
||||
"triggers": {"platform": "event", "event_type": "test_event"},
|
||||
"actions": {"event": "some_event"},
|
||||
}
|
||||
moon_config = {
|
||||
"id": "moon",
|
||||
"trigger": {"platform": "event", "event_type": "test_event2"},
|
||||
"action": {"event": "another_event"},
|
||||
"triggers": {"platform": "event", "event_type": "test_event2"},
|
||||
"actions": {"event": "another_event"},
|
||||
}
|
||||
await _setup_automation_or_script(
|
||||
hass, domain, [sun_config, moon_config], stored_traces=stored_traces
|
||||
|
@ -632,13 +632,13 @@ async def test_restore_traces_overflow(
|
|||
hass_storage["trace.saved_traces"] = saved_traces
|
||||
sun_config = {
|
||||
"id": "sun",
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": {"event": "some_event"},
|
||||
"triggers": {"platform": "event", "event_type": "test_event"},
|
||||
"actions": {"event": "some_event"},
|
||||
}
|
||||
moon_config = {
|
||||
"id": "moon",
|
||||
"trigger": {"platform": "event", "event_type": "test_event2"},
|
||||
"action": {"event": "another_event"},
|
||||
"triggers": {"platform": "event", "event_type": "test_event2"},
|
||||
"actions": {"event": "another_event"},
|
||||
}
|
||||
await _setup_automation_or_script(hass, domain, [sun_config, moon_config])
|
||||
await hass.async_start()
|
||||
|
@ -713,13 +713,13 @@ async def test_restore_traces_late_overflow(
|
|||
hass_storage["trace.saved_traces"] = saved_traces
|
||||
sun_config = {
|
||||
"id": "sun",
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": {"event": "some_event"},
|
||||
"triggers": {"platform": "event", "event_type": "test_event"},
|
||||
"actions": {"event": "some_event"},
|
||||
}
|
||||
moon_config = {
|
||||
"id": "moon",
|
||||
"trigger": {"platform": "event", "event_type": "test_event2"},
|
||||
"action": {"event": "another_event"},
|
||||
"triggers": {"platform": "event", "event_type": "test_event2"},
|
||||
"actions": {"event": "another_event"},
|
||||
}
|
||||
await _setup_automation_or_script(hass, domain, [sun_config, moon_config])
|
||||
await hass.async_start()
|
||||
|
@ -765,8 +765,8 @@ async def test_trace_no_traces(
|
|||
|
||||
sun_config = {
|
||||
"id": "sun",
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": {"event": "some_event"},
|
||||
"triggers": {"platform": "event", "event_type": "test_event"},
|
||||
"actions": {"event": "some_event"},
|
||||
}
|
||||
await _setup_automation_or_script(hass, domain, [sun_config], stored_traces=0)
|
||||
|
||||
|
@ -832,20 +832,20 @@ async def test_list_traces(
|
|||
|
||||
sun_config = {
|
||||
"id": "sun",
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": {"service": "test.automation"},
|
||||
"triggers": {"platform": "event", "event_type": "test_event"},
|
||||
"actions": {"service": "test.automation"},
|
||||
}
|
||||
moon_config = {
|
||||
"id": "moon",
|
||||
"trigger": [
|
||||
"triggers": [
|
||||
{"platform": "event", "event_type": "test_event2"},
|
||||
{"platform": "event", "event_type": "test_event3"},
|
||||
],
|
||||
"condition": {
|
||||
"conditions": {
|
||||
"condition": "template",
|
||||
"value_template": "{{ trigger.event.event_type=='test_event2' }}",
|
||||
},
|
||||
"action": {"event": "another_event"},
|
||||
"actions": {"event": "another_event"},
|
||||
}
|
||||
await _setup_automation_or_script(hass, domain, [sun_config, moon_config])
|
||||
|
||||
|
@ -965,8 +965,8 @@ async def test_nested_traces(
|
|||
|
||||
sun_config = {
|
||||
"id": "sun",
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": {"service": "script.moon"},
|
||||
"triggers": {"platform": "event", "event_type": "test_event"},
|
||||
"actions": {"service": "script.moon"},
|
||||
}
|
||||
moon_config = {"moon": {"sequence": {"event": "another_event"}}}
|
||||
await _setup_automation_or_script(hass, domain, [sun_config], moon_config)
|
||||
|
@ -1036,8 +1036,8 @@ async def test_breakpoints(
|
|||
|
||||
sun_config = {
|
||||
"id": "sun",
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": [
|
||||
"triggers": {"platform": "event", "event_type": "test_event"},
|
||||
"actions": [
|
||||
{"event": "event0"},
|
||||
{"event": "event1"},
|
||||
{"event": "event2"},
|
||||
|
@ -1206,8 +1206,8 @@ async def test_breakpoints_2(
|
|||
|
||||
sun_config = {
|
||||
"id": "sun",
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": [
|
||||
"triggers": {"platform": "event", "event_type": "test_event"},
|
||||
"actions": [
|
||||
{"event": "event0"},
|
||||
{"event": "event1"},
|
||||
{"event": "event2"},
|
||||
|
@ -1311,8 +1311,8 @@ async def test_breakpoints_3(
|
|||
|
||||
sun_config = {
|
||||
"id": "sun",
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": [
|
||||
"triggers": {"platform": "event", "event_type": "test_event"},
|
||||
"actions": [
|
||||
{"event": "event0"},
|
||||
{"event": "event1"},
|
||||
{"event": "event2"},
|
||||
|
|
|
@ -2566,18 +2566,18 @@ async def test_integration_setup_info(
|
|||
@pytest.mark.parametrize(
|
||||
("key", "config"),
|
||||
[
|
||||
("trigger", {"platform": "event", "event_type": "hello"}),
|
||||
("trigger", [{"platform": "event", "event_type": "hello"}]),
|
||||
("triggers", {"platform": "event", "event_type": "hello"}),
|
||||
("triggers", [{"platform": "event", "event_type": "hello"}]),
|
||||
(
|
||||
"condition",
|
||||
"conditions",
|
||||
{"condition": "state", "entity_id": "hello.world", "state": "paulus"},
|
||||
),
|
||||
(
|
||||
"condition",
|
||||
"conditions",
|
||||
[{"condition": "state", "entity_id": "hello.world", "state": "paulus"}],
|
||||
),
|
||||
("action", {"service": "domain_test.test_service"}),
|
||||
("action", [{"service": "domain_test.test_service"}]),
|
||||
("actions", {"service": "domain_test.test_service"}),
|
||||
("actions", [{"service": "domain_test.test_service"}]),
|
||||
],
|
||||
)
|
||||
async def test_validate_config_works(
|
||||
|
@ -2599,13 +2599,13 @@ async def test_validate_config_works(
|
|||
[
|
||||
# Raises vol.Invalid
|
||||
(
|
||||
"trigger",
|
||||
"triggers",
|
||||
{"platform": "non_existing", "event_type": "hello"},
|
||||
"Invalid platform 'non_existing' specified",
|
||||
),
|
||||
# Raises vol.Invalid
|
||||
(
|
||||
"condition",
|
||||
"conditions",
|
||||
{
|
||||
"condition": "non_existing",
|
||||
"entity_id": "hello.world",
|
||||
|
@ -2619,7 +2619,7 @@ async def test_validate_config_works(
|
|||
),
|
||||
# Raises HomeAssistantError
|
||||
(
|
||||
"condition",
|
||||
"conditions",
|
||||
{
|
||||
"above": 50,
|
||||
"condition": "device",
|
||||
|
@ -2632,7 +2632,7 @@ async def test_validate_config_works(
|
|||
),
|
||||
# Raises vol.Invalid
|
||||
(
|
||||
"action",
|
||||
"actions",
|
||||
{"non_existing": "domain_test.test_service"},
|
||||
"Unable to determine action @ data[0]",
|
||||
),
|
||||
|
|
|
@ -10,9 +10,9 @@ blueprint:
|
|||
selector:
|
||||
number:
|
||||
mode: "box"
|
||||
trigger:
|
||||
triggers:
|
||||
platform: event
|
||||
event_type: !input trigger_event
|
||||
action:
|
||||
actions:
|
||||
service: !input service_to_call
|
||||
entity_id: light.kitchen
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
blueprint:
|
||||
name: "Call service based on event"
|
||||
domain: automation
|
||||
input:
|
||||
trigger_event:
|
||||
selector:
|
||||
text:
|
||||
service_to_call:
|
||||
a_number:
|
||||
selector:
|
||||
number:
|
||||
mode: "box"
|
||||
trigger:
|
||||
platform: event
|
||||
event_type: !input trigger_event
|
||||
action:
|
||||
service: !input service_to_call
|
||||
entity_id: light.kitchen
|
Loading…
Reference in New Issue