From 246d455dc34ba24df0b9adacdc7094f48ae0958e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 09:28:50 -0600 Subject: [PATCH 1/7] [esp32_ble_client] Complete disconnection on failed OPEN_EVT in DISCONNECTING state When a connection fails to establish, the ESP-IDF stack sends DISCONNECT_EVT followed by OPEN_EVT with a failure status (e.g. 133). No CLOSE_EVT follows since no GATT connection was established. Previously the OPEN_EVT in DISCONNECTING state was silently ignored, leaving the client stuck in DISCONNECTING forever. Now we treat this failed OPEN_EVT as the terminal event and transition to IDLE. --- .../esp32_ble_client/ble_client_base.cpp | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 9370343c9c..3b35420d7b 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -295,10 +295,19 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ // ESP-IDF's BLE stack may send ESP_GATTC_OPEN_EVT after esp_ble_gattc_open() returns an // error, if the error occurred at the BTA/GATT layer. This can result in the event // arriving after we've already transitioned to IDLE state. - // It may also arrive during DISCONNECTING if the controller is still cleaning up. - if (this->state() == espbt::ClientState::IDLE || this->state() == espbt::ClientState::DISCONNECTING) { - ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in %s state (status=%d), ignoring", this->connection_index_, - this->address_str_, espbt::client_state_to_string(this->state()), param->open.status); + if (this->state() == espbt::ClientState::IDLE) { + ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in IDLE state (status=%d), ignoring", this->connection_index_, + this->address_str_, param->open.status); + break; + } else if (this->state() == espbt::ClientState::DISCONNECTING) { + // When a connection fails to establish, the ESP-IDF stack sends DISCONNECT_EVT + // (which we now transition to DISCONNECTING) followed by OPEN_EVT with a failure status. + // No CLOSE_EVT will follow since no GATT connection was established, so this + // failed OPEN_EVT is the terminal event — transition to IDLE here. + ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in DISCONNECTING state (status=%d), completing disconnection", + this->connection_index_, this->address_str_, param->open.status); + this->set_state(espbt::ClientState::IDLE); + this->conn_id_ = UNSET_CONN_ID; break; } From c75b7de2d3cb284545bd4d0e091c2ff1331e33f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 09:32:13 -0600 Subject: [PATCH 2/7] [esp32_ble_client] Let failed OPEN_EVT in DISCONNECTING state fall through to existing handler Instead of a separate early-return for DISCONNECTING state, let the failed OPEN_EVT fall through to the existing failed-status handler which already transitions to IDLE. --- .../components/esp32_ble_client/ble_client_base.cpp | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 3b35420d7b..026d55e1c6 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -299,16 +299,6 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in IDLE state (status=%d), ignoring", this->connection_index_, this->address_str_, param->open.status); break; - } else if (this->state() == espbt::ClientState::DISCONNECTING) { - // When a connection fails to establish, the ESP-IDF stack sends DISCONNECT_EVT - // (which we now transition to DISCONNECTING) followed by OPEN_EVT with a failure status. - // No CLOSE_EVT will follow since no GATT connection was established, so this - // failed OPEN_EVT is the terminal event — transition to IDLE here. - ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in DISCONNECTING state (status=%d), completing disconnection", - this->connection_index_, this->address_str_, param->open.status); - this->set_state(espbt::ClientState::IDLE); - this->conn_id_ = UNSET_CONN_ID; - break; } if (this->state() != espbt::ClientState::CONNECTING) { From 39c5cffaa13420f0cf0ec90bef6b32628582f614 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 09:32:55 -0600 Subject: [PATCH 3/7] [esp32_ble_client] Reset conn_id on failed OPEN_EVT to prevent stale match When OPEN_EVT arrives with a failure status, the connection was never established so no CLOSE_EVT may follow. Reset conn_id_ to prevent a stale value from matching a future CLOSE_EVT. --- esphome/components/esp32_ble_client/ble_client_base.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 026d55e1c6..6bf0a87b1e 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -311,6 +311,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { this->log_gattc_warning_("Connection open", param->open.status); this->set_state(espbt::ClientState::IDLE); + this->conn_id_ = UNSET_CONN_ID; break; } if (this->want_disconnect_) { From a5fc6b480a4ed509d4c5a811681ee60fcfa8b2fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 09:33:49 -0600 Subject: [PATCH 4/7] [esp32_ble_client] Add comment for conn_id reset on failed OPEN_EVT --- esphome/components/esp32_ble_client/ble_client_base.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 6bf0a87b1e..4ec93f6fd7 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -311,6 +311,8 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { this->log_gattc_warning_("Connection open", param->open.status); this->set_state(espbt::ClientState::IDLE); + // Reset conn_id since the connection was never established and + // CLOSE_EVT may not follow to clean it up this->conn_id_ = UNSET_CONN_ID; break; } From 22d9f5003477ff6e5f8ce41ba1823c1836d3823a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 09:34:57 -0600 Subject: [PATCH 5/7] [esp32_ble_client] Extract set_idle_() helper for IDLE + conn_id reset Consolidate the repeated set_state(IDLE) + conn_id_ = UNSET_CONN_ID pattern into a single helper to ensure they always stay paired. --- .../esp32_ble_client/ble_client_base.cpp | 14 ++++++++------ .../components/esp32_ble_client/ble_client_base.h | 2 ++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 4ec93f6fd7..67e30c3634 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -236,6 +236,11 @@ void BLEClientBase::log_warning_(const char *message) { ESP_LOGW(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, message); } +void BLEClientBase::set_idle_() { + this->set_state(espbt::ClientState::IDLE); + this->conn_id_ = UNSET_CONN_ID; +} + void BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, const char *param_type) { esp_ble_conn_update_params_t conn_params = {{0}}; @@ -310,10 +315,8 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { this->log_gattc_warning_("Connection open", param->open.status); - this->set_state(espbt::ClientState::IDLE); - // Reset conn_id since the connection was never established and - // CLOSE_EVT may not follow to clean it up - this->conn_id_ = UNSET_CONN_ID; + // Connection was never established so CLOSE_EVT may not follow + this->set_idle_(); break; } if (this->want_disconnect_) { @@ -394,8 +397,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ return false; this->log_gattc_lifecycle_event_("CLOSE"); this->release_services(); - this->set_state(espbt::ClientState::IDLE); - this->conn_id_ = UNSET_CONN_ID; + this->set_idle_(); break; } case ESP_GATTC_SEARCH_RES_EVT: { diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index c2336b2349..62d6704bba 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -137,6 +137,8 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { void log_gattc_warning_(const char *operation, esp_err_t err); void log_connection_params_(const char *param_type); void handle_connection_result_(esp_err_t ret); + /// Transition to IDLE and reset conn_id — call when the connection is fully dead. + void set_idle_(); // Compact error logging helpers to reduce flash usage void log_error_(const char *message); void log_error_(const char *message, int code); From a0b69effe4a9d0c3a6f9985778a7da2874e084bb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 09:35:47 -0600 Subject: [PATCH 6/7] [esp32_ble_client] Move set_idle_() to header for inlining --- esphome/components/esp32_ble_client/ble_client_base.cpp | 5 ----- esphome/components/esp32_ble_client/ble_client_base.h | 5 ++++- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 67e30c3634..906113aec3 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -236,11 +236,6 @@ void BLEClientBase::log_warning_(const char *message) { ESP_LOGW(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, message); } -void BLEClientBase::set_idle_() { - this->set_state(espbt::ClientState::IDLE); - this->conn_id_ = UNSET_CONN_ID; -} - void BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, const char *param_type) { esp_ble_conn_update_params_t conn_params = {{0}}; diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index 62d6704bba..9b7fc7b5ed 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -138,7 +138,10 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { void log_connection_params_(const char *param_type); void handle_connection_result_(esp_err_t ret); /// Transition to IDLE and reset conn_id — call when the connection is fully dead. - void set_idle_(); + void set_idle_() { + this->set_state(espbt::ClientState::IDLE); + this->conn_id_ = UNSET_CONN_ID; + } // Compact error logging helpers to reduce flash usage void log_error_(const char *message); void log_error_(const char *message, int code); From 1dba01e05b92b8274acf4da78aae532a14037803 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 09:48:23 -0600 Subject: [PATCH 7/7] [esp32_ble_client] Don't reset conn_id_ in want_disconnect_ path The CLOSE_EVT handler matches on conn_id_ to call set_idle_(). Resetting conn_id_ to UNSET_CONN_ID before CLOSE_EVT arrives causes the event to be dropped, leaving the client stuck in DISCONNECTING state permanently. Co-Authored-By: Claude Opus 4.6 --- esphome/components/esp32_ble_client/ble_client_base.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 906113aec3..72b3ebaa5a 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -318,8 +318,8 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ // Disconnect was requested after connecting started, // but before the connection was established. Now that we have // this->conn_id_ set, we can disconnect it. + // Don't reset conn_id_ here — CLOSE_EVT needs it to match and call set_idle_(). this->unconditional_disconnect(); - this->conn_id_ = UNSET_CONN_ID; break; } // MTU negotiation already started in ESP_GATTC_CONNECT_EVT