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]]]
|
tuple[AdvertisementDataCallback, dict[str, set[str]]]
|
||||||
] = []
|
] = []
|
||||||
self._all_history: dict[str, BluetoothServiceInfoBleak] = {}
|
self._all_history: dict[str, BluetoothServiceInfoBleak] = {}
|
||||||
self._non_connectable_history: dict[str, BluetoothServiceInfoBleak] = {}
|
|
||||||
self._connectable_history: dict[str, BluetoothServiceInfoBleak] = {}
|
self._connectable_history: dict[str, BluetoothServiceInfoBleak] = {}
|
||||||
self._non_connectable_scanners: list[BaseHaScanner] = []
|
self._non_connectable_scanners: list[BaseHaScanner] = []
|
||||||
self._connectable_scanners: list[BaseHaScanner] = []
|
self._connectable_scanners: list[BaseHaScanner] = []
|
||||||
|
@ -157,9 +156,8 @@ class BluetoothManager:
|
||||||
service_info.as_dict()
|
service_info.as_dict()
|
||||||
for service_info in self._connectable_history.values()
|
for service_info in self._connectable_history.values()
|
||||||
],
|
],
|
||||||
"non_connectable_history": [
|
"all_history": [
|
||||||
service_info.as_dict()
|
service_info.as_dict() for service_info in self._all_history.values()
|
||||||
for service_info in self._non_connectable_history.values()
|
|
||||||
],
|
],
|
||||||
"advertisement_tracker": self._advertisement_tracker.async_diagnostics(),
|
"advertisement_tracker": self._advertisement_tracker.async_diagnostics(),
|
||||||
}
|
}
|
||||||
|
@ -256,7 +254,6 @@ class BluetoothManager:
|
||||||
"""Watch for unavailable devices and cleanup state history."""
|
"""Watch for unavailable devices and cleanup state history."""
|
||||||
monotonic_now = MONOTONIC_TIME()
|
monotonic_now = MONOTONIC_TIME()
|
||||||
connectable_history = self._connectable_history
|
connectable_history = self._connectable_history
|
||||||
non_connectable_history = self._non_connectable_history
|
|
||||||
all_history = self._all_history
|
all_history = self._all_history
|
||||||
tracker = self._advertisement_tracker
|
tracker = self._advertisement_tracker
|
||||||
intervals = tracker.intervals
|
intervals = tracker.intervals
|
||||||
|
@ -280,8 +277,6 @@ class BluetoothManager:
|
||||||
if time_since_seen <= advertising_interval:
|
if time_since_seen <= advertising_interval:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
non_connectable_history.pop(address, None)
|
|
||||||
|
|
||||||
# The second loop (connectable=False) is responsible for removing
|
# The second loop (connectable=False) is responsible for removing
|
||||||
# the device from all the interval tracking since it is no longer
|
# the device from all the interval tracking since it is no longer
|
||||||
# available for both connectable and non-connectable
|
# available for both connectable and non-connectable
|
||||||
|
@ -363,8 +358,24 @@ class BluetoothManager:
|
||||||
device = service_info.device
|
device = service_info.device
|
||||||
address = device.address
|
address = device.address
|
||||||
all_history = self._all_history
|
all_history = self._all_history
|
||||||
|
connectable = service_info.connectable
|
||||||
|
connectable_history = self._connectable_history
|
||||||
|
|
||||||
source = service_info.source
|
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 (
|
if (
|
||||||
(old_service_info := all_history.get(address))
|
(old_service_info := all_history.get(address))
|
||||||
and source != old_service_info.source
|
and source != old_service_info.source
|
||||||
|
@ -372,12 +383,35 @@ class BluetoothManager:
|
||||||
old_service_info, service_info
|
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
|
return
|
||||||
|
|
||||||
if connectable := service_info.connectable:
|
if connectable:
|
||||||
self._connectable_history[address] = service_info
|
connectable_history[address] = service_info
|
||||||
else:
|
|
||||||
self._non_connectable_history[address] = service_info
|
|
||||||
all_history[address] = service_info
|
all_history[address] = service_info
|
||||||
|
|
||||||
# Track advertisement intervals to determine when we need to
|
# Track advertisement intervals to determine when we need to
|
||||||
|
|
|
@ -116,7 +116,7 @@ async def test_diagnostics(
|
||||||
"timings": {},
|
"timings": {},
|
||||||
},
|
},
|
||||||
"connectable_history": [],
|
"connectable_history": [],
|
||||||
"non_connectable_history": [],
|
"all_history": [],
|
||||||
"scanners": [
|
"scanners": [
|
||||||
{
|
{
|
||||||
"adapter": "hci0",
|
"adapter": "hci0",
|
||||||
|
@ -239,7 +239,30 @@ async def test_diagnostics_macos(
|
||||||
"time": ANY,
|
"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": [
|
"scanners": [
|
||||||
{
|
{
|
||||||
"adapter": "Core Bluetooth",
|
"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)
|
bluetooth.async_ble_device_from_address(hass, address, True)
|
||||||
is switchbot_device_poor_signal
|
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