Fix connectable Bluetooth devices not going available after scanner recovers (#84172)
This commit is contained in:
parent
756070cd81
commit
3bdf80574d
2 changed files with 209 additions and 13 deletions
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Add table
Reference in a new issue