[espnow] Validate radio-less variants need esp32_hosted; add P4 compile test

ESP-NOW rides the Wi-Fi PHY, so on a radio-less esp32 variant the espnow
component would otherwise fail with an inscrutable "undefined reference to
esp_now_*" at link time. Fail fast in final validation instead: the P4 must
have the esp32_hosted shim, and other radio-less variants have no ESP-NOW
path at all. Adds a P4 esp32_hosted + espnow compile test and unit tests for
the new validation.
This commit is contained in:
Jesse Hills
2026-07-21 11:44:50 +12:00
parent b6fff16930
commit be1dce26fd
3 changed files with 73 additions and 0 deletions
+20
View File
@@ -1,6 +1,7 @@
from esphome import automation, core
import esphome.codegen as cg
from esphome.components import wifi
from esphome.components.esp32 import VARIANT_ESP32P4, get_esp32_variant
from esphome.components.udp import CONF_ON_RECEIVE
import esphome.config_validation as cv
from esphome.const import (
@@ -14,6 +15,7 @@ from esphome.const import (
CONF_WIFI,
)
from esphome.core import HexInt
import esphome.final_validate as fv
from esphome.types import ConfigType
CODEOWNERS = ["@jesserockz"]
@@ -129,6 +131,24 @@ CONFIG_SCHEMA = cv.All(
)
def _validate_variant(config: ConfigType) -> ConfigType:
# ESP-NOW rides the Wi-Fi PHY. Radio-less esp32 variants have no native
# ESP-NOW; only the ESP32-P4 has a path, via the esp32_hosted shim that
# supplies the esp_now_* symbols. Fail here with a clear message instead of
# letting the build reach an "undefined reference to esp_now_*" link error.
variant = get_esp32_variant()
if wifi.variant_has_wifi(variant):
return config
if variant != VARIANT_ESP32P4:
raise cv.Invalid(f"ESP-NOW is not supported on {variant} (no Wi-Fi radio)")
if "esp32_hosted" not in fv.full_config.get():
raise cv.Invalid(f"ESP-NOW on {variant} requires the esp32_hosted component")
return config
FINAL_VALIDATE_SCHEMA = _validate_variant
async def _trigger_to_code(config):
if address := config.get(CONF_ADDRESS):
address = address.parts
@@ -0,0 +1,5 @@
# Exercises the ESP-NOW-over-hosted shim: on the ESP32-P4 host, esp32_hosted
# supplies the esp_now_* symbols that the espnow component links against.
packages:
esp32_hosted: !include common.yaml
espnow: !include ../espnow/common.yaml
@@ -0,0 +1,48 @@
"""Tests for the espnow component's final validation."""
import pytest
from esphome.components.esp32.const import (
VARIANT_ESP32C3,
VARIANT_ESP32H2,
VARIANT_ESP32P4,
)
from esphome.components.espnow import _validate_variant
import esphome.config_validation as cv
import esphome.final_validate as fv
from esphome.types import ConfigType
def _run(
monkeypatch, variant: str, full_config: dict, config: ConfigType
) -> ConfigType:
monkeypatch.setattr("esphome.components.espnow.get_esp32_variant", lambda: variant)
token = fv.full_config.set(full_config)
try:
return _validate_variant(config)
finally:
fv.full_config.reset(token)
def test_variant_with_native_wifi_passes(monkeypatch) -> None:
"""A variant with a native Wi-Fi PHY needs no shim; config passes through."""
config = {"id": "espnow"}
assert _run(monkeypatch, VARIANT_ESP32C3, {}, config) is config
def test_radioless_non_p4_variant_rejected(monkeypatch) -> None:
"""Radio-less variants without any ESP-NOW path are rejected outright."""
with pytest.raises(cv.Invalid, match="not supported"):
_run(monkeypatch, VARIANT_ESP32H2, {}, {})
def test_p4_without_esp32_hosted_rejected(monkeypatch) -> None:
"""The P4 needs the esp32_hosted shim to supply the esp_now_* symbols."""
with pytest.raises(cv.Invalid, match="esp32_hosted"):
_run(monkeypatch, VARIANT_ESP32P4, {}, {})
def test_p4_with_esp32_hosted_passes(monkeypatch) -> None:
"""The P4 with esp32_hosted present validates; config passes through."""
config = {"id": "espnow"}
assert _run(monkeypatch, VARIANT_ESP32P4, {"esp32_hosted": {}}, config) is config