2016-03-28 01:48:51 +00:00
|
|
|
"""Helpers for config validation using voluptuous."""
|
2019-07-31 19:25:30 +00:00
|
|
|
from datetime import (
|
2019-12-09 15:42:10 +00:00
|
|
|
date as date_sys,
|
2019-07-31 19:25:30 +00:00
|
|
|
datetime as datetime_sys,
|
|
|
|
time as time_sys,
|
2019-12-09 15:42:10 +00:00
|
|
|
timedelta,
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2019-12-22 18:51:39 +00:00
|
|
|
from enum import Enum
|
2019-12-09 15:42:10 +00:00
|
|
|
import inspect
|
|
|
|
import logging
|
2019-06-08 05:18:02 +00:00
|
|
|
from numbers import Number
|
2019-12-09 15:42:10 +00:00
|
|
|
import os
|
|
|
|
import re
|
2019-12-20 21:35:02 +00:00
|
|
|
from socket import _GLOBAL_DEFAULT_TIMEOUT # type: ignore # private, not in typeshed
|
2019-12-22 18:51:39 +00:00
|
|
|
from typing import (
|
|
|
|
Any,
|
|
|
|
Callable,
|
|
|
|
Dict,
|
|
|
|
Hashable,
|
|
|
|
List,
|
|
|
|
Optional,
|
|
|
|
Pattern,
|
|
|
|
Type,
|
|
|
|
TypeVar,
|
|
|
|
Union,
|
|
|
|
cast,
|
|
|
|
)
|
2019-02-08 10:14:50 +00:00
|
|
|
from urllib.parse import urlparse
|
2019-03-28 04:53:11 +00:00
|
|
|
from uuid import UUID
|
2016-08-07 23:26:35 +00:00
|
|
|
|
2019-02-08 10:14:50 +00:00
|
|
|
from pkg_resources import parse_version
|
2019-10-02 20:14:52 +00:00
|
|
|
import voluptuous as vol
|
|
|
|
import voluptuous_serialize
|
2016-03-28 01:48:51 +00:00
|
|
|
|
|
|
|
from homeassistant.const import (
|
2019-12-09 15:42:10 +00:00
|
|
|
ATTR_AREA_ID,
|
|
|
|
ATTR_ENTITY_ID,
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_ABOVE,
|
|
|
|
CONF_ALIAS,
|
2020-08-19 18:01:27 +00:00
|
|
|
CONF_ATTRIBUTE,
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_BELOW,
|
2020-07-14 17:22:54 +00:00
|
|
|
CONF_CHOOSE,
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_CONDITION,
|
2020-07-14 17:22:54 +00:00
|
|
|
CONF_CONDITIONS,
|
2020-03-05 19:44:42 +00:00
|
|
|
CONF_CONTINUE_ON_TIMEOUT,
|
2020-07-10 18:37:19 +00:00
|
|
|
CONF_COUNT,
|
2020-07-14 17:22:54 +00:00
|
|
|
CONF_DEFAULT,
|
2020-03-05 19:44:42 +00:00
|
|
|
CONF_DELAY,
|
2019-09-09 17:40:22 +00:00
|
|
|
CONF_DEVICE_ID,
|
2019-09-05 14:49:32 +00:00
|
|
|
CONF_DOMAIN,
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_ENTITY_ID,
|
|
|
|
CONF_ENTITY_NAMESPACE,
|
2020-03-05 19:44:42 +00:00
|
|
|
CONF_EVENT,
|
|
|
|
CONF_EVENT_DATA,
|
|
|
|
CONF_EVENT_DATA_TEMPLATE,
|
2019-09-05 14:49:32 +00:00
|
|
|
CONF_FOR,
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_PLATFORM,
|
2020-07-10 18:37:19 +00:00
|
|
|
CONF_REPEAT,
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_SCAN_INTERVAL,
|
2020-03-05 19:44:42 +00:00
|
|
|
CONF_SCENE,
|
2020-07-10 18:37:19 +00:00
|
|
|
CONF_SEQUENCE,
|
2020-03-05 19:44:42 +00:00
|
|
|
CONF_SERVICE,
|
|
|
|
CONF_SERVICE_TEMPLATE,
|
2019-09-05 14:49:32 +00:00
|
|
|
CONF_STATE,
|
2019-12-09 15:42:10 +00:00
|
|
|
CONF_TIMEOUT,
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_UNIT_SYSTEM_IMPERIAL,
|
|
|
|
CONF_UNIT_SYSTEM_METRIC,
|
2020-07-10 18:37:19 +00:00
|
|
|
CONF_UNTIL,
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_VALUE_TEMPLATE,
|
2020-09-11 11:16:25 +00:00
|
|
|
CONF_VARIABLES,
|
2020-08-21 09:38:25 +00:00
|
|
|
CONF_WAIT_FOR_TRIGGER,
|
2020-03-05 19:44:42 +00:00
|
|
|
CONF_WAIT_TEMPLATE,
|
2020-07-10 18:37:19 +00:00
|
|
|
CONF_WHILE,
|
2019-07-31 19:25:30 +00:00
|
|
|
ENTITY_MATCH_ALL,
|
2020-02-04 22:42:07 +00:00
|
|
|
ENTITY_MATCH_NONE,
|
2019-07-31 19:25:30 +00:00
|
|
|
SUN_EVENT_SUNRISE,
|
|
|
|
SUN_EVENT_SUNSET,
|
|
|
|
TEMP_CELSIUS,
|
|
|
|
TEMP_FAHRENHEIT,
|
|
|
|
WEEKDAYS,
|
|
|
|
__version__,
|
|
|
|
)
|
2019-12-09 15:42:10 +00:00
|
|
|
from homeassistant.core import split_entity_id, valid_entity_id
|
2016-09-28 04:29:55 +00:00
|
|
|
from homeassistant.exceptions import TemplateError
|
2020-09-11 10:24:16 +00:00
|
|
|
from homeassistant.helpers import (
|
|
|
|
script_variables as script_variables_helper,
|
|
|
|
template as template_helper,
|
|
|
|
)
|
2019-02-08 10:14:50 +00:00
|
|
|
from homeassistant.helpers.logging import KeywordStyleAdapter
|
|
|
|
from homeassistant.util import slugify as util_slugify
|
2019-12-09 15:42:10 +00:00
|
|
|
import homeassistant.util.dt as dt_util
|
2019-07-21 16:59:02 +00:00
|
|
|
|
2016-03-28 01:48:51 +00:00
|
|
|
# pylint: disable=invalid-name
|
|
|
|
|
2020-07-22 00:41:42 +00:00
|
|
|
TIME_PERIOD_ERROR = "offset {} should be format 'HH:MM', 'HH:MM:SS' or 'HH:MM:SS.F'"
|
2019-01-26 22:09:41 +00:00
|
|
|
|
2016-04-03 17:19:09 +00:00
|
|
|
# Home Assistant types
|
2016-04-01 03:19:59 +00:00
|
|
|
byte = vol.All(vol.Coerce(int), vol.Range(min=0, max=255))
|
|
|
|
small_float = vol.All(vol.Coerce(float), vol.Range(min=0, max=1))
|
2016-04-07 19:19:28 +00:00
|
|
|
positive_int = vol.All(vol.Coerce(int), vol.Range(min=0))
|
2020-10-11 20:04:49 +00:00
|
|
|
positive_float = vol.All(vol.Coerce(float), vol.Range(min=0))
|
2019-07-31 19:25:30 +00:00
|
|
|
latitude = vol.All(
|
|
|
|
vol.Coerce(float), vol.Range(min=-90, max=90), msg="invalid latitude"
|
|
|
|
)
|
|
|
|
longitude = vol.All(
|
|
|
|
vol.Coerce(float), vol.Range(min=-180, max=180), msg="invalid longitude"
|
|
|
|
)
|
2018-03-09 07:57:21 +00:00
|
|
|
gps = vol.ExactSequence([latitude, longitude])
|
2016-05-03 05:05:09 +00:00
|
|
|
sun_event = vol.All(vol.Lower, vol.Any(SUN_EVENT_SUNSET, SUN_EVENT_SUNRISE))
|
2016-08-17 03:55:29 +00:00
|
|
|
port = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535))
|
2016-04-03 17:19:09 +00:00
|
|
|
|
2016-08-07 23:26:35 +00:00
|
|
|
# typing typevar
|
2019-07-31 19:25:30 +00:00
|
|
|
T = TypeVar("T")
|
2016-08-07 23:26:35 +00:00
|
|
|
|
2016-04-03 17:19:09 +00:00
|
|
|
|
2016-04-21 22:52:20 +00:00
|
|
|
# Adapted from:
|
|
|
|
# https://github.com/alecthomas/voluptuous/issues/115#issuecomment-144464666
|
2016-08-07 23:26:35 +00:00
|
|
|
def has_at_least_one_key(*keys: str) -> Callable:
|
2017-05-02 16:18:47 +00:00
|
|
|
"""Validate that at least one key exists."""
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2016-08-07 23:26:35 +00:00
|
|
|
def validate(obj: Dict) -> Dict:
|
2016-04-21 22:52:20 +00:00
|
|
|
"""Test keys exist in dict."""
|
|
|
|
if not isinstance(obj, dict):
|
2019-07-31 19:25:30 +00:00
|
|
|
raise vol.Invalid("expected dictionary")
|
2016-04-21 22:52:20 +00:00
|
|
|
|
2020-10-28 19:43:48 +00:00
|
|
|
for k in obj:
|
2016-04-21 22:52:20 +00:00
|
|
|
if k in keys:
|
|
|
|
return obj
|
2019-09-27 15:48:48 +00:00
|
|
|
raise vol.Invalid("must contain at least one of {}.".format(", ".join(keys)))
|
2016-04-21 22:52:20 +00:00
|
|
|
|
|
|
|
return validate
|
|
|
|
|
|
|
|
|
2019-09-20 15:23:34 +00:00
|
|
|
def has_at_most_one_key(*keys: str) -> Callable[[Dict], Dict]:
|
2019-02-08 10:14:50 +00:00
|
|
|
"""Validate that zero keys exist or one key exists."""
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2019-02-08 10:14:50 +00:00
|
|
|
def validate(obj: Dict) -> Dict:
|
|
|
|
"""Test zero keys exist or one key exists in dict."""
|
|
|
|
if not isinstance(obj, dict):
|
2019-07-31 19:25:30 +00:00
|
|
|
raise vol.Invalid("expected dictionary")
|
2019-02-08 10:14:50 +00:00
|
|
|
|
|
|
|
if len(set(keys) & set(obj)) > 1:
|
2019-07-31 19:25:30 +00:00
|
|
|
raise vol.Invalid("must contain at most one of {}.".format(", ".join(keys)))
|
2019-02-08 10:14:50 +00:00
|
|
|
return obj
|
|
|
|
|
|
|
|
return validate
|
|
|
|
|
|
|
|
|
2016-08-07 23:26:35 +00:00
|
|
|
def boolean(value: Any) -> bool:
|
2016-04-03 17:19:09 +00:00
|
|
|
"""Validate and coerce a boolean value."""
|
2019-06-08 05:18:02 +00:00
|
|
|
if isinstance(value, bool):
|
|
|
|
return value
|
2016-04-03 17:19:09 +00:00
|
|
|
if isinstance(value, str):
|
2019-06-08 05:18:02 +00:00
|
|
|
value = value.lower().strip()
|
2019-07-31 19:25:30 +00:00
|
|
|
if value in ("1", "true", "yes", "on", "enable"):
|
2016-04-03 17:19:09 +00:00
|
|
|
return True
|
2019-07-31 19:25:30 +00:00
|
|
|
if value in ("0", "false", "no", "off", "disable"):
|
2016-04-03 17:19:09 +00:00
|
|
|
return False
|
2019-06-08 05:18:02 +00:00
|
|
|
elif isinstance(value, Number):
|
2019-07-21 16:59:02 +00:00
|
|
|
# type ignore: https://github.com/python/mypy/issues/3186
|
|
|
|
return value != 0 # type: ignore
|
2020-01-03 13:47:06 +00:00
|
|
|
raise vol.Invalid(f"invalid boolean value {value}")
|
2016-03-28 01:48:51 +00:00
|
|
|
|
|
|
|
|
2020-08-20 13:06:41 +00:00
|
|
|
_WS = re.compile("\\s*")
|
|
|
|
|
|
|
|
|
|
|
|
def whitespace(value: Any) -> str:
|
|
|
|
"""Validate result contains only whitespace."""
|
|
|
|
if isinstance(value, str) and _WS.fullmatch(value):
|
|
|
|
return value
|
|
|
|
|
|
|
|
raise vol.Invalid(f"contains non-whitespace: {value}")
|
|
|
|
|
|
|
|
|
2019-12-22 18:51:39 +00:00
|
|
|
def isdevice(value: Any) -> str:
|
2017-01-25 06:04:44 +00:00
|
|
|
"""Validate that value is a real device."""
|
|
|
|
try:
|
|
|
|
os.stat(value)
|
|
|
|
return str(value)
|
2020-08-28 11:50:32 +00:00
|
|
|
except OSError as err:
|
|
|
|
raise vol.Invalid(f"No device at {value} found") from err
|
2017-01-25 06:04:44 +00:00
|
|
|
|
|
|
|
|
2019-12-22 18:51:39 +00:00
|
|
|
def matches_regex(regex: str) -> Callable[[Any], str]:
|
2018-05-05 14:00:36 +00:00
|
|
|
"""Validate that the value is a string that matches a regex."""
|
2019-12-22 18:51:39 +00:00
|
|
|
compiled = re.compile(regex)
|
2018-05-05 14:00:36 +00:00
|
|
|
|
|
|
|
def validator(value: Any) -> str:
|
|
|
|
"""Validate that value matches the given regex."""
|
|
|
|
if not isinstance(value, str):
|
2020-01-03 13:47:06 +00:00
|
|
|
raise vol.Invalid(f"not a string value: {value}")
|
2018-05-05 14:00:36 +00:00
|
|
|
|
2019-12-22 18:51:39 +00:00
|
|
|
if not compiled.match(value):
|
2019-07-31 19:25:30 +00:00
|
|
|
raise vol.Invalid(
|
2020-01-03 13:47:06 +00:00
|
|
|
f"value {value} does not match regular expression {compiled.pattern}"
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2018-05-05 14:00:36 +00:00
|
|
|
|
|
|
|
return value
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2018-05-05 14:00:36 +00:00
|
|
|
return validator
|
|
|
|
|
|
|
|
|
2019-12-22 18:51:39 +00:00
|
|
|
def is_regex(value: Any) -> Pattern[Any]:
|
2018-05-05 14:00:36 +00:00
|
|
|
"""Validate that a string is a valid regular expression."""
|
|
|
|
try:
|
|
|
|
r = re.compile(value)
|
|
|
|
return r
|
2020-08-28 11:50:32 +00:00
|
|
|
except TypeError as err:
|
2019-07-31 19:25:30 +00:00
|
|
|
raise vol.Invalid(
|
2020-01-03 13:47:06 +00:00
|
|
|
f"value {value} is of the wrong type for a regular expression"
|
2020-08-28 11:50:32 +00:00
|
|
|
) from err
|
|
|
|
except re.error as err:
|
|
|
|
raise vol.Invalid(f"value {value} is not a valid regular expression") from err
|
2018-05-05 14:00:36 +00:00
|
|
|
|
|
|
|
|
2016-09-01 13:35:00 +00:00
|
|
|
def isfile(value: Any) -> str:
|
2016-04-07 17:52:25 +00:00
|
|
|
"""Validate that the value is an existing file."""
|
2016-09-01 13:35:00 +00:00
|
|
|
if value is None:
|
2019-07-31 19:25:30 +00:00
|
|
|
raise vol.Invalid("None is not file")
|
2016-09-03 23:32:43 +00:00
|
|
|
file_in = os.path.expanduser(str(value))
|
2016-09-01 13:35:00 +00:00
|
|
|
|
|
|
|
if not os.path.isfile(file_in):
|
2019-07-31 19:25:30 +00:00
|
|
|
raise vol.Invalid("not a file")
|
2016-09-01 13:35:00 +00:00
|
|
|
if not os.access(file_in, os.R_OK):
|
2019-07-31 19:25:30 +00:00
|
|
|
raise vol.Invalid("file not readable")
|
2016-09-01 13:35:00 +00:00
|
|
|
return file_in
|
2016-04-07 17:52:25 +00:00
|
|
|
|
|
|
|
|
2017-10-25 02:36:27 +00:00
|
|
|
def isdir(value: Any) -> str:
|
|
|
|
"""Validate that the value is an existing dir."""
|
|
|
|
if value is None:
|
2019-07-31 19:25:30 +00:00
|
|
|
raise vol.Invalid("not a directory")
|
2017-10-25 02:36:27 +00:00
|
|
|
dir_in = os.path.expanduser(str(value))
|
|
|
|
|
|
|
|
if not os.path.isdir(dir_in):
|
2019-07-31 19:25:30 +00:00
|
|
|
raise vol.Invalid("not a directory")
|
2017-10-25 02:36:27 +00:00
|
|
|
if not os.access(dir_in, os.R_OK):
|
2019-07-31 19:25:30 +00:00
|
|
|
raise vol.Invalid("directory not readable")
|
2017-10-25 02:36:27 +00:00
|
|
|
return dir_in
|
|
|
|
|
|
|
|
|
2019-08-10 23:30:33 +00:00
|
|
|
def ensure_list(value: Union[T, List[T], None]) -> List[T]:
|
2016-04-04 19:18:58 +00:00
|
|
|
"""Wrap value in list if it is not one."""
|
2017-01-05 19:33:22 +00:00
|
|
|
if value is None:
|
|
|
|
return []
|
2016-04-04 19:18:58 +00:00
|
|
|
return value if isinstance(value, list) else [value]
|
|
|
|
|
|
|
|
|
2016-08-07 23:26:35 +00:00
|
|
|
def entity_id(value: Any) -> str:
|
2016-03-28 01:48:51 +00:00
|
|
|
"""Validate Entity ID."""
|
2019-12-22 18:51:39 +00:00
|
|
|
str_value = string(value).lower()
|
|
|
|
if valid_entity_id(str_value):
|
|
|
|
return str_value
|
2019-01-26 22:09:41 +00:00
|
|
|
|
2020-01-03 13:47:06 +00:00
|
|
|
raise vol.Invalid(f"Entity ID {value} is an invalid entity id")
|
2016-03-28 01:48:51 +00:00
|
|
|
|
|
|
|
|
2019-08-10 23:30:33 +00:00
|
|
|
def entity_ids(value: Union[str, List]) -> List[str]:
|
2016-03-28 01:48:51 +00:00
|
|
|
"""Validate Entity IDs."""
|
2016-06-09 03:55:08 +00:00
|
|
|
if value is None:
|
2019-07-31 19:25:30 +00:00
|
|
|
raise vol.Invalid("Entity IDs can not be None")
|
2016-03-28 01:48:51 +00:00
|
|
|
if isinstance(value, str):
|
2019-07-31 19:25:30 +00:00
|
|
|
value = [ent_id.strip() for ent_id in value.split(",")]
|
2016-03-28 01:48:51 +00:00
|
|
|
|
2016-04-10 22:20:20 +00:00
|
|
|
return [entity_id(ent_id) for ent_id in value]
|
2016-03-28 01:48:51 +00:00
|
|
|
|
|
|
|
|
2020-02-04 22:42:07 +00:00
|
|
|
comp_entity_ids = vol.Any(
|
|
|
|
vol.All(vol.Lower, vol.Any(ENTITY_MATCH_ALL, ENTITY_MATCH_NONE)), entity_ids
|
|
|
|
)
|
2018-12-13 09:07:59 +00:00
|
|
|
|
|
|
|
|
2019-09-20 15:23:34 +00:00
|
|
|
def entity_domain(domain: str) -> Callable[[Any], str]:
|
2018-02-26 07:48:21 +00:00
|
|
|
"""Validate that entity belong to domain."""
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2018-02-26 07:48:21 +00:00
|
|
|
def validate(value: Any) -> str:
|
|
|
|
"""Test if entity domain is domain."""
|
|
|
|
ent_domain = entities_domain(domain)
|
|
|
|
return ent_domain(value)[0]
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2018-02-26 07:48:21 +00:00
|
|
|
return validate
|
|
|
|
|
|
|
|
|
2019-09-20 15:23:34 +00:00
|
|
|
def entities_domain(domain: str) -> Callable[[Union[str, List]], List[str]]:
|
2018-02-26 07:48:21 +00:00
|
|
|
"""Validate that entities belong to domain."""
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2019-08-10 23:30:33 +00:00
|
|
|
def validate(values: Union[str, List]) -> List[str]:
|
2018-02-26 07:48:21 +00:00
|
|
|
"""Test if entity domain is domain."""
|
|
|
|
values = entity_ids(values)
|
|
|
|
for ent_id in values:
|
|
|
|
if split_entity_id(ent_id)[0] != domain:
|
|
|
|
raise vol.Invalid(
|
2020-01-03 13:47:06 +00:00
|
|
|
f"Entity ID '{ent_id}' does not belong to domain '{domain}'"
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2018-02-26 07:48:21 +00:00
|
|
|
return values
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2018-02-26 07:48:21 +00:00
|
|
|
return validate
|
|
|
|
|
|
|
|
|
2019-12-22 18:51:39 +00:00
|
|
|
def enum(enumClass: Type[Enum]) -> vol.All:
|
2016-09-28 04:29:55 +00:00
|
|
|
"""Create validator for specified enum."""
|
|
|
|
return vol.All(vol.In(enumClass.__members__), enumClass.__getitem__)
|
|
|
|
|
|
|
|
|
2019-12-22 18:51:39 +00:00
|
|
|
def icon(value: Any) -> str:
|
2016-03-28 01:48:51 +00:00
|
|
|
"""Validate icon."""
|
2019-12-22 18:51:39 +00:00
|
|
|
str_value = str(value)
|
2016-03-28 01:48:51 +00:00
|
|
|
|
2019-12-22 18:51:39 +00:00
|
|
|
if ":" in str_value:
|
|
|
|
return str_value
|
2016-03-28 01:48:51 +00:00
|
|
|
|
2019-08-02 21:20:07 +00:00
|
|
|
raise vol.Invalid('Icons should be specified in the form "prefix:name"')
|
2016-03-28 01:48:51 +00:00
|
|
|
|
|
|
|
|
2016-04-21 22:52:20 +00:00
|
|
|
time_period_dict = vol.All(
|
2019-07-31 19:25:30 +00:00
|
|
|
dict,
|
|
|
|
vol.Schema(
|
|
|
|
{
|
2020-07-22 00:41:42 +00:00
|
|
|
"days": vol.Coerce(float),
|
|
|
|
"hours": vol.Coerce(float),
|
|
|
|
"minutes": vol.Coerce(float),
|
|
|
|
"seconds": vol.Coerce(float),
|
|
|
|
"milliseconds": vol.Coerce(float),
|
2019-07-31 19:25:30 +00:00
|
|
|
}
|
|
|
|
),
|
|
|
|
has_at_least_one_key("days", "hours", "minutes", "seconds", "milliseconds"),
|
|
|
|
lambda value: timedelta(**value),
|
|
|
|
)
|
2016-04-21 22:52:20 +00:00
|
|
|
|
|
|
|
|
2019-09-20 15:23:34 +00:00
|
|
|
def time(value: Any) -> time_sys:
|
2017-09-28 21:57:49 +00:00
|
|
|
"""Validate and transform a time."""
|
|
|
|
if isinstance(value, time_sys):
|
|
|
|
return value
|
|
|
|
|
|
|
|
try:
|
|
|
|
time_val = dt_util.parse_time(value)
|
2020-08-28 11:50:32 +00:00
|
|
|
except TypeError as err:
|
|
|
|
raise vol.Invalid("Not a parseable type") from err
|
2017-09-28 21:57:49 +00:00
|
|
|
|
|
|
|
if time_val is None:
|
2020-01-03 13:47:06 +00:00
|
|
|
raise vol.Invalid(f"Invalid time specified: {value}")
|
2017-09-28 21:57:49 +00:00
|
|
|
|
|
|
|
return time_val
|
|
|
|
|
|
|
|
|
2019-09-20 15:23:34 +00:00
|
|
|
def date(value: Any) -> date_sys:
|
2017-09-28 21:57:49 +00:00
|
|
|
"""Validate and transform a date."""
|
|
|
|
if isinstance(value, date_sys):
|
|
|
|
return value
|
|
|
|
|
|
|
|
try:
|
|
|
|
date_val = dt_util.parse_date(value)
|
2020-08-28 11:50:32 +00:00
|
|
|
except TypeError as err:
|
|
|
|
raise vol.Invalid("Not a parseable type") from err
|
2017-09-28 21:57:49 +00:00
|
|
|
|
|
|
|
if date_val is None:
|
|
|
|
raise vol.Invalid("Could not parse date")
|
|
|
|
|
|
|
|
return date_val
|
|
|
|
|
|
|
|
|
2016-08-07 23:26:35 +00:00
|
|
|
def time_period_str(value: str) -> timedelta:
|
2016-04-04 19:18:58 +00:00
|
|
|
"""Validate and transform time offset."""
|
2020-03-12 10:52:20 +00:00
|
|
|
if isinstance(value, int): # type: ignore
|
2019-07-31 19:25:30 +00:00
|
|
|
raise vol.Invalid("Make sure you wrap time values in quotes")
|
2019-02-27 21:10:40 +00:00
|
|
|
if not isinstance(value, str):
|
2016-04-28 10:03:57 +00:00
|
|
|
raise vol.Invalid(TIME_PERIOD_ERROR.format(value))
|
2016-04-04 19:18:58 +00:00
|
|
|
|
|
|
|
negative_offset = False
|
2019-07-31 19:25:30 +00:00
|
|
|
if value.startswith("-"):
|
2016-04-04 19:18:58 +00:00
|
|
|
negative_offset = True
|
|
|
|
value = value[1:]
|
2019-07-31 19:25:30 +00:00
|
|
|
elif value.startswith("+"):
|
2016-04-04 19:18:58 +00:00
|
|
|
value = value[1:]
|
|
|
|
|
2020-07-22 00:41:42 +00:00
|
|
|
parsed = value.split(":")
|
|
|
|
if len(parsed) not in (2, 3):
|
|
|
|
raise vol.Invalid(TIME_PERIOD_ERROR.format(value))
|
2016-04-04 19:18:58 +00:00
|
|
|
try:
|
2020-07-22 00:41:42 +00:00
|
|
|
hour = int(parsed[0])
|
|
|
|
minute = int(parsed[1])
|
|
|
|
try:
|
|
|
|
second = float(parsed[2])
|
|
|
|
except IndexError:
|
|
|
|
second = 0
|
2020-08-28 11:50:32 +00:00
|
|
|
except ValueError as err:
|
|
|
|
raise vol.Invalid(TIME_PERIOD_ERROR.format(value)) from err
|
2016-04-04 19:18:58 +00:00
|
|
|
|
|
|
|
offset = timedelta(hours=hour, minutes=minute, seconds=second)
|
|
|
|
|
|
|
|
if negative_offset:
|
|
|
|
offset *= -1
|
|
|
|
|
|
|
|
return offset
|
|
|
|
|
|
|
|
|
2020-07-22 00:41:42 +00:00
|
|
|
def time_period_seconds(value: Union[float, str]) -> timedelta:
|
2016-10-08 01:08:33 +00:00
|
|
|
"""Validate and transform seconds to a time offset."""
|
|
|
|
try:
|
2020-07-22 00:41:42 +00:00
|
|
|
return timedelta(seconds=float(value))
|
2020-08-28 11:50:32 +00:00
|
|
|
except (ValueError, TypeError) as err:
|
|
|
|
raise vol.Invalid(f"Expected seconds, got {value}") from err
|
2016-10-08 01:08:33 +00:00
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
time_period = vol.Any(time_period_str, time_period_seconds, timedelta, time_period_dict)
|
2016-04-21 22:52:20 +00:00
|
|
|
|
|
|
|
|
2019-12-22 18:51:39 +00:00
|
|
|
def match_all(value: T) -> T:
|
2017-05-02 16:18:47 +00:00
|
|
|
"""Validate that matches all values."""
|
2016-04-04 19:18:58 +00:00
|
|
|
return value
|
|
|
|
|
|
|
|
|
2016-08-07 23:26:35 +00:00
|
|
|
def positive_timedelta(value: timedelta) -> timedelta:
|
2016-04-21 22:52:20 +00:00
|
|
|
"""Validate timedelta is positive."""
|
|
|
|
if value < timedelta(0):
|
2019-07-31 19:25:30 +00:00
|
|
|
raise vol.Invalid("Time period should be positive")
|
2016-04-21 22:52:20 +00:00
|
|
|
return value
|
|
|
|
|
|
|
|
|
2019-10-02 20:14:52 +00:00
|
|
|
positive_time_period_dict = vol.All(time_period_dict, positive_timedelta)
|
2020-08-12 18:42:06 +00:00
|
|
|
positive_time_period = vol.All(time_period, positive_timedelta)
|
2019-10-02 20:14:52 +00:00
|
|
|
|
|
|
|
|
2019-08-10 23:30:33 +00:00
|
|
|
def remove_falsy(value: List[T]) -> List[T]:
|
2019-04-03 02:43:06 +00:00
|
|
|
"""Remove falsy values from a list."""
|
|
|
|
return [v for v in value if v]
|
|
|
|
|
|
|
|
|
2019-12-22 18:51:39 +00:00
|
|
|
def service(value: Any) -> str:
|
2016-04-03 17:19:09 +00:00
|
|
|
"""Validate service."""
|
|
|
|
# Services use same format as entities so we can use same helper.
|
2019-12-22 18:51:39 +00:00
|
|
|
str_value = string(value).lower()
|
|
|
|
if valid_entity_id(str_value):
|
|
|
|
return str_value
|
2020-08-24 14:21:48 +00:00
|
|
|
|
2020-01-03 13:47:06 +00:00
|
|
|
raise vol.Invalid(f"Service {value} does not match format <domain>.<name>")
|
2016-04-03 17:19:09 +00:00
|
|
|
|
|
|
|
|
2020-02-25 19:18:21 +00:00
|
|
|
def slug(value: Any) -> str:
|
|
|
|
"""Validate value is a valid slug."""
|
|
|
|
if value is None:
|
|
|
|
raise vol.Invalid("Slug should not be None")
|
|
|
|
str_value = str(value)
|
|
|
|
slg = util_slugify(str_value)
|
|
|
|
if str_value == slg:
|
|
|
|
return str_value
|
|
|
|
raise vol.Invalid(f"invalid slug {value} (try {slg})")
|
|
|
|
|
|
|
|
|
|
|
|
def schema_with_slug_keys(
|
|
|
|
value_schema: Union[T, Callable], *, slug_validator: Callable[[Any], str] = slug
|
|
|
|
) -> Callable:
|
2019-01-21 17:45:11 +00:00
|
|
|
"""Ensure dicts have slugs as keys.
|
|
|
|
|
|
|
|
Replacement of vol.Schema({cv.slug: value_schema}) to prevent misleading
|
|
|
|
"Extra keys" errors from voluptuous.
|
|
|
|
"""
|
|
|
|
schema = vol.Schema({str: value_schema})
|
|
|
|
|
|
|
|
def verify(value: Dict) -> Dict:
|
|
|
|
"""Validate all keys are slugs and then the value_schema."""
|
2019-01-22 00:36:04 +00:00
|
|
|
if not isinstance(value, dict):
|
2019-07-31 19:25:30 +00:00
|
|
|
raise vol.Invalid("expected dictionary")
|
2019-01-22 00:36:04 +00:00
|
|
|
|
2019-01-21 17:45:11 +00:00
|
|
|
for key in value.keys():
|
2020-02-25 19:18:21 +00:00
|
|
|
slug_validator(key)
|
2019-01-26 22:09:41 +00:00
|
|
|
|
2019-12-22 18:51:39 +00:00
|
|
|
return cast(Dict, schema(value))
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2019-01-21 17:45:11 +00:00
|
|
|
return verify
|
|
|
|
|
|
|
|
|
|
|
|
def slugify(value: Any) -> str:
|
2016-10-08 21:40:50 +00:00
|
|
|
"""Coerce a value to a slug."""
|
|
|
|
if value is None:
|
2019-07-31 19:25:30 +00:00
|
|
|
raise vol.Invalid("Slug should not be None")
|
2016-10-08 21:40:50 +00:00
|
|
|
slg = util_slugify(str(value))
|
2017-04-24 03:41:09 +00:00
|
|
|
if slg:
|
2016-10-08 21:40:50 +00:00
|
|
|
return slg
|
2020-01-03 13:47:06 +00:00
|
|
|
raise vol.Invalid(f"Unable to slugify {value}")
|
2016-10-08 21:40:50 +00:00
|
|
|
|
|
|
|
|
2016-08-07 23:26:35 +00:00
|
|
|
def string(value: Any) -> str:
|
2016-04-02 07:51:03 +00:00
|
|
|
"""Coerce value to string, except for None."""
|
2018-10-07 10:35:44 +00:00
|
|
|
if value is None:
|
2019-07-31 19:25:30 +00:00
|
|
|
raise vol.Invalid("string value is None")
|
2020-10-26 10:30:58 +00:00
|
|
|
|
|
|
|
if isinstance(value, template_helper.ResultWrapper):
|
|
|
|
value = value.render_result
|
|
|
|
|
|
|
|
elif isinstance(value, (list, dict)):
|
2019-07-31 19:25:30 +00:00
|
|
|
raise vol.Invalid("value should be a string")
|
2018-10-07 10:35:44 +00:00
|
|
|
|
|
|
|
return str(value)
|
2016-04-02 07:51:03 +00:00
|
|
|
|
|
|
|
|
2020-05-14 17:33:14 +00:00
|
|
|
def string_with_no_html(value: Any) -> str:
|
|
|
|
"""Validate that the value is a string without HTML."""
|
|
|
|
value = string(value)
|
|
|
|
regex = re.compile(r"<[a-z][\s\S]*>")
|
|
|
|
if regex.search(value):
|
|
|
|
raise vol.Invalid("the string should not contain HTML")
|
|
|
|
return str(value)
|
|
|
|
|
|
|
|
|
2019-09-20 15:23:34 +00:00
|
|
|
def temperature_unit(value: Any) -> str:
|
2016-03-28 01:48:51 +00:00
|
|
|
"""Validate and transform temperature unit."""
|
2016-04-03 17:19:09 +00:00
|
|
|
value = str(value).upper()
|
2019-07-31 19:25:30 +00:00
|
|
|
if value == "C":
|
2016-04-20 03:30:44 +00:00
|
|
|
return TEMP_CELSIUS
|
2019-07-31 19:25:30 +00:00
|
|
|
if value == "F":
|
2016-04-03 17:19:09 +00:00
|
|
|
return TEMP_FAHRENHEIT
|
2019-07-31 19:25:30 +00:00
|
|
|
raise vol.Invalid("invalid temperature unit (expected C or F)")
|
2016-04-03 17:19:09 +00:00
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
unit_system = vol.All(
|
|
|
|
vol.Lower, vol.Any(CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL)
|
|
|
|
)
|
2016-07-31 20:24:49 +00:00
|
|
|
|
|
|
|
|
2019-12-22 18:51:39 +00:00
|
|
|
def template(value: Optional[Any]) -> template_helper.Template:
|
2016-04-03 17:19:09 +00:00
|
|
|
"""Validate a jinja2 template."""
|
2019-05-01 02:54:25 +00:00
|
|
|
|
2016-04-03 17:19:09 +00:00
|
|
|
if value is None:
|
2019-07-31 19:25:30 +00:00
|
|
|
raise vol.Invalid("template value is None")
|
2019-02-27 21:10:40 +00:00
|
|
|
if isinstance(value, (list, dict, template_helper.Template)):
|
2019-07-31 19:25:30 +00:00
|
|
|
raise vol.Invalid("template value should be a string")
|
2016-04-03 17:19:09 +00:00
|
|
|
|
2019-12-22 18:51:39 +00:00
|
|
|
template_value = template_helper.Template(str(value)) # type: ignore
|
2016-09-28 04:29:55 +00:00
|
|
|
|
2016-04-03 17:19:09 +00:00
|
|
|
try:
|
2020-10-13 00:17:30 +00:00
|
|
|
template_value.ensure_valid() # type: ignore[no-untyped-call]
|
|
|
|
return template_value
|
2016-09-28 04:29:55 +00:00
|
|
|
except TemplateError as ex:
|
2020-08-28 11:50:32 +00:00
|
|
|
raise vol.Invalid(f"invalid template ({ex})") from ex
|
2016-03-28 01:48:51 +00:00
|
|
|
|
|
|
|
|
2020-08-24 14:21:48 +00:00
|
|
|
def dynamic_template(value: Optional[Any]) -> template_helper.Template:
|
|
|
|
"""Validate a dynamic (non static) jinja2 template."""
|
|
|
|
|
|
|
|
if value is None:
|
|
|
|
raise vol.Invalid("template value is None")
|
|
|
|
if isinstance(value, (list, dict, template_helper.Template)):
|
|
|
|
raise vol.Invalid("template value should be a string")
|
|
|
|
if not template_helper.is_template_string(str(value)):
|
|
|
|
raise vol.Invalid("template value does not contain a dynmamic template")
|
|
|
|
|
|
|
|
template_value = template_helper.Template(str(value)) # type: ignore
|
|
|
|
try:
|
2020-10-13 00:17:30 +00:00
|
|
|
template_value.ensure_valid() # type: ignore[no-untyped-call]
|
|
|
|
return template_value
|
2020-08-24 14:21:48 +00:00
|
|
|
except TemplateError as ex:
|
2020-08-28 11:50:32 +00:00
|
|
|
raise vol.Invalid(f"invalid template ({ex})") from ex
|
2020-08-24 14:21:48 +00:00
|
|
|
|
|
|
|
|
2019-12-22 18:51:39 +00:00
|
|
|
def template_complex(value: Any) -> Any:
|
2016-09-08 16:19:47 +00:00
|
|
|
"""Validate a complex jinja2 template."""
|
|
|
|
if isinstance(value, list):
|
2019-12-22 18:51:39 +00:00
|
|
|
return_list = value.copy()
|
|
|
|
for idx, element in enumerate(return_list):
|
|
|
|
return_list[idx] = template_complex(element)
|
|
|
|
return return_list
|
2016-09-08 16:19:47 +00:00
|
|
|
if isinstance(value, dict):
|
2020-08-21 20:42:05 +00:00
|
|
|
return {
|
|
|
|
template_complex(key): template_complex(element)
|
|
|
|
for key, element in value.items()
|
|
|
|
}
|
|
|
|
if isinstance(value, str) and template_helper.is_template_string(value):
|
2019-12-03 22:15:45 +00:00
|
|
|
return template(value)
|
2020-08-21 20:42:05 +00:00
|
|
|
|
2019-12-03 22:15:45 +00:00
|
|
|
return value
|
2016-09-08 16:19:47 +00:00
|
|
|
|
|
|
|
|
2020-08-12 18:42:06 +00:00
|
|
|
positive_time_period_template = vol.Any(
|
|
|
|
positive_time_period, template, template_complex
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2019-12-22 18:51:39 +00:00
|
|
|
def datetime(value: Any) -> datetime_sys:
|
2016-11-25 05:52:10 +00:00
|
|
|
"""Validate datetime."""
|
|
|
|
if isinstance(value, datetime_sys):
|
|
|
|
return value
|
|
|
|
|
|
|
|
try:
|
|
|
|
date_val = dt_util.parse_datetime(value)
|
|
|
|
except TypeError:
|
|
|
|
date_val = None
|
|
|
|
|
|
|
|
if date_val is None:
|
2020-01-03 13:47:06 +00:00
|
|
|
raise vol.Invalid(f"Invalid datetime specified: {value}")
|
2016-11-25 05:52:10 +00:00
|
|
|
|
|
|
|
return date_val
|
|
|
|
|
|
|
|
|
2019-12-22 18:51:39 +00:00
|
|
|
def time_zone(value: str) -> str:
|
2016-03-28 01:48:51 +00:00
|
|
|
"""Validate timezone."""
|
|
|
|
if dt_util.get_time_zone(value) is not None:
|
|
|
|
return value
|
|
|
|
raise vol.Invalid(
|
2019-07-31 19:25:30 +00:00
|
|
|
"Invalid time zone passed in. Valid options can be found here: "
|
|
|
|
"http://en.wikipedia.org/wiki/List_of_tz_database_time_zones"
|
|
|
|
)
|
2016-04-03 17:19:09 +00:00
|
|
|
|
2016-11-19 05:47:59 +00:00
|
|
|
|
2016-04-28 10:03:57 +00:00
|
|
|
weekdays = vol.All(ensure_list, [vol.In(WEEKDAYS)])
|
|
|
|
|
2016-04-03 17:19:09 +00:00
|
|
|
|
2019-12-22 18:51:39 +00:00
|
|
|
def socket_timeout(value: Optional[Any]) -> object:
|
2016-10-22 09:05:00 +00:00
|
|
|
"""Validate timeout float > 0.0.
|
|
|
|
|
|
|
|
None coerced to socket._GLOBAL_DEFAULT_TIMEOUT bare object.
|
|
|
|
"""
|
|
|
|
if value is None:
|
|
|
|
return _GLOBAL_DEFAULT_TIMEOUT
|
2018-07-23 08:16:05 +00:00
|
|
|
try:
|
|
|
|
float_value = float(value)
|
|
|
|
if float_value > 0.0:
|
|
|
|
return float_value
|
2020-01-02 19:17:10 +00:00
|
|
|
raise vol.Invalid("Invalid socket timeout value. float > 0.0 required.")
|
2020-01-03 13:47:06 +00:00
|
|
|
except Exception as err:
|
|
|
|
raise vol.Invalid(f"Invalid socket timeout: {err}")
|
2016-10-22 09:05:00 +00:00
|
|
|
|
|
|
|
|
2016-08-19 11:41:01 +00:00
|
|
|
# pylint: disable=no-value-for-parameter
|
|
|
|
def url(value: Any) -> str:
|
|
|
|
"""Validate an URL."""
|
|
|
|
url_in = str(value)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if urlparse(url_in).scheme in ["http", "https"]:
|
2019-12-22 18:51:39 +00:00
|
|
|
return cast(str, vol.Schema(vol.Url())(url_in))
|
2016-08-19 11:41:01 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
raise vol.Invalid("invalid url")
|
2016-08-19 11:41:01 +00:00
|
|
|
|
|
|
|
|
2019-12-22 18:51:39 +00:00
|
|
|
def x10_address(value: str) -> str:
|
2016-10-25 04:49:49 +00:00
|
|
|
"""Validate an x10 address."""
|
2019-07-31 19:25:30 +00:00
|
|
|
regex = re.compile(r"([A-Pa-p]{1})(?:[2-9]|1[0-6]?)$")
|
2016-10-25 04:49:49 +00:00
|
|
|
if not regex.match(value):
|
2019-07-31 19:25:30 +00:00
|
|
|
raise vol.Invalid("Invalid X10 Address")
|
2016-10-25 04:49:49 +00:00
|
|
|
return str(value).lower()
|
|
|
|
|
|
|
|
|
2019-12-22 18:51:39 +00:00
|
|
|
def uuid4_hex(value: Any) -> str:
|
2019-03-28 04:53:11 +00:00
|
|
|
"""Validate a v4 UUID in hex format."""
|
|
|
|
try:
|
|
|
|
result = UUID(value, version=4)
|
|
|
|
except (ValueError, AttributeError, TypeError) as error:
|
2019-07-31 19:25:30 +00:00
|
|
|
raise vol.Invalid("Invalid Version4 UUID", error_message=str(error))
|
2019-03-28 04:53:11 +00:00
|
|
|
|
|
|
|
if result.hex != value.lower():
|
|
|
|
# UUID() will create a uuid4 if input is invalid
|
2019-07-31 19:25:30 +00:00
|
|
|
raise vol.Invalid("Invalid Version4 UUID")
|
2019-03-28 04:53:11 +00:00
|
|
|
|
|
|
|
return result.hex
|
|
|
|
|
|
|
|
|
2019-08-10 23:30:33 +00:00
|
|
|
def ensure_list_csv(value: Any) -> List:
|
2017-01-22 19:19:50 +00:00
|
|
|
"""Ensure that input is a list or make one from comma-separated string."""
|
|
|
|
if isinstance(value, str):
|
2019-07-31 19:25:30 +00:00
|
|
|
return [member.strip() for member in value.split(",")]
|
2017-01-22 19:19:50 +00:00
|
|
|
return ensure_list(value)
|
|
|
|
|
|
|
|
|
2020-02-14 19:09:40 +00:00
|
|
|
class multi_select:
|
2020-02-13 21:12:09 +00:00
|
|
|
"""Multi select validator returning list of selected values."""
|
|
|
|
|
2020-02-14 19:09:40 +00:00
|
|
|
def __init__(self, options: dict) -> None:
|
|
|
|
"""Initialize multi select."""
|
|
|
|
self.options = options
|
|
|
|
|
|
|
|
def __call__(self, selected: list) -> list:
|
|
|
|
"""Validate input."""
|
2020-02-13 21:12:09 +00:00
|
|
|
if not isinstance(selected, list):
|
|
|
|
raise vol.Invalid("Not a list")
|
|
|
|
|
|
|
|
for value in selected:
|
2020-02-14 19:09:40 +00:00
|
|
|
if value not in self.options:
|
2020-02-13 21:12:09 +00:00
|
|
|
raise vol.Invalid(f"{value} is not a valid option")
|
|
|
|
|
|
|
|
return selected
|
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def deprecated(
|
|
|
|
key: str,
|
|
|
|
replacement_key: Optional[str] = None,
|
|
|
|
invalidation_version: Optional[str] = None,
|
|
|
|
default: Optional[Any] = None,
|
2019-09-20 15:23:34 +00:00
|
|
|
) -> Callable[[Dict], Dict]:
|
2019-02-08 10:14:50 +00:00
|
|
|
"""
|
|
|
|
Log key as deprecated and provide a replacement (if exists).
|
|
|
|
|
|
|
|
Expected behavior:
|
|
|
|
- Outputs the appropriate deprecation warning if key is detected
|
|
|
|
- Processes schema moving the value from key to replacement_key
|
|
|
|
- Processes schema changing nothing if only replacement_key provided
|
|
|
|
- No warning if only replacement_key provided
|
|
|
|
- No warning if neither key nor replacement_key are provided
|
|
|
|
- Adds replacement_key with default value in this case
|
|
|
|
- Once the invalidation_version is crossed, raises vol.Invalid if key
|
|
|
|
is detected
|
|
|
|
"""
|
2019-07-11 07:38:58 +00:00
|
|
|
module = inspect.getmodule(inspect.stack()[1][0])
|
|
|
|
if module is not None:
|
|
|
|
module_name = module.__name__
|
|
|
|
else:
|
2019-10-05 09:59:34 +00:00
|
|
|
# If Python is unable to access the sources files, the call stack frame
|
|
|
|
# will be missing information, so let's guard.
|
2020-10-02 22:04:11 +00:00
|
|
|
# https://github.com/home-assistant/core/issues/24982
|
2019-09-27 19:57:59 +00:00
|
|
|
module_name = __name__
|
2018-01-10 08:06:26 +00:00
|
|
|
|
2019-02-08 10:14:50 +00:00
|
|
|
if replacement_key and invalidation_version:
|
2019-07-31 19:25:30 +00:00
|
|
|
warning = (
|
2020-06-02 15:29:59 +00:00
|
|
|
"The '{key}' option is deprecated,"
|
|
|
|
" please replace it with '{replacement_key}'."
|
2020-06-10 06:26:55 +00:00
|
|
|
" This option {invalidation_status} invalid in version"
|
2019-07-31 19:25:30 +00:00
|
|
|
" {invalidation_version}"
|
|
|
|
)
|
2019-02-08 10:14:50 +00:00
|
|
|
elif replacement_key:
|
2019-07-31 19:25:30 +00:00
|
|
|
warning = (
|
2020-06-02 15:29:59 +00:00
|
|
|
"The '{key}' option is deprecated,"
|
|
|
|
" please replace it with '{replacement_key}'"
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2019-02-08 10:14:50 +00:00
|
|
|
elif invalidation_version:
|
2019-07-31 19:25:30 +00:00
|
|
|
warning = (
|
2020-06-02 15:29:59 +00:00
|
|
|
"The '{key}' option is deprecated,"
|
|
|
|
" please remove it from your configuration."
|
2020-06-10 06:26:55 +00:00
|
|
|
" This option {invalidation_status} invalid in version"
|
2019-07-31 19:25:30 +00:00
|
|
|
" {invalidation_version}"
|
|
|
|
)
|
2019-02-08 10:14:50 +00:00
|
|
|
else:
|
2019-07-31 19:25:30 +00:00
|
|
|
warning = (
|
2020-06-02 15:29:59 +00:00
|
|
|
"The '{key}' option is deprecated,"
|
|
|
|
" please remove it from your configuration"
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2019-02-08 10:14:50 +00:00
|
|
|
|
2020-06-02 15:29:59 +00:00
|
|
|
def check_for_invalid_version() -> None:
|
2019-02-08 10:14:50 +00:00
|
|
|
"""Raise error if current version has reached invalidation."""
|
|
|
|
if not invalidation_version:
|
|
|
|
return
|
|
|
|
|
|
|
|
if parse_version(__version__) >= parse_version(invalidation_version):
|
|
|
|
raise vol.Invalid(
|
|
|
|
warning.format(
|
|
|
|
key=key,
|
|
|
|
replacement_key=replacement_key,
|
2020-06-10 06:26:55 +00:00
|
|
|
invalidation_status="became",
|
2019-07-31 19:25:30 +00:00
|
|
|
invalidation_version=invalidation_version,
|
2019-02-08 10:14:50 +00:00
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2019-09-20 15:23:34 +00:00
|
|
|
def validator(config: Dict) -> Dict:
|
2018-01-10 08:06:26 +00:00
|
|
|
"""Check if key is in config and log warning."""
|
|
|
|
if key in config:
|
2020-06-02 15:29:59 +00:00
|
|
|
check_for_invalid_version()
|
2019-02-08 10:14:50 +00:00
|
|
|
KeywordStyleAdapter(logging.getLogger(module_name)).warning(
|
|
|
|
warning,
|
|
|
|
key=key,
|
|
|
|
replacement_key=replacement_key,
|
2020-06-10 06:26:55 +00:00
|
|
|
invalidation_status="will become",
|
2019-07-31 19:25:30 +00:00
|
|
|
invalidation_version=invalidation_version,
|
2019-02-08 10:14:50 +00:00
|
|
|
)
|
2020-06-02 15:29:59 +00:00
|
|
|
|
|
|
|
value = config[key]
|
2019-02-08 10:14:50 +00:00
|
|
|
if replacement_key:
|
|
|
|
config.pop(key)
|
|
|
|
else:
|
|
|
|
value = default
|
2020-06-02 15:29:59 +00:00
|
|
|
|
2019-07-21 16:59:02 +00:00
|
|
|
keys = [key]
|
|
|
|
if replacement_key:
|
|
|
|
keys.append(replacement_key)
|
|
|
|
if value is not None and (
|
2019-07-31 19:25:30 +00:00
|
|
|
replacement_key not in config or default == config.get(replacement_key)
|
|
|
|
):
|
2019-07-21 16:59:02 +00:00
|
|
|
config[replacement_key] = value
|
2019-02-08 10:14:50 +00:00
|
|
|
|
2019-07-21 16:59:02 +00:00
|
|
|
return has_at_most_one_key(*keys)(config)
|
2018-01-10 08:06:26 +00:00
|
|
|
|
|
|
|
return validator
|
|
|
|
|
|
|
|
|
2020-02-24 08:59:34 +00:00
|
|
|
def key_value_schemas(
|
|
|
|
key: str, value_schemas: Dict[str, vol.Schema]
|
|
|
|
) -> Callable[[Any], Dict[str, Any]]:
|
|
|
|
"""Create a validator that validates based on a value for specific key.
|
|
|
|
|
|
|
|
This gives better error messages.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def key_value_validator(value: Any) -> Dict[str, Any]:
|
|
|
|
if not isinstance(value, dict):
|
|
|
|
raise vol.Invalid("Expected a dictionary")
|
|
|
|
|
|
|
|
key_value = value.get(key)
|
|
|
|
|
|
|
|
if key_value not in value_schemas:
|
|
|
|
raise vol.Invalid(
|
2020-03-05 19:44:42 +00:00
|
|
|
f"Unexpected value for {key}: '{key_value}'. Expected {', '.join(value_schemas)}"
|
2020-02-24 08:59:34 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
return cast(Dict[str, Any], value_schemas[key_value](value))
|
|
|
|
|
|
|
|
return key_value_validator
|
|
|
|
|
|
|
|
|
2016-04-03 17:19:09 +00:00
|
|
|
# Validator helpers
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2019-12-22 18:51:39 +00:00
|
|
|
def key_dependency(
|
|
|
|
key: Hashable, dependency: Hashable
|
|
|
|
) -> Callable[[Dict[Hashable, Any]], Dict[Hashable, Any]]:
|
2016-04-04 19:18:58 +00:00
|
|
|
"""Validate that all dependencies exist for key."""
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2019-12-22 18:51:39 +00:00
|
|
|
def validator(value: Dict[Hashable, Any]) -> Dict[Hashable, Any]:
|
2016-04-04 19:18:58 +00:00
|
|
|
"""Test dependencies."""
|
|
|
|
if not isinstance(value, dict):
|
2019-07-31 19:25:30 +00:00
|
|
|
raise vol.Invalid("key dependencies require a dict")
|
2016-04-04 19:18:58 +00:00
|
|
|
if key in value and dependency not in value:
|
2019-07-31 19:25:30 +00:00
|
|
|
raise vol.Invalid(
|
2020-01-03 13:47:06 +00:00
|
|
|
f'dependency violation - key "{key}" requires '
|
|
|
|
f'key "{dependency}" to exist'
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2016-04-03 17:19:09 +00:00
|
|
|
|
2016-04-04 19:18:58 +00:00
|
|
|
return value
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2016-04-04 19:18:58 +00:00
|
|
|
return validator
|
2016-04-03 17:19:09 +00:00
|
|
|
|
|
|
|
|
2019-12-22 18:51:39 +00:00
|
|
|
def custom_serializer(schema: Any) -> Any:
|
2019-10-02 20:14:52 +00:00
|
|
|
"""Serialize additional types for voluptuous_serialize."""
|
|
|
|
if schema is positive_time_period_dict:
|
|
|
|
return {"type": "positive_time_period_dict"}
|
|
|
|
|
2020-10-09 07:36:54 +00:00
|
|
|
if schema is string:
|
|
|
|
return {"type": "string"}
|
|
|
|
|
|
|
|
if schema is boolean:
|
|
|
|
return {"type": "boolean"}
|
|
|
|
|
2020-02-14 19:09:40 +00:00
|
|
|
if isinstance(schema, multi_select):
|
|
|
|
return {"type": "multi_select", "options": schema.options}
|
2020-02-13 21:12:09 +00:00
|
|
|
|
2019-10-02 20:14:52 +00:00
|
|
|
return voluptuous_serialize.UNSUPPORTED
|
|
|
|
|
|
|
|
|
2016-04-03 17:19:09 +00:00
|
|
|
# Schemas
|
2019-07-31 19:25:30 +00:00
|
|
|
PLATFORM_SCHEMA = vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Required(CONF_PLATFORM): string,
|
|
|
|
vol.Optional(CONF_ENTITY_NAMESPACE): string,
|
|
|
|
vol.Optional(CONF_SCAN_INTERVAL): time_period,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
PLATFORM_SCHEMA_BASE = PLATFORM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA)
|
|
|
|
|
2020-02-02 23:36:39 +00:00
|
|
|
ENTITY_SERVICE_FIELDS = (ATTR_ENTITY_ID, ATTR_AREA_ID)
|
|
|
|
|
2019-12-03 00:23:12 +00:00
|
|
|
|
|
|
|
def make_entity_service_schema(
|
|
|
|
schema: dict, *, extra: int = vol.PREVENT_EXTRA
|
|
|
|
) -> vol.All:
|
|
|
|
"""Create an entity service schema."""
|
|
|
|
return vol.All(
|
|
|
|
vol.Schema(
|
|
|
|
{
|
|
|
|
**schema,
|
|
|
|
vol.Optional(ATTR_ENTITY_ID): comp_entity_ids,
|
2020-02-04 22:42:07 +00:00
|
|
|
vol.Optional(ATTR_AREA_ID): vol.Any(
|
|
|
|
ENTITY_MATCH_NONE, vol.All(ensure_list, [str])
|
|
|
|
),
|
2019-12-03 00:23:12 +00:00
|
|
|
},
|
|
|
|
extra=extra,
|
|
|
|
),
|
2020-02-02 23:36:39 +00:00
|
|
|
has_at_least_one_key(*ENTITY_SERVICE_FIELDS),
|
2019-12-03 00:23:12 +00:00
|
|
|
)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2020-09-11 10:24:16 +00:00
|
|
|
SCRIPT_VARIABLES_SCHEMA = vol.All(
|
|
|
|
vol.Schema({str: template_complex}),
|
|
|
|
# pylint: disable=unnecessary-lambda
|
|
|
|
lambda val: script_variables_helper.ScriptVariables(val),
|
|
|
|
)
|
2020-09-10 18:41:42 +00:00
|
|
|
|
|
|
|
|
2020-07-10 18:37:19 +00:00
|
|
|
def script_action(value: Any) -> dict:
|
|
|
|
"""Validate a script action."""
|
|
|
|
if not isinstance(value, dict):
|
|
|
|
raise vol.Invalid("expected dictionary")
|
|
|
|
|
|
|
|
return ACTION_TYPE_SCHEMAS[determine_script_action(value)](value)
|
|
|
|
|
|
|
|
|
|
|
|
SCRIPT_SCHEMA = vol.All(ensure_list, [script_action])
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
EVENT_SCHEMA = vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Optional(CONF_ALIAS): string,
|
2020-03-05 19:44:42 +00:00
|
|
|
vol.Required(CONF_EVENT): string,
|
2020-08-24 14:21:48 +00:00
|
|
|
vol.Optional(CONF_EVENT_DATA): vol.All(dict, template_complex),
|
|
|
|
vol.Optional(CONF_EVENT_DATA_TEMPLATE): vol.All(dict, template_complex),
|
2019-07-31 19:25:30 +00:00
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
SERVICE_SCHEMA = vol.All(
|
|
|
|
vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Optional(CONF_ALIAS): string,
|
2020-08-24 14:21:48 +00:00
|
|
|
vol.Exclusive(CONF_SERVICE, "service name"): vol.Any(
|
|
|
|
service, dynamic_template
|
|
|
|
),
|
|
|
|
vol.Exclusive(CONF_SERVICE_TEMPLATE, "service name"): vol.Any(
|
|
|
|
service, dynamic_template
|
|
|
|
),
|
|
|
|
vol.Optional("data"): vol.All(dict, template_complex),
|
|
|
|
vol.Optional("data_template"): vol.All(dict, template_complex),
|
2019-07-31 19:25:30 +00:00
|
|
|
vol.Optional(CONF_ENTITY_ID): comp_entity_ids,
|
|
|
|
}
|
|
|
|
),
|
2020-03-05 19:44:42 +00:00
|
|
|
has_at_least_one_key(CONF_SERVICE, CONF_SERVICE_TEMPLATE),
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
NUMERIC_STATE_CONDITION_SCHEMA = vol.All(
|
|
|
|
vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Required(CONF_CONDITION): "numeric_state",
|
2020-06-15 20:54:19 +00:00
|
|
|
vol.Required(CONF_ENTITY_ID): entity_ids,
|
2020-08-19 18:01:27 +00:00
|
|
|
vol.Optional(CONF_ATTRIBUTE): str,
|
2020-09-06 18:04:07 +00:00
|
|
|
CONF_BELOW: vol.Any(
|
|
|
|
vol.Coerce(float), vol.All(str, entity_domain("input_number"))
|
|
|
|
),
|
|
|
|
CONF_ABOVE: vol.Any(
|
|
|
|
vol.Coerce(float), vol.All(str, entity_domain("input_number"))
|
|
|
|
),
|
2019-07-31 19:25:30 +00:00
|
|
|
vol.Optional(CONF_VALUE_TEMPLATE): template,
|
|
|
|
}
|
|
|
|
),
|
|
|
|
has_at_least_one_key(CONF_BELOW, CONF_ABOVE),
|
|
|
|
)
|
|
|
|
|
2020-10-05 10:53:12 +00:00
|
|
|
STATE_CONDITION_BASE_SCHEMA = {
|
|
|
|
vol.Required(CONF_CONDITION): "state",
|
|
|
|
vol.Required(CONF_ENTITY_ID): entity_ids,
|
|
|
|
vol.Optional(CONF_ATTRIBUTE): str,
|
|
|
|
vol.Optional(CONF_FOR): positive_time_period,
|
|
|
|
# To support use_trigger_value in automation
|
|
|
|
# Deprecated 2016/04/25
|
|
|
|
vol.Optional("from"): str,
|
|
|
|
}
|
|
|
|
|
|
|
|
STATE_CONDITION_STATE_SCHEMA = vol.Schema(
|
|
|
|
{
|
|
|
|
**STATE_CONDITION_BASE_SCHEMA,
|
|
|
|
vol.Required(CONF_STATE): vol.Any(str, [str]),
|
|
|
|
}
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
|
|
|
|
2020-10-05 10:53:12 +00:00
|
|
|
STATE_CONDITION_ATTRIBUTE_SCHEMA = vol.Schema(
|
|
|
|
{
|
|
|
|
**STATE_CONDITION_BASE_SCHEMA,
|
|
|
|
vol.Required(CONF_STATE): match_all,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def STATE_CONDITION_SCHEMA(value: Any) -> dict: # pylint: disable=invalid-name
|
|
|
|
"""Validate a state condition."""
|
|
|
|
if not isinstance(value, dict):
|
|
|
|
raise vol.Invalid("Expected a dictionary")
|
|
|
|
|
|
|
|
if CONF_ATTRIBUTE in value:
|
|
|
|
validated: dict = STATE_CONDITION_ATTRIBUTE_SCHEMA(value)
|
|
|
|
else:
|
|
|
|
validated = STATE_CONDITION_STATE_SCHEMA(value)
|
|
|
|
|
|
|
|
return key_dependency("for", "state")(validated)
|
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
SUN_CONDITION_SCHEMA = vol.All(
|
|
|
|
vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Required(CONF_CONDITION): "sun",
|
|
|
|
vol.Optional("before"): sun_event,
|
|
|
|
vol.Optional("before_offset"): time_period,
|
|
|
|
vol.Optional("after"): vol.All(
|
|
|
|
vol.Lower, vol.Any(SUN_EVENT_SUNSET, SUN_EVENT_SUNRISE)
|
|
|
|
),
|
|
|
|
vol.Optional("after_offset"): time_period,
|
|
|
|
}
|
|
|
|
),
|
|
|
|
has_at_least_one_key("before", "after"),
|
|
|
|
)
|
|
|
|
|
|
|
|
TEMPLATE_CONDITION_SCHEMA = vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Required(CONF_CONDITION): "template",
|
|
|
|
vol.Required(CONF_VALUE_TEMPLATE): template,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
TIME_CONDITION_SCHEMA = vol.All(
|
|
|
|
vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Required(CONF_CONDITION): "time",
|
2020-09-06 14:06:09 +00:00
|
|
|
"before": vol.Any(time, vol.All(str, entity_domain("input_datetime"))),
|
|
|
|
"after": vol.Any(time, vol.All(str, entity_domain("input_datetime"))),
|
2019-07-31 19:25:30 +00:00
|
|
|
"weekday": weekdays,
|
|
|
|
}
|
|
|
|
),
|
|
|
|
has_at_least_one_key("before", "after", "weekday"),
|
|
|
|
)
|
|
|
|
|
|
|
|
ZONE_CONDITION_SCHEMA = vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Required(CONF_CONDITION): "zone",
|
2020-06-15 20:54:19 +00:00
|
|
|
vol.Required(CONF_ENTITY_ID): entity_ids,
|
2020-06-15 22:53:13 +00:00
|
|
|
"zone": entity_ids,
|
2019-07-31 19:25:30 +00:00
|
|
|
# To support use_trigger_value in automation
|
|
|
|
# Deprecated 2016/04/25
|
|
|
|
vol.Optional("event"): vol.Any("enter", "leave"),
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
AND_CONDITION_SCHEMA = vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Required(CONF_CONDITION): "and",
|
2020-07-14 17:22:54 +00:00
|
|
|
vol.Required(CONF_CONDITIONS): vol.All(
|
2019-07-31 19:25:30 +00:00
|
|
|
ensure_list,
|
|
|
|
# pylint: disable=unnecessary-lambda
|
|
|
|
[lambda value: CONDITION_SCHEMA(value)],
|
|
|
|
),
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
OR_CONDITION_SCHEMA = vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Required(CONF_CONDITION): "or",
|
2020-07-14 17:22:54 +00:00
|
|
|
vol.Required(CONF_CONDITIONS): vol.All(
|
2019-07-31 19:25:30 +00:00
|
|
|
ensure_list,
|
|
|
|
# pylint: disable=unnecessary-lambda
|
|
|
|
[lambda value: CONDITION_SCHEMA(value)],
|
|
|
|
),
|
|
|
|
}
|
|
|
|
)
|
2016-04-28 10:03:57 +00:00
|
|
|
|
2020-04-24 16:40:23 +00:00
|
|
|
NOT_CONDITION_SCHEMA = vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Required(CONF_CONDITION): "not",
|
2020-07-14 17:22:54 +00:00
|
|
|
vol.Required(CONF_CONDITIONS): vol.All(
|
2020-04-24 16:40:23 +00:00
|
|
|
ensure_list,
|
|
|
|
# pylint: disable=unnecessary-lambda
|
|
|
|
[lambda value: CONDITION_SCHEMA(value)],
|
|
|
|
),
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2019-09-24 21:57:05 +00:00
|
|
|
DEVICE_CONDITION_BASE_SCHEMA = vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Required(CONF_CONDITION): "device",
|
|
|
|
vol.Required(CONF_DEVICE_ID): str,
|
|
|
|
vol.Required(CONF_DOMAIN): str,
|
|
|
|
}
|
2019-09-05 14:49:32 +00:00
|
|
|
)
|
|
|
|
|
2019-09-24 21:57:05 +00:00
|
|
|
DEVICE_CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA)
|
|
|
|
|
2020-09-06 14:55:06 +00:00
|
|
|
CONDITION_SCHEMA: vol.Schema = vol.Schema(
|
|
|
|
vol.Any(
|
|
|
|
key_value_schemas(
|
|
|
|
CONF_CONDITION,
|
|
|
|
{
|
|
|
|
"numeric_state": NUMERIC_STATE_CONDITION_SCHEMA,
|
|
|
|
"state": STATE_CONDITION_SCHEMA,
|
|
|
|
"sun": SUN_CONDITION_SCHEMA,
|
|
|
|
"template": TEMPLATE_CONDITION_SCHEMA,
|
|
|
|
"time": TIME_CONDITION_SCHEMA,
|
|
|
|
"zone": ZONE_CONDITION_SCHEMA,
|
|
|
|
"and": AND_CONDITION_SCHEMA,
|
|
|
|
"or": OR_CONDITION_SCHEMA,
|
|
|
|
"not": NOT_CONDITION_SCHEMA,
|
|
|
|
"device": DEVICE_CONDITION_SCHEMA,
|
|
|
|
},
|
|
|
|
),
|
|
|
|
dynamic_template,
|
|
|
|
)
|
2019-09-04 03:36:04 +00:00
|
|
|
)
|
2016-04-28 10:03:57 +00:00
|
|
|
|
2020-08-17 16:54:56 +00:00
|
|
|
TRIGGER_SCHEMA = vol.All(
|
|
|
|
ensure_list, [vol.Schema({vol.Required(CONF_PLATFORM): str}, extra=vol.ALLOW_EXTRA)]
|
|
|
|
)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
_SCRIPT_DELAY_SCHEMA = vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Optional(CONF_ALIAS): string,
|
2020-08-12 18:42:06 +00:00
|
|
|
vol.Required(CONF_DELAY): positive_time_period_template,
|
2019-07-31 19:25:30 +00:00
|
|
|
}
|
|
|
|
)
|
2016-04-21 22:52:20 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
_SCRIPT_WAIT_TEMPLATE_SCHEMA = vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Optional(CONF_ALIAS): string,
|
2020-03-05 19:44:42 +00:00
|
|
|
vol.Required(CONF_WAIT_TEMPLATE): template,
|
2020-08-12 18:42:06 +00:00
|
|
|
vol.Optional(CONF_TIMEOUT): positive_time_period_template,
|
2020-03-05 19:44:42 +00:00
|
|
|
vol.Optional(CONF_CONTINUE_ON_TIMEOUT): boolean,
|
2019-07-31 19:25:30 +00:00
|
|
|
}
|
|
|
|
)
|
2017-02-12 21:27:53 +00:00
|
|
|
|
2019-09-24 21:57:05 +00:00
|
|
|
DEVICE_ACTION_BASE_SCHEMA = vol.Schema(
|
|
|
|
{vol.Required(CONF_DEVICE_ID): string, vol.Required(CONF_DOMAIN): str}
|
2019-09-05 23:26:22 +00:00
|
|
|
)
|
|
|
|
|
2019-09-24 21:57:05 +00:00
|
|
|
DEVICE_ACTION_SCHEMA = DEVICE_ACTION_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA)
|
|
|
|
|
2020-03-05 19:44:42 +00:00
|
|
|
_SCRIPT_SCENE_SCHEMA = vol.Schema({vol.Required(CONF_SCENE): entity_domain("scene")})
|
|
|
|
|
2020-07-10 18:37:19 +00:00
|
|
|
_SCRIPT_REPEAT_SCHEMA = vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Optional(CONF_ALIAS): string,
|
|
|
|
vol.Required(CONF_REPEAT): vol.All(
|
|
|
|
{
|
|
|
|
vol.Exclusive(CONF_COUNT, "repeat"): vol.Any(vol.Coerce(int), template),
|
|
|
|
vol.Exclusive(CONF_WHILE, "repeat"): vol.All(
|
|
|
|
ensure_list, [CONDITION_SCHEMA]
|
|
|
|
),
|
|
|
|
vol.Exclusive(CONF_UNTIL, "repeat"): vol.All(
|
|
|
|
ensure_list, [CONDITION_SCHEMA]
|
|
|
|
),
|
|
|
|
vol.Required(CONF_SEQUENCE): SCRIPT_SCHEMA,
|
|
|
|
},
|
|
|
|
has_at_least_one_key(CONF_COUNT, CONF_WHILE, CONF_UNTIL),
|
|
|
|
),
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2020-07-14 17:22:54 +00:00
|
|
|
_SCRIPT_CHOOSE_SCHEMA = vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Optional(CONF_ALIAS): string,
|
|
|
|
vol.Required(CONF_CHOOSE): vol.All(
|
|
|
|
ensure_list,
|
|
|
|
[
|
|
|
|
{
|
|
|
|
vol.Required(CONF_CONDITIONS): vol.All(
|
|
|
|
ensure_list, [CONDITION_SCHEMA]
|
|
|
|
),
|
|
|
|
vol.Required(CONF_SEQUENCE): SCRIPT_SCHEMA,
|
|
|
|
}
|
|
|
|
],
|
|
|
|
),
|
|
|
|
vol.Optional(CONF_DEFAULT): SCRIPT_SCHEMA,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2020-08-21 09:38:25 +00:00
|
|
|
_SCRIPT_WAIT_FOR_TRIGGER_SCHEMA = vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Optional(CONF_ALIAS): string,
|
|
|
|
vol.Required(CONF_WAIT_FOR_TRIGGER): TRIGGER_SCHEMA,
|
|
|
|
vol.Optional(CONF_TIMEOUT): positive_time_period_template,
|
|
|
|
vol.Optional(CONF_CONTINUE_ON_TIMEOUT): boolean,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2020-09-11 11:16:25 +00:00
|
|
|
_SCRIPT_SET_SCHEMA = vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Optional(CONF_ALIAS): string,
|
|
|
|
vol.Required(CONF_VARIABLES): SCRIPT_VARIABLES_SCHEMA,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2020-03-05 19:44:42 +00:00
|
|
|
SCRIPT_ACTION_DELAY = "delay"
|
|
|
|
SCRIPT_ACTION_WAIT_TEMPLATE = "wait_template"
|
|
|
|
SCRIPT_ACTION_CHECK_CONDITION = "condition"
|
|
|
|
SCRIPT_ACTION_FIRE_EVENT = "event"
|
|
|
|
SCRIPT_ACTION_CALL_SERVICE = "call_service"
|
|
|
|
SCRIPT_ACTION_DEVICE_AUTOMATION = "device"
|
|
|
|
SCRIPT_ACTION_ACTIVATE_SCENE = "scene"
|
2020-07-10 18:37:19 +00:00
|
|
|
SCRIPT_ACTION_REPEAT = "repeat"
|
2020-07-14 17:22:54 +00:00
|
|
|
SCRIPT_ACTION_CHOOSE = "choose"
|
2020-08-21 09:38:25 +00:00
|
|
|
SCRIPT_ACTION_WAIT_FOR_TRIGGER = "wait_for_trigger"
|
2020-09-11 11:16:25 +00:00
|
|
|
SCRIPT_ACTION_VARIABLES = "variables"
|
2020-03-05 19:44:42 +00:00
|
|
|
|
|
|
|
|
|
|
|
def determine_script_action(action: dict) -> str:
|
|
|
|
"""Determine action type."""
|
|
|
|
if CONF_DELAY in action:
|
|
|
|
return SCRIPT_ACTION_DELAY
|
|
|
|
|
|
|
|
if CONF_WAIT_TEMPLATE in action:
|
|
|
|
return SCRIPT_ACTION_WAIT_TEMPLATE
|
|
|
|
|
|
|
|
if CONF_CONDITION in action:
|
|
|
|
return SCRIPT_ACTION_CHECK_CONDITION
|
|
|
|
|
|
|
|
if CONF_EVENT in action:
|
|
|
|
return SCRIPT_ACTION_FIRE_EVENT
|
|
|
|
|
|
|
|
if CONF_DEVICE_ID in action:
|
|
|
|
return SCRIPT_ACTION_DEVICE_AUTOMATION
|
|
|
|
|
|
|
|
if CONF_SCENE in action:
|
|
|
|
return SCRIPT_ACTION_ACTIVATE_SCENE
|
|
|
|
|
2020-07-10 18:37:19 +00:00
|
|
|
if CONF_REPEAT in action:
|
|
|
|
return SCRIPT_ACTION_REPEAT
|
|
|
|
|
2020-07-14 17:22:54 +00:00
|
|
|
if CONF_CHOOSE in action:
|
|
|
|
return SCRIPT_ACTION_CHOOSE
|
|
|
|
|
2020-08-21 09:38:25 +00:00
|
|
|
if CONF_WAIT_FOR_TRIGGER in action:
|
|
|
|
return SCRIPT_ACTION_WAIT_FOR_TRIGGER
|
|
|
|
|
2020-09-11 11:16:25 +00:00
|
|
|
if CONF_VARIABLES in action:
|
|
|
|
return SCRIPT_ACTION_VARIABLES
|
|
|
|
|
2020-03-05 19:44:42 +00:00
|
|
|
return SCRIPT_ACTION_CALL_SERVICE
|
|
|
|
|
|
|
|
|
|
|
|
ACTION_TYPE_SCHEMAS: Dict[str, Callable[[Any], dict]] = {
|
|
|
|
SCRIPT_ACTION_CALL_SERVICE: SERVICE_SCHEMA,
|
|
|
|
SCRIPT_ACTION_DELAY: _SCRIPT_DELAY_SCHEMA,
|
|
|
|
SCRIPT_ACTION_WAIT_TEMPLATE: _SCRIPT_WAIT_TEMPLATE_SCHEMA,
|
|
|
|
SCRIPT_ACTION_FIRE_EVENT: EVENT_SCHEMA,
|
|
|
|
SCRIPT_ACTION_CHECK_CONDITION: CONDITION_SCHEMA,
|
|
|
|
SCRIPT_ACTION_DEVICE_AUTOMATION: DEVICE_ACTION_SCHEMA,
|
|
|
|
SCRIPT_ACTION_ACTIVATE_SCENE: _SCRIPT_SCENE_SCHEMA,
|
2020-07-10 18:37:19 +00:00
|
|
|
SCRIPT_ACTION_REPEAT: _SCRIPT_REPEAT_SCHEMA,
|
2020-07-14 17:22:54 +00:00
|
|
|
SCRIPT_ACTION_CHOOSE: _SCRIPT_CHOOSE_SCHEMA,
|
2020-08-21 09:38:25 +00:00
|
|
|
SCRIPT_ACTION_WAIT_FOR_TRIGGER: _SCRIPT_WAIT_FOR_TRIGGER_SCHEMA,
|
2020-09-11 11:16:25 +00:00
|
|
|
SCRIPT_ACTION_VARIABLES: _SCRIPT_SET_SCHEMA,
|
2020-03-05 19:44:42 +00:00
|
|
|
}
|