diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 64d8e035165..263985df4d4 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -1,3 +1,4 @@ +import functools import logging import re from typing import Any @@ -5,37 +6,6 @@ from typing import Any from esphome import automation from esphome.automation import LambdaAction, StatelessLambdaAction import esphome.codegen as cg -from esphome.components.esp32 import ( - VARIANT_ESP32, - VARIANT_ESP32C2, - VARIANT_ESP32C3, - VARIANT_ESP32C5, - VARIANT_ESP32C6, - VARIANT_ESP32C61, - VARIANT_ESP32H2, - VARIANT_ESP32H4, - VARIANT_ESP32H21, - VARIANT_ESP32P4, - VARIANT_ESP32S2, - VARIANT_ESP32S3, - VARIANT_ESP32S31, - add_idf_sdkconfig_option, - get_esp32_variant, - idf_version, - require_usb_serial_jtag_secondary, - require_vfs_termios, -) -from esphome.components.libretiny import get_libretiny_component, get_libretiny_family -from esphome.components.libretiny.const import ( - COMPONENT_BK72XX, - COMPONENT_LN882X, - COMPONENT_RTL87XX, -) -from esphome.components.zephyr import ( - zephyr_add_overlay, - zephyr_add_prj_conf, - zephyr_claim_cdc_acm, -) from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( @@ -117,30 +87,8 @@ CONF_TASK_LOG_BUFFER_SIZE = "task_log_buffer_size" CONF_WAIT_FOR_CDC = "wait_for_cdc" CONF_EARLY_MESSAGE = "early_message" -UART_SELECTION_ESP32 = { - VARIANT_ESP32: [UART0, UART1, UART2], - VARIANT_ESP32C2: [UART0, UART1], - VARIANT_ESP32C3: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], - VARIANT_ESP32C5: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], - VARIANT_ESP32C6: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], - VARIANT_ESP32C61: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], - VARIANT_ESP32H2: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], - VARIANT_ESP32H4: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], - VARIANT_ESP32H21: [UART0, UART1, USB_SERIAL_JTAG], - VARIANT_ESP32P4: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], - VARIANT_ESP32S2: [UART0, UART1, USB_CDC], - VARIANT_ESP32S3: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], - VARIANT_ESP32S31: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], -} - UART_SELECTION_ESP8266 = [UART0, UART0_SWAP, UART1] -UART_SELECTION_LIBRETINY = { - COMPONENT_BK72XX: [DEFAULT, UART1, UART2], - COMPONENT_LN882X: [DEFAULT, UART0, UART1, UART2], - COMPONENT_RTL87XX: [DEFAULT, UART0, UART1, UART2], -} - UART_SELECTION_RP2040 = [USB_CDC, UART0, UART1] UART_SELECTION_NRF52 = [USB_CDC, UART0] @@ -173,22 +121,99 @@ HARDWARE_UART_TO_SERIAL = { is_log_level = cv.one_of(*LOG_LEVELS, upper=True) +@functools.cache +def _uart_selection_esp32() -> dict[str, list[str]]: + from esphome.components.esp32 import ( + VARIANT_ESP32, + VARIANT_ESP32C2, + VARIANT_ESP32C3, + VARIANT_ESP32C5, + VARIANT_ESP32C6, + VARIANT_ESP32C61, + VARIANT_ESP32H2, + VARIANT_ESP32H4, + VARIANT_ESP32H21, + VARIANT_ESP32P4, + VARIANT_ESP32S2, + VARIANT_ESP32S3, + VARIANT_ESP32S31, + ) + + return { + VARIANT_ESP32: [UART0, UART1, UART2], + VARIANT_ESP32C2: [UART0, UART1], + VARIANT_ESP32C3: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], + VARIANT_ESP32C5: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], + VARIANT_ESP32C6: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], + VARIANT_ESP32C61: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], + VARIANT_ESP32H2: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], + VARIANT_ESP32H4: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], + VARIANT_ESP32H21: [UART0, UART1, USB_SERIAL_JTAG], + VARIANT_ESP32P4: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], + VARIANT_ESP32S2: [UART0, UART1, USB_CDC], + VARIANT_ESP32S3: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], + VARIANT_ESP32S31: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], + } + + +@functools.cache +def _uart_selection_libretiny() -> dict[str, list[str]]: + from esphome.components.libretiny.const import ( + COMPONENT_BK72XX, + COMPONENT_LN882X, + COMPONENT_RTL87XX, + ) + + return { + COMPONENT_BK72XX: [DEFAULT, UART1, UART2], + COMPONENT_LN882X: [DEFAULT, UART0, UART1, UART2], + COMPONENT_RTL87XX: [DEFAULT, UART0, UART1, UART2], + } + + +# These tables are introspected by external tooling (esphome/device-builder); +# built on access so the platform packages stay out of module import. +_LAZY_UART_SELECTION_TABLES = { + "UART_SELECTION_ESP32": _uart_selection_esp32, + "UART_SELECTION_LIBRETINY": _uart_selection_libretiny, +} + + +def __getattr__(name: str) -> dict[str, list[str]]: + if (table := _LAZY_UART_SELECTION_TABLES.get(name)) is not None: + return table() + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +def __dir__() -> list[str]: + return [*globals(), *_LAZY_UART_SELECTION_TABLES] + + def uart_selection(value: Any) -> str: if CORE.is_esp32: + from esphome.components.esp32 import get_esp32_variant + + uart_selection_esp32 = _uart_selection_esp32() variant = get_esp32_variant() - if variant in UART_SELECTION_ESP32: - return cv.one_of(*UART_SELECTION_ESP32[variant], upper=True)(value) + if variant in uart_selection_esp32: + return cv.one_of(*uart_selection_esp32[variant], upper=True)(value) if CORE.is_esp8266: return cv.one_of(*UART_SELECTION_ESP8266, upper=True)(value) if CORE.is_rp2: return cv.one_of(*UART_SELECTION_RP2040, upper=True)(value) if CORE.is_libretiny: + from esphome.components.libretiny import ( + get_libretiny_component, + get_libretiny_family, + ) + + uart_selection_libretiny = _uart_selection_libretiny() family = get_libretiny_family() - if family in UART_SELECTION_LIBRETINY: - return cv.one_of(*UART_SELECTION_LIBRETINY[family], upper=True)(value) + if family in uart_selection_libretiny: + return cv.one_of(*uart_selection_libretiny[family], upper=True)(value) component = get_libretiny_component() - if component in UART_SELECTION_LIBRETINY: - return cv.one_of(*UART_SELECTION_LIBRETINY[component], upper=True)(value) + if component in uart_selection_libretiny: + return cv.one_of(*uart_selection_libretiny[component], upper=True)(value) if CORE.is_host: raise cv.Invalid("Uart selection not valid for host platform") if CORE.is_nrf52: @@ -414,6 +439,8 @@ def _final_validate(config: ConfigType) -> None: and config[CONF_BAUD_RATE] != 0 and config.get(CONF_HARDWARE_UART) == USB_CDC ): + from esphome.components.zephyr import zephyr_claim_cdc_acm + zephyr_claim_cdc_acm(0, "logger", write_only=True) @@ -495,6 +522,8 @@ async def _late_logger_init(config: ConfigType) -> None: level = config[CONF_LEVEL] baud_rate: int = config[CONF_BAUD_RATE] if CORE.using_zephyr: + from esphome.components.zephyr import zephyr_add_prj_conf + task_log_buffer_size = config.get(CONF_TASK_LOG_BUFFER_SIZE, 0) if task_log_buffer_size > 0: zephyr_add_prj_conf("MPSC_PBUF", True) @@ -559,6 +588,14 @@ async def _late_logger_init(config: ConfigType) -> None: if CORE.is_esp32 and is_at_least_very_verbose: cg.add_build_flag("-DENABLE_I2C_DEBUG_BUFFER") if CORE.is_esp32: + from esphome.components.esp32 import ( + VARIANT_ESP32C2, + VARIANT_ESP32S2, + add_idf_sdkconfig_option, + get_esp32_variant, + idf_version, + ) + if config[CONF_HARDWARE_UART] == USB_CDC: add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_CDC", True) cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC") @@ -591,6 +628,11 @@ async def _late_logger_init(config: ConfigType) -> None: and config[CONF_HARDWARE_UART] != USB_SERIAL_JTAG and has_serial_logging ): + from esphome.components.esp32 import ( + require_usb_serial_jtag_secondary, + require_vfs_termios, + ) + require_usb_serial_jtag_secondary() require_vfs_termios() except cv.Invalid: @@ -607,6 +649,8 @@ async def _late_logger_init(config: ConfigType) -> None: cg.add_define("USE_LOGGER_EARLY_MESSAGE") if CORE.is_nrf52: + from esphome.components.zephyr import zephyr_add_overlay, zephyr_add_prj_conf + # esphome implement own fatal error handler which save PC/LR before reset zephyr_add_prj_conf("RESET_ON_FATAL_ERROR", False) zephyr_add_prj_conf("THREAD_LOCAL_STORAGE", True) diff --git a/tests/unit_tests/components/test_logger.py b/tests/unit_tests/components/test_logger.py new file mode 100644 index 00000000000..17509775c2c --- /dev/null +++ b/tests/unit_tests/components/test_logger.py @@ -0,0 +1,38 @@ +"""Tests for the logger component's lazy platform tables.""" + +import subprocess +import sys + +from esphome.components import logger + + +def test_uart_selection_tables_exposed_for_external_tooling() -> None: + """device-builder introspects these names off the module; keep them + accessible (and discoverable) and stable between accesses.""" + assert "UART_SELECTION_ESP32" in dir(logger) + assert "UART_SELECTION_LIBRETINY" in dir(logger) + assert logger.UART_SELECTION_ESP32["ESP32C3"] == [ + "UART0", + "UART1", + "USB_CDC", + "USB_SERIAL_JTAG", + ] + assert "bk72xx" in logger.UART_SELECTION_LIBRETINY + # Repeated access returns the same cached object + assert logger.UART_SELECTION_ESP32 is logger.UART_SELECTION_ESP32 + + +def test_dir_does_not_import_platform_packages() -> None: + """Listing the names must not load the platform packages; only reading a + table may. Checked in a fresh interpreter so nothing loaded by other tests + hides a leak, and nothing here changes what later tests see.""" + check = ( + "import sys; from esphome.components import logger; dir(logger); " + "print(','.join(m for m in ('esphome.components.esp32', " + "'esphome.components.libretiny', 'esphome.components.zephyr') " + "if m in sys.modules))" + ) + result = subprocess.run( + [sys.executable, "-c", check], capture_output=True, text=True, check=True + ) + assert result.stdout.strip() == ""