From 2f2634bf6bfe49f1a88ac24ad03972c77925cfdb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Aug 2026 08:54:28 -0500 Subject: [PATCH] [bluetooth_connection] Move BluetoothConnection out of bluetooth_proxy (#18129) --- CODEOWNERS | 1 + .../bluetooth_connection/__init__.py | 46 +++++++++++++++++++ .../bluetooth_connection.h | 37 +++++++++++++++ .../bluetooth_connection_esp32.cpp} | 22 +++++---- .../bluetooth_connection_esp32.h} | 16 +++++-- .../components/bluetooth_proxy/__init__.py | 19 ++++---- .../bluetooth_proxy/bluetooth_proxy.cpp | 24 +++++----- .../bluetooth_proxy/bluetooth_proxy.h | 40 +++++++--------- .../bluetooth_proxy/test_platform_gates.py | 18 +++++++- 9 files changed, 163 insertions(+), 60 deletions(-) create mode 100644 esphome/components/bluetooth_connection/__init__.py create mode 100644 esphome/components/bluetooth_connection/bluetooth_connection.h rename esphome/components/{bluetooth_proxy/bluetooth_connection.cpp => bluetooth_connection/bluetooth_connection_esp32.cpp} (98%) rename esphome/components/{bluetooth_proxy/bluetooth_connection.h => bluetooth_connection/bluetooth_connection_esp32.h} (86%) diff --git a/CODEOWNERS b/CODEOWNERS index d2e26edca39..9bcbe087c54 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -78,6 +78,7 @@ esphome/components/bl0942/* @dbuezas @dwmw2 esphome/components/ble_client/* @buxtronix @clydebarrow esphome/components/ble_device_base/* @Bl00d-B0b esphome/components/ble_nus/* @tomaszduda23 +esphome/components/bluetooth_connection/* @bdraco @jesserockz esphome/components/bluetooth_proxy/* @bdraco @jesserockz esphome/components/bm8563/* @abmantis esphome/components/bme280_base/* @esphome/core diff --git a/esphome/components/bluetooth_connection/__init__.py b/esphome/components/bluetooth_connection/__init__.py new file mode 100644 index 00000000000..1e85c4b8f91 --- /dev/null +++ b/esphome/components/bluetooth_connection/__init__.py @@ -0,0 +1,46 @@ +"""Per-platform GATT connection backends the Bluetooth proxy drives. + +Auto-loaded by bluetooth_proxy, no user-facing configuration; the proxy's +codegen declares and registers the connection instances. +""" + +import functools + +import esphome.codegen as cg +from esphome.config_helpers import filter_source_files_from_platform +from esphome.const import PlatformFramework +from esphome.core import CORE + + +def AUTO_LOAD() -> list[str]: + """The esp32 connection header includes esp32_ble_client, so the closure + must be self-satisfying; no target platform (tooling) gets the union.""" + if CORE.is_esp32 or CORE.target_platform is None: + return ["ble_device_base", "esp32_ble_client"] + return ["ble_device_base"] + + +CODEOWNERS = ["@bdraco", "@jesserockz"] + +bluetooth_connection_ns = cg.esphome_ns.namespace("bluetooth_connection") + + +@functools.cache +def esp32_connection_class() -> cg.MockObjClass: + """Lazy: importing esp32_ble_client registers esp32-only automations as + an import side effect, which must not leak into other platforms.""" + from esphome.components import esp32_ble_client + + return bluetooth_connection_ns.class_( + "BluetoothConnection", esp32_ble_client.BLEClientBase + ) + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "bluetooth_connection_esp32.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + } +) diff --git a/esphome/components/bluetooth_connection/bluetooth_connection.h b/esphome/components/bluetooth_connection/bluetooth_connection.h new file mode 100644 index 00000000000..f63fb93492e --- /dev/null +++ b/esphome/components/bluetooth_connection/bluetooth_connection.h @@ -0,0 +1,37 @@ +// Shared types for the per-platform GATT connection backends and the +// Bluetooth proxy that drives them. + +#pragma once + +#include "esphome/core/defines.h" + +#include "esphome/components/ble_device_base/ble_client_state.h" + +#ifdef USE_ESP32 +#include +#endif + +namespace esphome::bluetooth_connection { + +// Connection-owned error type for the API error fields, which are plain +// integers on the wire. Aliases esp_err_t on esp32 (where the values come from +// IDF calls); a bare int elsewhere. Owning the name instead of probing for +// esp_err_t keeps the header independent of how a platform's SDK spells its +// error type. +#ifdef USE_ESP32 +using conn_err_t = esp_err_t; +static constexpr conn_err_t CONN_OK = ESP_OK; +#else +using conn_err_t = int; +static constexpr conn_err_t CONN_OK = 0; +#endif + +// The ESPHome-private "not connected" wire value, shared with the neutral +// GATT contract so backend and wrapper cannot drift. +static constexpr conn_err_t GATT_NOT_CONNECTED = ble_device_base::GATT_ERR_NOT_CONNECTED; + +// send_service_ cursor states; >= 0 is the next service index to stream. +static constexpr int DONE_SENDING_SERVICES = -2; +static constexpr int INIT_SENDING_SERVICES = -3; + +} // namespace esphome::bluetooth_connection diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_connection/bluetooth_connection_esp32.cpp similarity index 98% rename from esphome/components/bluetooth_proxy/bluetooth_connection.cpp rename to esphome/components/bluetooth_connection/bluetooth_connection_esp32.cpp index 9820977a134..5274637b66d 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_connection/bluetooth_connection_esp32.cpp @@ -1,4 +1,4 @@ -#include "bluetooth_connection.h" +#include "bluetooth_connection_esp32.h" #include "esphome/components/api/api_pb2.h" #include "esphome/core/helpers.h" @@ -6,11 +6,13 @@ #ifdef USE_ESP32 -#include "bluetooth_proxy.h" +#include "esphome/components/bluetooth_proxy/bluetooth_proxy.h" -namespace esphome::bluetooth_proxy { +namespace esphome::bluetooth_connection { -static const char *const TAG = "bluetooth_proxy.connection"; +namespace espbt = esphome::esp32_ble_tracker; + +static const char *const TAG = "bluetooth_connection"; // This function is allocation-free and directly packs UUIDs into the output array // using precalculated constants for the Bluetooth base UUID @@ -516,7 +518,7 @@ void BluetoothConnection::gap_event_handler(esp_gap_ble_cb_event_t event, esp_bl esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) { if (!this->connected()) { this->log_gatt_not_connected_("read", "characteristic"); - return ESP_GATT_NOT_CONNECTED; + return GATT_NOT_CONNECTED; } ESP_LOGV(TAG, "[%d] [%s] Reading GATT characteristic handle %d", this->connection_index_, this->address_str_, handle); @@ -529,7 +531,7 @@ esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const uint8 bool response) { if (!this->connected()) { this->log_gatt_not_connected_("write", "characteristic"); - return ESP_GATT_NOT_CONNECTED; + return GATT_NOT_CONNECTED; } ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_, handle); @@ -545,7 +547,7 @@ esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const uint8 esp_err_t BluetoothConnection::read_descriptor(uint16_t handle) { if (!this->connected()) { this->log_gatt_not_connected_("read", "descriptor"); - return ESP_GATT_NOT_CONNECTED; + return GATT_NOT_CONNECTED; } ESP_LOGV(TAG, "[%d] [%s] Reading GATT descriptor handle %d", this->connection_index_, this->address_str_, handle); @@ -556,7 +558,7 @@ esp_err_t BluetoothConnection::read_descriptor(uint16_t handle) { esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const uint8_t *data, size_t length, bool response) { if (!this->connected()) { this->log_gatt_not_connected_("write", "descriptor"); - return ESP_GATT_NOT_CONNECTED; + return GATT_NOT_CONNECTED; } ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_, handle); @@ -572,7 +574,7 @@ esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const uint8_t * esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enable) { if (!this->connected()) { this->log_gatt_not_connected_("notify", "characteristic"); - return ESP_GATT_NOT_CONNECTED; + return GATT_NOT_CONNECTED; } if (enable) { @@ -592,6 +594,6 @@ esp32_ble_tracker::AdvertisementParserType BluetoothConnection::get_advertisemen return this->proxy_->get_advertisement_parser_type(); } -} // namespace esphome::bluetooth_proxy +} // namespace esphome::bluetooth_connection #endif // USE_ESP32 diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.h b/esphome/components/bluetooth_connection/bluetooth_connection_esp32.h similarity index 86% rename from esphome/components/bluetooth_proxy/bluetooth_connection.h rename to esphome/components/bluetooth_connection/bluetooth_connection_esp32.h index e5600f6af49..65e2d0777e2 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.h +++ b/esphome/components/bluetooth_connection/bluetooth_connection_esp32.h @@ -1,12 +1,18 @@ #pragma once +#include "esphome/core/defines.h" + #ifdef USE_ESP32 #include "esphome/components/esp32_ble_client/ble_client_base.h" -namespace esphome::bluetooth_proxy { +#include "bluetooth_connection.h" +namespace esphome::bluetooth_proxy { class BluetoothProxy; +} // namespace esphome::bluetooth_proxy + +namespace esphome::bluetooth_connection { class BluetoothConnection final : public esp32_ble_client::BLEClientBase { public: @@ -31,7 +37,7 @@ class BluetoothConnection final : public esp32_ble_client::BLEClientBase { void set_address(uint64_t address) override; protected: - friend class BluetoothProxy; + friend class bluetooth_proxy::BluetoothProxy; void on_disconnect_complete(esp_err_t reason) override; @@ -47,16 +53,16 @@ class BluetoothConnection final : public esp32_ble_client::BLEClientBase { // Memory optimized layout for 32-bit systems // Group 1: Pointers (4 bytes each, naturally aligned) - BluetoothProxy *proxy_; + bluetooth_proxy::BluetoothProxy *proxy_; // Group 2: 2-byte types - int16_t send_service_{-3}; // -3 = INIT_SENDING_SERVICES, -2 = DONE_SENDING_SERVICES, >=0 = service index + int16_t send_service_{INIT_SENDING_SERVICES}; // see bluetooth_connection.h cursor states // Group 3: 1-byte types bool seen_mtu_or_services_{false}; // 1 byte used, 1 byte padding }; -} // namespace esphome::bluetooth_proxy +} // namespace esphome::bluetooth_connection #endif // USE_ESP32 diff --git a/esphome/components/bluetooth_proxy/__init__.py b/esphome/components/bluetooth_proxy/__init__.py index bb05f1b21f0..5916132ab21 100644 --- a/esphome/components/bluetooth_proxy/__init__.py +++ b/esphome/components/bluetooth_proxy/__init__.py @@ -2,7 +2,7 @@ import functools import logging import esphome.codegen as cg -from esphome.components import ble_device_base +from esphome.components import ble_device_base, bluetooth_connection import esphome.config_validation as cv from esphome.const import CONF_ACTIVE, CONF_ID, PLATFORM_LN882X, PLATFORM_RP2 from esphome.core import CORE @@ -27,13 +27,18 @@ def AUTO_LOAD(config: ConfigType | None = None) -> list[str]: target platform set, so it takes one of the concrete branches. """ if CORE.is_esp32: - return ["esp32_ble_client", "esp32_ble_tracker"] + return ["bluetooth_connection", "esp32_ble_client", "esp32_ble_tracker"] if CORE.target_platform in _HUB_PLATFORMS: - return ["ble_device_base"] + return ["ble_device_base", "bluetooth_connection"] # No target platform, or one this component does not support: tooling # resolving the manifest (including the host-pinned dependency resolver) — # expose every arm so the closure keeps the esp32 BLE stack. - return ["ble_device_base", "esp32_ble_client", "esp32_ble_tracker"] + return [ + "ble_device_base", + "bluetooth_connection", + "esp32_ble_client", + "esp32_ble_tracker", + ] # Platforms with an in-tree ble_device_base BLE tracker hub whose controller @@ -67,7 +72,7 @@ _IDF_MAX_CONNECTIONS = 9 @functools.cache def _esp32_config_schema() -> cv.All: """Build the esp32 schema, importing the esp32 BLE stack only when used.""" - from esphome.components import esp32_ble, esp32_ble_client, esp32_ble_tracker + from esphome.components import esp32_ble, esp32_ble_tracker if esp32_ble.IDF_MAX_CONNECTIONS != _IDF_MAX_CONNECTIONS: raise cv.Invalid( @@ -77,9 +82,7 @@ def _esp32_config_schema() -> cv.All: f"update _IDF_MAX_CONNECTIONS in bluetooth_proxy/__init__.py" ) - BluetoothConnection = bluetooth_proxy_ns.class_( - "BluetoothConnection", esp32_ble_client.BLEClientBase - ) + BluetoothConnection = bluetooth_connection.esp32_connection_class() CONNECTION_SCHEMA = esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(BluetoothConnection), diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index e6810306116..08b58fc3b4a 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -124,7 +124,7 @@ void BluetoothProxy::log_not_connected_gatt_(const char *action, const char *typ void BluetoothProxy::handle_gatt_not_connected_(uint64_t address, uint16_t handle, const char *action, const char *type) { this->log_not_connected_gatt_(action, type); - this->send_gatt_error(address, handle, ESP_GATT_NOT_CONNECTED); + this->send_gatt_error(address, handle, GATT_NOT_CONNECTED); } #ifdef USE_ESP32 @@ -438,7 +438,7 @@ void BluetoothProxy::bluetooth_set_connection_params(const api::BluetoothSetConn ESP_LOGW(TAG, "[%d] [%s] Cannot set connection params, not connected", connection ? static_cast(connection->connection_index_) : -1, connection ? connection->address_str() : "unknown"); - resp.error = ESP_GATT_NOT_CONNECTED; + resp.error = GATT_NOT_CONNECTED; this->api_connection_->send_message(resp); return; } @@ -498,7 +498,7 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE: case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT: ESP_LOGW(TAG, "Active connections are not supported on this platform"); - this->send_device_connection(msg.address, false, 0, ESP_GATT_NOT_CONNECTED); + this->send_device_connection(msg.address, false, 0, GATT_NOT_CONNECTED); break; case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT: // Not an error: the device is already disconnected, which is the requested state. @@ -506,13 +506,13 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest this->send_connections_free(); break; case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR: - this->send_device_pairing(msg.address, false, ESP_GATT_NOT_CONNECTED); + this->send_device_pairing(msg.address, false, GATT_NOT_CONNECTED); break; case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR: - this->send_device_unpairing(msg.address, false, ESP_GATT_NOT_CONNECTED); + this->send_device_unpairing(msg.address, false, GATT_NOT_CONNECTED); break; case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE: - this->send_device_clear_cache(msg.address, false, ESP_GATT_NOT_CONNECTED); + this->send_device_clear_cache(msg.address, false, GATT_NOT_CONNECTED); break; } } @@ -546,7 +546,7 @@ void BluetoothProxy::bluetooth_set_connection_params(const api::BluetoothSetConn return; api::BluetoothSetConnectionParamsResponse resp; resp.address = msg.address; - resp.error = ESP_GATT_NOT_CONNECTED; + resp.error = GATT_NOT_CONNECTED; this->api_connection_->send_message(resp); } @@ -605,7 +605,7 @@ void BluetoothProxy::unsubscribe_api_connection(api::APIConnection *api_connecti #endif } -void BluetoothProxy::send_device_connection(uint64_t address, bool connected, uint16_t mtu, proxy_err_t error) { +void BluetoothProxy::send_device_connection(uint64_t address, bool connected, uint16_t mtu, conn_err_t error) { if (this->api_connection_ == nullptr) return; api::BluetoothDeviceConnectionResponse call; @@ -633,7 +633,7 @@ void BluetoothProxy::send_gatt_services_done(uint64_t address) { this->api_connection_->send_message(call); } -void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, proxy_err_t error) { +void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, conn_err_t error) { if (this->api_connection_ == nullptr) return; api::BluetoothGATTErrorResponse call; @@ -643,7 +643,7 @@ void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, proxy_er this->api_connection_->send_message(call); } -void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, proxy_err_t error) { +void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, conn_err_t error) { if (this->api_connection_ == nullptr) return; api::BluetoothDevicePairingResponse call; @@ -654,7 +654,7 @@ void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, proxy_er this->api_connection_->send_message(call); } -void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, proxy_err_t error) { +void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, conn_err_t error) { if (this->api_connection_ == nullptr) return; api::BluetoothDeviceUnpairingResponse call; @@ -667,7 +667,7 @@ void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, proxy // Shared by both platform paths: the neutral bluetooth_device_request() uses it to // answer a clear-cache request with a clean error, so it must not be esp32-guarded. -void BluetoothProxy::send_device_clear_cache(uint64_t address, bool success, proxy_err_t error) { +void BluetoothProxy::send_device_clear_cache(uint64_t address, bool success, conn_err_t error) { if (this->api_connection_ == nullptr) return; api::BluetoothDeviceClearCacheResponse call; diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index fd1f1839c9b..dbfc119d985 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -13,13 +13,13 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" -#include "esphome/components/ble_device_base/ble_client_state.h" +#include "esphome/components/bluetooth_connection/bluetooth_connection.h" #ifdef USE_ESP32 #include "esphome/components/esp32_ble_client/ble_client_base.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" -#include "bluetooth_connection.h" +#include "esphome/components/bluetooth_connection/bluetooth_connection_esp32.h" #ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID #include @@ -31,23 +31,16 @@ namespace esphome::bluetooth_proxy { -// Proxy-owned error type for the API error fields, which are plain integers on -// the wire. Aliases esp_err_t on esp32 (where the values come from IDF calls); -// a bare int elsewhere. Owning the name instead of probing for esp_err_t keeps -// the header independent of how a hub platform's SDK spells its error type. -#ifdef USE_ESP32 -using proxy_err_t = esp_err_t; -static constexpr proxy_err_t PROXY_OK = ESP_OK; -#else -using proxy_err_t = int; -static constexpr proxy_err_t PROXY_OK = 0; -#endif - -static constexpr proxy_err_t ESP_GATT_NOT_CONNECTED = ble_device_base::GATT_ERR_NOT_CONNECTED; -static constexpr int DONE_SENDING_SERVICES = -2; -static constexpr int INIT_SENDING_SERVICES = -3; +// The connection-domain types live in the bluetooth_connection component; +// re-exported here so the proxy code reads unqualified. +using bluetooth_connection::CONN_OK; +using bluetooth_connection::conn_err_t; +using bluetooth_connection::DONE_SENDING_SERVICES; +using bluetooth_connection::GATT_NOT_CONNECTED; +using bluetooth_connection::INIT_SENDING_SERVICES; #ifdef USE_ESP32 +using BluetoothConnection = bluetooth_connection::BluetoothConnection; using namespace esp32_ble_client; #endif @@ -79,7 +72,8 @@ enum BluetoothProxySubscriptionFlag : uint32_t { class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, public esp32_ble_tracker::BLEScannerStateListener, public Component { - friend class BluetoothConnection; // Allow connection to update connections_free_response_ + // Allow the connection to update connections_free_response_ + friend bluetooth_connection::BluetoothConnection; #else class BluetoothProxy final : public Component { #endif @@ -129,14 +123,14 @@ class BluetoothProxy final : public Component { void unsubscribe_api_connection(api::APIConnection *api_connection); api::APIConnection *get_api_connection() { return this->api_connection_; } - void send_device_connection(uint64_t address, bool connected, uint16_t mtu = 0, proxy_err_t error = PROXY_OK); + void send_device_connection(uint64_t address, bool connected, uint16_t mtu = 0, conn_err_t error = CONN_OK); void send_connections_free(); void send_connections_free(api::APIConnection *api_connection); void send_gatt_services_done(uint64_t address); - void send_gatt_error(uint64_t address, uint16_t handle, proxy_err_t error); - void send_device_pairing(uint64_t address, bool paired, proxy_err_t error = PROXY_OK); - void send_device_unpairing(uint64_t address, bool success, proxy_err_t error = PROXY_OK); - void send_device_clear_cache(uint64_t address, bool success, proxy_err_t error = PROXY_OK); + void send_gatt_error(uint64_t address, uint16_t handle, conn_err_t error); + void send_device_pairing(uint64_t address, bool paired, conn_err_t error = CONN_OK); + void send_device_unpairing(uint64_t address, bool success, conn_err_t error = CONN_OK); + void send_device_clear_cache(uint64_t address, bool success, conn_err_t error = CONN_OK); void bluetooth_scanner_set_mode(bool active); diff --git a/tests/component_tests/bluetooth_proxy/test_platform_gates.py b/tests/component_tests/bluetooth_proxy/test_platform_gates.py index 4d7997fbce7..c5240105fa3 100644 --- a/tests/component_tests/bluetooth_proxy/test_platform_gates.py +++ b/tests/component_tests/bluetooth_proxy/test_platform_gates.py @@ -5,12 +5,12 @@ advertisement-only arm applies its own defaults.""" import pytest from esphome import config_validation as cv -from esphome.components import bluetooth_proxy +from esphome.components import bluetooth_connection, bluetooth_proxy from esphome.const import CONF_ACTIVE, KEY_TARGET_PLATFORM from esphome.core import CORE, KEY_CORE -def _set_platform(platform: str) -> None: +def _set_platform(platform: str | None) -> None: CORE.data.setdefault(KEY_CORE, {})[KEY_TARGET_PLATFORM] = platform @@ -46,3 +46,17 @@ def test_hub_platform_accepts_the_advertisement_only_shape() -> None: _set_platform("ln882x") validated = bluetooth_proxy.CONFIG_SCHEMA({}) assert validated[CONF_ACTIVE] is False + + +def test_bluetooth_connection_auto_load_covers_its_includes() -> None: + # The esp32 connection header includes esp32_ble_client; the auto load + # must satisfy that closure itself (regression: it once relied on the + # consumer's auto loads). + _set_platform("esp32") + assert "esp32_ble_client" in bluetooth_connection.AUTO_LOAD() + _set_platform("rp2") + assert bluetooth_connection.AUTO_LOAD() == ["ble_device_base"] + # No target platform (tooling resolving the manifest): the union, so + # dependency closures stay complete for build_codeowners and friends. + _set_platform(None) + assert bluetooth_connection.AUTO_LOAD() == ["ble_device_base", "esp32_ble_client"]