1
0
mirror of https://github.com/esphome/esphome.git synced 2025-03-16 07:38:17 +00:00
esphome/esphomeyaml/config.py

517 lines
18 KiB
Python
Raw Normal View History

2018-04-07 01:23:03 +02:00
from __future__ import print_function
2018-11-19 22:12:24 +01:00
from collections import OrderedDict
2018-04-07 01:23:03 +02:00
import importlib
2018-11-24 18:32:18 +01:00
import json
2018-04-07 01:23:03 +02:00
import logging
2018-12-04 20:06:32 +01:00
import re
2018-04-07 01:23:03 +02:00
2018-12-04 20:06:32 +01:00
from typing import List, Optional, Tuple, Set, Union, Any, Dict
2018-04-07 01:23:03 +02:00
import voluptuous as vol
2018-11-19 22:12:24 +01:00
from esphomeyaml import core, core_config, yaml_util
2018-11-28 21:33:24 +01:00
from esphomeyaml.components import substitutions
2018-12-04 20:06:32 +01:00
from esphomeyaml.const import CONF_ESPHOMEYAML, CONF_PLATFORM, ESP_PLATFORMS
2018-12-03 22:14:10 +01:00
from esphomeyaml.core import CORE, EsphomeyamlError, ConfigType
2018-12-04 20:06:32 +01:00
from esphomeyaml.helpers import color, indent
from esphomeyaml.util import safe_print
2018-04-07 01:23:03 +02:00
_LOGGER = logging.getLogger(__name__)
_COMPONENT_CACHE = {}
def get_component(domain):
if domain in _COMPONENT_CACHE:
return _COMPONENT_CACHE[domain]
path = 'esphomeyaml.components.{}'.format(domain)
try:
module = importlib.import_module(path)
except (ImportError, ValueError) as err:
2018-04-07 01:23:03 +02:00
_LOGGER.debug(err)
else:
_COMPONENT_CACHE[domain] = module
return module
_LOGGER.error("Unable to find component %s", domain)
return None
def get_platform(domain, platform):
return get_component("{}.{}".format(domain, platform))
def is_platform_component(component):
return hasattr(component, 'PLATFORM_SCHEMA')
def iter_components(config):
for domain, conf in config.iteritems():
if domain == CONF_ESPHOMEYAML:
2018-11-19 22:12:24 +01:00
yield CONF_ESPHOMEYAML, core_config, conf
continue
component = get_component(domain)
yield domain, component, conf
if is_platform_component(component):
for p_config in conf:
p_name = u"{}.{}".format(domain, p_config[CONF_PLATFORM])
platform = get_component(p_name)
yield p_name, platform, p_config
2018-04-07 01:23:03 +02:00
2018-12-04 20:06:32 +01:00
ConfigPath = List[Union[basestring, int]]
def _path_begins_with_(path, other): # type: (ConfigPath, ConfigPath) -> bool
if len(path) < len(other):
return False
return path[:len(other)] == other
def _path_begins_with(path, other): # type: (ConfigPath, ConfigPath) -> bool
ret = _path_begins_with_(path, other)
# print('_path_begins_with({}, {}) -> {}'.format(path, other, ret))
return ret
2018-04-07 01:23:03 +02:00
class Config(OrderedDict):
def __init__(self):
super(Config, self).__init__()
2018-12-04 20:06:32 +01:00
self.errors = [] # type: List[Tuple[basestring, ConfigPath]]
self.domains = [] # type: List[Tuple[ConfigPath, basestring]]
2018-04-07 01:23:03 +02:00
2018-12-04 20:06:32 +01:00
def add_error(self, message, path):
# type: (basestring, ConfigPath) -> None
2018-04-07 01:23:03 +02:00
if not isinstance(message, unicode):
message = unicode(message)
2018-12-04 20:06:32 +01:00
self.errors.append((message, path))
def add_domain(self, path, name):
# type: (ConfigPath, basestring) -> None
self.domains.append((path, name))
def lookup_domain(self, path):
# type: (ConfigPath) -> Optional[basestring]
best_len = 0
best_domain = None
for d_path, domain in self.domains:
if len(d_path) < best_len:
continue
if _path_begins_with(path, d_path):
best_len = len(d_path)
best_domain = domain
return best_domain
def is_in_error_path(self, path):
for _, p in self.errors:
if _path_begins_with(p, path):
return True
return False
def get_error_for_path(self, path):
for msg, p in self.errors:
if p == path:
return msg
return None
def nested_item(self, path):
data = self
for item_index in path:
try:
data = data[item_index]
except (KeyError, IndexError, TypeError):
return {}
return data
2018-04-07 01:23:03 +02:00
2018-12-04 20:06:32 +01:00
def iter_ids(config, path=None):
path = path or []
2018-06-02 22:22:20 +02:00
if isinstance(config, core.ID):
2018-12-04 20:06:32 +01:00
yield config, path
2018-06-02 22:22:20 +02:00
elif isinstance(config, core.Lambda):
for id in config.requires_ids:
2018-12-04 20:06:32 +01:00
yield id, path
2018-06-02 22:22:20 +02:00
elif isinstance(config, list):
for i, item in enumerate(config):
2018-12-04 20:06:32 +01:00
for result in iter_ids(item, path + [i]):
2018-06-02 22:22:20 +02:00
yield result
elif isinstance(config, dict):
for key, value in config.iteritems():
2018-12-04 20:06:32 +01:00
for result in iter_ids(value, path + [key]):
2018-06-02 22:22:20 +02:00
yield result
2018-12-04 20:06:32 +01:00
def do_id_pass(result): # type: (Config) -> None
2018-11-19 22:12:24 +01:00
from esphomeyaml.cpp_generator import MockObjClass
2018-12-04 20:06:32 +01:00
declare_ids = [] # type: List[Tuple[core.ID, ConfigPath]]
searching_ids = [] # type: List[Tuple[core.ID, ConfigPath]]
for id, path in iter_ids(result):
2018-06-02 22:22:20 +02:00
if id.is_declaration:
if id.id is not None and any(v[0].id == id.id for v in declare_ids):
2018-12-04 20:06:32 +01:00
result.add_error(u"ID {} redefined!".format(id.id), path)
2018-06-02 22:22:20 +02:00
continue
2018-12-04 20:06:32 +01:00
declare_ids.append((id, path))
2018-06-02 22:22:20 +02:00
else:
2018-12-04 20:06:32 +01:00
searching_ids.append((id, path))
2018-06-02 22:22:20 +02:00
# Resolve default ids after manual IDs
2018-12-04 20:06:32 +01:00
for id, _ in declare_ids:
2018-06-02 22:22:20 +02:00
id.resolve([v[0].id for v in declare_ids])
# Check searched IDs
2018-12-04 20:06:32 +01:00
for id, path in searching_ids:
if id.id is not None:
# manually declared
match = next((v[0] for v in declare_ids if v[0].id == id.id), None)
if match is None:
# No declared ID with this name
2018-12-04 20:06:32 +01:00
result.add_error("Couldn't find ID {}".format(id.id), path)
continue
if not isinstance(match.type, MockObjClass) or not isinstance(id.type, MockObjClass):
continue
if not match.type.inherits_from(id.type):
result.add_error("ID '{}' of type {} doesn't inherit from {}. Please double check "
"your ID is pointing to the correct value"
2018-12-04 20:06:32 +01:00
"".format(id.id, match.type, id.type), path)
2018-06-02 22:22:20 +02:00
if id.id is None and id.type is not None:
for v in declare_ids:
if v[0] is None or not isinstance(v[0].type, MockObjClass):
continue
inherits = v[0].type.inherits_from(id.type)
if inherits:
id.id = v[0].id
break
else:
2018-12-04 20:06:32 +01:00
result.add_error("Couldn't resolve ID for type {}".format(id.type), path)
2018-06-02 22:22:20 +02:00
2018-04-07 01:23:03 +02:00
def validate_config(config):
result = Config()
2018-12-04 20:06:32 +01:00
def _comp_error(ex, path):
# type: (vol.Invalid, List[basestring]) -> None
if isinstance(ex, vol.MultipleInvalid):
errors = ex.errors
else:
errors = [ex]
for e in errors:
path_ = path + e.path
domain = result.lookup_domain(path_) or ''
result.add_error(_format_vol_invalid(e, config, path, domain), path_)
2018-04-07 01:23:03 +02:00
2018-12-04 20:06:32 +01:00
skip_paths = list() # type: List[ConfigPath]
2018-11-21 20:48:19 +01:00
# Step 1: Load everything
2018-12-04 20:06:32 +01:00
result.add_domain([CONF_ESPHOMEYAML], CONF_ESPHOMEYAML)
for domain, conf in config.iteritems():
domain = str(domain)
2018-12-03 22:14:10 +01:00
if domain == CONF_ESPHOMEYAML or domain.startswith(u'.'):
2018-12-04 20:06:32 +01:00
skip_paths.append([domain])
continue
2018-12-04 20:06:32 +01:00
result.add_domain([domain], domain)
result[domain] = conf
if conf is None:
2018-12-04 20:06:32 +01:00
config[domain] = conf = {}
component = get_component(domain)
if component is None:
2018-12-04 20:06:32 +01:00
result.add_error(u"Component not found: {}".format(domain), [domain])
skip_paths.append([domain])
2018-11-21 20:48:19 +01:00
continue
success = True
dependencies = getattr(component, 'DEPENDENCIES', [])
for dependency in dependencies:
if dependency not in config:
2018-12-04 20:06:32 +01:00
result.add_error(u"Component {} requires component {}".format(domain, dependency), [domain])
2018-11-21 20:48:19 +01:00
success = False
if not success:
2018-12-04 20:06:32 +01:00
skip_paths.append([domain])
2018-11-21 20:48:19 +01:00
continue
success = True
conflicts_with = getattr(component, 'CONFLICTS_WITH', [])
for conflict in conflicts_with:
if conflict not in config:
result.add_error(u"Component {} cannot be used together with component {}"
2018-12-04 20:06:32 +01:00
u"".format(domain, conflict), [domain])
2018-11-21 20:48:19 +01:00
success = False
if not success:
2018-12-04 20:06:32 +01:00
skip_paths.append([domain])
2018-11-21 20:48:19 +01:00
continue
esp_platforms = getattr(component, 'ESP_PLATFORMS', ESP_PLATFORMS)
if CORE.esp_platform not in esp_platforms:
2018-12-04 20:06:32 +01:00
result.add_error(u"Component {} doesn't support {}.".format(domain, CORE.esp_platform), [domain])
skip_paths.append([domain])
continue
if not hasattr(component, 'PLATFORM_SCHEMA'):
continue
2018-12-04 20:06:32 +01:00
if not isinstance(conf, list) and conf:
result[domain] = conf = [conf]
2018-12-03 22:14:10 +01:00
for i, p_config in enumerate(conf):
if not isinstance(p_config, dict):
2018-12-04 20:06:32 +01:00
result.add_error(u"Platform schemas must have 'platform:' key", [domain, i])
skip_paths.append([domain, i])
continue
2018-12-04 20:06:32 +01:00
p_name = p_config.get('platform')
if p_name is None:
2018-12-04 20:06:32 +01:00
result.add_error(u"No platform specified for {}".format(domain), [domain, i])
skip_paths.append([domain, i])
continue
p_domain = u'{}.{}'.format(domain, p_name)
2018-12-04 20:06:32 +01:00
result.add_domain([domain, i], p_domain)
platform = get_platform(domain, p_name)
if platform is None:
2018-12-04 20:06:32 +01:00
result.add_error(u"Platform not found: '{}'".format(p_domain), [domain, i])
skip_paths.append([domain, i])
2018-11-21 20:48:19 +01:00
continue
success = True
dependencies = getattr(platform, 'DEPENDENCIES', [])
for dependency in dependencies:
if dependency not in config:
2018-12-04 20:06:32 +01:00
result.add_error(u"Platform {} requires component {}".format(p_domain, dependency),
[domain, i])
2018-11-21 20:48:19 +01:00
success = False
if not success:
2018-12-04 20:06:32 +01:00
skip_paths.append([domain, i])
2018-11-21 20:48:19 +01:00
continue
success = True
conflicts_with = getattr(platform, 'CONFLICTS_WITH', [])
for conflict in conflicts_with:
if conflict not in config:
result.add_error(u"Platform {} cannot be used together with component {}"
2018-12-04 20:06:32 +01:00
u"".format(p_domain, conflict), [domain, i])
2018-11-21 20:48:19 +01:00
success = False
if not success:
2018-12-04 20:06:32 +01:00
skip_paths.append([domain, i])
2018-11-21 20:48:19 +01:00
continue
esp_platforms = getattr(platform, 'ESP_PLATFORMS', ESP_PLATFORMS)
if CORE.esp_platform not in esp_platforms:
2018-12-04 20:06:32 +01:00
result.add_error(u"Platform {} doesn't support {}.".format(p_domain, CORE.esp_platform),
[domain, i])
skip_paths.append([domain, i])
continue
# Step 2: Validate configuration
2018-04-07 01:23:03 +02:00
try:
2018-12-04 20:06:32 +01:00
result[CONF_ESPHOMEYAML] = config[CONF_ESPHOMEYAML]
2018-09-23 18:58:41 +02:00
result[CONF_ESPHOMEYAML] = core_config.CONFIG_SCHEMA(config[CONF_ESPHOMEYAML])
2018-04-07 01:23:03 +02:00
except vol.Invalid as ex:
2018-12-04 20:06:32 +01:00
_comp_error(ex, [CONF_ESPHOMEYAML])
2018-04-07 01:23:03 +02:00
for domain, conf in config.iteritems():
domain = str(domain)
2018-12-04 20:06:32 +01:00
if [domain] in skip_paths:
2018-04-07 01:23:03 +02:00
continue
2018-11-21 20:48:19 +01:00
component = get_component(domain)
2018-04-07 01:23:03 +02:00
if hasattr(component, 'CONFIG_SCHEMA'):
try:
validated = component.CONFIG_SCHEMA(conf)
result[domain] = validated
except vol.Invalid as ex:
2018-12-04 20:06:32 +01:00
_comp_error(ex, [domain])
2018-04-07 01:23:03 +02:00
continue
if not hasattr(component, 'PLATFORM_SCHEMA'):
continue
2018-12-04 20:06:32 +01:00
for i, p_config in enumerate(conf):
if [domain, i] in skip_paths:
2018-05-20 12:41:52 +02:00
continue
2018-12-04 20:06:32 +01:00
p_name = p_config['platform']
2018-11-21 20:48:19 +01:00
platform = get_platform(domain, p_name)
2018-05-20 12:41:52 +02:00
2018-12-04 20:06:32 +01:00
if hasattr(platform, 'PLATFORM_SCHEMA'):
2018-04-07 01:23:03 +02:00
try:
p_validated = platform.PLATFORM_SCHEMA(p_config)
except vol.Invalid as ex:
2018-12-04 20:06:32 +01:00
_comp_error(ex, [domain, i])
2018-04-07 01:23:03 +02:00
continue
2018-12-04 20:06:32 +01:00
result[domain][i] = p_validated
2018-06-02 22:22:20 +02:00
do_id_pass(result)
2018-04-07 01:23:03 +02:00
return result
2018-11-24 18:32:18 +01:00
def _nested_getitem(data, path):
for item_index in path:
try:
data = data[item_index]
except (KeyError, IndexError, TypeError):
return None
return data
def humanize_error(config, validation_error):
offending_item_summary = _nested_getitem(config, validation_error.path)
if isinstance(offending_item_summary, dict):
2018-12-04 20:06:32 +01:00
try:
offending_item_summary = json.dumps(offending_item_summary)
except (TypeError, ValueError):
pass
validation_error = unicode(validation_error)
m = re.match(r'^(.*)\s*for dictionary value @.*$', validation_error)
if m is not None:
validation_error = m.group(1)
validation_error = validation_error.strip()
if not validation_error.endswith(u'.'):
validation_error += u'.'
return u"{} Got '{}'".format(validation_error, offending_item_summary)
def _format_vol_invalid(ex, config, path, domain):
# type: (vol.Invalid, ConfigType, ConfigPath, basestring) -> unicode
message = u''
2018-04-07 01:23:03 +02:00
if u'extra keys not allowed' in ex.error_message:
2018-12-04 20:06:32 +01:00
message += u'[{}] is an invalid option for [{}].'.format(ex.path[-1], domain)
2018-11-24 18:32:18 +01:00
elif u'required key not provided' in ex.error_message:
2018-12-04 20:06:32 +01:00
message += u"'{}' is a required option for [{}].".format(ex.path[-1], domain)
2018-04-07 01:23:03 +02:00
else:
2018-12-04 20:06:32 +01:00
message += u'{}.'.format(humanize_error(_nested_getitem(config, path), ex))
2018-04-07 01:23:03 +02:00
return message
2018-11-19 22:12:24 +01:00
def load_config():
2018-04-07 01:23:03 +02:00
try:
2018-11-19 22:12:24 +01:00
config = yaml_util.load_yaml(CORE.config_path)
2018-04-07 01:23:03 +02:00
except OSError:
2018-11-19 22:12:24 +01:00
raise EsphomeyamlError(u"Could not read configuration file at {}".format(CORE.config_path))
CORE.raw_config = config
2018-11-28 21:33:24 +01:00
config = substitutions.do_substitution_pass(config)
2018-09-23 18:58:41 +02:00
core_config.preload_core_config(config)
2018-04-07 01:23:03 +02:00
try:
result = validate_config(config)
2018-11-19 22:12:24 +01:00
except EsphomeyamlError:
2018-06-02 22:22:20 +02:00
raise
except Exception:
2018-06-02 22:22:20 +02:00
_LOGGER.error(u"Unexpected exception while reading configuration:")
2018-04-07 01:23:03 +02:00
raise
return result
2018-11-28 21:33:24 +01:00
def line_info(obj):
2018-04-07 01:23:03 +02:00
"""Display line config source."""
if hasattr(obj, '__config_file__'):
return color('cyan', "[source {}:{}]"
2018-11-28 21:33:24 +01:00
.format(obj.__config_file__, obj.__line__ or '?'))
2018-12-04 20:06:32 +01:00
return None
def _print_on_next_line(obj):
if isinstance(obj, (list, tuple, dict)):
return True
if isinstance(obj, str):
return len(obj) > 80
if isinstance(obj, core.Lambda):
return len(obj.value) > 80
return False
def dump_dict(config, path, at_root=True):
# type: (Config, ConfigPath, bool) -> Tuple[unicode, bool]
conf = config.nested_item(path)
ret = u''
multiline = False
if at_root:
error = config.get_error_for_path(path)
if error is not None:
ret += u'\n' + color('bold_red', error) + u'\n'
if isinstance(conf, (list, tuple)):
multiline = True
for i, obj in enumerate(conf):
path_ = path + [i]
error = config.get_error_for_path(path_)
if error is not None:
ret += u'\n' + color('bold_red', error) + u'\n'
sep = u'- '
if config.is_in_error_path(path_):
sep = color('red', sep)
msg, _ = dump_dict(config, path_, at_root=False)
msg = indent(msg)
inf = line_info(config.nested_item(path_))
if inf is not None:
msg = inf + u'\n' + msg
elif msg:
msg = msg[2:]
ret += sep + msg + u'\n'
elif isinstance(conf, dict):
multiline = True
for k, v in conf.iteritems():
path_ = path + [k]
error = config.get_error_for_path(path_)
if error is not None:
ret += u'\n' + color('bold_red', error) + u'\n'
st = u'{}: '.format(k)
if config.is_in_error_path(path_):
st = color('red', st)
msg, m = dump_dict(config, path_, at_root=False)
inf = line_info(config.nested_item(path_))
if m:
msg = u'\n' + indent(msg)
if inf is not None:
if m:
msg = u' ' + inf + msg
else:
msg = msg + u' ' + inf
ret += st + msg + u'\n'
elif isinstance(conf, str):
if len(conf) > 80:
ret += u'|-\n'
conf = indent(conf)
error = config.get_error_for_path(path)
col = 'bold_red' if error else 'white'
ret += color(col, unicode(conf))
elif isinstance(conf, core.Lambda):
ret += u'|-\n'
conf = indent(unicode(conf.value))
error = config.get_error_for_path(path)
col = 'bold_red' if error else 'white'
ret += color(col, conf)
else:
error = config.get_error_for_path(path)
col = 'bold_red' if error else 'white'
ret += color(col, unicode(conf))
multiline = u'\n' in ret
return ret, multiline
2018-04-07 01:23:03 +02:00
2018-11-19 22:12:24 +01:00
def read_config():
2018-06-02 22:22:20 +02:00
_LOGGER.info("Reading configuration...")
try:
2018-11-19 22:12:24 +01:00
res = load_config()
except EsphomeyamlError as err:
2018-04-11 18:29:21 +02:00
_LOGGER.error(u"Error while reading config: %s", err)
return None
2018-12-04 20:06:32 +01:00
if res.errors:
safe_print(color('bold_red', u"Failed config"))
safe_print('')
for path, domain in res.domains:
if not res.is_in_error_path(path):
continue
safe_print(color('bold_red', u'{}:'.format(domain)) + u' ' + (line_info(res.nested_item(path)) or u''))
safe_print(indent(dump_dict(res, path)[0]))
2018-04-07 01:23:03 +02:00
return None
2018-08-13 19:11:33 +02:00
return OrderedDict(res)