2018-04-07 01:23:03 +02:00
""" Helpers for config validation using voluptuous. """
2021-09-20 11:47:51 +02:00
from dataclasses import dataclass
2018-04-07 01:23:03 +02:00
import logging
2018-09-23 18:58:41 +02:00
import os
2018-05-06 15:56:12 +02:00
import re
2019-04-22 21:56:30 +02:00
from contextlib import contextmanager
2018-06-12 21:18:04 +02:00
import uuid as uuid_
2019-04-17 12:06:00 +02:00
from datetime import datetime
2019-05-12 23:04:36 +02:00
from string import ascii_letters , digits
2018-04-07 01:23:03 +02:00
import voluptuous as vol
2019-02-13 16:54:02 +01:00
from esphome import core
2021-11-07 19:24:52 +01:00
import esphome . codegen as cg
2023-12-06 02:04:17 +01:00
from esphome . config_helpers import Extend , Remove
2021-03-07 20:03:16 +01:00
from esphome . const import (
ALLOWED_NAME_CHARS ,
CONF_AVAILABILITY ,
CONF_COMMAND_TOPIC ,
2022-01-23 09:05:37 +01:00
CONF_COMMAND_RETAIN ,
2021-08-10 03:45:31 +02:00
CONF_DISABLED_BY_DEFAULT ,
2021-03-07 20:03:16 +01:00
CONF_DISCOVERY ,
2021-11-07 19:24:52 +01:00
CONF_ENTITY_CATEGORY ,
2021-10-10 10:37:05 +02:00
CONF_ICON ,
2021-03-07 20:03:16 +01:00
CONF_ID ,
CONF_INTERNAL ,
CONF_NAME ,
CONF_PAYLOAD_AVAILABLE ,
CONF_PAYLOAD_NOT_AVAILABLE ,
CONF_RETAIN ,
2024-03-11 01:12:52 +01:00
CONF_QOS ,
2021-03-07 20:03:16 +01:00
CONF_SETUP_PRIORITY ,
CONF_STATE_TOPIC ,
CONF_TOPIC ,
2024-03-10 19:52:22 +01:00
CONF_YEAR ,
CONF_MONTH ,
CONF_DAY ,
2021-03-07 20:03:16 +01:00
CONF_HOUR ,
CONF_MINUTE ,
CONF_SECOND ,
CONF_VALUE ,
CONF_UPDATE_INTERVAL ,
CONF_TYPE_ID ,
CONF_TYPE ,
2023-02-23 02:22:39 +01:00
CONF_REF ,
CONF_URL ,
CONF_PATH ,
CONF_USERNAME ,
CONF_PASSWORD ,
2021-11-07 19:24:52 +01:00
ENTITY_CATEGORY_CONFIG ,
ENTITY_CATEGORY_DIAGNOSTIC ,
ENTITY_CATEGORY_NONE ,
2021-09-20 11:47:51 +02:00
KEY_CORE ,
KEY_FRAMEWORK_VERSION ,
KEY_TARGET_FRAMEWORK ,
2022-10-25 02:47:48 +02:00
KEY_TARGET_PLATFORM ,
2023-10-09 23:54:15 +02:00
PLATFORM_ESP32 ,
PLATFORM_ESP8266 ,
PLATFORM_RP2040 ,
2023-02-23 02:22:39 +01:00
TYPE_GIT ,
TYPE_LOCAL ,
2023-05-17 06:33:08 +02:00
VALID_SUBSTITUTIONS_CHARACTERS ,
2024-02-18 23:52:37 +01:00
__version__ as ESPHOME_VERSION ,
2021-03-07 20:03:16 +01:00
)
from esphome . core import (
CORE ,
HexInt ,
IPAddress ,
Lambda ,
TimePeriod ,
TimePeriodMicroseconds ,
TimePeriodMilliseconds ,
2023-11-08 09:34:44 +01:00
TimePeriodNanoseconds ,
2021-03-07 20:03:16 +01:00
TimePeriodSeconds ,
TimePeriodMinutes ,
)
2019-12-04 15:58:40 +01:00
from esphome . helpers import list_starts_with , add_class_to_obj
2022-06-17 03:46:20 +02:00
from esphome . schema_extractors import (
SCHEMA_EXTRACT ,
schema_extractor_list ,
schema_extractor ,
schema_extractor_registry ,
schema_extractor_typed ,
2021-05-15 01:35:39 +02:00
)
2022-01-24 20:10:27 +01:00
from esphome . util import parse_esphome_version
2019-02-26 19:22:33 +01:00
from esphome . voluptuous_schema import _Schema
2021-02-06 16:09:15 +01:00
from esphome . yaml_util import make_data_base
2018-04-07 01:23:03 +02:00
_LOGGER = logging . getLogger ( __name__ )
2023-05-17 06:33:08 +02:00
# pylint: disable=consider-using-f-string
VARIABLE_PROG = re . compile (
" \\ $([ {0} ]+| \\ {{ [ {0} ]* \\ }}) " . format ( VALID_SUBSTITUTIONS_CHARACTERS )
)
2018-04-10 17:17:46 +02:00
# pylint: disable=invalid-name
2019-02-26 19:22:33 +01:00
Schema = _Schema
2019-04-17 12:06:00 +02:00
All = vol . All
Coerce = vol . Coerce
Range = vol . Range
Invalid = vol . Invalid
MultipleInvalid = vol . MultipleInvalid
Any = vol . Any
Lower = vol . Lower
Upper = vol . Upper
Length = vol . Length
Exclusive = vol . Exclusive
Inclusive = vol . Inclusive
ALLOW_EXTRA = vol . ALLOW_EXTRA
2019-04-22 21:56:30 +02:00
UNDEFINED = vol . UNDEFINED
RequiredFieldInvalid = vol . RequiredFieldInvalid
2021-06-17 21:54:14 +02:00
# this sentinel object can be placed in an 'Invalid' path to say
# the rest of the error path is relative to the root config path
ROOT_CONFIG_PATH = object ( )
2018-04-07 01:23:03 +02:00
RESERVED_IDS = [
# C++ keywords http://en.cppreference.com/w/cpp/keyword
2023-07-04 02:28:19 +02:00
" alarm " ,
2021-03-07 20:03:16 +01:00
" alignas " ,
" alignof " ,
" and " ,
" and_eq " ,
" asm " ,
" auto " ,
" bitand " ,
" bitor " ,
" bool " ,
" break " ,
" case " ,
" catch " ,
" char " ,
" char16_t " ,
" char32_t " ,
" class " ,
2023-08-21 02:17:22 +02:00
" clock " ,
2021-03-07 20:03:16 +01:00
" compl " ,
" concept " ,
" const " ,
" constexpr " ,
" const_cast " ,
" continue " ,
" decltype " ,
" default " ,
" delete " ,
" do " ,
" double " ,
" dynamic_cast " ,
" else " ,
" enum " ,
" explicit " ,
" export " ,
" export " ,
" extern " ,
" false " ,
" float " ,
" for " ,
" friend " ,
" goto " ,
" if " ,
" inline " ,
" int " ,
" long " ,
" mutable " ,
" namespace " ,
" new " ,
" noexcept " ,
" not " ,
" not_eq " ,
" nullptr " ,
" operator " ,
" or " ,
" or_eq " ,
" private " ,
" protected " ,
" public " ,
" register " ,
" reinterpret_cast " ,
" requires " ,
" return " ,
" short " ,
" signed " ,
" sizeof " ,
" static " ,
" static_assert " ,
" static_cast " ,
" struct " ,
" switch " ,
" template " ,
2023-10-25 11:00:32 +02:00
" text " ,
2021-03-07 20:03:16 +01:00
" this " ,
" thread_local " ,
" throw " ,
" true " ,
" try " ,
" typedef " ,
" typeid " ,
" typename " ,
" union " ,
" unsigned " ,
" using " ,
" virtual " ,
" void " ,
" volatile " ,
" wchar_t " ,
" while " ,
" xor " ,
" xor_eq " ,
" App " ,
" pinMode " ,
" delay " ,
" delayMicroseconds " ,
" digitalRead " ,
" digitalWrite " ,
" INPUT " ,
" OUTPUT " ,
" uint8_t " ,
" uint16_t " ,
" uint32_t " ,
" uint64_t " ,
" int8_t " ,
" int16_t " ,
" int32_t " ,
" int64_t " ,
" close " ,
" pause " ,
" sleep " ,
" open " ,
" setup " ,
" loop " ,
2023-03-20 03:29:21 +01:00
" uart0 " ,
" uart1 " ,
" uart2 " ,
2018-04-07 01:23:03 +02:00
]
2019-04-22 21:56:30 +02:00
class Optional ( vol . Optional ) :
""" Mark a field as optional and optionally define a default for the field.
When no default is defined , the validated config will not contain the key .
You can check if the key is defined with ' CONF_<KEY> in config ' . Or to access
the key and return None if it does not exist , call config . get ( CONF_ < KEY > )
If a default * is * set , the resulting validated config will always contain the
default value . You can therefore directly access the value using the
' config[CONF_<KEY>] ' syntax .
In ESPHome , all configuration defaults should be defined with the Optional class
during config validation - specifically * not * in the C + + code or the code generation
phase .
"""
2019-10-18 09:17:16 +02:00
2019-04-22 21:56:30 +02:00
def __init__ ( self , key , default = UNDEFINED ) :
2019-12-07 18:28:55 +01:00
super ( ) . __init__ ( key , default = default )
2019-04-22 21:56:30 +02:00
class Required ( vol . Required ) :
""" Define a field to be required to be set. The validated configuration is guaranteed
to contain this key .
All required values should be acceessed with the ` config [ CONF_ < KEY > ] ` syntax in code
- * not * the ` config . get ( CONF_ < KEY > ) ` syntax .
"""
2019-10-18 09:17:16 +02:00
2021-06-17 21:54:14 +02:00
def __init__ ( self , key , msg = None ) :
super ( ) . __init__ ( key , msg = msg )
2019-04-22 21:56:30 +02:00
2024-05-10 00:57:47 +02:00
class FinalExternalInvalid ( Invalid ) :
""" Represents an invalid value in the final validation phase where the path should not be prepended. """
2019-04-22 21:56:30 +02:00
def check_not_templatable ( value ) :
if isinstance ( value , Lambda ) :
raise Invalid ( " This option is not templatable! " )
2018-04-07 01:23:03 +02:00
def alphanumeric ( value ) :
if value is None :
2019-04-17 12:06:00 +02:00
raise Invalid ( " string value is None " )
2019-12-07 18:28:55 +01:00
value = str ( value )
2018-04-07 01:23:03 +02:00
if not value . isalnum ( ) :
2020-07-14 13:11:58 +02:00
raise Invalid ( f " { value } is not alphanumeric " )
2018-04-07 01:23:03 +02:00
return value
def valid_name ( value ) :
value = string_strict ( value )
2023-05-17 06:33:08 +02:00
if CORE . vscode :
# If the value is a substitution, it can't be validated until the substitution
# is actually made.
sub_match = VARIABLE_PROG . search ( value )
if sub_match :
return value
2018-06-03 13:00:40 +02:00
for c in value :
if c not in ALLOWED_NAME_CHARS :
2021-03-07 20:03:16 +01:00
raise Invalid (
f " ' { c } ' is an invalid character for names. Valid characters are: "
f " { ALLOWED_NAME_CHARS } (lowercase, no spaces) "
)
2018-04-07 01:23:03 +02:00
return value
def string ( value ) :
2019-04-22 21:56:30 +02:00
""" Validate that a configuration value is a string. If not, automatically converts to a string.
Note that this can be lossy , for example the input value 60.00 ( float ) will be turned into
2024-04-26 13:41:43 +02:00
" 60.0 " ( string ) . For values where this could be a problem ` string_strict ` has to be used .
2019-04-22 21:56:30 +02:00
"""
check_not_templatable ( value )
2018-04-07 01:23:03 +02:00
if isinstance ( value , ( dict , list ) ) :
2019-04-17 12:06:00 +02:00
raise Invalid ( " string value cannot be dictionary or list. " )
2019-04-24 23:49:02 +02:00
if isinstance ( value , bool ) :
2021-03-07 20:03:16 +01:00
raise Invalid (
" Auto-converted this value to boolean, please wrap the value in quotes. "
)
2019-12-07 18:28:55 +01:00
if isinstance ( value , str ) :
2019-04-22 21:56:30 +02:00
return value
2018-04-07 01:23:03 +02:00
if value is not None :
2019-12-07 18:28:55 +01:00
return str ( value )
2019-04-17 12:06:00 +02:00
raise Invalid ( " string value is None " )
2018-04-07 01:23:03 +02:00
def string_strict ( value ) :
2019-04-22 21:56:30 +02:00
""" Like string, but only allows strings, and does not automatically convert other types to
strings . """
check_not_templatable ( value )
2019-12-07 18:28:55 +01:00
if isinstance ( value , str ) :
2018-04-07 01:23:03 +02:00
return value
2021-03-07 20:03:16 +01:00
raise Invalid (
2021-09-19 19:22:28 +02:00
f " Must be string, got { type ( value ) } . did you forget putting quotes around the value? "
2021-03-07 20:03:16 +01:00
)
2018-04-07 01:23:03 +02:00
def icon ( value ) :
2019-04-22 21:56:30 +02:00
""" Validate that a given config value is a valid icon. """
2018-04-07 01:23:03 +02:00
value = string_strict ( value )
2019-04-22 21:56:30 +02:00
if not value :
return value
2021-11-23 09:21:14 +01:00
if re . match ( " ^[ \\ w \\ -]+:[ \\ w \\ -]+$ " , value ) :
2018-04-07 01:23:03 +02:00
return value
2021-11-23 09:21:14 +01:00
raise Invalid (
' Icons must match the format " [icon pack]:[icon] " , e.g. " mdi:home-assistant " '
)
2018-04-07 01:23:03 +02:00
def boolean ( value ) :
2019-04-22 21:56:30 +02:00
""" Validate the given config option to be a boolean.
This option allows a bunch of different ways of expressing boolean values :
- instance of boolean
- ' true ' / ' false '
- ' yes ' / ' no '
- ' enable ' / disable
"""
check_not_templatable ( value )
if isinstance ( value , bool ) :
return value
2019-12-07 18:28:55 +01:00
if isinstance ( value , str ) :
2018-04-07 01:23:03 +02:00
value = value . lower ( )
2021-03-07 20:03:16 +01:00
if value in ( " true " , " yes " , " on " , " enable " ) :
2018-04-07 01:23:03 +02:00
return True
2021-03-07 20:03:16 +01:00
if value in ( " false " , " no " , " off " , " disable " ) :
2018-04-07 01:23:03 +02:00
return False
2021-03-07 20:03:16 +01:00
raise Invalid (
2021-09-19 19:22:28 +02:00
f " Expected boolean value, but cannot convert { value } to a boolean. Please use ' true ' or ' false ' "
2021-03-07 20:03:16 +01:00
)
2018-04-07 01:23:03 +02:00
2022-06-17 03:46:20 +02:00
@schema_extractor_list
2018-12-18 19:31:43 +01:00
def ensure_list ( * validators ) :
2019-04-22 21:56:30 +02:00
""" Validate this configuration option to be a list.
If the config value is not a list , it is automatically converted to a
single - item list .
None and empty dictionaries are converted to empty lists .
"""
2019-04-17 12:06:00 +02:00
user = All ( * validators )
2020-07-25 14:21:56 +02:00
list_schema = Schema ( [ user ] )
2018-12-18 19:31:43 +01:00
def validator ( value ) :
2019-04-22 21:56:30 +02:00
check_not_templatable ( value )
2018-12-18 19:31:43 +01:00
if value is None or ( isinstance ( value , dict ) and not value ) :
return [ ]
if not isinstance ( value , list ) :
return [ user ( value ) ]
2020-07-25 14:21:56 +02:00
return list_schema ( value )
2018-12-18 19:31:43 +01:00
return validator
2018-04-07 01:23:03 +02:00
2019-04-22 21:56:30 +02:00
def hex_int ( value ) :
""" Validate the given value to be a hex integer. This is mostly for cosmetic
purposes of the generated code .
"""
return HexInt ( int_ ( value ) )
2018-08-22 22:04:56 +02:00
2018-04-07 01:23:03 +02:00
2019-04-22 21:56:30 +02:00
def int_ ( value ) :
""" Validate that the config option is an integer.
2018-04-07 01:23:03 +02:00
2019-04-22 21:56:30 +02:00
Automatically also converts strings to ints .
"""
check_not_templatable ( value )
2019-12-07 18:28:55 +01:00
if isinstance ( value , int ) :
2019-04-22 21:56:30 +02:00
return value
2019-05-13 20:45:22 +02:00
if isinstance ( value , float ) :
if int ( value ) == value :
return int ( value )
2021-03-07 20:03:16 +01:00
raise Invalid (
2021-09-19 19:22:28 +02:00
f " This option only accepts integers with no fractional part. Please remove the fractional part from { value } "
2021-03-07 20:03:16 +01:00
)
2018-04-07 01:23:03 +02:00
value = string_strict ( value ) . lower ( )
2019-04-22 21:56:30 +02:00
base = 10
2021-03-07 20:03:16 +01:00
if value . startswith ( " 0x " ) :
2019-04-22 21:56:30 +02:00
base = 16
try :
return int ( value , base )
except ValueError :
2020-09-16 12:12:40 +02:00
# pylint: disable=raise-missing-from
2019-12-07 18:28:55 +01:00
raise Invalid ( f " Expected integer, but cannot parse { value } as an integer " )
2018-04-07 01:23:03 +02:00
2019-04-22 21:56:30 +02:00
def int_range ( min = None , max = None , min_included = True , max_included = True ) :
""" Validate that the config option is an integer in the given range. """
if min is not None :
2019-12-07 18:28:55 +01:00
assert isinstance ( min , int )
2019-04-22 21:56:30 +02:00
if max is not None :
2019-12-07 18:28:55 +01:00
assert isinstance ( max , int )
2021-03-07 20:03:16 +01:00
return All (
int_ ,
Range ( min = min , max = max , min_included = min_included , max_included = max_included ) ,
)
2019-04-22 21:56:30 +02:00
def hex_int_range ( min = None , max = None , min_included = True , max_included = True ) :
""" Validate that the config option is an integer in the given range. """
2021-03-07 20:03:16 +01:00
return All (
hex_int ,
Range ( min = min , max = max , min_included = min_included , max_included = max_included ) ,
)
2018-04-07 01:23:03 +02:00
2019-04-22 21:56:30 +02:00
def float_range ( min = None , max = None , min_included = True , max_included = True ) :
""" Validate that the config option is a floating point number in the given range. """
if min is not None :
assert isinstance ( min , ( int , float ) )
if max is not None :
assert isinstance ( max , ( int , float ) )
2021-03-07 20:03:16 +01:00
return All (
float_ ,
Range ( min = min , max = max , min_included = min_included , max_included = max_included ) ,
)
2019-04-22 21:56:30 +02:00
port = int_range ( min = 1 , max = 65535 )
float_ = Coerce ( float )
positive_float = float_range ( min = 0 )
zero_to_one_float = float_range ( min = 0 , max = 1 )
negative_one_to_one_float = float_range ( min = - 1 , max = 1 )
positive_int = int_range ( min = 0 )
positive_not_null_int = int_range ( min = 0 , min_included = False )
2018-04-07 01:23:03 +02:00
2019-02-26 19:38:28 +01:00
def validate_id_name ( value ) :
2019-04-22 21:56:30 +02:00
""" Validate that the given value would be a valid C++ identifier name. """
2018-06-01 18:06:18 +02:00
value = string ( value )
if not value :
2019-04-17 12:06:00 +02:00
raise Invalid ( " ID must not be empty " )
2018-06-01 18:06:18 +02:00
if value [ 0 ] . isdigit ( ) :
2019-04-17 12:06:00 +02:00
raise Invalid ( " First character in ID cannot be a digit. " )
2021-03-07 20:03:16 +01:00
if " - " in value :
raise Invalid (
" Dashes are not supported in IDs, please use underscores instead. "
)
2023-05-17 06:33:08 +02:00
if CORE . vscode :
# If the value is a substitution, it can't be validated until the substitution
# is actually made
sub_match = VARIABLE_PROG . match ( value )
if sub_match :
return value
2021-09-19 19:22:28 +02:00
valid_chars = f " { ascii_letters + digits } _ "
2018-06-01 18:06:18 +02:00
for char in value :
2019-05-12 23:04:36 +02:00
if char not in valid_chars :
2021-03-07 20:03:16 +01:00
raise Invalid (
2021-09-19 19:22:28 +02:00
f " IDs must only consist of upper/lowercase characters, the underscorecharacter and numbers. The character ' { char } ' cannot be used "
2021-03-07 20:03:16 +01:00
)
2018-04-07 01:23:03 +02:00
if value in RESERVED_IDS :
2019-12-07 18:28:55 +01:00
raise Invalid ( f " ID ' { value } ' is reserved internally and cannot be used " )
2019-04-24 23:49:02 +02:00
if value in CORE . loaded_integrations :
2021-03-07 20:03:16 +01:00
raise Invalid (
2021-09-19 19:22:28 +02:00
f " ID ' { value } ' conflicts with the name of an esphome integration, please use another ID name. "
2021-03-07 20:03:16 +01:00
)
2018-04-07 01:23:03 +02:00
return value
2019-04-22 21:56:30 +02:00
def use_id ( type ) :
""" Declare that this configuration option should point to an ID with the given type. """
2021-03-07 20:03:16 +01:00
2022-06-17 03:46:20 +02:00
@schema_extractor ( " use_id " )
2018-06-02 22:22:20 +02:00
def validator ( value ) :
2022-06-17 03:46:20 +02:00
if value == SCHEMA_EXTRACT :
return type
2019-04-22 21:56:30 +02:00
check_not_templatable ( value )
2018-06-02 22:22:20 +02:00
if value is None :
return core . ID ( None , is_declaration = False , type = type )
2021-03-07 20:03:16 +01:00
if (
isinstance ( value , core . ID )
and value . is_declaration is False
and value . type is type
) :
2019-04-22 21:56:30 +02:00
return value
2018-06-02 22:22:20 +02:00
2019-02-26 19:38:28 +01:00
return core . ID ( validate_id_name ( value ) , is_declaration = False , type = type )
2018-06-02 22:22:20 +02:00
return validator
2019-04-22 21:56:30 +02:00
def declare_id ( type ) :
""" Declare that this configuration option should be used to declare a variable ID
with the given type .
If two IDs with the same name exist , a validation error is thrown .
"""
2021-03-07 20:03:16 +01:00
2022-06-17 03:46:20 +02:00
@schema_extractor ( " declare_id " )
2018-06-02 22:22:20 +02:00
def validator ( value ) :
2022-06-17 03:46:20 +02:00
if value == SCHEMA_EXTRACT :
return type
2019-04-22 21:56:30 +02:00
check_not_templatable ( value )
2018-06-02 22:22:20 +02:00
if value is None :
return core . ID ( None , is_declaration = True , type = type )
2023-02-07 02:08:40 +01:00
if isinstance ( value , Extend ) :
raise Invalid ( f " Source for extension of ID ' { value . value } ' was not found. " )
2023-12-06 02:04:17 +01:00
if isinstance ( value , Remove ) :
raise Invalid ( f " Source for Removal of ID ' { value . value } ' was not found. " )
2019-02-26 19:38:28 +01:00
return core . ID ( validate_id_name ( value ) , is_declaration = True , type = type )
2018-06-02 22:22:20 +02:00
return validator
2018-05-20 12:41:52 +02:00
def templatable ( other_validators ) :
2019-04-22 21:56:30 +02:00
""" Validate that the configuration option can (optionally) be templated.
The user can declare a value as template by using the ' !lambda ' tag . In that case ,
validation is skipped . Otherwise ( if the value is not templated ) the validator given
as the first argument to this method is called .
"""
schema = Schema ( other_validators )
2022-06-17 03:46:20 +02:00
@schema_extractor ( " templatable " )
2018-05-20 12:41:52 +02:00
def validator ( value ) :
2022-06-17 03:46:20 +02:00
if value == SCHEMA_EXTRACT :
2022-04-03 09:30:22 +02:00
return other_validators
2022-06-17 03:46:20 +02:00
2018-05-20 12:41:52 +02:00
if isinstance ( value , Lambda ) :
2019-05-10 22:13:17 +02:00
return returning_lambda ( value )
2018-08-25 22:18:22 +02:00
if isinstance ( other_validators , dict ) :
2019-04-22 21:56:30 +02:00
return schema ( value )
return schema ( value )
2018-06-02 22:22:20 +02:00
2018-05-20 12:41:52 +02:00
return validator
2018-04-07 01:23:03 +02:00
def only_on ( platforms ) :
2021-09-20 11:47:51 +02:00
""" Validate that this option can only be specified on the given target platforms. """
2018-04-07 01:23:03 +02:00
if not isinstance ( platforms , list ) :
platforms = [ platforms ]
def validator_ ( obj ) :
2021-09-20 11:47:51 +02:00
if CORE . target_platform not in platforms :
2019-12-07 18:28:55 +01:00
raise Invalid ( f " This feature is only available on { platforms } " )
2018-04-07 01:23:03 +02:00
return obj
return validator_
2021-09-20 11:47:51 +02:00
def only_with_framework ( frameworks ) :
""" Validate that this option can only be specified on the given frameworks. """
if not isinstance ( frameworks , list ) :
frameworks = [ frameworks ]
def validator_ ( obj ) :
if CORE . target_framework not in frameworks :
raise Invalid (
f " This feature is only available with frameworks { frameworks } "
)
return obj
return validator_
2023-10-09 23:54:15 +02:00
only_on_esp32 = only_on ( PLATFORM_ESP32 )
only_on_esp8266 = only_on ( PLATFORM_ESP8266 )
only_on_rp2040 = only_on ( PLATFORM_RP2040 )
2021-09-20 11:47:51 +02:00
only_with_arduino = only_with_framework ( " arduino " )
only_with_esp_idf = only_with_framework ( " esp-idf " )
2018-04-07 01:23:03 +02:00
# Adapted from:
# https://github.com/alecthomas/voluptuous/issues/115#issuecomment-144464666
def has_at_least_one_key ( * keys ) :
2019-04-22 21:56:30 +02:00
""" Validate that at least one of the given keys exist in the config. """
2018-04-07 01:23:03 +02:00
def validate ( obj ) :
""" Test keys exist in dict. """
if not isinstance ( obj , dict ) :
2021-03-07 20:03:16 +01:00
raise Invalid ( " expected dictionary " )
2018-04-07 01:23:03 +02:00
2018-05-20 12:41:52 +02:00
if not any ( k in keys for k in obj ) :
2021-09-19 19:22:28 +02:00
raise Invalid ( f " Must contain at least one of { ' , ' . join ( keys ) } . " )
2018-05-20 12:41:52 +02:00
return obj
return validate
2018-06-01 22:58:23 +02:00
def has_exactly_one_key ( * keys ) :
2019-04-22 21:56:30 +02:00
""" Validate that exactly one of the given keys exist in the config. """
2021-03-07 20:03:16 +01:00
2018-05-20 12:41:52 +02:00
def validate ( obj ) :
if not isinstance ( obj , dict ) :
2021-03-07 20:03:16 +01:00
raise Invalid ( " expected dictionary " )
2018-05-20 12:41:52 +02:00
number = sum ( k in keys for k in obj )
if number > 1 :
2021-09-19 19:22:28 +02:00
raise Invalid ( f " Cannot specify more than one of { ' , ' . join ( keys ) } . " )
2018-05-20 12:41:52 +02:00
if number < 1 :
2021-09-19 19:22:28 +02:00
raise Invalid ( f " Must contain exactly one of { ' , ' . join ( keys ) } . " )
2018-05-20 12:41:52 +02:00
return obj
2018-04-07 01:23:03 +02:00
return validate
2018-09-23 18:58:41 +02:00
def has_at_most_one_key ( * keys ) :
2019-04-22 21:56:30 +02:00
""" Validate that at most one of the given keys exist in the config. """
2021-03-07 20:03:16 +01:00
2018-09-23 18:58:41 +02:00
def validate ( obj ) :
if not isinstance ( obj , dict ) :
2021-03-07 20:03:16 +01:00
raise Invalid ( " expected dictionary " )
2018-09-23 18:58:41 +02:00
number = sum ( k in keys for k in obj )
if number > 1 :
2021-09-19 19:22:28 +02:00
raise Invalid ( f " Cannot specify more than one of { ' , ' . join ( keys ) } . " )
2018-09-23 18:58:41 +02:00
return obj
return validate
2021-07-29 19:11:56 +02:00
def has_none_or_all_keys ( * keys ) :
""" Validate that none or all of the given keys exist in the config. """
def validate ( obj ) :
if not isinstance ( obj , dict ) :
raise Invalid ( " expected dictionary " )
number = sum ( k in keys for k in obj )
if number != 0 and number != len ( keys ) :
2021-09-19 19:22:28 +02:00
raise Invalid ( f " Must specify either none or all of { ' , ' . join ( keys ) } . " )
2021-07-29 19:11:56 +02:00
return obj
return validate
2021-03-07 20:03:16 +01:00
TIME_PERIOD_ERROR = (
" Time period {} should be format number + unit, for example 5ms, 5s, 5min, 5h "
)
2018-04-07 01:23:03 +02:00
2019-04-17 12:06:00 +02:00
time_period_dict = All (
2021-03-07 20:03:16 +01:00
Schema (
{
Optional ( " days " ) : float_ ,
Optional ( " hours " ) : float_ ,
Optional ( " minutes " ) : float_ ,
Optional ( " seconds " ) : float_ ,
Optional ( " milliseconds " ) : float_ ,
Optional ( " microseconds " ) : float_ ,
}
) ,
has_at_least_one_key (
" days " , " hours " , " minutes " , " seconds " , " milliseconds " , " microseconds "
) ,
lambda value : TimePeriod ( * * value ) ,
2019-04-22 21:56:30 +02:00
)
2018-05-14 11:50:56 +02:00
2018-04-07 01:23:03 +02:00
2018-05-06 15:56:12 +02:00
def time_period_str_colon ( value ) :
""" Validate and transform time offset with format HH:MM[:SS]. """
2018-04-07 01:23:03 +02:00
if isinstance ( value , int ) :
2021-03-07 20:03:16 +01:00
raise Invalid ( " Make sure you wrap time values in quotes " )
2019-02-28 10:33:22 +01:00
if not isinstance ( value , str ) :
2019-04-17 12:06:00 +02:00
raise Invalid ( TIME_PERIOD_ERROR . format ( value ) )
2018-04-07 01:23:03 +02:00
try :
2021-03-07 20:03:16 +01:00
parsed = [ int ( x ) for x in value . split ( " : " ) ]
2018-05-06 15:56:12 +02:00
except ValueError :
2020-09-16 12:12:40 +02:00
# pylint: disable=raise-missing-from
2019-04-17 12:06:00 +02:00
raise Invalid ( TIME_PERIOD_ERROR . format ( value ) )
2018-05-06 15:56:12 +02:00
if len ( parsed ) == 2 :
hour , minute = parsed
second = 0
elif len ( parsed ) == 3 :
hour , minute , second = parsed
else :
2019-04-17 12:06:00 +02:00
raise Invalid ( TIME_PERIOD_ERROR . format ( value ) )
2018-05-06 15:56:12 +02:00
2018-05-17 21:31:39 +02:00
return TimePeriod ( hours = hour , minutes = minute , seconds = second )
2018-05-06 15:56:12 +02:00
def time_period_str_unit ( value ) :
""" Validate and transform time period with time unit and integer value. """
2019-04-22 21:56:30 +02:00
check_not_templatable ( value )
2018-05-06 15:56:12 +02:00
if isinstance ( value , int ) :
2021-03-07 20:03:16 +01:00
raise Invalid (
2021-09-19 19:22:28 +02:00
f " Don ' t know what ' { value } ' means as it has no time *unit*! Did you mean ' { value } s ' ? "
2021-03-07 20:03:16 +01:00
)
2019-10-31 20:09:57 +01:00
if isinstance ( value , TimePeriod ) :
value = str ( value )
2019-12-07 18:28:55 +01:00
if not isinstance ( value , str ) :
2019-04-17 12:06:00 +02:00
raise Invalid ( " Expected string for time period with unit. " )
2018-05-06 15:56:12 +02:00
unit_to_kwarg = {
2023-11-08 09:34:44 +01:00
" ns " : " nanoseconds " ,
" nanoseconds " : " nanoseconds " ,
2021-03-07 20:03:16 +01:00
" us " : " microseconds " ,
" microseconds " : " microseconds " ,
" ms " : " milliseconds " ,
" milliseconds " : " milliseconds " ,
" s " : " seconds " ,
" sec " : " seconds " ,
" seconds " : " seconds " ,
" min " : " minutes " ,
" minutes " : " minutes " ,
" h " : " hours " ,
" hours " : " hours " ,
" d " : " days " ,
" days " : " days " ,
2018-05-06 15:56:12 +02:00
}
2018-05-06 17:40:37 +02:00
match = re . match ( r " ^([-+]?[0-9]* \ .?[0-9]*) \ s*( \ w*)$ " , value )
2018-05-06 15:56:12 +02:00
2018-12-05 21:22:06 +01:00
if match is None :
2021-09-19 19:22:28 +02:00
raise Invalid ( f " Expected time period with unit, got { value } " )
2018-12-05 21:22:06 +01:00
kwarg = unit_to_kwarg [ one_of ( * unit_to_kwarg ) ( match . group ( 2 ) ) ]
2018-05-06 15:56:12 +02:00
2023-11-08 09:34:44 +01:00
try :
return TimePeriod ( * * { kwarg : float ( match . group ( 1 ) ) } )
except ValueError as e :
raise Invalid ( e ) from e
2018-04-07 01:23:03 +02:00
2018-05-17 21:31:39 +02:00
def time_period_in_milliseconds_ ( value ) :
2018-05-14 11:50:56 +02:00
if value . microseconds is not None and value . microseconds != 0 :
2019-04-17 12:06:00 +02:00
raise Invalid ( " Maximum precision is milliseconds " )
2018-05-14 11:50:56 +02:00
return TimePeriodMilliseconds ( * * value . as_dict ( ) )
2018-04-07 01:23:03 +02:00
2018-05-17 21:31:39 +02:00
def time_period_in_microseconds_ ( value ) :
2023-11-08 09:34:44 +01:00
if value . nanoseconds is not None and value . nanoseconds != 0 :
raise Invalid ( " Maximum precision is microseconds " )
2018-05-14 11:50:56 +02:00
return TimePeriodMicroseconds ( * * value . as_dict ( ) )
2018-04-07 01:23:03 +02:00
2018-05-06 15:56:12 +02:00
2023-11-08 09:34:44 +01:00
def time_period_in_nanoseconds_ ( value ) :
return TimePeriodNanoseconds ( * * value . as_dict ( ) )
2018-05-17 21:31:39 +02:00
def time_period_in_seconds_ ( value ) :
2023-11-08 09:34:44 +01:00
if value . nanoseconds is not None and value . nanoseconds != 0 :
raise Invalid ( " Maximum precision is seconds " )
2018-05-14 11:50:56 +02:00
if value . microseconds is not None and value . microseconds != 0 :
2019-04-17 12:06:00 +02:00
raise Invalid ( " Maximum precision is seconds " )
2018-05-14 11:50:56 +02:00
if value . milliseconds is not None and value . milliseconds != 0 :
2019-04-17 12:06:00 +02:00
raise Invalid ( " Maximum precision is seconds " )
2018-05-14 11:50:56 +02:00
return TimePeriodSeconds ( * * value . as_dict ( ) )
2018-05-06 15:56:12 +02:00
2018-05-14 11:50:56 +02:00
2019-03-06 12:39:52 +01:00
def time_period_in_minutes_ ( value ) :
2023-11-08 09:34:44 +01:00
if value . nanoseconds is not None and value . nanoseconds != 0 :
raise Invalid ( " Maximum precision is minutes " )
2019-03-06 12:39:52 +01:00
if value . microseconds is not None and value . microseconds != 0 :
2019-04-17 12:06:00 +02:00
raise Invalid ( " Maximum precision is minutes " )
2019-03-06 12:39:52 +01:00
if value . milliseconds is not None and value . milliseconds != 0 :
2019-04-17 12:06:00 +02:00
raise Invalid ( " Maximum precision is minutes " )
2019-03-06 12:39:52 +01:00
if value . seconds is not None and value . seconds != 0 :
2019-04-17 12:06:00 +02:00
raise Invalid ( " Maximum precision is minutes " )
2019-03-06 12:39:52 +01:00
return TimePeriodMinutes ( * * value . as_dict ( ) )
2018-08-24 22:44:15 +02:00
def update_interval ( value ) :
2021-03-07 20:03:16 +01:00
if value == " never " :
2018-08-24 22:44:15 +02:00
return 4294967295 # uint32_t max
return positive_time_period_milliseconds ( value )
2019-04-17 12:06:00 +02:00
time_period = Any ( time_period_str_unit , time_period_str_colon , time_period_dict )
positive_time_period = All ( time_period , Range ( min = TimePeriod ( ) ) )
2021-03-07 20:03:16 +01:00
positive_time_period_milliseconds = All (
positive_time_period , time_period_in_milliseconds_
)
2019-04-17 12:06:00 +02:00
positive_time_period_seconds = All ( positive_time_period , time_period_in_seconds_ )
positive_time_period_minutes = All ( positive_time_period , time_period_in_minutes_ )
time_period_microseconds = All ( time_period , time_period_in_microseconds_ )
2021-03-07 20:03:16 +01:00
positive_time_period_microseconds = All (
positive_time_period , time_period_in_microseconds_
)
2023-11-08 09:34:44 +01:00
positive_time_period_nanoseconds = All (
positive_time_period , time_period_in_nanoseconds_
)
2021-03-07 20:03:16 +01:00
positive_not_null_time_period = All (
time_period , Range ( min = TimePeriod ( ) , min_included = False )
)
2018-04-07 01:23:03 +02:00
2018-06-11 10:01:54 +02:00
2019-04-08 21:57:25 +02:00
def time_of_day ( value ) :
2024-05-07 02:47:07 +02:00
return date_time ( date = False , time = True ) ( value )
2024-03-10 19:52:22 +01:00
2024-05-07 02:47:07 +02:00
def date_time ( date : bool , time : bool ) :
2024-03-10 19:52:22 +01:00
pattern_str = r " ^ " # Start of string
2024-05-07 02:47:07 +02:00
if date :
pattern_str + = r " \ d {4} - \ d { 1,2}- \ d { 1,2} "
if time :
pattern_str + = r " "
if time :
2024-03-10 19:52:22 +01:00
pattern_str + = (
2024-05-07 02:47:07 +02:00
r " \ d { 1,2}: \ d {2} " # Hour/Minute
r " (: \ d {2} )? " # 1. Seconds
r " ( " # 2. Optional AM/PM group
r " ( \ s)? " # 3. Optional Space
2024-03-10 19:52:22 +01:00
r " (?:AM|PM|am|pm) " # AM/PM string matching
r " )? " # End optional AM/PM group
)
pattern_str + = r " $ " # End of string
pattern = re . compile ( pattern_str )
exc_message = " "
2024-05-07 02:47:07 +02:00
if date :
2024-03-10 19:52:22 +01:00
exc_message + = " date "
2024-05-07 02:47:07 +02:00
if time :
2024-03-10 19:52:22 +01:00
exc_message + = " time "
schema = Schema ( { } )
2024-05-07 02:47:07 +02:00
if date :
2024-03-10 19:52:22 +01:00
schema = schema . extend (
{
2024-05-07 02:47:07 +02:00
Required ( CONF_YEAR ) : int_range ( min = 1970 , max = 3000 ) ,
Required ( CONF_MONTH ) : int_range ( min = 1 , max = 12 ) ,
Required ( CONF_DAY ) : int_range ( min = 1 , max = 31 ) ,
2024-03-10 19:52:22 +01:00
}
)
2024-05-07 02:47:07 +02:00
if time :
2024-03-10 19:52:22 +01:00
schema = schema . extend (
{
2024-05-07 02:47:07 +02:00
Required ( CONF_HOUR ) : int_range ( min = 0 , max = 23 ) ,
Required ( CONF_MINUTE ) : int_range ( min = 0 , max = 59 ) ,
Required ( CONF_SECOND ) : int_range ( min = 0 , max = 59 ) ,
2024-03-10 19:52:22 +01:00
}
)
def validator ( value ) :
if isinstance ( value , dict ) :
return schema ( value )
value = string ( value )
match = pattern . match ( value )
if match is None :
# pylint: disable=raise-missing-from
raise Invalid ( f " Invalid { exc_message } : { value } " )
2024-05-07 02:47:07 +02:00
if time :
has_seconds = match [ 1 ] is not None
has_ampm = match [ 2 ] is not None
has_ampm_space = match [ 3 ] is not None
2024-03-10 19:52:22 +01:00
format = " "
2024-05-07 02:47:07 +02:00
if date :
2024-03-10 19:52:22 +01:00
format + = " % Y- % m- %d "
2024-05-07 02:47:07 +02:00
if time :
2024-03-10 19:52:22 +01:00
format + = " "
2024-05-07 02:47:07 +02:00
if time :
if has_ampm :
format + = " % I: % M "
else :
format + = " % H: % M "
2024-03-10 19:52:22 +01:00
if has_seconds :
format + = " : % S "
if has_ampm_space :
format + = " "
if has_ampm :
format + = " % p "
2019-04-08 21:57:25 +02:00
try :
2024-03-10 19:52:22 +01:00
date_obj = datetime . strptime ( value , format )
except ValueError as err :
2020-09-16 12:12:40 +02:00
# pylint: disable=raise-missing-from
2024-03-10 19:52:22 +01:00
raise Invalid ( f " Invalid { exc_message } : { err } " )
2019-04-08 21:57:25 +02:00
2024-03-10 19:52:22 +01:00
return_value = { }
2024-05-07 02:47:07 +02:00
if date :
2024-03-10 19:52:22 +01:00
return_value [ CONF_YEAR ] = date_obj . year
return_value [ CONF_MONTH ] = date_obj . month
return_value [ CONF_DAY ] = date_obj . day
2024-05-07 02:47:07 +02:00
if time :
2024-03-10 19:52:22 +01:00
return_value [ CONF_HOUR ] = date_obj . hour
return_value [ CONF_MINUTE ] = date_obj . minute
return_value [ CONF_SECOND ] = date_obj . second if has_seconds else 0
return schema ( return_value )
return validator
2019-04-08 21:57:25 +02:00
2018-06-11 10:01:54 +02:00
def mac_address ( value ) :
value = string_strict ( value )
2021-03-07 20:03:16 +01:00
parts = value . split ( " : " )
2018-06-11 10:01:54 +02:00
if len ( parts ) != 6 :
2019-04-17 12:06:00 +02:00
raise Invalid ( " MAC Address must consist of 6 : (colon) separated parts " )
2018-06-11 10:01:54 +02:00
parts_int = [ ]
if any ( len ( part ) != 2 for part in parts ) :
2019-04-17 12:06:00 +02:00
raise Invalid ( " MAC Address must be format XX:XX:XX:XX:XX:XX " )
2018-06-11 10:01:54 +02:00
for part in parts :
try :
parts_int . append ( int ( part , 16 ) )
except ValueError :
2020-09-16 12:12:40 +02:00
# pylint: disable=raise-missing-from
2019-04-17 12:06:00 +02:00
raise Invalid ( " MAC Address parts must be hexadecimal values from 00 to FF " )
2018-06-11 10:01:54 +02:00
return core . MACAddress ( * parts_int )
2020-05-27 00:33:28 +02:00
def bind_key ( value ) :
value = string_strict ( value )
2021-03-07 20:03:16 +01:00
parts = [ value [ i : i + 2 ] for i in range ( 0 , len ( value ) , 2 ) ]
2020-05-27 00:33:28 +02:00
if len ( parts ) != 16 :
raise Invalid ( " Bind key must consist of 16 hexadecimal numbers " )
parts_int = [ ]
if any ( len ( part ) != 2 for part in parts ) :
raise Invalid ( " Bind key must be format XX " )
for part in parts :
try :
parts_int . append ( int ( part , 16 ) )
except ValueError :
2020-09-16 12:12:40 +02:00
# pylint: disable=raise-missing-from
2020-05-27 00:33:28 +02:00
raise Invalid ( " Bind key must be hex values from 00 to FF " )
2021-03-07 20:03:16 +01:00
return " " . join ( f " { part : 02X } " for part in parts_int )
2020-05-27 00:33:28 +02:00
2018-06-12 21:18:04 +02:00
def uuid ( value ) :
2019-04-17 12:06:00 +02:00
return Coerce ( uuid_ . UUID ) ( value )
2018-06-12 21:18:04 +02:00
2018-04-07 01:23:03 +02:00
METRIC_SUFFIXES = {
2021-03-07 20:03:16 +01:00
" E " : 1e18 ,
" P " : 1e15 ,
" T " : 1e12 ,
" G " : 1e9 ,
" M " : 1e6 ,
" k " : 1e3 ,
" da " : 10 ,
" d " : 1e-1 ,
" c " : 1e-2 ,
" m " : 0.001 ,
" µ " : 1e-6 ,
" u " : 1e-6 ,
" n " : 1e-9 ,
" p " : 1e-12 ,
" f " : 1e-15 ,
" a " : 1e-18 ,
" " : 1 ,
2018-04-07 01:23:03 +02:00
}
2019-05-11 12:31:00 +02:00
def float_with_unit ( quantity , regex_suffix , optional_unit = False ) :
2021-03-07 20:03:16 +01:00
pattern = re . compile (
2021-09-19 19:22:28 +02:00
f " ^([-+]?[0-9]* \\ .?[0-9]*) \\ s*( \\ w*?) { regex_suffix } $ " , re . UNICODE
2021-03-07 20:03:16 +01:00
)
2018-05-14 11:50:56 +02:00
2018-08-13 19:11:33 +02:00
def validator ( value ) :
2019-05-11 12:31:00 +02:00
if optional_unit :
try :
return float_ ( value )
except Invalid :
pass
2018-08-13 19:11:33 +02:00
match = pattern . match ( string ( value ) )
if match is None :
2019-12-07 18:28:55 +01:00
raise Invalid ( f " Expected { quantity } with unit, got { value } " )
2018-08-13 19:11:33 +02:00
mantissa = float ( match . group ( 1 ) )
if match . group ( 2 ) not in METRIC_SUFFIXES :
2021-09-19 19:22:28 +02:00
raise Invalid ( f " Invalid { quantity } suffix { match . group ( 2 ) } " )
2018-08-13 19:11:33 +02:00
multiplier = METRIC_SUFFIXES [ match . group ( 2 ) ]
return mantissa * multiplier
return validator
2018-05-14 11:50:56 +02:00
2019-12-07 18:28:55 +01:00
frequency = float_with_unit ( " frequency " , " (Hz|HZ|hz)? " )
resistance = float_with_unit ( " resistance " , " (Ω|Ω|ohm|Ohm|OHM)? " )
current = float_with_unit ( " current " , " (a|A|amp|Amp|amps|Amps|ampere|Ampere)? " )
voltage = float_with_unit ( " voltage " , " (v|V|volt|Volts)? " )
distance = float_with_unit ( " distance " , " (m) " )
framerate = float_with_unit ( " framerate " , " (FPS|fps|Fps|FpS|Hz) " )
angle = float_with_unit ( " angle " , " (°|deg) " , optional_unit = True )
_temperature_c = float_with_unit ( " temperature " , " (°C|° C|°|C)? " )
_temperature_k = float_with_unit ( " temperature " , " (° K|° K|K)? " )
_temperature_f = float_with_unit ( " temperature " , " (°F|° F|F)? " )
decibel = float_with_unit ( " decibel " , " (dB|dBm|db|dbm) " , optional_unit = True )
2020-11-15 19:03:08 +01:00
pressure = float_with_unit ( " pressure " , " (bar|Bar) " , optional_unit = True )
2019-04-08 18:08:58 +02:00
def temperature ( value ) :
2021-08-31 04:00:58 +02:00
err = None
2019-04-08 18:08:58 +02:00
try :
return _temperature_c ( value )
2021-08-31 04:00:58 +02:00
except Invalid as orig_err :
err = orig_err
2019-04-08 18:08:58 +02:00
try :
kelvin = _temperature_k ( value )
return kelvin - 273.15
2019-04-17 12:06:00 +02:00
except Invalid :
2019-04-08 18:08:58 +02:00
pass
try :
fahrenheit = _temperature_f ( value )
2019-04-17 12:06:00 +02:00
return ( fahrenheit - 32 ) * ( 5 / 9 )
except Invalid :
2019-04-08 18:08:58 +02:00
pass
2021-08-31 04:00:58 +02:00
raise err
2019-04-08 18:08:58 +02:00
2023-08-23 03:01:34 +02:00
def temperature_delta ( value ) :
err = None
try :
return _temperature_c ( value )
except Invalid as orig_err :
err = orig_err
try :
return _temperature_k ( value )
except Invalid :
pass
try :
fahrenheit = _temperature_f ( value )
return fahrenheit * ( 5 / 9 )
except Invalid :
pass
raise err
2021-03-07 20:03:16 +01:00
_color_temperature_mireds = float_with_unit ( " Color Temperature " , r " (mireds|Mireds) " )
_color_temperature_kelvin = float_with_unit ( " Color Temperature " , r " (K|Kelvin) " )
2019-04-08 18:08:58 +02:00
def color_temperature ( value ) :
try :
val = _color_temperature_mireds ( value )
2019-04-17 12:06:00 +02:00
except Invalid :
2019-04-08 18:08:58 +02:00
val = 1000000.0 / _color_temperature_kelvin ( value )
if val < 0 :
2019-04-17 12:06:00 +02:00
raise Invalid ( " Color temperature cannot be negative " )
2019-04-08 18:08:58 +02:00
return val
2018-04-07 01:23:03 +02:00
2018-06-11 10:01:54 +02:00
def validate_bytes ( value ) :
value = string ( value )
match = re . match ( r " ^([0-9]+) \ s*( \ w*?)(?:byte|B|b)?s?$ " , value )
if match is None :
2019-12-07 18:28:55 +01:00
raise Invalid ( f " Expected number of bytes with unit, got { value } " )
2018-06-11 10:01:54 +02:00
mantissa = int ( match . group ( 1 ) )
if match . group ( 2 ) not in METRIC_SUFFIXES :
2021-09-19 19:22:28 +02:00
raise Invalid ( f " Invalid metric suffix { match . group ( 2 ) } " )
2018-06-11 10:01:54 +02:00
multiplier = METRIC_SUFFIXES [ match . group ( 2 ) ]
if multiplier < 1 :
2021-03-07 20:03:16 +01:00
raise Invalid (
2021-09-19 19:22:28 +02:00
f " Only suffixes with positive exponents are supported. Got { match . group ( 2 ) } "
2021-03-07 20:03:16 +01:00
)
2018-06-11 10:01:54 +02:00
return int ( mantissa * multiplier )
2018-04-07 01:23:03 +02:00
def hostname ( value ) :
value = string ( value )
2021-10-22 12:09:47 +02:00
if re . match ( r " ^[a-z0-9-] { 1,63}$ " , value , re . IGNORECASE ) is not None :
return value
raise Invalid ( f " Invalid hostname: { value } " )
2018-04-07 01:23:03 +02:00
2018-12-02 13:15:28 +01:00
def domain ( value ) :
value = string ( value )
2019-01-15 20:13:20 +01:00
if re . match ( vol . DOMAIN_REGEX , value ) is not None :
return value
try :
return str ( ipv4 ( value ) )
2020-09-16 12:12:40 +02:00
except Invalid as err :
raise Invalid ( f " Invalid domain: { value } " ) from err
2018-12-02 13:15:28 +01:00
2018-10-20 15:18:12 +02:00
def domain_name ( value ) :
2019-02-10 23:55:13 +01:00
value = string_strict ( value )
if not value :
return value
2021-03-07 20:03:16 +01:00
if not value . startswith ( " . " ) :
2019-04-17 12:06:00 +02:00
raise Invalid ( " Domain name must start with . " )
2021-03-07 20:03:16 +01:00
if value . startswith ( " .. " ) :
2019-04-17 12:06:00 +02:00
raise Invalid ( " Domain name must start with single . " )
2018-05-21 15:47:30 +02:00
for c in value :
2021-03-07 20:03:16 +01:00
if not ( c . isalnum ( ) or c in " ._- " ) :
raise Invalid (
" Domain name can only have alphanumeric characters and _ or - "
)
2018-05-21 15:47:30 +02:00
return value
2018-04-07 01:23:03 +02:00
def ssid ( value ) :
2019-02-08 17:52:42 +01:00
value = string_strict ( value )
2018-04-07 01:23:03 +02:00
if not value :
2019-04-17 12:06:00 +02:00
raise Invalid ( " SSID can ' t be empty. " )
2018-12-18 19:31:43 +01:00
if len ( value ) > 32 :
2019-04-17 12:06:00 +02:00
raise Invalid ( " SSID can ' t be longer than 32 characters " )
2018-04-07 01:23:03 +02:00
return value
def ipv4 ( value ) :
if isinstance ( value , list ) :
parts = value
2019-12-07 18:28:55 +01:00
elif isinstance ( value , str ) :
2021-03-07 20:03:16 +01:00
parts = value . split ( " . " )
2018-12-18 19:31:43 +01:00
elif isinstance ( value , IPAddress ) :
return value
2018-04-07 01:23:03 +02:00
else :
2022-06-21 06:27:33 +02:00
raise Invalid ( " IPv4 address must consist of either string or integer list " )
2018-04-07 01:23:03 +02:00
if len ( parts ) != 4 :
2022-06-21 06:27:33 +02:00
raise Invalid ( " IPv4 address must consist of four point-separated integers " )
2018-04-07 01:23:03 +02:00
parts_ = list ( map ( int , parts ) )
if not all ( 0 < = x < 256 for x in parts_ ) :
2019-04-17 12:06:00 +02:00
raise Invalid ( " IPv4 address parts must be in range from 0 to 255 " )
2018-04-07 01:23:03 +02:00
return IPAddress ( * parts_ )
2018-05-06 15:56:12 +02:00
def _valid_topic ( value ) :
""" Validate that this is a valid topic name/filter. """
2023-10-26 02:37:50 +02:00
if value is None : # Used to disable publishing and subscribing
return " "
2018-05-06 15:56:12 +02:00
if isinstance ( value , dict ) :
2019-04-17 12:06:00 +02:00
raise Invalid ( " Can ' t use dictionary with topic " )
2018-05-06 15:56:12 +02:00
value = string ( value )
try :
2021-03-07 20:03:16 +01:00
raw_value = value . encode ( " utf-8 " )
2020-09-16 12:12:40 +02:00
except UnicodeError as err :
raise Invalid ( " MQTT topic name/filter must be valid UTF-8 string. " ) from err
2018-05-06 15:56:12 +02:00
if not raw_value :
2019-04-17 12:06:00 +02:00
raise Invalid ( " MQTT topic name/filter must not be empty. " )
2018-05-06 15:56:12 +02:00
if len ( raw_value ) > 65535 :
2021-03-07 20:03:16 +01:00
raise Invalid (
2022-06-21 06:27:33 +02:00
" MQTT topic name/filter must not be longer than 65535 encoded bytes. "
2021-03-07 20:03:16 +01:00
)
if " \0 " in value :
2022-06-21 06:27:33 +02:00
raise Invalid ( " MQTT topic name/filter must not contain null character. " )
2018-05-06 15:56:12 +02:00
return value
def subscribe_topic ( value ) :
""" Validate that we can subscribe using this MQTT topic. """
value = _valid_topic ( value )
2021-03-07 20:03:16 +01:00
for i in ( i for i , c in enumerate ( value ) if c == " + " ) :
if ( i > 0 and value [ i - 1 ] != " / " ) or (
i < len ( value ) - 1 and value [ i + 1 ] != " / "
) :
raise Invalid (
2022-06-21 06:27:33 +02:00
" Single-level wildcard must occupy an entire level of the filter "
2021-03-07 20:03:16 +01:00
)
index = value . find ( " # " )
2018-05-06 15:56:12 +02:00
if index != - 1 :
if index != len ( value ) - 1 :
# If there are multiple wildcards, this will also trigger
2021-03-07 20:03:16 +01:00
raise Invalid (
" Multi-level wildcard must be the last "
" character in the topic filter. "
)
if len ( value ) > 1 and value [ index - 1 ] != " / " :
2022-06-21 06:27:33 +02:00
raise Invalid ( " Multi-level wildcard must be after a topic level separator. " )
2018-05-06 15:56:12 +02:00
return value
2018-04-07 01:23:03 +02:00
def publish_topic ( value ) :
2018-05-06 15:56:12 +02:00
""" Validate that we can publish using this MQTT topic. """
value = _valid_topic ( value )
2021-03-07 20:03:16 +01:00
if " + " in value or " # " in value :
2019-04-17 12:06:00 +02:00
raise Invalid ( " Wildcards can not be used in topic names " )
2018-04-07 01:23:03 +02:00
return value
2018-05-06 15:56:12 +02:00
def mqtt_payload ( value ) :
if value is None :
2021-03-07 20:03:16 +01:00
return " "
2018-05-06 15:56:12 +02:00
return string ( value )
2018-05-20 12:41:52 +02:00
def mqtt_qos ( value ) :
try :
value = int ( value )
except ( TypeError , ValueError ) :
2020-09-16 12:12:40 +02:00
# pylint: disable=raise-missing-from
2019-12-07 18:28:55 +01:00
raise Invalid ( f " MQTT Quality of Service must be integer, got { value } " )
2018-05-20 12:41:52 +02:00
return one_of ( 0 , 1 , 2 ) ( value )
2019-01-06 09:47:47 +01:00
def requires_component ( comp ) :
2019-04-22 21:56:30 +02:00
""" Validate that this option can only be specified when the component `comp` is loaded. """
2022-11-23 23:09:19 +01:00
2019-01-06 09:47:47 +01:00
def validator ( value ) :
2021-09-20 11:47:51 +02:00
if comp not in CORE . loaded_integrations :
2019-12-07 18:28:55 +01:00
raise Invalid ( f " This option requires component { comp } " )
2019-01-06 09:47:47 +01:00
return value
2019-02-13 16:54:02 +01:00
2019-01-06 09:47:47 +01:00
return validator
2019-04-22 21:56:30 +02:00
uint8_t = int_range ( min = 0 , max = 255 )
uint16_t = int_range ( min = 0 , max = 65535 )
uint32_t = int_range ( min = 0 , max = 4294967295 )
2021-06-18 03:54:46 +02:00
uint64_t = int_range ( min = 0 , max = 18446744073709551615 )
2019-04-22 21:56:30 +02:00
hex_uint8_t = hex_int_range ( min = 0 , max = 255 )
hex_uint16_t = hex_int_range ( min = 0 , max = 65535 )
hex_uint32_t = hex_int_range ( min = 0 , max = 4294967295 )
2021-06-18 03:54:46 +02:00
hex_uint64_t = hex_int_range ( min = 0 , max = 18446744073709551615 )
2018-04-07 01:23:03 +02:00
i2c_address = hex_uint8_t
2018-05-27 14:15:24 +02:00
def percentage ( value ) :
2019-04-22 21:56:30 +02:00
""" Validate that the value is a percentage.
The resulting value is an integer in the range 0.0 to 1.0 .
"""
2019-03-13 16:40:09 +01:00
value = possibly_negative_percentage ( value )
return zero_to_one_float ( value )
def possibly_negative_percentage ( value ) :
2020-06-20 01:59:19 +02:00
has_percent_sign = False
if isinstance ( value , str ) :
try :
2021-03-07 20:03:16 +01:00
if value . endswith ( " % " ) :
2023-03-15 23:20:18 +01:00
has_percent_sign = True
2020-06-20 01:59:19 +02:00
value = float ( value [ : - 1 ] . rstrip ( ) ) / 100.0
else :
value = float ( value )
except ValueError :
2020-09-16 12:12:40 +02:00
# pylint: disable=raise-missing-from
2020-06-20 01:59:19 +02:00
raise Invalid ( " invalid number " )
2022-06-07 13:00:27 +02:00
try :
if value > 1 :
msg = " Percentage must not be higher than 100 % . "
if not has_percent_sign :
msg + = " Please put a percent sign after the number! "
raise Invalid ( msg )
if value < - 1 :
msg = " Percentage must not be smaller than -100 % . "
if not has_percent_sign :
msg + = " Please put a percent sign after the number! "
raise Invalid ( msg )
except TypeError :
raise Invalid ( # pylint: disable=raise-missing-from
" Expected percentage or float between -1.0 and 1.0 "
)
2019-03-13 16:40:09 +01:00
return negative_one_to_one_float ( value )
2018-05-27 14:15:24 +02:00
2018-06-11 10:01:54 +02:00
def percentage_int ( value ) :
2021-03-07 20:03:16 +01:00
if isinstance ( value , str ) and value . endswith ( " % " ) :
2018-06-11 10:01:54 +02:00
value = int ( value [ : - 1 ] . rstrip ( ) )
return value
2018-05-14 11:50:56 +02:00
def invalid ( message ) :
2019-04-22 21:56:30 +02:00
""" Mark this value as invalid. Each time *any* value is passed here it will result in a
validation error with the given message .
"""
2021-03-07 20:03:16 +01:00
2018-05-14 11:50:56 +02:00
def validator ( value ) :
2019-04-17 12:06:00 +02:00
raise Invalid ( message )
2018-06-02 22:22:20 +02:00
2018-05-14 11:50:56 +02:00
return validator
2018-04-07 01:23:03 +02:00
def valid ( value ) :
2021-06-17 21:54:14 +02:00
""" A validator that is always valid and returns the value as-is. """
2018-04-07 01:23:03 +02:00
return value
2019-04-22 21:56:30 +02:00
@contextmanager
def prepend_path ( path ) :
""" A contextmanager helper to prepend a path to all voluptuous errors. """
if not isinstance ( path , ( list , tuple ) ) :
path = [ path ]
try :
yield
except vol . Invalid as e :
e . prepend ( path )
raise e
@contextmanager
def remove_prepend_path ( path ) :
""" A contextmanager helper to remove a path from a voluptuous error. """
if not isinstance ( path , ( list , tuple ) ) :
path = [ path ]
try :
yield
except vol . Invalid as e :
if list_starts_with ( e . path , path ) :
# Can't set e.path (namedtuple
for _ in range ( len ( path ) ) :
e . path . pop ( 0 )
raise e
2018-11-23 13:51:22 +01:00
def one_of ( * values , * * kwargs ) :
2019-04-22 21:56:30 +02:00
""" Validate that the config option is one of the given values.
: param values : The valid values for this type
: Keyword Arguments :
- * lower * ( ` ` bool ` ` , default = False ) : Whether to convert the incoming values to lowercase
strings .
- * upper * ( ` ` bool ` ` , default = False ) : Whether to convert the incoming values to uppercase
strings .
- * int * ( ` ` bool ` ` , default = False ) : Whether to convert the incoming values to integers .
- * float * ( ` ` bool ` ` , default = False ) : Whether to convert the incoming values to floats .
- * space * ( ` ` str ` ` , default = ' ' ) : What to convert spaces in the input string to .
"""
2021-03-07 20:03:16 +01:00
options = " , " . join ( f " ' { x } ' " for x in values )
lower = kwargs . pop ( " lower " , False )
upper = kwargs . pop ( " upper " , False )
string_ = kwargs . pop ( " string " , False ) or lower or upper
to_int = kwargs . pop ( " int " , False )
to_float = kwargs . pop ( " float " , False )
space = kwargs . pop ( " space " , " " )
2019-04-22 21:56:30 +02:00
if kwargs :
raise ValueError
2018-05-20 12:41:52 +02:00
2022-06-17 03:46:20 +02:00
@schema_extractor ( " one_of " )
2018-05-20 12:41:52 +02:00
def validator ( value ) :
2022-06-17 03:46:20 +02:00
if value == SCHEMA_EXTRACT :
2021-05-15 01:35:39 +02:00
return values
2018-11-23 13:51:22 +01:00
if string_ :
value = string ( value )
2021-03-07 20:03:16 +01:00
value = value . replace ( " " , space )
2018-11-23 13:51:22 +01:00
if to_int :
value = int_ ( value )
2019-04-17 12:06:00 +02:00
if to_float :
value = float_ ( value )
2018-11-23 13:51:22 +01:00
if lower :
2019-04-17 12:06:00 +02:00
value = Lower ( value )
2018-11-23 13:51:22 +01:00
if upper :
2019-04-17 12:06:00 +02:00
value = Upper ( value )
2018-05-20 12:41:52 +02:00
if value not in values :
2019-04-22 21:56:30 +02:00
import difflib
2021-03-07 20:03:16 +01:00
2019-12-07 18:28:55 +01:00
options_ = [ str ( x ) for x in values ]
option = str ( value )
2019-04-22 21:56:30 +02:00
matches = difflib . get_close_matches ( option , options_ )
if matches :
2021-09-19 19:22:28 +02:00
matches_str = " , " . join ( f " ' { x } ' " for x in matches )
raise Invalid ( f " Unknown value ' { value } ' , did you mean { matches_str } ? " )
2019-12-07 18:28:55 +01:00
raise Invalid ( f " Unknown value ' { value } ' , valid options are { options } . " )
2019-04-22 21:56:30 +02:00
return value
return validator
def enum ( mapping , * * kwargs ) :
""" Validate this config option against an enum mapping.
The mapping should be a dictionary with the key representing the config value name and
a value representing the expression to set during code generation .
Accepts all kwargs of one_of .
"""
assert isinstance ( mapping , dict )
one_of_validator = one_of ( * mapping , * * kwargs )
2022-06-17 03:46:20 +02:00
@schema_extractor ( " enum " )
2019-04-22 21:56:30 +02:00
def validator ( value ) :
2022-06-17 03:46:20 +02:00
if value == SCHEMA_EXTRACT :
2021-05-15 01:35:39 +02:00
return mapping
2019-12-04 15:58:40 +01:00
value = one_of_validator ( value )
value = add_class_to_obj ( value , core . EnumValue )
2019-04-22 21:56:30 +02:00
value . enum_value = mapping [ value ]
2018-05-20 12:41:52 +02:00
return value
2018-06-02 22:22:20 +02:00
2018-05-20 12:41:52 +02:00
return validator
2023-01-10 01:54:15 +01:00
LAMBDA_ENTITY_ID_PROG = re . compile ( r " \ Wid \ ( \ s*([a-zA-Z0-9_]+ \ .[.a-zA-Z0-9_]+) \ s* \ ) " )
2019-04-24 23:49:02 +02:00
2018-05-20 12:41:52 +02:00
def lambda_ ( value ) :
2019-04-22 21:56:30 +02:00
""" Coerce this configuration option to a lambda. """
2019-04-24 23:49:02 +02:00
if not isinstance ( value , Lambda ) :
2021-02-06 16:09:15 +01:00
value = make_data_base ( Lambda ( string_strict ( value ) ) , value )
2019-04-24 23:49:02 +02:00
entity_id_parts = re . split ( LAMBDA_ENTITY_ID_PROG , value . value )
if len ( entity_id_parts ) != 1 :
2021-03-07 20:03:16 +01:00
entity_ids = " " . join (
2021-09-19 19:22:28 +02:00
f " ' { entity_id_parts [ i ] } ' " for i in range ( 1 , len ( entity_id_parts ) , 2 )
2021-03-07 20:03:16 +01:00
)
raise Invalid (
2021-09-19 19:22:28 +02:00
f " Lambda contains reference to entity-id-style ID { entity_ids } . The id() wrapper only works for ESPHome-internal types. For importing states from Home Assistant use the ' homeassistant ' sensor platforms. "
2021-03-07 20:03:16 +01:00
)
2019-04-24 23:49:02 +02:00
return value
2018-05-20 12:41:52 +02:00
2019-05-10 22:13:17 +02:00
def returning_lambda ( value ) :
""" Coerce this configuration option to a lambda.
Additionally , make sure the lambda returns something .
"""
value = lambda_ ( value )
2021-03-07 20:03:16 +01:00
if " return " not in value . value :
raise Invalid (
" Lambda doesn ' t contain a ' return ' statement, but the lambda "
" is expected to return a value. \n "
" Please make sure the lambda contains at least one "
" return statement. "
)
2019-05-10 22:13:17 +02:00
return value
2018-08-18 21:40:59 +02:00
def dimensions ( value ) :
if isinstance ( value , list ) :
if len ( value ) != 2 :
2021-09-19 19:22:28 +02:00
raise Invalid ( f " Dimensions must have a length of two, not { len ( value ) } " )
2018-08-18 21:40:59 +02:00
try :
width , height = int ( value [ 0 ] ) , int ( value [ 1 ] )
except ValueError :
2020-09-16 12:12:40 +02:00
# pylint: disable=raise-missing-from
2019-12-07 18:28:55 +01:00
raise Invalid ( " Width and height dimensions must be integers " )
2018-08-18 21:40:59 +02:00
if width < = 0 or height < = 0 :
2019-12-07 18:28:55 +01:00
raise Invalid ( " Width and height must at least be 1 " )
2018-08-18 21:40:59 +02:00
return [ width , height ]
value = string ( value )
match = re . match ( r " \ s*([0-9]+) \ s*[xX] \ s*([0-9]+) \ s* " , value )
if not match :
2021-03-07 20:03:16 +01:00
raise Invalid (
" Invalid value ' {} ' for dimensions. Only WIDTHxHEIGHT is allowed. "
)
2018-08-18 21:40:59 +02:00
return dimensions ( [ match . group ( 1 ) , match . group ( 2 ) ] )
2018-09-23 18:58:41 +02:00
def directory ( value ) :
2019-10-18 09:17:16 +02:00
import json
2021-03-07 20:03:16 +01:00
2018-09-23 18:58:41 +02:00
value = string ( value )
2019-04-22 21:56:30 +02:00
path = CORE . relative_config_path ( value )
2019-10-18 09:17:16 +02:00
2021-03-07 20:03:16 +01:00
if CORE . vscode and (
not CORE . ace or os . path . abspath ( path ) == os . path . abspath ( CORE . config_path )
) :
print (
json . dumps (
{
" type " : " check_directory_exists " ,
" path " : path ,
}
)
)
2019-12-07 18:28:55 +01:00
data = json . loads ( input ( ) )
2021-03-07 20:03:16 +01:00
assert data [ " type " ] == " directory_exists_response "
if data [ " content " ] :
2019-10-18 09:17:16 +02:00
return value
2021-03-07 20:03:16 +01:00
raise Invalid (
2021-09-19 19:22:28 +02:00
f " Could not find directory ' { path } ' . Please make sure it exists (full path: { os . path . abspath ( path ) } ). "
2021-03-07 20:03:16 +01:00
)
2019-10-18 09:17:16 +02:00
2018-09-23 18:58:41 +02:00
if not os . path . exists ( path ) :
2021-03-07 20:03:16 +01:00
raise Invalid (
2021-09-19 19:22:28 +02:00
f " Could not find directory ' { path } ' . Please make sure it exists (full path: { os . path . abspath ( path ) } ). "
2021-03-07 20:03:16 +01:00
)
2018-09-23 18:58:41 +02:00
if not os . path . isdir ( path ) :
2021-03-07 20:03:16 +01:00
raise Invalid (
2021-09-19 19:22:28 +02:00
f " Path ' { path } ' is not a directory (full path: { os . path . abspath ( path ) } ). "
2021-03-07 20:03:16 +01:00
)
2018-09-23 18:58:41 +02:00
return value
def file_ ( value ) :
2019-10-18 09:17:16 +02:00
import json
2021-03-07 20:03:16 +01:00
2018-09-23 18:58:41 +02:00
value = string ( value )
2019-04-22 21:56:30 +02:00
path = CORE . relative_config_path ( value )
2019-10-18 09:17:16 +02:00
2021-03-07 20:03:16 +01:00
if CORE . vscode and (
not CORE . ace or os . path . abspath ( path ) == os . path . abspath ( CORE . config_path )
) :
print (
json . dumps (
{
" type " : " check_file_exists " ,
" path " : path ,
}
)
)
2019-12-07 18:28:55 +01:00
data = json . loads ( input ( ) )
2021-03-07 20:03:16 +01:00
assert data [ " type " ] == " file_exists_response "
if data [ " content " ] :
2019-10-18 09:17:16 +02:00
return value
2021-03-07 20:03:16 +01:00
raise Invalid (
2021-09-19 19:22:28 +02:00
f " Could not find file ' { path } ' . Please make sure it exists (full path: { os . path . abspath ( path ) } ). "
2021-03-07 20:03:16 +01:00
)
2019-10-18 09:17:16 +02:00
2018-09-23 18:58:41 +02:00
if not os . path . exists ( path ) :
2021-03-07 20:03:16 +01:00
raise Invalid (
2021-09-19 19:22:28 +02:00
f " Could not find file ' { path } ' . Please make sure it exists (full path: { os . path . abspath ( path ) } ). "
2021-03-07 20:03:16 +01:00
)
2018-09-23 18:58:41 +02:00
if not os . path . isfile ( path ) :
2021-03-07 20:03:16 +01:00
raise Invalid (
2021-09-19 19:22:28 +02:00
f " Path ' { path } ' is not a file (full path: { os . path . abspath ( path ) } ). "
2021-03-07 20:03:16 +01:00
)
2018-09-23 18:58:41 +02:00
return value
2021-03-07 20:03:16 +01:00
ENTITY_ID_CHARACTERS = " abcdefghijklmnopqrstuvwxyz0123456789_ "
2018-12-18 19:31:43 +01:00
def entity_id ( value ) :
2019-04-22 21:56:30 +02:00
""" Validate that this option represents a valid Home Assistant entity id.
Should only be used for ' homeassistant ' platforms .
"""
2018-12-18 19:31:43 +01:00
value = string_strict ( value ) . lower ( )
2021-03-07 20:03:16 +01:00
if value . count ( " . " ) != 1 :
2019-04-17 12:06:00 +02:00
raise Invalid ( " Entity ID must have exactly one dot in it " )
2021-03-07 20:03:16 +01:00
for x in value . split ( " . " ) :
2019-01-06 19:03:34 +01:00
for c in x :
if c not in ENTITY_ID_CHARACTERS :
2019-12-07 18:28:55 +01:00
raise Invalid ( f " Invalid character for entity ID: { c } " )
2018-12-18 19:31:43 +01:00
return value
2019-04-17 12:06:00 +02:00
def extract_keys ( schema ) :
2019-04-22 21:56:30 +02:00
""" Extract the names of the keys from the given schema. """
2019-04-17 12:06:00 +02:00
if isinstance ( schema , Schema ) :
schema = schema . schema
assert isinstance ( schema , dict )
2019-04-22 21:56:30 +02:00
keys = [ ]
for skey in list ( schema . keys ( ) ) :
2019-12-07 18:28:55 +01:00
if isinstance ( skey , str ) :
2019-04-22 21:56:30 +02:00
keys . append ( skey )
2019-12-07 18:28:55 +01:00
elif isinstance ( skey , vol . Marker ) and isinstance ( skey . schema , str ) :
2019-04-22 21:56:30 +02:00
keys . append ( skey . schema )
else :
raise ValueError ( )
2019-04-17 12:06:00 +02:00
keys . sort ( )
return keys
2022-06-17 03:46:20 +02:00
@schema_extractor_typed
2019-05-15 10:55:35 +02:00
def typed_schema ( schemas , * * kwargs ) :
""" Create a schema that has a key to distinguish between schemas """
2021-03-07 20:03:16 +01:00
key = kwargs . pop ( " key " , CONF_TYPE )
2021-05-23 22:24:24 +02:00
default_schema_option = kwargs . pop ( " default_type " , None )
2024-04-17 01:04:10 +02:00
enum_mapping = kwargs . pop ( " enum " , None )
if enum_mapping is not None :
assert isinstance ( enum_mapping , dict )
assert set ( enum_mapping . keys ( ) ) == set ( schemas . keys ( ) )
2019-05-15 10:55:35 +02:00
key_validator = one_of ( * schemas , * * kwargs )
def validator ( value ) :
if not isinstance ( value , dict ) :
raise Invalid ( " Value must be dict " )
value = value . copy ( )
2021-05-23 22:24:24 +02:00
schema_option = value . pop ( key , default_schema_option )
if schema_option is None :
2021-09-19 19:22:28 +02:00
raise Invalid ( f " { key } not specified! " )
2021-05-23 22:24:24 +02:00
key_v = key_validator ( schema_option )
2024-04-17 01:04:10 +02:00
if enum_mapping is not None :
key_v = add_class_to_obj ( key_v , core . EnumValue )
key_v . enum_value = enum_mapping [ key_v ]
2021-11-10 19:35:31 +01:00
value = Schema ( schemas [ key_v ] ) ( value )
2019-05-15 10:55:35 +02:00
value [ key ] = key_v
2019-05-27 09:58:55 +02:00
return value
2019-05-15 10:55:35 +02:00
return validator
2019-04-17 12:06:00 +02:00
class GenerateID ( Optional ) :
2019-04-22 21:56:30 +02:00
""" Mark this key as being an auto-generated ID key. """
2019-10-18 09:17:16 +02:00
2018-06-02 22:22:20 +02:00
def __init__ ( self , key = CONF_ID ) :
2019-12-07 18:28:55 +01:00
super ( ) . __init__ ( key , default = lambda : None )
2018-04-07 01:23:03 +02:00
2023-12-14 09:33:04 +01:00
def _get_priority_default ( * args ) :
for arg in args :
if arg is not vol . UNDEFINED :
return arg
return vol . UNDEFINED
2019-04-17 12:06:00 +02:00
class SplitDefault ( Optional ) :
2019-04-22 21:56:30 +02:00
""" Mark this key to have a split default for ESP8266/ESP32. """
2019-10-18 09:17:16 +02:00
2021-09-20 11:47:51 +02:00
def __init__ (
self ,
key ,
esp8266 = vol . UNDEFINED ,
esp32 = vol . UNDEFINED ,
esp32_arduino = vol . UNDEFINED ,
esp32_idf = vol . UNDEFINED ,
2023-12-14 05:47:31 +01:00
esp32_s2 = vol . UNDEFINED ,
esp32_s2_arduino = vol . UNDEFINED ,
esp32_s2_idf = vol . UNDEFINED ,
esp32_s3 = vol . UNDEFINED ,
esp32_s3_arduino = vol . UNDEFINED ,
esp32_s3_idf = vol . UNDEFINED ,
2023-12-15 01:24:52 +01:00
esp32_c3 = vol . UNDEFINED ,
esp32_c3_arduino = vol . UNDEFINED ,
esp32_c3_idf = vol . UNDEFINED ,
2022-10-20 05:50:39 +02:00
rp2040 = vol . UNDEFINED ,
2023-09-05 00:16:08 +02:00
bk72xx = vol . UNDEFINED ,
rtl87xx = vol . UNDEFINED ,
2023-05-10 01:38:18 +02:00
host = vol . UNDEFINED ,
2021-09-20 11:47:51 +02:00
) :
2019-12-07 18:28:55 +01:00
super ( ) . __init__ ( key )
2019-04-17 12:06:00 +02:00
self . _esp8266_default = vol . default_factory ( esp8266 )
2021-09-20 11:47:51 +02:00
self . _esp32_arduino_default = vol . default_factory (
2023-12-15 01:24:52 +01:00
_get_priority_default ( esp32_arduino , esp32 )
2021-09-20 11:47:51 +02:00
)
self . _esp32_idf_default = vol . default_factory (
2023-12-15 01:24:52 +01:00
_get_priority_default ( esp32_idf , esp32 )
2021-09-20 11:47:51 +02:00
)
2023-12-14 05:47:31 +01:00
self . _esp32_s2_arduino_default = vol . default_factory (
2023-12-15 01:24:52 +01:00
_get_priority_default ( esp32_s2_arduino , esp32_s2 , esp32_arduino , esp32 )
2023-12-14 05:47:31 +01:00
)
self . _esp32_s2_idf_default = vol . default_factory (
2023-12-15 01:24:52 +01:00
_get_priority_default ( esp32_s2_idf , esp32_s2 , esp32_idf , esp32 )
2023-12-14 05:47:31 +01:00
)
self . _esp32_s3_arduino_default = vol . default_factory (
2023-12-15 01:24:52 +01:00
_get_priority_default ( esp32_s3_arduino , esp32_s3 , esp32_arduino , esp32 )
2023-12-14 05:47:31 +01:00
)
self . _esp32_s3_idf_default = vol . default_factory (
2023-12-15 01:24:52 +01:00
_get_priority_default ( esp32_s3_idf , esp32_s3 , esp32_idf , esp32 )
)
self . _esp32_c3_arduino_default = vol . default_factory (
_get_priority_default ( esp32_c3_arduino , esp32_c3 , esp32_arduino , esp32 )
)
self . _esp32_c3_idf_default = vol . default_factory (
_get_priority_default ( esp32_c3_idf , esp32_c3 , esp32_idf , esp32 )
2023-12-14 05:47:31 +01:00
)
2022-10-20 05:50:39 +02:00
self . _rp2040_default = vol . default_factory ( rp2040 )
2023-09-05 00:16:08 +02:00
self . _bk72xx_default = vol . default_factory ( bk72xx )
self . _rtl87xx_default = vol . default_factory ( rtl87xx )
2023-05-10 01:38:18 +02:00
self . _host_default = vol . default_factory ( host )
2019-04-17 12:06:00 +02:00
@property
def default ( self ) :
if CORE . is_esp8266 :
return self . _esp8266_default
2023-12-14 05:47:31 +01:00
if CORE . is_esp32 :
from esphome . components . esp32 import get_esp32_variant
from esphome . components . esp32 . const import (
VARIANT_ESP32S2 ,
VARIANT_ESP32S3 ,
2023-12-15 01:24:52 +01:00
VARIANT_ESP32C3 ,
2023-12-14 05:47:31 +01:00
)
variant = get_esp32_variant ( )
if variant == VARIANT_ESP32S2 :
if CORE . using_arduino :
return self . _esp32_s2_arduino_default
if CORE . using_esp_idf :
return self . _esp32_s2_idf_default
elif variant == VARIANT_ESP32S3 :
if CORE . using_arduino :
return self . _esp32_s3_arduino_default
if CORE . using_esp_idf :
return self . _esp32_s3_idf_default
2023-12-15 01:24:52 +01:00
elif variant == VARIANT_ESP32C3 :
if CORE . using_arduino :
return self . _esp32_c3_arduino_default
if CORE . using_esp_idf :
return self . _esp32_c3_idf_default
2023-12-14 05:47:31 +01:00
else :
if CORE . using_arduino :
return self . _esp32_arduino_default
if CORE . using_esp_idf :
return self . _esp32_idf_default
2022-10-20 05:50:39 +02:00
if CORE . is_rp2040 :
return self . _rp2040_default
2023-09-05 00:16:08 +02:00
if CORE . is_bk72xx :
return self . _bk72xx_default
if CORE . is_rtl87xx :
return self . _rtl87xx_default
2023-05-10 01:38:18 +02:00
if CORE . is_host :
return self . _host_default
2021-09-20 11:47:51 +02:00
raise NotImplementedError
2019-04-17 12:06:00 +02:00
@default.setter
def default ( self , value ) :
# Ignore default set from vol.Optional
pass
class OnlyWith ( Optional ) :
2019-04-22 21:56:30 +02:00
""" Set the default value only if the given component is loaded. """
2019-10-18 09:17:16 +02:00
2019-04-17 12:06:00 +02:00
def __init__ ( self , key , component , default = None ) :
2019-12-07 18:28:55 +01:00
super ( ) . __init__ ( key )
2019-04-17 12:06:00 +02:00
self . _component = component
self . _default = vol . default_factory ( default )
@property
def default ( self ) :
2021-09-20 11:47:51 +02:00
if self . _component in CORE . loaded_integrations :
2020-10-15 15:14:07 +02:00
return self . _default
return vol . UNDEFINED
2019-04-17 12:06:00 +02:00
@default.setter
def default ( self , value ) :
# Ignore default set from vol.Optional
pass
2021-10-10 10:37:05 +02:00
def _entity_base_validator ( config ) :
2019-04-22 21:56:30 +02:00
if CONF_NAME not in config and CONF_ID not in config :
raise Invalid ( " At least one of ' id: ' or ' name: ' is required! " )
if CONF_NAME not in config :
id = config [ CONF_ID ]
if not id . is_manual :
2019-04-17 12:06:00 +02:00
raise Invalid ( " At least one of ' id: ' or ' name: ' is required! " )
2019-04-22 21:56:30 +02:00
config [ CONF_NAME ] = id . id
config [ CONF_INTERNAL ] = True
2018-06-11 10:01:54 +02:00
return config
2023-03-27 00:49:09 +02:00
if config [ CONF_NAME ] is None :
config [ CONF_NAME ] = " "
2019-04-22 21:56:30 +02:00
return config
def ensure_schema ( schema ) :
if not isinstance ( schema , vol . Schema ) :
return Schema ( schema )
return schema
2018-06-11 10:01:54 +02:00
2019-04-22 21:56:30 +02:00
def validate_registry_entry ( name , registry ) :
2021-03-07 20:03:16 +01:00
base_schema = ensure_schema ( registry . base_schema ) . extend (
{
Optional ( CONF_TYPE_ID ) : valid ,
} ,
extra = ALLOW_EXTRA ,
)
2019-04-22 21:56:30 +02:00
ignore_keys = extract_keys ( base_schema )
2018-06-11 10:01:54 +02:00
2022-06-17 03:46:20 +02:00
@schema_extractor_registry ( registry )
2019-04-17 12:06:00 +02:00
def validator ( value ) :
2019-12-07 18:28:55 +01:00
if isinstance ( value , str ) :
2019-04-17 12:06:00 +02:00
value = { value : { } }
if not isinstance ( value , dict ) :
2021-03-07 20:03:16 +01:00
raise Invalid (
2021-09-19 19:22:28 +02:00
f " { name . title ( ) } must consist of key-value mapping! Got { value } "
2021-03-07 20:03:16 +01:00
)
2019-04-22 21:56:30 +02:00
key = next ( ( x for x in value if x not in ignore_keys ) , None )
2019-04-17 12:06:00 +02:00
if key is None :
2019-12-07 18:28:55 +01:00
raise Invalid ( f " Key missing from { name } ! Got { value } " )
2019-04-17 12:06:00 +02:00
if key not in registry :
2019-12-07 18:28:55 +01:00
raise Invalid ( f " Unable to find { name } with the name ' { key } ' " , [ key ] )
2019-04-22 21:56:30 +02:00
key2 = next ( ( x for x in value if x != key and x not in ignore_keys ) , None )
2019-04-17 12:06:00 +02:00
if key2 is not None :
2021-03-07 20:03:16 +01:00
raise Invalid (
2021-09-19 19:22:28 +02:00
f " Cannot have two { name } s in one item. Key ' { key } ' overrides ' { key2 } ' ! "
f " Did you forget to indent the block inside the { key } ? "
2021-03-07 20:03:16 +01:00
)
2019-04-22 21:56:30 +02:00
if value [ key ] is None :
value [ key ] = { }
registry_entry = registry [ key ]
2019-06-30 12:18:41 +02:00
value = value . copy ( )
2019-04-22 21:56:30 +02:00
with prepend_path ( [ key ] ) :
value [ key ] = registry_entry . schema ( value [ key ] )
if registry_entry . type_id is not None :
2021-03-07 20:03:16 +01:00
my_base_schema = base_schema . extend (
{ GenerateID ( CONF_TYPE_ID ) : declare_id ( registry_entry . type_id ) }
)
2019-04-22 21:56:30 +02:00
value = my_base_schema ( value )
return value
2019-04-17 12:06:00 +02:00
return validator
2023-03-27 00:49:09 +02:00
def none ( value ) :
if value in ( " none " , " None " ) :
return None
if boolean ( value ) is False :
return None
raise Invalid ( " Must be none " )
def requires_friendly_name ( message ) :
def validate ( value ) :
if CORE . friendly_name is None :
raise Invalid ( message )
return value
return validate
2019-04-22 21:56:30 +02:00
def validate_registry ( name , registry ) :
return ensure_list ( validate_registry_entry ( name , registry ) )
2019-04-17 12:06:00 +02:00
2019-05-29 19:32:18 +02:00
def maybe_simple_value ( * validators , * * kwargs ) :
2021-03-07 20:03:16 +01:00
key = kwargs . pop ( " key " , CONF_VALUE )
2019-04-17 12:06:00 +02:00
validator = All ( * validators )
2022-06-17 03:46:20 +02:00
@schema_extractor ( " maybe " )
2019-04-17 12:06:00 +02:00
def validate ( value ) :
2022-06-17 03:46:20 +02:00
if value == SCHEMA_EXTRACT :
return ( validator , key )
2019-05-29 19:32:18 +02:00
if isinstance ( value , dict ) and key in value :
2019-04-17 12:06:00 +02:00
return validator ( value )
2019-05-29 19:32:18 +02:00
return validator ( { key : value } )
2019-04-17 12:06:00 +02:00
return validate
2018-04-07 01:23:03 +02:00
2023-10-10 09:00:05 +02:00
ENTITY_CATEGORIES = {
2021-11-07 19:24:52 +01:00
ENTITY_CATEGORY_NONE : cg . EntityCategory . ENTITY_CATEGORY_NONE ,
ENTITY_CATEGORY_CONFIG : cg . EntityCategory . ENTITY_CATEGORY_CONFIG ,
ENTITY_CATEGORY_DIAGNOSTIC : cg . EntityCategory . ENTITY_CATEGORY_DIAGNOSTIC ,
}
def entity_category ( value ) :
2023-10-10 09:00:05 +02:00
return enum ( ENTITY_CATEGORIES , lower = True ) ( value )
2021-11-07 19:24:52 +01:00
2021-03-07 20:03:16 +01:00
MQTT_COMPONENT_AVAILABILITY_SCHEMA = Schema (
{
Required ( CONF_TOPIC ) : subscribe_topic ,
Optional ( CONF_PAYLOAD_AVAILABLE , default = " online " ) : mqtt_payload ,
Optional ( CONF_PAYLOAD_NOT_AVAILABLE , default = " offline " ) : mqtt_payload ,
}
)
MQTT_COMPONENT_SCHEMA = Schema (
{
2024-03-11 01:12:52 +01:00
Optional ( CONF_QOS ) : All ( requires_component ( " mqtt " ) , int_range ( min = 0 , max = 2 ) ) ,
2021-03-07 20:03:16 +01:00
Optional ( CONF_RETAIN ) : All ( requires_component ( " mqtt " ) , boolean ) ,
Optional ( CONF_DISCOVERY ) : All ( requires_component ( " mqtt " ) , boolean ) ,
Optional ( CONF_STATE_TOPIC ) : All ( requires_component ( " mqtt " ) , publish_topic ) ,
Optional ( CONF_AVAILABILITY ) : All (
requires_component ( " mqtt " ) , Any ( None , MQTT_COMPONENT_AVAILABILITY_SCHEMA )
) ,
}
)
2018-04-07 01:23:03 +02:00
2021-03-07 20:03:16 +01:00
MQTT_COMMAND_COMPONENT_SCHEMA = MQTT_COMPONENT_SCHEMA . extend (
{
Optional ( CONF_COMMAND_TOPIC ) : All ( requires_component ( " mqtt " ) , subscribe_topic ) ,
2022-01-23 09:05:37 +01:00
Optional ( CONF_COMMAND_RETAIN ) : All ( requires_component ( " mqtt " ) , boolean ) ,
2021-03-07 20:03:16 +01:00
}
)
2018-11-12 23:30:31 +01:00
2021-10-10 10:37:05 +02:00
ENTITY_BASE_SCHEMA = Schema (
2021-08-10 03:45:31 +02:00
{
2023-03-27 00:49:09 +02:00
Optional ( CONF_NAME ) : Any (
All (
none ,
requires_friendly_name (
" Name cannot be None when esphome->friendly_name is not set! "
) ,
) ,
string ,
) ,
2021-08-10 03:45:31 +02:00
Optional ( CONF_INTERNAL ) : boolean ,
Optional ( CONF_DISABLED_BY_DEFAULT , default = False ) : boolean ,
2021-10-10 10:37:05 +02:00
Optional ( CONF_ICON ) : icon ,
2021-11-07 19:24:52 +01:00
Optional ( CONF_ENTITY_CATEGORY ) : entity_category ,
2021-08-10 03:45:31 +02:00
}
)
2021-10-10 10:37:05 +02:00
ENTITY_BASE_SCHEMA . add_extra ( _entity_base_validator )
2021-08-10 03:45:31 +02:00
2021-03-07 20:03:16 +01:00
COMPONENT_SCHEMA = Schema ( { Optional ( CONF_SETUP_PRIORITY ) : float_ } )
2019-04-22 21:56:30 +02:00
def polling_component_schema ( default_update_interval ) :
""" Validate that this component represents a PollingComponent with a configurable
update_interval .
: param default_update_interval : The default update interval to set for the integration .
"""
if default_update_interval is None :
2021-03-07 20:03:16 +01:00
return COMPONENT_SCHEMA . extend (
{
Required ( CONF_UPDATE_INTERVAL ) : default_update_interval ,
}
)
2019-12-07 18:28:55 +01:00
assert isinstance ( default_update_interval , str )
2021-03-07 20:03:16 +01:00
return COMPONENT_SCHEMA . extend (
{
Optional (
CONF_UPDATE_INTERVAL , default = default_update_interval
) : update_interval ,
}
)
2021-05-07 20:02:17 +02:00
def url ( value ) :
import urllib . parse
value = string_strict ( value )
try :
parsed = urllib . parse . urlparse ( value )
except ValueError as e :
raise Invalid ( " Not a valid URL " ) from e
2024-06-05 21:27:06 +02:00
if parsed . scheme and parsed . netloc or parsed . scheme == " file " :
return parsed . geturl ( )
raise Invalid ( " Expected a file scheme or a URL scheme with host " )
2021-09-05 22:23:06 +02:00
def git_ref ( value ) :
2024-06-05 21:27:06 +02:00
if re . match ( r " [a-zA-Z0-9_./-]+ " , value ) is None :
2021-09-05 22:23:06 +02:00
raise Invalid ( " Not a valid git ref " )
return value
def source_refresh ( value : str ) :
if value . lower ( ) == " always " :
return source_refresh ( " 0s " )
if value . lower ( ) == " never " :
2022-11-24 01:31:37 +01:00
return source_refresh ( " 365250d " )
2021-09-05 22:23:06 +02:00
return positive_time_period_seconds ( value )
2021-09-20 11:47:51 +02:00
@dataclass ( frozen = True , order = True )
class Version :
major : int
minor : int
patch : int
def __str__ ( self ) :
return f " { self . major } . { self . minor } . { self . patch } "
@classmethod
def parse ( cls , value : str ) - > " Version " :
2022-10-05 05:30:56 +02:00
match = re . match ( r " ^( \ d+).( \ d+).( \ d+)-? \ w*$ " , value )
2021-09-20 11:47:51 +02:00
if match is None :
raise ValueError ( f " Not a valid version number { value } " )
major = int ( match [ 1 ] )
minor = int ( match [ 2 ] )
patch = int ( match [ 3 ] )
return Version ( major = major , minor = minor , patch = patch )
def version_number ( value ) :
value = string_strict ( value )
try :
return str ( Version . parse ( value ) )
except ValueError as e :
2022-10-05 05:30:56 +02:00
raise Invalid ( " Not a valid version number " ) from e
2021-09-20 11:47:51 +02:00
2024-02-18 23:52:37 +01:00
def validate_esphome_version ( value : str ) :
min_version = Version . parse ( value )
current_version = Version . parse ( ESPHOME_VERSION )
if current_version < min_version :
raise Invalid (
f " Your ESPHome version is too old. Please update to at least { min_version } "
)
return value
2021-10-26 10:55:09 +02:00
def platformio_version_constraint ( value ) :
# for documentation on valid version constraints:
# https://docs.platformio.org/en/latest/core/userguide/platforms/cmd_install.html#cmd-platform-install
value = string_strict ( value )
constraints = [ ]
for item in value . split ( " , " ) :
# find and strip prefix operator
op = None
for test_op in ( " ^ " , " ~ " , " >= " , " > " , " <= " , " < " , " != " ) :
if item . startswith ( test_op ) :
op = test_op
item = item [ len ( test_op ) : ]
break
constraints . append ( ( op , version_number ( item ) ) )
return constraints
2021-09-20 11:47:51 +02:00
def require_framework_version (
* ,
esp_idf = None ,
esp32_arduino = None ,
esp8266_arduino = None ,
2022-10-25 02:47:48 +02:00
rp2040_arduino = None ,
2022-02-14 23:57:47 +01:00
max_version = False ,
extra_message = None ,
2021-09-20 11:47:51 +02:00
) :
def validator ( value ) :
core_data = CORE . data [ KEY_CORE ]
framework = core_data [ KEY_TARGET_FRAMEWORK ]
if framework == " esp-idf " :
if esp_idf is None :
2022-02-14 23:57:47 +01:00
msg = " This feature is incompatible with esp-idf "
if extra_message :
msg + = f " . { extra_message } "
raise Invalid ( msg )
2021-09-20 11:47:51 +02:00
required = esp_idf
elif CORE . is_esp32 and framework == " arduino " :
if esp32_arduino is None :
2022-02-14 23:57:47 +01:00
msg = " This feature is incompatible with ESP32 using arduino framework "
if extra_message :
msg + = f " . { extra_message } "
raise Invalid ( msg )
2021-09-20 11:47:51 +02:00
required = esp32_arduino
elif CORE . is_esp8266 and framework == " arduino " :
if esp8266_arduino is None :
2022-02-14 23:57:47 +01:00
msg = " This feature is incompatible with ESP8266 "
if extra_message :
msg + = f " . { extra_message } "
raise Invalid ( msg )
2021-09-20 11:47:51 +02:00
required = esp8266_arduino
2022-10-25 02:47:48 +02:00
elif CORE . is_rp2040 and framework == " arduino " :
if rp2040_arduino is None :
msg = " This feature is incompatible with RP2040 "
if extra_message :
msg + = f " . { extra_message } "
raise Invalid ( msg )
required = rp2040_arduino
2021-09-20 11:47:51 +02:00
else :
2022-10-25 02:47:48 +02:00
raise Invalid (
f """
Internal Error : require_framework_version does not support this platform configuration
platform : { core_data [ KEY_TARGET_PLATFORM ] }
framework : { framework }
Please report this issue on GitHub - > https : / / github . com / esphome / issues / issues / new ? template = bug_report . yml .
"""
)
2022-02-14 23:57:47 +01:00
if max_version :
if core_data [ KEY_FRAMEWORK_VERSION ] > required :
msg = f " This feature requires framework version { required } or lower "
if extra_message :
msg + = f " . { extra_message } "
raise Invalid ( msg )
return value
2021-09-20 11:47:51 +02:00
if core_data [ KEY_FRAMEWORK_VERSION ] < required :
2022-02-14 23:57:47 +01:00
msg = f " This feature requires at least framework version { required } "
if extra_message :
msg + = f " . { extra_message } "
raise Invalid ( msg )
2021-09-20 11:47:51 +02:00
return value
return validator
2022-01-24 20:10:27 +01:00
def require_esphome_version ( year , month , patch ) :
def validator ( value ) :
esphome_version = parse_esphome_version ( )
if esphome_version < ( year , month , patch ) :
requires_version = f " { year } . { month } . { patch } "
raise Invalid (
f " This component requires at least ESPHome version { requires_version } "
)
return value
return validator
2021-09-20 11:47:51 +02:00
@contextmanager
def suppress_invalid ( ) :
try :
yield
except vol . Invalid :
pass
2023-02-23 02:22:39 +01:00
2024-01-17 08:28:46 +01:00
GIT_SCHEMA = Schema (
{
Required ( CONF_URL ) : url ,
Optional ( CONF_REF ) : git_ref ,
Optional ( CONF_USERNAME ) : string ,
Optional ( CONF_PASSWORD ) : string ,
2024-05-06 05:19:13 +02:00
Optional ( CONF_PATH ) : string ,
2024-01-17 08:28:46 +01:00
}
)
LOCAL_SCHEMA = Schema (
{
Required ( CONF_PATH ) : directory ,
}
)
2023-02-23 02:22:39 +01:00
def validate_source_shorthand ( value ) :
if not isinstance ( value , str ) :
raise Invalid ( " Shorthand only for strings " )
try :
return SOURCE_SCHEMA ( { CONF_TYPE : TYPE_LOCAL , CONF_PATH : value } )
except Invalid :
pass
# Regex for GitHub repo name with optional branch/tag
# Note: git allows other branch/tag names as well, but never seen them used before
m = re . match (
r " github://(?:([a-zA-Z0-9 \ -]+)/([a-zA-Z0-9 \ - \ ._]+)(?:@([a-zA-Z0-9 \ -_. \ ./]+))?|pr#([0-9]+)) " ,
value ,
)
if m is None :
raise Invalid (
" Source is not a file system path, in expected github://username/name[@branch-or-tag] or github://pr#1234 format! "
)
if m . group ( 4 ) :
conf = {
CONF_TYPE : TYPE_GIT ,
CONF_URL : " https://github.com/esphome/esphome.git " ,
CONF_REF : f " pull/ { m . group ( 4 ) } /head " ,
}
else :
conf = {
CONF_TYPE : TYPE_GIT ,
CONF_URL : f " https://github.com/ { m . group ( 1 ) } / { m . group ( 2 ) } .git " ,
}
if m . group ( 3 ) :
conf [ CONF_REF ] = m . group ( 3 )
return SOURCE_SCHEMA ( conf )
SOURCE_SCHEMA = Any (
validate_source_shorthand ,
typed_schema (
{
2024-01-17 08:28:46 +01:00
TYPE_GIT : GIT_SCHEMA ,
TYPE_LOCAL : LOCAL_SCHEMA ,
2023-02-23 02:22:39 +01:00
}
) ,
)