mirror of
https://github.com/esphome/esphome.git
synced 2026-08-31 01:56:01 +00:00
Own the platform dispatch in bluetooth_connection and gate the proxy surface
This commit is contained in:
@@ -8,14 +8,24 @@ gatt_client_schema() + new_gatt_backend().
|
||||
"""
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
from esphome.const import PLATFORM_RP2, PlatformFramework
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import PLATFORM_ESP32, PLATFORM_RP2, PlatformFramework
|
||||
from esphome.core import CORE
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT
|
||||
from esphome.types import ConfigType
|
||||
|
||||
|
||||
def AUTO_LOAD() -> list[str]:
|
||||
def AUTO_LOAD(config: ConfigType | None = None) -> list[str]:
|
||||
"""ble_device_base plus the platform BLE stack the build's backend
|
||||
registers with, so consumers stay platform-blind. The platform-less arm
|
||||
serves tooling that resolves the manifest without a target."""
|
||||
if CORE.is_esp32:
|
||||
return ["ble_device_base", "esp32_ble_tracker"]
|
||||
if CORE.target_platform == PLATFORM_RP2:
|
||||
return ["ble_device_base", "rp2040_ble"]
|
||||
if CORE.target_platform is None:
|
||||
return ["ble_device_base", "esp32_ble_tracker", "rp2040_ble"]
|
||||
return ["ble_device_base"]
|
||||
|
||||
|
||||
@@ -31,6 +41,9 @@ RP2_MAX_CONNECTIONS = 1
|
||||
# registry of which hub platforms run the connection-capable proxy.
|
||||
HUB_MAX_CONNECTIONS: dict[str, int] = {PLATFORM_RP2: RP2_MAX_CONNECTIONS}
|
||||
|
||||
# Every platform with a GATT backend; gates dedicated-backend consumers.
|
||||
GATT_CLIENT_PLATFORMS = [PLATFORM_ESP32, PLATFORM_RP2]
|
||||
|
||||
# The hub-platform wrapper and the rp2 BTstack backend codegen classes.
|
||||
HubBluetoothConnection = bluetooth_connection_ns.class_("BluetoothConnection")
|
||||
RP2GattClient = bluetooth_connection_ns.class_("RP2GattClient", cg.Component)
|
||||
@@ -65,6 +78,27 @@ def gatt_client_schema() -> cv.Schema:
|
||||
)
|
||||
|
||||
|
||||
def gatt_client_config_schema(base_schema: cv.Schema, consumer: str) -> cv.All:
|
||||
"""Wrap a dedicated-backend consumer's schema so the consumer stays
|
||||
platform-blind: gates on the platforms with a backend, folds in
|
||||
gatt_client_schema(), and does the esp32 controller-slot bookkeeping.
|
||||
`consumer` names the component in slot-exhaustion errors."""
|
||||
|
||||
def apply(config: ConfigType) -> ConfigType:
|
||||
if config is SCHEMA_EXTRACT:
|
||||
# The language-schema dumper runs without a platform; expose the
|
||||
# consumer's own keys.
|
||||
return base_schema
|
||||
config = base_schema.extend(gatt_client_schema())(config)
|
||||
if CORE.is_esp32:
|
||||
from esphome.components import esp32_ble
|
||||
|
||||
esp32_ble.consume_connection_slots(1, consumer)(config)
|
||||
return config
|
||||
|
||||
return cv.All(cv.only_on(GATT_CLIENT_PLATFORMS), apply)
|
||||
|
||||
|
||||
async def new_gatt_backend(config: ConfigType) -> cg.MockObj:
|
||||
"""Instantiate the backend declared by gatt_client_schema(), register it
|
||||
with its platform stack, and claim one neutral GATT client slot.
|
||||
@@ -77,7 +111,9 @@ async def new_gatt_backend(config: ConfigType) -> cg.MockObj:
|
||||
|
||||
ble_device_base.request_gatt_client()
|
||||
backend = cg.new_Pvariable(config[CONF_BACKEND_ID])
|
||||
await cg.register_component(backend, config)
|
||||
# The backend has no user-facing component options; an empty config keeps
|
||||
# the consumer's own keys (update_interval, ...) off it.
|
||||
await cg.register_component(backend, {})
|
||||
if CORE.is_esp32:
|
||||
from esphome.components import esp32_ble_tracker
|
||||
|
||||
|
||||
@@ -16,11 +16,13 @@
|
||||
#include <esp_err.h>
|
||||
#endif
|
||||
|
||||
// The connection-aware API request handlers are compiled: a GATT backend is
|
||||
// wired by codegen (one slot per connection). Advertisement-only builds get
|
||||
// the clean-error handlers; address-scoped maintenance (unpair, cache clear)
|
||||
// still works there through the per-platform free functions below.
|
||||
#ifdef USE_BLE_GATT_CLIENT
|
||||
// The connection-aware API request handlers are compiled: a proxy is present
|
||||
// and a GATT backend is wired by codegen (one slot per connection).
|
||||
// Advertisement-only builds get the clean-error handlers; address-scoped
|
||||
// maintenance (unpair, cache clear) still works there through the
|
||||
// per-platform free functions below. Backend-only builds (a dedicated-backend
|
||||
// consumer without bluetooth_proxy) compile none of this API surface.
|
||||
#if defined(USE_BLE_GATT_CLIENT) && defined(USE_BLUETOOTH_PROXY)
|
||||
#define BLUETOOTH_CONNECTION_HAS_GATT
|
||||
#endif
|
||||
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
|
||||
#if defined(USE_ESP32_BLE) && defined(USE_BLE_GATT_CLIENT)
|
||||
|
||||
// The in-place streamer serves the proxy's service-discovery API; backend-only
|
||||
// builds compile without the proxy headers or the streamer.
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
#include "bluetooth_connection.h"
|
||||
#include "bluetooth_connection_hub.h"
|
||||
|
||||
#include "esphome/components/bluetooth_proxy/bluetooth_proxy.h"
|
||||
#endif
|
||||
|
||||
#include "esphome/components/ble_device_base/ble_client_state.h"
|
||||
#include "esphome/core/hal.h"
|
||||
@@ -281,9 +285,8 @@ bool BluedroidGattClient::build_service_table_() {
|
||||
for (uint16_t c = 0; c < svc_chars; c++) {
|
||||
esp_gattc_char_elem_t chr;
|
||||
uint16_t char_count = 1;
|
||||
auto status =
|
||||
esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, svc.start_handle, svc.end_handle, &chr,
|
||||
&char_count, c);
|
||||
auto status = esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, svc.start_handle, svc.end_handle, &chr,
|
||||
&char_count, c);
|
||||
if (status == ESP_GATT_INVALID_OFFSET || status == ESP_GATT_NOT_FOUND) {
|
||||
break;
|
||||
}
|
||||
@@ -335,9 +338,8 @@ bool BluedroidGattClient::build_service_table_() {
|
||||
for (uint16_t c = 0; char_index < char_total; c++) {
|
||||
esp_gattc_char_elem_t chr;
|
||||
uint16_t char_count = 1;
|
||||
auto status =
|
||||
esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, svc.start_handle, svc.end_handle, &chr,
|
||||
&char_count, c);
|
||||
auto status = esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, svc.start_handle, svc.end_handle, &chr,
|
||||
&char_count, c);
|
||||
if (status == ESP_GATT_INVALID_OFFSET || status == ESP_GATT_NOT_FOUND) {
|
||||
break;
|
||||
}
|
||||
@@ -447,6 +449,7 @@ void BluedroidGattClient::handle_search_cmpl_() {
|
||||
this->sink_.on_service_discovery_done(0);
|
||||
}
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void BluedroidGattClient::stream_service_batch(BluetoothConnection &conn) {
|
||||
if (this->services_released_ || conn.send_service_ >= this->service_total_) {
|
||||
conn.send_service_ = DONE_SENDING_SERVICES;
|
||||
@@ -576,6 +579,7 @@ void BluedroidGattClient::stream_service_batch(BluetoothConnection &conn) {
|
||||
conn.send_service_ = batch_start;
|
||||
}
|
||||
}
|
||||
#endif // USE_BLUETOOTH_PROXY
|
||||
|
||||
// ---- events ----
|
||||
|
||||
@@ -711,27 +715,25 @@ bool BluedroidGattClient::handle_gattc_event_(esp_gattc_cb_event_t event, esp_ga
|
||||
if (this->conn_id_ != param->read.conn_id)
|
||||
return false;
|
||||
bool ok = param->read.status == ESP_GATT_OK;
|
||||
this->sink_.on_read_result(param->read.handle, ok ? param->read.value : nullptr,
|
||||
ok ? param->read.value_len : 0, ok ? 0 : param->read.status);
|
||||
this->sink_.on_read_result(param->read.handle, ok ? param->read.value : nullptr, ok ? param->read.value_len : 0,
|
||||
ok ? 0 : param->read.status);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_WRITE_CHAR_EVT:
|
||||
case ESP_GATTC_WRITE_DESCR_EVT: {
|
||||
if (this->conn_id_ != param->write.conn_id)
|
||||
return false;
|
||||
this->sink_.on_write_result(param->write.handle,
|
||||
param->write.status == ESP_GATT_OK ? 0 : param->write.status);
|
||||
this->sink_.on_write_result(param->write.handle, param->write.status == ESP_GATT_OK ? 0 : param->write.status);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
|
||||
this->sink_.on_notify_state(param->reg_for_notify.handle, true,
|
||||
param->reg_for_notify.status == ESP_GATT_OK ? 0 : param->reg_for_notify.status);
|
||||
param->reg_for_notify.status == ESP_GATT_OK ? 0 : param->reg_for_notify.status);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
|
||||
this->sink_.on_notify_state(
|
||||
param->unreg_for_notify.handle, false,
|
||||
param->unreg_for_notify.status == ESP_GATT_OK ? 0 : param->unreg_for_notify.status);
|
||||
this->sink_.on_notify_state(param->unreg_for_notify.handle, false,
|
||||
param->unreg_for_notify.status == ESP_GATT_OK ? 0 : param->unreg_for_notify.status);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_NOTIFY_EVT: {
|
||||
@@ -759,8 +761,8 @@ void BluedroidGattClient::handle_gap_event_(esp_gap_ble_cb_event_t event, esp_bl
|
||||
case ESP_GAP_BLE_AUTH_CMPL_EVT: {
|
||||
if (!this->check_addr_(param->ble_security.auth_cmpl.bd_addr))
|
||||
break;
|
||||
this->sink_.on_pairing_result(
|
||||
param->ble_security.auth_cmpl.success ? 0 : param->ble_security.auth_cmpl.fail_reason);
|
||||
this->sink_.on_pairing_result(param->ble_security.auth_cmpl.success ? 0
|
||||
: param->ble_security.auth_cmpl.fail_reason);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -22,8 +22,10 @@
|
||||
|
||||
namespace esphome::bluetooth_connection {
|
||||
|
||||
class BluetoothConnection;
|
||||
class BluedroidGattClient;
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
class BluetoothConnection;
|
||||
#endif
|
||||
|
||||
// The tracker-facing half: owns the ClientState the promote loop reads and
|
||||
// forwards events/commands to the engine.
|
||||
@@ -75,10 +77,12 @@ class BluedroidGattClient final : public Component {
|
||||
ble_device_base::GattServiceTable get_service_table();
|
||||
void release_services();
|
||||
|
||||
/// In-place service streamer (the wrapper detects and prefers it): builds
|
||||
/// one api response batch directly from Bluedroid's cached database, so the
|
||||
/// streaming peak is the response itself - the old esp32 model.
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
/// In-place service streamer (the proxy wrapper detects and prefers it):
|
||||
/// builds one api response batch directly from Bluedroid's cached database,
|
||||
/// so the streaming peak is the response itself - the old esp32 model.
|
||||
void stream_service_batch(BluetoothConnection &conn);
|
||||
#endif
|
||||
|
||||
void set_connection_type(esp32_ble_tracker::ConnectionType ct) { this->connection_type_ = ct; }
|
||||
bool disconnect_pending() const { return this->shim_.disconnect_pending(); }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Hub-platform connection wrapper (USE_RP2 hub builds today).
|
||||
#include "bluetooth_connection_hub.h"
|
||||
|
||||
#ifdef USE_BLE_GATT_CLIENT
|
||||
#if defined(USE_BLE_GATT_CLIENT) && defined(USE_BLUETOOTH_PROXY)
|
||||
|
||||
#include "esphome/components/api/api_pb2.h"
|
||||
#include "esphome/components/bluetooth_proxy/bluetooth_proxy.h"
|
||||
@@ -433,4 +433,4 @@ void BluetoothConnection::send_service_for_discovery_() {
|
||||
|
||||
} // namespace esphome::bluetooth_connection
|
||||
|
||||
#endif // USE_BLE_GATT_CLIENT
|
||||
#endif // USE_BLE_GATT_CLIENT && USE_BLUETOOTH_PROXY
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_BLE_GATT_CLIENT
|
||||
// The wrapper exists to serve the proxy's API surface; dedicated-backend
|
||||
// consumers (radon_eye_rd200) drive the backend directly, so backend-only
|
||||
// builds compile this header empty.
|
||||
#if defined(USE_BLE_GATT_CLIENT) && defined(USE_BLUETOOTH_PROXY)
|
||||
|
||||
#include "bluetooth_connection.h"
|
||||
|
||||
@@ -151,4 +154,4 @@ static_assert(ble_device_base::GattClientEventSinkContract<BluetoothConnection>,
|
||||
|
||||
} // namespace esphome::bluetooth_connection
|
||||
|
||||
#endif // USE_BLE_GATT_CLIENT
|
||||
#endif // USE_BLE_GATT_CLIENT && USE_BLUETOOTH_PROXY
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#ifdef USE_BLE_GATT_CLIENT
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cstring>
|
||||
@@ -24,6 +25,10 @@ static const char *const WRITE_CHARACTERISTIC_UUID_V2 = "00001524-0000-1000-8000
|
||||
static const char *const READ_CHARACTERISTIC_UUID_V2 = "00001525-0000-1000-8000-00805f9b34fb";
|
||||
static const uint8_t WRITE_COMMAND_V2 = 0x40;
|
||||
|
||||
// Minimum notification payload carrying all three measurements.
|
||||
static const uint16_t MESSAGE_MIN_LEN_V1 = 20;
|
||||
static const uint16_t MESSAGE_MIN_LEN_V2 = 68;
|
||||
|
||||
// BLE public address type (shared code space with the API/backends).
|
||||
static const uint8_t BLE_ADDR_TYPE_PUBLIC = 0;
|
||||
|
||||
@@ -161,7 +166,8 @@ void RadonEyeRD200::read_sensors_(const uint8_t *value, uint16_t value_len) {
|
||||
|
||||
uint8_t command = value[0];
|
||||
|
||||
if ((command == WRITE_COMMAND_V1 && value_len < 20) || (command == WRITE_COMMAND_V2 && value_len < 68)) {
|
||||
if ((command == WRITE_COMMAND_V1 && value_len < MESSAGE_MIN_LEN_V1) ||
|
||||
(command == WRITE_COMMAND_V2 && value_len < MESSAGE_MIN_LEN_V2)) {
|
||||
ESP_LOGW(TAG, "Unexpected command 0x%02X message length %d", command, value_len);
|
||||
return;
|
||||
}
|
||||
@@ -170,8 +176,11 @@ void RadonEyeRD200::read_sensors_(const uint8_t *value, uint16_t value_len) {
|
||||
// 501085EBB9400000000000000000220025000000
|
||||
// Example data V2:
|
||||
// 4042323230313033525532303338330652443230304e56322e302e3200014a00060a00080000000300010079300000e01108001c00020000003822005c8f423fa4709d3f
|
||||
ESP_LOGV(TAG, "radon sensors raw bytes");
|
||||
ESP_LOG_BUFFER_HEX_LEVEL(TAG, value, value_len, ESP_LOG_VERBOSE);
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
// Sized for the longest supported message; format_hex_to truncates longer.
|
||||
char hex_buf[format_hex_size(MESSAGE_MIN_LEN_V2)];
|
||||
ESP_LOGV(TAG, "radon sensors raw bytes: %s", format_hex_to(hex_buf, value, value_len));
|
||||
#endif
|
||||
|
||||
// Convert from pCi/L to Bq/m³
|
||||
constexpr float convert_to_bwpm3 = 37.0;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import functools
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import bluetooth_connection, sensor
|
||||
import esphome.config_validation as cv
|
||||
@@ -9,25 +7,12 @@ from esphome.const import (
|
||||
CONF_RADON,
|
||||
CONF_RADON_LONG_TERM,
|
||||
ICON_RADIOACTIVE,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_RP2,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_BECQUEREL_PER_CUBIC_METER,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
|
||||
def AUTO_LOAD(config: ConfigType | None = None) -> list[str]:
|
||||
"""The GATT backend plus the platform BLE stack it registers with; the
|
||||
union arm serves tooling that resolves the manifest without a target
|
||||
platform (the bluetooth_proxy pattern)."""
|
||||
if CORE.is_esp32:
|
||||
return ["bluetooth_connection", "esp32_ble_tracker"]
|
||||
if CORE.target_platform == PLATFORM_RP2:
|
||||
return ["bluetooth_connection", "rp2040_ble"]
|
||||
return ["bluetooth_connection", "esp32_ble_tracker", "rp2040_ble"]
|
||||
|
||||
AUTO_LOAD = ["bluetooth_connection"]
|
||||
|
||||
radon_eye_rd200_ns = cg.esphome_ns.namespace("radon_eye_rd200")
|
||||
RadonEyeRD200 = radon_eye_rd200_ns.class_("RadonEyeRD200", cg.PollingComponent)
|
||||
@@ -39,47 +24,22 @@ _SENSOR_SCHEMA = sensor.sensor_schema(
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def _schema_for_platform(platform: str) -> cv.Schema | cv.All:
|
||||
"""Built per platform (cached): the backend id's class and the esp32
|
||||
controller-slot consumption depend on the target."""
|
||||
schema = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(RadonEyeRD200),
|
||||
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
|
||||
cv.Optional(CONF_RADON): _SENSOR_SCHEMA,
|
||||
cv.Optional(CONF_RADON_LONG_TERM): _SENSOR_SCHEMA,
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("5min"))
|
||||
.extend(bluetooth_connection.gatt_client_schema())
|
||||
)
|
||||
if platform == PLATFORM_ESP32:
|
||||
from esphome.components import esp32_ble
|
||||
|
||||
return cv.All(
|
||||
schema, esp32_ble.consume_connection_slots(1, "radon_eye_rd200")
|
||||
)
|
||||
return schema
|
||||
|
||||
|
||||
def _platform_schema(config: ConfigType) -> ConfigType:
|
||||
return _schema_for_platform(CORE.target_platform)(config)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.only_on([PLATFORM_ESP32, PLATFORM_RP2]),
|
||||
_platform_schema,
|
||||
CONFIG_SCHEMA = bluetooth_connection.gatt_client_config_schema(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(RadonEyeRD200),
|
||||
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
|
||||
cv.Optional(CONF_RADON): _SENSOR_SCHEMA,
|
||||
cv.Optional(CONF_RADON_LONG_TERM): _SENSOR_SCHEMA,
|
||||
}
|
||||
).extend(cv.polling_component_schema("5min")),
|
||||
"radon_eye_rd200",
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
backend = await bluetooth_connection.new_gatt_backend(config)
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID], backend, config[CONF_MAC_ADDRESS].as_hex
|
||||
)
|
||||
var = cg.new_Pvariable(config[CONF_ID], backend, config[CONF_MAC_ADDRESS].as_hex)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
if (radon := config.get(CONF_RADON)) is not None:
|
||||
|
||||
@@ -14,6 +14,17 @@ import voluptuous as vol
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.bluetooth_proxy import CONFIG_SCHEMA, _esp32_config_schema
|
||||
from esphome.const import KEY_CORE, KEY_TARGET_PLATFORM, PLATFORM_ESP32
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
def _esp32_schema_keys() -> dict[str, object]:
|
||||
# The builder resolves the backend schema through the platform-dispatched
|
||||
# bluetooth_connection.gatt_client_schema(), so the platform must be set
|
||||
# (conftest's autouse reset restores CORE after each test).
|
||||
CORE.data.setdefault(KEY_CORE, {})[KEY_TARGET_PLATFORM] = PLATFORM_ESP32
|
||||
return _keys(_schema_of(_esp32_config_schema()))
|
||||
|
||||
|
||||
# esp32-schema keys with no place in the outer schema: COMPONENT_SCHEMA
|
||||
# plumbing (derived, so a future core key does not fail this component's test),
|
||||
@@ -38,7 +49,7 @@ def _keys(schema: vol.Schema) -> dict[str, object]:
|
||||
|
||||
def test_outer_scalar_keys_exist_in_esp32_schema() -> None:
|
||||
outer = _keys(_schema_of(CONFIG_SCHEMA))
|
||||
esp32 = _keys(_schema_of(_esp32_config_schema()))
|
||||
esp32 = _esp32_schema_keys()
|
||||
missing = set(outer) - set(esp32)
|
||||
assert not missing, (
|
||||
f"outer CONFIG_SCHEMA declares {sorted(missing)} which the esp32 schema "
|
||||
@@ -51,7 +62,7 @@ def test_esp32_scalars_all_walkable() -> None:
|
||||
"""Every non-generated esp32 scalar option must appear in the outer schema
|
||||
(connections is deliberately excluded — it must validate exactly once)."""
|
||||
outer = _keys(_schema_of(CONFIG_SCHEMA))
|
||||
esp32 = _keys(_schema_of(_esp32_config_schema()))
|
||||
esp32 = _esp32_schema_keys()
|
||||
scalar = {
|
||||
name
|
||||
for name, key in esp32.items()
|
||||
|
||||
@@ -178,11 +178,21 @@ def test_rp2_rejects_esp32_only_keys_by_name(
|
||||
|
||||
|
||||
def test_bluetooth_connection_auto_load_covers_its_includes() -> None:
|
||||
# Every backend builds on ble_device_base alone; the Bluedroid backend
|
||||
# talks to IDF directly, so esp32_ble_client is no longer in the closure.
|
||||
for platform in ("esp32", "rp2", None):
|
||||
_set_platform(platform)
|
||||
assert bluetooth_connection.AUTO_LOAD() == ["ble_device_base"]
|
||||
# The backend registers with its platform BLE stack, so that dependency
|
||||
# lives here and consumers (proxy, radon_eye_rd200) stay platform-blind;
|
||||
# the platform-less arm is the union for manifest-resolving tooling.
|
||||
_set_platform("esp32")
|
||||
assert bluetooth_connection.AUTO_LOAD() == ["ble_device_base", "esp32_ble_tracker"]
|
||||
_set_platform("rp2")
|
||||
assert bluetooth_connection.AUTO_LOAD() == ["ble_device_base", "rp2040_ble"]
|
||||
_set_platform("ln882x")
|
||||
assert bluetooth_connection.AUTO_LOAD() == ["ble_device_base"]
|
||||
_set_platform(None)
|
||||
assert bluetooth_connection.AUTO_LOAD() == [
|
||||
"ble_device_base",
|
||||
"esp32_ble_tracker",
|
||||
"rp2040_ble",
|
||||
]
|
||||
|
||||
|
||||
def test_every_registered_hub_platform_has_a_schema_arm() -> None:
|
||||
|
||||
Reference in New Issue
Block a user