mirror of
https://github.com/home-assistant/core.git
synced 2025-04-27 18:57:57 +00:00
Enhance automation integration to use new features in script helper (#37479)
This commit is contained in:
parent
c3b5bf7437
commit
f7c4900d5c
@ -9,10 +9,13 @@ import voluptuous as vol
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_NAME,
|
||||
CONF_ALIAS,
|
||||
CONF_DEVICE_ID,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_ID,
|
||||
CONF_MODE,
|
||||
CONF_PLATFORM,
|
||||
CONF_QUEUE_SIZE,
|
||||
CONF_ZONE,
|
||||
EVENT_HOMEASSISTANT_STARTED,
|
||||
SERVICE_RELOAD,
|
||||
@ -23,11 +26,12 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import Context, CoreState, HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import condition, extract_domain_configs, script
|
||||
from homeassistant.helpers import condition, extract_domain_configs
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.script import SCRIPT_BASE_SCHEMA, Script, validate_queue_size
|
||||
from homeassistant.helpers.service import async_register_admin_service
|
||||
from homeassistant.helpers.typing import TemplateVarsType
|
||||
from homeassistant.loader import bind_hass
|
||||
@ -41,7 +45,6 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
|
||||
GROUP_NAME_ALL_AUTOMATIONS = "all automations"
|
||||
|
||||
CONF_ALIAS = "alias"
|
||||
CONF_DESCRIPTION = "description"
|
||||
CONF_HIDE_ENTITY = "hide_entity"
|
||||
|
||||
@ -96,7 +99,7 @@ _CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA])
|
||||
|
||||
PLATFORM_SCHEMA = vol.All(
|
||||
cv.deprecated(CONF_HIDE_ENTITY, invalidation_version="0.110"),
|
||||
vol.Schema(
|
||||
SCRIPT_BASE_SCHEMA.extend(
|
||||
{
|
||||
# str on purpose
|
||||
CONF_ID: str,
|
||||
@ -109,6 +112,7 @@ PLATFORM_SCHEMA = vol.All(
|
||||
vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
}
|
||||
),
|
||||
validate_queue_size,
|
||||
)
|
||||
|
||||
|
||||
@ -389,7 +393,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
||||
try:
|
||||
await self.action_script.async_run(variables, trigger_context)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
_LOGGER.exception("While executing automation %s", self.entity_id)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Remove listeners when removing automation from Home Assistant."""
|
||||
@ -498,8 +502,13 @@ async def _async_process_config(hass, config, component):
|
||||
|
||||
initial_state = config_block.get(CONF_INITIAL_STATE)
|
||||
|
||||
action_script = script.Script(
|
||||
hass, config_block.get(CONF_ACTION, {}), name, logger=_LOGGER
|
||||
action_script = Script(
|
||||
hass,
|
||||
config_block[CONF_ACTION],
|
||||
name,
|
||||
script_mode=config_block[CONF_MODE],
|
||||
queue_size=config_block.get(CONF_QUEUE_SIZE, 0),
|
||||
logger=_LOGGER,
|
||||
)
|
||||
|
||||
if CONF_CONDITION in config_block:
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Config validation helper for the automation integration."""
|
||||
import asyncio
|
||||
import importlib
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@ -8,13 +9,20 @@ from homeassistant.components.device_automation.exceptions import (
|
||||
InvalidDeviceAutomationConfig,
|
||||
)
|
||||
from homeassistant.config import async_log_exception, config_without_domain
|
||||
from homeassistant.const import CONF_PLATFORM
|
||||
from homeassistant.const import CONF_ALIAS, CONF_ID, CONF_MODE, CONF_PLATFORM
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import condition, config_per_platform, script
|
||||
from homeassistant.helpers import condition, config_per_platform
|
||||
from homeassistant.helpers.script import (
|
||||
SCRIPT_MODE_LEGACY,
|
||||
async_validate_action_config,
|
||||
warn_deprecated_legacy,
|
||||
)
|
||||
from homeassistant.loader import IntegrationNotFound
|
||||
|
||||
from . import CONF_ACTION, CONF_CONDITION, CONF_TRIGGER, DOMAIN, PLATFORM_SCHEMA
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# mypy: allow-untyped-calls, allow-untyped-defs
|
||||
# mypy: no-check-untyped-defs, no-warn-return-any
|
||||
|
||||
@ -44,10 +52,7 @@ async def async_validate_config_item(hass, config, full_config=None):
|
||||
)
|
||||
|
||||
config[CONF_ACTION] = await asyncio.gather(
|
||||
*[
|
||||
script.async_validate_action_config(hass, action)
|
||||
for action in config[CONF_ACTION]
|
||||
]
|
||||
*[async_validate_action_config(hass, action) for action in config[CONF_ACTION]]
|
||||
)
|
||||
|
||||
return config
|
||||
@ -69,24 +74,54 @@ async def _try_async_validate_config_item(hass, config, full_config=None):
|
||||
return config
|
||||
|
||||
|
||||
def _deprecated_legacy_mode(config):
|
||||
legacy_names = []
|
||||
legacy_unnamed_found = False
|
||||
|
||||
for cfg in config[DOMAIN]:
|
||||
mode = cfg.get(CONF_MODE)
|
||||
if mode is None:
|
||||
cfg[CONF_MODE] = SCRIPT_MODE_LEGACY
|
||||
name = cfg.get(CONF_ID) or cfg.get(CONF_ALIAS)
|
||||
if name:
|
||||
legacy_names.append(name)
|
||||
else:
|
||||
legacy_unnamed_found = True
|
||||
|
||||
if legacy_names or legacy_unnamed_found:
|
||||
msgs = []
|
||||
if legacy_unnamed_found:
|
||||
msgs.append("unnamed automations")
|
||||
if legacy_names:
|
||||
if len(legacy_names) == 1:
|
||||
base_msg = "this automation"
|
||||
else:
|
||||
base_msg = "these automations"
|
||||
msgs.append(f"{base_msg}: {', '.join(legacy_names)}")
|
||||
warn_deprecated_legacy(_LOGGER, " and ".join(msgs))
|
||||
|
||||
return config
|
||||
|
||||
|
||||
async def async_validate_config(hass, config):
|
||||
"""Validate config."""
|
||||
validated_automations = await asyncio.gather(
|
||||
*(
|
||||
_try_async_validate_config_item(hass, p_config, config)
|
||||
for _, p_config in config_per_platform(config, DOMAIN)
|
||||
automations = list(
|
||||
filter(
|
||||
lambda x: x is not None,
|
||||
await asyncio.gather(
|
||||
*(
|
||||
_try_async_validate_config_item(hass, p_config, config)
|
||||
for _, p_config in config_per_platform(config, DOMAIN)
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
automations = [
|
||||
validated_automation
|
||||
for validated_automation in validated_automations
|
||||
if validated_automation is not None
|
||||
]
|
||||
|
||||
# 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] = automations
|
||||
|
||||
_deprecated_legacy_mode(config)
|
||||
|
||||
return config
|
||||
|
@ -11,6 +11,7 @@ from homeassistant.const import (
|
||||
CONF_ALIAS,
|
||||
CONF_ICON,
|
||||
CONF_MODE,
|
||||
CONF_QUEUE_SIZE,
|
||||
SERVICE_RELOAD,
|
||||
SERVICE_TOGGLE,
|
||||
SERVICE_TURN_OFF,
|
||||
@ -23,11 +24,11 @@ from homeassistant.helpers.config_validation import make_entity_service_schema
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.script import (
|
||||
DEFAULT_QUEUE_MAX,
|
||||
SCRIPT_MODE_CHOICES,
|
||||
SCRIPT_BASE_SCHEMA,
|
||||
SCRIPT_MODE_LEGACY,
|
||||
SCRIPT_MODE_QUEUE,
|
||||
Script,
|
||||
validate_queue_size,
|
||||
warn_deprecated_legacy,
|
||||
)
|
||||
from homeassistant.helpers.service import async_set_service_schema
|
||||
from homeassistant.loader import bind_hass
|
||||
@ -44,7 +45,6 @@ CONF_DESCRIPTION = "description"
|
||||
CONF_EXAMPLE = "example"
|
||||
CONF_FIELDS = "fields"
|
||||
CONF_SEQUENCE = "sequence"
|
||||
CONF_QUEUE_MAX = "queue_size"
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
|
||||
@ -59,55 +59,32 @@ def _deprecated_legacy_mode(config):
|
||||
legacy_scripts.append(object_id)
|
||||
cfg[CONF_MODE] = SCRIPT_MODE_LEGACY
|
||||
if legacy_scripts:
|
||||
_LOGGER.warning(
|
||||
"Script behavior has changed. "
|
||||
"To continue using previous behavior, which is now deprecated, "
|
||||
"add '%s: %s' to script(s): %s.",
|
||||
CONF_MODE,
|
||||
SCRIPT_MODE_LEGACY,
|
||||
", ".join(legacy_scripts),
|
||||
)
|
||||
warn_deprecated_legacy(_LOGGER, f"script(s): {', '.join(legacy_scripts)}")
|
||||
return config
|
||||
|
||||
|
||||
def _queue_max(config):
|
||||
for object_id, cfg in config.items():
|
||||
mode = cfg[CONF_MODE]
|
||||
queue_max = cfg.get(CONF_QUEUE_MAX)
|
||||
if mode == SCRIPT_MODE_QUEUE:
|
||||
if queue_max is None:
|
||||
cfg[CONF_QUEUE_MAX] = DEFAULT_QUEUE_MAX
|
||||
elif queue_max is not None:
|
||||
raise vol.Invalid(
|
||||
f"{CONF_QUEUE_MAX} not valid with {mode} {CONF_MODE} "
|
||||
f"for script '{object_id}'"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
SCRIPT_ENTRY_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_ALIAS): cv.string,
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
vol.Required(CONF_SEQUENCE): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_DESCRIPTION, default=""): cv.string,
|
||||
vol.Optional(CONF_FIELDS, default={}): {
|
||||
cv.string: {
|
||||
vol.Optional(CONF_DESCRIPTION): cv.string,
|
||||
vol.Optional(CONF_EXAMPLE): cv.string,
|
||||
}
|
||||
},
|
||||
vol.Optional(CONF_MODE): vol.In(SCRIPT_MODE_CHOICES),
|
||||
vol.Optional(CONF_QUEUE_MAX): vol.All(vol.Coerce(int), vol.Range(min=2)),
|
||||
}
|
||||
SCRIPT_ENTRY_SCHEMA = vol.All(
|
||||
SCRIPT_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_ALIAS): cv.string,
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
vol.Required(CONF_SEQUENCE): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_DESCRIPTION, default=""): cv.string,
|
||||
vol.Optional(CONF_FIELDS, default={}): {
|
||||
cv.string: {
|
||||
vol.Optional(CONF_DESCRIPTION): cv.string,
|
||||
vol.Optional(CONF_EXAMPLE): cv.string,
|
||||
}
|
||||
},
|
||||
}
|
||||
),
|
||||
validate_queue_size,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.All(
|
||||
cv.schema_with_slug_keys(SCRIPT_ENTRY_SCHEMA),
|
||||
_deprecated_legacy_mode,
|
||||
_queue_max,
|
||||
cv.schema_with_slug_keys(SCRIPT_ENTRY_SCHEMA), _deprecated_legacy_mode
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
@ -271,7 +248,7 @@ async def _async_process_config(hass, config, component):
|
||||
cfg.get(CONF_ICON),
|
||||
cfg[CONF_SEQUENCE],
|
||||
cfg[CONF_MODE],
|
||||
cfg.get(CONF_QUEUE_MAX, 0),
|
||||
cfg.get(CONF_QUEUE_SIZE, 0),
|
||||
)
|
||||
)
|
||||
|
||||
@ -303,7 +280,7 @@ class ScriptEntity(ToggleEntity):
|
||||
|
||||
icon = None
|
||||
|
||||
def __init__(self, hass, object_id, name, icon, sequence, mode, queue_max):
|
||||
def __init__(self, hass, object_id, name, icon, sequence, mode, queue_size):
|
||||
"""Initialize the script."""
|
||||
self.object_id = object_id
|
||||
self.icon = icon
|
||||
@ -314,7 +291,7 @@ class ScriptEntity(ToggleEntity):
|
||||
name,
|
||||
self.async_change_listener,
|
||||
mode,
|
||||
queue_max,
|
||||
queue_size,
|
||||
logging.getLogger(f"{__name__}.{object_id}"),
|
||||
)
|
||||
self._changed = asyncio.Event()
|
||||
|
@ -134,6 +134,7 @@ CONF_PREFIX = "prefix"
|
||||
CONF_PROFILE_NAME = "profile_name"
|
||||
CONF_PROTOCOL = "protocol"
|
||||
CONF_PROXY_SSL = "proxy_ssl"
|
||||
CONF_QUEUE_SIZE = "queue_size"
|
||||
CONF_QUOTE = "quote"
|
||||
CONF_RADIUS = "radius"
|
||||
CONF_RECIPIENT = "recipient"
|
||||
|
@ -24,6 +24,8 @@ from homeassistant.const import (
|
||||
CONF_EVENT,
|
||||
CONF_EVENT_DATA,
|
||||
CONF_EVENT_DATA_TEMPLATE,
|
||||
CONF_MODE,
|
||||
CONF_QUEUE_SIZE,
|
||||
CONF_SCENE,
|
||||
CONF_TIMEOUT,
|
||||
CONF_WAIT_TEMPLATE,
|
||||
@ -72,11 +74,42 @@ SCRIPT_MODE_CHOICES = [
|
||||
]
|
||||
DEFAULT_SCRIPT_MODE = SCRIPT_MODE_LEGACY
|
||||
|
||||
DEFAULT_QUEUE_MAX = 10
|
||||
DEFAULT_QUEUE_SIZE = 10
|
||||
|
||||
_LOG_EXCEPTION = logging.ERROR + 1
|
||||
_TIMEOUT_MSG = "Timeout reached, abort script."
|
||||
|
||||
SCRIPT_BASE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_MODE): vol.In(SCRIPT_MODE_CHOICES),
|
||||
vol.Optional(CONF_QUEUE_SIZE): vol.All(vol.Coerce(int), vol.Range(min=2)),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def warn_deprecated_legacy(logger, msg):
|
||||
"""Warn about deprecated legacy mode."""
|
||||
logger.warning(
|
||||
"Script behavior has changed. "
|
||||
"To continue using previous behavior, which is now deprecated, "
|
||||
"add '%s: %s' to %s.",
|
||||
CONF_MODE,
|
||||
SCRIPT_MODE_LEGACY,
|
||||
msg,
|
||||
)
|
||||
|
||||
|
||||
def validate_queue_size(config):
|
||||
"""Validate queue_size option."""
|
||||
mode = config.get(CONF_MODE, DEFAULT_SCRIPT_MODE)
|
||||
queue_size = config.get(CONF_QUEUE_SIZE)
|
||||
if mode == SCRIPT_MODE_QUEUE:
|
||||
if queue_size is None:
|
||||
config[CONF_QUEUE_SIZE] = DEFAULT_QUEUE_SIZE
|
||||
elif queue_size is not None:
|
||||
raise vol.Invalid(f"{CONF_QUEUE_SIZE} not valid with {mode} {CONF_MODE}")
|
||||
return config
|
||||
|
||||
|
||||
async def async_validate_action_config(
|
||||
hass: HomeAssistant, config: ConfigType
|
||||
@ -673,7 +706,7 @@ class Script:
|
||||
name: Optional[str] = None,
|
||||
change_listener: Optional[Callable[..., Any]] = None,
|
||||
script_mode: str = DEFAULT_SCRIPT_MODE,
|
||||
queue_max: int = DEFAULT_QUEUE_MAX,
|
||||
queue_size: int = DEFAULT_QUEUE_SIZE,
|
||||
logger: Optional[logging.Logger] = None,
|
||||
log_exceptions: bool = True,
|
||||
) -> None:
|
||||
@ -702,7 +735,7 @@ class Script:
|
||||
|
||||
self._runs: List[_ScriptRunBase] = []
|
||||
if script_mode == SCRIPT_MODE_QUEUE:
|
||||
self._queue_max = queue_max
|
||||
self._queue_size = queue_size
|
||||
self._queue_len = 0
|
||||
self._queue_lck = asyncio.Lock()
|
||||
self._config_cache: Dict[Set[Tuple], Callable[..., bool]] = {}
|
||||
@ -806,7 +839,7 @@ class Script:
|
||||
self._queue_len,
|
||||
"s" if self._queue_len > 1 else "",
|
||||
)
|
||||
if self._queue_len >= self._queue_max:
|
||||
if self._queue_len >= self._queue_size:
|
||||
raise QueueFull
|
||||
|
||||
if self.is_legacy:
|
||||
|
@ -1070,3 +1070,35 @@ async def test_logbook_humanify_automation_triggered_event(hass):
|
||||
assert event2["domain"] == "automation"
|
||||
assert event2["message"] == "has been triggered"
|
||||
assert event2["entity_id"] == "automation.bye"
|
||||
|
||||
|
||||
async def test_invalid_config(hass):
|
||||
"""Test invalid config."""
|
||||
with assert_setup_component(0, automation.DOMAIN):
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"mode": "parallel",
|
||||
"queue_size": 5,
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": [],
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def test_config_legacy(hass, caplog):
|
||||
"""Test config defaulting to legacy mode."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": [],
|
||||
}
|
||||
},
|
||||
)
|
||||
assert "To continue using previous behavior, which is now deprecated" in caplog.text
|
||||
|
Loading…
x
Reference in New Issue
Block a user