From dec5d0449bd1c844ba89f9c5900d48db8b17a466 Mon Sep 17 00:00:00 2001 From: plazarre Date: Mon, 27 Apr 2026 06:51:54 -0500 Subject: [PATCH 1/4] [esp32_ble_tracker] Hold COEX_PREFER_BT for the lifetime of any active connection (#16036) Co-authored-by: Paul Lazarre Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 22 ++++++++++++++----- .../esp32_ble_tracker/esp32_ble_tracker.h | 10 ++++++++- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index c7f2319d69..f57cb7f5dc 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -166,8 +166,9 @@ void ESP32BLETracker::loop() { ClientStateCounts counts = this->count_client_states_(); if (counts != this->client_state_counts_) { this->client_state_counts_ = counts; - ESP_LOGD(TAG, "connecting: %d, discovered: %d, disconnecting: %d", this->client_state_counts_.connecting, - this->client_state_counts_.discovered, this->client_state_counts_.disconnecting); + ESP_LOGD(TAG, "connecting: %d, discovered: %d, disconnecting: %d, active: %d", + this->client_state_counts_.connecting, this->client_state_counts_.discovered, + this->client_state_counts_.disconnecting, this->client_state_counts_.active); } // Scanner failure: reached when set_scanner_state_(FAILED) or scan_set_param_failed_ set @@ -190,10 +191,18 @@ void ESP32BLETracker::loop() { */ // Start scan: reached when scanner_state_ becomes IDLE (via set_scanner_state_()) and - // all clients are idle (their state changes increment version when they finish) + // no clients are in the transient CONNECTING / DISCOVERED / DISCONNECTING states + // (their state changes increment version when they finish). CONNECTED / ESTABLISHED + // clients do NOT block this branch — the coex revert below has its own active-count gate. if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting && !counts.discovered) { #ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE - this->update_coex_preference_(false); + // Only revert to BALANCE when no connections are active. Established connections + // continue to need PREFER_BT so peer GATT responses can reach us while WiFi traffic + // (advertisement upload, log streaming) competes for the shared radio. Reverting too + // early causes Bluedroid to time out at ~20s and synthesize status=133. + if (!counts.active) { + this->update_coex_preference_(false); + } #endif if (this->scan_continuous_) { this->start_scan_(false); // first = false @@ -701,9 +710,10 @@ void ESP32BLETracker::dump_config() { this->scan_active_ ? "ACTIVE" : "PASSIVE", YESNO(this->scan_continuous_)); ESP_LOGCONFIG(TAG, " Scanner State: %s\n" - " Connecting: %d, discovered: %d, disconnecting: %d", + " Connecting: %d, discovered: %d, disconnecting: %d, active: %d", this->scanner_state_to_string_(this->scanner_state_), this->client_state_counts_.connecting, - this->client_state_counts_.discovered, this->client_state_counts_.disconnecting); + this->client_state_counts_.discovered, this->client_state_counts_.disconnecting, + this->client_state_counts_.active); if (this->scan_start_fail_count_) { ESP_LOGCONFIG(TAG, " Scan Start Fail Count: %d", this->scan_start_fail_count_); } diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 43405b02b7..78ff60f374 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -160,9 +160,13 @@ struct ClientStateCounts { uint8_t connecting = 0; uint8_t discovered = 0; uint8_t disconnecting = 0; + // CONNECTED + ESTABLISHED clients. Tracked so coex stays at PREFER_BT + // while active connections may still need to send/receive GATT traffic. + uint8_t active = 0; bool operator==(const ClientStateCounts &other) const { - return connecting == other.connecting && discovered == other.discovered && disconnecting == other.disconnecting; + return connecting == other.connecting && discovered == other.discovered && disconnecting == other.disconnecting && + active == other.active; } bool operator!=(const ClientStateCounts &other) const { return !(*this == other); } @@ -381,6 +385,10 @@ class ESP32BLETracker : public Component, case ClientState::CONNECTING: counts.connecting++; break; + case ClientState::CONNECTED: + case ClientState::ESTABLISHED: + counts.active++; + break; default: break; } From 24c6a0d711ea99aee1bb57c9f8336a8597e89a1c Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Mon, 27 Apr 2026 08:17:02 -0400 Subject: [PATCH 2/4] [audio] Bump microDecoder library to v0.2.0 (#16054) --- esphome/components/audio/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index fee582ca25..fe111be31e 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -220,7 +220,7 @@ async def to_code(config): data = _get_data() if data.micro_decoder_support: - add_idf_component(name="esphome/micro-decoder", ref="0.1.1") + add_idf_component(name="esphome/micro-decoder", ref="0.2.0") # All codecs are enabled by default in micro-decoder, so disable the ones that aren't requested to save flash if not data.flac_support: diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 11531e6d7b..cb7f5903cf 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -6,7 +6,7 @@ dependencies: esphome/esp-micro-speech-features: version: 1.2.3 esphome/micro-decoder: - version: 0.1.1 + version: 0.2.0 esphome/micro-flac: version: 0.1.1 esphome/micro-opus: From e232325a33a986fc2c9ced917760f0863cb8583b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Apr 2026 07:43:21 -0500 Subject: [PATCH 3/4] [wifi] Add phy_mode option for ESP8266 --- esphome/components/wifi/__init__.py | 15 +++++++++++++++ esphome/components/wifi/wifi_component.h | 18 ++++++++++++++++++ .../components/wifi/wifi_component_esp8266.cpp | 13 +++++++++++++ esphome/core/defines.h | 1 + tests/components/wifi/test.esp8266-ard.yaml | 1 + 5 files changed, 48 insertions(+) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index bc4e177219..65bd5f775c 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -73,6 +73,7 @@ NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4] CONF_SAVE = "save" CONF_BAND_MODE = "band_mode" CONF_MIN_AUTH_MODE = "min_auth_mode" +CONF_PHY_MODE = "phy_mode" CONF_POST_CONNECT_ROAMING = "post_connect_roaming" # Maximum number of WiFi networks that can be configured @@ -112,6 +113,13 @@ WIFI_MIN_AUTH_MODES = { "WPA3": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA3, } VALIDATE_WIFI_MIN_AUTH_MODE = cv.enum(WIFI_MIN_AUTH_MODES, upper=True) + +WiFi8266PhyMode = wifi_ns.enum("WiFi8266PhyMode") +WIFI_8266_PHY_MODES = { + "11B": WiFi8266PhyMode.WIFI_8266_PHY_MODE_11B, + "11G": WiFi8266PhyMode.WIFI_8266_PHY_MODE_11G, + "11N": WiFi8266PhyMode.WIFI_8266_PHY_MODE_11N, +} WiFiConnectedCondition = wifi_ns.class_("WiFiConnectedCondition", Condition) WiFiEnabledCondition = wifi_ns.class_("WiFiEnabledCondition", Condition) WiFiAPActiveCondition = wifi_ns.class_("WiFiAPActiveCondition", Condition) @@ -406,6 +414,10 @@ CONFIG_SCHEMA = cv.All( cv.only_on_esp32, only_on_variant(supported=[const.VARIANT_ESP32C5]), ), + cv.Optional(CONF_PHY_MODE): cv.All( + cv.enum(WIFI_8266_PHY_MODES, upper=True), + cv.only_on_esp8266, + ), cv.Optional(CONF_PASSIVE_SCAN, default=False): cv.boolean, cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, cv.Optional(CONF_POST_CONNECT_ROAMING, default=True): cv.boolean, @@ -569,6 +581,9 @@ async def to_code(config): if CORE.is_esp8266: cg.add_library("ESP8266WiFi", None) + if CONF_PHY_MODE in config: + cg.add_define("USE_WIFI_PHY_MODE") + cg.add(var.set_phy_mode(config[CONF_PHY_MODE])) elif CORE.is_rp2040: cg.add_library("WiFi", None) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 53fb0728fb..ef301e3d4f 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -345,6 +345,15 @@ enum WifiMinAuthMode : uint8_t { WIFI_MIN_AUTH_MODE_WPA3, }; +#ifdef USE_WIFI_PHY_MODE +// Values match ESP8266 SDK phy_mode_t (PHY_MODE_11B=1, PHY_MODE_11G=2, PHY_MODE_11N=3). +enum WiFi8266PhyMode : uint8_t { + WIFI_8266_PHY_MODE_11B = 1, + WIFI_8266_PHY_MODE_11G = 2, + WIFI_8266_PHY_MODE_11N = 3, +}; +#endif + #ifdef USE_ESP32 struct IDFWiFiEvent; #endif @@ -455,6 +464,9 @@ class WiFiComponent final : public Component { #if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) void set_band_mode(wifi_band_mode_t band_mode) { this->band_mode_ = band_mode; } #endif +#ifdef USE_WIFI_PHY_MODE + void set_phy_mode(WiFi8266PhyMode phy_mode) { this->phy_mode_ = phy_mode; } +#endif void set_passive_scan(bool passive); @@ -672,6 +684,9 @@ class WiFiComponent final : public Component { bool wifi_apply_power_save_(); #if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) bool wifi_apply_band_mode_(); +#endif +#ifdef USE_WIFI_PHY_MODE + bool wifi_apply_phy_mode_(); #endif bool wifi_sta_ip_config_(const optional &manual_ip); bool wifi_apply_hostname_(); @@ -810,6 +825,9 @@ class WiFiComponent final : public Component { WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; #if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) wifi_band_mode_t band_mode_{WIFI_BAND_MODE_AUTO}; +#endif +#ifdef USE_WIFI_PHY_MODE + WiFi8266PhyMode phy_mode_{WIFI_8266_PHY_MODE_11N}; #endif WifiMinAuthMode min_auth_mode_{WIFI_MIN_AUTH_MODE_WPA2}; WiFiRetryPhase retry_phase_{WiFiRetryPhase::INITIAL_CONNECT}; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index bf3a0d2949..f2ae69f596 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -621,10 +621,23 @@ bool WiFiComponent::wifi_sta_pre_setup_() { ESP_LOGV(TAG, "Disabling Auto-Connect failed"); } +#ifdef USE_WIFI_PHY_MODE + if (!this->wifi_apply_phy_mode_()) { + ESP_LOGV(TAG, "Setting PHY Mode failed"); + } +#endif + delay(10); return true; } +#ifdef USE_WIFI_PHY_MODE +bool WiFiComponent::wifi_apply_phy_mode_() { + // Values of WiFi8266PhyMode are aligned with the SDK's phy_mode_t enum. + return wifi_set_phy_mode(static_cast(this->phy_mode_)); +} +#endif + void WiFiComponent::wifi_pre_setup_() { wifi_set_event_handler_cb(&WiFiComponent::wifi_event_callback); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index f929b224ca..c1025e3cb2 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -282,6 +282,7 @@ #define USE_CAPTIVE_PORTAL_GZIP #define USE_WIFI_11KV_SUPPORT #define USE_WIFI_FAST_CONNECT +#define USE_WIFI_PHY_MODE #define USE_WIFI_IP_STATE_LISTENERS #define USE_WIFI_SCAN_RESULTS_LISTENERS #define USE_WIFI_CONNECT_STATE_LISTENERS diff --git a/tests/components/wifi/test.esp8266-ard.yaml b/tests/components/wifi/test.esp8266-ard.yaml index 709a639ad6..ffeec136d3 100644 --- a/tests/components/wifi/test.esp8266-ard.yaml +++ b/tests/components/wifi/test.esp8266-ard.yaml @@ -1,6 +1,7 @@ wifi: min_auth_mode: WPA2 post_connect_roaming: true + phy_mode: 11G packages: - !include common.yaml From 4f4ad64f19bd0e50315ee19299a32878d59cb9b9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Apr 2026 07:49:29 -0500 Subject: [PATCH 4/4] [wifi] Add explicit AUTO option to phy_mode --- esphome/components/wifi/__init__.py | 1 + esphome/components/wifi/wifi_component.h | 6 ++++-- esphome/components/wifi/wifi_component_esp8266.cpp | 2 ++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 65bd5f775c..69544f3636 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -116,6 +116,7 @@ VALIDATE_WIFI_MIN_AUTH_MODE = cv.enum(WIFI_MIN_AUTH_MODES, upper=True) WiFi8266PhyMode = wifi_ns.enum("WiFi8266PhyMode") WIFI_8266_PHY_MODES = { + "AUTO": WiFi8266PhyMode.WIFI_8266_PHY_MODE_AUTO, "11B": WiFi8266PhyMode.WIFI_8266_PHY_MODE_11B, "11G": WiFi8266PhyMode.WIFI_8266_PHY_MODE_11G, "11N": WiFi8266PhyMode.WIFI_8266_PHY_MODE_11N, diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index ef301e3d4f..0437267a1f 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -346,8 +346,10 @@ enum WifiMinAuthMode : uint8_t { }; #ifdef USE_WIFI_PHY_MODE -// Values match ESP8266 SDK phy_mode_t (PHY_MODE_11B=1, PHY_MODE_11G=2, PHY_MODE_11N=3). +// Values 1-3 match ESP8266 SDK phy_mode_t (PHY_MODE_11B=1, PHY_MODE_11G=2, PHY_MODE_11N=3). +// AUTO leaves the SDK at its default (no wifi_set_phy_mode() call). enum WiFi8266PhyMode : uint8_t { + WIFI_8266_PHY_MODE_AUTO = 0, WIFI_8266_PHY_MODE_11B = 1, WIFI_8266_PHY_MODE_11G = 2, WIFI_8266_PHY_MODE_11N = 3, @@ -827,7 +829,7 @@ class WiFiComponent final : public Component { wifi_band_mode_t band_mode_{WIFI_BAND_MODE_AUTO}; #endif #ifdef USE_WIFI_PHY_MODE - WiFi8266PhyMode phy_mode_{WIFI_8266_PHY_MODE_11N}; + WiFi8266PhyMode phy_mode_{WIFI_8266_PHY_MODE_AUTO}; #endif WifiMinAuthMode min_auth_mode_{WIFI_MIN_AUTH_MODE_WPA2}; WiFiRetryPhase retry_phase_{WiFiRetryPhase::INITIAL_CONNECT}; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index f2ae69f596..9d2c1bb1be 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -633,6 +633,8 @@ bool WiFiComponent::wifi_sta_pre_setup_() { #ifdef USE_WIFI_PHY_MODE bool WiFiComponent::wifi_apply_phy_mode_() { + if (this->phy_mode_ == WIFI_8266_PHY_MODE_AUTO) + return true; // Values of WiFi8266PhyMode are aligned with the SDK's phy_mode_t enum. return wifi_set_phy_mode(static_cast(this->phy_mode_)); }