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

This commit is contained in:
J. Nick Koston 2022-12-19 02:37:29 -10:00 committed by Paulus Schoutsen
parent 756070cd81
commit 3bdf80574d
2 changed files with 209 additions and 13 deletions
homeassistant/components/bluetooth
tests/components/bluetooth

View file

@ -369,6 +369,7 @@ class BluetoothManager:
all_history = self._all_history
connectable = service_info.connectable
connectable_history = self._connectable_history
old_connectable_service_info = connectable and connectable_history.get(address)
source = service_info.source
debug = _LOGGER.isEnabledFor(logging.DEBUG)
@ -399,7 +400,6 @@ class BluetoothManager:
# 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
@ -442,17 +442,24 @@ class BluetoothManager:
tracker.async_collect(service_info)
# If the advertisement data is the same as the last time we saw it, we
# don't need to do anything else.
if old_service_info and not (
service_info.manufacturer_data != old_service_info.manufacturer_data
or service_info.service_data != old_service_info.service_data
or service_info.service_uuids != old_service_info.service_uuids
or service_info.name != old_service_info.name
# don't need to do anything else unless its connectable and we are missing
# 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
or service_info.service_data != old_service_info.service_data
or service_info.service_uuids != old_service_info.service_uuids
or service_info.name != old_service_info.name
)
):
return
is_connectable_by_any_source = address in self._connectable_history
if not connectable and is_connectable_by_any_source:
if not connectable and old_connectable_service_info:
# 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
@ -481,7 +488,7 @@ class BluetoothManager:
matched_domains,
)
if is_connectable_by_any_source:
if connectable or old_connectable_service_info:
# Bleak callbacks must get a connectable device
for callback_filters in self._bleak_callbacks:
_dispatch_bleak_callback(*callback_filters, device, advertisement_data)

View file

@ -1,27 +1,47 @@
"""Tests for the Bluetooth integration manager."""
from datetime import timedelta
import time
from unittest.mock import patch
from bleak.backends.scanner import BLEDevice
from bleak.backends.scanner import AdvertisementData, BLEDevice
from bluetooth_adapters import AdvertisementHistory
import pytest
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import BaseHaScanner
from homeassistant.components.bluetooth import (
BaseHaScanner,
BaseHaRemoteScanner,
BluetoothChange,
BluetoothScanningMode,
BluetoothServiceInfo,
BluetoothServiceInfoBleak,
HaBluetoothConnector,
async_ble_device_from_address,
async_get_advertisement_callback,
async_scanner_count,
async_track_unavailable,
)
from homeassistant.components.bluetooth.const import UNAVAILABLE_TRACK_SECONDS
from homeassistant.components.bluetooth.manager import (
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.setup import async_setup_component
from homeassistant.util import dt as dt_util
from . import (
MockBleakClient,
_get_manager,
generate_advertisement_data,
inject_advertisement_with_source,
inject_advertisement_with_time_and_source,
inject_advertisement_with_time_and_source_connectable,
)
from tests.common import async_fire_time_changed, load_fixture
@pytest.fixture
def register_hci0_scanner(hass: HomeAssistant) -> None:
@ -514,3 +534,172 @@ async def test_switching_adapters_when_one_stop_scanning(
)
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()