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:
parent
d38d21ab3a
commit
f70f972d88
3 changed files with 118 additions and 13 deletions
|
@ -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,8 +358,24 @@ 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
|
||||
|
@ -372,12 +383,35 @@ class BluetoothManager:
|
|||
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
|
||||
|
||||
connectable_history[address] = service_info
|
||||
|
||||
return
|
||||
|
||||
if connectable := service_info.connectable:
|
||||
self._connectable_history[address] = service_info
|
||||
else:
|
||||
self._non_connectable_history[address] = service_info
|
||||
if connectable:
|
||||
connectable_history[address] = service_info
|
||||
|
||||
all_history[address] = service_info
|
||||
|
||||
# Track advertisement intervals to determine when we need to
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue