Fix connectable Bluetooth devices not being seen if the nearest scanner is non-connectable (#80388)

If we saw the non-connectable scanner advertisement first we would reject
the connectable scanner advertisement because it had worse signal strength.

In this case we need to check both
This commit is contained in:
J. Nick Koston 2022-10-17 20:13:26 -05:00 committed by GitHub
parent d38d21ab3a
commit f70f972d88
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 118 additions and 13 deletions

View file

@ -123,7 +123,6 @@ class BluetoothManager:
tuple[AdvertisementDataCallback, dict[str, set[str]]]
] = []
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] = []
@ -157,9 +156,8 @@ class BluetoothManager:
service_info.as_dict()
for service_info in self._connectable_history.values()
],
"non_connectable_history": [
service_info.as_dict()
for service_info in self._non_connectable_history.values()
"all_history": [
service_info.as_dict() for service_info in self._all_history.values()
],
"advertisement_tracker": self._advertisement_tracker.async_diagnostics(),
}
@ -256,7 +254,6 @@ class BluetoothManager:
"""Watch for unavailable devices and cleanup state history."""
monotonic_now = MONOTONIC_TIME()
connectable_history = self._connectable_history
non_connectable_history = self._non_connectable_history
all_history = self._all_history
tracker = self._advertisement_tracker
intervals = tracker.intervals
@ -280,8 +277,6 @@ class BluetoothManager:
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
@ -363,21 +358,60 @@ class BluetoothManager:
device = service_info.device
address = device.address
all_history = self._all_history
connectable = service_info.connectable
connectable_history = self._connectable_history
source = service_info.source
# This logic is complex due to the many combinations of scanners that are supported.
#
# We need to handle multiple connectable and non-connectable scanners
# and we need to handle the case where a device is connectable on one scanner
# but not on another.
#
# The device may also be connectable only by a scanner that has worse signal strength
# than a non-connectable scanner.
#
# all_history - the history of all advertisements from all scanners with the best
# advertisement from each scanner
# connectable_history - the history of all connectable advertisements from all scanners
# with the best advertisement from each connectable scanner
#
if (
(old_service_info := all_history.get(address))
and source != old_service_info.source
and self._prefer_previous_adv_from_different_source(
old_service_info, service_info
)
):
# If we are rejecting the new advertisement and the device is connectable
# but not in the connectable history or the connectable source is the same
# as the new source, we need to add it to the connectable history
if connectable:
old_connectable_service_info = connectable_history.get(address)
if old_connectable_service_info and (
# If its the same as the preferred source, we are done
# as we know we prefer the old advertisement
# from the check above
(old_connectable_service_info is old_service_info)
# If the old connectable source is different from the preferred
# source, we need to check it as well to see if we prefer
# the old connectable advertisement
or (
source != old_connectable_service_info.source
and self._prefer_previous_adv_from_different_source(
old_connectable_service_info, service_info
)
)
):
return
if connectable := service_info.connectable:
self._connectable_history[address] = service_info
else:
self._non_connectable_history[address] = service_info
connectable_history[address] = service_info
return
if connectable:
connectable_history[address] = service_info
all_history[address] = service_info
# Track advertisement intervals to determine when we need to

View file

@ -116,7 +116,7 @@ async def test_diagnostics(
"timings": {},
},
"connectable_history": [],
"non_connectable_history": [],
"all_history": [],
"scanners": [
{
"adapter": "hci0",
@ -239,7 +239,30 @@ async def test_diagnostics_macos(
"time": ANY,
}
],
"non_connectable_history": [],
"all_history": [
{
"address": "44:44:33:11:23:45",
"advertisement": [
"wohand",
{"1": {"__type": "<class " "'bytes'>", "repr": "b'\\x01'"}},
{},
[],
-127,
-127,
[[]],
],
"connectable": True,
"manufacturer_data": {
"1": {"__type": "<class " "'bytes'>", "repr": "b'\\x01'"}
},
"name": "wohand",
"rssi": -127,
"service_data": {},
"service_uuids": [],
"source": "local",
"time": ANY,
}
],
"scanners": [
{
"adapter": "Core Bluetooth",

View file

@ -336,3 +336,51 @@ async def test_switching_adapters_based_on_rssi_connectable_to_non_connectable(
bluetooth.async_ble_device_from_address(hass, address, True)
is switchbot_device_poor_signal
)
async def test_connectable_advertisement_can_be_retrieved_with_best_path_is_non_connectable(
hass, enable_bluetooth
):
"""Test we can still get a connectable BLEDevice when the best path is non-connectable.
In this case the the device is closer to a non-connectable scanner, but the
at least one connectable scanner has the device in range.
"""
address = "44:44:33:11:23:45"
now = time.monotonic()
switchbot_device_good_signal = BLEDevice(address, "wohand_good_signal")
switchbot_adv_good_signal = generate_advertisement_data(
local_name="wohand_good_signal", service_uuids=[], rssi=-60
)
inject_advertisement_with_time_and_source_connectable(
hass,
switchbot_device_good_signal,
switchbot_adv_good_signal,
now,
"hci1",
False,
)
assert (
bluetooth.async_ble_device_from_address(hass, address, False)
is switchbot_device_good_signal
)
assert bluetooth.async_ble_device_from_address(hass, address, True) is None
switchbot_device_poor_signal = BLEDevice(address, "wohand_poor_signal")
switchbot_adv_poor_signal = generate_advertisement_data(
local_name="wohand_poor_signal", service_uuids=[], rssi=-100
)
inject_advertisement_with_time_and_source_connectable(
hass, switchbot_device_poor_signal, switchbot_adv_poor_signal, now, "hci0", True
)
assert (
bluetooth.async_ble_device_from_address(hass, address, False)
is switchbot_device_good_signal
)
assert (
bluetooth.async_ble_device_from_address(hass, address, True)
is switchbot_device_poor_signal
)