From 0c032bc431fb76ab5bc239e64733390c4bdfeef4 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Thu, 23 Jan 2025 00:06:07 +0100 Subject: [PATCH] [core] add support for custom platform (#7616) Co-authored-by: Tomasz Duda --- esphome/components/bk72xx/__init__.py | 1 + esphome/components/esp32/__init__.py | 1 + esphome/components/esp8266/__init__.py | 1 + esphome/components/host/__init__.py | 1 + esphome/components/libretiny/__init__.py | 1 + esphome/components/rp2040/__init__.py | 1 + esphome/components/rtl87xx/__init__.py | 2 ++ esphome/config.py | 9 +++--- esphome/const.py | 9 ------ esphome/core/config.py | 37 +++++++++++++++++++++--- esphome/loader.py | 16 +++++++--- 11 files changed, 57 insertions(+), 22 deletions(-) diff --git a/esphome/components/bk72xx/__init__.py b/esphome/components/bk72xx/__init__.py index b5122de956..5b14d0529d 100644 --- a/esphome/components/bk72xx/__init__.py +++ b/esphome/components/bk72xx/__init__.py @@ -19,6 +19,7 @@ from .boards import BK72XX_BOARD_PINS, BK72XX_BOARDS CODEOWNERS = ["@kuba2k2"] AUTO_LOAD = ["libretiny"] +IS_TARGET_PLATFORM = True COMPONENT_DATA = LibreTinyComponent( name=COMPONENT_BK72XX, diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 98db45831a..23b84f3d13 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -64,6 +64,7 @@ from .gpio import esp32_pin_to_code # noqa _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@esphome/core"] AUTO_LOAD = ["preferences"] +IS_TARGET_PLATFORM = True CONF_RELEASE = "release" diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index c73027fe1b..c949e53aa6 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -34,6 +34,7 @@ from .gpio import PinInitialState, add_pin_initial_states_array CODEOWNERS = ["@esphome/core"] _LOGGER = logging.getLogger(__name__) AUTO_LOAD = ["preferences"] +IS_TARGET_PLATFORM = True def set_core_data(config): diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py index eb8cfbd984..e275adafa9 100644 --- a/esphome/components/host/__init__.py +++ b/esphome/components/host/__init__.py @@ -17,6 +17,7 @@ from .gpio import host_pin_to_code # noqa CODEOWNERS = ["@esphome/core", "@clydebarrow"] AUTO_LOAD = ["network", "preferences"] +IS_TARGET_PLATFORM = True def set_core_data(config): diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index b29d2e309c..5bdfb15e19 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -47,6 +47,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@kuba2k2"] AUTO_LOAD = ["preferences"] +IS_TARGET_PLATFORM = True def _detect_variant(value): diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index b04e539182..3d73cad195 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -27,6 +27,7 @@ from .gpio import rp2040_pin_to_code # noqa _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@jesserockz"] AUTO_LOAD = ["preferences"] +IS_TARGET_PLATFORM = True def set_core_data(config): diff --git a/esphome/components/rtl87xx/__init__.py b/esphome/components/rtl87xx/__init__.py index 4c1956f0f4..109c986f75 100644 --- a/esphome/components/rtl87xx/__init__.py +++ b/esphome/components/rtl87xx/__init__.py @@ -19,6 +19,8 @@ from .boards import RTL87XX_BOARD_PINS, RTL87XX_BOARDS CODEOWNERS = ["@kuba2k2"] AUTO_LOAD = ["libretiny"] +IS_TARGET_PLATFORM = True + COMPONENT_DATA = LibreTinyComponent( name=COMPONENT_RTL87XX, diff --git a/esphome/config.py b/esphome/config.py index 65e9ac29bc..09ee2a8f9b 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -22,7 +22,6 @@ from esphome.const import ( CONF_PACKAGES, CONF_PLATFORM, CONF_SUBSTITUTIONS, - TARGET_PLATFORMS, ) from esphome.core import CORE, DocumentRange, EsphomeError import esphome.core.config as core_config @@ -833,7 +832,7 @@ def validate_config( result[CONF_ESPHOME] = config[CONF_ESPHOME] result.add_output_path([CONF_ESPHOME], CONF_ESPHOME) try: - core_config.preload_core_config(config, result) + target_platform = core_config.preload_core_config(config, result) except vol.Invalid as err: result.add_error(err) return result @@ -845,9 +844,9 @@ def validate_config( cv.All(cv.version_number, cv.validate_esphome_version)(min_version) # First run platform validation steps - for key in TARGET_PLATFORMS: - if key in config: - result.add_validation_step(LoadValidationStep(key, config[key])) + result.add_validation_step( + LoadValidationStep(target_platform, config[target_platform]) + ) result.run_validation_steps() if result.errors: diff --git a/esphome/const.py b/esphome/const.py index 284f8d5f78..95bf6afc02 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -15,15 +15,6 @@ PLATFORM_LIBRETINY_OLDSTYLE = "libretiny" PLATFORM_RP2040 = "rp2040" PLATFORM_RTL87XX = "rtl87xx" -TARGET_PLATFORMS = [ - PLATFORM_BK72XX, - PLATFORM_ESP32, - PLATFORM_ESP8266, - PLATFORM_HOST, - PLATFORM_LIBRETINY_OLDSTYLE, - PLATFORM_RP2040, - PLATFORM_RTL87XX, -] SOURCE_FILE_EXTENSIONS = {".cpp", ".hpp", ".h", ".c", ".tcc", ".ino"} HEADER_FILE_EXTENSIONS = {".h", ".hpp", ".tcc"} diff --git a/esphome/core/config.py b/esphome/core/config.py index c6a3b1b294..06ae1d7747 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -1,6 +1,7 @@ import logging import multiprocessing import os +from pathlib import Path from esphome import automation import esphome.codegen as cg @@ -28,7 +29,6 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_VERSION, KEY_CORE, - TARGET_PLATFORMS, __version__ as ESPHOME_VERSION, ) from esphome.core import CORE, coroutine_with_priority @@ -174,7 +174,31 @@ PRELOAD_CONFIG_SCHEMA = cv.Schema( ) -def preload_core_config(config, result): +def _is_target_platform(name): + from esphome.loader import get_component + + try: + if get_component(name, True).is_target_platform: + return True + except KeyError: + pass + return False + + +def _list_target_platforms(): + target_platforms = [] + root = Path(__file__).parents[1] + for path in (root / "components").iterdir(): + if not path.is_dir(): + continue + if not (path / "__init__.py").is_file(): + continue + if _is_target_platform(path.name): + target_platforms += [path.name] + return target_platforms + + +def preload_core_config(config, result) -> str: with cv.prepend_path(CONF_ESPHOME): conf = PRELOAD_CONFIG_SCHEMA(config[CONF_ESPHOME]) @@ -187,12 +211,16 @@ def preload_core_config(config, result): conf[CONF_BUILD_PATH] = os.path.join(build_path, CORE.name) CORE.build_path = CORE.relative_internal_path(conf[CONF_BUILD_PATH]) - target_platforms = [key for key in TARGET_PLATFORMS if key in config] + target_platforms = [] + + for domain, _ in config.items(): + if _is_target_platform(domain): + target_platforms += [domain] if not target_platforms: raise cv.Invalid( "Platform missing. You must include one of the available platform keys: " - + ", ".join(TARGET_PLATFORMS), + + ", ".join(_list_target_platforms()), [CONF_ESPHOME], ) if len(target_platforms) > 1: @@ -202,6 +230,7 @@ def preload_core_config(config, result): ) config[CONF_ESPHOME] = conf + return target_platforms[0] def include_file(path, basename): diff --git a/esphome/loader.py b/esphome/loader.py index d808805119..0fb4187b04 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -52,6 +52,10 @@ class ComponentManifest: def is_platform_component(self) -> bool: return getattr(self.module, "IS_PLATFORM_COMPONENT", False) + @property + def is_target_platform(self) -> bool: + return getattr(self.module, "IS_TARGET_PLATFORM", False) + @property def config_schema(self) -> Optional[Any]: return getattr(self.module, "CONFIG_SCHEMA", None) @@ -169,13 +173,15 @@ def install_custom_components_meta_finder(): install_meta_finder(custom_components_dir) -def _lookup_module(domain): +def _lookup_module(domain, exception): if domain in _COMPONENT_CACHE: return _COMPONENT_CACHE[domain] try: module = importlib.import_module(f"esphome.components.{domain}") except ImportError as e: + if exception: + raise if "No module named" in str(e): _LOGGER.info( "Unable to import component %s: %s", domain, str(e), exc_info=False @@ -184,6 +190,8 @@ def _lookup_module(domain): _LOGGER.error("Unable to import component %s:", domain, exc_info=True) return None except Exception: # pylint: disable=broad-except + if exception: + raise _LOGGER.error("Unable to load component %s:", domain, exc_info=True) return None @@ -192,14 +200,14 @@ def _lookup_module(domain): return manif -def get_component(domain): +def get_component(domain, exception=False): assert "." not in domain - return _lookup_module(domain) + return _lookup_module(domain, exception) def get_platform(domain, platform): full = f"{platform}.{domain}" - return _lookup_module(full) + return _lookup_module(full, False) _COMPONENT_CACHE = {}