diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py index bc7d517695..37f8ea32b3 100644 --- a/esphome/components/ble_client/__init__.py +++ b/esphome/components/ble_client/__init__.py @@ -67,7 +67,7 @@ CONF_AUTO_CONNECT = "auto_connect" MULTI_CONF = True -CONFIG_SCHEMA = ( +CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(BLEClient), @@ -114,7 +114,8 @@ CONFIG_SCHEMA = ( } ) .extend(cv.COMPONENT_SCHEMA) - .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA), + esp32_ble_tracker.consume_connection_slots(1, "ble_client"), ) CONF_BLE_CLIENT_ID = "ble_client_id" diff --git a/esphome/components/bluetooth_proxy/__init__.py b/esphome/components/bluetooth_proxy/__init__.py index edfea50473..04ac9116c7 100644 --- a/esphome/components/bluetooth_proxy/__init__.py +++ b/esphome/components/bluetooth_proxy/__init__.py @@ -8,9 +8,10 @@ AUTO_LOAD = ["esp32_ble_client", "esp32_ble_tracker"] DEPENDENCIES = ["api", "esp32"] CODEOWNERS = ["@jesserockz"] +CONF_CONNECTION_SLOTS = "connection_slots" CONF_CACHE_SERVICES = "cache_services" CONF_CONNECTIONS = "connections" -MAX_CONNECTIONS = 3 +DEFAULT_CONNECTION_SLOTS = 3 bluetooth_proxy_ns = cg.esphome_ns.namespace("bluetooth_proxy") @@ -35,28 +36,42 @@ def validate_connections(config): "Connections can only be used if the proxy is set to active" ) elif config[CONF_ACTIVE]: - conf = config.copy() - conf[CONF_CONNECTIONS] = [CONNECTION_SCHEMA({}) for _ in range(MAX_CONNECTIONS)] - return conf + connection_slots: int = config[CONF_CONNECTION_SLOTS] + esp32_ble_tracker.consume_connection_slots(connection_slots, "bluetooth_proxy")( + config + ) + 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_CONNECTIONS): cv.All( - cv.ensure_list(CONNECTION_SCHEMA), - cv.Length(min=1, max=MAX_CONNECTIONS), - ), - } - ) - .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) - .extend(cv.COMPONENT_SCHEMA), + ( + 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, ) diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 425ce54b9f..68be2cbbe9 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -1,3 +1,9 @@ +from __future__ import annotations + +from collections.abc import MutableMapping +import logging +from typing import Any, Callable + from esphome import automation import esphome.codegen as cg from esphome.components import esp32_ble @@ -29,11 +35,21 @@ from esphome.core import CORE AUTO_LOAD = ["esp32_ble"] DEPENDENCIES = ["esp32"] +KEY_ESP32_BLE_TRACKER = "esp32_ble_tracker" +KEY_USED_CONNECTION_SLOTS = "used_connection_slots" + +CONF_MAX_CONNECTIONS = "max_connections" CONF_ESP32_BLE_ID = "esp32_ble_id" CONF_SCAN_PARAMETERS = "scan_parameters" CONF_WINDOW = "window" CONF_CONTINUOUS = "continuous" CONF_ON_SCAN_END = "on_scan_end" + +DEFAULT_MAX_CONNECTIONS = 3 +IDF_MAX_CONNECTIONS = 9 + +_LOGGER = logging.getLogger(__name__) + esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker") ESP32BLETracker = esp32_ble_tracker_ns.class_( "ESP32BLETracker", @@ -112,61 +128,126 @@ def as_reversed_hex_array(value): ) -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(ESP32BLETracker), - cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE), - cv.Optional(CONF_SCAN_PARAMETERS, default={}): cv.All( - cv.Schema( +def max_connections() -> int: + return IDF_MAX_CONNECTIONS if CORE.using_esp_idf else DEFAULT_MAX_CONNECTIONS + + +def consume_connection_slots( + value: int, consumer: str +) -> Callable[[MutableMapping], MutableMapping]: + def _consume_connection_slots(config: MutableMapping) -> MutableMapping: + data: dict[str, Any] = CORE.data.setdefault(KEY_ESP32_BLE_TRACKER, {}) + slots: list[str] = data.setdefault(KEY_USED_CONNECTION_SLOTS, []) + slots.extend([consumer] * value) + return config + + return _consume_connection_slots + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ESP32BLETracker), + cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE), + cv.Optional(CONF_MAX_CONNECTIONS, default=DEFAULT_MAX_CONNECTIONS): cv.All( + cv.positive_int, cv.Range(min=0, max=max_connections()) + ), + cv.Optional(CONF_SCAN_PARAMETERS, default={}): cv.All( + cv.Schema( + { + cv.Optional( + CONF_DURATION, default="5min" + ): cv.positive_time_period_seconds, + cv.Optional( + CONF_INTERVAL, default="320ms" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_WINDOW, default="30ms" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_ACTIVE, default=True): cv.boolean, + cv.Optional(CONF_CONTINUOUS, default=True): cv.boolean, + } + ), + validate_scan_parameters, + ), + cv.Optional(CONF_ON_BLE_ADVERTISE): automation.validate_automation( { - cv.Optional( - CONF_DURATION, default="5min" - ): cv.positive_time_period_seconds, - cv.Optional( - CONF_INTERVAL, default="320ms" - ): cv.positive_time_period_milliseconds, - cv.Optional( - CONF_WINDOW, default="30ms" - ): cv.positive_time_period_milliseconds, - cv.Optional(CONF_ACTIVE, default=True): cv.boolean, - cv.Optional(CONF_CONTINUOUS, default=True): cv.boolean, + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ESPBTAdvertiseTrigger + ), + cv.Optional(CONF_MAC_ADDRESS): cv.ensure_list(cv.mac_address), } ), - validate_scan_parameters, - ), - cv.Optional(CONF_ON_BLE_ADVERTISE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESPBTAdvertiseTrigger), - cv.Optional(CONF_MAC_ADDRESS): cv.ensure_list(cv.mac_address), - } - ), - cv.Optional(CONF_ON_BLE_SERVICE_DATA_ADVERTISE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - BLEServiceDataAdvertiseTrigger - ), - cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, - cv.Required(CONF_SERVICE_UUID): bt_uuid, - } - ), - cv.Optional( - CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE - ): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - BLEManufacturerDataAdvertiseTrigger - ), - cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, - cv.Required(CONF_MANUFACTURER_ID): bt_uuid, - } - ), - cv.Optional(CONF_ON_SCAN_END): automation.validate_automation( - {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(BLEEndOfScanTrigger)} - ), - } -).extend(cv.COMPONENT_SCHEMA) + cv.Optional( + CONF_ON_BLE_SERVICE_DATA_ADVERTISE + ): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + BLEServiceDataAdvertiseTrigger + ), + cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, + cv.Required(CONF_SERVICE_UUID): bt_uuid, + } + ), + cv.Optional( + CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE + ): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + BLEManufacturerDataAdvertiseTrigger + ), + cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, + cv.Required(CONF_MANUFACTURER_ID): bt_uuid, + } + ), + cv.Optional(CONF_ON_SCAN_END): automation.validate_automation( + {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(BLEEndOfScanTrigger)} + ), + } + ).extend(cv.COMPONENT_SCHEMA), +) -FINAL_VALIDATE_SCHEMA = esp32_ble.validate_variant + +def validate_remaining_connections(config): + data: dict[str, Any] = CORE.data.get(KEY_ESP32_BLE_TRACKER, {}) + slots: list[str] = data.get(KEY_USED_CONNECTION_SLOTS, []) + used_slots = len(slots) + if used_slots <= config[CONF_MAX_CONNECTIONS]: + return config + slot_users = ", ".join(slots) + hard_limit = max_connections() + + if used_slots < hard_limit: + _LOGGER.warning( + "esp32_ble_tracker exceeded `%s`: components attempted to consume %d " + "connection slot(s) out of available configured maximum %d connection " + "slot(s); The system automatically increased `%s` to %d to match the " + "number of used connection slot(s) by components: %s.", + CONF_MAX_CONNECTIONS, + used_slots, + config[CONF_MAX_CONNECTIONS], + CONF_MAX_CONNECTIONS, + used_slots, + slot_users, + ) + config[CONF_MAX_CONNECTIONS] = used_slots + return config + + msg = ( + f"esp32_ble_tracker exceeded `{CONF_MAX_CONNECTIONS}`: " + f"components attempted to consume {used_slots} connection slot(s) " + f"out of available configured maximum {config[CONF_MAX_CONNECTIONS]} " + f"connection slot(s); Decrease the number of BLE clients ({slot_users})" + ) + if config[CONF_MAX_CONNECTIONS] < hard_limit: + msg += f" or increase {CONF_MAX_CONNECTIONS}` to {used_slots}" + msg += f" to stay under the {hard_limit} connection slot(s) limit." + raise cv.Invalid(msg) + + +FINAL_VALIDATE_SCHEMA = cv.All( + validate_remaining_connections, esp32_ble.validate_variant +) ESP_BLE_DEVICE_SCHEMA = cv.Schema( { @@ -238,6 +319,9 @@ async def to_code(config): else: add_idf_sdkconfig_option("CONFIG_BTU_TASK_STACK_SIZE", 8192) add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", 9) + add_idf_sdkconfig_option( + "CONFIG_BTDM_CTRL_BLE_MAX_CONN", config[CONF_MAX_CONNECTIONS] + ) # CONFIG_BT_GATTC_NOTIF_REG_MAX controls the number of # max notifications in 5.x, setting CONFIG_BT_ACL_CONNECTIONS # is enough in 4.x diff --git a/tests/components/bluetooth_proxy/common.yaml b/tests/components/bluetooth_proxy/common.yaml new file mode 100644 index 0000000000..5e84f4a678 --- /dev/null +++ b/tests/components/bluetooth_proxy/common.yaml @@ -0,0 +1,8 @@ +wifi: + ssid: MySSID + password: password1 + +ota: + - platform: esphome + +api: diff --git a/tests/components/bluetooth_proxy/test.esp32-ard.yaml b/tests/components/bluetooth_proxy/test.esp32-ard.yaml new file mode 100644 index 0000000000..bf01b65b6f --- /dev/null +++ b/tests/components/bluetooth_proxy/test.esp32-ard.yaml @@ -0,0 +1,8 @@ +<<: !include common.yaml + +esp32_ble_tracker: + max_connections: 3 + +bluetooth_proxy: + active: true + connection_slots: 2 diff --git a/tests/components/bluetooth_proxy/test.esp32-c3-ard.yaml b/tests/components/bluetooth_proxy/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..bf01b65b6f --- /dev/null +++ b/tests/components/bluetooth_proxy/test.esp32-c3-ard.yaml @@ -0,0 +1,8 @@ +<<: !include common.yaml + +esp32_ble_tracker: + max_connections: 3 + +bluetooth_proxy: + active: true + connection_slots: 2 diff --git a/tests/components/bluetooth_proxy/test.esp32-c3-idf.yaml b/tests/components/bluetooth_proxy/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..6c27bd35d0 --- /dev/null +++ b/tests/components/bluetooth_proxy/test.esp32-c3-idf.yaml @@ -0,0 +1,8 @@ +<<: !include common.yaml + +esp32_ble_tracker: + max_connections: 9 + +bluetooth_proxy: + active: true + connection_slots: 9 diff --git a/tests/components/bluetooth_proxy/test.esp32-idf.yaml b/tests/components/bluetooth_proxy/test.esp32-idf.yaml new file mode 100644 index 0000000000..6c27bd35d0 --- /dev/null +++ b/tests/components/bluetooth_proxy/test.esp32-idf.yaml @@ -0,0 +1,8 @@ +<<: !include common.yaml + +esp32_ble_tracker: + max_connections: 9 + +bluetooth_proxy: + active: true + connection_slots: 9 diff --git a/tests/components/esp32_ble_tracker/test.esp32-ard.yaml b/tests/components/esp32_ble_tracker/test.esp32-ard.yaml index dade44d145..070fffd68b 100644 --- a/tests/components/esp32_ble_tracker/test.esp32-ard.yaml +++ b/tests/components/esp32_ble_tracker/test.esp32-ard.yaml @@ -1 +1,4 @@ <<: !include common.yaml + +esp32_ble_tracker: + max_connections: 3 diff --git a/tests/components/esp32_ble_tracker/test.esp32-c3-ard.yaml b/tests/components/esp32_ble_tracker/test.esp32-c3-ard.yaml index dade44d145..070fffd68b 100644 --- a/tests/components/esp32_ble_tracker/test.esp32-c3-ard.yaml +++ b/tests/components/esp32_ble_tracker/test.esp32-c3-ard.yaml @@ -1 +1,4 @@ <<: !include common.yaml + +esp32_ble_tracker: + max_connections: 3 diff --git a/tests/components/esp32_ble_tracker/test.esp32-c3-idf.yaml b/tests/components/esp32_ble_tracker/test.esp32-c3-idf.yaml index dade44d145..5e09f5020e 100644 --- a/tests/components/esp32_ble_tracker/test.esp32-c3-idf.yaml +++ b/tests/components/esp32_ble_tracker/test.esp32-c3-idf.yaml @@ -1 +1,4 @@ <<: !include common.yaml + +esp32_ble_tracker: + max_connections: 9 diff --git a/tests/components/esp32_ble_tracker/test.esp32-idf.yaml b/tests/components/esp32_ble_tracker/test.esp32-idf.yaml index dade44d145..5e09f5020e 100644 --- a/tests/components/esp32_ble_tracker/test.esp32-idf.yaml +++ b/tests/components/esp32_ble_tracker/test.esp32-idf.yaml @@ -1 +1,4 @@ <<: !include common.yaml + +esp32_ble_tracker: + max_connections: 9