Add support for esphome ble client connections v3 (#82815)

This commit is contained in:
J. Nick Koston 2022-11-28 16:56:18 -10:00 committed by GitHub
parent 230b50d099
commit fb98128b9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 84 additions and 9 deletions

View File

@ -36,6 +36,13 @@ DISCONNECT_TIMEOUT = 5.0
CONNECT_FREE_SLOT_TIMEOUT = 2.0 CONNECT_FREE_SLOT_TIMEOUT = 2.0
GATT_READ_TIMEOUT = 30.0 GATT_READ_TIMEOUT = 30.0
# CCCD (Characteristic Client Config Descriptor)
CCCD_UUID = "00002902-0000-1000-8000-00805f9b34fb"
CCCD_NOTIFY_BYTES = b"\x01\x00"
CCCD_INDICATE_BYTES = b"\x02\x00"
MIN_BLUETOOTH_PROXY_VERSION_HAS_CACHE = 3
DEFAULT_MAX_WRITE_WITHOUT_RESPONSE = DEFAULT_MTU - GATT_HEADER_SIZE DEFAULT_MAX_WRITE_WITHOUT_RESPONSE = DEFAULT_MTU - GATT_HEADER_SIZE
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -137,6 +144,9 @@ class ESPHomeClient(BaseBleakClient):
self._cancel_connection_state: CALLBACK_TYPE | None = None self._cancel_connection_state: CALLBACK_TYPE | None = None
self._notify_cancels: dict[int, Callable[[], Coroutine[Any, Any, None]]] = {} self._notify_cancels: dict[int, Callable[[], Coroutine[Any, Any, None]]] = {}
self._disconnected_event: asyncio.Event | None = None self._disconnected_event: asyncio.Event | None = None
device_info = self.entry_data.device_info
assert device_info is not None
self._connection_version = device_info.bluetooth_proxy_version
def __str__(self) -> str: def __str__(self) -> str:
"""Return the string representation of the client.""" """Return the string representation of the client."""
@ -206,7 +216,14 @@ class ESPHomeClient(BaseBleakClient):
Boolean representing connection status. Boolean representing connection status.
""" """
await self._wait_for_free_connection_slot(CONNECT_FREE_SLOT_TIMEOUT) await self._wait_for_free_connection_slot(CONNECT_FREE_SLOT_TIMEOUT)
entry_data = self.entry_data
self._mtu = entry_data.get_gatt_mtu_cache(self._address_as_int)
has_cache = bool(
dangerous_use_bleak_cache
and self._connection_version >= MIN_BLUETOOTH_PROXY_VERSION_HAS_CACHE
and entry_data.get_gatt_services_cache(self._address_as_int)
and self._mtu
)
connected_future: asyncio.Future[bool] = asyncio.Future() connected_future: asyncio.Future[bool] = asyncio.Future()
def _on_bluetooth_connection_state( def _on_bluetooth_connection_state(
@ -224,7 +241,9 @@ class ESPHomeClient(BaseBleakClient):
) )
if connected: if connected:
self._is_connected = True self._is_connected = True
self._mtu = mtu if not self._mtu:
self._mtu = mtu
entry_data.set_gatt_mtu_cache(self._address_as_int, mtu)
else: else:
self._async_ble_device_disconnected() self._async_ble_device_disconnected()
@ -258,7 +277,7 @@ class ESPHomeClient(BaseBleakClient):
self._ble_device.name, self._ble_device.name,
self._ble_device.address, self._ble_device.address,
) )
self.entry_data.disconnect_callbacks.append(self._async_esp_disconnected) entry_data.disconnect_callbacks.append(self._async_esp_disconnected)
connected_future.set_result(connected) connected_future.set_result(connected)
timeout = kwargs.get("timeout", self._timeout) timeout = kwargs.get("timeout", self._timeout)
@ -271,6 +290,8 @@ class ESPHomeClient(BaseBleakClient):
self._address_as_int, self._address_as_int,
_on_bluetooth_connection_state, _on_bluetooth_connection_state,
timeout=timeout, timeout=timeout,
has_cache=has_cache,
version=self._connection_version,
) )
) )
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
@ -344,9 +365,13 @@ class ESPHomeClient(BaseBleakClient):
""" """
address_as_int = self._address_as_int address_as_int = self._address_as_int
entry_data = self.entry_data entry_data = self.entry_data
if dangerous_use_bleak_cache and ( # If the connection version >= 3, we must use the cache
cached_services := entry_data.get_gatt_services_cache(address_as_int) # because the esp has already wiped the services list to
): # save memory.
if (
self._connection_version >= MIN_BLUETOOTH_PROXY_VERSION_HAS_CACHE
or dangerous_use_bleak_cache
) and (cached_services := entry_data.get_gatt_services_cache(address_as_int)):
_LOGGER.debug( _LOGGER.debug(
"%s: %s - %s: Cached services hit", "%s: %s - %s: Cached services hit",
self._source, self._source,
@ -516,6 +541,14 @@ class ESPHomeClient(BaseBleakClient):
f"characteristic:{characteristic.uuid} " f"characteristic:{characteristic.uuid} "
f"handle:{ble_handle}" f"handle:{ble_handle}"
) )
if (
"notify" not in characteristic.properties
and "indicate" not in characteristic.properties
):
raise BleakError(
f"Characteristic {characteristic.uuid} does not have notify or indicate property set."
)
cancel_coro = await self._client.bluetooth_gatt_start_notify( cancel_coro = await self._client.bluetooth_gatt_start_notify(
self._address_as_int, self._address_as_int,
ble_handle, ble_handle,
@ -523,6 +556,37 @@ class ESPHomeClient(BaseBleakClient):
) )
self._notify_cancels[ble_handle] = cancel_coro self._notify_cancels[ble_handle] = cancel_coro
if self._connection_version < MIN_BLUETOOTH_PROXY_VERSION_HAS_CACHE:
return
# For connection v3 we are responsible for enabling notifications
# on the cccd (characteristic client config descriptor) handle since
# the esp32 will not have resolved the characteristic descriptors to
# save memory since doing so can exhaust the memory and cause a soft
# reset
cccd_descriptor = characteristic.get_descriptor(CCCD_UUID)
if not cccd_descriptor:
raise BleakError(
f"Characteristic {characteristic.uuid} does not have a "
"characteristic client config descriptor."
)
_LOGGER.debug(
"%s: %s - %s: Writing to CCD descriptor %s for notifications with properties=%s",
self._source,
self._ble_device.name,
self._ble_device.address,
cccd_descriptor.handle,
characteristic.properties,
)
supports_notify = "notify" in characteristic.properties
await self._client.bluetooth_gatt_write_descriptor(
self._address_as_int,
cccd_descriptor.handle,
CCCD_NOTIFY_BYTES if supports_notify else CCCD_INDICATE_BYTES,
wait_for_response=False,
)
@api_error_as_bleak_error @api_error_as_bleak_error
async def stop_notify( async def stop_notify(
self, self,

View File

@ -98,6 +98,9 @@ class RuntimeEntryData:
_gatt_services_cache: MutableMapping[int, BleakGATTServiceCollection] = field( _gatt_services_cache: MutableMapping[int, BleakGATTServiceCollection] = field(
default_factory=lambda: LRU(MAX_CACHED_SERVICES) # type: ignore[no-any-return] default_factory=lambda: LRU(MAX_CACHED_SERVICES) # type: ignore[no-any-return]
) )
_gatt_mtu_cache: MutableMapping[int, int] = field(
default_factory=lambda: LRU(MAX_CACHED_SERVICES) # type: ignore[no-any-return]
)
@property @property
def name(self) -> str: def name(self) -> str:
@ -116,6 +119,14 @@ class RuntimeEntryData:
"""Set the BleakGATTServiceCollection for the given address.""" """Set the BleakGATTServiceCollection for the given address."""
self._gatt_services_cache[address] = services self._gatt_services_cache[address] = services
def get_gatt_mtu_cache(self, address: int) -> int | None:
"""Get the mtu cache for the given address."""
return self._gatt_mtu_cache.get(address)
def set_gatt_mtu_cache(self, address: int, mtu: int) -> None:
"""Set the mtu cache for the given address."""
self._gatt_mtu_cache[address] = mtu
@callback @callback
def async_update_ble_connection_limits(self, free: int, limit: int) -> None: def async_update_ble_connection_limits(self, free: int, limit: int) -> None:
"""Update the BLE connection limits.""" """Update the BLE connection limits."""

View File

@ -3,7 +3,7 @@
"name": "ESPHome", "name": "ESPHome",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/esphome", "documentation": "https://www.home-assistant.io/integrations/esphome",
"requirements": ["aioesphomeapi==12.0.1"], "requirements": ["aioesphomeapi==12.1.0"],
"zeroconf": ["_esphomelib._tcp.local."], "zeroconf": ["_esphomelib._tcp.local."],
"dhcp": [{ "registered_devices": true }], "dhcp": [{ "registered_devices": true }],
"codeowners": ["@OttoWinter", "@jesserockz"], "codeowners": ["@OttoWinter", "@jesserockz"],

View File

@ -156,7 +156,7 @@ aioecowitt==2022.09.3
aioemonitor==1.0.5 aioemonitor==1.0.5
# homeassistant.components.esphome # homeassistant.components.esphome
aioesphomeapi==12.0.1 aioesphomeapi==12.1.0
# homeassistant.components.flo # homeassistant.components.flo
aioflo==2021.11.0 aioflo==2021.11.0

View File

@ -143,7 +143,7 @@ aioecowitt==2022.09.3
aioemonitor==1.0.5 aioemonitor==1.0.5
# homeassistant.components.esphome # homeassistant.components.esphome
aioesphomeapi==12.0.1 aioesphomeapi==12.1.0
# homeassistant.components.flo # homeassistant.components.flo
aioflo==2021.11.0 aioflo==2021.11.0