From a2a7a6add627f011ec2caacf776613a4c635856a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 21 Aug 2026 13:51:41 -0500 Subject: [PATCH] [esp32] Exclude the WiFi and Bluetooth stacks from builds that do not use them --- esphome/components/esp32/__init__.py | 6 ++++++ esphome/components/esp32_ble/__init__.py | 2 ++ esphome/components/esp32_ble_tracker/__init__.py | 2 ++ esphome/components/espnow/__init__.py | 5 +++++ esphome/components/mdns/__init__.py | 9 ++++++++- esphome/components/wifi/__init__.py | 4 ++++ esphome/components/zigbee/zigbee_esp32.py | 5 +++++ .../esp32/config/exclusion_reincludes_espnow.yaml | 11 +++++++++++ .../config/exclusion_reincludes_wifi_ble.yaml | 13 +++++++++++++ tests/component_tests/esp32/test_esp32.py | 14 ++++++++++++++ 10 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 tests/component_tests/esp32/config/exclusion_reincludes_espnow.yaml create mode 100644 tests/component_tests/esp32/config/exclusion_reincludes_wifi_ble.yaml diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index d6ed6d9399..9d26818adf 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -212,11 +212,13 @@ COMPILER_OPTIMIZATIONS = { # builds that need them. DEFAULT_EXCLUDED_IDF_COMPONENTS = ( "app_trace", # CPU trace/SystemView support - unused by ESPHome + "bt", # Bluetooth stack - re-included by esp32_ble; its REQUIRES pulls the WiFi stack back "cmock", # Unit testing mock framework - ESPHome doesn't use IDF's testing "console", # Console REPL - unused by ESPHome; espressif/mdns pulls it back when configured "driver", # Legacy driver shim - only needed by esp32_touch, esp32_can for legacy headers "esp-tls", # TLS wrapper - re-included by http_request, mqtt, web_server_idf "esp_adc", # ADC driver - only needed by adc component + "esp_coex", # WiFi/BT coexistence - re-included by esp32_ble_tracker, zigbee; esp_wifi/bt pull it back "esp_driver_cam", # Camera driver - the esp32-camera managed component pulls it back "esp_driver_dac", # DAC driver - only needed by esp32_dac component "esp_driver_gptimer", # General purpose timer - re-included by ac_dimmer, opentherm, Arduino BLE libs @@ -239,8 +241,11 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = ( "esp_https_server", # HTTPS server - ESPHome has its own web server "esp_lcd", # LCD controller drivers - only needed by display component "esp_local_ctrl", # Local control over HTTPS/BLE - ESPHome has native API + "esp_phy", # RF PHY - esp_wifi/bt/ieee802154 pull it back when they are in the build + "esp_wifi", # WiFi stack - re-included by wifi, espnow; bt pulls it back for BLE builds "espcoredump", # Core dump support - ESPHome has its own debug component "fatfs", # FAT filesystem - ESPHome doesn't use filesystem storage + "ieee802154", # 802.15.4 radio - IDF openthread and the Zigbee libs pull it back "json", # cJSON library - ESPHome uses ArduinoJson instead "mqtt", # ESP-IDF MQTT library - ESPHome has its own MQTT implementation "openthread", # Thread protocol - only needed by openthread component @@ -255,6 +260,7 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = ( "unity", # Unit testing framework - ESPHome doesn't use IDF's testing "wear_levelling", # Flash wear levelling for fatfs - unused since fatfs unused "wifi_provisioning", # WiFi provisioning - ESPHome uses its own improv implementation + "wpa_supplicant", # WPA supplicant - esp_wifi pulls it back when the WiFi stack is in the build ) # Additional IDF managed components to exclude for Arduino framework builds diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 79747c6f31..1d9313e8ec 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -19,6 +19,7 @@ from esphome.components.esp32 import ( add_idf_sdkconfig_option, const, get_esp32_variant, + include_builtin_idf_component, request_bluetooth, ) from esphome.components.esp32.const import VARIANT_ESP32C2 @@ -520,6 +521,7 @@ FINAL_VALIDATE_SCHEMA = final_validation async def to_code(config: ConfigType) -> None: + include_builtin_idf_component("bt") var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) cg.add(var.set_io_capability(config[CONF_IO_CAPABILITY])) diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 4f6355df70..eaf5cbaf78 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -11,6 +11,7 @@ from esphome.components.const import CONF_ON_SCAN_END, CONF_SCAN_PARAMETERS, CON from esphome.components.esp32 import ( add_idf_sdkconfig_option, idf_version, + include_builtin_idf_component, request_bluetooth, request_software_coexistence, ) @@ -355,6 +356,7 @@ async def to_code(config: ConfigType) -> None: if config.get(CONF_SOFTWARE_COEXISTENCE): cg.add_define("USE_ESP32_BLE_SOFTWARE_COEXISTENCE") + include_builtin_idf_component("esp_coex") # This needs to be run as a job with very low priority so that all components have diff --git a/esphome/components/espnow/__init__.py b/esphome/components/espnow/__init__.py index ee3732c406..5541a6ee97 100644 --- a/esphome/components/espnow/__init__.py +++ b/esphome/components/espnow/__init__.py @@ -155,6 +155,11 @@ async def to_code(config: ConfigType) -> None: cg.add_define("USE_ESPNOW") cg.add_define("USE_ESPNOW_MAX_PAYLOAD_SIZE", config[CONF_MAX_PAYLOAD_SIZE]) + if CORE.is_esp32: + from esphome.components.esp32 import include_builtin_idf_component + + include_builtin_idf_component("esp_wifi") + if CONF_WIFI in CORE.config: # Track the Wi-Fi channel via connect events instead of polling every loop wifi.request_wifi_connect_state_listener() diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 24bce0cc3c..93d15c156d 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -1,5 +1,5 @@ import esphome.codegen as cg -from esphome.components.esp32 import add_idf_component +from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option from esphome.config_helpers import filter_source_files_from_platform, get_logger_level import esphome.config_validation as cv from esphome.const import ( @@ -209,6 +209,13 @@ async def to_code(config): if CORE.is_esp32: add_idf_component(name="espressif/mdns", ref="1.11.3") + # The mdns console CLI is never used by ESPHome + add_idf_sdkconfig_option("CONFIG_MDNS_ENABLE_CONSOLE_CLI", False) + if "wifi" not in CORE.config: + # Without WiFi the predefined STA/AP interface handlers are dead + # code; disabling them lets mdns build without the WiFi stack. + add_idf_sdkconfig_option("CONFIG_MDNS_PREDEF_NETIF_STA", False) + add_idf_sdkconfig_option("CONFIG_MDNS_PREDEF_NETIF_AP", False) cg.add_define("USE_MDNS") diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 4bb6629da1..2cb0fd79de 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -10,6 +10,7 @@ from esphome.components.esp32 import ( add_idf_sdkconfig_option, const, get_esp32_variant, + include_builtin_idf_component, only_on_variant, request_wifi, ) @@ -647,6 +648,9 @@ async def to_code(config): # drops SoftAP support / the LWIP DHCP server when AP mode is unused. if CORE.is_esp32: request_wifi(ap=CONF_AP in config) + include_builtin_idf_component("esp_wifi") + # wifi_component.cpp includes esp_eap_client.h/esp_wpa2.h + include_builtin_idf_component("wpa_supplicant") # Disable Enterprise WiFi support if no EAP is configured if CORE.is_esp32: diff --git a/esphome/components/zigbee/zigbee_esp32.py b/esphome/components/zigbee/zigbee_esp32.py index ade45e8cc3..57fa3b2a00 100644 --- a/esphome/components/zigbee/zigbee_esp32.py +++ b/esphome/components/zigbee/zigbee_esp32.py @@ -9,6 +9,7 @@ from esphome.components.esp32 import ( add_idf_component, add_idf_sdkconfig_option, add_partition, + include_builtin_idf_component, require_vfs_select, ) import esphome.config_validation as cv @@ -288,6 +289,10 @@ async def esp32_to_code(config: ConfigType) -> "MockObj": ref="2.0.4", ) + if CONF_WIFI in CORE.config: + # zigbee_esp32.cpp uses esp_coexist.h when WiFi is present + include_builtin_idf_component("esp_coex") + # add sdkconfigs later so they can overwrite esp32 defaults CORE.add_job(_zigbee_add_sdkconfigs, config) diff --git a/tests/component_tests/esp32/config/exclusion_reincludes_espnow.yaml b/tests/component_tests/esp32/config/exclusion_reincludes_espnow.yaml new file mode 100644 index 0000000000..2ede6c42df --- /dev/null +++ b/tests/component_tests/esp32/config/exclusion_reincludes_espnow.yaml @@ -0,0 +1,11 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + +espnow: + channel: 1 + auto_add_peer: true diff --git a/tests/component_tests/esp32/config/exclusion_reincludes_wifi_ble.yaml b/tests/component_tests/esp32/config/exclusion_reincludes_wifi_ble.yaml new file mode 100644 index 0000000000..883abdb5fa --- /dev/null +++ b/tests/component_tests/esp32/config/exclusion_reincludes_wifi_ble.yaml @@ -0,0 +1,13 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + ssid: MySSID + password: password1 + +esp32_ble_tracker: diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py index 7208318d3a..1bb4011575 100644 --- a/tests/component_tests/esp32/test_esp32.py +++ b/tests/component_tests/esp32/test_esp32.py @@ -268,6 +268,20 @@ def test_esp32_configuration_errors( ("esp-tls", "esp_http_client"), id="nextion", ), + pytest.param( + # esp_wifi from wifi, bt from esp32_ble, esp_coex from + # esp32_ble_tracker's software coexistence (defaults on with wifi). + # wpa_supplicant/esp_phy stay excluded; IDF requirement expansion + # pulls them back via esp_wifi. + "exclusion_reincludes_wifi_ble.yaml", + ("esp_wifi", "bt", "esp_coex"), + id="wifi_ble", + ), + pytest.param( + "exclusion_reincludes_espnow.yaml", + ("esp_wifi",), + id="espnow", + ), ], ) def test_default_exclusions_reincluded_by_owning_components(