Home Assistant Unofficial Reference 2024.12.1
config.py
Go to the documentation of this file.
1 """Config validation helper for the automation 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 
13 from homeassistant.components import blueprint
14 from homeassistant.components.trace import TRACE_CONFIG_SCHEMA
15 from homeassistant.config import config_per_platform, config_without_domain
16 from homeassistant.const import (
17  CONF_ALIAS,
18  CONF_CONDITION,
19  CONF_CONDITIONS,
20  CONF_DESCRIPTION,
21  CONF_ID,
22  CONF_VARIABLES,
23 )
24 from homeassistant.core import HomeAssistant
25 from homeassistant.exceptions import HomeAssistantError
26 from homeassistant.helpers import config_validation as cv, script
27 from homeassistant.helpers.condition import async_validate_conditions_config
28 from homeassistant.helpers.trigger import async_validate_trigger_config
29 from homeassistant.helpers.typing import ConfigType
30 from homeassistant.util.yaml.input import UndefinedSubstitution
31 
32 from .const import (
33  CONF_ACTION,
34  CONF_ACTIONS,
35  CONF_HIDE_ENTITY,
36  CONF_INITIAL_STATE,
37  CONF_TRACE,
38  CONF_TRIGGER,
39  CONF_TRIGGER_VARIABLES,
40  CONF_TRIGGERS,
41  DOMAIN,
42  LOGGER,
43 )
44 from .helpers import async_get_blueprints
45 
46 PACKAGE_MERGE_HINT = "list"
47 
48 _MINIMAL_PLATFORM_SCHEMA = vol.Schema(
49  {
50  CONF_ID: str,
51  CONF_ALIAS: cv.string,
52  vol.Optional(CONF_DESCRIPTION): cv.string,
53  },
54  extra=vol.ALLOW_EXTRA,
55 )
56 
57 
58 def _backward_compat_schema(value: Any | None) -> Any:
59  """Backward compatibility for automations."""
60 
61  if not isinstance(value, dict):
62  return value
63 
64  # `trigger` has been renamed to `triggers`
65  if CONF_TRIGGER in value:
66  if CONF_TRIGGERS in value:
67  raise vol.Invalid(
68  "Cannot specify both 'trigger' and 'triggers'. Please use 'triggers' only."
69  )
70  value[CONF_TRIGGERS] = value.pop(CONF_TRIGGER)
71 
72  # `condition` has been renamed to `conditions`
73  if CONF_CONDITION in value:
74  if CONF_CONDITIONS in value:
75  raise vol.Invalid(
76  "Cannot specify both 'condition' and 'conditions'. Please use 'conditions' only."
77  )
78  value[CONF_CONDITIONS] = value.pop(CONF_CONDITION)
79 
80  # `action` has been renamed to `actions`
81  if CONF_ACTION in value:
82  if CONF_ACTIONS in value:
83  raise vol.Invalid(
84  "Cannot specify both 'action' and 'actions'. Please use 'actions' only."
85  )
86  value[CONF_ACTIONS] = value.pop(CONF_ACTION)
87 
88  return value
89 
90 
91 PLATFORM_SCHEMA = vol.All(
92  _backward_compat_schema,
93  cv.deprecated(CONF_HIDE_ENTITY),
94  script.make_script_schema(
95  {
96  # str on purpose
97  CONF_ID: str,
98  CONF_ALIAS: cv.string,
99  vol.Optional(CONF_DESCRIPTION): cv.string,
100  vol.Optional(CONF_TRACE, default={}): TRACE_CONFIG_SCHEMA,
101  vol.Optional(CONF_INITIAL_STATE): cv.boolean,
102  vol.Optional(CONF_HIDE_ENTITY): cv.boolean,
103  vol.Required(CONF_TRIGGERS): cv.TRIGGER_SCHEMA,
104  vol.Optional(CONF_CONDITIONS): cv.CONDITIONS_SCHEMA,
105  vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA,
106  vol.Optional(CONF_TRIGGER_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA,
107  vol.Required(CONF_ACTIONS): cv.SCRIPT_SCHEMA,
108  },
109  script.SCRIPT_MODE_SINGLE,
110  ),
111 )
112 
113 AUTOMATION_BLUEPRINT_SCHEMA = vol.All(
114  _backward_compat_schema, blueprint.schemas.BLUEPRINT_SCHEMA
115 )
116 
117 
118 async def _async_validate_config_item( # noqa: C901
119  hass: HomeAssistant,
120  config: ConfigType,
121  raise_on_errors: bool,
122  warn_on_errors: bool,
123 ) -> AutomationConfig:
124  """Validate config item."""
125  raw_config = None
126  raw_blueprint_inputs = None
127  uses_blueprint = False
128  with suppress(ValueError):
129  raw_config = dict(config)
130 
131  def _humanize(err: Exception, config: ConfigType) -> str:
132  """Humanize vol.Invalid, stringify other exceptions."""
133  if isinstance(err, vol.Invalid):
134  return humanize_error(config, err)
135  return str(err)
136 
137  def _log_invalid_automation(
138  err: Exception,
139  automation_name: str,
140  problem: str,
141  config: ConfigType,
142  ) -> None:
143  """Log an error about invalid automation."""
144  if not warn_on_errors:
145  return
146 
147  if uses_blueprint:
148  LOGGER.error(
149  "Blueprint '%s' generated invalid automation with inputs %s: %s",
150  blueprint_inputs.blueprint.name,
151  blueprint_inputs.inputs,
152  _humanize(err, config),
153  )
154  return
155 
156  LOGGER.error(
157  "%s %s and has been disabled: %s",
158  automation_name,
159  problem,
160  _humanize(err, config),
161  )
162  return
163 
164  def _set_validation_status(
165  automation_config: AutomationConfig,
166  validation_status: ValidationStatus,
167  validation_error: Exception,
168  config: ConfigType,
169  ) -> None:
170  """Set validation status."""
171  if uses_blueprint:
172  validation_status = ValidationStatus.FAILED_BLUEPRINT
173  automation_config.validation_status = validation_status
174  automation_config.validation_error = _humanize(validation_error, config)
175 
176  def _minimal_config(
177  validation_status: ValidationStatus,
178  validation_error: Exception,
179  config: ConfigType,
180  ) -> AutomationConfig:
181  """Try validating id, alias and description."""
182  minimal_config = _MINIMAL_PLATFORM_SCHEMA(config)
183  automation_config = AutomationConfig(minimal_config)
184  automation_config.raw_blueprint_inputs = raw_blueprint_inputs
185  automation_config.raw_config = raw_config
186  _set_validation_status(
187  automation_config, validation_status, validation_error, config
188  )
189  return automation_config
190 
191  if blueprint.is_blueprint_instance_config(config):
192  uses_blueprint = True
193  blueprints = async_get_blueprints(hass)
194  try:
195  blueprint_inputs = await blueprints.async_inputs_from_config(
197  )
198  except blueprint.BlueprintException as err:
199  if warn_on_errors:
200  LOGGER.error(
201  "Failed to generate automation from blueprint: %s",
202  err,
203  )
204  if raise_on_errors:
205  raise
206  return _minimal_config(ValidationStatus.FAILED_BLUEPRINT, err, config)
207 
208  raw_blueprint_inputs = blueprint_inputs.config_with_inputs
209 
210  try:
211  config = blueprint_inputs.async_substitute()
212  raw_config = dict(config)
213  except UndefinedSubstitution as err:
214  if warn_on_errors:
215  LOGGER.error(
216  "Blueprint '%s' failed to generate automation with inputs %s: %s",
217  blueprint_inputs.blueprint.name,
218  blueprint_inputs.inputs,
219  err,
220  )
221  if raise_on_errors:
222  raise HomeAssistantError(err) from err
223  return _minimal_config(ValidationStatus.FAILED_BLUEPRINT, err, config)
224 
225  automation_name = "Unnamed automation"
226  if isinstance(config, Mapping):
227  if CONF_ALIAS in config:
228  automation_name = f"Automation with alias '{config[CONF_ALIAS]}'"
229  elif CONF_ID in config:
230  automation_name = f"Automation with ID '{config[CONF_ID]}'"
231 
232  try:
233  validated_config = PLATFORM_SCHEMA(config)
234  except vol.Invalid as err:
235  _log_invalid_automation(err, automation_name, "could not be validated", config)
236  if raise_on_errors:
237  raise
238  return _minimal_config(ValidationStatus.FAILED_SCHEMA, err, config)
239 
240  automation_config = AutomationConfig(validated_config)
241  automation_config.raw_blueprint_inputs = raw_blueprint_inputs
242  automation_config.raw_config = raw_config
243 
244  try:
245  automation_config[CONF_TRIGGERS] = await async_validate_trigger_config(
246  hass, validated_config[CONF_TRIGGERS]
247  )
248  except (
249  vol.Invalid,
250  HomeAssistantError,
251  ) as err:
252  _log_invalid_automation(
253  err, automation_name, "failed to setup triggers", validated_config
254  )
255  if raise_on_errors:
256  raise
257  _set_validation_status(
258  automation_config, ValidationStatus.FAILED_TRIGGERS, err, validated_config
259  )
260  return automation_config
261 
262  if CONF_CONDITIONS in validated_config:
263  try:
264  automation_config[CONF_CONDITIONS] = await async_validate_conditions_config(
265  hass, validated_config[CONF_CONDITIONS]
266  )
267  except (
268  vol.Invalid,
269  HomeAssistantError,
270  ) as err:
271  _log_invalid_automation(
272  err, automation_name, "failed to setup conditions", validated_config
273  )
274  if raise_on_errors:
275  raise
276  _set_validation_status(
277  automation_config,
278  ValidationStatus.FAILED_CONDITIONS,
279  err,
280  validated_config,
281  )
282  return automation_config
283 
284  try:
285  automation_config[CONF_ACTIONS] = await script.async_validate_actions_config(
286  hass, validated_config[CONF_ACTIONS]
287  )
288  except (
289  vol.Invalid,
290  HomeAssistantError,
291  ) as err:
292  _log_invalid_automation(
293  err, automation_name, "failed to setup actions", validated_config
294  )
295  if raise_on_errors:
296  raise
297  _set_validation_status(
298  automation_config, ValidationStatus.FAILED_ACTIONS, err, validated_config
299  )
300  return automation_config
301 
302  return automation_config
303 
304 
305 class ValidationStatus(StrEnum):
306  """What was changed in a config entry."""
307 
308  FAILED_ACTIONS = "failed_actions"
309  FAILED_BLUEPRINT = "failed_blueprint"
310  FAILED_CONDITIONS = "failed_conditions"
311  FAILED_SCHEMA = "failed_schema"
312  FAILED_TRIGGERS = "failed_triggers"
313  OK = "ok"
314 
315 
317  """Dummy class to allow adding attributes."""
318 
319  raw_config: dict[str, Any] | None = None
320  raw_blueprint_inputs: dict[str, Any] | None = None
321  validation_status: ValidationStatus = ValidationStatus.OK
322  validation_error: str | None = None
323 
324 
326  hass: HomeAssistant,
327  config: dict[str, Any],
328 ) -> AutomationConfig | None:
329  """Validate config item."""
330  try:
331  return await _async_validate_config_item(hass, config, False, True)
332  except (vol.Invalid, HomeAssistantError):
333  return None
334 
335 
337  hass: HomeAssistant,
338  config_key: str,
339  config: dict[str, Any],
340 ) -> AutomationConfig | None:
341  """Validate config item, called by EditAutomationConfigView."""
342  return await _async_validate_config_item(hass, config, True, False)
343 
344 
345 async def async_validate_config(hass: HomeAssistant, config: ConfigType) -> ConfigType:
346  """Validate config."""
347  # No gather here since _try_async_validate_config_item is unlikely to suspend
348  # and the cost of creating many tasks is not worth the benefit.
349  automations = list(
350  filter(
351  lambda x: x is not None,
352  [
353  await _try_async_validate_config_item(hass, p_config)
354  for _, p_config in config_per_platform(config, DOMAIN)
355  ],
356  )
357  )
358 
359  # Create a copy of the configuration with all config for current
360  # component removed and add validated config back in.
361  config = config_without_domain(config, DOMAIN)
362  config[DOMAIN] = automations
363 
364  return config
ConfigType async_validate_config(HomeAssistant hass, ConfigType config)
Definition: config.py:345
Any _backward_compat_schema(Any|None value)
Definition: config.py:58
AutomationConfig|None _try_async_validate_config_item(HomeAssistant hass, dict[str, Any] config)
Definition: config.py:328
AutomationConfig|None async_validate_config_item(HomeAssistant hass, str config_key, dict[str, Any] config)
Definition: config.py:340
AutomationConfig _async_validate_config_item(HomeAssistant hass, ConfigType config, bool raise_on_errors, bool warn_on_errors)
Definition: config.py:123
blueprint.DomainBlueprints async_get_blueprints(HomeAssistant hass)
Definition: helpers.py:29
ConfigType async_validate_trigger_config(HomeAssistant hass, ConfigType config)
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|Template] async_validate_conditions_config(HomeAssistant hass, list[ConfigType] conditions)
Definition: condition.py:1073