Add Blueprint foundation to Scripts (#48621)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>pull/49794/head
parent
cd84595429
commit
89e7983ee0
|
@ -1,6 +1,9 @@
|
|||
"""Provide configuration end points for scripts."""
|
||||
from homeassistant.components.script import DOMAIN, SCRIPT_ENTRY_SCHEMA
|
||||
from homeassistant.components.script.config import async_validate_config_item
|
||||
from homeassistant.components.script import DOMAIN
|
||||
from homeassistant.components.script.config import (
|
||||
SCRIPT_ENTITY_SCHEMA,
|
||||
async_validate_config_item,
|
||||
)
|
||||
from homeassistant.config import SCRIPT_CONFIG_PATH
|
||||
from homeassistant.const import SERVICE_RELOAD
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
@ -21,7 +24,7 @@ async def async_setup(hass):
|
|||
"config",
|
||||
SCRIPT_CONFIG_PATH,
|
||||
cv.slug,
|
||||
SCRIPT_ENTRY_SCHEMA,
|
||||
SCRIPT_ENTITY_SCHEMA,
|
||||
post_write_hook=hook,
|
||||
data_validator=async_validate_config_item,
|
||||
)
|
||||
|
|
|
@ -3,21 +3,21 @@ from __future__ import annotations
|
|||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any, Dict, cast
|
||||
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from homeassistant.components.trace import TRACE_CONFIG_SCHEMA
|
||||
from homeassistant.components.blueprint import BlueprintInputs
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_MODE,
|
||||
ATTR_NAME,
|
||||
CONF_ALIAS,
|
||||
CONF_DEFAULT,
|
||||
CONF_DESCRIPTION,
|
||||
CONF_ICON,
|
||||
CONF_MODE,
|
||||
CONF_NAME,
|
||||
CONF_SELECTOR,
|
||||
CONF_SEQUENCE,
|
||||
CONF_VARIABLES,
|
||||
SERVICE_RELOAD,
|
||||
|
@ -27,6 +27,7 @@ from homeassistant.const import (
|
|||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import extract_domain_configs
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.config_validation import make_entity_service_schema
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
|
@ -36,63 +37,27 @@ from homeassistant.helpers.script import (
|
|||
ATTR_MAX,
|
||||
CONF_MAX,
|
||||
CONF_MAX_EXCEEDED,
|
||||
SCRIPT_MODE_SINGLE,
|
||||
Script,
|
||||
make_script_schema,
|
||||
)
|
||||
from homeassistant.helpers.selector import validate_selector
|
||||
from homeassistant.helpers.service import async_set_service_schema
|
||||
from homeassistant.helpers.trace import trace_get, trace_path
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
from .config import ScriptConfig, async_validate_config_item
|
||||
from .const import (
|
||||
ATTR_LAST_ACTION,
|
||||
ATTR_LAST_TRIGGERED,
|
||||
ATTR_VARIABLES,
|
||||
CONF_FIELDS,
|
||||
CONF_TRACE,
|
||||
DOMAIN,
|
||||
ENTITY_ID_FORMAT,
|
||||
EVENT_SCRIPT_STARTED,
|
||||
LOGGER,
|
||||
)
|
||||
from .helpers import async_get_blueprints
|
||||
from .trace import trace_script
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "script"
|
||||
|
||||
ATTR_LAST_ACTION = "last_action"
|
||||
ATTR_LAST_TRIGGERED = "last_triggered"
|
||||
ATTR_VARIABLES = "variables"
|
||||
|
||||
CONF_ADVANCED = "advanced"
|
||||
CONF_EXAMPLE = "example"
|
||||
CONF_FIELDS = "fields"
|
||||
CONF_REQUIRED = "required"
|
||||
CONF_TRACE = "trace"
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
|
||||
EVENT_SCRIPT_STARTED = "script_started"
|
||||
|
||||
|
||||
SCRIPT_ENTRY_SCHEMA = make_script_schema(
|
||||
{
|
||||
vol.Optional(CONF_ALIAS): cv.string,
|
||||
vol.Optional(CONF_TRACE, default={}): TRACE_CONFIG_SCHEMA,
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
vol.Required(CONF_SEQUENCE): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_DESCRIPTION, default=""): cv.string,
|
||||
vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA,
|
||||
vol.Optional(CONF_FIELDS, default={}): {
|
||||
cv.string: {
|
||||
vol.Optional(CONF_ADVANCED, default=False): cv.boolean,
|
||||
vol.Optional(CONF_DEFAULT): cv.match_all,
|
||||
vol.Optional(CONF_DESCRIPTION): cv.string,
|
||||
vol.Optional(CONF_EXAMPLE): cv.string,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_REQUIRED, default=False): cv.boolean,
|
||||
vol.Optional(CONF_SELECTOR): validate_selector,
|
||||
}
|
||||
},
|
||||
},
|
||||
SCRIPT_MODE_SINGLE,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{DOMAIN: cv.schema_with_slug_keys(SCRIPT_ENTRY_SCHEMA)}, extra=vol.ALLOW_EXTRA
|
||||
)
|
||||
|
||||
SCRIPT_SERVICE_SCHEMA = vol.Schema(dict)
|
||||
SCRIPT_TURN_ONOFF_SCHEMA = make_entity_service_schema(
|
||||
{vol.Optional(ATTR_VARIABLES): {str: cv.match_all}}
|
||||
|
@ -201,9 +166,13 @@ def areas_in_script(hass: HomeAssistant, entity_id: str) -> list[str]:
|
|||
|
||||
async def async_setup(hass, config):
|
||||
"""Load the scripts from the configuration."""
|
||||
hass.data[DOMAIN] = component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||
hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass)
|
||||
|
||||
await _async_process_config(hass, config, component)
|
||||
# To register scripts as valid domain for Blueprint
|
||||
async_get_blueprints(hass)
|
||||
|
||||
if not await _async_process_config(hass, config, component):
|
||||
await async_get_blueprints(hass).async_populate()
|
||||
|
||||
async def reload_service(service):
|
||||
"""Call a service to reload scripts."""
|
||||
|
@ -257,8 +226,50 @@ async def async_setup(hass, config):
|
|||
return True
|
||||
|
||||
|
||||
async def _async_process_config(hass, config, component):
|
||||
"""Process script configuration."""
|
||||
async def _async_process_config(hass, config, component) -> bool:
|
||||
"""Process script configuration.
|
||||
|
||||
Return true, if Blueprints were used.
|
||||
"""
|
||||
entities = []
|
||||
blueprints_used = False
|
||||
|
||||
for config_key in extract_domain_configs(config, DOMAIN):
|
||||
conf: dict[str, dict[str, Any] | BlueprintInputs] = config[config_key]
|
||||
|
||||
for object_id, config_block in conf.items():
|
||||
raw_blueprint_inputs = None
|
||||
raw_config = None
|
||||
|
||||
if isinstance(config_block, BlueprintInputs):
|
||||
blueprints_used = True
|
||||
blueprint_inputs = config_block
|
||||
raw_blueprint_inputs = blueprint_inputs.config_with_inputs
|
||||
|
||||
try:
|
||||
raw_config = blueprint_inputs.async_substitute()
|
||||
config_block = cast(
|
||||
Dict[str, Any],
|
||||
await async_validate_config_item(hass, raw_config),
|
||||
)
|
||||
except vol.Invalid as err:
|
||||
LOGGER.error(
|
||||
"Blueprint %s generated invalid script with input %s: %s",
|
||||
blueprint_inputs.blueprint.name,
|
||||
blueprint_inputs.inputs,
|
||||
humanize_error(config_block, err),
|
||||
)
|
||||
continue
|
||||
else:
|
||||
raw_config = cast(ScriptConfig, config_block).raw_config
|
||||
|
||||
entities.append(
|
||||
ScriptEntity(
|
||||
hass, object_id, config_block, raw_config, raw_blueprint_inputs
|
||||
)
|
||||
)
|
||||
|
||||
await component.async_add_entities(entities)
|
||||
|
||||
async def service_handler(service):
|
||||
"""Execute a service call to script.<script name>."""
|
||||
|
@ -268,33 +279,21 @@ async def _async_process_config(hass, config, component):
|
|||
variables=service.data, context=service.context
|
||||
)
|
||||
|
||||
script_entities = [
|
||||
ScriptEntity(hass, object_id, cfg, cfg.raw_config)
|
||||
for object_id, cfg in config.get(DOMAIN, {}).items()
|
||||
]
|
||||
|
||||
await component.async_add_entities(script_entities)
|
||||
|
||||
# Register services for all entities that were created successfully.
|
||||
for script_entity in script_entities:
|
||||
object_id = script_entity.object_id
|
||||
if component.get_entity(script_entity.entity_id) is None:
|
||||
_LOGGER.error("Couldn't load script %s", object_id)
|
||||
continue
|
||||
|
||||
cfg = config[DOMAIN][object_id]
|
||||
|
||||
for entity in entities:
|
||||
hass.services.async_register(
|
||||
DOMAIN, object_id, service_handler, schema=SCRIPT_SERVICE_SCHEMA
|
||||
DOMAIN, entity.object_id, service_handler, schema=SCRIPT_SERVICE_SCHEMA
|
||||
)
|
||||
|
||||
# Register the service description
|
||||
service_desc = {
|
||||
CONF_NAME: script_entity.name,
|
||||
CONF_DESCRIPTION: cfg[CONF_DESCRIPTION],
|
||||
CONF_FIELDS: cfg[CONF_FIELDS],
|
||||
CONF_NAME: entity.name,
|
||||
CONF_DESCRIPTION: entity.description,
|
||||
CONF_FIELDS: entity.fields,
|
||||
}
|
||||
async_set_service_schema(hass, DOMAIN, object_id, service_desc)
|
||||
async_set_service_schema(hass, DOMAIN, entity.object_id, service_desc)
|
||||
|
||||
return blueprints_used
|
||||
|
||||
|
||||
class ScriptEntity(ToggleEntity):
|
||||
|
@ -302,10 +301,13 @@ class ScriptEntity(ToggleEntity):
|
|||
|
||||
icon = None
|
||||
|
||||
def __init__(self, hass, object_id, cfg, raw_config):
|
||||
def __init__(self, hass, object_id, cfg, raw_config, blueprint_inputs):
|
||||
"""Initialize the script."""
|
||||
self.object_id = object_id
|
||||
self.icon = cfg.get(CONF_ICON)
|
||||
self.description = cfg[CONF_DESCRIPTION]
|
||||
self.fields = cfg[CONF_FIELDS]
|
||||
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
||||
self.script = Script(
|
||||
hass,
|
||||
|
@ -323,6 +325,7 @@ class ScriptEntity(ToggleEntity):
|
|||
self._changed = asyncio.Event()
|
||||
self._raw_config = raw_config
|
||||
self._trace_config = cfg[CONF_TRACE]
|
||||
self._blueprint_inputs = blueprint_inputs
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
@ -388,7 +391,12 @@ class ScriptEntity(ToggleEntity):
|
|||
|
||||
async def _async_run(self, variables, context):
|
||||
with trace_script(
|
||||
self.hass, self.object_id, self._raw_config, context, self._trace_config
|
||||
self.hass,
|
||||
self.object_id,
|
||||
self._raw_config,
|
||||
self._blueprint_inputs,
|
||||
context,
|
||||
self._trace_config,
|
||||
) as script_trace:
|
||||
# Prepare tracing the execution of the script's sequence
|
||||
script_trace.set_trace(trace_get())
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
blueprint:
|
||||
name: Confirmable Notification
|
||||
description: >-
|
||||
A script that sends an actionable notification with a confirmation before
|
||||
running the specified action.
|
||||
domain: script
|
||||
source_url: https://github.com/home-assistant/core/blob/master/homeassistant/components/script/blueprints/confirmable_notification.yaml
|
||||
input:
|
||||
notify_device:
|
||||
name: Device to notify
|
||||
description: Device needs to run the official Home Assistant app to receive notifications.
|
||||
selector:
|
||||
device:
|
||||
integration: mobile_app
|
||||
title:
|
||||
name: "Title"
|
||||
description: "The title of the button shown in the notification."
|
||||
default: ""
|
||||
selector:
|
||||
text:
|
||||
message:
|
||||
name: "Message"
|
||||
description: "The message body"
|
||||
selector:
|
||||
text:
|
||||
confirm_text:
|
||||
name: "Confirmation Text"
|
||||
description: "Text to show on the confirmation button"
|
||||
default: "Confirm"
|
||||
selector:
|
||||
text:
|
||||
confirm_action:
|
||||
name: "Confirmation Action"
|
||||
description: "Action to run when notification is confirmed"
|
||||
default: []
|
||||
selector:
|
||||
action:
|
||||
dismiss_text:
|
||||
name: "Dismiss Text"
|
||||
description: "Text to show on the dismiss button"
|
||||
default: "Dismiss"
|
||||
selector:
|
||||
text:
|
||||
dismiss_action:
|
||||
name: "Dismiss Action"
|
||||
description: "Action to run when notification is dismissed"
|
||||
default: []
|
||||
selector:
|
||||
action:
|
||||
|
||||
mode: restart
|
||||
|
||||
sequence:
|
||||
- alias: "Send notification"
|
||||
domain: mobile_app
|
||||
type: notify
|
||||
device_id: !input notify_device
|
||||
title: !input title
|
||||
message: !input message
|
||||
data:
|
||||
actions:
|
||||
- action: "CONFIRM"
|
||||
title: !input confirm_text
|
||||
- action: "DISMISS"
|
||||
title: !input dismiss_text
|
||||
- alias: "Awaiting response"
|
||||
wait_for_trigger:
|
||||
- platform: event
|
||||
event_type: mobile_app_notification_action
|
||||
- choose:
|
||||
- conditions: "{{ wait.trigger.event.data.action == 'CONFIRM' }}"
|
||||
sequence: !input confirm_action
|
||||
- conditions: "{{ wait.trigger.event.data.action == 'DISMISS' }}"
|
||||
sequence: !input dismiss_action
|
|
@ -1,20 +1,75 @@
|
|||
"""Config validation helper for the script integration."""
|
||||
import asyncio
|
||||
from contextlib import suppress
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config import async_log_exception
|
||||
from homeassistant.const import CONF_SEQUENCE
|
||||
from homeassistant.components.blueprint import (
|
||||
BlueprintInputs,
|
||||
is_blueprint_instance_config,
|
||||
)
|
||||
from homeassistant.components.trace import TRACE_CONFIG_SCHEMA
|
||||
from homeassistant.config import async_log_exception, config_without_domain
|
||||
from homeassistant.const import (
|
||||
CONF_ALIAS,
|
||||
CONF_DEFAULT,
|
||||
CONF_DESCRIPTION,
|
||||
CONF_ICON,
|
||||
CONF_NAME,
|
||||
CONF_SELECTOR,
|
||||
CONF_SEQUENCE,
|
||||
CONF_VARIABLES,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.script import async_validate_action_config
|
||||
from homeassistant.helpers import config_per_platform, config_validation as cv
|
||||
from homeassistant.helpers.script import (
|
||||
SCRIPT_MODE_SINGLE,
|
||||
async_validate_action_config,
|
||||
make_script_schema,
|
||||
)
|
||||
from homeassistant.helpers.selector import validate_selector
|
||||
|
||||
from . import DOMAIN, SCRIPT_ENTRY_SCHEMA
|
||||
from .const import (
|
||||
CONF_ADVANCED,
|
||||
CONF_EXAMPLE,
|
||||
CONF_FIELDS,
|
||||
CONF_REQUIRED,
|
||||
CONF_TRACE,
|
||||
DOMAIN,
|
||||
)
|
||||
from .helpers import async_get_blueprints
|
||||
|
||||
SCRIPT_ENTITY_SCHEMA = make_script_schema(
|
||||
{
|
||||
vol.Optional(CONF_ALIAS): cv.string,
|
||||
vol.Optional(CONF_TRACE, default={}): TRACE_CONFIG_SCHEMA,
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
vol.Required(CONF_SEQUENCE): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_DESCRIPTION, default=""): cv.string,
|
||||
vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA,
|
||||
vol.Optional(CONF_FIELDS, default={}): {
|
||||
cv.string: {
|
||||
vol.Optional(CONF_ADVANCED, default=False): cv.boolean,
|
||||
vol.Optional(CONF_DEFAULT): cv.match_all,
|
||||
vol.Optional(CONF_DESCRIPTION): cv.string,
|
||||
vol.Optional(CONF_EXAMPLE): cv.string,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_REQUIRED, default=False): cv.boolean,
|
||||
vol.Optional(CONF_SELECTOR): validate_selector,
|
||||
}
|
||||
},
|
||||
},
|
||||
SCRIPT_MODE_SINGLE,
|
||||
)
|
||||
|
||||
|
||||
async def async_validate_config_item(hass, config, full_config=None):
|
||||
"""Validate config item."""
|
||||
config = SCRIPT_ENTRY_SCHEMA(config)
|
||||
if is_blueprint_instance_config(config):
|
||||
blueprints = async_get_blueprints(hass)
|
||||
return await blueprints.async_inputs_from_config(config)
|
||||
|
||||
config = SCRIPT_ENTITY_SCHEMA(config)
|
||||
config[CONF_SEQUENCE] = await asyncio.gather(
|
||||
*[
|
||||
async_validate_action_config(hass, action)
|
||||
|
@ -34,11 +89,8 @@ class ScriptConfig(dict):
|
|||
async def _try_async_validate_config_item(hass, object_id, config, full_config=None):
|
||||
"""Validate config item."""
|
||||
raw_config = None
|
||||
try:
|
||||
with suppress(ValueError): # Invalid config
|
||||
raw_config = dict(config)
|
||||
except ValueError:
|
||||
# Invalid config
|
||||
pass
|
||||
|
||||
try:
|
||||
cv.slug(object_id)
|
||||
|
@ -47,6 +99,9 @@ async def _try_async_validate_config_item(hass, object_id, config, full_config=N
|
|||
async_log_exception(ex, DOMAIN, full_config or config, hass)
|
||||
return None
|
||||
|
||||
if isinstance(config, BlueprintInputs):
|
||||
return config
|
||||
|
||||
config = ScriptConfig(config)
|
||||
config.raw_config = raw_config
|
||||
return config
|
||||
|
@ -54,12 +109,16 @@ async def _try_async_validate_config_item(hass, object_id, config, full_config=N
|
|||
|
||||
async def async_validate_config(hass, config):
|
||||
"""Validate config."""
|
||||
if DOMAIN in config:
|
||||
validated_config = {}
|
||||
for object_id, cfg in config[DOMAIN].items():
|
||||
scripts = {}
|
||||
for _, p_config in config_per_platform(config, DOMAIN):
|
||||
for object_id, cfg in p_config.items():
|
||||
cfg = await _try_async_validate_config_item(hass, object_id, cfg, config)
|
||||
if cfg is not None:
|
||||
validated_config[object_id] = cfg
|
||||
config[DOMAIN] = validated_config
|
||||
scripts[object_id] = cfg
|
||||
|
||||
# Create a copy of the configuration with all config for current
|
||||
# component removed and add validated config back in.
|
||||
config = config_without_domain(config, DOMAIN)
|
||||
config[DOMAIN] = scripts
|
||||
|
||||
return config
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
"""Constants for the script integration."""
|
||||
import logging
|
||||
|
||||
DOMAIN = "script"
|
||||
|
||||
ATTR_LAST_ACTION = "last_action"
|
||||
ATTR_LAST_TRIGGERED = "last_triggered"
|
||||
ATTR_VARIABLES = "variables"
|
||||
|
||||
CONF_ADVANCED = "advanced"
|
||||
CONF_EXAMPLE = "example"
|
||||
CONF_FIELDS = "fields"
|
||||
CONF_REQUIRED = "required"
|
||||
CONF_TRACE = "trace"
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
|
||||
EVENT_SCRIPT_STARTED = "script_started"
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
|
@ -0,0 +1,15 @@
|
|||
"""Helpers for automation integration."""
|
||||
from homeassistant.components.blueprint import DomainBlueprints
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
DATA_BLUEPRINTS = "script_blueprints"
|
||||
|
||||
|
||||
@singleton(DATA_BLUEPRINTS)
|
||||
@callback
|
||||
def async_get_blueprints(hass: HomeAssistant) -> DomainBlueprints:
|
||||
"""Get script blueprints."""
|
||||
return DomainBlueprints(hass, DOMAIN, LOGGER)
|
|
@ -2,7 +2,7 @@
|
|||
"domain": "script",
|
||||
"name": "Scripts",
|
||||
"documentation": "https://www.home-assistant.io/integrations/script",
|
||||
"dependencies": ["trace"],
|
||||
"dependencies": ["blueprint", "trace"],
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"quality_scale": "internal"
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
"""Trace support for script."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.trace import ActionTrace, async_store_trace
|
||||
from homeassistant.components.trace.const import CONF_STORED_TRACES
|
||||
from homeassistant.core import Context
|
||||
from homeassistant.core import Context, HomeAssistant
|
||||
|
||||
|
||||
class ScriptTrace(ActionTrace):
|
||||
|
@ -16,17 +17,25 @@ class ScriptTrace(ActionTrace):
|
|||
self,
|
||||
item_id: str,
|
||||
config: dict[str, Any],
|
||||
blueprint_inputs: dict[str, Any],
|
||||
context: Context,
|
||||
):
|
||||
) -> None:
|
||||
"""Container for automation trace."""
|
||||
key = ("script", item_id)
|
||||
super().__init__(key, config, None, context)
|
||||
super().__init__(key, config, blueprint_inputs, context)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def trace_script(hass, item_id, config, context, trace_config):
|
||||
def trace_script(
|
||||
hass: HomeAssistant,
|
||||
item_id: str,
|
||||
config: dict[str, Any],
|
||||
blueprint_inputs: dict[str, Any],
|
||||
context: Context,
|
||||
trace_config: dict[str, Any],
|
||||
) -> Iterator[ScriptTrace]:
|
||||
"""Trace execution of a script."""
|
||||
trace = ScriptTrace(item_id, config, context)
|
||||
trace = ScriptTrace(item_id, config, blueprint_inputs, context)
|
||||
async_store_trace(hass, trace, trace_config[CONF_STORED_TRACES])
|
||||
|
||||
try:
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
"""demo conftest."""
|
||||
from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
|
||||
from tests.components.light.conftest import mock_light_profiles # noqa: F401
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
"""Conftest for emulated_hue tests."""
|
||||
|
||||
from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
|
|
@ -0,0 +1,3 @@
|
|||
"""Conftest for script tests."""
|
||||
|
||||
from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
|
|
@ -1,2 +1,3 @@
|
|||
"""Test fixtures for mqtt component."""
|
||||
from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
|
||||
from tests.components.light.conftest import mock_light_profiles # noqa: F401
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
"""Conftest for script tests."""
|
||||
|
||||
from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
|
|
@ -0,0 +1,114 @@
|
|||
"""Test script blueprints."""
|
||||
import asyncio
|
||||
import contextlib
|
||||
import pathlib
|
||||
from typing import Iterator
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components import script
|
||||
from homeassistant.components.blueprint.models import Blueprint, DomainBlueprints
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import yaml
|
||||
|
||||
from tests.common import async_mock_service
|
||||
|
||||
BUILTIN_BLUEPRINT_FOLDER = pathlib.Path(script.__file__).parent / "blueprints"
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def patch_blueprint(blueprint_path: str, data_path: str) -> Iterator[None]:
|
||||
"""Patch blueprint loading from a different source."""
|
||||
orig_load = DomainBlueprints._load_blueprint
|
||||
|
||||
@callback
|
||||
def mock_load_blueprint(self, path: str) -> Blueprint:
|
||||
if path != blueprint_path:
|
||||
assert False, f"Unexpected blueprint {path}"
|
||||
return orig_load(self, path)
|
||||
|
||||
return Blueprint(
|
||||
yaml.load_yaml(data_path), expected_domain=self.domain, path=path
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.blueprint.models.DomainBlueprints._load_blueprint",
|
||||
mock_load_blueprint,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
async def test_confirmable_notification(hass: HomeAssistant) -> None:
|
||||
"""Test confirmable notification blueprint."""
|
||||
with patch_blueprint(
|
||||
"confirmable_notification.yaml",
|
||||
BUILTIN_BLUEPRINT_FOLDER / "confirmable_notification.yaml",
|
||||
):
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
script.DOMAIN,
|
||||
{
|
||||
"script": {
|
||||
"confirm": {
|
||||
"use_blueprint": {
|
||||
"path": "confirmable_notification.yaml",
|
||||
"input": {
|
||||
"notify_device": "frodo",
|
||||
"title": "Lord of the things",
|
||||
"message": "Throw ring in mountain?",
|
||||
"confirm_action": [
|
||||
{
|
||||
"service": "homeassistant.turn_on",
|
||||
"target": {"entity_id": "mount.doom"},
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
turn_on_calls = async_mock_service(hass, "homeassistant", "turn_on")
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.mobile_app.device_action.async_call_action_from_config"
|
||||
) as mock_call_action:
|
||||
|
||||
# Trigger script
|
||||
await hass.services.async_call(script.DOMAIN, "confirm")
|
||||
|
||||
# Give script the time to attach the trigger.
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
hass.bus.async_fire("mobile_app_notification_action", {"action": "CONFIRM"})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_call_action.mock_calls) == 1
|
||||
_hass, config, variables, _context = mock_call_action.mock_calls[0][1]
|
||||
|
||||
title_tpl = config.pop("title")
|
||||
message_tpl = config.pop("message")
|
||||
title_tpl.hass = hass
|
||||
message_tpl.hass = hass
|
||||
|
||||
assert config == {
|
||||
"alias": "Send notification",
|
||||
"domain": "mobile_app",
|
||||
"type": "notify",
|
||||
"device_id": "frodo",
|
||||
"data": {
|
||||
"actions": [
|
||||
{"action": "CONFIRM", "title": "Confirm"},
|
||||
{"action": "DISMISS", "title": "Dismiss"},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
assert title_tpl.async_render(variables) == "Lord of the things"
|
||||
assert message_tpl.async_render(variables) == "Throw ring in mountain?"
|
||||
|
||||
assert len(turn_on_calls) == 1
|
||||
assert turn_on_calls[0].data == {
|
||||
"entity_id": ["mount.doom"],
|
||||
}
|
|
@ -518,6 +518,36 @@ async def test_config_basic(hass):
|
|||
assert test_script.attributes["icon"] == "mdi:party"
|
||||
|
||||
|
||||
async def test_config_multiple_domains(hass):
|
||||
"""Test splitting configuration over multiple domains."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
"script",
|
||||
{
|
||||
"script": {
|
||||
"first_script": {
|
||||
"alias": "Main domain",
|
||||
"sequence": [],
|
||||
}
|
||||
},
|
||||
"script second": {
|
||||
"second_script": {
|
||||
"alias": "Secondary domain",
|
||||
"sequence": [],
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
test_script = hass.states.get("script.first_script")
|
||||
assert test_script
|
||||
assert test_script.name == "Main domain"
|
||||
|
||||
test_script = hass.states.get("script.second_script")
|
||||
assert test_script
|
||||
assert test_script.name == "Secondary domain"
|
||||
|
||||
|
||||
async def test_logbook_humanify_script_started_event(hass):
|
||||
"""Test humanifying script started event."""
|
||||
hass.config.components.add("recorder")
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
"""Conftest for trace tests."""
|
||||
|
||||
from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
|
|
@ -780,7 +780,6 @@ async def test_merge_id_schema(hass):
|
|||
types = {
|
||||
"panel_custom": "list",
|
||||
"group": "dict",
|
||||
"script": "dict",
|
||||
"input_boolean": "dict",
|
||||
"shell_command": "dict",
|
||||
"qwikswitch": "dict",
|
||||
|
|
Loading…
Reference in New Issue