mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
Fix connectable Bluetooth devices not going available after scanner recovers (#84172)
This commit is contained in:
parent
1ab6352a87
commit
bb3feceb57
@ -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)
|
||||||
|
@ -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()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user