import logging import esphome.codegen as cg from esphome.components import esp32_ble, esp32_ble_client, esp32_ble_tracker from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.components.esp32_ble import BTLoggers import esphome.config_validation as cv from esphome.const import CONF_ACTIVE, CONF_ID from esphome.core import CORE from esphome.log import AnsiFore, color AUTO_LOAD = ["esp32_ble_client", "esp32_ble_tracker"] DEPENDENCIES = ["api", "esp32"] CODEOWNERS = ["@jesserockz", "@bdraco"] _LOGGER = logging.getLogger(__name__) CONF_CONNECTION_SLOTS = "connection_slots" CONF_CACHE_SERVICES = "cache_services" CONF_CONNECTIONS = "connections" DEFAULT_CONNECTION_SLOTS = 3 bluetooth_proxy_ns = cg.esphome_ns.namespace("bluetooth_proxy") BluetoothProxy = bluetooth_proxy_ns.class_( "BluetoothProxy", esp32_ble_tracker.ESPBTDeviceListener, cg.Component ) BluetoothConnection = bluetooth_proxy_ns.class_( "BluetoothConnection", esp32_ble_client.BLEClientBase ) CONNECTION_SCHEMA = esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(BluetoothConnection), } ).extend(cv.COMPONENT_SCHEMA) def validate_connections(config): if CONF_CONNECTIONS in config: if not config[CONF_ACTIVE]: raise cv.Invalid( "Connections can only be used if the proxy is set to active" ) elif config[CONF_ACTIVE]: connection_slots: int = config[CONF_CONNECTION_SLOTS] esp32_ble_tracker.consume_connection_slots(connection_slots, "bluetooth_proxy")( config ) # Warn about connection slot waste when using Arduino framework if CORE.using_arduino and connection_slots: _LOGGER.warning( "Bluetooth Proxy with active connections on Arduino framework has suboptimal performance.\n" "If BLE connections fail, they can waste connection slots for 10 seconds because\n" "Arduino doesn't allow configuring the BLE connection timeout (fixed at 30s).\n" "ESP-IDF framework allows setting it to 20s to match client timeouts.\n" "\n" "To switch to ESP-IDF, add this to your YAML:\n" " esp32:\n" " framework:\n" " type: esp-idf\n" "\n" "For detailed migration instructions, see:\n" "%s", color( AnsiFore.BLUE, "https://esphome.io/guides/esp32_arduino_to_idf.html" ), ) return { **config, CONF_CONNECTIONS: [CONNECTION_SCHEMA({}) for _ in range(connection_slots)], } return config CONFIG_SCHEMA = cv.All( ( cv.Schema( { cv.GenerateID(): cv.declare_id(BluetoothProxy), cv.Optional(CONF_ACTIVE, default=False): cv.boolean, cv.SplitDefault(CONF_CACHE_SERVICES, esp32_idf=True): cv.All( cv.only_with_esp_idf, cv.boolean ), cv.Optional( CONF_CONNECTION_SLOTS, default=DEFAULT_CONNECTION_SLOTS, ): cv.All( cv.positive_int, cv.Range(min=1, max=esp32_ble_tracker.max_connections()), ), cv.Optional(CONF_CONNECTIONS): cv.All( cv.ensure_list(CONNECTION_SCHEMA), cv.Length(min=1, max=esp32_ble_tracker.max_connections()), ), } ) .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) .extend(cv.COMPONENT_SCHEMA) ), validate_connections, ) async def to_code(config): # Register the loggers this component needs esp32_ble.register_bt_logger(BTLoggers.GATT, BTLoggers.L2CAP, BTLoggers.SMP) var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) cg.add(var.set_active(config[CONF_ACTIVE])) await esp32_ble_tracker.register_raw_ble_device(var, config) # Define max connections for protobuf fixed array connection_count = len(config.get(CONF_CONNECTIONS, [])) cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", connection_count) # Define batch size for BLE advertisements # Each advertisement is up to 80 bytes when packaged (including protocol overhead) # 16 advertisements × 80 bytes (worst case) = 1280 bytes out of ~1320 bytes usable payload # This achieves ~97% WiFi MTU utilization while staying under the limit cg.add_define("BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE", 16) for connection_conf in config.get(CONF_CONNECTIONS, []): connection_var = cg.new_Pvariable(connection_conf[CONF_ID]) await cg.register_component(connection_var, connection_conf) cg.add(var.register_connection(connection_var)) await esp32_ble_tracker.register_raw_client(connection_var, connection_conf) if config.get(CONF_CACHE_SERVICES): add_idf_sdkconfig_option("CONFIG_BT_GATTC_CACHE_NVS_FLASH", True) cg.add_define("USE_BLUETOOTH_PROXY")