diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 2132e1c8b66..387615cdc29 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -228,7 +228,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: integration_matcher = IntegrationMatcher(await async_get_bluetooth(hass)) integration_matcher.async_setup() manager = BluetoothManager(hass, integration_matcher) - manager.async_setup() + await manager.async_setup() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, manager.async_stop) hass.data[DATA_MANAGER] = models.MANAGER = manager adapters = await manager.async_get_bluetooth_adapters() diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index edc9bb1edde..533867496bf 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -45,7 +45,7 @@ from .models import ( BluetoothServiceInfoBleak, ) 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: from bleak.backends.device import BLEDevice @@ -213,10 +213,15 @@ class BluetoothManager: self._adapters = await async_get_bluetooth_adapters() return self._find_adapter_by_address(address) - @hass_callback - def async_setup(self) -> None: + async def async_setup(self) -> None: """Set up the bluetooth manager.""" 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() @hass_callback diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 1b1ec016e82..9c368ebf82a 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "requirements": [ "bleak==0.17.0", "bleak-retry-connector==1.17.1", - "bluetooth-adapters==0.4.1", + "bluetooth-adapters==0.5.1", "bluetooth-auto-recovery==0.3.3", "dbus-fast==1.4.0" ], diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index 184e8775f07..f9bfcf7bb79 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -19,13 +19,7 @@ from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData from dbus_fast import InvalidMessageError -from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import ( - CALLBACK_TYPE, - Event, - HomeAssistant, - callback as hass_callback, -) +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.event import async_track_time_interval from homeassistant.util.package import is_docker_env @@ -133,7 +127,6 @@ class HaScanner(BaseHaScanner): self.scanner = scanner self.adapter = adapter self._start_stop_lock = asyncio.Lock() - self._cancel_stop: CALLBACK_TYPE | None = None self._cancel_watchdog: CALLBACK_TYPE | None = None self._last_detection = 0.0 self._start_time = 0.0 @@ -318,9 +311,6 @@ class HaScanner(BaseHaScanner): break self._async_setup_scanner_watchdog() - self._cancel_stop = self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, self._async_hass_stopping - ) @hass_callback def _async_setup_scanner_watchdog(self) -> None: @@ -368,11 +358,6 @@ class HaScanner(BaseHaScanner): 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: """Reset the adapter.""" # 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: """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) try: await self.scanner.stop() # type: ignore[no-untyped-call] diff --git a/homeassistant/components/bluetooth/util.py b/homeassistant/components/bluetooth/util.py index 3f6c862e53d..d04685f34d9 100644 --- a/homeassistant/components/bluetooth/util.py +++ b/homeassistant/components/bluetooth/util.py @@ -2,6 +2,7 @@ from __future__ import annotations import platform +import time from bluetooth_auto_recovery import recover_adapter @@ -15,6 +16,38 @@ from .const import ( WINDOWS_DEFAULT_BLUETOOTH_ADAPTER, 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]: diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8f6ffbf2cbd..065bd8469c2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ awesomeversion==22.9.0 bcrypt==3.1.7 bleak-retry-connector==1.17.1 bleak==0.17.0 -bluetooth-adapters==0.4.1 +bluetooth-adapters==0.5.1 bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 9df89439043..8ef6d6c1ee7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -430,7 +430,7 @@ bluemaestro-ble==0.2.0 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.4.1 +bluetooth-adapters==0.5.1 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 733342febdb..90eeabcba37 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -341,7 +341,7 @@ blinkpy==0.19.2 bluemaestro-ble==0.2.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.4.1 +bluetooth-adapters==0.5.1 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.3 diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py index 3447012ace5..58b6e596629 100644 --- a/tests/components/bluetooth/conftest.py +++ b/tests/components/bluetooth/conftest.py @@ -1,10 +1,20 @@ """Tests for the bluetooth component.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, MagicMock, patch 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") def macos_adapter(): """Fixture that mocks the macos adapter.""" @@ -25,7 +35,7 @@ def windows_adapter(): @pytest.fixture(name="one_adapter") -def one_adapter_fixture(): +def one_adapter_fixture(bluez_dbus_mock): """Fixture that mocks one adapter on Linux.""" with patch( "homeassistant.components.bluetooth.platform.system", return_value="Linux" @@ -54,7 +64,7 @@ def one_adapter_fixture(): @pytest.fixture(name="two_adapters") -def two_adapters_fixture(): +def two_adapters_fixture(bluez_dbus_mock): """Fixture that mocks two adapters on Linux.""" with patch( "homeassistant.components.bluetooth.platform.system", return_value="Linux" diff --git a/tests/components/bluetooth/test_active_update_coordinator.py b/tests/components/bluetooth/test_active_update_coordinator.py index 4dbe32d69d2..318934d77b7 100644 --- a/tests/components/bluetooth/test_active_update_coordinator.py +++ b/tests/components/bluetooth/test_active_update_coordinator.py @@ -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.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -92,7 +94,9 @@ async def test_basic_usage(hass: HomeAssistant, mock_bleak_scanner_start): 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.""" 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( - hass: HomeAssistant, mock_bleak_scanner_start, caplog + hass: HomeAssistant, mock_bleak_scanner_start, mock_bluetooth_adapters, caplog ): """Test bleak error handling and recovery.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -212,7 +216,9 @@ async def test_bleak_error_and_recover( 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.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -267,7 +273,9 @@ async def test_poll_failure_and_recover(hass: HomeAssistant, mock_bleak_scanner_ 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.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -314,7 +322,9 @@ async def test_second_poll_needed(hass: HomeAssistant, mock_bleak_scanner_start) 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.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) diff --git a/tests/components/bluetooth/test_config_flow.py b/tests/components/bluetooth/test_config_flow.py index aa40666c80a..4c1e8f660b3 100644 --- a/tests/components/bluetooth/test_config_flow.py +++ b/tests/components/bluetooth/test_config_flow.py @@ -18,7 +18,11 @@ from tests.common import MockConfigEntry 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.""" await async_setup_component(hass, "config", {}) @@ -38,6 +42,7 @@ async def test_options_flow_disabled_not_setup( ) response = await ws_client.receive_json() 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): @@ -262,7 +267,9 @@ async def test_async_step_integration_discovery_already_exists(hass): 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.""" entry = MockConfigEntry( 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["data"][CONF_PASSIVE] is False + await hass.config_entries.async_unload(entry.entry_id) 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.""" await async_setup_component(hass, "config", {}) @@ -334,10 +346,11 @@ async def test_options_flow_disabled_macos( ) response = await ws_client.receive_json() assert response["result"][0]["supports_options"] is False + await hass.config_entries.async_unload(entry.entry_id) 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.""" await async_setup_component(hass, "config", {}) @@ -363,3 +376,4 @@ async def test_options_flow_enabled_linux( ) response = await ws_client.receive_json() assert response["result"][0]["supports_options"] is True + await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 33627f6a787..1c3c58bc7ab 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -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 -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.""" with patch( "bluetooth_adapters.get_bluetooth_adapter_details", return_value={} diff --git a/tests/components/bluetooth/test_manager.py b/tests/components/bluetooth/test_manager.py index 9ce5985318b..c05b9424508 100644 --- a/tests/components/bluetooth/test_manager.py +++ b/tests/components/bluetooth/test_manager.py @@ -1,10 +1,13 @@ """Tests for the Bluetooth integration manager.""" +from unittest.mock import AsyncMock, MagicMock, patch from bleak.backends.scanner import AdvertisementData, BLEDevice +from bluetooth_adapters import AdvertisementHistory from homeassistant.components import bluetooth from homeassistant.components.bluetooth.manager import STALE_ADVERTISEMENT_SECONDS +from homeassistant.setup import async_setup_component from . import ( 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) 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 diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index bf7c0a48467..15845abc5ac 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -59,7 +59,7 @@ class MyCoordinator(PassiveBluetoothDataUpdateCoordinator): 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.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) 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( - hass, mock_bleak_scanner_start + hass, mock_bleak_scanner_start, mock_bluetooth_adapters ): """Test contexts can be passed for compatibility with DataUpdateCoordinator.""" 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( - 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.""" with patch( @@ -165,7 +165,9 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable( 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.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) coordinator = MyCoordinator( diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index e7260d22e4b..482f3b3a94f 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -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.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -196,7 +196,9 @@ async def test_basic_usage(hass, mock_bleak_scanner_start): 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.""" with patch( "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() -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.""" 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() -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.""" 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() -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.""" 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.""" 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.""" 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( - hass, mock_bleak_scanner_start + hass, mock_bleak_scanner_start, mock_bluetooth_adapters ): """Test with a mock entity platform.""" 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.""" 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( - hass, caplog, mock_bleak_scanner_start + hass, caplog, mock_bleak_scanner_start, mock_bluetooth_adapters ): """Test we handle exceptions from the update method.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) diff --git a/tests/conftest.py b/tests/conftest.py index 4a02083610b..2c95770974b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -991,6 +991,8 @@ def mock_bluetooth_adapters(): """Fixture to mock bluetooth adapters.""" with patch( "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" + ), patch( + "bluetooth_adapters.BlueZDBusObjects", return_value=MagicMock(load=AsyncMock()) ), patch( "bluetooth_adapters.get_bluetooth_adapter_details", return_value={