From ed002feee9c0e37ef1f887b5b99d28ccf5f05003 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Aug 2026 12:19:43 -0500 Subject: [PATCH] Address review: mid-fan-out release, self-enforcing cap, scaffolding note - The fan-out early return releases the borrowed table itself instead of relying on the backend's own teardown release - A registered non-esp32 backend platform without a slot cap now fails validation loudly instead of failing open (the test pin remains) - new_gatt_backend's docstring says the service_table flag is forward scaffolding for the first esp32 direct consumer, not load-bearing on any current build --- esphome/components/ble_client/ble_client_gatt.cpp | 6 +++++- esphome/components/bluetooth_connection/__init__.py | 10 ++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) 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: