mirror of
https://github.com/esphome/esphome.git
synced 2025-09-06 21:32:21 +01:00
ESP-IDF support and generic target platforms (#2303)
* Socket refactor and SSL * esp-idf temp * Fixes * Echo component and noise * Add noise API transport support * Updates * ESP-IDF * Complete * Fixes * Fixes * Versions update * New i2c APIs * Complete i2c refactor * SPI migration * Revert ESP Preferences migration, too complex for now * OTA support * Remove echo again * Remove ssl again * GPIOFlags updates * Rename esphal and ICACHE_RAM_ATTR * Make ESP32 arduino compilable again * Fix GPIO flags * Complete pin registry refactor and fixes * Fixes to make test1 compile * Remove sdkconfig file * Ignore sdkconfig file * Fixes in reviewing * Make test2 compile * Make test4 compile * Make test5 compile * Run clang-format * Fix lint errors * Use esp-idf APIs instead of btStart * Another round of fixes * Start implementing ESP8266 * Make test3 compile * Guard esp8266 code * Lint * Reformat * Fixes * Fixes v2 * more fixes * ESP-IDF tidy target * Convert ARDUINO_ARCH_ESPxx * Update WiFiSignalSensor * Update time ifdefs * OTA needs millis from hal * RestartSwitch needs delay from hal * ESP-IDF Uart * Fix OTA blank password * Allow setting sdkconfig * Fix idf partitions and allow setting sdkconfig from yaml * Re-add read/write compat APIs and fix esp8266 uart * Fix esp8266 store log strings in flash * Fix ESP32 arduino preferences not initialized * Update ifdefs * Change how sdkconfig change is detected * Add checks to ci-custom and fix them * Run clang-format * Add esp-idf clang-tidy target and fix errors * Fixes from clang-tidy idf round 2 * Fixes from compiling tests with esp-idf * Run clang-format * Switch test5.yaml to esp-idf * Implement ESP8266 Preferences * Lint * Re-do PIO package version selection a bit * Fix arduinoespressif32 package version * Fix unit tests * Lint * Lint fixes * Fix readv/writev not defined * Fix graphing component * Re-add all old options from core/config.py Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import collections
|
||||
import abc
|
||||
import functools
|
||||
import heapq
|
||||
import logging
|
||||
import re
|
||||
|
||||
@@ -15,6 +17,7 @@ from esphome.const import (
|
||||
CONF_PACKAGES,
|
||||
CONF_SUBSTITUTIONS,
|
||||
CONF_EXTERNAL_COMPONENTS,
|
||||
TARGET_PLATFORMS,
|
||||
)
|
||||
from esphome.core import CORE, EsphomeError
|
||||
from esphome.helpers import indent
|
||||
@@ -56,6 +59,27 @@ def _path_begins_with(path, other): # type: (ConfigPath, ConfigPath) -> bool
|
||||
return path[: len(other)] == other
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class _ValidationStepTask:
|
||||
def __init__(self, priority: float, id_number: int, step: "ConfigValidationStep"):
|
||||
self.priority = priority
|
||||
self.id_number = id_number
|
||||
self.step = step
|
||||
|
||||
@property
|
||||
def _cmp_tuple(self) -> Tuple[float, int]:
|
||||
return (-self.priority, self.id_number)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self._cmp_tuple == other._cmp_tuple
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self == other)
|
||||
|
||||
def __lt__(self, other):
|
||||
return self._cmp_tuple < other._cmp_tuple
|
||||
|
||||
|
||||
class Config(OrderedDict, fv.FinalValidateConfig):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -68,6 +92,10 @@ class Config(OrderedDict, fv.FinalValidateConfig):
|
||||
# A list of components ids with the config path
|
||||
self.declare_ids = [] # type: List[Tuple[core.ID, ConfigPath]]
|
||||
self._data = {}
|
||||
# Store pending validation tasks (in heap order)
|
||||
self._validation_tasks: List[_ValidationStepTask] = []
|
||||
# ID to ensure stable order for keys with equal priority
|
||||
self._validation_tasks_id = 0
|
||||
|
||||
def add_error(self, error):
|
||||
# type: (vol.Invalid) -> None
|
||||
@@ -83,6 +111,18 @@ class Config(OrderedDict, fv.FinalValidateConfig):
|
||||
error.path = error.path[last_root + 1 :]
|
||||
self.errors.append(error)
|
||||
|
||||
def add_validation_step(self, step: "ConfigValidationStep"):
|
||||
id_num = self._validation_tasks_id
|
||||
self._validation_tasks_id += 1
|
||||
heapq.heappush(
|
||||
self._validation_tasks, _ValidationStepTask(step.priority, id_num, step)
|
||||
)
|
||||
|
||||
def run_validation_steps(self):
|
||||
while self._validation_tasks:
|
||||
task = heapq.heappop(self._validation_tasks)
|
||||
task.step.run(self)
|
||||
|
||||
@contextmanager
|
||||
def catch_error(self, path=None):
|
||||
path = path or []
|
||||
@@ -206,92 +246,6 @@ def iter_ids(config, path=None):
|
||||
yield from iter_ids(value, path + [key])
|
||||
|
||||
|
||||
def do_id_pass(result): # type: (Config) -> None
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
from esphome.cpp_types import Component
|
||||
|
||||
searching_ids = [] # type: List[Tuple[core.ID, ConfigPath]]
|
||||
for id, path in iter_ids(result):
|
||||
if id.is_declaration:
|
||||
if id.id is not None:
|
||||
# Look for duplicate definitions
|
||||
match = next((v for v in result.declare_ids if v[0].id == id.id), None)
|
||||
if match is not None:
|
||||
opath = "->".join(str(v) for v in match[1])
|
||||
result.add_str_error(f"ID {id.id} redefined! Check {opath}", path)
|
||||
continue
|
||||
result.declare_ids.append((id, path))
|
||||
else:
|
||||
searching_ids.append((id, path))
|
||||
# Resolve default ids after manual IDs
|
||||
for id, _ in result.declare_ids:
|
||||
id.resolve([v[0].id for v in result.declare_ids])
|
||||
if isinstance(id.type, MockObjClass) and id.type.inherits_from(Component):
|
||||
CORE.component_ids.add(id.id)
|
||||
|
||||
# Check searched IDs
|
||||
for id, path in searching_ids:
|
||||
if id.id is not None:
|
||||
# manually declared
|
||||
match = next((v[0] for v in result.declare_ids if v[0].id == id.id), None)
|
||||
if match is None or not match.is_manual:
|
||||
# No declared ID with this name
|
||||
import difflib
|
||||
|
||||
error = f"Couldn't find ID '{id.id}'. Please check you have defined an ID with that name in your configuration."
|
||||
# Find candidates
|
||||
matches = difflib.get_close_matches(
|
||||
id.id, [v[0].id for v in result.declare_ids if v[0].is_manual]
|
||||
)
|
||||
if matches:
|
||||
matches_s = ", ".join(f'"{x}"' for x in matches)
|
||||
error += f" These IDs look similar: {matches_s}."
|
||||
result.add_str_error(error, 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_str_error(
|
||||
f"ID '{id.id}' of type {match.type} doesn't inherit from {id.type}. Please double check your ID is pointing to the correct value",
|
||||
path,
|
||||
)
|
||||
|
||||
if id.id is None and id.type is not None:
|
||||
matches = []
|
||||
for v in result.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:
|
||||
matches.append(v[0])
|
||||
|
||||
if len(matches) == 0:
|
||||
result.add_str_error(
|
||||
f"Couldn't find any component that can be used for '{id.type}'. Are you missing a hub declaration?",
|
||||
path,
|
||||
)
|
||||
elif len(matches) == 1:
|
||||
id.id = matches[0].id
|
||||
elif len(matches) > 1:
|
||||
if str(id.type) == "time::RealTimeClock":
|
||||
id.id = matches[0].id
|
||||
else:
|
||||
manual_declared_count = sum(1 for m in matches if m.is_manual)
|
||||
if manual_declared_count > 0:
|
||||
ids = ", ".join([f"'{m.id}'" for m in matches if m.is_manual])
|
||||
result.add_str_error(
|
||||
f"Too many candidates found for '{path[-1]}' type '{id.type}' {'Some are' if manual_declared_count > 1 else 'One is'} {ids}",
|
||||
path,
|
||||
)
|
||||
else:
|
||||
result.add_str_error(
|
||||
f"Too many candidates found for '{path[-1]}' type '{id.type}' You must assign an explicit ID to the parent component you want to use.",
|
||||
path,
|
||||
)
|
||||
|
||||
|
||||
def recursive_check_replaceme(value):
|
||||
if isinstance(value, list):
|
||||
return cv.Schema([recursive_check_replaceme])(value)
|
||||
@@ -310,6 +264,389 @@ def recursive_check_replaceme(value):
|
||||
return value
|
||||
|
||||
|
||||
class ConfigValidationStep(abc.ABC):
|
||||
"""A step to for the validation phase."""
|
||||
|
||||
# Priority of this step, higher means run earlier
|
||||
priority: float = 0.0
|
||||
|
||||
@abc.abstractmethod
|
||||
def run(self, result: Config) -> None:
|
||||
...
|
||||
|
||||
|
||||
class LoadValidationStep(ConfigValidationStep):
|
||||
"""Load step, this step is called once for each domain config fragment.
|
||||
|
||||
Responsibilties:
|
||||
- Load component code
|
||||
- Ensure all AUTO_LOADs are added
|
||||
- Set output paths of result
|
||||
"""
|
||||
|
||||
def __init__(self, domain: str, conf: ConfigType):
|
||||
self.domain = domain
|
||||
self.conf = conf
|
||||
|
||||
def run(self, result: Config) -> None:
|
||||
if self.domain.startswith("."):
|
||||
# Ignore top-level keys starting with a dot
|
||||
return
|
||||
result.add_output_path([self.domain], self.domain)
|
||||
result[self.domain] = self.conf
|
||||
component = get_component(self.domain)
|
||||
path = [self.domain]
|
||||
if component is None:
|
||||
result.add_str_error(f"Component not found: {self.domain}", path)
|
||||
return
|
||||
CORE.loaded_integrations.add(self.domain)
|
||||
|
||||
# Process AUTO_LOAD
|
||||
for load in component.auto_load:
|
||||
if load not in result:
|
||||
result.add_validation_step(AutoLoadValidationStep(load))
|
||||
|
||||
if not component.is_platform_component:
|
||||
result.add_validation_step(
|
||||
MetadataValidationStep([self.domain], self.domain, self.conf, component)
|
||||
)
|
||||
return
|
||||
|
||||
# This is a platform component, proceed to reading platform entries
|
||||
# Remove this is as an output path
|
||||
result.remove_output_path([self.domain], self.domain)
|
||||
|
||||
# Ensure conf is a list
|
||||
if not self.conf:
|
||||
result[self.domain] = self.conf = []
|
||||
elif not isinstance(self.conf, list):
|
||||
result[self.domain] = self.conf = [self.conf]
|
||||
|
||||
for i, p_config in enumerate(self.conf):
|
||||
path = [self.domain, i]
|
||||
# Construct temporary unknown output path
|
||||
p_domain = f"{self.domain}.unknown"
|
||||
result.add_output_path(path, p_domain)
|
||||
result[self.domain][i] = p_config
|
||||
if not isinstance(p_config, dict):
|
||||
result.add_str_error("Platform schemas must be key-value pairs.", path)
|
||||
continue
|
||||
p_name = p_config.get("platform")
|
||||
if p_name is None:
|
||||
result.add_str_error("No platform specified! See 'platform' key.", path)
|
||||
continue
|
||||
# Remove temp output path and construct new one
|
||||
result.remove_output_path(path, p_domain)
|
||||
p_domain = f"{self.domain}.{p_name}"
|
||||
result.add_output_path(path, p_domain)
|
||||
# Try Load platform
|
||||
platform = get_platform(self.domain, p_name)
|
||||
if platform is None:
|
||||
result.add_str_error(f"Platform not found: '{p_domain}'", path)
|
||||
continue
|
||||
CORE.loaded_integrations.add(p_name)
|
||||
|
||||
# Process AUTO_LOAD
|
||||
for load in platform.auto_load:
|
||||
if load not in result:
|
||||
result.add_validation_step(AutoLoadValidationStep(load))
|
||||
|
||||
result.add_validation_step(
|
||||
MetadataValidationStep(path, p_domain, p_config, platform)
|
||||
)
|
||||
|
||||
|
||||
class AutoLoadValidationStep(ConfigValidationStep):
|
||||
"""Auto load step. This step is used to automatically load components if
|
||||
a component requested that with AUTO_LOAD.
|
||||
"""
|
||||
|
||||
# Only load after all regular loads have taken place
|
||||
priority = -1.0
|
||||
|
||||
def __init__(self, domain: str):
|
||||
self.domain = domain
|
||||
|
||||
def run(self, result: Config) -> None:
|
||||
if self.domain in result:
|
||||
# already loaded
|
||||
return
|
||||
result.add_validation_step(LoadValidationStep(self.domain, core.AutoLoad()))
|
||||
|
||||
|
||||
class MetadataValidationStep(ConfigValidationStep):
|
||||
"""Validate component metadata
|
||||
|
||||
Responsibilties:
|
||||
- Config transformation (nullable, multi conf)
|
||||
- Check dependencies
|
||||
- Check conflicts
|
||||
- Check supported target platforms
|
||||
"""
|
||||
|
||||
# All components need to be loaded first to ensure dependency check works
|
||||
priority = -2.0
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
path: ConfigPath,
|
||||
domain: str,
|
||||
conf: ConfigType,
|
||||
component: ComponentManifest,
|
||||
) -> None:
|
||||
self.path = path
|
||||
self.domain = domain
|
||||
self.conf = conf
|
||||
self.comp = component
|
||||
|
||||
def run(self, result: Config) -> None:
|
||||
if self.conf is None:
|
||||
result[self.domain] = self.conf = {}
|
||||
|
||||
success = True
|
||||
for dependency in self.comp.dependencies:
|
||||
if dependency not in result:
|
||||
result.add_str_error(
|
||||
f"Component {self.domain} requires component {dependency}",
|
||||
self.path,
|
||||
)
|
||||
success = False
|
||||
if not success:
|
||||
return
|
||||
|
||||
success = True
|
||||
for conflict in self.comp.conflicts_with:
|
||||
if conflict in result:
|
||||
result.add_str_error(
|
||||
f"Component {self.domain} cannot be used together with component {conflict}",
|
||||
self.path,
|
||||
)
|
||||
success = False
|
||||
if not success:
|
||||
return
|
||||
|
||||
if (
|
||||
not self.comp.is_platform_component
|
||||
and self.comp.config_schema is None
|
||||
and not isinstance(self.conf, core.AutoLoad)
|
||||
):
|
||||
result.add_str_error(
|
||||
f"Component {self.domain} cannot be loaded via YAML "
|
||||
"(no CONFIG_SCHEMA).",
|
||||
self.path,
|
||||
)
|
||||
return
|
||||
|
||||
if self.comp.multi_conf:
|
||||
if not isinstance(self.conf, list):
|
||||
result[self.domain] = self.conf = [self.conf]
|
||||
if (
|
||||
not isinstance(self.comp.multi_conf, bool)
|
||||
and len(self.conf) > self.comp.multi_conf
|
||||
):
|
||||
result.add_str_error(
|
||||
f"Component {self.domain} supports a maximum of {self.comp.multi_conf} "
|
||||
f"entries ({len(self.conf)} found).",
|
||||
self.path,
|
||||
)
|
||||
return
|
||||
for i, part_conf in enumerate(self.conf):
|
||||
result.add_validation_step(
|
||||
SchemaValidationStep(
|
||||
self.domain, self.path + [i], part_conf, self.comp
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
result.add_validation_step(
|
||||
SchemaValidationStep(self.domain, self.path, self.conf, self.comp)
|
||||
)
|
||||
|
||||
|
||||
class SchemaValidationStep(ConfigValidationStep):
|
||||
"""Schema validation step.
|
||||
|
||||
During this step all CONFIG_SCHEMAs are checked against the configs.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, domain: str, path: ConfigPath, conf: ConfigType, comp: ComponentManifest
|
||||
):
|
||||
self.path = path
|
||||
self.conf = conf
|
||||
self.comp = comp
|
||||
|
||||
def run(self, result: Config) -> None:
|
||||
if self.comp.config_schema is None:
|
||||
return
|
||||
with result.catch_error(self.path):
|
||||
if self.comp.is_platform:
|
||||
# Remove 'platform' key for validation
|
||||
input_conf = OrderedDict(self.conf)
|
||||
platform_val = input_conf.pop("platform")
|
||||
schema = cv.Schema(self.comp.config_schema)
|
||||
validated = schema(input_conf)
|
||||
# Ensure result is OrderedDict so we can call move_to_end
|
||||
if not isinstance(validated, OrderedDict):
|
||||
validated = OrderedDict(validated)
|
||||
validated["platform"] = platform_val
|
||||
validated.move_to_end("platform", last=False)
|
||||
result.set_by_path(self.path, validated)
|
||||
else:
|
||||
schema = cv.Schema(self.comp.config_schema)
|
||||
validated = schema(self.conf)
|
||||
result.set_by_path(self.path, validated)
|
||||
|
||||
result.add_validation_step(FinalValidateValidationStep(self.path, self.comp))
|
||||
|
||||
|
||||
class IDPassValidationStep(ConfigValidationStep):
|
||||
"""ID Pass step.
|
||||
|
||||
During this step all ID references are checked.
|
||||
|
||||
If an automatic ID reference is used, a fitting declared ID is automatically searched.
|
||||
Also checks duplicate ID names, and that referenced IDs are declared.
|
||||
"""
|
||||
|
||||
# Has to happen after all schemas validated
|
||||
priority = -10.0
|
||||
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
def run(self, result: Config) -> None:
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
from esphome.cpp_types import Component
|
||||
|
||||
if result.errors:
|
||||
# If result already has errors, skip this step
|
||||
# Otherwise the user will get a bunch of missing ID warnings
|
||||
# because the component that did not validate doesn't have any IDs set
|
||||
return
|
||||
|
||||
searching_ids = [] # type: List[Tuple[core.ID, ConfigPath]]
|
||||
for id, path in iter_ids(result):
|
||||
if id.is_declaration:
|
||||
if id.id is not None:
|
||||
# Look for duplicate definitions
|
||||
match = next(
|
||||
(v for v in result.declare_ids if v[0].id == id.id), None
|
||||
)
|
||||
if match is not None:
|
||||
opath = "->".join(str(v) for v in match[1])
|
||||
result.add_str_error(
|
||||
f"ID {id.id} redefined! Check {opath}", path
|
||||
)
|
||||
continue
|
||||
result.declare_ids.append((id, path))
|
||||
else:
|
||||
searching_ids.append((id, path))
|
||||
|
||||
# Resolve default ids after manual IDs
|
||||
for id, _ in result.declare_ids:
|
||||
id.resolve([v[0].id for v in result.declare_ids])
|
||||
if isinstance(id.type, MockObjClass) and id.type.inherits_from(Component):
|
||||
CORE.component_ids.add(id.id)
|
||||
|
||||
# Check searched IDs
|
||||
for id, path in searching_ids:
|
||||
if id.id is not None:
|
||||
# manually declared
|
||||
match = next(
|
||||
(v[0] for v in result.declare_ids if v[0].id == id.id), None
|
||||
)
|
||||
if match is None or not match.is_manual:
|
||||
# No declared ID with this name
|
||||
import difflib
|
||||
|
||||
error = (
|
||||
f"Couldn't find ID '{id.id}'. Please check you have defined "
|
||||
"an ID with that name in your configuration."
|
||||
)
|
||||
# Find candidates
|
||||
matches = difflib.get_close_matches(
|
||||
id.id, [v[0].id for v in result.declare_ids if v[0].is_manual]
|
||||
)
|
||||
if matches:
|
||||
matches_s = ", ".join(f'"{x}"' for x in matches)
|
||||
error += f" These IDs look similar: {matches_s}."
|
||||
result.add_str_error(error, 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_str_error(
|
||||
f"ID '{id.id}' of type {match.type} doesn't inherit from {id.type}. "
|
||||
"Please double check your ID is pointing to the correct value",
|
||||
path,
|
||||
)
|
||||
|
||||
if id.id is None and id.type is not None:
|
||||
matches = []
|
||||
for v in result.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:
|
||||
matches.append(v[0])
|
||||
|
||||
if len(matches) == 0:
|
||||
result.add_str_error(
|
||||
f"Couldn't find any component that can be used for '{id.type}'. Are you missing a hub declaration?",
|
||||
path,
|
||||
)
|
||||
elif len(matches) == 1:
|
||||
id.id = matches[0].id
|
||||
elif len(matches) > 1:
|
||||
if str(id.type) == "time::RealTimeClock":
|
||||
id.id = matches[0].id
|
||||
else:
|
||||
manual_declared_count = sum(1 for m in matches if m.is_manual)
|
||||
if manual_declared_count > 0:
|
||||
ids = ", ".join(
|
||||
[f"'{m.id}'" for m in matches if m.is_manual]
|
||||
)
|
||||
result.add_str_error(
|
||||
f"Too many candidates found for '{path[-1]}' type '{id.type}' {'Some are' if manual_declared_count > 1 else 'One is'} {ids}",
|
||||
path,
|
||||
)
|
||||
else:
|
||||
result.add_str_error(
|
||||
f"Too many candidates found for '{path[-1]}' type '{id.type}' You must assign an explicit ID to the parent component you want to use.",
|
||||
path,
|
||||
)
|
||||
|
||||
|
||||
class FinalValidateValidationStep(ConfigValidationStep):
|
||||
"""Run final_validate_schema for all components."""
|
||||
|
||||
# Has to happen after ID pass validated
|
||||
priority = -20.0
|
||||
|
||||
def __init__(self, path: ConfigPath, comp: ComponentManifest) -> None:
|
||||
self.path = path
|
||||
self.comp = comp
|
||||
|
||||
def run(self, result: Config) -> None:
|
||||
if result.errors:
|
||||
# If result already has errors, skip this step
|
||||
return
|
||||
|
||||
if self.comp.final_validate_schema is None:
|
||||
return
|
||||
|
||||
token = fv.full_config.set(result)
|
||||
|
||||
conf = result.get_nested_item(self.path)
|
||||
with result.catch_error(self.path):
|
||||
self.comp.final_validate_schema(conf)
|
||||
|
||||
fv.full_config.reset(token)
|
||||
|
||||
|
||||
def validate_config(config, command_line_substitutions):
|
||||
result = Config()
|
||||
|
||||
@@ -384,217 +721,31 @@ def validate_config(config, command_line_substitutions):
|
||||
result[CONF_ESPHOME] = config[CONF_ESPHOME]
|
||||
result.add_output_path([CONF_ESPHOME], CONF_ESPHOME)
|
||||
try:
|
||||
core_config.preload_core_config(config)
|
||||
core_config.preload_core_config(config, result)
|
||||
except vol.Invalid as err:
|
||||
result.add_error(err)
|
||||
return result
|
||||
# Remove temporary esphome config path again, it will be reloaded later
|
||||
result.remove_output_path([CONF_ESPHOME], CONF_ESPHOME)
|
||||
|
||||
# 3. Load components.
|
||||
# Load components (also AUTO_LOAD) and set output paths of result
|
||||
# Queue of items to load, FIFO
|
||||
load_queue = collections.deque()
|
||||
# First run platform validation steps
|
||||
for key in TARGET_PLATFORMS:
|
||||
if key in config:
|
||||
result.add_validation_step(LoadValidationStep(key, config[key]))
|
||||
result.run_validation_steps()
|
||||
|
||||
if result.errors:
|
||||
return result
|
||||
|
||||
for domain, conf in config.items():
|
||||
load_queue.append((domain, conf))
|
||||
result.add_validation_step(LoadValidationStep(domain, conf))
|
||||
result.add_validation_step(IDPassValidationStep())
|
||||
|
||||
# List of items to enter next stage
|
||||
check_queue = (
|
||||
[]
|
||||
) # type: List[Tuple[ConfigPath, str, ConfigType, ComponentManifest]]
|
||||
|
||||
# This step handles:
|
||||
# - Adding output path
|
||||
# - Auto Load
|
||||
# - Loading configs into result
|
||||
|
||||
while load_queue:
|
||||
domain, conf = load_queue.popleft()
|
||||
if domain.startswith("."):
|
||||
# Ignore top-level keys starting with a dot
|
||||
continue
|
||||
result.add_output_path([domain], domain)
|
||||
result[domain] = conf
|
||||
component = get_component(domain)
|
||||
path = [domain]
|
||||
if component is None:
|
||||
result.add_str_error(f"Component not found: {domain}", path)
|
||||
continue
|
||||
CORE.loaded_integrations.add(domain)
|
||||
|
||||
# Process AUTO_LOAD
|
||||
for load in component.auto_load:
|
||||
if load not in config:
|
||||
load_conf = core.AutoLoad()
|
||||
config[load] = load_conf
|
||||
load_queue.append((load, load_conf))
|
||||
|
||||
if not component.is_platform_component:
|
||||
check_queue.append(([domain], domain, conf, component))
|
||||
continue
|
||||
|
||||
# This is a platform component, proceed to reading platform entries
|
||||
# Remove this is as an output path
|
||||
result.remove_output_path([domain], domain)
|
||||
|
||||
# Ensure conf is a list
|
||||
if not conf:
|
||||
result[domain] = conf = []
|
||||
elif not isinstance(conf, list):
|
||||
result[domain] = conf = [conf]
|
||||
|
||||
for i, p_config in enumerate(conf):
|
||||
path = [domain, i]
|
||||
# Construct temporary unknown output path
|
||||
p_domain = f"{domain}.unknown"
|
||||
result.add_output_path(path, p_domain)
|
||||
result[domain][i] = p_config
|
||||
if not isinstance(p_config, dict):
|
||||
result.add_str_error("Platform schemas must be key-value pairs.", path)
|
||||
continue
|
||||
p_name = p_config.get("platform")
|
||||
if p_name is None:
|
||||
result.add_str_error("No platform specified! See 'platform' key.", path)
|
||||
continue
|
||||
# Remove temp output path and construct new one
|
||||
result.remove_output_path(path, p_domain)
|
||||
p_domain = f"{domain}.{p_name}"
|
||||
result.add_output_path(path, p_domain)
|
||||
# Try Load platform
|
||||
platform = get_platform(domain, p_name)
|
||||
if platform is None:
|
||||
result.add_str_error(f"Platform not found: '{p_domain}'", path)
|
||||
continue
|
||||
CORE.loaded_integrations.add(p_name)
|
||||
|
||||
# Process AUTO_LOAD
|
||||
for load in platform.auto_load:
|
||||
if load not in config:
|
||||
load_conf = core.AutoLoad()
|
||||
config[load] = load_conf
|
||||
load_queue.append((load, load_conf))
|
||||
|
||||
check_queue.append((path, p_domain, p_config, platform))
|
||||
|
||||
# 4. Validate component metadata, including
|
||||
# - Transformation (nullable, multi conf)
|
||||
# - Dependencies
|
||||
# - Conflicts
|
||||
# - Supported ESP Platform
|
||||
|
||||
# List of items to proceed to next stage
|
||||
validate_queue = [] # type: List[Tuple[ConfigPath, ConfigType, ComponentManifest]]
|
||||
for path, domain, conf, comp in check_queue:
|
||||
if conf is None:
|
||||
result[domain] = conf = {}
|
||||
|
||||
success = True
|
||||
for dependency in comp.dependencies:
|
||||
if dependency not in config:
|
||||
result.add_str_error(
|
||||
f"Component {domain} requires component {dependency}",
|
||||
path,
|
||||
)
|
||||
success = False
|
||||
if not success:
|
||||
continue
|
||||
|
||||
success = True
|
||||
for conflict in comp.conflicts_with:
|
||||
if conflict in config:
|
||||
result.add_str_error(
|
||||
f"Component {domain} cannot be used together with component {conflict}",
|
||||
path,
|
||||
)
|
||||
success = False
|
||||
if not success:
|
||||
continue
|
||||
|
||||
if CORE.esp_platform not in comp.esp_platforms:
|
||||
result.add_str_error(
|
||||
f"Component {domain} doesn't support {CORE.esp_platform}.",
|
||||
path,
|
||||
)
|
||||
continue
|
||||
|
||||
if (
|
||||
not comp.is_platform_component
|
||||
and comp.config_schema is None
|
||||
and not isinstance(conf, core.AutoLoad)
|
||||
):
|
||||
result.add_str_error(
|
||||
f"Component {domain} cannot be loaded via YAML (no CONFIG_SCHEMA).",
|
||||
path,
|
||||
)
|
||||
continue
|
||||
|
||||
if comp.multi_conf:
|
||||
if not isinstance(conf, list):
|
||||
result[domain] = conf = [conf]
|
||||
if not isinstance(comp.multi_conf, bool) and len(conf) > comp.multi_conf:
|
||||
result.add_str_error(
|
||||
f"Component {domain} supports a maximum of {comp.multi_conf} entries ({len(conf)} found).",
|
||||
path,
|
||||
)
|
||||
continue
|
||||
for i, part_conf in enumerate(conf):
|
||||
validate_queue.append((path + [i], part_conf, comp))
|
||||
continue
|
||||
|
||||
validate_queue.append((path, conf, comp))
|
||||
|
||||
# 5. Validate configuration schema
|
||||
for path, conf, comp in validate_queue:
|
||||
if comp.config_schema is None:
|
||||
continue
|
||||
with result.catch_error(path):
|
||||
if comp.is_platform:
|
||||
# Remove 'platform' key for validation
|
||||
input_conf = OrderedDict(conf)
|
||||
platform_val = input_conf.pop("platform")
|
||||
validated = comp.config_schema(input_conf)
|
||||
# Ensure result is OrderedDict so we can call move_to_end
|
||||
if not isinstance(validated, OrderedDict):
|
||||
validated = OrderedDict(validated)
|
||||
validated["platform"] = platform_val
|
||||
validated.move_to_end("platform", last=False)
|
||||
result.set_by_path(path, validated)
|
||||
else:
|
||||
validated = comp.config_schema(conf)
|
||||
result.set_by_path(path, validated)
|
||||
|
||||
# 6. If no validation errors, check IDs
|
||||
if not result.errors:
|
||||
# Only parse IDs if no validation error. Otherwise
|
||||
# user gets confusing messages
|
||||
do_id_pass(result)
|
||||
|
||||
# 7. Final validation
|
||||
if not result.errors:
|
||||
# Inter - components validation
|
||||
token = fv.full_config.set(result)
|
||||
|
||||
for path, _, comp in validate_queue:
|
||||
if comp.final_validate_schema is None:
|
||||
continue
|
||||
conf = result.get_nested_item(path)
|
||||
with result.catch_error(path):
|
||||
comp.final_validate_schema(conf)
|
||||
|
||||
fv.full_config.reset(token)
|
||||
result.run_validation_steps()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
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):
|
||||
validation_error = str(validation_error)
|
||||
m = re.match(
|
||||
@@ -661,7 +812,6 @@ def _load_config(command_line_substitutions):
|
||||
config = yaml_util.load_yaml(CORE.config_path)
|
||||
except EsphomeError as e:
|
||||
raise InvalidYAMLError(e) from e
|
||||
CORE.raw_config = config
|
||||
|
||||
try:
|
||||
result = validate_config(config, command_line_substitutions)
|
||||
|
Reference in New Issue
Block a user