Improve performance of Bluetooth device fallback (#79078)
This commit is contained in:
parent
a8b8b245d1
commit
75f6f9b5e2
10 changed files with 155 additions and 24 deletions
|
@ -235,6 +235,23 @@ class BluetoothManager:
|
||||||
self._cancel_unavailable_tracking.clear()
|
self._cancel_unavailable_tracking.clear()
|
||||||
uninstall_multiple_bleak_catcher()
|
uninstall_multiple_bleak_catcher()
|
||||||
|
|
||||||
|
async def async_get_devices_by_address(
|
||||||
|
self, address: str, connectable: bool
|
||||||
|
) -> list[BLEDevice]:
|
||||||
|
"""Get devices by address."""
|
||||||
|
types_ = (True,) if connectable else (True, False)
|
||||||
|
return [
|
||||||
|
device
|
||||||
|
for device in await asyncio.gather(
|
||||||
|
*(
|
||||||
|
scanner.async_get_device_by_address(address)
|
||||||
|
for type_ in types_
|
||||||
|
for scanner in self._get_scanners_by_type(type_)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if device is not None
|
||||||
|
]
|
||||||
|
|
||||||
@hass_callback
|
@hass_callback
|
||||||
def async_all_discovered_devices(self, connectable: bool) -> Iterable[BLEDevice]:
|
def async_all_discovered_devices(self, connectable: bool) -> Iterable[BLEDevice]:
|
||||||
"""Return all of discovered devices from all the scanners including duplicates."""
|
"""Return all of discovered devices from all the scanners including duplicates."""
|
||||||
|
|
|
@ -6,9 +6,9 @@
|
||||||
"after_dependencies": ["hassio"],
|
"after_dependencies": ["hassio"],
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"bleak==0.18.0",
|
"bleak==0.18.1",
|
||||||
"bleak-retry-connector==2.0.2",
|
"bleak-retry-connector==2.1.0",
|
||||||
"bluetooth-adapters==0.5.1",
|
"bluetooth-adapters==0.5.2",
|
||||||
"bluetooth-auto-recovery==0.3.3",
|
"bluetooth-auto-recovery==0.3.3",
|
||||||
"dbus-fast==1.14.0"
|
"dbus-fast==1.14.0"
|
||||||
],
|
],
|
||||||
|
|
|
@ -91,6 +91,10 @@ class BaseHaScanner:
|
||||||
def discovered_devices(self) -> list[BLEDevice]:
|
def discovered_devices(self) -> list[BLEDevice]:
|
||||||
"""Return a list of discovered devices."""
|
"""Return a list of discovered devices."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def async_get_device_by_address(self, address: str) -> BLEDevice | None:
|
||||||
|
"""Get a device by address."""
|
||||||
|
|
||||||
async def async_diagnostics(self) -> dict[str, Any]:
|
async def async_diagnostics(self) -> dict[str, Any]:
|
||||||
"""Return diagnostic information about the scanner."""
|
"""Return diagnostic information about the scanner."""
|
||||||
return {
|
return {
|
||||||
|
@ -256,7 +260,9 @@ class HaBleakClientWrapper(BleakClient):
|
||||||
async def connect(self, **kwargs: Any) -> bool:
|
async def connect(self, **kwargs: Any) -> bool:
|
||||||
"""Connect to the specified GATT server."""
|
"""Connect to the specified GATT server."""
|
||||||
if not self._backend:
|
if not self._backend:
|
||||||
wrapped_backend = self._async_get_backend()
|
wrapped_backend = (
|
||||||
|
self._async_get_backend() or await self._async_get_fallback_backend()
|
||||||
|
)
|
||||||
self._backend = wrapped_backend.client(
|
self._backend = wrapped_backend.client(
|
||||||
await freshen_ble_device(wrapped_backend.device)
|
await freshen_ble_device(wrapped_backend.device)
|
||||||
or wrapped_backend.device,
|
or wrapped_backend.device,
|
||||||
|
@ -286,7 +292,7 @@ class HaBleakClientWrapper(BleakClient):
|
||||||
return _HaWrappedBleakBackend(ble_device, connector.client)
|
return _HaWrappedBleakBackend(ble_device, connector.client)
|
||||||
|
|
||||||
@hass_callback
|
@hass_callback
|
||||||
def _async_get_backend(self) -> _HaWrappedBleakBackend:
|
def _async_get_backend(self) -> _HaWrappedBleakBackend | None:
|
||||||
"""Get the bleak backend for the given address."""
|
"""Get the bleak backend for the given address."""
|
||||||
assert MANAGER is not None
|
assert MANAGER is not None
|
||||||
address = self.__address
|
address = self.__address
|
||||||
|
@ -297,6 +303,10 @@ class HaBleakClientWrapper(BleakClient):
|
||||||
if backend := self._async_get_backend_for_ble_device(ble_device):
|
if backend := self._async_get_backend_for_ble_device(ble_device):
|
||||||
return backend
|
return backend
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _async_get_fallback_backend(self) -> _HaWrappedBleakBackend:
|
||||||
|
"""Get a fallback backend for the given address."""
|
||||||
#
|
#
|
||||||
# The preferred backend cannot currently connect the device
|
# The preferred backend cannot currently connect the device
|
||||||
# because it is likely out of connection slots.
|
# because it is likely out of connection slots.
|
||||||
|
@ -304,16 +314,11 @@ class HaBleakClientWrapper(BleakClient):
|
||||||
# We need to try all backends to find one that can
|
# We need to try all backends to find one that can
|
||||||
# connect to the device.
|
# connect to the device.
|
||||||
#
|
#
|
||||||
# Currently we have to search all the discovered devices
|
assert MANAGER is not None
|
||||||
# because the bleak API does not allow us to get the
|
address = self.__address
|
||||||
# details for a specific device.
|
devices = await MANAGER.async_get_devices_by_address(address, True)
|
||||||
#
|
|
||||||
for ble_device in sorted(
|
for ble_device in sorted(
|
||||||
(
|
devices,
|
||||||
ble_device
|
|
||||||
for ble_device in MANAGER.async_all_discovered_devices(True)
|
|
||||||
if ble_device.address == address
|
|
||||||
),
|
|
||||||
key=lambda ble_device: ble_device.rssi or NO_RSSI_VALUE,
|
key=lambda ble_device: ble_device.rssi or NO_RSSI_VALUE,
|
||||||
reverse=True,
|
reverse=True,
|
||||||
):
|
):
|
||||||
|
|
|
@ -17,6 +17,7 @@ from bleak.backends.bluezdbus.advertisement_monitor import OrPattern
|
||||||
from bleak.backends.bluezdbus.scanner import BlueZScannerArgs
|
from bleak.backends.bluezdbus.scanner import BlueZScannerArgs
|
||||||
from bleak.backends.device import BLEDevice
|
from bleak.backends.device import BLEDevice
|
||||||
from bleak.backends.scanner import AdvertisementData
|
from bleak.backends.scanner import AdvertisementData
|
||||||
|
from bleak_retry_connector import get_device_by_adapter
|
||||||
from dbus_fast import InvalidMessageError
|
from dbus_fast import InvalidMessageError
|
||||||
|
|
||||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback
|
||||||
|
@ -140,6 +141,16 @@ class HaScanner(BaseHaScanner):
|
||||||
"""Return a list of discovered devices."""
|
"""Return a list of discovered devices."""
|
||||||
return self.scanner.discovered_devices
|
return self.scanner.discovered_devices
|
||||||
|
|
||||||
|
async def async_get_device_by_address(self, address: str) -> BLEDevice | None:
|
||||||
|
"""Get a device by address."""
|
||||||
|
if platform.system() == "Linux":
|
||||||
|
return await get_device_by_adapter(address, self.adapter)
|
||||||
|
# We don't have a fast version of this for MacOS yet
|
||||||
|
return next(
|
||||||
|
(device for device in self.discovered_devices if device.address == address),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
async def async_diagnostics(self) -> dict[str, Any]:
|
async def async_diagnostics(self) -> dict[str, Any]:
|
||||||
"""Return diagnostic information about the scanner."""
|
"""Return diagnostic information about the scanner."""
|
||||||
base_diag = await super().async_diagnostics()
|
base_diag = await super().async_diagnostics()
|
||||||
|
|
|
@ -13,7 +13,7 @@ ORIGINAL_BLEAK_CLIENT = bleak.BleakClient
|
||||||
def install_multiple_bleak_catcher() -> None:
|
def install_multiple_bleak_catcher() -> None:
|
||||||
"""Wrap the bleak classes to return the shared instance if multiple instances are detected."""
|
"""Wrap the bleak classes to return the shared instance if multiple instances are detected."""
|
||||||
bleak.BleakScanner = HaBleakScannerWrapper # type: ignore[misc, assignment]
|
bleak.BleakScanner = HaBleakScannerWrapper # type: ignore[misc, assignment]
|
||||||
bleak.BleakClient = HaBleakClientWrapper # type: ignore[misc, assignment]
|
bleak.BleakClient = HaBleakClientWrapper # type: ignore[misc]
|
||||||
|
|
||||||
|
|
||||||
def uninstall_multiple_bleak_catcher() -> None:
|
def uninstall_multiple_bleak_catcher() -> None:
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
"""Bluetooth scanner for esphome."""
|
"""Bluetooth scanner for esphome."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
import datetime
|
import datetime
|
||||||
|
@ -77,6 +78,10 @@ class ESPHomeScannner(BaseHaScanner):
|
||||||
"""Return a list of discovered devices."""
|
"""Return a list of discovered devices."""
|
||||||
return list(self._discovered_devices.values())
|
return list(self._discovered_devices.values())
|
||||||
|
|
||||||
|
async def async_get_device_by_address(self, address: str) -> BLEDevice | None:
|
||||||
|
"""Get a device by address."""
|
||||||
|
return self._discovered_devices.get(address)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_on_advertisement(self, adv: BluetoothLEAdvertisement) -> None:
|
def async_on_advertisement(self, adv: BluetoothLEAdvertisement) -> None:
|
||||||
"""Call the registered callback."""
|
"""Call the registered callback."""
|
||||||
|
|
|
@ -10,9 +10,9 @@ atomicwrites-homeassistant==1.4.1
|
||||||
attrs==21.2.0
|
attrs==21.2.0
|
||||||
awesomeversion==22.9.0
|
awesomeversion==22.9.0
|
||||||
bcrypt==3.1.7
|
bcrypt==3.1.7
|
||||||
bleak-retry-connector==2.0.2
|
bleak-retry-connector==2.1.0
|
||||||
bleak==0.18.0
|
bleak==0.18.1
|
||||||
bluetooth-adapters==0.5.1
|
bluetooth-adapters==0.5.2
|
||||||
bluetooth-auto-recovery==0.3.3
|
bluetooth-auto-recovery==0.3.3
|
||||||
certifi>=2021.5.30
|
certifi>=2021.5.30
|
||||||
ciso8601==2.2.0
|
ciso8601==2.2.0
|
||||||
|
|
|
@ -410,10 +410,10 @@ bimmer_connected==0.10.4
|
||||||
bizkaibus==0.1.1
|
bizkaibus==0.1.1
|
||||||
|
|
||||||
# homeassistant.components.bluetooth
|
# homeassistant.components.bluetooth
|
||||||
bleak-retry-connector==2.0.2
|
bleak-retry-connector==2.1.0
|
||||||
|
|
||||||
# homeassistant.components.bluetooth
|
# homeassistant.components.bluetooth
|
||||||
bleak==0.18.0
|
bleak==0.18.1
|
||||||
|
|
||||||
# homeassistant.components.blebox
|
# homeassistant.components.blebox
|
||||||
blebox_uniapi==2.0.2
|
blebox_uniapi==2.0.2
|
||||||
|
@ -435,7 +435,7 @@ bluemaestro-ble==0.2.0
|
||||||
# bluepy==1.3.0
|
# bluepy==1.3.0
|
||||||
|
|
||||||
# homeassistant.components.bluetooth
|
# homeassistant.components.bluetooth
|
||||||
bluetooth-adapters==0.5.1
|
bluetooth-adapters==0.5.2
|
||||||
|
|
||||||
# homeassistant.components.bluetooth
|
# homeassistant.components.bluetooth
|
||||||
bluetooth-auto-recovery==0.3.3
|
bluetooth-auto-recovery==0.3.3
|
||||||
|
|
|
@ -331,10 +331,10 @@ bellows==0.33.1
|
||||||
bimmer_connected==0.10.4
|
bimmer_connected==0.10.4
|
||||||
|
|
||||||
# homeassistant.components.bluetooth
|
# homeassistant.components.bluetooth
|
||||||
bleak-retry-connector==2.0.2
|
bleak-retry-connector==2.1.0
|
||||||
|
|
||||||
# homeassistant.components.bluetooth
|
# homeassistant.components.bluetooth
|
||||||
bleak==0.18.0
|
bleak==0.18.1
|
||||||
|
|
||||||
# homeassistant.components.blebox
|
# homeassistant.components.blebox
|
||||||
blebox_uniapi==2.0.2
|
blebox_uniapi==2.0.2
|
||||||
|
@ -346,7 +346,7 @@ blinkpy==0.19.2
|
||||||
bluemaestro-ble==0.2.0
|
bluemaestro-ble==0.2.0
|
||||||
|
|
||||||
# homeassistant.components.bluetooth
|
# homeassistant.components.bluetooth
|
||||||
bluetooth-adapters==0.5.1
|
bluetooth-adapters==0.5.2
|
||||||
|
|
||||||
# homeassistant.components.bluetooth
|
# homeassistant.components.bluetooth
|
||||||
bluetooth-auto-recovery==0.3.3
|
bluetooth-auto-recovery==0.3.3
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
"""Tests for the Bluetooth integration models."""
|
"""Tests for the Bluetooth integration models."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
@ -197,6 +198,12 @@ async def test_ble_device_with_proxy_client_out_of_connections_uses_best_availab
|
||||||
"""Return a list of discovered devices."""
|
"""Return a list of discovered devices."""
|
||||||
return [switchbot_proxy_device_has_connection_slot]
|
return [switchbot_proxy_device_has_connection_slot]
|
||||||
|
|
||||||
|
async def async_get_device_by_address(self, address: str) -> BLEDevice | None:
|
||||||
|
"""Return a list of discovered devices."""
|
||||||
|
if address == switchbot_proxy_device_has_connection_slot.address:
|
||||||
|
return switchbot_proxy_device_has_connection_slot
|
||||||
|
return None
|
||||||
|
|
||||||
scanner = FakeScanner()
|
scanner = FakeScanner()
|
||||||
cancel = manager.async_register_scanner(scanner, True)
|
cancel = manager.async_register_scanner(scanner, True)
|
||||||
assert manager.async_discovered_devices(True) == [
|
assert manager.async_discovered_devices(True) == [
|
||||||
|
@ -210,3 +217,89 @@ async def test_ble_device_with_proxy_client_out_of_connections_uses_best_availab
|
||||||
client.set_disconnected_callback(lambda client: None)
|
client.set_disconnected_callback(lambda client: None)
|
||||||
await client.disconnect()
|
await client.disconnect()
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_ble_device_with_proxy_client_out_of_connections_uses_best_available_macos(
|
||||||
|
hass, enable_bluetooth, macos_adapter
|
||||||
|
):
|
||||||
|
"""Test we switch to the next available proxy when one runs out of connections on MacOS."""
|
||||||
|
manager = _get_manager()
|
||||||
|
|
||||||
|
switchbot_proxy_device_no_connection_slot = BLEDevice(
|
||||||
|
"44:44:33:11:23:45",
|
||||||
|
"wohand_no_connection_slot",
|
||||||
|
{
|
||||||
|
"connector": HaBluetoothConnector(
|
||||||
|
MockBleakClient, "mock_bleak_client", lambda: False
|
||||||
|
),
|
||||||
|
"path": "/org/bluez/hci0/dev_44_44_33_11_23_45",
|
||||||
|
},
|
||||||
|
rssi=-30,
|
||||||
|
)
|
||||||
|
switchbot_proxy_device_no_connection_slot.metadata["delegate"] = 0
|
||||||
|
|
||||||
|
switchbot_proxy_device_has_connection_slot = BLEDevice(
|
||||||
|
"44:44:33:11:23:45",
|
||||||
|
"wohand_has_connection_slot",
|
||||||
|
{
|
||||||
|
"connector": HaBluetoothConnector(
|
||||||
|
MockBleakClient, "mock_bleak_client", lambda: True
|
||||||
|
),
|
||||||
|
"path": "/org/bluez/hci0/dev_44_44_33_11_23_45",
|
||||||
|
},
|
||||||
|
rssi=-40,
|
||||||
|
)
|
||||||
|
switchbot_proxy_device_has_connection_slot.metadata["delegate"] = 0
|
||||||
|
|
||||||
|
switchbot_device = BLEDevice(
|
||||||
|
"44:44:33:11:23:45",
|
||||||
|
"wohand",
|
||||||
|
{},
|
||||||
|
rssi=-100,
|
||||||
|
)
|
||||||
|
switchbot_device.metadata["delegate"] = 0
|
||||||
|
switchbot_adv = AdvertisementData(
|
||||||
|
local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"}
|
||||||
|
)
|
||||||
|
|
||||||
|
inject_advertisement_with_source(
|
||||||
|
hass, switchbot_device, switchbot_adv, "00:00:00:00:00:01"
|
||||||
|
)
|
||||||
|
inject_advertisement_with_source(
|
||||||
|
hass,
|
||||||
|
switchbot_proxy_device_has_connection_slot,
|
||||||
|
switchbot_adv,
|
||||||
|
"esp32_has_connection_slot",
|
||||||
|
)
|
||||||
|
inject_advertisement_with_source(
|
||||||
|
hass,
|
||||||
|
switchbot_proxy_device_no_connection_slot,
|
||||||
|
switchbot_adv,
|
||||||
|
"esp32_no_connection_slot",
|
||||||
|
)
|
||||||
|
|
||||||
|
class FakeScanner(BaseHaScanner):
|
||||||
|
@property
|
||||||
|
def discovered_devices(self) -> list[BLEDevice]:
|
||||||
|
"""Return a list of discovered devices."""
|
||||||
|
return [switchbot_proxy_device_has_connection_slot]
|
||||||
|
|
||||||
|
async def async_get_device_by_address(self, address: str) -> BLEDevice | None:
|
||||||
|
"""Return a list of discovered devices."""
|
||||||
|
if address == switchbot_proxy_device_has_connection_slot.address:
|
||||||
|
return switchbot_proxy_device_has_connection_slot
|
||||||
|
return None
|
||||||
|
|
||||||
|
scanner = FakeScanner()
|
||||||
|
cancel = manager.async_register_scanner(scanner, True)
|
||||||
|
assert manager.async_discovered_devices(True) == [
|
||||||
|
switchbot_proxy_device_no_connection_slot
|
||||||
|
]
|
||||||
|
|
||||||
|
client = HaBleakClientWrapper(switchbot_proxy_device_no_connection_slot)
|
||||||
|
with patch("bleak.get_platform_client_backend_type"):
|
||||||
|
await client.connect()
|
||||||
|
assert client.is_connected is True
|
||||||
|
client.set_disconnected_callback(lambda client: None)
|
||||||
|
await client.disconnect()
|
||||||
|
cancel()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue