1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-06 05:12:21 +01:00

Cleanup dashboard JS (#491)

* Cleanup dashboard JS

* Add vscode

* Save start_mark/end_mark

* Updates

* Updates

* Remove need for cv.nameable

It's a bit hacky but removes so much bloat from integrations

* Add enum helper

* Document APIs, and Improvements

* Fixes

* Fixes

* Update PULL_REQUEST_TEMPLATE.md

* Updates

* Updates

* Updates
This commit is contained in:
Otto Winter
2019-04-22 21:56:30 +02:00
committed by GitHub
parent 6682c43dfa
commit 8e75980ebd
359 changed files with 4395 additions and 4223 deletions

View File

@@ -5,6 +5,7 @@ from __future__ import print_function
import logging
import os
import re
from contextlib import contextmanager
import uuid as uuid_
from datetime import datetime
@@ -14,9 +15,10 @@ 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_RETAIN, CONF_SETUP_PRIORITY, CONF_STATE_TOPIC, CONF_TOPIC, \
CONF_HOUR, CONF_MINUTE, CONF_SECOND, CONF_VALUE
CONF_HOUR, CONF_MINUTE, CONF_SECOND, CONF_VALUE, CONF_UPDATE_INTERVAL, CONF_TYPE_ID
from esphome.core import CORE, HexInt, IPAddress, Lambda, TimePeriod, TimePeriodMicroseconds, \
TimePeriodMilliseconds, TimePeriodSeconds, TimePeriodMinutes
from esphome.helpers import list_starts_with
from esphome.py_compat import integer_types, string_types, text_type, IS_PY2
from esphome.voluptuous_schema import _Schema
@@ -25,8 +27,6 @@ _LOGGER = logging.getLogger(__name__)
# pylint: disable=invalid-name
Schema = _Schema
Optional = vol.Optional
Required = vol.Required
All = vol.All
Coerce = vol.Coerce
Range = vol.Range
@@ -39,14 +39,8 @@ 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))
UNDEFINED = vol.UNDEFINED
RequiredFieldInvalid = vol.RequiredFieldInvalid
ALLOWED_NAME_CHARS = u'abcdefghijklmnopqrstuvwxyz0123456789_'
@@ -71,6 +65,41 @@ RESERVED_IDS = [
]
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.
"""
def __init__(self, key, default=UNDEFINED):
super(Optional, self).__init__(key, default=default)
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.
"""
def __init__(self, key):
super(Required, self).__init__(key)
def check_not_templatable(value):
if isinstance(value, Lambda):
raise Invalid("This option is not templatable!")
def alphanumeric(value):
if value is None:
raise Invalid("string value is None")
@@ -90,46 +119,77 @@ def valid_name(value):
def string(value):
"""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
"60.0" (string). For values where this could be a problem `string_string` has to be used.
"""
check_not_templatable(value)
if isinstance(value, (dict, list)):
raise Invalid("string value cannot be dictionary or list.")
if isinstance(value, text_type):
return value
if value is not None:
return text_type(value)
raise Invalid("string value is None")
def string_strict(value):
"""Strictly only allow strings."""
if isinstance(value, string_types):
"""Like string, but only allows strings, and does not automatically convert other types to
strings."""
check_not_templatable(value)
if isinstance(value, text_type):
return value
if isinstance(value, string_types):
return text_type(value)
raise Invalid("Must be string, got {}. did you forget putting quotes "
"around the value?".format(type(value)))
def icon(value):
"""Validate icon."""
"""Validate that a given config value is a valid icon."""
value = string_strict(value)
if not value:
return value
if value.startswith('mdi:'):
return value
raise Invalid('Icons should start with prefix "mdi:"')
def boolean(value):
"""Validate and coerce a boolean value."""
"""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
if isinstance(value, str):
value = value.lower()
if value in ('1', 'true', 'yes', 'on', 'enable'):
if value in ('true', 'yes', 'on', 'enable'):
return True
if value in ('0', 'false', 'no', 'off', 'disable'):
if value in ('false', 'no', 'off', 'disable'):
return False
raise Invalid('invalid boolean value {}'.format(value))
return bool(value)
raise Invalid(u"Expected boolean value, but cannot convert {} to a boolean. "
u"Please use 'true' or 'false'".format(value))
def ensure_list(*validators):
"""Wrap value in list if it is not one."""
"""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.
"""
user = All(*validators)
def validator(value):
check_not_templatable(value)
if value is None or (isinstance(value, dict) and not value):
return []
if not isinstance(value, list):
@@ -138,56 +198,80 @@ def ensure_list(*validators):
errs = []
for i, val in enumerate(value):
try:
ret.append(user(val))
except vol.MultipleInvalid as err:
err.prepend([i])
with prepend_path([i]):
ret.append(user(val))
except MultipleInvalid as err:
errs.extend(err.errors)
except Invalid as err:
err.prepend([i])
errs.append(err)
if errs:
raise vol.MultipleInvalid(errs)
raise MultipleInvalid(errs)
return ret
return validator
def ensure_list_not_empty(value):
if isinstance(value, list):
return value
return [value]
def ensure_dict(value):
if value is None:
return {}
if not isinstance(value, dict):
raise Invalid("Expected a dictionary")
return value
def hex_int_(value):
if isinstance(value, integer_types):
return HexInt(value)
value = string_strict(value).lower()
if value.startswith('0x'):
return HexInt(int(value, 16))
return HexInt(int(value))
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))
def int_(value):
"""Validate that the config option is an integer.
Automatically also converts strings to ints.
"""
check_not_templatable(value)
if isinstance(value, integer_types):
return value
value = string_strict(value).lower()
base = 10
if value.startswith('0x'):
return int(value, 16)
return int(value)
base = 16
try:
return int(value, base)
except ValueError:
raise Invalid(u"Expected integer, but cannot parse {} as an integer".format(value))
hex_int = Coerce(hex_int_)
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:
assert isinstance(min, integer_types)
if max is not None:
assert isinstance(max, integer_types)
return All(int_, Range(min=min, max=max, min_included=min_included, max_included=max_included))
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."""
return All(hex_int,
Range(min=min, max=max, min_included=min_included, max_included=max_included))
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))
return All(float_, Range(min=min, max=max, min_included=min_included,
max_included=max_included))
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)
def validate_id_name(value):
"""Validate that the given value would be a valid C++ identifier name."""
value = string(value)
if not value:
raise Invalid("ID must not be empty")
@@ -205,18 +289,28 @@ def validate_id_name(value):
return value
def use_variable_id(type):
def use_id(type):
"""Declare that this configuration option should point to an ID with the given type."""
def validator(value):
check_not_templatable(value)
if value is None:
return core.ID(None, is_declaration=False, type=type)
if isinstance(value, core.ID) and value.is_declaration is False and value.type is type:
return value
return core.ID(validate_id_name(value), is_declaration=False, type=type)
return validator
def declare_variable_id(type):
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.
"""
def validator(value):
check_not_templatable(value)
if value is None:
return core.ID(None, is_declaration=True, type=type)
@@ -226,17 +320,26 @@ def declare_variable_id(type):
def templatable(other_validators):
"""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)
def validator(value):
if isinstance(value, Lambda):
return value
if isinstance(other_validators, dict):
return Schema(other_validators)(value)
return other_validators(value)
return schema(value)
return schema(value)
return validator
def only_on(platforms):
"""Validate that this option can only be specified on the given ESP platforms."""
if not isinstance(platforms, list):
platforms = [platforms]
@@ -255,7 +358,7 @@ only_on_esp8266 = only_on('ESP8266')
# Adapted from:
# https://github.com/alecthomas/voluptuous/issues/115#issuecomment-144464666
def has_at_least_one_key(*keys):
"""Validate that at least one key exists."""
"""Validate that at least one of the given keys exist in the config."""
def validate(obj):
"""Test keys exist in dict."""
@@ -270,6 +373,7 @@ def has_at_least_one_key(*keys):
def has_exactly_one_key(*keys):
"""Validate that exactly one of the given keys exist in the config."""
def validate(obj):
if not isinstance(obj, dict):
raise Invalid('expected dictionary')
@@ -285,6 +389,7 @@ def has_exactly_one_key(*keys):
def has_at_most_one_key(*keys):
"""Validate that at most one of the given keys exist in the config."""
def validate(obj):
if not isinstance(obj, dict):
raise Invalid('expected dictionary')
@@ -300,17 +405,17 @@ 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 = All(
dict, Schema({
'days': float_,
'hours': float_,
'minutes': float_,
'seconds': float_,
'milliseconds': float_,
'microseconds': float_,
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))
has_at_least_one_key('days', 'hours', 'minutes', 'seconds', 'milliseconds', 'microseconds'),
lambda value: TimePeriod(**value)
)
def time_period_str_colon(value):
@@ -338,6 +443,8 @@ def time_period_str_colon(value):
def time_period_str_unit(value):
"""Validate and transform time period with time unit and integer value."""
check_not_templatable(value)
if isinstance(value, int):
raise Invalid("Don't know what '{0}' means as it has no time *unit*! Did you mean "
"'{0}s'?".format(value))
@@ -682,6 +789,7 @@ def mqtt_qos(value):
def requires_component(comp):
"""Validate that this option can only be specified when the component `comp` is loaded."""
def validator(value):
if comp not in CORE.raw_config:
raise Invalid("This option requires component {}".format(comp))
@@ -690,16 +798,20 @@ def requires_component(comp):
return validator
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))
uint8_t = int_range(min=0, max=255)
uint16_t = int_range(min=0, max=65535)
uint32_t = int_range(min=0, max=4294967295)
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)
i2c_address = hex_uint8_t
def percentage(value):
"""Validate that the value is a percentage.
The resulting value is an integer in the range 0.0 to 1.0.
"""
value = possibly_negative_percentage(value)
return zero_to_one_float(value)
@@ -728,6 +840,9 @@ def percentage_int(value):
def invalid(message):
"""Mark this value as invalid. Each time *any* value is passed here it will result in a
validation error with the given message.
"""
def validator(value):
raise Invalid(message)
@@ -738,14 +853,56 @@ def valid(value):
return value
@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
def one_of(*values, **kwargs):
"""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.
"""
options = u', '.join(u"'{}'".format(x) for x in values)
lower = kwargs.get('lower', False)
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', ' ')
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', ' ')
if kwargs:
raise ValueError
def validator(value):
if string_:
@@ -760,13 +917,44 @@ def one_of(*values, **kwargs):
if upper:
value = Upper(value)
if value not in values:
raise Invalid(u"Unknown value '{}', must be one of {}".format(value, options))
import difflib
options_ = [text_type(x) for x in values]
option = text_type(value)
matches = difflib.get_close_matches(option, options_)
if matches:
raise Invalid(u"Unknown value '{}', did you mean {}?"
u"".format(value, u", ".join(u"'{}'".format(x) for x in matches)))
raise Invalid(u"Unknown value '{}', valid options are {}.".format(value, options))
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)
def validator(value):
from esphome.yaml_util import make_data_base
value = make_data_base(one_of_validator(value))
cls = value.__class__
value.__class__ = cls.__class__(cls.__name__ + "Enum", (cls, core.EnumValue), {})
value.enum_value = mapping[value]
return value
return validator
def lambda_(value):
"""Coerce this configuration option to a lambda."""
if isinstance(value, Lambda):
return value
return Lambda(string_strict(value))
@@ -792,23 +980,25 @@ def dimensions(value):
def directory(value):
value = string(value)
path = CORE.relative_path(value)
path = CORE.relative_config_path(value)
if not os.path.exists(path):
raise Invalid(u"Could not find directory '{}'. Please make sure it exists.".format(
path))
raise Invalid(u"Could not find directory '{}'. Please make sure it exists (full path: {})."
u"".format(path, os.path.abspath(path)))
if not os.path.isdir(path):
raise Invalid(u"Path '{}' is not a directory.".format(path))
raise Invalid(u"Path '{}' is not a directory (full path: {})."
u"".format(path, os.path.abspath(path)))
return value
def file_(value):
value = string(value)
path = CORE.relative_path(value)
path = CORE.relative_config_path(value)
if not os.path.exists(path):
raise Invalid(u"Could not find file '{}'. Please make sure it exists.".format(
path))
raise Invalid(u"Could not find file '{}'. Please make sure it exists (full path: {})."
u"".format(path, os.path.abspath(path)))
if not os.path.isfile(path):
raise Invalid(u"Path '{}' is not a file.".format(path))
raise Invalid(u"Path '{}' is not a file (full path: {})."
u"".format(path, os.path.abspath(path)))
return value
@@ -816,6 +1006,10 @@ ENTITY_ID_CHARACTERS = 'abcdefghijklmnopqrstuvwxyz0123456789_'
def entity_id(value):
"""Validate that this option represents a valid Home Assistant entity id.
Should only be used for 'homeassistant' platforms.
"""
value = string_strict(value).lower()
if value.count('.') != 1:
raise Invalid("Entity ID must have exactly one dot in it")
@@ -827,20 +1021,30 @@ def entity_id(value):
def extract_keys(schema):
"""Extract the names of the keys from the given schema."""
if isinstance(schema, Schema):
schema = schema.schema
assert isinstance(schema, dict)
keys = list(schema.keys())
keys = []
for skey in list(schema.keys()):
if isinstance(skey, string_types):
keys.append(skey)
elif isinstance(skey, vol.Marker) and isinstance(skey.schema, string_types):
keys.append(skey.schema)
else:
raise ValueError()
keys.sort()
return keys
class GenerateID(Optional):
"""Mark this key as being an auto-generated ID key."""
def __init__(self, key=CONF_ID):
super(GenerateID, self).__init__(key, default=lambda: None)
class SplitDefault(Optional):
"""Mark this key to have a split default for ESP8266/ESP32."""
def __init__(self, key, esp8266=vol.UNDEFINED, esp32=vol.UNDEFINED):
super(SplitDefault, self).__init__(key)
self._esp8266_default = vol.default_factory(esp8266)
@@ -861,6 +1065,7 @@ class SplitDefault(Optional):
class OnlyWith(Optional):
"""Set the default value only if the given component is loaded."""
def __init__(self, key, component, default=None):
super(OnlyWith, self).__init__(key)
self._component = component
@@ -878,56 +1083,70 @@ class OnlyWith(Optional):
pass
def nameable(*schemas):
def validator(config):
config = All(*schemas)(config)
if CONF_NAME not in config and CONF_ID not in config:
print(config)
def _nameable_validator(config):
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:
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:
print(config)
raise Invalid("At least one of 'id:' or 'name:' is required!")
config[CONF_NAME] = id.id
config[CONF_INTERNAL] = True
return config
config[CONF_NAME] = id.id
config[CONF_INTERNAL] = True
return config
return validator
return config
def validate_registry_entry(name, registry, ignore_keys):
def ensure_schema(schema):
if not isinstance(schema, vol.Schema):
return Schema(schema)
return schema
def validate_registry_entry(name, registry):
base_schema = ensure_schema(registry.base_schema).extend({
Optional(CONF_TYPE_ID): valid,
}, extra=ALLOW_EXTRA)
ignore_keys = extract_keys(base_schema)
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)
value = base_schema(value)
key = next((x for x in value if x not in ignore_keys), None)
if key is None:
raise Invalid(u"Key missing from {}! Got {}".format(name, item))
raise Invalid(u"Key missing from {}! Got {}".format(name, value))
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)
raise Invalid(u"Unable to find {} with the name '{}'".format(name, key), [key])
key2 = next((x for x in value 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
raise 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))
if value[key] is None:
value[key] = {}
registry_entry = registry[key]
with prepend_path([key]):
value[key] = registry_entry.schema(value[key])
if registry_entry.type_id is not None:
my_base_schema = base_schema.extend({
GenerateID(CONF_TYPE_ID): declare_id(registry_entry.type_id)
})
value = my_base_schema(value)
return value
return validator
def validate_registry(name, registry, ignore_keys):
return ensure_list(validate_registry_entry(name, registry, ignore_keys))
def validate_registry(name, registry):
return ensure_list(validate_registry_entry(name, registry))
def maybe_simple_value(*validators):
@@ -956,6 +1175,7 @@ MQTT_COMPONENT_SCHEMA = Schema({
Any(None, MQTT_COMPONENT_AVAILABILITY_SCHEMA)),
Optional(CONF_INTERNAL): boolean,
})
MQTT_COMPONENT_SCHEMA.add_extra(_nameable_validator)
MQTT_COMMAND_COMPONENT_SCHEMA = MQTT_COMPONENT_SCHEMA.extend({
Optional(CONF_COMMAND_TOPIC): All(requires_component('mqtt'), subscribe_topic),
@@ -964,3 +1184,19 @@ MQTT_COMMAND_COMPONENT_SCHEMA = MQTT_COMPONENT_SCHEMA.extend({
COMPONENT_SCHEMA = Schema({
Optional(CONF_SETUP_PRIORITY): float_
})
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:
return COMPONENT_SCHEMA.extend({
Required(CONF_UPDATE_INTERVAL): default_update_interval,
})
assert isinstance(default_update_interval, string_types)
return COMPONENT_SCHEMA.extend({
Optional(CONF_UPDATE_INTERVAL, default=default_update_interval): update_interval,
})