Bump bleak to 0.19.0 (#80349)

This commit is contained in:
J. Nick Koston 2022-10-15 07:57:23 -10:00 committed by GitHub
parent 3460e0b074
commit d12cbab6c4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 638 additions and 390 deletions

View file

@ -3,6 +3,7 @@ from __future__ import annotations
import asyncio
from collections.abc import Callable, Iterable
from dataclasses import replace
from datetime import datetime, timedelta
import itertools
import logging
@ -121,7 +122,8 @@ class BluetoothManager:
self._bleak_callbacks: list[
tuple[AdvertisementDataCallback, dict[str, set[str]]]
] = []
self._history: dict[str, BluetoothServiceInfoBleak] = {}
self._all_history: dict[str, BluetoothServiceInfoBleak] = {}
self._non_connectable_history: dict[str, BluetoothServiceInfoBleak] = {}
self._connectable_history: dict[str, BluetoothServiceInfoBleak] = {}
self._non_connectable_scanners: list[BaseHaScanner] = []
self._connectable_scanners: list[BaseHaScanner] = []
@ -155,8 +157,9 @@ class BluetoothManager:
service_info.as_dict()
for service_info in self._connectable_history.values()
],
"history": [
service_info.as_dict() for service_info in self._history.values()
"non_connectable_history": [
service_info.as_dict()
for service_info in self._non_connectable_history.values()
],
"advertisement_tracker": self._advertisement_tracker.async_diagnostics(),
}
@ -189,7 +192,7 @@ class BluetoothManager:
# Everything is connectable so it fall into both
# buckets since the host system can only provide
# connectable devices
self._history = history.copy()
self._all_history = history.copy()
self._connectable_history = history.copy()
self.async_setup_unavailable_tracking()
@ -202,32 +205,32 @@ class BluetoothManager:
self._cancel_unavailable_tracking = None
uninstall_multiple_bleak_catcher()
async def async_get_devices_by_address(
@hass_callback
def async_get_discovered_devices_and_advertisement_data_by_address(
self, address: str, connectable: bool
) -> list[BLEDevice]:
"""Get devices by address."""
) -> list[tuple[BLEDevice, AdvertisementData]]:
"""Get devices and advertisement_data 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_)
)
device_advertisement_data
for device_advertisement_data in (
scanner.discovered_devices_and_advertisement_data.get(address)
for type_ in types_
for scanner in self._get_scanners_by_type(type_)
)
if device is not None
if device_advertisement_data is not None
]
@hass_callback
def async_all_discovered_devices(self, connectable: bool) -> Iterable[BLEDevice]:
"""Return all of discovered devices from all the scanners including duplicates."""
def _async_all_discovered_addresses(self, connectable: bool) -> Iterable[str]:
"""Return all of discovered addresses from all the scanners including duplicates."""
yield from itertools.chain.from_iterable(
scanner.discovered_devices for scanner in self._get_scanners_by_type(True)
scanner.discovered_devices_and_advertisement_data
for scanner in self._get_scanners_by_type(True)
)
if not connectable:
yield from itertools.chain.from_iterable(
scanner.discovered_devices
scanner.discovered_devices_and_advertisement_data
for scanner in self._get_scanners_by_type(False)
)
@ -253,33 +256,38 @@ class BluetoothManager:
"""Watch for unavailable devices and cleanup state history."""
monotonic_now = MONOTONIC_TIME()
connectable_history = self._connectable_history
all_history = self._history
removed_addresses: set[str] = set()
non_connectable_history = self._non_connectable_history
all_history = self._all_history
tracker = self._advertisement_tracker
intervals = tracker.intervals
for connectable in (True, False):
unavailable_callbacks = self._get_unavailable_callbacks_by_type(connectable)
intervals = self._advertisement_tracker.intervals
history = connectable_history if connectable else all_history
history_set = set(history)
active_addresses = {
device.address
for device in self.async_all_discovered_devices(connectable)
}
disappeared = history_set.difference(active_addresses)
disappeared = set(history).difference(
self._async_all_discovered_addresses(connectable)
)
for address in disappeared:
#
# For non-connectable devices we also check the device has exceeded
# the advertising interval before we mark it as unavailable
# since it may have gone to sleep and since we do not need an active connection
# to it we can only determine its availability by the lack of advertisements
#
if not connectable and (advertising_interval := intervals.get(address)):
time_since_seen = monotonic_now - history[address].time
if time_since_seen <= advertising_interval:
continue
if not connectable:
#
# For non-connectable devices we also check the device has exceeded
# the advertising interval before we mark it as unavailable
# since it may have gone to sleep and since we do not need an active connection
# to it we can only determine its availability by the lack of advertisements
#
if advertising_interval := intervals.get(address):
time_since_seen = monotonic_now - all_history[address].time
if time_since_seen <= advertising_interval:
continue
non_connectable_history.pop(address, None)
# The second loop (connectable=False) is responsible for removing
# the device from all the interval tracking since it is no longer
# available for both connectable and non-connectable
tracker.async_remove_address(address)
service_info = history.pop(address)
removed_addresses.add(address)
if not (callbacks := unavailable_callbacks.get(address)):
continue
@ -290,14 +298,10 @@ class BluetoothManager:
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Error in unavailable callback")
# If we removed the device from both the connectable history
# and all history then we can remove it from the advertisement tracker
for address in removed_addresses:
if address not in connectable_history and address not in all_history:
self._advertisement_tracker.async_remove_address(address)
def _prefer_previous_adv_from_different_source(
self, old: BluetoothServiceInfoBleak, new: BluetoothServiceInfoBleak
self,
old: BluetoothServiceInfoBleak,
new: BluetoothServiceInfoBleak,
) -> bool:
"""Prefer previous advertisement from a different source if it is better."""
if new.time - old.time > (
@ -308,8 +312,8 @@ class BluetoothManager:
# If the old advertisement is stale, any new advertisement is preferred
_LOGGER.debug(
"%s (%s): Switching from %s[%s] to %s[%s] (time elapsed:%s > stale seconds:%s)",
new.advertisement.local_name,
new.device.address,
new.name,
new.address,
old.source,
old.connectable,
new.source,
@ -318,19 +322,21 @@ class BluetoothManager:
stale_seconds,
)
return False
if new.device.rssi - RSSI_SWITCH_THRESHOLD > (old.device.rssi or NO_RSSI_VALUE):
if (new.rssi or NO_RSSI_VALUE) - RSSI_SWITCH_THRESHOLD > (
old.rssi or NO_RSSI_VALUE
):
# If new advertisement is RSSI_SWITCH_THRESHOLD more, the new one is preferred
_LOGGER.debug(
"%s (%s): Switching from %s[%s] to %s[%s] (new rssi:%s - threshold:%s > old rssi:%s)",
new.advertisement.local_name,
new.device.address,
new.name,
new.address,
old.source,
old.connectable,
new.source,
new.connectable,
new.device.rssi,
new.rssi,
RSSI_SWITCH_THRESHOLD,
old.device.rssi,
old.rssi,
)
return False
return True
@ -355,9 +361,9 @@ class BluetoothManager:
return
device = service_info.device
connectable = service_info.connectable
address = device.address
all_history = self._connectable_history if connectable else self._history
all_history = self._all_history
source = service_info.source
if (
(old_service_info := all_history.get(address))
@ -368,11 +374,11 @@ class BluetoothManager:
):
return
self._history[address] = service_info
if connectable:
if connectable := service_info.connectable:
self._connectable_history[address] = service_info
# Bleak callbacks must get a connectable device
else:
self._non_connectable_history[address] = service_info
all_history[address] = service_info
# Track advertisement intervals to determine when we need to
# switch adapters or mark a device as unavailable
@ -393,11 +399,18 @@ class BluetoothManager:
):
return
if connectable:
if is_connectable_by_any_source := address in self._connectable_history:
# Bleak callbacks must get a connectable device
for callback_filters in self._bleak_callbacks:
_dispatch_bleak_callback(*callback_filters, device, advertisement_data)
if not connectable and is_connectable_by_any_source:
# Since we have a connectable path and our BleakClient will
# route any connection attempts to the connectable path, we
# mark the service_info as connectable so that the callbacks
# will be called and the device can be discovered.
service_info = replace(service_info, connectable=True)
matched_domains = self._integration_matcher.match_domains(service_info)
_LOGGER.debug(
"%s: %s %s connectable: %s match: %s rssi: %s",
@ -406,7 +419,7 @@ class BluetoothManager:
advertisement_data,
connectable,
matched_domains,
device.rssi,
advertisement_data.rssi,
)
for match in self._callback_index.match_callbacks(service_info):
@ -518,27 +531,23 @@ class BluetoothManager:
def _get_scanners_by_type(self, connectable: bool) -> list[BaseHaScanner]:
"""Return the scanners by type."""
return (
self._connectable_scanners
if connectable
else self._non_connectable_scanners
)
if connectable:
return self._connectable_scanners
return self._non_connectable_scanners
def _get_unavailable_callbacks_by_type(
self, connectable: bool
) -> dict[str, list[Callable[[BluetoothServiceInfoBleak], None]]]:
"""Return the unavailable callbacks by type."""
return (
self._connectable_unavailable_callbacks
if connectable
else self._unavailable_callbacks
)
if connectable:
return self._connectable_unavailable_callbacks
return self._unavailable_callbacks
def _get_history_by_type(
self, connectable: bool
) -> dict[str, BluetoothServiceInfoBleak]:
"""Return the history by type."""
return self._connectable_history if connectable else self._history
return self._connectable_history if connectable else self._all_history
def async_register_scanner(
self, scanner: BaseHaScanner, connectable: bool