diff --git a/esphome/components/ble_client/ble_client_gatt.cpp b/esphome/components/ble_client/ble_client_gatt.cpp index c5744b3745..809768fde2 100644 --- a/esphome/components/ble_client/ble_client_gatt.cpp +++ b/esphome/components/ble_client/ble_client_gatt.cpp @@ -157,7 +157,11 @@ void BLEClient::on_service_discovery_done(int error) { for (auto *node : this->nodes_) { node->on_connected(table); if (this->state_ != State::CONNECTED) { - return; // A node tore the link down mid-fan-out; the teardown settled. + // A node tore the link down mid-fan-out; the teardown settled. Release + // the borrowed table here since the normal release below is skipped + // (idempotent when the backend released on teardown already). + this->backend_->release_services(); + return; } } this->backend_->release_services(); diff --git a/esphome/components/bluetooth_connection/__init__.py b/esphome/components/bluetooth_connection/__init__.py index 6c8da1da6e..62ac8be41a 100644 --- a/esphome/components/bluetooth_connection/__init__.py +++ b/esphome/components/bluetooth_connection/__init__.py @@ -182,6 +182,16 @@ def _validate_slot_totals(config: ConfigType) -> ConfigType: if CORE.testing_mode: return config if (cap := HUB_MAX_CONNECTIONS.get(CORE.target_platform)) is None: + # esp32's budget lives in esp32_ble; any other registered backend + # platform must carry a cap here or fail loudly, never fail open. + if ( + CORE.target_platform != PLATFORM_ESP32 + and CORE.target_platform in _PLATFORM_BACKENDS + ): + raise cv.Invalid( + f"{CORE.target_platform} has a GATT backend but no slot cap " + "in HUB_MAX_CONNECTIONS" + ) return config claimed = _ledger().consumers if len(claimed) > cap: