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