1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-19 15:00:27 +01:00
Files
core/homeassistant/components/script/config.py
T
2026-04-30 21:14:48 +02:00

312 lines
9.5 KiB
Python

"""Config validation helper for the script integration."""
from collections.abc import Mapping
from contextlib import suppress
from enum import StrEnum
from typing import Any
import voluptuous as vol
from voluptuous.humanize import humanize_error
from homeassistant.components.blueprint import (
BlueprintException,
is_blueprint_instance_config,
)
from homeassistant.components.trace import TRACE_CONFIG_SCHEMA
from homeassistant.config import config_per_platform, config_without_domain
from homeassistant.const import (
CONF_ALIAS,
CONF_DEFAULT,
CONF_DESCRIPTION,
CONF_ICON,
CONF_NAME,
CONF_SELECTOR,
CONF_SEQUENCE,
CONF_VARIABLES,
SERVICE_RELOAD,
SERVICE_TOGGLE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.script import (
SCRIPT_MODE_SINGLE,
async_validate_actions_config,
make_script_schema,
)
from homeassistant.helpers.selector import validate_selector
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.yaml.input import UndefinedSubstitution
from .const import (
CONF_ADVANCED,
CONF_EXAMPLE,
CONF_FIELDS,
CONF_REQUIRED,
CONF_TRACE,
DOMAIN,
LOGGER,
)
from .helpers import async_get_blueprints
PACKAGE_MERGE_HINT = "dict"
_MINIMAL_SCRIPT_ENTITY_SCHEMA = vol.Schema(
{
CONF_ALIAS: cv.string,
vol.Optional(CONF_DESCRIPTION): cv.string,
},
extra=vol.ALLOW_EXTRA,
)
_INVALID_OBJECT_IDS = {
SERVICE_RELOAD,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
SERVICE_TOGGLE,
}
_SCRIPT_OBJECT_ID_SCHEMA = vol.All(
cv.slug,
vol.NotIn(
_INVALID_OBJECT_IDS,
(
"A script's object_id must not be one of "
f"{', '.join(sorted(_INVALID_OBJECT_IDS))}"
),
),
)
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: HomeAssistant,
object_id: str,
config: ConfigType,
raise_on_errors: bool,
warn_on_errors: bool,
) -> ScriptConfig:
"""Validate config item."""
raw_config = None
raw_blueprint_inputs = None
uses_blueprint = False
with suppress(ValueError): # Invalid config
raw_config = dict(config)
def _humanize(err: Exception, data: Any) -> str:
"""Humanize vol.Invalid, stringify other exceptions."""
if isinstance(err, vol.Invalid):
return humanize_error(data, err)
return str(err)
def _log_invalid_script(
err: Exception,
script_name: str,
problem: str,
data: Any,
) -> None:
"""Log an error about invalid script."""
if not warn_on_errors:
return
if uses_blueprint:
LOGGER.error(
"Blueprint '%s' generated invalid script with inputs %s: %s",
blueprint_inputs.blueprint.name,
blueprint_inputs.inputs,
_humanize(err, data),
)
return
LOGGER.error(
"%s %s and has been disabled: %s",
script_name,
problem,
_humanize(err, data),
)
return
def _set_validation_status(
script_config: ScriptConfig,
validation_status: ValidationStatus,
validation_error: Exception,
config: ConfigType,
) -> None:
"""Set validation status."""
if uses_blueprint:
validation_status = ValidationStatus.FAILED_BLUEPRINT
script_config.validation_status = validation_status
script_config.validation_error = _humanize(validation_error, config)
def _minimal_config(
validation_status: ValidationStatus,
validation_error: Exception,
config: ConfigType,
) -> ScriptConfig:
"""Try validating id, alias and description."""
minimal_config = _MINIMAL_SCRIPT_ENTITY_SCHEMA(config)
script_config = ScriptConfig(minimal_config)
script_config.raw_blueprint_inputs = raw_blueprint_inputs
script_config.raw_config = raw_config
_set_validation_status(
script_config, validation_status, validation_error, config
)
return script_config
if is_blueprint_instance_config(config):
uses_blueprint = True
blueprints = async_get_blueprints(hass)
try:
blueprint_inputs = await blueprints.async_inputs_from_config(config)
except BlueprintException as err:
if warn_on_errors:
LOGGER.error(
"Failed to generate script from blueprint: %s",
err,
)
if raise_on_errors:
raise
return _minimal_config(ValidationStatus.FAILED_BLUEPRINT, err, config)
raw_blueprint_inputs = blueprint_inputs.config_with_inputs
try:
config = blueprint_inputs.async_substitute()
raw_config = dict(config)
except UndefinedSubstitution as err:
if warn_on_errors:
LOGGER.error(
"Blueprint '%s' failed to generate script with inputs %s: %s",
blueprint_inputs.blueprint.name,
blueprint_inputs.inputs,
err,
)
if raise_on_errors:
raise HomeAssistantError(err) from err
return _minimal_config(ValidationStatus.FAILED_BLUEPRINT, err, config)
script_name = f"Script with object id '{object_id}'"
if isinstance(config, Mapping):
if CONF_ALIAS in config:
script_name = f"Script with alias '{config[CONF_ALIAS]}'"
try:
_SCRIPT_OBJECT_ID_SCHEMA(object_id)
except vol.Invalid as err:
_log_invalid_script(err, script_name, "has invalid object id", object_id)
raise
try:
validated_config = SCRIPT_ENTITY_SCHEMA(config)
except vol.Invalid as err:
_log_invalid_script(err, script_name, "could not be validated", config)
if raise_on_errors:
raise
return _minimal_config(ValidationStatus.FAILED_SCHEMA, err, config)
script_config = ScriptConfig(validated_config)
script_config.raw_blueprint_inputs = raw_blueprint_inputs
script_config.raw_config = raw_config
try:
script_config[CONF_SEQUENCE] = await async_validate_actions_config(
hass, validated_config[CONF_SEQUENCE]
)
except (
vol.Invalid,
HomeAssistantError,
) as err:
_log_invalid_script(
err, script_name, "failed to setup sequence", validated_config
)
if raise_on_errors:
raise
_set_validation_status(
script_config, ValidationStatus.FAILED_SEQUENCE, err, validated_config
)
return script_config
return script_config
class ValidationStatus(StrEnum):
"""What was changed in a config entry."""
FAILED_BLUEPRINT = "failed_blueprint"
FAILED_SCHEMA = "failed_schema"
FAILED_SEQUENCE = "failed_sequence"
OK = "ok"
class ScriptConfig(dict):
"""Dummy class to allow adding attributes."""
raw_config: ConfigType | None = None
raw_blueprint_inputs: ConfigType | None = None
validation_status: ValidationStatus = ValidationStatus.OK
validation_error: str | None = None
async def _try_async_validate_config_item(
hass: HomeAssistant,
object_id: str,
config: ConfigType,
) -> ScriptConfig | None:
"""Validate config item."""
try:
return await _async_validate_config_item(hass, object_id, config, False, True)
except vol.Invalid, HomeAssistantError:
return None
async def async_validate_config_item(
hass: HomeAssistant,
object_id: str,
config: dict[str, Any],
) -> ScriptConfig | None:
"""Validate config item, called by EditScriptConfigView."""
return await _async_validate_config_item(hass, object_id, config, True, False)
async def async_validate_config(hass: HomeAssistant, config: ConfigType) -> ConfigType:
"""Validate config."""
scripts = {}
for _, p_config in config_per_platform(config, DOMAIN):
for object_id, cfg in p_config.items():
if object_id in scripts:
LOGGER.warning("Duplicate script detected with name: '%s'", object_id)
continue
cfg = await _try_async_validate_config_item(hass, object_id, cfg)
if cfg is not None:
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