Restore history from bluetooth stack at startup (#78612)

This commit is contained in:
J. Nick Koston 2022-09-17 16:58:19 -05:00 committed by GitHub
parent 13d3f4c3b2
commit 18eef5da1f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 151 additions and 55 deletions

View file

@ -228,7 +228,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
integration_matcher = IntegrationMatcher(await async_get_bluetooth(hass)) integration_matcher = IntegrationMatcher(await async_get_bluetooth(hass))
integration_matcher.async_setup() integration_matcher.async_setup()
manager = BluetoothManager(hass, integration_matcher) manager = BluetoothManager(hass, integration_matcher)
manager.async_setup() await manager.async_setup()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, manager.async_stop) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, manager.async_stop)
hass.data[DATA_MANAGER] = models.MANAGER = manager hass.data[DATA_MANAGER] = models.MANAGER = manager
adapters = await manager.async_get_bluetooth_adapters() adapters = await manager.async_get_bluetooth_adapters()

View file

@ -45,7 +45,7 @@ from .models import (
BluetoothServiceInfoBleak, BluetoothServiceInfoBleak,
) )
from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher
from .util import async_get_bluetooth_adapters from .util import async_get_bluetooth_adapters, async_load_history_from_system
if TYPE_CHECKING: if TYPE_CHECKING:
from bleak.backends.device import BLEDevice from bleak.backends.device import BLEDevice
@ -213,10 +213,15 @@ class BluetoothManager:
self._adapters = await async_get_bluetooth_adapters() self._adapters = await async_get_bluetooth_adapters()
return self._find_adapter_by_address(address) return self._find_adapter_by_address(address)
@hass_callback async def async_setup(self) -> None:
def async_setup(self) -> None:
"""Set up the bluetooth manager.""" """Set up the bluetooth manager."""
install_multiple_bleak_catcher() install_multiple_bleak_catcher()
history = await async_load_history_from_system()
# Everything is connectable so it fall into both
# buckets since the host system can only provide
# connectable devices
self._history = history.copy()
self._connectable_history = history.copy()
self.async_setup_unavailable_tracking() self.async_setup_unavailable_tracking()
@hass_callback @hass_callback

View file

@ -7,7 +7,7 @@
"requirements": [ "requirements": [
"bleak==0.17.0", "bleak==0.17.0",
"bleak-retry-connector==1.17.1", "bleak-retry-connector==1.17.1",
"bluetooth-adapters==0.4.1", "bluetooth-adapters==0.5.1",
"bluetooth-auto-recovery==0.3.3", "bluetooth-auto-recovery==0.3.3",
"dbus-fast==1.4.0" "dbus-fast==1.4.0"
], ],

View file

@ -19,13 +19,7 @@ from bleak.backends.device import BLEDevice
from bleak.backends.scanner import AdvertisementData from bleak.backends.scanner import AdvertisementData
from dbus_fast import InvalidMessageError from dbus_fast import InvalidMessageError
from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback
from homeassistant.core import (
CALLBACK_TYPE,
Event,
HomeAssistant,
callback as hass_callback,
)
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.event import async_track_time_interval
from homeassistant.util.package import is_docker_env from homeassistant.util.package import is_docker_env
@ -133,7 +127,6 @@ class HaScanner(BaseHaScanner):
self.scanner = scanner self.scanner = scanner
self.adapter = adapter self.adapter = adapter
self._start_stop_lock = asyncio.Lock() self._start_stop_lock = asyncio.Lock()
self._cancel_stop: CALLBACK_TYPE | None = None
self._cancel_watchdog: CALLBACK_TYPE | None = None self._cancel_watchdog: CALLBACK_TYPE | None = None
self._last_detection = 0.0 self._last_detection = 0.0
self._start_time = 0.0 self._start_time = 0.0
@ -318,9 +311,6 @@ class HaScanner(BaseHaScanner):
break break
self._async_setup_scanner_watchdog() self._async_setup_scanner_watchdog()
self._cancel_stop = self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, self._async_hass_stopping
)
@hass_callback @hass_callback
def _async_setup_scanner_watchdog(self) -> None: def _async_setup_scanner_watchdog(self) -> None:
@ -368,11 +358,6 @@ class HaScanner(BaseHaScanner):
exc_info=True, exc_info=True,
) )
async def _async_hass_stopping(self, event: Event) -> None:
"""Stop the Bluetooth integration at shutdown."""
self._cancel_stop = None
await self.async_stop()
async def _async_reset_adapter(self) -> None: async def _async_reset_adapter(self) -> None:
"""Reset the adapter.""" """Reset the adapter."""
# There is currently nothing the user can do to fix this # There is currently nothing the user can do to fix this
@ -396,9 +381,6 @@ class HaScanner(BaseHaScanner):
async def _async_stop_scanner(self) -> None: async def _async_stop_scanner(self) -> None:
"""Stop bluetooth discovery under the lock.""" """Stop bluetooth discovery under the lock."""
if self._cancel_stop:
self._cancel_stop()
self._cancel_stop = None
_LOGGER.debug("%s: Stopping bluetooth discovery", self.name) _LOGGER.debug("%s: Stopping bluetooth discovery", self.name)
try: try:
await self.scanner.stop() # type: ignore[no-untyped-call] await self.scanner.stop() # type: ignore[no-untyped-call]

View file

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import platform import platform
import time
from bluetooth_auto_recovery import recover_adapter from bluetooth_auto_recovery import recover_adapter
@ -15,6 +16,38 @@ from .const import (
WINDOWS_DEFAULT_BLUETOOTH_ADAPTER, WINDOWS_DEFAULT_BLUETOOTH_ADAPTER,
AdapterDetails, AdapterDetails,
) )
from .models import BluetoothServiceInfoBleak
async def async_load_history_from_system() -> dict[str, BluetoothServiceInfoBleak]:
"""Load the device and advertisement_data history if available on the current system."""
if platform.system() != "Linux":
return {}
from bluetooth_adapters import ( # pylint: disable=import-outside-toplevel
BlueZDBusObjects,
)
bluez_dbus = BlueZDBusObjects()
await bluez_dbus.load()
now = time.monotonic()
return {
address: BluetoothServiceInfoBleak(
name=history.advertisement_data.local_name
or history.device.name
or history.device.address,
address=history.device.address,
rssi=history.device.rssi,
manufacturer_data=history.advertisement_data.manufacturer_data,
service_data=history.advertisement_data.service_data,
service_uuids=history.advertisement_data.service_uuids,
source=history.source,
device=history.device,
advertisement=history.advertisement_data,
connectable=False,
time=now,
)
for address, history in bluez_dbus.history.items()
}
async def async_get_bluetooth_adapters() -> dict[str, AdapterDetails]: async def async_get_bluetooth_adapters() -> dict[str, AdapterDetails]:

View file

@ -12,7 +12,7 @@ awesomeversion==22.9.0
bcrypt==3.1.7 bcrypt==3.1.7
bleak-retry-connector==1.17.1 bleak-retry-connector==1.17.1
bleak==0.17.0 bleak==0.17.0
bluetooth-adapters==0.4.1 bluetooth-adapters==0.5.1
bluetooth-auto-recovery==0.3.3 bluetooth-auto-recovery==0.3.3
certifi>=2021.5.30 certifi>=2021.5.30
ciso8601==2.2.0 ciso8601==2.2.0

View file

@ -430,7 +430,7 @@ bluemaestro-ble==0.2.0
# bluepy==1.3.0 # bluepy==1.3.0
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bluetooth-adapters==0.4.1 bluetooth-adapters==0.5.1
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bluetooth-auto-recovery==0.3.3 bluetooth-auto-recovery==0.3.3

View file

@ -341,7 +341,7 @@ blinkpy==0.19.2
bluemaestro-ble==0.2.0 bluemaestro-ble==0.2.0
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bluetooth-adapters==0.4.1 bluetooth-adapters==0.5.1
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bluetooth-auto-recovery==0.3.3 bluetooth-auto-recovery==0.3.3

View file

@ -1,10 +1,20 @@
"""Tests for the bluetooth component.""" """Tests for the bluetooth component."""
from unittest.mock import patch from unittest.mock import AsyncMock, MagicMock, patch
import pytest import pytest
@pytest.fixture(name="bluez_dbus_mock")
def bluez_dbus_mock():
"""Fixture that mocks out the bluez dbus calls."""
# Must patch directly since this is loaded on demand only
with patch(
"bluetooth_adapters.BlueZDBusObjects", return_value=MagicMock(load=AsyncMock())
):
yield
@pytest.fixture(name="macos_adapter") @pytest.fixture(name="macos_adapter")
def macos_adapter(): def macos_adapter():
"""Fixture that mocks the macos adapter.""" """Fixture that mocks the macos adapter."""
@ -25,7 +35,7 @@ def windows_adapter():
@pytest.fixture(name="one_adapter") @pytest.fixture(name="one_adapter")
def one_adapter_fixture(): def one_adapter_fixture(bluez_dbus_mock):
"""Fixture that mocks one adapter on Linux.""" """Fixture that mocks one adapter on Linux."""
with patch( with patch(
"homeassistant.components.bluetooth.platform.system", return_value="Linux" "homeassistant.components.bluetooth.platform.system", return_value="Linux"
@ -54,7 +64,7 @@ def one_adapter_fixture():
@pytest.fixture(name="two_adapters") @pytest.fixture(name="two_adapters")
def two_adapters_fixture(): def two_adapters_fixture(bluez_dbus_mock):
"""Fixture that mocks two adapters on Linux.""" """Fixture that mocks two adapters on Linux."""
with patch( with patch(
"homeassistant.components.bluetooth.platform.system", return_value="Linux" "homeassistant.components.bluetooth.platform.system", return_value="Linux"

View file

@ -47,7 +47,9 @@ GENERIC_BLUETOOTH_SERVICE_INFO_2 = BluetoothServiceInfo(
) )
async def test_basic_usage(hass: HomeAssistant, mock_bleak_scanner_start): async def test_basic_usage(
hass: HomeAssistant, mock_bleak_scanner_start, mock_bluetooth_adapters
):
"""Test basic usage of the ActiveBluetoothProcessorCoordinator.""" """Test basic usage of the ActiveBluetoothProcessorCoordinator."""
await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
@ -92,7 +94,9 @@ async def test_basic_usage(hass: HomeAssistant, mock_bleak_scanner_start):
cancel() cancel()
async def test_poll_can_be_skipped(hass: HomeAssistant, mock_bleak_scanner_start): async def test_poll_can_be_skipped(
hass: HomeAssistant, mock_bleak_scanner_start, mock_bluetooth_adapters
):
"""Test need_poll callback works and can skip a poll if its not needed.""" """Test need_poll callback works and can skip a poll if its not needed."""
await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
@ -151,7 +155,7 @@ async def test_poll_can_be_skipped(hass: HomeAssistant, mock_bleak_scanner_start
async def test_bleak_error_and_recover( async def test_bleak_error_and_recover(
hass: HomeAssistant, mock_bleak_scanner_start, caplog hass: HomeAssistant, mock_bleak_scanner_start, mock_bluetooth_adapters, caplog
): ):
"""Test bleak error handling and recovery.""" """Test bleak error handling and recovery."""
await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
@ -212,7 +216,9 @@ async def test_bleak_error_and_recover(
cancel() cancel()
async def test_poll_failure_and_recover(hass: HomeAssistant, mock_bleak_scanner_start): async def test_poll_failure_and_recover(
hass: HomeAssistant, mock_bleak_scanner_start, mock_bluetooth_adapters
):
"""Test error handling and recovery.""" """Test error handling and recovery."""
await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
@ -267,7 +273,9 @@ async def test_poll_failure_and_recover(hass: HomeAssistant, mock_bleak_scanner_
cancel() cancel()
async def test_second_poll_needed(hass: HomeAssistant, mock_bleak_scanner_start): async def test_second_poll_needed(
hass: HomeAssistant, mock_bleak_scanner_start, mock_bluetooth_adapters
):
"""If a poll is queued, by the time it starts it may no longer be needed.""" """If a poll is queued, by the time it starts it may no longer be needed."""
await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
@ -314,7 +322,9 @@ async def test_second_poll_needed(hass: HomeAssistant, mock_bleak_scanner_start)
cancel() cancel()
async def test_rate_limit(hass: HomeAssistant, mock_bleak_scanner_start): async def test_rate_limit(
hass: HomeAssistant, mock_bleak_scanner_start, mock_bluetooth_adapters
):
"""Test error handling and recovery.""" """Test error handling and recovery."""
await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await async_setup_component(hass, DOMAIN, {DOMAIN: {}})

View file

@ -18,7 +18,11 @@ from tests.common import MockConfigEntry
async def test_options_flow_disabled_not_setup( async def test_options_flow_disabled_not_setup(
hass, hass_ws_client, mock_bleak_scanner_start, macos_adapter hass,
hass_ws_client,
mock_bleak_scanner_start,
mock_bluetooth_adapters,
macos_adapter,
): ):
"""Test options are disabled if the integration has not been setup.""" """Test options are disabled if the integration has not been setup."""
await async_setup_component(hass, "config", {}) await async_setup_component(hass, "config", {})
@ -38,6 +42,7 @@ async def test_options_flow_disabled_not_setup(
) )
response = await ws_client.receive_json() response = await ws_client.receive_json()
assert response["result"][0]["supports_options"] is False assert response["result"][0]["supports_options"] is False
await hass.config_entries.async_unload(entry.entry_id)
async def test_async_step_user_macos(hass, macos_adapter): async def test_async_step_user_macos(hass, macos_adapter):
@ -262,7 +267,9 @@ async def test_async_step_integration_discovery_already_exists(hass):
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured"
async def test_options_flow_linux(hass, mock_bleak_scanner_start, one_adapter): async def test_options_flow_linux(
hass, mock_bleak_scanner_start, mock_bluetooth_adapters, one_adapter
):
"""Test options on Linux.""" """Test options on Linux."""
entry = MockConfigEntry( entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
@ -308,10 +315,15 @@ async def test_options_flow_linux(hass, mock_bleak_scanner_start, one_adapter):
assert result["type"] == FlowResultType.CREATE_ENTRY assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["data"][CONF_PASSIVE] is False assert result["data"][CONF_PASSIVE] is False
await hass.config_entries.async_unload(entry.entry_id)
async def test_options_flow_disabled_macos( async def test_options_flow_disabled_macos(
hass, hass_ws_client, mock_bleak_scanner_start, macos_adapter hass,
hass_ws_client,
mock_bleak_scanner_start,
mock_bluetooth_adapters,
macos_adapter,
): ):
"""Test options are disabled on MacOS.""" """Test options are disabled on MacOS."""
await async_setup_component(hass, "config", {}) await async_setup_component(hass, "config", {})
@ -334,10 +346,11 @@ async def test_options_flow_disabled_macos(
) )
response = await ws_client.receive_json() response = await ws_client.receive_json()
assert response["result"][0]["supports_options"] is False assert response["result"][0]["supports_options"] is False
await hass.config_entries.async_unload(entry.entry_id)
async def test_options_flow_enabled_linux( async def test_options_flow_enabled_linux(
hass, hass_ws_client, mock_bleak_scanner_start, one_adapter hass, hass_ws_client, mock_bleak_scanner_start, mock_bluetooth_adapters, one_adapter
): ):
"""Test options are enabled on Linux.""" """Test options are enabled on Linux."""
await async_setup_component(hass, "config", {}) await async_setup_component(hass, "config", {})
@ -363,3 +376,4 @@ async def test_options_flow_enabled_linux(
) )
response = await ws_client.receive_json() response = await ws_client.receive_json()
assert response["result"][0]["supports_options"] is True assert response["result"][0]["supports_options"] is True
await hass.config_entries.async_unload(entry.entry_id)

View file

@ -2446,7 +2446,7 @@ async def test_auto_detect_bluetooth_adapters_linux_multiple(hass, two_adapters)
assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 2 assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 2
async def test_auto_detect_bluetooth_adapters_linux_none_found(hass): async def test_auto_detect_bluetooth_adapters_linux_none_found(hass, bluez_dbus_mock):
"""Test we auto detect bluetooth adapters on linux with no adapters found.""" """Test we auto detect bluetooth adapters on linux with no adapters found."""
with patch( with patch(
"bluetooth_adapters.get_bluetooth_adapter_details", return_value={} "bluetooth_adapters.get_bluetooth_adapter_details", return_value={}

View file

@ -1,10 +1,13 @@
"""Tests for the Bluetooth integration manager.""" """Tests for the Bluetooth integration manager."""
from unittest.mock import AsyncMock, MagicMock, patch
from bleak.backends.scanner import AdvertisementData, BLEDevice from bleak.backends.scanner import AdvertisementData, BLEDevice
from bluetooth_adapters import AdvertisementHistory
from homeassistant.components import bluetooth from homeassistant.components import bluetooth
from homeassistant.components.bluetooth.manager import STALE_ADVERTISEMENT_SECONDS from homeassistant.components.bluetooth.manager import STALE_ADVERTISEMENT_SECONDS
from homeassistant.setup import async_setup_component
from . import ( from . import (
inject_advertisement_with_source, inject_advertisement_with_source,
@ -176,3 +179,24 @@ async def test_switching_adapters_based_on_stale(hass, enable_bluetooth):
bluetooth.async_ble_device_from_address(hass, address) bluetooth.async_ble_device_from_address(hass, address)
is switchbot_device_poor_signal_hci1 is switchbot_device_poor_signal_hci1
) )
async def test_restore_history_from_dbus(hass, one_adapter):
"""Test we can restore history from dbus."""
address = "AA:BB:CC:CC:CC:FF"
ble_device = BLEDevice(address, "name")
history = {
address: AdvertisementHistory(
ble_device, AdvertisementData(local_name="name"), "hci0"
)
}
with patch(
"bluetooth_adapters.BlueZDBusObjects",
return_value=MagicMock(load=AsyncMock(), history=history),
):
assert await async_setup_component(hass, bluetooth.DOMAIN, {})
await hass.async_block_till_done()
assert bluetooth.async_ble_device_from_address(hass, address) is ble_device

View file

@ -59,7 +59,7 @@ class MyCoordinator(PassiveBluetoothDataUpdateCoordinator):
super()._async_handle_bluetooth_event(service_info, change) super()._async_handle_bluetooth_event(service_info, change)
async def test_basic_usage(hass, mock_bleak_scanner_start): async def test_basic_usage(hass, mock_bleak_scanner_start, mock_bluetooth_adapters):
"""Test basic usage of the PassiveBluetoothDataUpdateCoordinator.""" """Test basic usage of the PassiveBluetoothDataUpdateCoordinator."""
await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
coordinator = MyCoordinator( coordinator = MyCoordinator(
@ -88,7 +88,7 @@ async def test_basic_usage(hass, mock_bleak_scanner_start):
async def test_context_compatiblity_with_data_update_coordinator( async def test_context_compatiblity_with_data_update_coordinator(
hass, mock_bleak_scanner_start hass, mock_bleak_scanner_start, mock_bluetooth_adapters
): ):
"""Test contexts can be passed for compatibility with DataUpdateCoordinator.""" """Test contexts can be passed for compatibility with DataUpdateCoordinator."""
await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
@ -124,7 +124,7 @@ async def test_context_compatiblity_with_data_update_coordinator(
async def test_unavailable_callbacks_mark_the_coordinator_unavailable( async def test_unavailable_callbacks_mark_the_coordinator_unavailable(
hass, mock_bleak_scanner_start hass, mock_bleak_scanner_start, mock_bluetooth_adapters
): ):
"""Test that the coordinator goes unavailable when the bluetooth stack no longer sees the device.""" """Test that the coordinator goes unavailable when the bluetooth stack no longer sees the device."""
with patch( with patch(
@ -165,7 +165,9 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable(
assert coordinator.available is False assert coordinator.available is False
async def test_passive_bluetooth_coordinator_entity(hass, mock_bleak_scanner_start): async def test_passive_bluetooth_coordinator_entity(
hass, mock_bleak_scanner_start, mock_bluetooth_adapters
):
"""Test integration of PassiveBluetoothDataUpdateCoordinator with PassiveBluetoothCoordinatorEntity.""" """Test integration of PassiveBluetoothDataUpdateCoordinator with PassiveBluetoothCoordinatorEntity."""
await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
coordinator = MyCoordinator( coordinator = MyCoordinator(

View file

@ -98,7 +98,7 @@ GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate(
) )
async def test_basic_usage(hass, mock_bleak_scanner_start): async def test_basic_usage(hass, mock_bleak_scanner_start, mock_bluetooth_adapters):
"""Test basic usage of the PassiveBluetoothProcessorCoordinator.""" """Test basic usage of the PassiveBluetoothProcessorCoordinator."""
await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
@ -196,7 +196,9 @@ async def test_basic_usage(hass, mock_bleak_scanner_start):
cancel_coordinator() cancel_coordinator()
async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): async def test_unavailable_after_no_data(
hass, mock_bleak_scanner_start, mock_bluetooth_adapters
):
"""Test that the coordinator is unavailable after no data for a while.""" """Test that the coordinator is unavailable after no data for a while."""
with patch( with patch(
"bleak.BleakScanner.discovered_devices", # Must patch before we setup "bleak.BleakScanner.discovered_devices", # Must patch before we setup
@ -290,7 +292,9 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start):
cancel_coordinator() cancel_coordinator()
async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): async def test_no_updates_once_stopping(
hass, mock_bleak_scanner_start, mock_bluetooth_adapters
):
"""Test updates are ignored once hass is stopping.""" """Test updates are ignored once hass is stopping."""
await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
@ -343,7 +347,9 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start):
cancel_coordinator() cancel_coordinator()
async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_start): async def test_exception_from_update_method(
hass, caplog, mock_bleak_scanner_start, mock_bluetooth_adapters
):
"""Test we handle exceptions from the update method.""" """Test we handle exceptions from the update method."""
await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
@ -406,7 +412,9 @@ async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_sta
cancel_coordinator() cancel_coordinator()
async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): async def test_bad_data_from_update_method(
hass, mock_bleak_scanner_start, mock_bluetooth_adapters
):
"""Test we handle bad data from the update method.""" """Test we handle bad data from the update method."""
await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
@ -758,7 +766,9 @@ GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = (
) )
async def test_integration_with_entity(hass, mock_bleak_scanner_start): async def test_integration_with_entity(
hass, mock_bleak_scanner_start, mock_bluetooth_adapters
):
"""Test integration of PassiveBluetoothProcessorCoordinator with PassiveBluetoothCoordinatorEntity.""" """Test integration of PassiveBluetoothProcessorCoordinator with PassiveBluetoothCoordinatorEntity."""
await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
@ -888,7 +898,9 @@ NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate(
) )
async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner_start): async def test_integration_with_entity_without_a_device(
hass, mock_bleak_scanner_start, mock_bluetooth_adapters
):
"""Test integration with PassiveBluetoothCoordinatorEntity with no device.""" """Test integration with PassiveBluetoothCoordinatorEntity with no device."""
await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
@ -950,7 +962,7 @@ async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner
async def test_passive_bluetooth_entity_with_entity_platform( async def test_passive_bluetooth_entity_with_entity_platform(
hass, mock_bleak_scanner_start hass, mock_bleak_scanner_start, mock_bluetooth_adapters
): ):
"""Test with a mock entity platform.""" """Test with a mock entity platform."""
await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
@ -1048,7 +1060,9 @@ BINARY_SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate(
) )
async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_start): async def test_integration_multiple_entity_platforms(
hass, mock_bleak_scanner_start, mock_bluetooth_adapters
):
"""Test integration of PassiveBluetoothProcessorCoordinator with multiple platforms.""" """Test integration of PassiveBluetoothProcessorCoordinator with multiple platforms."""
await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
@ -1138,7 +1152,7 @@ async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_st
async def test_exception_from_coordinator_update_method( async def test_exception_from_coordinator_update_method(
hass, caplog, mock_bleak_scanner_start hass, caplog, mock_bleak_scanner_start, mock_bluetooth_adapters
): ):
"""Test we handle exceptions from the update method.""" """Test we handle exceptions from the update method."""
await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await async_setup_component(hass, DOMAIN, {DOMAIN: {}})

View file

@ -991,6 +991,8 @@ def mock_bluetooth_adapters():
"""Fixture to mock bluetooth adapters.""" """Fixture to mock bluetooth adapters."""
with patch( with patch(
"homeassistant.components.bluetooth.util.platform.system", return_value="Linux" "homeassistant.components.bluetooth.util.platform.system", return_value="Linux"
), patch(
"bluetooth_adapters.BlueZDBusObjects", return_value=MagicMock(load=AsyncMock())
), patch( ), patch(
"bluetooth_adapters.get_bluetooth_adapter_details", "bluetooth_adapters.get_bluetooth_adapter_details",
return_value={ return_value={