Home Assistant Unofficial Reference 2024.12.1
config_validation.py
Go to the documentation of this file.
1 """Helpers for config validation using voluptuous."""
2 
3 # PEP 563 seems to break typing.get_type_hints when used
4 # with PEP 695 syntax. Fixed in Python 3.13.
5 # from __future__ import annotations
6 
7 from collections.abc import Callable, Hashable, Mapping
8 import contextlib
9 from contextvars import ContextVar
10 from datetime import (
11  date as date_sys,
12  datetime as datetime_sys,
13  time as time_sys,
14  timedelta,
15 )
16 from enum import Enum, StrEnum
17 import functools
18 import logging
19 from numbers import Number
20 import os
21 import re
22 from socket import ( # type: ignore[attr-defined] # private, not in typeshed
23  _GLOBAL_DEFAULT_TIMEOUT,
24 )
25 import threading
26 from typing import Any, cast, overload
27 from urllib.parse import urlparse
28 from uuid import UUID
29 
30 import voluptuous as vol
31 import voluptuous_serialize
32 
33 from homeassistant.const import (
34  ATTR_AREA_ID,
35  ATTR_DEVICE_ID,
36  ATTR_ENTITY_ID,
37  ATTR_FLOOR_ID,
38  ATTR_LABEL_ID,
39  CONF_ABOVE,
40  CONF_ACTION,
41  CONF_ALIAS,
42  CONF_ATTRIBUTE,
43  CONF_BELOW,
44  CONF_CHOOSE,
45  CONF_CONDITION,
46  CONF_CONDITIONS,
47  CONF_CONTINUE_ON_ERROR,
48  CONF_CONTINUE_ON_TIMEOUT,
49  CONF_COUNT,
50  CONF_DEFAULT,
51  CONF_DELAY,
52  CONF_DEVICE_ID,
53  CONF_DOMAIN,
54  CONF_ELSE,
55  CONF_ENABLED,
56  CONF_ENTITY_ID,
57  CONF_ENTITY_NAMESPACE,
58  CONF_ERROR,
59  CONF_EVENT,
60  CONF_EVENT_DATA,
61  CONF_EVENT_DATA_TEMPLATE,
62  CONF_FOR,
63  CONF_FOR_EACH,
64  CONF_ID,
65  CONF_IF,
66  CONF_MATCH,
67  CONF_PARALLEL,
68  CONF_PLATFORM,
69  CONF_REPEAT,
70  CONF_RESPONSE_VARIABLE,
71  CONF_SCAN_INTERVAL,
72  CONF_SCENE,
73  CONF_SEQUENCE,
74  CONF_SERVICE,
75  CONF_SERVICE_DATA,
76  CONF_SERVICE_DATA_TEMPLATE,
77  CONF_SERVICE_TEMPLATE,
78  CONF_SET_CONVERSATION_RESPONSE,
79  CONF_STATE,
80  CONF_STOP,
81  CONF_TARGET,
82  CONF_THEN,
83  CONF_TIMEOUT,
84  CONF_TRIGGER,
85  CONF_TRIGGERS,
86  CONF_UNTIL,
87  CONF_VALUE_TEMPLATE,
88  CONF_VARIABLES,
89  CONF_WAIT_FOR_TRIGGER,
90  CONF_WAIT_TEMPLATE,
91  CONF_WHILE,
92  ENTITY_MATCH_ALL,
93  ENTITY_MATCH_ANY,
94  ENTITY_MATCH_NONE,
95  SUN_EVENT_SUNRISE,
96  SUN_EVENT_SUNSET,
97  WEEKDAYS,
98  UnitOfTemperature,
99 )
100 from homeassistant.core import (
101  DOMAIN as HOMEASSISTANT_DOMAIN,
102  HomeAssistant,
103  async_get_hass,
104  async_get_hass_or_none,
105  split_entity_id,
106  valid_entity_id,
107 )
108 from homeassistant.exceptions import HomeAssistantError, TemplateError
109 from homeassistant.generated import currencies
110 from homeassistant.generated.countries import COUNTRIES
111 from homeassistant.generated.languages import LANGUAGES
112 from homeassistant.util import raise_if_invalid_path, slugify as util_slugify
113 import homeassistant.util.dt as dt_util
114 from homeassistant.util.yaml.objects import NodeStrClass
115 
116 from . import script_variables as script_variables_helper, template as template_helper
117 from .frame import get_integration_logger
118 from .typing import VolDictType, VolSchemaType
119 
120 TIME_PERIOD_ERROR = "offset {} should be format 'HH:MM', 'HH:MM:SS' or 'HH:MM:SS.F'"
121 
122 
124  """Raised when validation must happen in an executor thread."""
125 
126 
127 class _Hass(threading.local):
128  """Container which makes a HomeAssistant instance available to validators."""
129 
130  hass: HomeAssistant | None = None
131 
132 
133 _hass = _Hass()
134 """Set when doing async friendly schema validation."""
135 
136 
137 def _async_get_hass_or_none() -> HomeAssistant | None:
138  """Return the HomeAssistant instance or None.
139 
140  First tries core.async_get_hass_or_none, then _hass which is
141  set when doing async friendly schema validation.
142  """
143  return async_get_hass_or_none() or _hass.hass
144 
145 
146 _validating_async: ContextVar[bool] = ContextVar("_validating_async", default=False)
147 """Set to True when doing async friendly schema validation."""
148 
149 
150 def not_async_friendly[**_P, _R](validator: Callable[_P, _R]) -> Callable[_P, _R]:
151  """Mark a validator as not async friendly.
152 
153  This makes validation happen in an executor thread if validation is done by
154  async_validate, otherwise does nothing.
155  """
156 
157  @functools.wraps(validator)
158  def _not_async_friendly(*args: _P.args, **kwargs: _P.kwargs) -> _R:
159  if _validating_async.get() and async_get_hass_or_none():
160  # Raise if doing async friendly validation and validation
161  # is happening in the event loop
162  raise MustValidateInExecutor
163  return validator(*args, **kwargs)
164 
165  return _not_async_friendly
166 
167 
168 class UrlProtocolSchema(StrEnum):
169  """Valid URL protocol schema values."""
170 
171  HTTP = "http"
172  HTTPS = "https"
173  HOMEASSISTANT = "homeassistant"
174 
175 
176 EXTERNAL_URL_PROTOCOL_SCHEMA_LIST = frozenset(
177  {UrlProtocolSchema.HTTP, UrlProtocolSchema.HTTPS}
178 )
179 CONFIGURATION_URL_PROTOCOL_SCHEMA_LIST = frozenset(
180  {UrlProtocolSchema.HOMEASSISTANT, UrlProtocolSchema.HTTP, UrlProtocolSchema.HTTPS}
181 )
182 
183 # Home Assistant types
184 byte = vol.All(vol.Coerce(int), vol.Range(min=0, max=255))
185 small_float = vol.All(vol.Coerce(float), vol.Range(min=0, max=1))
186 positive_int = vol.All(vol.Coerce(int), vol.Range(min=0))
187 positive_float = vol.All(vol.Coerce(float), vol.Range(min=0))
188 latitude = vol.All(
189  vol.Coerce(float), vol.Range(min=-90, max=90), msg="invalid latitude"
190 )
191 longitude = vol.All(
192  vol.Coerce(float), vol.Range(min=-180, max=180), msg="invalid longitude"
193 )
194 gps = vol.ExactSequence([latitude, longitude])
195 sun_event = vol.All(vol.Lower, vol.Any(SUN_EVENT_SUNSET, SUN_EVENT_SUNRISE))
196 port = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535))
197 
198 
199 def path(value: Any) -> str:
200  """Validate it's a safe path."""
201  if not isinstance(value, str):
202  raise vol.Invalid("Expected a string")
203 
204  try:
205  raise_if_invalid_path(value)
206  except ValueError as err:
207  raise vol.Invalid("Invalid path") from err
208 
209  return value
210 
211 
212 # Adapted from:
213 # https://github.com/alecthomas/voluptuous/issues/115#issuecomment-144464666
214 def has_at_least_one_key(*keys: Any) -> Callable[[dict], dict]:
215  """Validate that at least one key exists."""
216  key_set = set(keys)
217 
218  def validate(obj: dict) -> dict:
219  """Test keys exist in dict."""
220  if not isinstance(obj, dict):
221  raise vol.Invalid("expected dictionary")
222 
223  if not key_set.isdisjoint(obj):
224  return obj
225  expected = ", ".join(str(k) for k in keys)
226  raise vol.Invalid(f"must contain at least one of {expected}.")
227 
228  return validate
229 
230 
231 def has_at_most_one_key(*keys: Any) -> Callable[[dict], dict]:
232  """Validate that zero keys exist or one key exists."""
233 
234  def validate(obj: dict) -> dict:
235  """Test zero keys exist or one key exists in dict."""
236  if not isinstance(obj, dict):
237  raise vol.Invalid("expected dictionary")
238 
239  if len(set(keys) & set(obj)) > 1:
240  expected = ", ".join(str(k) for k in keys)
241  raise vol.Invalid(f"must contain at most one of {expected}.")
242  return obj
243 
244  return validate
245 
246 
247 def boolean(value: Any) -> bool:
248  """Validate and coerce a boolean value."""
249  if isinstance(value, bool):
250  return value
251  if isinstance(value, str):
252  value = value.lower().strip()
253  if value in ("1", "true", "yes", "on", "enable"):
254  return True
255  if value in ("0", "false", "no", "off", "disable"):
256  return False
257  elif isinstance(value, Number):
258  # type ignore: https://github.com/python/mypy/issues/3186
259  return value != 0 # type: ignore[comparison-overlap]
260  raise vol.Invalid(f"invalid boolean value {value}")
261 
262 
263 def whitespace(value: Any) -> str:
264  """Validate result contains only whitespace."""
265  if isinstance(value, str) and (value == "" or value.isspace()):
266  return value
267 
268  raise vol.Invalid(f"contains non-whitespace: {value}")
269 
270 
271 @not_async_friendly
272 def isdevice(value: Any) -> str:
273  """Validate that value is a real device."""
274  try:
275  os.stat(value)
276  return str(value)
277  except OSError as err:
278  raise vol.Invalid(f"No device at {value} found") from err
279 
280 
281 def matches_regex(regex: str) -> Callable[[Any], str]:
282  """Validate that the value is a string that matches a regex."""
283  compiled = re.compile(regex)
284 
285  def validator(value: Any) -> str:
286  """Validate that value matches the given regex."""
287  if not isinstance(value, str):
288  raise vol.Invalid(f"not a string value: {value}")
289 
290  if not compiled.match(value):
291  raise vol.Invalid(
292  f"value {value} does not match regular expression {compiled.pattern}"
293  )
294 
295  return value
296 
297  return validator
298 
299 
300 def is_regex(value: Any) -> re.Pattern[Any]:
301  """Validate that a string is a valid regular expression."""
302  try:
303  r = re.compile(value)
304  except TypeError as err:
305  raise vol.Invalid(
306  f"value {value} is of the wrong type for a regular expression"
307  ) from err
308  except re.error as err:
309  raise vol.Invalid(f"value {value} is not a valid regular expression") from err
310  return r
311 
312 
313 @not_async_friendly
314 def isfile(value: Any) -> str:
315  """Validate that the value is an existing file."""
316  if value is None:
317  raise vol.Invalid("None is not file")
318  file_in = os.path.expanduser(str(value))
319 
320  if not os.path.isfile(file_in):
321  raise vol.Invalid("not a file")
322  if not os.access(file_in, os.R_OK):
323  raise vol.Invalid("file not readable")
324  return file_in
325 
326 
327 @not_async_friendly
328 def isdir(value: Any) -> str:
329  """Validate that the value is an existing dir."""
330  if value is None:
331  raise vol.Invalid("not a directory")
332  dir_in = os.path.expanduser(str(value))
333 
334  if not os.path.isdir(dir_in):
335  raise vol.Invalid("not a directory")
336  if not os.access(dir_in, os.R_OK):
337  raise vol.Invalid("directory not readable")
338  return dir_in
339 
340 
341 @overload
342 def ensure_list(value: None) -> list[Any]: ...
343 
344 
345 @overload
346 def ensure_list[_T](value: list[_T]) -> list[_T]: ...
347 
348 
349 @overload
350 def ensure_list[_T](value: list[_T] | _T) -> list[_T]: ...
351 
352 
353 def ensure_list[_T](value: _T | None) -> list[_T] | list[Any]:
354  """Wrap value in list if it is not one."""
355  if value is None:
356  return []
357  return cast("list[_T]", value) if isinstance(value, list) else [value]
358 
359 
360 def entity_id(value: Any) -> str:
361  """Validate Entity ID."""
362  str_value = string(value).lower()
363  if valid_entity_id(str_value):
364  return str_value
365 
366  raise vol.Invalid(f"Entity ID {value} is an invalid entity ID")
367 
368 
369 def entity_id_or_uuid(value: Any) -> str:
370  """Validate Entity specified by entity_id or uuid."""
371  with contextlib.suppress(vol.Invalid):
372  return entity_id(value)
373  with contextlib.suppress(vol.Invalid):
374  return fake_uuid4_hex(value)
375  raise vol.Invalid(f"Entity {value} is neither a valid entity ID nor a valid UUID")
376 
377 
378 def _entity_ids(value: str | list, allow_uuid: bool) -> list[str]:
379  """Help validate entity IDs or UUIDs."""
380  if value is None:
381  raise vol.Invalid("Entity IDs cannot be None")
382  if isinstance(value, str):
383  value = [ent_id.strip() for ent_id in value.split(",")]
384 
385  validator = entity_id_or_uuid if allow_uuid else entity_id
386  return [validator(ent_id) for ent_id in value]
387 
388 
389 def entity_ids(value: str | list) -> list[str]:
390  """Validate Entity IDs."""
391  return _entity_ids(value, False)
392 
393 
394 def entity_ids_or_uuids(value: str | list) -> list[str]:
395  """Validate entities specified by entity IDs or UUIDs."""
396  return _entity_ids(value, True)
397 
398 
399 comp_entity_ids = vol.Any(
400  vol.All(vol.Lower, vol.Any(ENTITY_MATCH_ALL, ENTITY_MATCH_NONE)), entity_ids
401 )
402 
403 
404 comp_entity_ids_or_uuids = vol.Any(
405  vol.All(vol.Lower, vol.Any(ENTITY_MATCH_ALL, ENTITY_MATCH_NONE)),
406  entity_ids_or_uuids,
407 )
408 
409 
410 def domain_key(config_key: Any) -> str:
411  """Validate a top level config key with an optional label and return the domain.
412 
413  A domain is separated from a label by one or more spaces, empty labels are not
414  allowed.
415 
416  Examples:
417  'hue' returns 'hue'
418  'hue 1' returns 'hue'
419  'hue 1' returns 'hue'
420  'hue ' raises
421  'hue ' raises
422 
423  """
424  if not isinstance(config_key, str):
425  raise vol.Invalid("invalid domain", path=[config_key])
426 
427  parts = config_key.partition(" ")
428  _domain = parts[0] if parts[2].strip(" ") else config_key
429  if not _domain or _domain.strip(" ") != _domain:
430  raise vol.Invalid("invalid domain", path=[config_key])
431 
432  return _domain
433 
434 
435 def entity_domain(domain: str | list[str]) -> Callable[[Any], str]:
436  """Validate that entity belong to domain."""
437  ent_domain = entities_domain(domain)
438 
439  def validate(value: str) -> str:
440  """Test if entity domain is domain."""
441  validated = ent_domain(value)
442  if len(validated) != 1:
443  raise vol.Invalid(f"Expected exactly 1 entity, got {len(validated)}")
444  return validated[0]
445 
446  return validate
447 
448 
449 def entities_domain(domain: str | list[str]) -> Callable[[str | list], list[str]]:
450  """Validate that entities belong to domain."""
451  if isinstance(domain, str):
452 
453  def check_invalid(val: str) -> bool:
454  return val != domain
455 
456  else:
457 
458  def check_invalid(val: str) -> bool:
459  return val not in domain
460 
461  def validate(values: str | list) -> list[str]:
462  """Test if entity domain is domain."""
463  values = entity_ids(values)
464  for ent_id in values:
465  if check_invalid(split_entity_id(ent_id)[0]):
466  raise vol.Invalid(
467  f"Entity ID '{ent_id}' does not belong to domain '{domain}'"
468  )
469  return values
470 
471  return validate
472 
473 
474 def enum(enumClass: type[Enum]) -> vol.All:
475  """Create validator for specified enum."""
476  return vol.All(vol.In(enumClass.__members__), enumClass.__getitem__)
477 
478 
479 def icon(value: Any) -> str:
480  """Validate icon."""
481  str_value = str(value)
482 
483  if ":" in str_value:
484  return str_value
485 
486  raise vol.Invalid('Icons should be specified in the form "prefix:name"')
487 
488 
489 _COLOR_HEX = re.compile(r"^#[0-9A-F]{6}$", re.IGNORECASE)
490 
491 
492 def color_hex(value: Any) -> str:
493  """Validate a hex color code."""
494  str_value = str(value)
495 
496  if not _COLOR_HEX.match(str_value):
497  raise vol.Invalid("Color should be in the format #RRGGBB")
498 
499  return str_value
500 
501 
502 _TIME_PERIOD_DICT_KEYS = ("days", "hours", "minutes", "seconds", "milliseconds")
503 
504 time_period_dict = vol.All(
505  dict,
506  vol.Schema(
507  {
508  "days": vol.Coerce(float),
509  "hours": vol.Coerce(float),
510  "minutes": vol.Coerce(float),
511  "seconds": vol.Coerce(float),
512  "milliseconds": vol.Coerce(float),
513  }
514  ),
515  has_at_least_one_key(*_TIME_PERIOD_DICT_KEYS),
516  lambda value: timedelta(**value),
517 )
518 
519 
520 def time(value: Any) -> time_sys:
521  """Validate and transform a time."""
522  if isinstance(value, time_sys):
523  return value
524 
525  try:
526  time_val = dt_util.parse_time(value)
527  except TypeError as err:
528  raise vol.Invalid("Not a parseable type") from err
529 
530  if time_val is None:
531  raise vol.Invalid(f"Invalid time specified: {value}")
532 
533  return time_val
534 
535 
536 def date(value: Any) -> date_sys:
537  """Validate and transform a date."""
538  if isinstance(value, date_sys):
539  return value
540 
541  try:
542  date_val = dt_util.parse_date(value)
543  except TypeError as err:
544  raise vol.Invalid("Not a parseable type") from err
545 
546  if date_val is None:
547  raise vol.Invalid("Could not parse date")
548 
549  return date_val
550 
551 
552 def time_period_str(value: str) -> timedelta:
553  """Validate and transform time offset."""
554  if isinstance(value, int): # type: ignore[unreachable]
555  raise vol.Invalid("Make sure you wrap time values in quotes")
556  if not isinstance(value, str):
557  raise vol.Invalid(TIME_PERIOD_ERROR.format(value))
558 
559  negative_offset = False
560  if value.startswith("-"):
561  negative_offset = True
562  value = value[1:]
563  elif value.startswith("+"):
564  value = value[1:]
565 
566  parsed = value.split(":")
567  if len(parsed) not in (2, 3):
568  raise vol.Invalid(TIME_PERIOD_ERROR.format(value))
569  try:
570  hour = int(parsed[0])
571  minute = int(parsed[1])
572  try:
573  second = float(parsed[2])
574  except IndexError:
575  second = 0
576  except ValueError as err:
577  raise vol.Invalid(TIME_PERIOD_ERROR.format(value)) from err
578 
579  offset = timedelta(hours=hour, minutes=minute, seconds=second)
580 
581  if negative_offset:
582  offset *= -1
583 
584  return offset
585 
586 
587 def time_period_seconds(value: float | str) -> timedelta:
588  """Validate and transform seconds to a time offset."""
589  try:
590  return timedelta(seconds=float(value))
591  except (ValueError, TypeError) as err:
592  raise vol.Invalid(f"Expected seconds, got {value}") from err
593 
594 
595 time_period = vol.Any(time_period_str, time_period_seconds, timedelta, time_period_dict)
596 
597 
598 def match_all[_T](value: _T) -> _T:
599  """Validate that matches all values."""
600  return value
601 
602 
603 def positive_timedelta(value: timedelta) -> timedelta:
604  """Validate timedelta is positive."""
605  if value < timedelta(0):
606  raise vol.Invalid("Time period should be positive")
607  return value
608 
609 
610 positive_time_period_dict = vol.All(time_period_dict, positive_timedelta)
611 positive_time_period = vol.All(time_period, positive_timedelta)
612 
613 
614 def remove_falsy[_T](value: list[_T]) -> list[_T]:
615  """Remove falsy values from a list."""
616  return [v for v in value if v]
617 
618 
619 def service(value: Any) -> str:
620  """Validate service."""
621  # Services use same format as entities so we can use same helper.
622  str_value = string(value).lower()
623  if valid_entity_id(str_value):
624  return str_value
625 
626  raise vol.Invalid(f"Service {value} does not match format <domain>.<name>")
627 
628 
629 def slug(value: Any) -> str:
630  """Validate value is a valid slug."""
631  if value is None:
632  raise vol.Invalid("Slug should not be None")
633  str_value = str(value)
634  slg = util_slugify(str_value)
635  if str_value == slg:
636  return str_value
637  raise vol.Invalid(f"invalid slug {value} (try {slg})")
638 
639 
641  value_schema: dict | Callable, *, slug_validator: Callable[[Any], str] = slug
642 ) -> Callable:
643  """Ensure dicts have slugs as keys.
644 
645  Replacement of vol.Schema({cv.slug: value_schema}) to prevent misleading
646  "Extra keys" errors from voluptuous.
647  """
648  schema = vol.Schema({str: value_schema})
649 
650  def verify(value: dict) -> dict:
651  """Validate all keys are slugs and then the value_schema."""
652  if not isinstance(value, dict):
653  raise vol.Invalid("expected dictionary")
654 
655  for key in value:
656  slug_validator(key)
657 
658  return cast(dict, schema(value))
659 
660  return verify
661 
662 
663 def slugify(value: Any) -> str:
664  """Coerce a value to a slug."""
665  if value is None:
666  raise vol.Invalid("Slug should not be None")
667  slg = util_slugify(str(value))
668  if slg:
669  return slg
670  raise vol.Invalid(f"Unable to slugify {value}")
671 
672 
673 def string(value: Any) -> str:
674  """Coerce value to string, except for None."""
675  if value is None:
676  raise vol.Invalid("string value is None")
677 
678  # This is expected to be the most common case, so check it first.
679  if (
680  type(value) is str # noqa: E721
681  or type(value) is NodeStrClass
682  or isinstance(value, str)
683  ):
684  return value
685 
686  if isinstance(value, template_helper.ResultWrapper):
687  value = value.render_result
688 
689  elif isinstance(value, (list, dict)):
690  raise vol.Invalid("value should be a string")
691 
692  return str(value)
693 
694 
695 def string_with_no_html(value: Any) -> str:
696  """Validate that the value is a string without HTML."""
697  value = string(value)
698  regex = re.compile(r"<[a-z].*?>", re.IGNORECASE)
699  if regex.search(value):
700  raise vol.Invalid("the string should not contain HTML")
701  return str(value)
702 
703 
704 def temperature_unit(value: Any) -> UnitOfTemperature:
705  """Validate and transform temperature unit."""
706  value = str(value).upper()
707  if value == "C":
708  return UnitOfTemperature.CELSIUS
709  if value == "F":
710  return UnitOfTemperature.FAHRENHEIT
711  raise vol.Invalid("invalid temperature unit (expected C or F)")
712 
713 
714 def template(value: Any | None) -> template_helper.Template:
715  """Validate a jinja2 template."""
716  if value is None:
717  raise vol.Invalid("template value is None")
718  if isinstance(value, (list, dict, template_helper.Template)):
719  raise vol.Invalid("template value should be a string")
720  if not (hass := _async_get_hass_or_none()):
721  # pylint: disable-next=import-outside-toplevel
722  from .frame import ReportBehavior, report_usage
723 
724  report_usage(
725  (
726  "validates schema outside the event loop, "
727  "which will stop working in HA Core 2025.10"
728  ),
729  core_behavior=ReportBehavior.LOG,
730  )
731 
732  template_value = template_helper.Template(str(value), hass)
733 
734  try:
735  template_value.ensure_valid()
736  except TemplateError as ex:
737  raise vol.Invalid(f"invalid template ({ex})") from ex
738  return template_value
739 
740 
741 def dynamic_template(value: Any | None) -> template_helper.Template:
742  """Validate a dynamic (non static) jinja2 template."""
743  if value is None:
744  raise vol.Invalid("template value is None")
745  if isinstance(value, (list, dict, template_helper.Template)):
746  raise vol.Invalid("template value should be a string")
747  if not template_helper.is_template_string(str(value)):
748  raise vol.Invalid("template value does not contain a dynamic template")
749  if not (hass := _async_get_hass_or_none()):
750  # pylint: disable-next=import-outside-toplevel
751  from .frame import ReportBehavior, report_usage
752 
753  report_usage(
754  (
755  "validates schema outside the event loop, "
756  "which will stop working in HA Core 2025.10"
757  ),
758  core_behavior=ReportBehavior.LOG,
759  )
760 
761  template_value = template_helper.Template(str(value), hass)
762 
763  try:
764  template_value.ensure_valid()
765  except TemplateError as ex:
766  raise vol.Invalid(f"invalid template ({ex})") from ex
767  return template_value
768 
769 
770 def template_complex(value: Any) -> Any:
771  """Validate a complex jinja2 template."""
772  if isinstance(value, list):
773  return_list = value.copy()
774  for idx, element in enumerate(return_list):
775  return_list[idx] = template_complex(element)
776  return return_list
777  if isinstance(value, dict):
778  return {
779  template_complex(key): template_complex(element)
780  for key, element in value.items()
781  }
782  if isinstance(value, str) and template_helper.is_template_string(value):
783  return template(value)
784 
785  return value
786 
787 
789  """Do basic validation of a positive time period expressed as a templated dict."""
790  if not isinstance(value, dict) or not value:
791  raise vol.Invalid("template should be a dict")
792  for key, element in value.items():
793  if not isinstance(key, str):
794  raise vol.Invalid("key should be a string")
795  if not template_helper.is_template_string(key):
796  vol.In(_TIME_PERIOD_DICT_KEYS)(key)
797  if not isinstance(element, str) or (
798  isinstance(element, str) and not template_helper.is_template_string(element)
799  ):
800  vol.All(vol.Coerce(float), vol.Range(min=0))(element)
801  return template_complex(value)
802 
803 
804 positive_time_period_template = vol.Any(
805  positive_time_period, dynamic_template, _positive_time_period_template_complex
806 )
807 
808 
809 def datetime(value: Any) -> datetime_sys:
810  """Validate datetime."""
811  if isinstance(value, datetime_sys):
812  return value
813 
814  try:
815  date_val = dt_util.parse_datetime(value)
816  except TypeError:
817  date_val = None
818 
819  if date_val is None:
820  raise vol.Invalid(f"Invalid datetime specified: {value}")
821 
822  return date_val
823 
824 
825 def time_zone(value: str) -> str:
826  """Validate timezone."""
827  if dt_util.get_time_zone(value) is not None:
828  return value
829  raise vol.Invalid(
830  "Invalid time zone passed in. Valid options can be found here: "
831  "http://en.wikipedia.org/wiki/List_of_tz_database_time_zones"
832  )
833 
834 
835 weekdays = vol.All(ensure_list, [vol.In(WEEKDAYS)])
836 
837 
838 def socket_timeout(value: Any | None) -> object:
839  """Validate timeout float > 0.0.
840 
841  None coerced to socket._GLOBAL_DEFAULT_TIMEOUT bare object.
842  """
843  if value is None:
844  return _GLOBAL_DEFAULT_TIMEOUT
845  try:
846  float_value = float(value)
847  if float_value > 0.0:
848  return float_value
849  except Exception as err:
850  raise vol.Invalid(f"Invalid socket timeout: {err}") from err
851  raise vol.Invalid("Invalid socket timeout value. float > 0.0 required.")
852 
853 
854 def url(
855  value: Any,
856  _schema_list: frozenset[UrlProtocolSchema] = EXTERNAL_URL_PROTOCOL_SCHEMA_LIST,
857 ) -> str:
858  """Validate an URL."""
859  url_in = str(value)
860 
861  if urlparse(url_in).scheme in _schema_list:
862  return cast(str, vol.Schema(vol.Url())(url_in))
863 
864  raise vol.Invalid("invalid url")
865 
866 
867 def configuration_url(value: Any) -> str:
868  """Validate an URL that allows the homeassistant schema."""
869  return url(value, CONFIGURATION_URL_PROTOCOL_SCHEMA_LIST)
870 
871 
872 def url_no_path(value: Any) -> str:
873  """Validate a url without a path."""
874  url_in = url(value)
875 
876  if urlparse(url_in).path not in ("", "/"):
877  raise vol.Invalid("url is not allowed to have a path component")
878 
879  return url_in
880 
881 
882 def x10_address(value: str) -> str:
883  """Validate an x10 address."""
884  regex = re.compile(r"([A-Pa-p]{1})(?:[2-9]|1[0-6]?)$")
885  if not regex.match(value):
886  raise vol.Invalid("Invalid X10 Address")
887  return str(value).lower()
888 
889 
890 def uuid4_hex(value: Any) -> str:
891  """Validate a v4 UUID in hex format."""
892  try:
893  result = UUID(value, version=4)
894  except (ValueError, AttributeError, TypeError) as error:
895  raise vol.Invalid("Invalid Version4 UUID", error_message=str(error)) from error
896 
897  if result.hex != value.lower():
898  # UUID() will create a uuid4 if input is invalid
899  raise vol.Invalid("Invalid Version4 UUID")
900 
901  return result.hex
902 
903 
904 _FAKE_UUID_4_HEX = re.compile(r"^[0-9a-f]{32}$")
905 
906 
907 def fake_uuid4_hex(value: Any) -> str:
908  """Validate a fake v4 UUID generated by random_uuid_hex."""
909  try:
910  if not _FAKE_UUID_4_HEX.match(value):
911  raise vol.Invalid("Invalid UUID")
912  except TypeError as exc:
913  raise vol.Invalid("Invalid UUID") from exc
914  return cast(str, value) # Pattern.match throws if input is not a string
915 
916 
917 def ensure_list_csv(value: Any) -> list:
918  """Ensure that input is a list or make one from comma-separated string."""
919  if isinstance(value, str):
920  return [member.strip() for member in value.split(",")]
921  return ensure_list(value)
922 
923 
925  """Multi select validator returning list of selected values."""
926 
927  def __init__(self, options: dict | list) -> None:
928  """Initialize multi select."""
929  self.optionsoptions = options
930 
931  def __call__(self, selected: list) -> list:
932  """Validate input."""
933  if not isinstance(selected, list):
934  raise vol.Invalid("Not a list")
935 
936  for value in selected:
937  if value not in self.optionsoptions:
938  raise vol.Invalid(f"{value} is not a valid option")
939 
940  return selected
941 
942 
944  key: str,
945  replacement_key: str | None,
946  default: Any | None,
947  raise_if_present: bool,
948  option_removed: bool,
949 ) -> Callable[[dict], dict]:
950  """Log key as deprecated and provide a replacement (if exists) or fail.
951 
952  Expected behavior:
953  - Outputs or throws the appropriate deprecation warning if key is detected
954  - Outputs or throws the appropriate error if key is detected
955  and removed from support
956  - Processes schema moving the value from key to replacement_key
957  - Processes schema changing nothing if only replacement_key provided
958  - No warning if only replacement_key provided
959  - No warning if neither key nor replacement_key are provided
960  - Adds replacement_key with default value in this case
961  """
962 
963  def validator(config: dict) -> dict:
964  """Check if key is in config and log warning or error."""
965  if key in config:
966  if option_removed:
967  level = logging.ERROR
968  option_status = "has been removed"
969  else:
970  level = logging.WARNING
971  option_status = "is deprecated"
972 
973  try:
974  near = (
975  f"near {config.__config_file__}" # type: ignore[attr-defined]
976  f":{config.__line__} " # type: ignore[attr-defined]
977  )
978  except AttributeError:
979  near = ""
980  arguments: tuple[str, ...]
981  if replacement_key:
982  warning = "The '%s' option %s%s, please replace it with '%s'"
983  arguments = (key, near, option_status, replacement_key)
984  else:
985  warning = (
986  "The '%s' option %s%s, please remove it from your configuration"
987  )
988  arguments = (key, near, option_status)
989 
990  if raise_if_present:
991  raise vol.Invalid(warning % arguments)
992 
993  get_integration_logger(__name__).log(level, warning, *arguments)
994  value = config[key]
995  if replacement_key or option_removed:
996  config.pop(key)
997  else:
998  value = default
999 
1000  keys = [key]
1001  if replacement_key:
1002  keys.append(replacement_key)
1003  if value is not None and (
1004  replacement_key not in config or default == config.get(replacement_key)
1005  ):
1006  config[replacement_key] = value
1007 
1008  return has_at_most_one_key(*keys)(config)
1009 
1010  return validator
1011 
1012 
1014  key: str,
1015  replacement_key: str | None = None,
1016  default: Any | None = None,
1017  raise_if_present: bool | None = False,
1018 ) -> Callable[[dict], dict]:
1019  """Log key as deprecated and provide a replacement (if exists).
1020 
1021  Expected behavior:
1022  - Outputs the appropriate deprecation warning if key is detected
1023  or raises an exception
1024  - Processes schema moving the value from key to replacement_key
1025  - Processes schema changing nothing if only replacement_key provided
1026  - No warning if only replacement_key provided
1027  - No warning if neither key nor replacement_key are provided
1028  - Adds replacement_key with default value in this case
1029  """
1030  return _deprecated_or_removed(
1031  key,
1032  replacement_key=replacement_key,
1033  default=default,
1034  raise_if_present=raise_if_present or False,
1035  option_removed=False,
1036  )
1037 
1038 
1040  key: str,
1041  default: Any | None = None,
1042  raise_if_present: bool | None = True,
1043 ) -> Callable[[dict], dict]:
1044  """Log key as deprecated and fail the config validation.
1045 
1046  Expected behavior:
1047  - Outputs the appropriate error if key is detected and removed from
1048  support or raises an exception.
1049  """
1050  return _deprecated_or_removed(
1051  key,
1052  replacement_key=None,
1053  default=default,
1054  raise_if_present=raise_if_present or False,
1055  option_removed=True,
1056  )
1057 
1058 
1060  key: str,
1061  value_schemas: dict[Hashable, VolSchemaType | Callable[[Any], dict[str, Any]]],
1062  default_schema: VolSchemaType | None = None,
1063  default_description: str | None = None,
1064 ) -> Callable[[Any], dict[Hashable, Any]]:
1065  """Create a validator that validates based on a value for specific key.
1066 
1067  This gives better error messages.
1068  """
1069 
1070  def key_value_validator(value: Any) -> dict[Hashable, Any]:
1071  if not isinstance(value, dict):
1072  raise vol.Invalid("Expected a dictionary")
1073 
1074  key_value = value.get(key)
1075 
1076  if isinstance(key_value, Hashable) and key_value in value_schemas:
1077  return cast(dict[Hashable, Any], value_schemas[key_value](value))
1078 
1079  if default_schema:
1080  with contextlib.suppress(vol.Invalid):
1081  return cast(dict[Hashable, Any], default_schema(value))
1082 
1083  alternatives = ", ".join(str(alternative) for alternative in value_schemas)
1084  if default_description:
1085  alternatives = f"{alternatives}, {default_description}"
1086  raise vol.Invalid(
1087  f"Unexpected value for {key}: '{key_value}'. Expected {alternatives}"
1088  )
1089 
1090  return key_value_validator
1091 
1092 
1093 # Validator helpers
1094 
1095 
1096 def key_dependency[_KT: Hashable, _VT](
1097  key: Hashable, dependency: Hashable
1098 ) -> Callable[[dict[_KT, _VT]], dict[_KT, _VT]]:
1099  """Validate that all dependencies exist for key."""
1100 
1101  def validator(value: dict[_KT, _VT]) -> dict[_KT, _VT]:
1102  """Test dependencies."""
1103  if not isinstance(value, dict):
1104  raise vol.Invalid("key dependencies require a dict")
1105  if key in value and dependency not in value:
1106  raise vol.Invalid(
1107  f'dependency violation - key "{key}" requires '
1108  f'key "{dependency}" to exist'
1109  )
1110 
1111  return value
1112 
1113  return validator
1114 
1115 
1116 def custom_serializer(schema: Any) -> Any:
1117  """Serialize additional types for voluptuous_serialize."""
1118  return _custom_serializer(schema, allow_section=True)
1119 
1120 
1121 def _custom_serializer(schema: Any, *, allow_section: bool) -> Any:
1122  """Serialize additional types for voluptuous_serialize."""
1123  from homeassistant import data_entry_flow # pylint: disable=import-outside-toplevel
1124 
1125  from . import selector # pylint: disable=import-outside-toplevel
1126 
1127  if schema is positive_time_period_dict:
1128  return {"type": "positive_time_period_dict"}
1129 
1130  if schema is string:
1131  return {"type": "string"}
1132 
1133  if schema is boolean:
1134  return {"type": "boolean"}
1135 
1136  if isinstance(schema, data_entry_flow.section):
1137  if not allow_section:
1138  raise ValueError("Nesting expandable sections is not supported")
1139  return {
1140  "type": "expandable",
1141  "schema": voluptuous_serialize.convert(
1142  schema.schema,
1143  custom_serializer=functools.partial(
1144  _custom_serializer, allow_section=False
1145  ),
1146  ),
1147  "expanded": not schema.options["collapsed"],
1148  }
1149 
1150  if isinstance(schema, multi_select):
1151  return {"type": "multi_select", "options": schema.options}
1152 
1153  if isinstance(schema, selector.Selector):
1154  return schema.serialize()
1155 
1156  return voluptuous_serialize.UNSUPPORTED
1157 
1158 
1159 def expand_condition_shorthand(value: Any | None) -> Any:
1160  """Expand boolean condition shorthand notations."""
1161 
1162  if not isinstance(value, dict) or CONF_CONDITIONS in value:
1163  return value
1164 
1165  for key, schema in (
1166  ("and", AND_CONDITION_SHORTHAND_SCHEMA),
1167  ("or", OR_CONDITION_SHORTHAND_SCHEMA),
1168  ("not", NOT_CONDITION_SHORTHAND_SCHEMA),
1169  ):
1170  try:
1171  schema(value)
1172  return {
1173  CONF_CONDITION: key,
1174  CONF_CONDITIONS: value[key],
1175  **{k: value[k] for k in value if k != key},
1176  }
1177  except vol.MultipleInvalid:
1178  pass
1179 
1180  if isinstance(value.get(CONF_CONDITION), list):
1181  try:
1183  return {
1184  CONF_CONDITION: "and",
1185  CONF_CONDITIONS: value[CONF_CONDITION],
1186  **{k: value[k] for k in value if k != CONF_CONDITION},
1187  }
1188  except vol.MultipleInvalid:
1189  pass
1190 
1191  return value
1192 
1193 
1194 # Schemas
1195 def empty_config_schema(domain: str) -> Callable[[dict], dict]:
1196  """Return a config schema which logs if there are configuration parameters."""
1197 
1198  def validator(config: dict) -> dict:
1199  if config_domain := config.get(domain):
1200  get_integration_logger(__name__).error(
1201  (
1202  "The %s integration does not support any configuration parameters, "
1203  "got %s. Please remove the configuration parameters from your "
1204  "configuration."
1205  ),
1206  domain,
1207  config_domain,
1208  )
1209  return config
1210 
1211  return validator
1212 
1213 
1215  domain: str,
1216  issue_base: str,
1217  translation_key: str,
1218  translation_placeholders: dict[str, str],
1219 ) -> Callable[[dict], dict]:
1220  """Return a config schema which logs if attempted to setup from YAML."""
1221 
1222  def raise_issue() -> None:
1223  # pylint: disable-next=import-outside-toplevel
1224  from .issue_registry import IssueSeverity, async_create_issue
1225 
1226  # HomeAssistantError is raised if called from the wrong thread
1227  with contextlib.suppress(HomeAssistantError):
1228  hass = async_get_hass()
1230  hass,
1231  HOMEASSISTANT_DOMAIN,
1232  f"{issue_base}_{domain}",
1233  is_fixable=False,
1234  issue_domain=domain,
1235  severity=IssueSeverity.ERROR,
1236  translation_key=translation_key,
1237  translation_placeholders={"domain": domain} | translation_placeholders,
1238  )
1239 
1240  def validator(config: dict) -> dict:
1241  if domain in config:
1242  get_integration_logger(__name__).error(
1243  (
1244  "The %s integration does not support YAML setup, please remove it "
1245  "from your configuration file"
1246  ),
1247  domain,
1248  )
1249  raise_issue()
1250  return config
1251 
1252  return validator
1253 
1254 
1255 def config_entry_only_config_schema(domain: str) -> Callable[[dict], dict]:
1256  """Return a config schema which logs if attempted to setup from YAML.
1257 
1258  Use this when an integration's __init__.py defines setup or async_setup
1259  but setup from yaml is not supported.
1260  """
1261 
1262  return _no_yaml_config_schema(
1263  domain,
1264  "config_entry_only",
1265  "config_entry_only",
1266  {"add_integration": f"/config/integrations/dashboard/add?domain={domain}"},
1267  )
1268 
1269 
1270 def platform_only_config_schema(domain: str) -> Callable[[dict], dict]:
1271  """Return a config schema which logs if attempted to setup from YAML.
1272 
1273  Use this when an integration's __init__.py defines setup or async_setup
1274  but setup from the integration key is not supported.
1275  """
1276 
1277  return _no_yaml_config_schema(
1278  domain,
1279  "platform_only",
1280  "platform_only",
1281  {},
1282  )
1283 
1284 
1285 PLATFORM_SCHEMA = vol.Schema(
1286  {
1287  vol.Required(CONF_PLATFORM): string,
1288  vol.Optional(CONF_ENTITY_NAMESPACE): string,
1289  vol.Optional(CONF_SCAN_INTERVAL): time_period,
1290  }
1291 )
1292 
1293 PLATFORM_SCHEMA_BASE = PLATFORM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA)
1294 
1295 ENTITY_SERVICE_FIELDS: VolDictType = {
1296  # Either accept static entity IDs, a single dynamic template or a mixed list
1297  # of static and dynamic templates. While this could be solved with a single
1298  # complex template, handling it like this, keeps config validation useful.
1299  vol.Optional(ATTR_ENTITY_ID): vol.Any(
1300  comp_entity_ids, dynamic_template, vol.All(list, template_complex)
1301  ),
1302  vol.Optional(ATTR_DEVICE_ID): vol.Any(
1303  ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)])
1304  ),
1305  vol.Optional(ATTR_AREA_ID): vol.Any(
1306  ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)])
1307  ),
1308  vol.Optional(ATTR_FLOOR_ID): vol.Any(
1309  ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)])
1310  ),
1311  vol.Optional(ATTR_LABEL_ID): vol.Any(
1312  ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)])
1313  ),
1314 }
1315 
1316 TARGET_SERVICE_FIELDS = {
1317  # Same as ENTITY_SERVICE_FIELDS but supports specifying entity by entity registry
1318  # ID.
1319  # Either accept static entity IDs, a single dynamic template or a mixed list
1320  # of static and dynamic templates. While this could be solved with a single
1321  # complex template, handling it like this, keeps config validation useful.
1322  vol.Optional(ATTR_ENTITY_ID): vol.Any(
1323  comp_entity_ids_or_uuids, dynamic_template, vol.All(list, template_complex)
1324  ),
1325  vol.Optional(ATTR_DEVICE_ID): vol.Any(
1326  ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)])
1327  ),
1328  vol.Optional(ATTR_AREA_ID): vol.Any(
1329  ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)])
1330  ),
1331  vol.Optional(ATTR_FLOOR_ID): vol.Any(
1332  ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)])
1333  ),
1334  vol.Optional(ATTR_LABEL_ID): vol.Any(
1335  ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)])
1336  ),
1337 }
1338 
1339 
1340 _HAS_ENTITY_SERVICE_FIELD = has_at_least_one_key(*ENTITY_SERVICE_FIELDS)
1341 
1342 
1343 def is_entity_service_schema(validator: VolSchemaType) -> bool:
1344  """Check if the passed validator is an entity schema validator.
1345 
1346  The validator must be either of:
1347  - A validator returned by cv._make_entity_service_schema
1348  - A validator returned by cv._make_entity_service_schema, wrapped in a vol.Schema
1349  - A validator returned by cv._make_entity_service_schema, wrapped in a vol.All
1350  Nesting is allowed.
1351  """
1352  if hasattr(validator, "_entity_service_schema"):
1353  return True
1354  if isinstance(validator, (vol.All)):
1355  return any(is_entity_service_schema(val) for val in validator.validators)
1356  if isinstance(validator, (vol.Schema)):
1357  return is_entity_service_schema(validator.schema)
1358 
1359  return False
1360 
1361 
1362 def _make_entity_service_schema(schema: dict, extra: int) -> VolSchemaType:
1363  """Create an entity service schema."""
1364  validator = vol.All(
1365  vol.Schema(
1366  {
1367  # The frontend stores data here. Don't use in core.
1368  vol.Remove("metadata"): dict,
1369  **schema,
1370  **ENTITY_SERVICE_FIELDS,
1371  },
1372  extra=extra,
1373  ),
1374  _HAS_ENTITY_SERVICE_FIELD,
1375  )
1376  setattr(validator, "_entity_service_schema", True)
1377  return validator
1378 
1379 
1380 BASE_ENTITY_SCHEMA = _make_entity_service_schema({}, vol.PREVENT_EXTRA)
1381 
1382 
1384  schema: dict | None, *, extra: int = vol.PREVENT_EXTRA
1385 ) -> VolSchemaType:
1386  """Create an entity service schema."""
1387  if not schema and extra == vol.PREVENT_EXTRA:
1388  # If the schema is empty and we don't allow extra keys, we can return
1389  # the base schema and avoid compiling a new schema which is the case
1390  # for ~50% of services.
1391  return BASE_ENTITY_SCHEMA
1392  return _make_entity_service_schema(schema or {}, extra)
1393 
1394 
1395 SCRIPT_CONVERSATION_RESPONSE_SCHEMA = vol.Any(template, None)
1396 
1397 
1398 SCRIPT_VARIABLES_SCHEMA = vol.All(
1399  vol.Schema({str: template_complex}),
1400  # pylint: disable-next=unnecessary-lambda
1401  lambda val: script_variables_helper.ScriptVariables(val),
1402 )
1403 
1404 
1405 def script_action(value: Any) -> dict:
1406  """Validate a script action."""
1407  if not isinstance(value, dict):
1408  raise vol.Invalid("expected dictionary")
1409 
1410  try:
1411  action = determine_script_action(value)
1412  except ValueError as err:
1413  raise vol.Invalid(str(err)) from err
1414 
1415  return ACTION_TYPE_SCHEMAS[action](value)
1416 
1417 
1418 SCRIPT_SCHEMA = vol.All(ensure_list, [script_action])
1419 
1420 SCRIPT_ACTION_BASE_SCHEMA: VolDictType = {
1421  vol.Optional(CONF_ALIAS): string,
1422  vol.Optional(CONF_CONTINUE_ON_ERROR): boolean,
1423  vol.Optional(CONF_ENABLED): vol.Any(boolean, template),
1424 }
1425 
1426 EVENT_SCHEMA = vol.Schema(
1427  {
1428  **SCRIPT_ACTION_BASE_SCHEMA,
1429  vol.Required(CONF_EVENT): string,
1430  vol.Optional(CONF_EVENT_DATA): vol.All(dict, template_complex),
1431  vol.Optional(CONF_EVENT_DATA_TEMPLATE): vol.All(dict, template_complex),
1432  }
1433 )
1434 
1435 
1436 def _backward_compat_service_schema(value: Any | None) -> Any:
1437  """Backward compatibility for service schemas."""
1438 
1439  if not isinstance(value, dict):
1440  return value
1441 
1442  # `service` has been renamed to `action`
1443  if CONF_SERVICE in value:
1444  if CONF_ACTION in value:
1445  raise vol.Invalid(
1446  "Cannot specify both 'service' and 'action'. Please use 'action' only."
1447  )
1448  value[CONF_ACTION] = value.pop(CONF_SERVICE)
1449 
1450  return value
1451 
1452 
1453 SERVICE_SCHEMA = vol.All(
1454  _backward_compat_service_schema,
1455  vol.Schema(
1456  {
1457  **SCRIPT_ACTION_BASE_SCHEMA,
1458  vol.Exclusive(CONF_ACTION, "service name"): vol.Any(
1459  service, dynamic_template
1460  ),
1461  vol.Exclusive(CONF_SERVICE_TEMPLATE, "service name"): vol.Any(
1462  service, dynamic_template
1463  ),
1464  vol.Optional(CONF_SERVICE_DATA): vol.Any(
1465  template, vol.All(dict, template_complex)
1466  ),
1467  vol.Optional(CONF_SERVICE_DATA_TEMPLATE): vol.Any(
1468  template, vol.All(dict, template_complex)
1469  ),
1470  vol.Optional(CONF_ENTITY_ID): comp_entity_ids,
1471  vol.Optional(CONF_TARGET): vol.Any(TARGET_SERVICE_FIELDS, dynamic_template),
1472  vol.Optional(CONF_RESPONSE_VARIABLE): str,
1473  # The frontend stores data here. Don't use in core.
1474  vol.Remove("metadata"): dict,
1475  }
1476  ),
1477  has_at_least_one_key(CONF_ACTION, CONF_SERVICE_TEMPLATE),
1478 )
1479 
1480 NUMERIC_STATE_THRESHOLD_SCHEMA = vol.Any(
1481  vol.Coerce(float),
1482  vol.All(str, entity_domain(["input_number", "number", "sensor", "zone"])),
1483 )
1484 
1485 CONDITION_BASE_SCHEMA: VolDictType = {
1486  vol.Optional(CONF_ALIAS): string,
1487  vol.Optional(CONF_ENABLED): vol.Any(boolean, template),
1488 }
1489 
1490 NUMERIC_STATE_CONDITION_SCHEMA = vol.All(
1491  vol.Schema(
1492  {
1493  **CONDITION_BASE_SCHEMA,
1494  vol.Required(CONF_CONDITION): "numeric_state",
1495  vol.Required(CONF_ENTITY_ID): entity_ids_or_uuids,
1496  vol.Optional(CONF_ATTRIBUTE): str,
1497  CONF_BELOW: NUMERIC_STATE_THRESHOLD_SCHEMA,
1498  CONF_ABOVE: NUMERIC_STATE_THRESHOLD_SCHEMA,
1499  vol.Optional(CONF_VALUE_TEMPLATE): template,
1500  }
1501  ),
1502  has_at_least_one_key(CONF_BELOW, CONF_ABOVE),
1503 )
1504 
1505 STATE_CONDITION_BASE_SCHEMA = {
1506  **CONDITION_BASE_SCHEMA,
1507  vol.Required(CONF_CONDITION): "state",
1508  vol.Required(CONF_ENTITY_ID): entity_ids_or_uuids,
1509  vol.Optional(CONF_MATCH, default=ENTITY_MATCH_ALL): vol.All(
1510  vol.Lower, vol.Any(ENTITY_MATCH_ALL, ENTITY_MATCH_ANY)
1511  ),
1512  vol.Optional(CONF_ATTRIBUTE): str,
1513  vol.Optional(CONF_FOR): positive_time_period_template,
1514  # To support use_trigger_value in automation
1515  # Deprecated 2016/04/25
1516  vol.Optional("from"): str,
1517 }
1518 
1519 STATE_CONDITION_STATE_SCHEMA = vol.Schema(
1520  {
1521  **STATE_CONDITION_BASE_SCHEMA,
1522  vol.Required(CONF_STATE): vol.Any(str, [str]),
1523  }
1524 )
1525 
1526 STATE_CONDITION_ATTRIBUTE_SCHEMA = vol.Schema(
1527  {
1528  **STATE_CONDITION_BASE_SCHEMA,
1529  vol.Required(CONF_STATE): match_all,
1530  }
1531 )
1532 
1533 
1534 def STATE_CONDITION_SCHEMA(value: Any) -> dict[str, Any]:
1535  """Validate a state condition."""
1536  if not isinstance(value, dict):
1537  raise vol.Invalid("Expected a dictionary")
1538 
1539  if CONF_ATTRIBUTE in value:
1540  validated: dict[str, Any] = STATE_CONDITION_ATTRIBUTE_SCHEMA(value)
1541  else:
1542  validated = STATE_CONDITION_STATE_SCHEMA(value)
1543 
1544  return key_dependency("for", "state")(validated)
1545 
1546 
1547 SUN_CONDITION_SCHEMA = vol.All(
1548  vol.Schema(
1549  {
1550  **CONDITION_BASE_SCHEMA,
1551  vol.Required(CONF_CONDITION): "sun",
1552  vol.Optional("before"): sun_event,
1553  vol.Optional("before_offset"): time_period,
1554  vol.Optional("after"): vol.All(
1555  vol.Lower, vol.Any(SUN_EVENT_SUNSET, SUN_EVENT_SUNRISE)
1556  ),
1557  vol.Optional("after_offset"): time_period,
1558  }
1559  ),
1560  has_at_least_one_key("before", "after"),
1561 )
1562 
1563 TEMPLATE_CONDITION_SCHEMA = vol.Schema(
1564  {
1565  **CONDITION_BASE_SCHEMA,
1566  vol.Required(CONF_CONDITION): "template",
1567  vol.Required(CONF_VALUE_TEMPLATE): template,
1568  }
1569 )
1570 
1571 TIME_CONDITION_SCHEMA = vol.All(
1572  vol.Schema(
1573  {
1574  **CONDITION_BASE_SCHEMA,
1575  vol.Required(CONF_CONDITION): "time",
1576  vol.Optional("before"): vol.Any(
1577  time, vol.All(str, entity_domain(["input_datetime", "time", "sensor"]))
1578  ),
1579  vol.Optional("after"): vol.Any(
1580  time, vol.All(str, entity_domain(["input_datetime", "time", "sensor"]))
1581  ),
1582  vol.Optional("weekday"): weekdays,
1583  }
1584  ),
1585  has_at_least_one_key("before", "after", "weekday"),
1586 )
1587 
1588 TRIGGER_CONDITION_SCHEMA = vol.Schema(
1589  {
1590  **CONDITION_BASE_SCHEMA,
1591  vol.Required(CONF_CONDITION): "trigger",
1592  vol.Required(CONF_ID): vol.All(ensure_list, [string]),
1593  }
1594 )
1595 
1596 ZONE_CONDITION_SCHEMA = vol.Schema(
1597  {
1598  **CONDITION_BASE_SCHEMA,
1599  vol.Required(CONF_CONDITION): "zone",
1600  vol.Required(CONF_ENTITY_ID): entity_ids,
1601  vol.Required("zone"): entity_ids,
1602  # To support use_trigger_value in automation
1603  # Deprecated 2016/04/25
1604  vol.Optional("event"): vol.Any("enter", "leave"),
1605  }
1606 )
1607 
1608 AND_CONDITION_SCHEMA = vol.Schema(
1609  {
1610  **CONDITION_BASE_SCHEMA,
1611  vol.Required(CONF_CONDITION): "and",
1612  vol.Required(CONF_CONDITIONS): vol.All(
1613  ensure_list,
1614  # pylint: disable-next=unnecessary-lambda
1615  [lambda value: CONDITION_SCHEMA(value)],
1616  ),
1617  }
1618 )
1619 
1620 AND_CONDITION_SHORTHAND_SCHEMA = vol.Schema(
1621  {
1622  **CONDITION_BASE_SCHEMA,
1623  vol.Required("and"): vol.All(
1624  ensure_list,
1625  # pylint: disable-next=unnecessary-lambda
1626  [lambda value: CONDITION_SCHEMA(value)],
1627  ),
1628  }
1629 )
1630 
1631 OR_CONDITION_SCHEMA = vol.Schema(
1632  {
1633  **CONDITION_BASE_SCHEMA,
1634  vol.Required(CONF_CONDITION): "or",
1635  vol.Required(CONF_CONDITIONS): vol.All(
1636  ensure_list,
1637  # pylint: disable-next=unnecessary-lambda
1638  [lambda value: CONDITION_SCHEMA(value)],
1639  ),
1640  }
1641 )
1642 
1643 OR_CONDITION_SHORTHAND_SCHEMA = vol.Schema(
1644  {
1645  **CONDITION_BASE_SCHEMA,
1646  vol.Required("or"): vol.All(
1647  ensure_list,
1648  # pylint: disable-next=unnecessary-lambda
1649  [lambda value: CONDITION_SCHEMA(value)],
1650  ),
1651  }
1652 )
1653 
1654 NOT_CONDITION_SCHEMA = vol.Schema(
1655  {
1656  **CONDITION_BASE_SCHEMA,
1657  vol.Required(CONF_CONDITION): "not",
1658  vol.Required(CONF_CONDITIONS): vol.All(
1659  ensure_list,
1660  # pylint: disable-next=unnecessary-lambda
1661  [lambda value: CONDITION_SCHEMA(value)],
1662  ),
1663  }
1664 )
1665 
1666 NOT_CONDITION_SHORTHAND_SCHEMA = vol.Schema(
1667  {
1668  **CONDITION_BASE_SCHEMA,
1669  vol.Required("not"): vol.All(
1670  ensure_list,
1671  # pylint: disable-next=unnecessary-lambda
1672  [lambda value: CONDITION_SCHEMA(value)],
1673  ),
1674  }
1675 )
1676 
1677 DEVICE_CONDITION_BASE_SCHEMA = vol.Schema(
1678  {
1679  **CONDITION_BASE_SCHEMA,
1680  vol.Required(CONF_CONDITION): "device",
1681  vol.Required(CONF_DEVICE_ID): str,
1682  vol.Required(CONF_DOMAIN): str,
1683  vol.Remove("metadata"): dict,
1684  }
1685 )
1686 
1687 DEVICE_CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA)
1688 
1689 dynamic_template_condition_action = vol.All(
1690  # Wrap a shorthand template condition in a template condition
1691  dynamic_template,
1692  lambda config: {
1693  CONF_VALUE_TEMPLATE: config,
1694  CONF_CONDITION: "template",
1695  },
1696 )
1697 
1698 CONDITION_SHORTHAND_SCHEMA = vol.Schema(
1699  {
1700  **CONDITION_BASE_SCHEMA,
1701  vol.Required(CONF_CONDITION): vol.All(
1702  ensure_list,
1703  # pylint: disable-next=unnecessary-lambda
1704  [lambda value: CONDITION_SCHEMA(value)],
1705  ),
1706  }
1707 )
1708 
1709 CONDITION_SCHEMA: vol.Schema = vol.Schema(
1710  vol.Any(
1711  vol.All(
1712  expand_condition_shorthand,
1714  CONF_CONDITION,
1715  {
1716  "and": AND_CONDITION_SCHEMA,
1717  "device": DEVICE_CONDITION_SCHEMA,
1718  "not": NOT_CONDITION_SCHEMA,
1719  "numeric_state": NUMERIC_STATE_CONDITION_SCHEMA,
1720  "or": OR_CONDITION_SCHEMA,
1721  "state": STATE_CONDITION_SCHEMA,
1722  "sun": SUN_CONDITION_SCHEMA,
1723  "template": TEMPLATE_CONDITION_SCHEMA,
1724  "time": TIME_CONDITION_SCHEMA,
1725  "trigger": TRIGGER_CONDITION_SCHEMA,
1726  "zone": ZONE_CONDITION_SCHEMA,
1727  },
1728  ),
1729  ),
1730  dynamic_template_condition_action,
1731  )
1732 )
1733 
1734 CONDITIONS_SCHEMA = vol.All(ensure_list, [CONDITION_SCHEMA])
1735 
1736 dynamic_template_condition_action = vol.All(
1737  # Wrap a shorthand template condition action in a template condition
1738  vol.Schema(
1739  {**CONDITION_BASE_SCHEMA, vol.Required(CONF_CONDITION): dynamic_template}
1740  ),
1741  lambda config: {
1742  **config,
1743  CONF_VALUE_TEMPLATE: config[CONF_CONDITION],
1744  CONF_CONDITION: "template",
1745  },
1746 )
1747 
1748 
1749 CONDITION_ACTION_SCHEMA: vol.Schema = vol.Schema(
1750  vol.All(
1751  expand_condition_shorthand,
1753  CONF_CONDITION,
1754  {
1755  "and": AND_CONDITION_SCHEMA,
1756  "device": DEVICE_CONDITION_SCHEMA,
1757  "not": NOT_CONDITION_SCHEMA,
1758  "numeric_state": NUMERIC_STATE_CONDITION_SCHEMA,
1759  "or": OR_CONDITION_SCHEMA,
1760  "state": STATE_CONDITION_SCHEMA,
1761  "sun": SUN_CONDITION_SCHEMA,
1762  "template": TEMPLATE_CONDITION_SCHEMA,
1763  "time": TIME_CONDITION_SCHEMA,
1764  "trigger": TRIGGER_CONDITION_SCHEMA,
1765  "zone": ZONE_CONDITION_SCHEMA,
1766  },
1767  dynamic_template_condition_action,
1768  "a list of conditions or a valid template",
1769  ),
1770  )
1771 )
1772 
1773 
1774 def _trigger_pre_validator(value: Any | None) -> Any:
1775  """Rewrite trigger `trigger` to `platform`.
1776 
1777  `platform` has been renamed to `trigger` in user documentation and in the automation
1778  editor. The Python trigger implementation still uses `platform`, so we need to
1779  rename `trigger` to `platform.
1780  """
1781 
1782  if not isinstance(value, Mapping):
1783  # If the value is not a mapping, we let that be handled by the TRIGGER_SCHEMA
1784  return value
1785 
1786  if CONF_TRIGGER in value:
1787  if CONF_PLATFORM in value:
1788  raise vol.Invalid(
1789  "Cannot specify both 'platform' and 'trigger'. Please use 'trigger' only."
1790  )
1791  value = dict(value)
1792  value[CONF_PLATFORM] = value.pop(CONF_TRIGGER)
1793  elif CONF_PLATFORM not in value:
1794  raise vol.Invalid("required key not provided", [CONF_TRIGGER])
1795 
1796  return value
1797 
1798 
1799 TRIGGER_BASE_SCHEMA = vol.Schema(
1800  {
1801  vol.Optional(CONF_ALIAS): str,
1802  vol.Required(CONF_PLATFORM): str,
1803  vol.Optional(CONF_ID): str,
1804  vol.Optional(CONF_VARIABLES): SCRIPT_VARIABLES_SCHEMA,
1805  vol.Optional(CONF_ENABLED): vol.Any(boolean, template),
1806  }
1807 )
1808 
1809 
1810 _base_trigger_validator_schema = TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA)
1811 
1812 
1813 def _base_trigger_list_flatten(triggers: list[Any]) -> list[Any]:
1814  """Flatten trigger arrays containing 'triggers:' sublists into a single list of triggers."""
1815  flatlist = []
1816  for t in triggers:
1817  if CONF_TRIGGERS in t and len(t) == 1:
1818  triggerlist = ensure_list(t[CONF_TRIGGERS])
1819  flatlist.extend(triggerlist)
1820  else:
1821  flatlist.append(t)
1822 
1823  return flatlist
1824 
1825 
1826 # This is first round of validation, we don't want to process the config here already,
1827 # just ensure basics as platform and ID are there.
1828 def _base_trigger_validator(value: Any) -> Any:
1830  return value
1831 
1832 
1833 TRIGGER_SCHEMA = vol.All(
1834  ensure_list,
1835  _base_trigger_list_flatten,
1836  [vol.All(_trigger_pre_validator, _base_trigger_validator)],
1837 )
1838 
1839 _SCRIPT_DELAY_SCHEMA = vol.Schema(
1840  {
1841  **SCRIPT_ACTION_BASE_SCHEMA,
1842  vol.Required(CONF_DELAY): positive_time_period_template,
1843  }
1844 )
1845 
1846 _SCRIPT_WAIT_TEMPLATE_SCHEMA = vol.Schema(
1847  {
1848  **SCRIPT_ACTION_BASE_SCHEMA,
1849  vol.Required(CONF_WAIT_TEMPLATE): template,
1850  vol.Optional(CONF_TIMEOUT): positive_time_period_template,
1851  vol.Optional(CONF_CONTINUE_ON_TIMEOUT): boolean,
1852  }
1853 )
1854 
1855 DEVICE_ACTION_BASE_SCHEMA = vol.Schema(
1856  {
1857  **SCRIPT_ACTION_BASE_SCHEMA,
1858  vol.Required(CONF_DEVICE_ID): string,
1859  vol.Required(CONF_DOMAIN): str,
1860  vol.Remove("metadata"): dict,
1861  }
1862 )
1863 
1864 DEVICE_ACTION_SCHEMA = DEVICE_ACTION_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA)
1865 
1866 _SCRIPT_SCENE_SCHEMA = vol.Schema(
1867  {**SCRIPT_ACTION_BASE_SCHEMA, vol.Required(CONF_SCENE): entity_domain("scene")}
1868 )
1869 
1870 _SCRIPT_REPEAT_SCHEMA = vol.Schema(
1871  {
1872  **SCRIPT_ACTION_BASE_SCHEMA,
1873  vol.Required(CONF_REPEAT): vol.All(
1874  {
1875  vol.Exclusive(CONF_COUNT, "repeat"): vol.Any(vol.Coerce(int), template),
1876  vol.Exclusive(CONF_FOR_EACH, "repeat"): vol.Any(
1877  dynamic_template, vol.All(list, template_complex)
1878  ),
1879  vol.Exclusive(CONF_WHILE, "repeat"): vol.All(
1880  ensure_list, [CONDITION_SCHEMA]
1881  ),
1882  vol.Exclusive(CONF_UNTIL, "repeat"): vol.All(
1883  ensure_list, [CONDITION_SCHEMA]
1884  ),
1885  vol.Required(CONF_SEQUENCE): SCRIPT_SCHEMA,
1886  },
1887  has_at_least_one_key(CONF_COUNT, CONF_FOR_EACH, CONF_WHILE, CONF_UNTIL),
1888  ),
1889  }
1890 )
1891 
1892 _SCRIPT_CHOOSE_SCHEMA = vol.Schema(
1893  {
1894  **SCRIPT_ACTION_BASE_SCHEMA,
1895  vol.Required(CONF_CHOOSE): vol.All(
1896  ensure_list,
1897  [
1898  {
1899  vol.Optional(CONF_ALIAS): string,
1900  vol.Required(CONF_CONDITIONS): vol.All(
1901  ensure_list, [CONDITION_SCHEMA]
1902  ),
1903  vol.Required(CONF_SEQUENCE): SCRIPT_SCHEMA,
1904  }
1905  ],
1906  ),
1907  vol.Optional(CONF_DEFAULT): SCRIPT_SCHEMA,
1908  }
1909 )
1910 
1911 _SCRIPT_WAIT_FOR_TRIGGER_SCHEMA = vol.Schema(
1912  {
1913  **SCRIPT_ACTION_BASE_SCHEMA,
1914  vol.Required(CONF_WAIT_FOR_TRIGGER): TRIGGER_SCHEMA,
1915  vol.Optional(CONF_TIMEOUT): positive_time_period_template,
1916  vol.Optional(CONF_CONTINUE_ON_TIMEOUT): boolean,
1917  }
1918 )
1919 
1920 _SCRIPT_IF_SCHEMA = vol.Schema(
1921  {
1922  **SCRIPT_ACTION_BASE_SCHEMA,
1923  vol.Required(CONF_IF): vol.All(ensure_list, [CONDITION_SCHEMA]),
1924  vol.Required(CONF_THEN): SCRIPT_SCHEMA,
1925  vol.Optional(CONF_ELSE): SCRIPT_SCHEMA,
1926  }
1927 )
1928 
1929 _SCRIPT_SET_SCHEMA = vol.Schema(
1930  {
1931  **SCRIPT_ACTION_BASE_SCHEMA,
1932  vol.Required(CONF_VARIABLES): SCRIPT_VARIABLES_SCHEMA,
1933  }
1934 )
1935 
1936 _SCRIPT_SET_CONVERSATION_RESPONSE_SCHEMA = vol.Schema(
1937  {
1938  **SCRIPT_ACTION_BASE_SCHEMA,
1939  vol.Required(
1940  CONF_SET_CONVERSATION_RESPONSE
1941  ): SCRIPT_CONVERSATION_RESPONSE_SCHEMA,
1942  }
1943 )
1944 
1945 _SCRIPT_STOP_SCHEMA = vol.Schema(
1946  {
1947  **SCRIPT_ACTION_BASE_SCHEMA,
1948  vol.Required(CONF_STOP): vol.Any(None, string),
1949  vol.Exclusive(CONF_ERROR, "error_or_response"): boolean,
1950  vol.Exclusive(
1951  CONF_RESPONSE_VARIABLE,
1952  "error_or_response",
1953  msg="not allowed to add a response to an error stop action",
1954  ): str,
1955  }
1956 )
1957 
1958 _SCRIPT_SEQUENCE_SCHEMA = vol.Schema(
1959  {
1960  **SCRIPT_ACTION_BASE_SCHEMA,
1961  vol.Required(CONF_SEQUENCE): SCRIPT_SCHEMA,
1962  }
1963 )
1964 
1965 _parallel_sequence_action = vol.All(
1966  # Wrap a shorthand sequences in a parallel action
1967  SCRIPT_SCHEMA,
1968  lambda config: {
1969  CONF_SEQUENCE: config,
1970  },
1971 )
1972 
1973 _SCRIPT_PARALLEL_SCHEMA = vol.Schema(
1974  {
1975  **SCRIPT_ACTION_BASE_SCHEMA,
1976  vol.Required(CONF_PARALLEL): vol.All(
1977  ensure_list, [vol.Any(_SCRIPT_SEQUENCE_SCHEMA, _parallel_sequence_action)]
1978  ),
1979  }
1980 )
1981 
1982 
1983 SCRIPT_ACTION_ACTIVATE_SCENE = "scene"
1984 SCRIPT_ACTION_CALL_SERVICE = "call_service"
1985 SCRIPT_ACTION_CHECK_CONDITION = "condition"
1986 SCRIPT_ACTION_CHOOSE = "choose"
1987 SCRIPT_ACTION_DELAY = "delay"
1988 SCRIPT_ACTION_DEVICE_AUTOMATION = "device"
1989 SCRIPT_ACTION_FIRE_EVENT = "event"
1990 SCRIPT_ACTION_IF = "if"
1991 SCRIPT_ACTION_PARALLEL = "parallel"
1992 SCRIPT_ACTION_REPEAT = "repeat"
1993 SCRIPT_ACTION_SEQUENCE = "sequence"
1994 SCRIPT_ACTION_SET_CONVERSATION_RESPONSE = "set_conversation_response"
1995 SCRIPT_ACTION_STOP = "stop"
1996 SCRIPT_ACTION_VARIABLES = "variables"
1997 SCRIPT_ACTION_WAIT_FOR_TRIGGER = "wait_for_trigger"
1998 SCRIPT_ACTION_WAIT_TEMPLATE = "wait_template"
1999 
2000 
2001 ACTIONS_MAP = {
2002  CONF_DELAY: SCRIPT_ACTION_DELAY,
2003  CONF_WAIT_TEMPLATE: SCRIPT_ACTION_WAIT_TEMPLATE,
2004  CONF_CONDITION: SCRIPT_ACTION_CHECK_CONDITION,
2005  "and": SCRIPT_ACTION_CHECK_CONDITION,
2006  "or": SCRIPT_ACTION_CHECK_CONDITION,
2007  "not": SCRIPT_ACTION_CHECK_CONDITION,
2008  CONF_EVENT: SCRIPT_ACTION_FIRE_EVENT,
2009  CONF_DEVICE_ID: SCRIPT_ACTION_DEVICE_AUTOMATION,
2010  CONF_SCENE: SCRIPT_ACTION_ACTIVATE_SCENE,
2011  CONF_REPEAT: SCRIPT_ACTION_REPEAT,
2012  CONF_CHOOSE: SCRIPT_ACTION_CHOOSE,
2013  CONF_WAIT_FOR_TRIGGER: SCRIPT_ACTION_WAIT_FOR_TRIGGER,
2014  CONF_VARIABLES: SCRIPT_ACTION_VARIABLES,
2015  CONF_IF: SCRIPT_ACTION_IF,
2016  CONF_ACTION: SCRIPT_ACTION_CALL_SERVICE,
2017  CONF_SERVICE: SCRIPT_ACTION_CALL_SERVICE,
2018  CONF_SERVICE_TEMPLATE: SCRIPT_ACTION_CALL_SERVICE,
2019  CONF_STOP: SCRIPT_ACTION_STOP,
2020  CONF_PARALLEL: SCRIPT_ACTION_PARALLEL,
2021  CONF_SEQUENCE: SCRIPT_ACTION_SEQUENCE,
2022  CONF_SET_CONVERSATION_RESPONSE: SCRIPT_ACTION_SET_CONVERSATION_RESPONSE,
2023 }
2024 
2025 ACTIONS_SET = set(ACTIONS_MAP)
2026 
2027 
2028 def determine_script_action(action: dict[str, Any]) -> str:
2029  """Determine action type."""
2030  if not (actions := ACTIONS_SET.intersection(action)):
2031  raise ValueError("Unable to determine action")
2032  if len(actions) > 1:
2033  # Ambiguous action, select the first one in the
2034  # order of the ACTIONS_MAP
2035  for action_key, _script_action in ACTIONS_MAP.items():
2036  if action_key in actions:
2037  return _script_action
2038  return ACTIONS_MAP[actions.pop()]
2039 
2040 
2041 ACTION_TYPE_SCHEMAS: dict[str, Callable[[Any], dict]] = {
2042  SCRIPT_ACTION_ACTIVATE_SCENE: _SCRIPT_SCENE_SCHEMA,
2043  SCRIPT_ACTION_CALL_SERVICE: SERVICE_SCHEMA,
2044  SCRIPT_ACTION_CHECK_CONDITION: CONDITION_ACTION_SCHEMA,
2045  SCRIPT_ACTION_CHOOSE: _SCRIPT_CHOOSE_SCHEMA,
2046  SCRIPT_ACTION_DELAY: _SCRIPT_DELAY_SCHEMA,
2047  SCRIPT_ACTION_DEVICE_AUTOMATION: DEVICE_ACTION_SCHEMA,
2048  SCRIPT_ACTION_FIRE_EVENT: EVENT_SCHEMA,
2049  SCRIPT_ACTION_IF: _SCRIPT_IF_SCHEMA,
2050  SCRIPT_ACTION_PARALLEL: _SCRIPT_PARALLEL_SCHEMA,
2051  SCRIPT_ACTION_REPEAT: _SCRIPT_REPEAT_SCHEMA,
2052  SCRIPT_ACTION_SEQUENCE: _SCRIPT_SEQUENCE_SCHEMA,
2053  SCRIPT_ACTION_SET_CONVERSATION_RESPONSE: _SCRIPT_SET_CONVERSATION_RESPONSE_SCHEMA,
2054  SCRIPT_ACTION_STOP: _SCRIPT_STOP_SCHEMA,
2055  SCRIPT_ACTION_VARIABLES: _SCRIPT_SET_SCHEMA,
2056  SCRIPT_ACTION_WAIT_FOR_TRIGGER: _SCRIPT_WAIT_FOR_TRIGGER_SCHEMA,
2057  SCRIPT_ACTION_WAIT_TEMPLATE: _SCRIPT_WAIT_TEMPLATE_SCHEMA,
2058 }
2059 
2060 
2061 currency = vol.In(
2062  currencies.ACTIVE_CURRENCIES, msg="invalid ISO 4217 formatted currency"
2063 )
2064 
2065 historic_currency = vol.In(
2066  currencies.HISTORIC_CURRENCIES, msg="invalid ISO 4217 formatted historic currency"
2067 )
2068 
2069 country = vol.In(COUNTRIES, msg="invalid ISO 3166 formatted country")
2070 
2071 language = vol.In(LANGUAGES, msg="invalid RFC 5646 formatted language")
2072 
2073 
2074 async def async_validate(
2075  hass: HomeAssistant, validator: Callable[[Any], Any], value: Any
2076 ) -> Any:
2077  """Async friendly schema validation.
2078 
2079  If a validator decorated with @not_async_friendly is called, validation will be
2080  deferred to an executor. If not, validation will happen in the event loop.
2081  """
2082  _validating_async.set(True)
2083  try:
2084  return validator(value)
2085  except MustValidateInExecutor:
2086  return await hass.async_add_executor_job(
2087  _validate_in_executor, hass, validator, value
2088  )
2089  finally:
2090  _validating_async.set(False)
2091 
2092 
2094  hass: HomeAssistant, validator: Callable[[Any], Any], value: Any
2095 ) -> Any:
2096  _hass.hass = hass
2097  try:
2098  return validator(value)
2099  finally:
2100  _hass.hass = None
vol.Schema default_schema(BaseAsyncGateway gateway, ChildSensor child, ValueType value_type_name)
Definition: helpers.py:96
None async_create_issue(HomeAssistant hass, str entry_id)
Definition: repairs.py:69
dict[str, Any] validate(SchemaCommonFlowHandler handler, dict[str, Any] user_input)
Definition: config_flow.py:27
bool valid_entity_id(str entity_id)
Definition: core.py:235
HomeAssistant|None async_get_hass_or_none()
Definition: core.py:299
HomeAssistant async_get_hass()
Definition: core.py:286
tuple[str, str] split_entity_id(str entity_id)
Definition: core.py:214
VolSchemaType make_entity_service_schema(dict|None schema, *int extra=vol.PREVENT_EXTRA)
Any _custom_serializer(Any schema, *bool allow_section)
str url(Any value, frozenset[UrlProtocolSchema] _schema_list=EXTERNAL_URL_PROTOCOL_SCHEMA_LIST)
Callable[[dict], dict] has_at_least_one_key(*Any keys)
Callable[[dict], dict] _no_yaml_config_schema(str domain, str issue_base, str translation_key, dict[str, str] translation_placeholders)
str determine_script_action(dict[str, Any] action)
VolSchemaType _make_entity_service_schema(dict schema, int extra)
Callable[[dict], dict] _deprecated_or_removed(str key, str|None replacement_key, Any|None default, bool raise_if_present, bool option_removed)
Callable[[dict], dict] platform_only_config_schema(str domain)
Callable[[dict], dict] empty_config_schema(str domain)
Callable[[dict], dict] removed(str key, Any|None default=None, bool|None raise_if_present=True)
vol.All enum(type[Enum] enumClass)
Callable[[dict], dict] has_at_most_one_key(*Any keys)
template_helper.Template dynamic_template(Any|None value)
Callable[[Any], dict[Hashable, Any]] key_value_schemas(str key, dict[Hashable, VolSchemaType|Callable[[Any], dict[str, Any]]] value_schemas, VolSchemaType|None default_schema=None, str|None default_description=None)
UnitOfTemperature temperature_unit(Any value)
dict[str, Any] STATE_CONDITION_SCHEMA(Any value)
list[str] _entity_ids(str|list value, bool allow_uuid)
Callable[[Any], str] entity_domain(str|list[str] domain)
bool is_entity_service_schema(VolSchemaType validator)
Callable[[dict], dict] config_entry_only_config_schema(str domain)
list[str] entity_ids_or_uuids(str|list value)
Callable[[Any], str] matches_regex(str regex)
Callable[[dict], dict] deprecated(str key, str|None replacement_key=None, Any|None default=None, bool|None raise_if_present=False)
Any async_validate(HomeAssistant hass, Callable[[Any], Any] validator, Any value)
list[Any] _base_trigger_list_flatten(list[Any] triggers)
Callable[[str|list], list[str]] entities_domain(str|list[str] domain)
timedelta positive_timedelta(timedelta value)
template_helper.Template template(Any|None value)
timedelta time_period_seconds(float|str value)
Any _validate_in_executor(HomeAssistant hass, Callable[[Any], Any] validator, Any value)
Callable schema_with_slug_keys(dict|Callable value_schema, *Callable[[Any], str] slug_validator=slug)
None report_usage(str what, *str|None breaks_in_ha_version=None, ReportBehavior core_behavior=ReportBehavior.ERROR, ReportBehavior core_integration_behavior=ReportBehavior.LOG, ReportBehavior custom_integration_behavior=ReportBehavior.LOG, set[str]|None exclude_integrations=None, str|None integration_domain=None, int level=logging.WARNING)
Definition: frame.py:195
logging.Logger get_integration_logger(str fallback_name)
Definition: frame.py:58
None raise_if_invalid_path(str path)
Definition: __init__.py:32