Home Assistant Unofficial Reference 2024.12.1
config.py
Go to the documentation of this file.
1 """Config validation helper for the script integration."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Mapping
6 from contextlib import suppress
7 from enum import StrEnum
8 from typing import Any
9 
10 import voluptuous as vol
11 from voluptuous.humanize import humanize_error
12 
14  BlueprintException,
15  is_blueprint_instance_config,
16 )
17 from homeassistant.components.trace import TRACE_CONFIG_SCHEMA
18 from homeassistant.config import config_per_platform, config_without_domain
19 from homeassistant.const import (
20  CONF_ALIAS,
21  CONF_DEFAULT,
22  CONF_DESCRIPTION,
23  CONF_ICON,
24  CONF_NAME,
25  CONF_SELECTOR,
26  CONF_SEQUENCE,
27  CONF_VARIABLES,
28  SERVICE_RELOAD,
29  SERVICE_TOGGLE,
30  SERVICE_TURN_OFF,
31  SERVICE_TURN_ON,
32 )
33 from homeassistant.core import HomeAssistant
34 from homeassistant.exceptions import HomeAssistantError
35 from homeassistant.helpers import config_validation as cv
36 from homeassistant.helpers.script import (
37  SCRIPT_MODE_SINGLE,
38  async_validate_actions_config,
39  make_script_schema,
40 )
41 from homeassistant.helpers.selector import validate_selector
42 from homeassistant.helpers.typing import ConfigType
43 from homeassistant.util.yaml.input import UndefinedSubstitution
44 
45 from .const import (
46  CONF_ADVANCED,
47  CONF_EXAMPLE,
48  CONF_FIELDS,
49  CONF_REQUIRED,
50  CONF_TRACE,
51  DOMAIN,
52  LOGGER,
53 )
54 from .helpers import async_get_blueprints
55 
56 PACKAGE_MERGE_HINT = "dict"
57 
58 _MINIMAL_SCRIPT_ENTITY_SCHEMA = vol.Schema(
59  {
60  CONF_ALIAS: cv.string,
61  vol.Optional(CONF_DESCRIPTION): cv.string,
62  },
63  extra=vol.ALLOW_EXTRA,
64 )
65 
66 _INVALID_OBJECT_IDS = {
67  SERVICE_RELOAD,
68  SERVICE_TURN_OFF,
69  SERVICE_TURN_ON,
70  SERVICE_TOGGLE,
71 }
72 
73 _SCRIPT_OBJECT_ID_SCHEMA = vol.All(
74  cv.slug,
75  vol.NotIn(
76  _INVALID_OBJECT_IDS,
77  (
78  "A script's object_id must not be one of "
79  f"{', '.join(sorted(_INVALID_OBJECT_IDS))}"
80  ),
81  ),
82 )
83 
84 SCRIPT_ENTITY_SCHEMA = make_script_schema(
85  {
86  vol.Optional(CONF_ALIAS): cv.string,
87  vol.Optional(CONF_TRACE, default={}): TRACE_CONFIG_SCHEMA,
88  vol.Optional(CONF_ICON): cv.icon,
89  vol.Required(CONF_SEQUENCE): cv.SCRIPT_SCHEMA,
90  vol.Optional(CONF_DESCRIPTION, default=""): cv.string,
91  vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA,
92  vol.Optional(CONF_FIELDS, default={}): {
93  cv.string: {
94  vol.Optional(CONF_ADVANCED, default=False): cv.boolean,
95  vol.Optional(CONF_DEFAULT): cv.match_all,
96  vol.Optional(CONF_DESCRIPTION): cv.string,
97  vol.Optional(CONF_EXAMPLE): cv.string,
98  vol.Optional(CONF_NAME): cv.string,
99  vol.Optional(CONF_REQUIRED, default=False): cv.boolean,
100  vol.Optional(CONF_SELECTOR): validate_selector,
101  }
102  },
103  },
104  SCRIPT_MODE_SINGLE,
105 )
106 
107 
109  hass: HomeAssistant,
110  object_id: str,
111  config: ConfigType,
112  raise_on_errors: bool,
113  warn_on_errors: bool,
114 ) -> ScriptConfig:
115  """Validate config item."""
116  raw_config = None
117  raw_blueprint_inputs = None
118  uses_blueprint = False
119  with suppress(ValueError): # Invalid config
120  raw_config = dict(config)
121 
122  def _humanize(err: Exception, data: Any) -> str:
123  """Humanize vol.Invalid, stringify other exceptions."""
124  if isinstance(err, vol.Invalid):
125  return humanize_error(data, err)
126  return str(err)
127 
128  def _log_invalid_script(
129  err: Exception,
130  script_name: str,
131  problem: str,
132  data: Any,
133  ) -> None:
134  """Log an error about invalid script."""
135  if not warn_on_errors:
136  return
137 
138  if uses_blueprint:
139  LOGGER.error(
140  "Blueprint '%s' generated invalid script with inputs %s: %s",
141  blueprint_inputs.blueprint.name,
142  blueprint_inputs.inputs,
143  _humanize(err, data),
144  )
145  return
146 
147  LOGGER.error(
148  "%s %s and has been disabled: %s",
149  script_name,
150  problem,
151  _humanize(err, data),
152  )
153  return
154 
155  def _set_validation_status(
156  script_config: ScriptConfig,
157  validation_status: ValidationStatus,
158  validation_error: Exception,
159  config: ConfigType,
160  ) -> None:
161  """Set validation status."""
162  if uses_blueprint:
163  validation_status = ValidationStatus.FAILED_BLUEPRINT
164  script_config.validation_status = validation_status
165  script_config.validation_error = _humanize(validation_error, config)
166 
167  def _minimal_config(
168  validation_status: ValidationStatus,
169  validation_error: Exception,
170  config: ConfigType,
171  ) -> ScriptConfig:
172  """Try validating id, alias and description."""
173  minimal_config = _MINIMAL_SCRIPT_ENTITY_SCHEMA(config)
174  script_config = ScriptConfig(minimal_config)
175  script_config.raw_blueprint_inputs = raw_blueprint_inputs
176  script_config.raw_config = raw_config
177  _set_validation_status(
178  script_config, validation_status, validation_error, config
179  )
180  return script_config
181 
182  if is_blueprint_instance_config(config):
183  uses_blueprint = True
184  blueprints = async_get_blueprints(hass)
185  try:
186  blueprint_inputs = await blueprints.async_inputs_from_config(config)
187  except BlueprintException as err:
188  if warn_on_errors:
189  LOGGER.error(
190  "Failed to generate script from blueprint: %s",
191  err,
192  )
193  if raise_on_errors:
194  raise
195  return _minimal_config(ValidationStatus.FAILED_BLUEPRINT, err, config)
196 
197  raw_blueprint_inputs = blueprint_inputs.config_with_inputs
198 
199  try:
200  config = blueprint_inputs.async_substitute()
201  raw_config = dict(config)
202  except UndefinedSubstitution as err:
203  if warn_on_errors:
204  LOGGER.error(
205  "Blueprint '%s' failed to generate script with inputs %s: %s",
206  blueprint_inputs.blueprint.name,
207  blueprint_inputs.inputs,
208  err,
209  )
210  if raise_on_errors:
211  raise HomeAssistantError(err) from err
212  return _minimal_config(ValidationStatus.FAILED_BLUEPRINT, err, config)
213 
214  script_name = f"Script with object id '{object_id}'"
215  if isinstance(config, Mapping):
216  if CONF_ALIAS in config:
217  script_name = f"Script with alias '{config[CONF_ALIAS]}'"
218 
219  try:
220  _SCRIPT_OBJECT_ID_SCHEMA(object_id)
221  except vol.Invalid as err:
222  _log_invalid_script(err, script_name, "has invalid object id", object_id)
223  raise
224  try:
225  validated_config = SCRIPT_ENTITY_SCHEMA(config)
226  except vol.Invalid as err:
227  _log_invalid_script(err, script_name, "could not be validated", config)
228  if raise_on_errors:
229  raise
230  return _minimal_config(ValidationStatus.FAILED_SCHEMA, err, config)
231 
232  script_config = ScriptConfig(validated_config)
233  script_config.raw_blueprint_inputs = raw_blueprint_inputs
234  script_config.raw_config = raw_config
235 
236  try:
237  script_config[CONF_SEQUENCE] = await async_validate_actions_config(
238  hass, validated_config[CONF_SEQUENCE]
239  )
240  except (
241  vol.Invalid,
242  HomeAssistantError,
243  ) as err:
244  _log_invalid_script(
245  err, script_name, "failed to setup sequence", validated_config
246  )
247  if raise_on_errors:
248  raise
249  _set_validation_status(
250  script_config, ValidationStatus.FAILED_SEQUENCE, err, validated_config
251  )
252  return script_config
253 
254  return script_config
255 
256 
257 class ValidationStatus(StrEnum):
258  """What was changed in a config entry."""
259 
260  FAILED_BLUEPRINT = "failed_blueprint"
261  FAILED_SCHEMA = "failed_schema"
262  FAILED_SEQUENCE = "failed_sequence"
263  OK = "ok"
264 
265 
267  """Dummy class to allow adding attributes."""
268 
269  raw_config: ConfigType | None = None
270  raw_blueprint_inputs: ConfigType | None = None
271  validation_status: ValidationStatus = ValidationStatus.OK
272  validation_error: str | None = None
273 
274 
276  hass: HomeAssistant,
277  object_id: str,
278  config: ConfigType,
279 ) -> ScriptConfig | None:
280  """Validate config item."""
281  try:
282  return await _async_validate_config_item(hass, object_id, config, False, True)
283  except (vol.Invalid, HomeAssistantError):
284  return None
285 
286 
288  hass: HomeAssistant,
289  object_id: str,
290  config: dict[str, Any],
291 ) -> ScriptConfig | None:
292  """Validate config item, called by EditScriptConfigView."""
293  return await _async_validate_config_item(hass, object_id, config, True, False)
294 
295 
296 async def async_validate_config(hass: HomeAssistant, config: ConfigType) -> ConfigType:
297  """Validate config."""
298  scripts = {}
299  for _, p_config in config_per_platform(config, DOMAIN):
300  for object_id, cfg in p_config.items():
301  if object_id in scripts:
302  LOGGER.warning("Duplicate script detected with name: '%s'", object_id)
303  continue
304  cfg = await _try_async_validate_config_item(hass, object_id, cfg)
305  if cfg is not None:
306  scripts[object_id] = cfg
307 
308  # Create a copy of the configuration with all config for current
309  # component removed and add validated config back in.
310  config = config_without_domain(config, DOMAIN)
311  config[DOMAIN] = scripts
312 
313  return config
blueprint.DomainBlueprints async_get_blueprints(HomeAssistant hass)
Definition: helpers.py:29
bool is_blueprint_instance_config(Any config)
Definition: schemas.py:75
ScriptConfig|None async_validate_config_item(HomeAssistant hass, str object_id, dict[str, Any] config)
Definition: config.py:291
ScriptConfig|None _try_async_validate_config_item(HomeAssistant hass, str object_id, ConfigType config)
Definition: config.py:279
ScriptConfig _async_validate_config_item(HomeAssistant hass, str object_id, ConfigType config, bool raise_on_errors, bool warn_on_errors)
Definition: config.py:114
ConfigType async_validate_config(HomeAssistant hass, ConfigType config)
Definition: config.py:296
Iterable[tuple[str|None, ConfigType]] config_per_platform(ConfigType config, str domain)
Definition: config.py:969
ConfigType config_without_domain(ConfigType config, str domain)
Definition: config.py:1313
str humanize_error(HomeAssistant hass, vol.Invalid validation_error, str domain, dict config, str|None link, int max_sub_error_length=MAX_VALIDATION_ERROR_ITEM_LENGTH)
Definition: config.py:520
list[ConfigType] async_validate_actions_config(HomeAssistant hass, list[ConfigType] actions)
Definition: script.py:300
vol.Schema make_script_schema(Mapping[Any, Any] schema, str default_script_mode, int extra=vol.PREVENT_EXTRA)
Definition: script.py:264