Fix connectable Bluetooth devices not going available after scanner recovers (#84172)

This commit is contained in:
J. Nick Koston 2022-12-19 02:37:29 -10:00 committed by GitHub
parent 1ab6352a87
commit bb3feceb57
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 207 additions and 14 deletions

View File

@ -369,6 +369,7 @@ class BluetoothManager:
all_history = self._all_history all_history = self._all_history
connectable = service_info.connectable connectable = service_info.connectable
connectable_history = self._connectable_history connectable_history = self._connectable_history
old_connectable_service_info = connectable and connectable_history.get(address)
source = service_info.source source = service_info.source
debug = _LOGGER.isEnabledFor(logging.DEBUG) debug = _LOGGER.isEnabledFor(logging.DEBUG)
@ -399,7 +400,6 @@ class BluetoothManager:
# but not in the connectable history or the connectable source is the same # 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 # as the new source, we need to add it to the connectable history
if connectable: if connectable:
old_connectable_service_info = connectable_history.get(address)
if old_connectable_service_info and ( if old_connectable_service_info and (
# If its the same as the preferred source, we are done # If its the same as the preferred source, we are done
# as we know we prefer the old advertisement # as we know we prefer the old advertisement
@ -442,17 +442,24 @@ class BluetoothManager:
tracker.async_collect(service_info) tracker.async_collect(service_info)
# If the advertisement data is the same as the last time we saw it, we # If the advertisement data is the same as the last time we saw it, we
# don't need to do anything else. # don't need to do anything else unless its connectable and we are missing
if old_service_info and not ( # connectable history for the device so we can make it available again
# after unavailable callbacks.
if (
# Ensure its not a connectable device missing from connectable history
not (connectable and not old_connectable_service_info)
# Than check if advertisement data is the same
and old_service_info
and not (
service_info.manufacturer_data != old_service_info.manufacturer_data service_info.manufacturer_data != old_service_info.manufacturer_data
or service_info.service_data != old_service_info.service_data or service_info.service_data != old_service_info.service_data
or service_info.service_uuids != old_service_info.service_uuids or service_info.service_uuids != old_service_info.service_uuids
or service_info.name != old_service_info.name or service_info.name != old_service_info.name
)
): ):
return return
is_connectable_by_any_source = address in self._connectable_history if not connectable and old_connectable_service_info:
if not connectable and is_connectable_by_any_source:
# Since we have a connectable path and our BleakClient will # Since we have a connectable path and our BleakClient will
# route any connection attempts to the connectable path, we # route any connection attempts to the connectable path, we
# mark the service_info as connectable so that the callbacks # mark the service_info as connectable so that the callbacks
@ -481,7 +488,7 @@ class BluetoothManager:
matched_domains, matched_domains,
) )
if is_connectable_by_any_source: if connectable or old_connectable_service_info:
# Bleak callbacks must get a connectable device # Bleak callbacks must get a connectable device
for callback_filters in self._bleak_callbacks: for callback_filters in self._bleak_callbacks:
_dispatch_bleak_callback(*callback_filters, device, advertisement_data) _dispatch_bleak_callback(*callback_filters, device, advertisement_data)

View File

@ -1,30 +1,47 @@
"""Tests for the Bluetooth integration manager.""" """Tests for the Bluetooth integration manager."""
from datetime import timedelta
import time import time
from unittest.mock import patch from unittest.mock import patch
from bleak.backends.scanner import BLEDevice from bleak.backends.scanner import AdvertisementData, BLEDevice
from bluetooth_adapters import AdvertisementHistory from bluetooth_adapters import AdvertisementHistory
import pytest import pytest
from homeassistant.components import bluetooth from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import storage from homeassistant.components.bluetooth import (
BaseHaRemoteScanner,
BluetoothChange,
BluetoothScanningMode,
BluetoothServiceInfo,
BluetoothServiceInfoBleak,
HaBluetoothConnector,
async_ble_device_from_address,
async_get_advertisement_callback,
async_scanner_count,
async_track_unavailable,
storage,
)
from homeassistant.components.bluetooth.const import UNAVAILABLE_TRACK_SECONDS
from homeassistant.components.bluetooth.manager import ( from homeassistant.components.bluetooth.manager import (
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.json import json_loads from homeassistant.helpers.json import json_loads
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
from . import ( from . import (
FakeScanner, FakeScanner,
MockBleakClient,
_get_manager,
generate_advertisement_data, generate_advertisement_data,
inject_advertisement_with_source, inject_advertisement_with_source,
inject_advertisement_with_time_and_source, inject_advertisement_with_time_and_source,
inject_advertisement_with_time_and_source_connectable, inject_advertisement_with_time_and_source_connectable,
) )
from tests.common import load_fixture from tests.common import async_fire_time_changed, load_fixture
@pytest.fixture @pytest.fixture
@ -588,3 +605,172 @@ async def test_switching_adapters_when_one_stop_scanning(
) )
cancel_hci2() cancel_hci2()
async def test_goes_unavailable_connectable_only_and_recovers(
hass, mock_bluetooth_adapters
):
"""Test all connectable scanners go unavailable, and than recover when there is a non-connectable scanner."""
assert await async_setup_component(hass, bluetooth.DOMAIN, {})
await hass.async_block_till_done()
assert async_scanner_count(hass, connectable=True) == 0
assert async_scanner_count(hass, connectable=False) == 0
switchbot_device_connectable = BLEDevice(
"44:44:33:11:23:45",
"wohand",
{},
rssi=-100,
)
switchbot_device_non_connectable = BLEDevice(
"44:44:33:11:23:45",
"wohand",
{},
rssi=-100,
)
switchbot_device_adv = generate_advertisement_data(
local_name="wohand",
service_uuids=["050a021a-0000-1000-8000-00805f9b34fb"],
service_data={"050a021a-0000-1000-8000-00805f9b34fb": b"\n\xff"},
manufacturer_data={1: b"\x01"},
rssi=-100,
)
callbacks = []
def _fake_subscriber(
service_info: BluetoothServiceInfo,
change: BluetoothChange,
) -> None:
"""Fake subscriber for the BleakScanner."""
callbacks.append((service_info, change))
cancel = bluetooth.async_register_callback(
hass,
_fake_subscriber,
{"address": "44:44:33:11:23:45", "connectable": True},
BluetoothScanningMode.ACTIVE,
)
class FakeScanner(BaseHaRemoteScanner):
def inject_advertisement(
self, device: BLEDevice, advertisement_data: AdvertisementData
) -> None:
"""Inject an advertisement."""
self._async_on_advertisement(
device.address,
advertisement_data.rssi,
device.name,
advertisement_data.service_uuids,
advertisement_data.service_data,
advertisement_data.manufacturer_data,
advertisement_data.tx_power,
{"scanner_specific_data": "test"},
)
new_info_callback = async_get_advertisement_callback(hass)
connector = (
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
)
connectable_scanner = FakeScanner(
hass,
"connectable",
"connectable",
new_info_callback,
connector,
True,
)
unsetup_connectable_scanner = connectable_scanner.async_setup()
cancel_connectable_scanner = _get_manager().async_register_scanner(
connectable_scanner, True
)
connectable_scanner.inject_advertisement(
switchbot_device_connectable, switchbot_device_adv
)
assert async_ble_device_from_address(hass, "44:44:33:11:23:45") is not None
assert async_scanner_count(hass, connectable=True) == 1
assert len(callbacks) == 1
assert (
"44:44:33:11:23:45"
in connectable_scanner.discovered_devices_and_advertisement_data
)
not_connectable_scanner = FakeScanner(
hass,
"not_connectable",
"not_connectable",
new_info_callback,
connector,
False,
)
unsetup_not_connectable_scanner = not_connectable_scanner.async_setup()
cancel_not_connectable_scanner = _get_manager().async_register_scanner(
not_connectable_scanner, False
)
not_connectable_scanner.inject_advertisement(
switchbot_device_non_connectable, switchbot_device_adv
)
assert async_scanner_count(hass, connectable=True) == 1
assert async_scanner_count(hass, connectable=False) == 2
assert (
"44:44:33:11:23:45"
in not_connectable_scanner.discovered_devices_and_advertisement_data
)
unavailable_callbacks: list[BluetoothServiceInfoBleak] = []
@callback
def _unavailable_callback(service_info: BluetoothServiceInfoBleak) -> None:
"""Wrong device unavailable callback."""
nonlocal unavailable_callbacks
unavailable_callbacks.append(service_info.address)
cancel_unavailable = async_track_unavailable(
hass,
_unavailable_callback,
switchbot_device_connectable.address,
connectable=True,
)
assert async_scanner_count(hass, connectable=True) == 1
cancel_connectable_scanner()
unsetup_connectable_scanner()
assert async_scanner_count(hass, connectable=True) == 0
assert async_scanner_count(hass, connectable=False) == 1
async_fire_time_changed(
hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS)
)
await hass.async_block_till_done()
assert "44:44:33:11:23:45" in unavailable_callbacks
cancel_unavailable()
connectable_scanner_2 = FakeScanner(
hass,
"connectable",
"connectable",
new_info_callback,
connector,
True,
)
unsetup_connectable_scanner_2 = connectable_scanner_2.async_setup()
cancel_connectable_scanner_2 = _get_manager().async_register_scanner(
connectable_scanner, True
)
connectable_scanner_2.inject_advertisement(
switchbot_device_connectable, switchbot_device_adv
)
assert (
"44:44:33:11:23:45"
in connectable_scanner_2.discovered_devices_and_advertisement_data
)
# We should get another callback to make the device available again
assert len(callbacks) == 2
cancel()
cancel_connectable_scanner_2()
unsetup_connectable_scanner_2()
cancel_not_connectable_scanner()
unsetup_not_connectable_scanner()