From 2f4705e2a3b52806970eb1b8e19860801e3f676e Mon Sep 17 00:00:00 2001
From: Otto Winter <otto@otto-winter.com>
Date: Tue, 4 Dec 2018 20:30:16 +0100
Subject: [PATCH] Improve error messages

---
 esphomeyaml/config.py            | 49 +++++++++++++++++++++++++++-----
 esphomeyaml/config_validation.py |  4 +--
 2 files changed, 44 insertions(+), 9 deletions(-)

diff --git a/esphomeyaml/config.py b/esphomeyaml/config.py
index 452112713f..97cca2245f 100644
--- a/esphomeyaml/config.py
+++ b/esphomeyaml/config.py
@@ -91,6 +91,9 @@ class Config(OrderedDict):
         # type: (ConfigPath, basestring) -> None
         self.domains.append((path, name))
 
+    def remove_domain(self, path, name):
+        self.domains.remove((path, name))
+
     def lookup_domain(self, path):
         # type: (ConfigPath) -> Optional[basestring]
         best_len = 0
@@ -111,7 +114,7 @@ class Config(OrderedDict):
 
     def get_error_for_path(self, path):
         for msg, p in self.errors:
-            if p == path:
+            if self.nested_item_path(p) == path:
                 return msg
         return None
 
@@ -124,6 +127,17 @@ class Config(OrderedDict):
                 return {}
         return data
 
+    def nested_item_path(self, path):
+        data = self
+        part = []
+        for item_index in path:
+            try:
+                data = data[item_index]
+            except (KeyError, IndexError, TypeError):
+                return part
+            part.append(item_index)
+        return part
+
 
 def iter_ids(config, path=None):
     path = path or []
@@ -252,6 +266,8 @@ def validate_config(config):
         if not hasattr(component, 'PLATFORM_SCHEMA'):
             continue
 
+        result.remove_domain([domain], domain)
+
         if not isinstance(conf, list) and conf:
             result[domain] = conf = [conf]
 
@@ -374,9 +390,17 @@ def _format_vol_invalid(ex, config, path, domain):
     # type: (vol.Invalid, ConfigType, ConfigPath, basestring) -> unicode
     message = u''
     if u'extra keys not allowed' in ex.error_message:
-        message += u'[{}] is an invalid option for [{}].'.format(ex.path[-1], domain)
+        try:
+            paren = ex.path[-2]
+        except IndexError:
+            paren = domain
+        message += u'[{}] is an invalid option for [{}].'.format(ex.path[-1], paren)
     elif u'required key not provided' in ex.error_message:
-        message += u"'{}' is a required option for [{}].".format(ex.path[-1], domain)
+        try:
+            paren = ex.path[-2]
+        except IndexError:
+            paren = domain
+        message += u"'{}' is a required option for [{}].".format(ex.path[-1], paren)
     else:
         message += u'{}.'.format(humanize_error(_nested_getitem(config, path), ex))
 
@@ -434,6 +458,10 @@ def dump_dict(config, path, at_root=True):
 
     if isinstance(conf, (list, tuple)):
         multiline = True
+        if not conf:
+            ret += u'[]'
+            multiline = False
+
         for i, obj in enumerate(conf):
             path_ = path + [i]
             error = config.get_error_for_path(path_)
@@ -453,6 +481,10 @@ def dump_dict(config, path, at_root=True):
             ret += sep + msg + u'\n'
     elif isinstance(conf, dict):
         multiline = True
+        if not conf:
+            ret += u'{}'
+            multiline = False
+
         for k, v in conf.iteritems():
             path_ = path + [k]
             error = config.get_error_for_path(path_)
@@ -475,18 +507,21 @@ def dump_dict(config, path, at_root=True):
                     msg = msg + u' ' + inf
             ret += st + msg + u'\n'
     elif isinstance(conf, str):
+        if not conf:
+            conf += u"''"
+
         if len(conf) > 80:
-            ret += u'|-\n'
-            conf = indent(conf)
+            conf = u'|-\n' + 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))
+        conf = u'!lambda |-\n' + indent(unicode(conf.value))
         error = config.get_error_for_path(path)
         col = 'bold_red' if error else 'white'
         ret += color(col, conf)
+    elif conf is None:
+        pass
     else:
         error = config.get_error_for_path(path)
         col = 'bold_red' if error else 'white'
diff --git a/esphomeyaml/config_validation.py b/esphomeyaml/config_validation.py
index a19440fefe..e0a05029ee 100644
--- a/esphomeyaml/config_validation.py
+++ b/esphomeyaml/config_validation.py
@@ -321,11 +321,11 @@ def time_period_str_unit(value):
 
     match = re.match(r"^([-+]?[0-9]*\.?[0-9]*)\s*(\w*)$", value)
 
-    if match is None or match.group(2) not in unit_to_kwarg:
+    if match is None:
         raise vol.Invalid(u"Expected time period with unit, "
                           u"got {}".format(value))
+    kwarg = unit_to_kwarg[one_of(*unit_to_kwarg)(match.group(2))]
 
-    kwarg = unit_to_kwarg[match.group(2)]
     return TimePeriod(**{kwarg: float(match.group(1))})