Fix ESPHome BLE client raising confusing error when not connected (#104146)

pull/104149/head^2
J. Nick Koston 2023-11-20 03:08:44 -06:00 committed by GitHub
parent ae2099b2eb
commit a9384d6d4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 107 additions and 89 deletions

View File

@ -75,15 +75,13 @@ def verify_connected(func: _WrapFuncType) -> _WrapFuncType:
self: ESPHomeClient, *args: Any, **kwargs: Any
) -> Any:
# pylint: disable=protected-access
if not self._is_connected:
raise BleakError(f"{self._description} is not connected")
loop = self._loop
disconnected_futures = self._disconnected_futures
disconnected_future = loop.create_future()
disconnected_futures.add(disconnected_future)
ble_device = self._ble_device
disconnect_message = (
f"{self._source_name }: {ble_device.name} - {ble_device.address}: "
"Disconnected during operation"
)
disconnect_message = f"{self._description}: Disconnected during operation"
try:
async with interrupt(disconnected_future, BleakError, disconnect_message):
return await func(self, *args, **kwargs)
@ -115,10 +113,8 @@ def api_error_as_bleak_error(func: _WrapFuncType) -> _WrapFuncType:
if ex.error.error == -1:
# pylint: disable=protected-access
_LOGGER.debug(
"%s: %s - %s: BLE device disconnected during %s operation",
self._source_name,
self._ble_device.name,
self._ble_device.address,
"%s: BLE device disconnected during %s operation",
self._description,
func.__name__,
)
self._async_ble_device_disconnected()
@ -159,10 +155,11 @@ class ESPHomeClient(BaseBleakClient):
assert isinstance(address_or_ble_device, BLEDevice)
super().__init__(address_or_ble_device, *args, **kwargs)
self._loop = asyncio.get_running_loop()
self._ble_device = address_or_ble_device
self._address_as_int = mac_to_int(self._ble_device.address)
assert self._ble_device.details is not None
self._source = self._ble_device.details["source"]
ble_device = address_or_ble_device
self._ble_device = ble_device
self._address_as_int = mac_to_int(ble_device.address)
assert ble_device.details is not None
self._source = ble_device.details["source"]
self._cache = client_data.cache
self._bluetooth_device = client_data.bluetooth_device
self._client = client_data.client
@ -177,8 +174,11 @@ class ESPHomeClient(BaseBleakClient):
self._feature_flags = device_info.bluetooth_proxy_feature_flags_compat(
client_data.api_version
)
self._address_type = address_or_ble_device.details["address_type"]
self._address_type = ble_device.details["address_type"]
self._source_name = f"{client_data.title} [{self._source}]"
self._description = (
f"{self._source_name}: {ble_device.name} - {ble_device.address}"
)
scanner = client_data.scanner
assert scanner is not None
self._scanner = scanner
@ -196,12 +196,10 @@ class ESPHomeClient(BaseBleakClient):
except (AssertionError, ValueError) as ex:
_LOGGER.debug(
(
"%s: %s - %s: Failed to unsubscribe from connection state (likely"
"%s: Failed to unsubscribe from connection state (likely"
" connection dropped): %s"
),
self._source_name,
self._ble_device.name,
self._ble_device.address,
self._description,
ex,
)
self._cancel_connection_state = None
@ -224,22 +222,12 @@ class ESPHomeClient(BaseBleakClient):
was_connected = self._is_connected
self._async_disconnected_cleanup()
if was_connected:
_LOGGER.debug(
"%s: %s - %s: BLE device disconnected",
self._source_name,
self._ble_device.name,
self._ble_device.address,
)
_LOGGER.debug("%s: BLE device disconnected", self._description)
self._async_call_bleak_disconnected_callback()
def _async_esp_disconnected(self) -> None:
"""Handle the esp32 client disconnecting from us."""
_LOGGER.debug(
"%s: %s - %s: ESP device disconnected",
self._source_name,
self._ble_device.name,
self._ble_device.address,
)
_LOGGER.debug("%s: ESP device disconnected", self._description)
self._disconnect_callbacks.remove(self._async_esp_disconnected)
self._async_ble_device_disconnected()
@ -258,10 +246,8 @@ class ESPHomeClient(BaseBleakClient):
) -> None:
"""Handle a connect or disconnect."""
_LOGGER.debug(
"%s: %s - %s: Connection state changed to connected=%s mtu=%s error=%s",
self._source_name,
self._ble_device.name,
self._ble_device.address,
"%s: Connection state changed to connected=%s mtu=%s error=%s",
self._description,
connected,
mtu,
error,
@ -300,10 +286,8 @@ class ESPHomeClient(BaseBleakClient):
return
_LOGGER.debug(
"%s: %s - %s: connected, registering for disconnected callbacks",
self._source_name,
self._ble_device.name,
self._ble_device.address,
"%s: connected, registering for disconnected callbacks",
self._description,
)
self._disconnect_callbacks.append(self._async_esp_disconnected)
connected_future.set_result(connected)
@ -403,10 +387,8 @@ class ESPHomeClient(BaseBleakClient):
if bluetooth_device.ble_connections_free:
return
_LOGGER.debug(
"%s: %s - %s: Out of connection slots, waiting for a free one",
self._source_name,
self._ble_device.name,
self._ble_device.address,
"%s: Out of connection slots, waiting for a free one",
self._description,
)
async with asyncio.timeout(timeout):
await bluetooth_device.wait_for_ble_connections_free()
@ -434,7 +416,7 @@ class ESPHomeClient(BaseBleakClient):
if response.paired:
return True
_LOGGER.error(
"Pairing with %s failed due to error: %s", self.address, response.error
"%s: Pairing failed due to error: %s", self._description, response.error
)
return False
@ -451,7 +433,7 @@ class ESPHomeClient(BaseBleakClient):
if response.success:
return True
_LOGGER.error(
"Unpairing with %s failed due to error: %s", self.address, response.error
"%s: Unpairing failed due to error: %s", self._description, response.error
)
return False
@ -486,30 +468,14 @@ class ESPHomeClient(BaseBleakClient):
self._feature_flags & BluetoothProxyFeature.REMOTE_CACHING
or dangerous_use_bleak_cache
) and (cached_services := cache.get_gatt_services_cache(address_as_int)):
_LOGGER.debug(
"%s: %s - %s: Cached services hit",
self._source_name,
self._ble_device.name,
self._ble_device.address,
)
_LOGGER.debug("%s: Cached services hit", self._description)
self.services = cached_services
return self.services
_LOGGER.debug(
"%s: %s - %s: Cached services miss",
self._source_name,
self._ble_device.name,
self._ble_device.address,
)
_LOGGER.debug("%s: Cached services miss", self._description)
esphome_services = await self._client.bluetooth_gatt_get_services(
address_as_int
)
_LOGGER.debug(
"%s: %s - %s: Got services: %s",
self._source_name,
self._ble_device.name,
self._ble_device.address,
esphome_services,
)
_LOGGER.debug("%s: Got services: %s", self._description, esphome_services)
max_write_without_response = self.mtu_size - GATT_HEADER_SIZE
services = BleakGATTServiceCollection() # type: ignore[no-untyped-call]
for service in esphome_services.services:
@ -538,12 +504,7 @@ class ESPHomeClient(BaseBleakClient):
raise BleakError("Failed to get services from remote esp")
self.services = services
_LOGGER.debug(
"%s: %s - %s: Cached services saved",
self._source_name,
self._ble_device.name,
self._ble_device.address,
)
_LOGGER.debug("%s: Cached services saved", self._description)
cache.set_gatt_services_cache(address_as_int, services)
return services
@ -552,13 +513,15 @@ class ESPHomeClient(BaseBleakClient):
) -> BleakGATTCharacteristic:
"""Resolve a characteristic specifier to a BleakGATTCharacteristic object."""
if (services := self.services) is None:
raise BleakError("Services have not been resolved")
raise BleakError(f"{self._description}: Services have not been resolved")
if not isinstance(char_specifier, BleakGATTCharacteristic):
characteristic = services.get_characteristic(char_specifier)
else:
characteristic = char_specifier
if not characteristic:
raise BleakError(f"Characteristic {char_specifier} was not found!")
raise BleakError(
f"{self._description}: Characteristic {char_specifier} was not found!"
)
return characteristic
@verify_connected
@ -579,8 +542,8 @@ class ESPHomeClient(BaseBleakClient):
if response.success:
return True
_LOGGER.error(
"Clear cache failed with %s failed due to error: %s",
self.address,
"%s: Clear cache failed due to error: %s",
self._description,
response.error,
)
return False
@ -692,7 +655,7 @@ class ESPHomeClient(BaseBleakClient):
ble_handle = characteristic.handle
if ble_handle in self._notify_cancels:
raise BleakError(
"Notifications are already enabled on "
f"{self._description}: Notifications are already enabled on "
f"service:{characteristic.service_uuid} "
f"characteristic:{characteristic.uuid} "
f"handle:{ble_handle}"
@ -702,8 +665,8 @@ class ESPHomeClient(BaseBleakClient):
and "indicate" not in characteristic.properties
):
raise BleakError(
f"Characteristic {characteristic.uuid} does not have notify or indicate"
" property set."
f"{self._description}: Characteristic {characteristic.uuid} "
"does not have notify or indicate property set."
)
self._notify_cancels[
@ -725,18 +688,13 @@ class ESPHomeClient(BaseBleakClient):
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."
f"{self._description}: 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_name,
self._ble_device.name,
self._ble_device.address,
"%s: Writing to CCD descriptor %s for notifications with properties=%s",
self._description,
cccd_descriptor.handle,
characteristic.properties,
)
@ -774,12 +732,10 @@ class ESPHomeClient(BaseBleakClient):
if self._cancel_connection_state:
_LOGGER.warning(
(
"%s: %s - %s: ESPHomeClient bleak client was not properly"
"%s: ESPHomeClient bleak client was not properly"
" disconnected before destruction"
),
self._source_name,
self._ble_device.name,
self._ble_device.address,
self._description,
)
if not self._loop.is_closed():
self._loop.call_soon_threadsafe(self._async_disconnected_cleanup)

View File

@ -0,0 +1,62 @@
"""Tests for ESPHomeClient."""
from __future__ import annotations
from aioesphomeapi import APIClient, APIVersion, BluetoothProxyFeature, DeviceInfo
from bleak.exc import BleakError
import pytest
from homeassistant.components.bluetooth import HaBluetoothConnector
from homeassistant.components.esphome.bluetooth.cache import ESPHomeBluetoothCache
from homeassistant.components.esphome.bluetooth.client import (
ESPHomeClient,
ESPHomeClientData,
)
from homeassistant.components.esphome.bluetooth.device import ESPHomeBluetoothDevice
from homeassistant.components.esphome.bluetooth.scanner import ESPHomeScanner
from homeassistant.core import HomeAssistant
from tests.components.bluetooth import generate_ble_device
ESP_MAC_ADDRESS = "AA:BB:CC:DD:EE:FF"
ESP_NAME = "proxy"
@pytest.fixture(name="client_data")
async def client_data_fixture(
hass: HomeAssistant, mock_client: APIClient
) -> ESPHomeClientData:
"""Return a client data fixture."""
connector = HaBluetoothConnector(ESPHomeClientData, ESP_MAC_ADDRESS, lambda: True)
return ESPHomeClientData(
bluetooth_device=ESPHomeBluetoothDevice(ESP_NAME, ESP_MAC_ADDRESS),
cache=ESPHomeBluetoothCache(),
client=mock_client,
device_info=DeviceInfo(
mac_address=ESP_MAC_ADDRESS,
name=ESP_NAME,
bluetooth_proxy_feature_flags=BluetoothProxyFeature.PASSIVE_SCAN
& BluetoothProxyFeature.ACTIVE_CONNECTIONS
& BluetoothProxyFeature.REMOTE_CACHING
& BluetoothProxyFeature.PAIRING
& BluetoothProxyFeature.CACHE_CLEARING
& BluetoothProxyFeature.RAW_ADVERTISEMENTS,
),
api_version=APIVersion(1, 9),
title=ESP_NAME,
scanner=ESPHomeScanner(
hass, ESP_MAC_ADDRESS, ESP_NAME, lambda info: None, connector, True
),
)
async def test_client_usage_while_not_connected(client_data: ESPHomeClientData) -> None:
"""Test client usage while not connected."""
ble_device = generate_ble_device(
"CC:BB:AA:DD:EE:FF", details={"source": ESP_MAC_ADDRESS, "address_type": 1}
)
client = ESPHomeClient(ble_device, client_data=client_data)
with pytest.raises(
BleakError, match=f"{ESP_NAME}.*{ESP_MAC_ADDRESS}.*not connected"
):
await client.write_gatt_char("test", b"test") is False