mirror of
https://github.com/esphome/esphome.git
synced 2025-09-06 05:12:21 +01:00
🏗 Merge C++ into python codebase (#504)
## Description: Move esphome-core codebase into esphome (and a bunch of other refactors). See https://github.com/esphome/feature-requests/issues/97 Yes this is a shit ton of work and no there's no way to automate it :( But it will be worth it 👍 Progress: - Core support (file copy etc): 80% - Base Abstractions (light, switch): ~50% - Integrations: ~10% - Working? Yes, (but only with ported components). Other refactors: - Moves all codegen related stuff into a single class: `esphome.codegen` (imported as `cg`) - Rework coroutine syntax - Move from `component/platform.py` to `domain/component.py` structure as with HA - Move all defaults out of C++ and into config validation. - Remove `make_...` helpers from Application class. Reason: Merge conflicts with every single new integration. - Pointer Variables are stored globally instead of locally in setup(). Reason: stack size limit. Future work: - Rework const.py - Move all `CONF_...` into a conf class (usage `conf.UPDATE_INTERVAL` vs `CONF_UPDATE_INTERVAL`). Reason: Less convoluted import block - Enable loading from `custom_components` folder. **Related issue (if applicable):** https://github.com/esphome/feature-requests/issues/97 **Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):** esphome/esphome-docs#<esphome-docs PR number goes here> ## Checklist: - [ ] The code change is tested and works locally. - [ ] Tests have been added to verify that the new code works (under `tests/` folder). If user exposed functionality or configuration variables are added/changed: - [ ] Documentation added/updated in [esphomedocs](https://github.com/OttoWinter/esphomedocs).
This commit is contained in:
@@ -2,19 +2,19 @@
|
||||
"""Helpers for config validation using voluptuous."""
|
||||
from __future__ import print_function
|
||||
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import uuid as uuid_
|
||||
from datetime import datetime
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from esphome import core
|
||||
from esphome.const import CONF_AVAILABILITY, CONF_COMMAND_TOPIC, CONF_DISCOVERY, CONF_ID, \
|
||||
CONF_INTERNAL, CONF_NAME, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_PLATFORM, \
|
||||
CONF_RETAIN, CONF_SETUP_PRIORITY, CONF_STATE_TOPIC, CONF_TOPIC, ESP_PLATFORM_ESP32, \
|
||||
ESP_PLATFORM_ESP8266, CONF_HOUR, CONF_MINUTE, CONF_SECOND
|
||||
CONF_INTERNAL, CONF_NAME, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, \
|
||||
CONF_RETAIN, CONF_SETUP_PRIORITY, CONF_STATE_TOPIC, CONF_TOPIC, \
|
||||
CONF_HOUR, CONF_MINUTE, CONF_SECOND, CONF_VALUE
|
||||
from esphome.core import CORE, HexInt, IPAddress, Lambda, TimePeriod, TimePeriodMicroseconds, \
|
||||
TimePeriodMilliseconds, TimePeriodSeconds, TimePeriodMinutes
|
||||
from esphome.py_compat import integer_types, string_types, text_type, IS_PY2
|
||||
@@ -25,13 +25,28 @@ _LOGGER = logging.getLogger(__name__)
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
Schema = _Schema
|
||||
port = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535))
|
||||
float_ = vol.Coerce(float)
|
||||
positive_float = vol.All(float_, vol.Range(min=0))
|
||||
zero_to_one_float = vol.All(float_, vol.Range(min=0, max=1))
|
||||
negative_one_to_one_float = vol.All(float_, vol.Range(min=-1, max=1))
|
||||
positive_int = vol.All(vol.Coerce(int), vol.Range(min=0))
|
||||
positive_not_null_int = vol.All(vol.Coerce(int), vol.Range(min=0, min_included=False))
|
||||
Optional = vol.Optional
|
||||
Required = vol.Required
|
||||
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
|
||||
|
||||
port = All(Coerce(int), Range(min=1, max=65535))
|
||||
float_ = Coerce(float)
|
||||
positive_float = All(float_, Range(min=0))
|
||||
zero_to_one_float = All(float_, Range(min=0, max=1))
|
||||
negative_one_to_one_float = All(float_, Range(min=-1, max=1))
|
||||
positive_int = All(Coerce(int), Range(min=0))
|
||||
positive_not_null_int = All(Coerce(int), Range(min=0, min_included=False))
|
||||
|
||||
ALLOWED_NAME_CHARS = u'abcdefghijklmnopqrstuvwxyz0123456789_'
|
||||
|
||||
@@ -51,15 +66,17 @@ RESERVED_IDS = [
|
||||
'App', 'pinMode', 'delay', 'delayMicroseconds', 'digitalRead', 'digitalWrite', 'INPUT',
|
||||
'OUTPUT',
|
||||
'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t', 'int8_t', 'int16_t', 'int32_t', 'int64_t',
|
||||
'display', 'i2c', 'spi', 'uart', 'sensor', 'binary_sensor', 'climate', 'cover', 'text_sensor',
|
||||
'api', 'fan', 'light', 'gpio', 'mqtt', 'ota', 'power_supply', 'wifi'
|
||||
]
|
||||
|
||||
|
||||
def alphanumeric(value):
|
||||
if value is None:
|
||||
raise vol.Invalid("string value is None")
|
||||
raise Invalid("string value is None")
|
||||
value = text_type(value)
|
||||
if not value.isalnum():
|
||||
raise vol.Invalid("string value is not alphanumeric")
|
||||
raise Invalid("string value is not alphanumeric")
|
||||
return value
|
||||
|
||||
|
||||
@@ -67,25 +84,25 @@ def valid_name(value):
|
||||
value = string_strict(value)
|
||||
for c in value:
|
||||
if c not in ALLOWED_NAME_CHARS:
|
||||
raise vol.Invalid(u"'{}' is an invalid character for names. Valid characters are: {}"
|
||||
u" (lowercase, no spaces)".format(c, ALLOWED_NAME_CHARS))
|
||||
raise Invalid(u"'{}' is an invalid character for names. Valid characters are: {}"
|
||||
u" (lowercase, no spaces)".format(c, ALLOWED_NAME_CHARS))
|
||||
return value
|
||||
|
||||
|
||||
def string(value):
|
||||
if isinstance(value, (dict, list)):
|
||||
raise vol.Invalid("string value cannot be dictionary or list.")
|
||||
raise Invalid("string value cannot be dictionary or list.")
|
||||
if value is not None:
|
||||
return text_type(value)
|
||||
raise vol.Invalid("string value is None")
|
||||
raise Invalid("string value is None")
|
||||
|
||||
|
||||
def string_strict(value):
|
||||
"""Strictly only allow strings."""
|
||||
if isinstance(value, string_types):
|
||||
return value
|
||||
raise vol.Invalid("Must be string, got {}. did you forget putting quotes "
|
||||
"around the value?".format(type(value)))
|
||||
raise Invalid("Must be string, got {}. did you forget putting quotes "
|
||||
"around the value?".format(type(value)))
|
||||
|
||||
|
||||
def icon(value):
|
||||
@@ -93,7 +110,7 @@ def icon(value):
|
||||
value = string_strict(value)
|
||||
if value.startswith('mdi:'):
|
||||
return value
|
||||
raise vol.Invalid('Icons should start with prefix "mdi:"')
|
||||
raise Invalid('Icons should start with prefix "mdi:"')
|
||||
|
||||
|
||||
def boolean(value):
|
||||
@@ -104,13 +121,13 @@ def boolean(value):
|
||||
return True
|
||||
if value in ('0', 'false', 'no', 'off', 'disable'):
|
||||
return False
|
||||
raise vol.Invalid('invalid boolean value {}'.format(value))
|
||||
raise Invalid('invalid boolean value {}'.format(value))
|
||||
return bool(value)
|
||||
|
||||
|
||||
def ensure_list(*validators):
|
||||
"""Wrap value in list if it is not one."""
|
||||
user = vol.All(*validators)
|
||||
user = All(*validators)
|
||||
|
||||
def validator(value):
|
||||
if value is None or (isinstance(value, dict) and not value):
|
||||
@@ -118,12 +135,18 @@ def ensure_list(*validators):
|
||||
if not isinstance(value, list):
|
||||
return [user(value)]
|
||||
ret = []
|
||||
errs = []
|
||||
for i, val in enumerate(value):
|
||||
try:
|
||||
ret.append(user(val))
|
||||
except vol.Invalid as err:
|
||||
except vol.MultipleInvalid as err:
|
||||
err.prepend([i])
|
||||
raise err
|
||||
errs.extend(err.errors)
|
||||
except Invalid as err:
|
||||
err.prepend([i])
|
||||
errs.append(err)
|
||||
if errs:
|
||||
raise vol.MultipleInvalid(errs)
|
||||
return ret
|
||||
|
||||
return validator
|
||||
@@ -139,7 +162,7 @@ def ensure_dict(value):
|
||||
if value is None:
|
||||
return {}
|
||||
if not isinstance(value, dict):
|
||||
raise vol.Invalid("Expected a dictionary")
|
||||
raise Invalid("Expected a dictionary")
|
||||
return value
|
||||
|
||||
|
||||
@@ -161,24 +184,24 @@ def int_(value):
|
||||
return int(value)
|
||||
|
||||
|
||||
hex_int = vol.Coerce(hex_int_)
|
||||
hex_int = Coerce(hex_int_)
|
||||
|
||||
|
||||
def validate_id_name(value):
|
||||
value = string(value)
|
||||
if not value:
|
||||
raise vol.Invalid("ID must not be empty")
|
||||
raise Invalid("ID must not be empty")
|
||||
if value[0].isdigit():
|
||||
raise vol.Invalid("First character in ID cannot be a digit.")
|
||||
raise Invalid("First character in ID cannot be a digit.")
|
||||
if '-' in value:
|
||||
raise vol.Invalid("Dashes are not supported in IDs, please use underscores instead.")
|
||||
raise Invalid("Dashes are not supported in IDs, please use underscores instead.")
|
||||
for char in value:
|
||||
if char != '_' and not char.isalnum():
|
||||
raise vol.Invalid(u"IDs must only consist of upper/lowercase characters, the underscore"
|
||||
u"character and numbers. The character '{}' cannot be used"
|
||||
u"".format(char))
|
||||
raise Invalid(u"IDs must only consist of upper/lowercase characters, the underscore"
|
||||
u"character and numbers. The character '{}' cannot be used"
|
||||
u"".format(char))
|
||||
if value in RESERVED_IDS:
|
||||
raise vol.Invalid(u"ID {} is reserved internally and cannot be used".format(value))
|
||||
raise Invalid(u"ID {} is reserved internally and cannot be used".format(value))
|
||||
return value
|
||||
|
||||
|
||||
@@ -219,14 +242,14 @@ def only_on(platforms):
|
||||
|
||||
def validator_(obj):
|
||||
if CORE.esp_platform not in platforms:
|
||||
raise vol.Invalid(u"This feature is only available on {}".format(platforms))
|
||||
raise Invalid(u"This feature is only available on {}".format(platforms))
|
||||
return obj
|
||||
|
||||
return validator_
|
||||
|
||||
|
||||
only_on_esp32 = only_on(ESP_PLATFORM_ESP32)
|
||||
only_on_esp8266 = only_on(ESP_PLATFORM_ESP8266)
|
||||
only_on_esp32 = only_on('ESP32')
|
||||
only_on_esp8266 = only_on('ESP8266')
|
||||
|
||||
|
||||
# Adapted from:
|
||||
@@ -237,10 +260,10 @@ def has_at_least_one_key(*keys):
|
||||
def validate(obj):
|
||||
"""Test keys exist in dict."""
|
||||
if not isinstance(obj, dict):
|
||||
raise vol.Invalid('expected dictionary')
|
||||
raise Invalid('expected dictionary')
|
||||
|
||||
if not any(k in keys for k in obj):
|
||||
raise vol.Invalid('Must contain at least one of {}.'.format(', '.join(keys)))
|
||||
raise Invalid('Must contain at least one of {}.'.format(', '.join(keys)))
|
||||
return obj
|
||||
|
||||
return validate
|
||||
@@ -249,13 +272,13 @@ def has_at_least_one_key(*keys):
|
||||
def has_exactly_one_key(*keys):
|
||||
def validate(obj):
|
||||
if not isinstance(obj, dict):
|
||||
raise vol.Invalid('expected dictionary')
|
||||
raise Invalid('expected dictionary')
|
||||
|
||||
number = sum(k in keys for k in obj)
|
||||
if number > 1:
|
||||
raise vol.Invalid("Cannot specify more than one of {}.".format(', '.join(keys)))
|
||||
raise Invalid("Cannot specify more than one of {}.".format(', '.join(keys)))
|
||||
if number < 1:
|
||||
raise vol.Invalid('Must contain exactly one of {}.'.format(', '.join(keys)))
|
||||
raise Invalid('Must contain exactly one of {}.'.format(', '.join(keys)))
|
||||
return obj
|
||||
|
||||
return validate
|
||||
@@ -264,11 +287,11 @@ def has_exactly_one_key(*keys):
|
||||
def has_at_most_one_key(*keys):
|
||||
def validate(obj):
|
||||
if not isinstance(obj, dict):
|
||||
raise vol.Invalid('expected dictionary')
|
||||
raise Invalid('expected dictionary')
|
||||
|
||||
number = sum(k in keys for k in obj)
|
||||
if number > 1:
|
||||
raise vol.Invalid("Cannot specify more than one of {}.".format(', '.join(keys)))
|
||||
raise Invalid("Cannot specify more than one of {}.".format(', '.join(keys)))
|
||||
return obj
|
||||
|
||||
return validate
|
||||
@@ -276,7 +299,7 @@ def has_at_most_one_key(*keys):
|
||||
|
||||
TIME_PERIOD_ERROR = "Time period {} should be format number + unit, for example 5ms, 5s, 5min, 5h"
|
||||
|
||||
time_period_dict = vol.All(
|
||||
time_period_dict = All(
|
||||
dict, Schema({
|
||||
'days': float_,
|
||||
'hours': float_,
|
||||
@@ -293,14 +316,14 @@ time_period_dict = vol.All(
|
||||
def time_period_str_colon(value):
|
||||
"""Validate and transform time offset with format HH:MM[:SS]."""
|
||||
if isinstance(value, int):
|
||||
raise vol.Invalid('Make sure you wrap time values in quotes')
|
||||
raise Invalid('Make sure you wrap time values in quotes')
|
||||
if not isinstance(value, str):
|
||||
raise vol.Invalid(TIME_PERIOD_ERROR.format(value))
|
||||
raise Invalid(TIME_PERIOD_ERROR.format(value))
|
||||
|
||||
try:
|
||||
parsed = [int(x) for x in value.split(':')]
|
||||
except ValueError:
|
||||
raise vol.Invalid(TIME_PERIOD_ERROR.format(value))
|
||||
raise Invalid(TIME_PERIOD_ERROR.format(value))
|
||||
|
||||
if len(parsed) == 2:
|
||||
hour, minute = parsed
|
||||
@@ -308,7 +331,7 @@ def time_period_str_colon(value):
|
||||
elif len(parsed) == 3:
|
||||
hour, minute, second = parsed
|
||||
else:
|
||||
raise vol.Invalid(TIME_PERIOD_ERROR.format(value))
|
||||
raise Invalid(TIME_PERIOD_ERROR.format(value))
|
||||
|
||||
return TimePeriod(hours=hour, minutes=minute, seconds=second)
|
||||
|
||||
@@ -316,10 +339,10 @@ def time_period_str_colon(value):
|
||||
def time_period_str_unit(value):
|
||||
"""Validate and transform time period with time unit and integer value."""
|
||||
if isinstance(value, int):
|
||||
raise vol.Invalid("Don't know what '{0}' means as it has no time *unit*! Did you mean "
|
||||
"'{0}s'?".format(value))
|
||||
raise Invalid("Don't know what '{0}' means as it has no time *unit*! Did you mean "
|
||||
"'{0}s'?".format(value))
|
||||
if not isinstance(value, string_types):
|
||||
raise vol.Invalid("Expected string for time period with unit.")
|
||||
raise Invalid("Expected string for time period with unit.")
|
||||
|
||||
unit_to_kwarg = {
|
||||
'us': 'microseconds',
|
||||
@@ -340,8 +363,8 @@ def time_period_str_unit(value):
|
||||
match = re.match(r"^([-+]?[0-9]*\.?[0-9]*)\s*(\w*)$", value)
|
||||
|
||||
if match is None:
|
||||
raise vol.Invalid(u"Expected time period with unit, "
|
||||
u"got {}".format(value))
|
||||
raise Invalid(u"Expected time period with unit, "
|
||||
u"got {}".format(value))
|
||||
kwarg = unit_to_kwarg[one_of(*unit_to_kwarg)(match.group(2))]
|
||||
|
||||
return TimePeriod(**{kwarg: float(match.group(1))})
|
||||
@@ -349,7 +372,7 @@ def time_period_str_unit(value):
|
||||
|
||||
def time_period_in_milliseconds_(value):
|
||||
if value.microseconds is not None and value.microseconds != 0:
|
||||
raise vol.Invalid("Maximum precision is milliseconds")
|
||||
raise Invalid("Maximum precision is milliseconds")
|
||||
return TimePeriodMilliseconds(**value.as_dict())
|
||||
|
||||
|
||||
@@ -359,19 +382,19 @@ def time_period_in_microseconds_(value):
|
||||
|
||||
def time_period_in_seconds_(value):
|
||||
if value.microseconds is not None and value.microseconds != 0:
|
||||
raise vol.Invalid("Maximum precision is seconds")
|
||||
raise Invalid("Maximum precision is seconds")
|
||||
if value.milliseconds is not None and value.milliseconds != 0:
|
||||
raise vol.Invalid("Maximum precision is seconds")
|
||||
raise Invalid("Maximum precision is seconds")
|
||||
return TimePeriodSeconds(**value.as_dict())
|
||||
|
||||
|
||||
def time_period_in_minutes_(value):
|
||||
if value.microseconds is not None and value.microseconds != 0:
|
||||
raise vol.Invalid("Maximum precision is minutes")
|
||||
raise Invalid("Maximum precision is minutes")
|
||||
if value.milliseconds is not None and value.milliseconds != 0:
|
||||
raise vol.Invalid("Maximum precision is minutes")
|
||||
raise Invalid("Maximum precision is minutes")
|
||||
if value.seconds is not None and value.seconds != 0:
|
||||
raise vol.Invalid("Maximum precision is minutes")
|
||||
raise Invalid("Maximum precision is minutes")
|
||||
return TimePeriodMinutes(**value.as_dict())
|
||||
|
||||
|
||||
@@ -381,15 +404,15 @@ def update_interval(value):
|
||||
return positive_time_period_milliseconds(value)
|
||||
|
||||
|
||||
time_period = vol.Any(time_period_str_unit, time_period_str_colon, time_period_dict)
|
||||
positive_time_period = vol.All(time_period, vol.Range(min=TimePeriod()))
|
||||
positive_time_period_milliseconds = vol.All(positive_time_period, time_period_in_milliseconds_)
|
||||
positive_time_period_seconds = vol.All(positive_time_period, time_period_in_seconds_)
|
||||
positive_time_period_minutes = vol.All(positive_time_period, time_period_in_minutes_)
|
||||
time_period_microseconds = vol.All(time_period, time_period_in_microseconds_)
|
||||
positive_time_period_microseconds = vol.All(positive_time_period, time_period_in_microseconds_)
|
||||
positive_not_null_time_period = vol.All(time_period,
|
||||
vol.Range(min=TimePeriod(), min_included=False))
|
||||
time_period = Any(time_period_str_unit, time_period_str_colon, time_period_dict)
|
||||
positive_time_period = All(time_period, Range(min=TimePeriod()))
|
||||
positive_time_period_milliseconds = All(positive_time_period, time_period_in_milliseconds_)
|
||||
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_)
|
||||
positive_time_period_microseconds = All(positive_time_period, time_period_in_microseconds_)
|
||||
positive_not_null_time_period = All(time_period,
|
||||
Range(min=TimePeriod(), min_included=False))
|
||||
|
||||
|
||||
def time_of_day(value):
|
||||
@@ -400,7 +423,7 @@ def time_of_day(value):
|
||||
try:
|
||||
date = datetime.strptime(value, '%H:%M:%S %p')
|
||||
except ValueError:
|
||||
raise vol.Invalid("Invalid time of day: {}".format(err))
|
||||
raise Invalid("Invalid time of day: {}".format(err))
|
||||
|
||||
return {
|
||||
CONF_HOUR: date.hour,
|
||||
@@ -413,21 +436,21 @@ def mac_address(value):
|
||||
value = string_strict(value)
|
||||
parts = value.split(':')
|
||||
if len(parts) != 6:
|
||||
raise vol.Invalid("MAC Address must consist of 6 : (colon) separated parts")
|
||||
raise Invalid("MAC Address must consist of 6 : (colon) separated parts")
|
||||
parts_int = []
|
||||
if any(len(part) != 2 for part in parts):
|
||||
raise vol.Invalid("MAC Address must be format XX:XX:XX:XX:XX:XX")
|
||||
raise Invalid("MAC Address must be format XX:XX:XX:XX:XX:XX")
|
||||
for part in parts:
|
||||
try:
|
||||
parts_int.append(int(part, 16))
|
||||
except ValueError:
|
||||
raise vol.Invalid("MAC Address parts must be hexadecimal values from 00 to FF")
|
||||
raise Invalid("MAC Address parts must be hexadecimal values from 00 to FF")
|
||||
|
||||
return core.MACAddress(*parts_int)
|
||||
|
||||
|
||||
def uuid(value):
|
||||
return vol.Coerce(uuid_.UUID)(value)
|
||||
return Coerce(uuid_.UUID)(value)
|
||||
|
||||
|
||||
METRIC_SUFFIXES = {
|
||||
@@ -444,11 +467,11 @@ def float_with_unit(quantity, regex_suffix):
|
||||
match = pattern.match(string(value))
|
||||
|
||||
if match is None:
|
||||
raise vol.Invalid(u"Expected {} with unit, got {}".format(quantity, value))
|
||||
raise Invalid(u"Expected {} with unit, got {}".format(quantity, value))
|
||||
|
||||
mantissa = float(match.group(1))
|
||||
if match.group(2) not in METRIC_SUFFIXES:
|
||||
raise vol.Invalid(u"Invalid {} suffix {}".format(quantity, match.group(2)))
|
||||
raise Invalid(u"Invalid {} suffix {}".format(quantity, match.group(2)))
|
||||
|
||||
multiplier = METRIC_SUFFIXES[match.group(2)]
|
||||
return mantissa * multiplier
|
||||
@@ -477,25 +500,25 @@ if IS_PY2:
|
||||
output += u' for ' + self.error_type
|
||||
return output + path
|
||||
|
||||
vol.Invalid.__unicode__ = _vol_invalid_unicode
|
||||
Invalid.__unicode__ = _vol_invalid_unicode
|
||||
|
||||
|
||||
def temperature(value):
|
||||
try:
|
||||
return _temperature_c(value)
|
||||
except vol.Invalid as orig_err: # noqa
|
||||
except Invalid as orig_err: # noqa
|
||||
pass
|
||||
|
||||
try:
|
||||
kelvin = _temperature_k(value)
|
||||
return kelvin - 273.15
|
||||
except vol.Invalid:
|
||||
except Invalid:
|
||||
pass
|
||||
|
||||
try:
|
||||
fahrenheit = _temperature_f(value)
|
||||
return (fahrenheit - 32) * (5/9)
|
||||
except vol.Invalid:
|
||||
return (fahrenheit - 32) * (5 / 9)
|
||||
except Invalid:
|
||||
pass
|
||||
|
||||
raise orig_err # noqa
|
||||
@@ -508,10 +531,10 @@ _color_temperature_kelvin = float_with_unit('Color Temperature', r'(K|Kelvin)')
|
||||
def color_temperature(value):
|
||||
try:
|
||||
val = _color_temperature_mireds(value)
|
||||
except vol.Invalid:
|
||||
except Invalid:
|
||||
val = 1000000.0 / _color_temperature_kelvin(value)
|
||||
if val < 0:
|
||||
raise vol.Invalid("Color temperature cannot be negative")
|
||||
raise Invalid("Color temperature cannot be negative")
|
||||
return val
|
||||
|
||||
|
||||
@@ -520,25 +543,25 @@ def validate_bytes(value):
|
||||
match = re.match(r"^([0-9]+)\s*(\w*?)(?:byte|B|b)?s?$", value)
|
||||
|
||||
if match is None:
|
||||
raise vol.Invalid(u"Expected number of bytes with unit, got {}".format(value))
|
||||
raise Invalid(u"Expected number of bytes with unit, got {}".format(value))
|
||||
|
||||
mantissa = int(match.group(1))
|
||||
if match.group(2) not in METRIC_SUFFIXES:
|
||||
raise vol.Invalid(u"Invalid metric suffix {}".format(match.group(2)))
|
||||
raise Invalid(u"Invalid metric suffix {}".format(match.group(2)))
|
||||
multiplier = METRIC_SUFFIXES[match.group(2)]
|
||||
if multiplier < 1:
|
||||
raise vol.Invalid(u"Only suffixes with positive exponents are supported. "
|
||||
u"Got {}".format(match.group(2)))
|
||||
raise Invalid(u"Only suffixes with positive exponents are supported. "
|
||||
u"Got {}".format(match.group(2)))
|
||||
return int(mantissa * multiplier)
|
||||
|
||||
|
||||
def hostname(value):
|
||||
value = string(value)
|
||||
if len(value) > 63:
|
||||
raise vol.Invalid("Hostnames can only be 63 characters long")
|
||||
raise Invalid("Hostnames can only be 63 characters long")
|
||||
for c in value:
|
||||
if not (c.isalnum() or c in '_-'):
|
||||
raise vol.Invalid("Hostname can only have alphanumeric characters and _ or -")
|
||||
raise Invalid("Hostname can only have alphanumeric characters and _ or -")
|
||||
return value
|
||||
|
||||
|
||||
@@ -548,8 +571,8 @@ def domain(value):
|
||||
return value
|
||||
try:
|
||||
return str(ipv4(value))
|
||||
except vol.Invalid:
|
||||
raise vol.Invalid("Invalid domain: {}".format(value))
|
||||
except Invalid:
|
||||
raise Invalid("Invalid domain: {}".format(value))
|
||||
|
||||
|
||||
def domain_name(value):
|
||||
@@ -557,21 +580,21 @@ def domain_name(value):
|
||||
if not value:
|
||||
return value
|
||||
if not value.startswith('.'):
|
||||
raise vol.Invalid("Domain name must start with .")
|
||||
raise Invalid("Domain name must start with .")
|
||||
if value.startswith('..'):
|
||||
raise vol.Invalid("Domain name must start with single .")
|
||||
raise Invalid("Domain name must start with single .")
|
||||
for c in value:
|
||||
if not (c.isalnum() or c in '._-'):
|
||||
raise vol.Invalid("Domain name can only have alphanumeric characters and _ or -")
|
||||
raise Invalid("Domain name can only have alphanumeric characters and _ or -")
|
||||
return value
|
||||
|
||||
|
||||
def ssid(value):
|
||||
value = string_strict(value)
|
||||
if not value:
|
||||
raise vol.Invalid("SSID can't be empty.")
|
||||
raise Invalid("SSID can't be empty.")
|
||||
if len(value) > 32:
|
||||
raise vol.Invalid("SSID can't be longer than 32 characters")
|
||||
raise Invalid("SSID can't be longer than 32 characters")
|
||||
return value
|
||||
|
||||
|
||||
@@ -583,34 +606,34 @@ def ipv4(value):
|
||||
elif isinstance(value, IPAddress):
|
||||
return value
|
||||
else:
|
||||
raise vol.Invalid("IPv4 address must consist of either string or "
|
||||
"integer list")
|
||||
raise Invalid("IPv4 address must consist of either string or "
|
||||
"integer list")
|
||||
if len(parts) != 4:
|
||||
raise vol.Invalid("IPv4 address must consist of four point-separated "
|
||||
"integers")
|
||||
raise Invalid("IPv4 address must consist of four point-separated "
|
||||
"integers")
|
||||
parts_ = list(map(int, parts))
|
||||
if not all(0 <= x < 256 for x in parts_):
|
||||
raise vol.Invalid("IPv4 address parts must be in range from 0 to 255")
|
||||
raise Invalid("IPv4 address parts must be in range from 0 to 255")
|
||||
return IPAddress(*parts_)
|
||||
|
||||
|
||||
def _valid_topic(value):
|
||||
"""Validate that this is a valid topic name/filter."""
|
||||
if isinstance(value, dict):
|
||||
raise vol.Invalid("Can't use dictionary with topic")
|
||||
raise Invalid("Can't use dictionary with topic")
|
||||
value = string(value)
|
||||
try:
|
||||
raw_value = value.encode('utf-8')
|
||||
except UnicodeError:
|
||||
raise vol.Invalid("MQTT topic name/filter must be valid UTF-8 string.")
|
||||
raise Invalid("MQTT topic name/filter must be valid UTF-8 string.")
|
||||
if not raw_value:
|
||||
raise vol.Invalid("MQTT topic name/filter must not be empty.")
|
||||
raise Invalid("MQTT topic name/filter must not be empty.")
|
||||
if len(raw_value) > 65535:
|
||||
raise vol.Invalid("MQTT topic name/filter must not be longer than "
|
||||
"65535 encoded bytes.")
|
||||
raise Invalid("MQTT topic name/filter must not be longer than "
|
||||
"65535 encoded bytes.")
|
||||
if '\0' in value:
|
||||
raise vol.Invalid("MQTT topic name/filter must not contain null "
|
||||
"character.")
|
||||
raise Invalid("MQTT topic name/filter must not contain null "
|
||||
"character.")
|
||||
return value
|
||||
|
||||
|
||||
@@ -620,18 +643,18 @@ def subscribe_topic(value):
|
||||
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 vol.Invalid("Single-level wildcard must occupy an entire "
|
||||
"level of the filter")
|
||||
raise Invalid("Single-level wildcard must occupy an entire "
|
||||
"level of the filter")
|
||||
|
||||
index = value.find('#')
|
||||
if index != -1:
|
||||
if index != len(value) - 1:
|
||||
# If there are multiple wildcards, this will also trigger
|
||||
raise vol.Invalid("Multi-level wildcard must be the last "
|
||||
"character in the topic filter.")
|
||||
raise Invalid("Multi-level wildcard must be the last "
|
||||
"character in the topic filter.")
|
||||
if len(value) > 1 and value[index - 1] != '/':
|
||||
raise vol.Invalid("Multi-level wildcard must be after a topic "
|
||||
"level separator.")
|
||||
raise Invalid("Multi-level wildcard must be after a topic "
|
||||
"level separator.")
|
||||
|
||||
return value
|
||||
|
||||
@@ -640,7 +663,7 @@ def publish_topic(value):
|
||||
"""Validate that we can publish using this MQTT topic."""
|
||||
value = _valid_topic(value)
|
||||
if '+' in value or '#' in value:
|
||||
raise vol.Invalid("Wildcards can not be used in topic names")
|
||||
raise Invalid("Wildcards can not be used in topic names")
|
||||
return value
|
||||
|
||||
|
||||
@@ -654,25 +677,25 @@ def mqtt_qos(value):
|
||||
try:
|
||||
value = int(value)
|
||||
except (TypeError, ValueError):
|
||||
raise vol.Invalid(u"MQTT Quality of Service must be integer, got {}".format(value))
|
||||
raise Invalid(u"MQTT Quality of Service must be integer, got {}".format(value))
|
||||
return one_of(0, 1, 2)(value)
|
||||
|
||||
|
||||
def requires_component(comp):
|
||||
def validator(value):
|
||||
if comp not in CORE.raw_config:
|
||||
raise vol.Invalid("This option requires component {}".format(comp))
|
||||
raise Invalid("This option requires component {}".format(comp))
|
||||
return value
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
uint8_t = vol.All(int_, vol.Range(min=0, max=255))
|
||||
uint16_t = vol.All(int_, vol.Range(min=0, max=65535))
|
||||
uint32_t = vol.All(int_, vol.Range(min=0, max=4294967295))
|
||||
hex_uint8_t = vol.All(hex_int, vol.Range(min=0, max=255))
|
||||
hex_uint16_t = vol.All(hex_int, vol.Range(min=0, max=65535))
|
||||
hex_uint32_t = vol.All(hex_int, vol.Range(min=0, max=4294967295))
|
||||
uint8_t = All(int_, Range(min=0, max=255))
|
||||
uint16_t = All(int_, Range(min=0, max=65535))
|
||||
uint32_t = All(int_, Range(min=0, max=4294967295))
|
||||
hex_uint8_t = All(hex_int, Range(min=0, max=255))
|
||||
hex_uint16_t = All(hex_int, Range(min=0, max=65535))
|
||||
hex_uint32_t = All(hex_int, Range(min=0, max=4294967295))
|
||||
i2c_address = hex_uint8_t
|
||||
|
||||
|
||||
@@ -689,12 +712,12 @@ def possibly_negative_percentage(value):
|
||||
msg = "Percentage must not be higher than 100%."
|
||||
if not has_percent_sign:
|
||||
msg += " Please put a percent sign after the number!"
|
||||
raise vol.Invalid(msg)
|
||||
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 vol.Invalid(msg)
|
||||
raise Invalid(msg)
|
||||
return negative_one_to_one_float(value)
|
||||
|
||||
|
||||
@@ -706,7 +729,7 @@ def percentage_int(value):
|
||||
|
||||
def invalid(message):
|
||||
def validator(value):
|
||||
raise vol.Invalid(message)
|
||||
raise Invalid(message)
|
||||
|
||||
return validator
|
||||
|
||||
@@ -721,6 +744,7 @@ def one_of(*values, **kwargs):
|
||||
upper = kwargs.get('upper', False)
|
||||
string_ = kwargs.get('string', False) or lower or upper
|
||||
to_int = kwargs.get('int', False)
|
||||
to_float = kwargs.get('float', False)
|
||||
space = kwargs.get('space', ' ')
|
||||
|
||||
def validator(value):
|
||||
@@ -729,12 +753,14 @@ def one_of(*values, **kwargs):
|
||||
value = value.replace(' ', space)
|
||||
if to_int:
|
||||
value = int_(value)
|
||||
if to_float:
|
||||
value = float_(value)
|
||||
if lower:
|
||||
value = vol.Lower(value)
|
||||
value = Lower(value)
|
||||
if upper:
|
||||
value = vol.Upper(value)
|
||||
value = Upper(value)
|
||||
if value not in values:
|
||||
raise vol.Invalid(u"Unknown value '{}', must be one of {}".format(value, options))
|
||||
raise Invalid(u"Unknown value '{}', must be one of {}".format(value, options))
|
||||
return value
|
||||
|
||||
return validator
|
||||
@@ -749,18 +775,18 @@ def lambda_(value):
|
||||
def dimensions(value):
|
||||
if isinstance(value, list):
|
||||
if len(value) != 2:
|
||||
raise vol.Invalid(u"Dimensions must have a length of two, not {}".format(len(value)))
|
||||
raise Invalid(u"Dimensions must have a length of two, not {}".format(len(value)))
|
||||
try:
|
||||
width, height = int(value[0]), int(value[1])
|
||||
except ValueError:
|
||||
raise vol.Invalid(u"Width and height dimensions must be integers")
|
||||
raise Invalid(u"Width and height dimensions must be integers")
|
||||
if width <= 0 or height <= 0:
|
||||
raise vol.Invalid(u"Width and height must at least be 1")
|
||||
raise Invalid(u"Width and height must at least be 1")
|
||||
return [width, height]
|
||||
value = string(value)
|
||||
match = re.match(r"\s*([0-9]+)\s*[xX]\s*([0-9]+)\s*", value)
|
||||
if not match:
|
||||
raise vol.Invalid(u"Invalid value '{}' for dimensions. Only WIDTHxHEIGHT is allowed.")
|
||||
raise Invalid(u"Invalid value '{}' for dimensions. Only WIDTHxHEIGHT is allowed.")
|
||||
return dimensions([match.group(1), match.group(2)])
|
||||
|
||||
|
||||
@@ -768,10 +794,10 @@ def directory(value):
|
||||
value = string(value)
|
||||
path = CORE.relative_path(value)
|
||||
if not os.path.exists(path):
|
||||
raise vol.Invalid(u"Could not find directory '{}'. Please make sure it exists.".format(
|
||||
raise Invalid(u"Could not find directory '{}'. Please make sure it exists.".format(
|
||||
path))
|
||||
if not os.path.isdir(path):
|
||||
raise vol.Invalid(u"Path '{}' is not a directory.".format(path))
|
||||
raise Invalid(u"Path '{}' is not a directory.".format(path))
|
||||
return value
|
||||
|
||||
|
||||
@@ -779,10 +805,10 @@ def file_(value):
|
||||
value = string(value)
|
||||
path = CORE.relative_path(value)
|
||||
if not os.path.exists(path):
|
||||
raise vol.Invalid(u"Could not find file '{}'. Please make sure it exists.".format(
|
||||
raise Invalid(u"Could not find file '{}'. Please make sure it exists.".format(
|
||||
path))
|
||||
if not os.path.isfile(path):
|
||||
raise vol.Invalid(u"Path '{}' is not a file.".format(path))
|
||||
raise Invalid(u"Path '{}' is not a file.".format(path))
|
||||
return value
|
||||
|
||||
|
||||
@@ -792,28 +818,77 @@ ENTITY_ID_CHARACTERS = 'abcdefghijklmnopqrstuvwxyz0123456789_'
|
||||
def entity_id(value):
|
||||
value = string_strict(value).lower()
|
||||
if value.count('.') != 1:
|
||||
raise vol.Invalid("Entity ID must have exactly one dot in it")
|
||||
raise Invalid("Entity ID must have exactly one dot in it")
|
||||
for x in value.split('.'):
|
||||
for c in x:
|
||||
if c not in ENTITY_ID_CHARACTERS:
|
||||
raise vol.Invalid("Invalid character for entity ID: {}".format(c))
|
||||
raise Invalid("Invalid character for entity ID: {}".format(c))
|
||||
return value
|
||||
|
||||
|
||||
class GenerateID(vol.Optional):
|
||||
def extract_keys(schema):
|
||||
if isinstance(schema, Schema):
|
||||
schema = schema.schema
|
||||
assert isinstance(schema, dict)
|
||||
keys = list(schema.keys())
|
||||
keys.sort()
|
||||
return keys
|
||||
|
||||
|
||||
class GenerateID(Optional):
|
||||
def __init__(self, key=CONF_ID):
|
||||
super(GenerateID, self).__init__(key, default=lambda: None)
|
||||
|
||||
|
||||
class SplitDefault(Optional):
|
||||
def __init__(self, key, esp8266=vol.UNDEFINED, esp32=vol.UNDEFINED):
|
||||
super(SplitDefault, self).__init__(key)
|
||||
self._esp8266_default = vol.default_factory(esp8266)
|
||||
self._esp32_default = vol.default_factory(esp32)
|
||||
|
||||
@property
|
||||
def default(self):
|
||||
if CORE.is_esp8266:
|
||||
return self._esp8266_default
|
||||
if CORE.is_esp32:
|
||||
return self._esp32_default
|
||||
raise ValueError
|
||||
|
||||
@default.setter
|
||||
def default(self, value):
|
||||
# Ignore default set from vol.Optional
|
||||
pass
|
||||
|
||||
|
||||
class OnlyWith(Optional):
|
||||
def __init__(self, key, component, default=None):
|
||||
super(OnlyWith, self).__init__(key)
|
||||
self._component = component
|
||||
self._default = vol.default_factory(default)
|
||||
|
||||
@property
|
||||
def default(self):
|
||||
if self._component not in CORE.raw_config:
|
||||
return vol.UNDEFINED
|
||||
return self._default
|
||||
|
||||
@default.setter
|
||||
def default(self, value):
|
||||
# Ignore default set from vol.Optional
|
||||
pass
|
||||
|
||||
|
||||
def nameable(*schemas):
|
||||
def validator(config):
|
||||
config = vol.All(*schemas)(config)
|
||||
config = All(*schemas)(config)
|
||||
if CONF_NAME not in config and CONF_ID not in config:
|
||||
raise vol.Invalid("At least one of 'id:' or 'name:' is required!")
|
||||
print(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:
|
||||
raise vol.Invalid("At least one of 'id:' or 'name:' is required!")
|
||||
print(config)
|
||||
raise Invalid("At least one of 'id:' or 'name:' is required!")
|
||||
config[CONF_NAME] = id.id
|
||||
config[CONF_INTERNAL] = True
|
||||
return config
|
||||
@@ -822,30 +897,70 @@ def nameable(*schemas):
|
||||
return validator
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = Schema({
|
||||
vol.Required(CONF_PLATFORM): valid,
|
||||
})
|
||||
def validate_registry_entry(name, registry, ignore_keys):
|
||||
def validator(value):
|
||||
if isinstance(value, string_types):
|
||||
value = {value: {}}
|
||||
if not isinstance(value, dict):
|
||||
raise Invalid(u"{} must consist of key-value mapping! Got {}"
|
||||
u"".format(name.title(), value))
|
||||
item = value.copy()
|
||||
key = next((x for x in item if x not in ignore_keys), None)
|
||||
if key is None:
|
||||
raise Invalid(u"Key missing from {}! Got {}".format(name, item))
|
||||
if key not in registry:
|
||||
raise vol.Invalid(u"Unable to find {} with the name '{}'".format(name, key))
|
||||
key2 = next((x for x in item if x != key and x not in ignore_keys), None)
|
||||
if key2 is not None:
|
||||
raise vol.Invalid(u"Cannot have two {0}s in one item. Key '{1}' overrides '{2}'! "
|
||||
u"Did you forget to indent the block inside the {0}?"
|
||||
u"".format(name, key, key2))
|
||||
validator_ = registry[key][0]
|
||||
try:
|
||||
item[key] = validator_(item[key] or {})
|
||||
except vol.Invalid as err:
|
||||
err.prepend([key])
|
||||
raise err
|
||||
return item
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
def validate_registry(name, registry, ignore_keys):
|
||||
return ensure_list(validate_registry_entry(name, registry, ignore_keys))
|
||||
|
||||
|
||||
def maybe_simple_value(*validators):
|
||||
validator = All(*validators)
|
||||
|
||||
def validate(value):
|
||||
if isinstance(value, dict) and CONF_VALUE in value:
|
||||
return validator(value)
|
||||
return validator({CONF_VALUE: value})
|
||||
|
||||
return validate
|
||||
|
||||
|
||||
MQTT_COMPONENT_AVAILABILITY_SCHEMA = Schema({
|
||||
vol.Required(CONF_TOPIC): subscribe_topic,
|
||||
vol.Optional(CONF_PAYLOAD_AVAILABLE, default='online'): mqtt_payload,
|
||||
vol.Optional(CONF_PAYLOAD_NOT_AVAILABLE, default='offline'): mqtt_payload,
|
||||
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({
|
||||
vol.Optional(CONF_NAME): string,
|
||||
vol.Optional(CONF_RETAIN): vol.All(requires_component('mqtt'), boolean),
|
||||
vol.Optional(CONF_DISCOVERY): vol.All(requires_component('mqtt'), boolean),
|
||||
vol.Optional(CONF_STATE_TOPIC): vol.All(requires_component('mqtt'), publish_topic),
|
||||
vol.Optional(CONF_AVAILABILITY): vol.All(requires_component('mqtt'),
|
||||
vol.Any(None, MQTT_COMPONENT_AVAILABILITY_SCHEMA)),
|
||||
vol.Optional(CONF_INTERNAL): boolean,
|
||||
Optional(CONF_NAME): string,
|
||||
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)),
|
||||
Optional(CONF_INTERNAL): boolean,
|
||||
})
|
||||
|
||||
MQTT_COMMAND_COMPONENT_SCHEMA = MQTT_COMPONENT_SCHEMA.extend({
|
||||
vol.Optional(CONF_COMMAND_TOPIC): vol.All(requires_component('mqtt'), subscribe_topic),
|
||||
Optional(CONF_COMMAND_TOPIC): All(requires_component('mqtt'), subscribe_topic),
|
||||
})
|
||||
|
||||
COMPONENT_SCHEMA = Schema({
|
||||
vol.Optional(CONF_SETUP_PRIORITY): float_
|
||||
Optional(CONF_SETUP_PRIORITY): float_
|
||||
})
|
||||
|
Reference in New Issue
Block a user