mirror of
https://github.com/esphome/esphome.git
synced 2025-09-08 06:12:20 +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:
@@ -1,9 +1,10 @@
|
||||
from __future__ import print_function
|
||||
|
||||
from collections import OrderedDict
|
||||
import collections
|
||||
import importlib
|
||||
import logging
|
||||
import re
|
||||
import os.path
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -13,7 +14,7 @@ from esphome.const import CONF_ESPHOME, CONF_PLATFORM, ESP_PLATFORMS
|
||||
from esphome.core import CORE, EsphomeError
|
||||
from esphome.helpers import color, indent
|
||||
from esphome.py_compat import text_type
|
||||
from esphome.util import safe_print
|
||||
from esphome.util import safe_print, OrderedDict
|
||||
|
||||
# pylint: disable=unused-import, wrong-import-order
|
||||
from typing import List, Optional, Tuple, Union # noqa
|
||||
@@ -26,46 +27,135 @@ _LOGGER = logging.getLogger(__name__)
|
||||
_COMPONENT_CACHE = {}
|
||||
|
||||
|
||||
def get_component(domain):
|
||||
class ComponentManifest(object):
|
||||
def __init__(self, module, base_components_path, is_core=False, is_platform=False):
|
||||
self.module = module
|
||||
self._is_core = is_core
|
||||
self.is_platform = is_platform
|
||||
self.base_components_path = base_components_path
|
||||
|
||||
@property
|
||||
def is_platform_component(self):
|
||||
return getattr(self.module, 'IS_PLATFORM_COMPONENT', False)
|
||||
|
||||
@property
|
||||
def config_schema(self):
|
||||
return getattr(self.module, 'CONFIG_SCHEMA', None)
|
||||
|
||||
@property
|
||||
def is_multi_conf(self):
|
||||
return getattr(self.module, 'MULTI_CONF', False)
|
||||
|
||||
@property
|
||||
def to_code(self):
|
||||
return getattr(self.module, 'to_code', None)
|
||||
|
||||
@property
|
||||
def esp_platforms(self):
|
||||
return getattr(self.module, 'ESP_PLATFORMS', ESP_PLATFORMS)
|
||||
|
||||
@property
|
||||
def dependencies(self):
|
||||
return getattr(self.module, 'DEPENDENCIES', [])
|
||||
|
||||
@property
|
||||
def conflicts_with(self):
|
||||
return getattr(self.module, 'CONFLICTS_WITH', [])
|
||||
|
||||
@property
|
||||
def auto_load(self):
|
||||
return getattr(self.module, 'AUTO_LOAD', [])
|
||||
|
||||
@property
|
||||
def to_code_priority(self):
|
||||
return getattr(self.module, 'TO_CODE_PRIORITY', [])
|
||||
|
||||
def _get_flags_set(self, name, config):
|
||||
if not hasattr(self.module, name):
|
||||
return set()
|
||||
obj = getattr(self.module, name)
|
||||
if callable(obj):
|
||||
obj = obj(config)
|
||||
if obj is None:
|
||||
return set()
|
||||
if not isinstance(obj, (list, tuple, set)):
|
||||
obj = [obj]
|
||||
return set(obj)
|
||||
|
||||
@property
|
||||
def source_files(self):
|
||||
if self._is_core:
|
||||
core_p = os.path.abspath(os.path.join(os.path.dirname(__file__), 'core'))
|
||||
source_files = core.find_source_files(os.path.join(core_p, 'dummy'))
|
||||
ret = {}
|
||||
for f in source_files:
|
||||
ret['esphome/core/{}'.format(f)] = os.path.join(core_p, f)
|
||||
return ret
|
||||
|
||||
source_files = core.find_source_files(self.module.__file__)
|
||||
ret = {}
|
||||
# Make paths absolute
|
||||
directory = os.path.abspath(os.path.dirname(self.module.__file__))
|
||||
for x in source_files:
|
||||
full_file = os.path.join(directory, x)
|
||||
rel = os.path.relpath(full_file, self.base_components_path)
|
||||
# Always use / for C++ include names
|
||||
rel = rel.replace(os.sep, '/')
|
||||
target_file = 'esphome/components/{}'.format(rel)
|
||||
ret[target_file] = full_file
|
||||
return ret
|
||||
|
||||
|
||||
CORE_COMPONENTS_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), 'components'))
|
||||
|
||||
|
||||
def _lookup_module(domain, is_platform):
|
||||
if domain in _COMPONENT_CACHE:
|
||||
return _COMPONENT_CACHE[domain]
|
||||
|
||||
path = 'esphome.components.{}'.format(domain)
|
||||
try:
|
||||
module = importlib.import_module(path)
|
||||
except (ImportError, ValueError) as err:
|
||||
_LOGGER.debug(err)
|
||||
except ImportError:
|
||||
return None
|
||||
except Exception: # pylint: disable=broad-except
|
||||
import traceback
|
||||
_LOGGER.error("Unable to load component %s:", domain)
|
||||
traceback.print_exc()
|
||||
return None
|
||||
else:
|
||||
_COMPONENT_CACHE[domain] = module
|
||||
return module
|
||||
manif = ComponentManifest(module, CORE_COMPONENTS_PATH, is_platform=is_platform)
|
||||
_COMPONENT_CACHE[domain] = manif
|
||||
return manif
|
||||
|
||||
_LOGGER.error("Unable to find component %s", domain)
|
||||
return None
|
||||
|
||||
def get_component(domain):
|
||||
assert '.' not in domain
|
||||
return _lookup_module(domain, False)
|
||||
|
||||
|
||||
def get_platform(domain, platform):
|
||||
return get_component("{}.{}".format(domain, platform))
|
||||
full = '{}.{}'.format(platform, domain)
|
||||
return _lookup_module(full, True)
|
||||
|
||||
|
||||
def is_platform_component(component):
|
||||
return hasattr(component, 'PLATFORM_SCHEMA')
|
||||
_COMPONENT_CACHE['esphome'] = ComponentManifest(
|
||||
core_config, CORE_COMPONENTS_PATH, is_core=True, is_platform=False,
|
||||
)
|
||||
|
||||
|
||||
def iter_components(config):
|
||||
for domain, conf in config.items():
|
||||
if domain == CONF_ESPHOME:
|
||||
yield CONF_ESPHOME, core_config, conf
|
||||
continue
|
||||
component = get_component(domain)
|
||||
if getattr(component, 'MULTI_CONF', False):
|
||||
if component.is_multi_conf:
|
||||
for conf_ in conf:
|
||||
yield domain, component, conf_
|
||||
else:
|
||||
yield domain, component, conf
|
||||
if is_platform_component(component):
|
||||
if component.is_platform_component:
|
||||
for p_config in conf:
|
||||
p_name = u"{}.{}".format(domain, p_config[CONF_PLATFORM])
|
||||
platform = get_component(p_name)
|
||||
platform = get_platform(domain, p_config[CONF_PLATFORM])
|
||||
yield p_name, platform, p_config
|
||||
|
||||
|
||||
@@ -229,8 +319,12 @@ def validate_config(config):
|
||||
# Step 1: Load everything
|
||||
result.add_domain([CONF_ESPHOME], CONF_ESPHOME)
|
||||
result[CONF_ESPHOME] = config[CONF_ESPHOME]
|
||||
|
||||
config_queue = collections.deque()
|
||||
for domain, conf in config.items():
|
||||
config_queue.append((domain, conf))
|
||||
|
||||
while config_queue:
|
||||
domain, conf = config_queue.popleft()
|
||||
domain = str(domain)
|
||||
if domain == CONF_ESPHOME or domain.startswith(u'.'):
|
||||
skip_paths.append([domain])
|
||||
@@ -245,12 +339,11 @@ def validate_config(config):
|
||||
skip_paths.append([domain])
|
||||
continue
|
||||
|
||||
if not isinstance(conf, list) and getattr(component, 'MULTI_CONF', False):
|
||||
if component.is_multi_conf and not isinstance(conf, list):
|
||||
result[domain] = conf = [conf]
|
||||
|
||||
success = True
|
||||
dependencies = getattr(component, 'DEPENDENCIES', [])
|
||||
for dependency in dependencies:
|
||||
for dependency in component.dependencies:
|
||||
if dependency not in config:
|
||||
result.add_error(u"Component {} requires component {}".format(domain, dependency),
|
||||
[domain])
|
||||
@@ -260,8 +353,7 @@ def validate_config(config):
|
||||
continue
|
||||
|
||||
success = True
|
||||
conflicts_with = getattr(component, 'CONFLICTS_WITH', [])
|
||||
for conflict in conflicts_with:
|
||||
for conflict in component.conflicts_with:
|
||||
if conflict in config:
|
||||
result.add_error(u"Component {} cannot be used together with component {}"
|
||||
u"".format(domain, conflict), [domain])
|
||||
@@ -270,14 +362,23 @@ def validate_config(config):
|
||||
skip_paths.append([domain])
|
||||
continue
|
||||
|
||||
esp_platforms = getattr(component, 'ESP_PLATFORMS', ESP_PLATFORMS)
|
||||
if CORE.esp_platform not in esp_platforms:
|
||||
for load in component.auto_load:
|
||||
if load not in config:
|
||||
conf = core.AutoLoad()
|
||||
config[load] = conf
|
||||
config_queue.append((load, conf))
|
||||
|
||||
if CORE.esp_platform not in component.esp_platforms:
|
||||
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'):
|
||||
if not component.is_platform_component:
|
||||
if component.config_schema is None and not isinstance(conf, core.AutoLoad):
|
||||
result.add_error(u"Component {} cannot be loaded via YAML (no CONFIG_SCHEMA)."
|
||||
u"".format(domain), [domain])
|
||||
skip_paths.append([domain])
|
||||
continue
|
||||
|
||||
result.remove_domain([domain], domain)
|
||||
@@ -304,8 +405,7 @@ def validate_config(config):
|
||||
continue
|
||||
|
||||
success = True
|
||||
dependencies = getattr(platform, 'DEPENDENCIES', [])
|
||||
for dependency in dependencies:
|
||||
for dependency in platform.dependencies:
|
||||
if dependency not in config:
|
||||
result.add_error(u"Platform {} requires component {}"
|
||||
u"".format(p_domain, dependency), [domain, i])
|
||||
@@ -315,8 +415,7 @@ def validate_config(config):
|
||||
continue
|
||||
|
||||
success = True
|
||||
conflicts_with = getattr(platform, 'CONFLICTS_WITH', [])
|
||||
for conflict in conflicts_with:
|
||||
for conflict in platform.conflicts_with:
|
||||
if conflict in config:
|
||||
result.add_error(u"Platform {} cannot be used together with component {}"
|
||||
u"".format(p_domain, conflict), [domain, i])
|
||||
@@ -325,13 +424,23 @@ def validate_config(config):
|
||||
skip_paths.append([domain, i])
|
||||
continue
|
||||
|
||||
esp_platforms = getattr(platform, 'ESP_PLATFORMS', ESP_PLATFORMS)
|
||||
if CORE.esp_platform not in esp_platforms:
|
||||
for load in platform.auto_load:
|
||||
if load not in config:
|
||||
conf = core.AutoLoad()
|
||||
config[load] = conf
|
||||
config_queue.append((load, conf))
|
||||
|
||||
if CORE.esp_platform not in platform.esp_platforms:
|
||||
result.add_error(u"Platform {} doesn't support {}."
|
||||
u"".format(p_domain, CORE.esp_platform), [domain, i])
|
||||
skip_paths.append([domain, i])
|
||||
continue
|
||||
|
||||
if platform.config_schema is None:
|
||||
result.add_error(u"Platform {} cannot be loaded via YAML (no PLATFORM_SCHEMA)."
|
||||
u"".format(p_domain), [domain, i])
|
||||
skip_paths.append([domain])
|
||||
|
||||
# Step 2: Validate configuration
|
||||
try:
|
||||
result[CONF_ESPHOME] = core_config.CONFIG_SCHEMA(result[CONF_ESPHOME])
|
||||
@@ -344,25 +453,24 @@ def validate_config(config):
|
||||
continue
|
||||
component = get_component(domain)
|
||||
|
||||
if hasattr(component, 'CONFIG_SCHEMA'):
|
||||
multi_conf = getattr(component, 'MULTI_CONF', False)
|
||||
if not component.is_platform_component:
|
||||
if component.config_schema is None:
|
||||
continue
|
||||
|
||||
if multi_conf:
|
||||
if component.is_multi_conf:
|
||||
for i, conf_ in enumerate(conf):
|
||||
try:
|
||||
validated = component.CONFIG_SCHEMA(conf_)
|
||||
validated = component.config_schema(conf_)
|
||||
result[domain][i] = validated
|
||||
except vol.Invalid as ex:
|
||||
_comp_error(ex, [domain, i])
|
||||
else:
|
||||
try:
|
||||
validated = component.CONFIG_SCHEMA(conf)
|
||||
validated = component.config_schema(conf)
|
||||
result[domain] = validated
|
||||
except vol.Invalid as ex:
|
||||
_comp_error(ex, [domain])
|
||||
continue
|
||||
|
||||
if not hasattr(component, 'PLATFORM_SCHEMA'):
|
||||
continue
|
||||
|
||||
for i, p_config in enumerate(conf):
|
||||
@@ -371,12 +479,19 @@ def validate_config(config):
|
||||
p_name = p_config['platform']
|
||||
platform = get_platform(domain, p_name)
|
||||
|
||||
if hasattr(platform, 'PLATFORM_SCHEMA'):
|
||||
if platform.config_schema is not None:
|
||||
# Remove 'platform' key for validation
|
||||
input_conf = OrderedDict(p_config)
|
||||
platform_val = input_conf.pop('platform')
|
||||
try:
|
||||
p_validated = platform.PLATFORM_SCHEMA(p_config)
|
||||
p_validated = platform.config_schema(input_conf)
|
||||
except vol.Invalid as ex:
|
||||
_comp_error(ex, [domain, i])
|
||||
continue
|
||||
if not isinstance(p_validated, OrderedDict):
|
||||
p_validated = OrderedDict(p_validated)
|
||||
p_validated['platform'] = platform_val
|
||||
p_validated.move_to_end('platform', last=False)
|
||||
result[domain][i] = p_validated
|
||||
|
||||
if not result.errors:
|
||||
@@ -574,7 +689,7 @@ def strip_default_ids(config):
|
||||
to_remove = []
|
||||
for i, x in enumerate(config):
|
||||
x = config[i] = strip_default_ids(x)
|
||||
if isinstance(x, core.ID) and not x.is_manual:
|
||||
if (isinstance(x, core.ID) and not x.is_manual) or isinstance(x, core.AutoLoad):
|
||||
to_remove.append(x)
|
||||
for x in to_remove:
|
||||
config.remove(x)
|
||||
@@ -582,7 +697,7 @@ def strip_default_ids(config):
|
||||
to_remove = []
|
||||
for k, v in config.items():
|
||||
v = config[k] = strip_default_ids(v)
|
||||
if isinstance(v, core.ID) and not v.is_manual:
|
||||
if (isinstance(v, core.ID) and not v.is_manual) or isinstance(v, core.AutoLoad):
|
||||
to_remove.append(k)
|
||||
for k in to_remove:
|
||||
config.pop(k)
|
||||
|
Reference in New Issue
Block a user