From d143df3c4941125c8693be542a1f545f74fd184e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 7 May 2026 14:59:39 -0500 Subject: [PATCH] [wifi/rp2040] Add stable wifi-capability helpers for device-builder Add public helpers so the device-builder dashboard backend (esphome/device-builder) doesn't have to import the internal NO_WIFI_VARIANTS list / BOARDS dict to decide whether its basic- setup wizard should emit a wifi: block: - esphome.components.wifi.has_native_wifi(*, platform, board=None, variant=None) - central dispatcher that takes whichever platform-relevant fields the caller has and routes to the right per-platform check internally. As ESPHome adds new platforms (5-6 planned), this is the single place to teach them about Wi-Fi capability - external tooling does not have to grow a parallel per-platform switch. - esphome.components.wifi.variant_has_wifi(variant) - building block for the ESP32 branch (False for H2 / P4 which require esp32_hosted). - esphome.components.rp2040.board_id_has_wifi(board_id) - building block for the RP2040 branch (existing no-arg board_has_wifi() unchanged, now delegates to the new helper). New name (rather than overloading board_has_wifi) so device-builder can feature- detect ESPHome support via try/except ImportError on the new symbol - an older ESPHome would import board_has_wifi successfully but raise TypeError on the explicit-arg call, defeating the fallback. All three wrap implementation-detail tables (NO_WIFI_VARIANTS, the generated BOARDS dict) with explicit-arg signatures + tests, so device-builder pins against a stable surface and a future refactor of the underlying tables doesn't silently break the wizard. Same shape as the device-builder API stabilisation in #16206 and #16290. --- esphome/components/rp2040/__init__.py | 16 +++- esphome/components/wifi/__init__.py | 61 +++++++++++++- tests/unit_tests/components/test_rp2040.py | 29 +++++++ tests/unit_tests/components/test_wifi.py | 96 ++++++++++++++++++++++ 4 files changed, 199 insertions(+), 3 deletions(-) create mode 100644 tests/unit_tests/components/test_rp2040.py create mode 100644 tests/unit_tests/components/test_wifi.py diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 79ed00cb41..7e450578cd 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -48,7 +48,21 @@ def board_has_wifi() -> bool: Returns True for unknown/custom boards to avoid rejecting valid configurations for boards not in the generated list. """ - board_info = boards.BOARDS.get(get_board()) + return board_id_has_wifi(get_board()) + + +def board_id_has_wifi(board_id: str) -> bool: + """Return True if *board_id* has WiFi (CYW43 wireless chip). + + Returns True for unknown/custom boards to avoid rejecting valid + configurations for boards not in the generated list. + + Used by device-builder (esphome/device-builder) — separate + explicit-arg helper so callers outside the compile pipeline + don't need ``CORE`` set up to query the board map. Please keep + the signature stable. + """ + board_info = boards.BOARDS.get(board_id) if board_info is None: return True return board_info.get("wifi", False) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 69544f3636..316d432140 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -67,9 +67,66 @@ _LOGGER = logging.getLogger(__name__) AUTO_LOAD = ["network"] -_LOGGER = logging.getLogger(__name__) - NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4] + + +def variant_has_wifi(variant: str) -> bool: + """Return True if *variant* has a native WiFi PHY. + + Variants without a native PHY (ESP32-H2, ESP32-P4) need the + ``esp32_hosted`` co-processor to use ``wifi:``. + + Used by device-builder (esphome/device-builder) to decide whether + its basic-setup wizard emits a ``wifi:`` block — please keep the + signature stable. + """ + return variant not in NO_WIFI_VARIANTS + + +_WIFI_FIRST_PLATFORMS: frozenset[str] = frozenset( + { + Platform.ESP8266, + Platform.BK72XX, + Platform.RTL87XX, + Platform.LN882X, + # Legacy umbrella key for the LibreTiny families (bk72xx / + # rtl87xx / ln882x); still produced by older configs that + # haven't migrated to the per-family keys. + Platform.LIBRETINY_OLDSTYLE, + } +) + + +def has_native_wifi( + *, platform: str, board: str | None = None, variant: str | None = None +) -> bool: + """Return True when the given platform/board/variant has native WiFi. + + Single dispatch entry point for tooling that needs to decide + whether emitting a ``wifi:`` block produces a compilable + config. Caller passes whichever platform-relevant fields they + have (``variant`` for ESP32, ``board`` for RP2040), and this + function routes to the right per-platform check internally. + + Allowlist-based: unknown / Wi-Fi-less platforms (``host``, + ``nrf52``) return False so a future platform added to ESPHome + fails closed in external tooling rather than silently emitting + a ``wifi:`` block the new platform's component would reject. + + Used by device-builder (esphome/device-builder)'s basic-setup + wizard. Centralised here so callers don't have to special-case + each platform — as ESPHome adds new platforms, this dispatcher + is the one place to teach them about Wi-Fi capability. + """ + if platform == Platform.ESP32: + return variant_has_wifi(variant) if variant else True + if platform == Platform.RP2040: + from esphome.components.rp2040 import board_id_has_wifi + + return board_id_has_wifi(board) if board else True + return platform in _WIFI_FIRST_PLATFORMS + + CONF_SAVE = "save" CONF_BAND_MODE = "band_mode" CONF_MIN_AUTH_MODE = "min_auth_mode" diff --git a/tests/unit_tests/components/test_rp2040.py b/tests/unit_tests/components/test_rp2040.py new file mode 100644 index 0000000000..25a9ade567 --- /dev/null +++ b/tests/unit_tests/components/test_rp2040.py @@ -0,0 +1,29 @@ +"""Tests for RP2040 component public helpers.""" + +from esphome.components.rp2040 import board_id_has_wifi + + +def test_board_id_has_wifi_for_known_wifi_board() -> None: + """``rpipicow`` is the canonical Pico W → True.""" + assert board_id_has_wifi("rpipicow") is True + + +def test_board_id_has_wifi_for_known_non_wifi_board() -> None: + """Plain ``rpipico`` has no CYW43 → False.""" + assert board_id_has_wifi("rpipico") is False + + +def test_board_id_has_wifi_for_rp2350_w_variant() -> None: + """``rpipico2w`` is the RP2350 Pico 2 W → True.""" + assert board_id_has_wifi("rpipico2w") is True + + +def test_board_id_has_wifi_for_unknown_board_returns_true() -> None: + """Unknown ids fail open so a custom board is not rejected. + + The validator falls back to ESPHome's compile-time check; the + helper returning True here means the wizard emits a ``wifi:`` + block and any genuinely-unsupported config trips the existing + "no CYW43" guard at compile time. + """ + assert board_id_has_wifi("not-a-real-board-id") is True diff --git a/tests/unit_tests/components/test_wifi.py b/tests/unit_tests/components/test_wifi.py new file mode 100644 index 0000000000..e93ae4b503 --- /dev/null +++ b/tests/unit_tests/components/test_wifi.py @@ -0,0 +1,96 @@ +"""Tests for WiFi component public helpers.""" + +import pytest + +from esphome.components.esp32 import const +from esphome.components.wifi import has_native_wifi, variant_has_wifi +from esphome.const import Platform + + +@pytest.mark.parametrize( + "variant", + [ + const.VARIANT_ESP32, + const.VARIANT_ESP32S2, + const.VARIANT_ESP32S3, + const.VARIANT_ESP32C3, + const.VARIANT_ESP32C6, + ], +) +def test_variant_has_wifi_for_native_phy_variants(variant: str) -> None: + """Variants with a native WiFi PHY → True.""" + assert variant_has_wifi(variant) is True + + +@pytest.mark.parametrize( + "variant", + [ + const.VARIANT_ESP32H2, + const.VARIANT_ESP32P4, + ], +) +def test_variant_has_wifi_for_no_phy_variants(variant: str) -> None: + """Variants that need ``esp32_hosted`` → False.""" + assert variant_has_wifi(variant) is False + + +def test_has_native_wifi_dispatches_esp32_to_variant_check() -> None: + """ESP32 platform routes through ``variant_has_wifi``.""" + assert ( + has_native_wifi(platform=Platform.ESP32, variant=const.VARIANT_ESP32C3) is True + ) + assert ( + has_native_wifi(platform=Platform.ESP32, variant=const.VARIANT_ESP32H2) is False + ) + + +def test_has_native_wifi_dispatches_rp2040_to_board_check() -> None: + """RP2040 platform routes through ``rp2040.board_id_has_wifi``.""" + assert has_native_wifi(platform=Platform.RP2040, board="rpipicow") is True + assert has_native_wifi(platform=Platform.RP2040, board="rpipico") is False + + +def test_has_native_wifi_returns_false_for_nrf52() -> None: + """nRF52 family is BLE-only — no Wi-Fi PHY in the platform.""" + assert has_native_wifi(platform=Platform.NRF52) is False + + +def test_has_native_wifi_returns_false_for_host() -> None: + """``host`` platform compiles ESPHome to a host binary — no radio at all.""" + assert has_native_wifi(platform=Platform.HOST) is False + + +def test_has_native_wifi_returns_false_for_unknown_platform() -> None: + """Unknown platform string fails closed. + + A future platform added to ESPHome that's missed here returns + False rather than silently emitting a ``wifi:`` block external + tooling would have to compile and reject — fail-closed surfaces + the gap as an obvious "needs wifi support added" signal. + """ + assert has_native_wifi(platform="not-a-real-platform") is False + + +@pytest.mark.parametrize( + "platform", + [ + Platform.ESP8266, + Platform.BK72XX, + Platform.RTL87XX, + Platform.LN882X, + Platform.LIBRETINY_OLDSTYLE, + ], +) +def test_has_native_wifi_returns_true_for_wifi_first_platforms(platform: str) -> None: + """Catch-all Wi-Fi-first platforms → True regardless of board / variant.""" + assert has_native_wifi(platform=platform) is True + + +def test_has_native_wifi_esp32_without_variant_assumes_wifi() -> None: + """ESP32 without a variant id falls open to True (the chip family default).""" + assert has_native_wifi(platform=Platform.ESP32) is True + + +def test_has_native_wifi_rp2040_without_board_assumes_wifi() -> None: + """RP2040 without a board id falls open to True (custom-board default).""" + assert has_native_wifi(platform=Platform.RP2040) is True