From cd59d3ab81b189cf6d14f91d88d92051e3c43dee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 18 Aug 2022 15:41:07 -1000 Subject: [PATCH] Add support for multiple Bluetooth adapters (#76963) --- .../components/bluetooth/__init__.py | 119 +++++-- .../components/bluetooth/config_flow.py | 150 +++++---- homeassistant/components/bluetooth/const.py | 28 +- homeassistant/components/bluetooth/manager.py | 30 +- .../components/bluetooth/manifest.json | 2 +- .../components/bluetooth/strings.json | 23 +- .../components/bluetooth/translations/en.json | 21 +- homeassistant/components/bluetooth/util.py | 70 +++- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bluetooth/__init__.py | 28 +- tests/components/bluetooth/conftest.py | 70 ++++ .../components/bluetooth/test_config_flow.py | 305 +++++++++--------- tests/components/bluetooth/test_init.py | 277 ++++++++-------- tests/components/bluetooth/test_scanner.py | 81 ++--- tests/conftest.py | 17 +- 17 files changed, 738 insertions(+), 489 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 8bb275c94fd..e659cec60a0 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from asyncio import Future from collections.abc import Callable +import platform from typing import TYPE_CHECKING import async_timeout @@ -11,11 +12,22 @@ from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback as hass_callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import discovery_flow +from homeassistant.helpers import device_registry as dr, discovery_flow from homeassistant.loader import async_get_bluetooth from . import models -from .const import CONF_ADAPTER, DATA_MANAGER, DOMAIN, SOURCE_LOCAL +from .const import ( + ADAPTER_ADDRESS, + ADAPTER_HW_VERSION, + ADAPTER_SW_VERSION, + CONF_ADAPTER, + CONF_DETAILS, + DATA_MANAGER, + DEFAULT_ADDRESS, + DOMAIN, + SOURCE_LOCAL, + AdapterDetails, +) from .manager import BluetoothManager from .match import BluetoothCallbackMatcher, IntegrationMatcher from .models import ( @@ -28,7 +40,7 @@ from .models import ( ProcessAdvertisementCallback, ) from .scanner import HaScanner, create_bleak_scanner -from .util import async_get_bluetooth_adapters +from .util import adapter_human_name, adapter_unique_name, async_default_adapter if TYPE_CHECKING: from bleak.backends.device import BLEDevice @@ -164,37 +176,88 @@ def async_rediscover_address(hass: HomeAssistant, address: str) -> None: manager.async_rediscover_address(address) -async def _async_has_bluetooth_adapter() -> bool: - """Return if the device has a bluetooth adapter.""" - return bool(await async_get_bluetooth_adapters()) - - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the bluetooth integration.""" integration_matcher = IntegrationMatcher(await async_get_bluetooth(hass)) + manager = BluetoothManager(hass, integration_matcher) manager.async_setup() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, manager.async_stop) hass.data[DATA_MANAGER] = models.MANAGER = manager - # The config entry is responsible for starting the manager - # if its enabled - if hass.config_entries.async_entries(DOMAIN): - return True - if DOMAIN in config: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={} - ) + adapters = await manager.async_get_bluetooth_adapters() + + async_migrate_entries(hass, adapters) + await async_discover_adapters(hass, adapters) + + return True + + +@hass_callback +def async_migrate_entries( + hass: HomeAssistant, + adapters: dict[str, AdapterDetails], +) -> None: + """Migrate config entries to support multiple.""" + current_entries = hass.config_entries.async_entries(DOMAIN) + default_adapter = async_default_adapter() + + for entry in current_entries: + if entry.unique_id: + continue + + address = DEFAULT_ADDRESS + adapter = entry.options.get(CONF_ADAPTER, default_adapter) + if adapter in adapters: + address = adapters[adapter][ADAPTER_ADDRESS] + hass.config_entries.async_update_entry( + entry, title=adapter_unique_name(adapter, address), unique_id=address ) - elif await _async_has_bluetooth_adapter(): + + +async def async_discover_adapters( + hass: HomeAssistant, + adapters: dict[str, AdapterDetails], +) -> None: + """Discover adapters and start flows.""" + if platform.system() == "Windows": + # We currently do not have a good way to detect if a bluetooth device is + # available on Windows. We will just assume that it is not unless they + # actively add it. + return + + for adapter, details in adapters.items(): discovery_flow.async_create_flow( hass, DOMAIN, context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={}, + data={CONF_ADAPTER: adapter, CONF_DETAILS: details}, ) - return True + + +async def async_update_device( + entry: config_entries.ConfigEntry, + manager: BluetoothManager, + adapter: str, + address: str, +) -> None: + """Update device registry entry. + + The physical adapter can change from hci0/hci1 on reboot + or if the user moves around the usb sticks so we need to + update the device with the new location so they can + figure out where the adapter is. + """ + adapters = await manager.async_get_bluetooth_adapters() + details = adapters[adapter] + registry = dr.async_get(manager.hass) + registry.async_get_or_create( + config_entry_id=entry.entry_id, + name=adapter_human_name(adapter, details[ADAPTER_ADDRESS]), + connections={(dr.CONNECTION_BLUETOOTH, details[ADAPTER_ADDRESS])}, + sw_version=details.get(ADAPTER_SW_VERSION), + hw_version=details.get(ADAPTER_HW_VERSION), + ) async def async_setup_entry( @@ -202,7 +265,12 @@ async def async_setup_entry( ) -> bool: """Set up a config entry for a bluetooth scanner.""" manager: BluetoothManager = hass.data[DATA_MANAGER] - adapter: str | None = entry.options.get(CONF_ADAPTER) + address = entry.unique_id + assert address is not None + adapter = await manager.async_get_adapter_from_address(address) + if adapter is None: + raise ConfigEntryNotReady(f"Bluetooth adapter with address {address} not found") + try: bleak_scanner = create_bleak_scanner(BluetoothScanningMode.ACTIVE, adapter) except RuntimeError as err: @@ -211,18 +279,11 @@ async def async_setup_entry( entry.async_on_unload(scanner.async_register_callback(manager.scanner_adv_received)) await scanner.async_start() entry.async_on_unload(manager.async_register_scanner(scanner)) - entry.async_on_unload(entry.add_update_listener(_async_update_listener)) + await async_update_device(entry, manager, adapter, address) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = scanner return True -async def _async_update_listener( - hass: HomeAssistant, entry: config_entries.ConfigEntry -) -> None: - """Handle options update.""" - await hass.config_entries.async_reload(entry.entry_id) - - async def async_unload_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry ) -> bool: diff --git a/homeassistant/components/bluetooth/config_flow.py b/homeassistant/components/bluetooth/config_flow.py index 1a0be8706bf..2435a1e39ed 100644 --- a/homeassistant/components/bluetooth/config_flow.py +++ b/homeassistant/components/bluetooth/config_flow.py @@ -1,16 +1,16 @@ """Config flow to configure the Bluetooth integration.""" from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast import voluptuous as vol from homeassistant.components import onboarding -from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow -from homeassistant.core import callback +from homeassistant.config_entries import ConfigFlow +from homeassistant.helpers.typing import DiscoveryInfoType -from .const import CONF_ADAPTER, DEFAULT_NAME, DOMAIN -from .util import async_get_bluetooth_adapters +from .const import ADAPTER_ADDRESS, CONF_ADAPTER, CONF_DETAILS, DOMAIN, AdapterDetails +from .util import adapter_human_name, adapter_unique_name, async_get_bluetooth_adapters if TYPE_CHECKING: from homeassistant.data_entry_flow import FlowResult @@ -21,60 +21,94 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 + def __init__(self) -> None: + """Initialize the config flow.""" + self._adapter: str | None = None + self._details: AdapterDetails | None = None + self._adapters: dict[str, AdapterDetails] = {} + + async def async_step_integration_discovery( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: + """Handle a flow initialized by discovery.""" + self._adapter = cast(str, discovery_info[CONF_ADAPTER]) + self._details = cast(AdapterDetails, discovery_info[CONF_DETAILS]) + await self.async_set_unique_id(self._details[ADAPTER_ADDRESS]) + self._abort_if_unique_id_configured() + self.context["title_placeholders"] = { + "name": adapter_human_name(self._adapter, self._details[ADAPTER_ADDRESS]) + } + return await self.async_step_single_adapter() + + async def async_step_single_adapter( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Select an adapter.""" + adapter = self._adapter + details = self._details + assert adapter is not None + assert details is not None + + address = details[ADAPTER_ADDRESS] + + if user_input is not None or not onboarding.async_is_onboarded(self.hass): + await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=adapter_unique_name(adapter, address), data={} + ) + + return self.async_show_form( + step_id="single_adapter", + description_placeholders={"name": adapter_human_name(adapter, address)}, + ) + + async def async_step_multiple_adapters( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + if user_input is not None: + assert self._adapters is not None + adapter = user_input[CONF_ADAPTER] + address = self._adapters[adapter][ADAPTER_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=adapter_unique_name(adapter, address), data={} + ) + + configured_addresses = self._async_current_ids() + self._adapters = await async_get_bluetooth_adapters() + unconfigured_adapters = [ + adapter + for adapter, details in self._adapters.items() + if details[ADAPTER_ADDRESS] not in configured_addresses + ] + if not unconfigured_adapters: + return self.async_abort(reason="no_adapters") + if len(unconfigured_adapters) == 1: + self._adapter = list(self._adapters)[0] + self._details = self._adapters[self._adapter] + return await self.async_step_single_adapter() + + return self.async_show_form( + step_id="multiple_adapters", + data_schema=vol.Schema( + { + vol.Required(CONF_ADAPTER): vol.In( + { + adapter: adapter_human_name( + adapter, self._adapters[adapter][ADAPTER_ADDRESS] + ) + for adapter in sorted(unconfigured_adapters) + } + ), + } + ), + ) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a flow initialized by the user.""" - return await self.async_step_enable_bluetooth() - - async def async_step_enable_bluetooth( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle a flow initialized by the user or import.""" - if self._async_current_entries(): - return self.async_abort(reason="already_configured") - - if user_input is not None or not onboarding.async_is_onboarded(self.hass): - return self.async_create_entry(title=DEFAULT_NAME, data={}) - - return self.async_show_form(step_id="enable_bluetooth") - - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: - """Handle import from configuration.yaml.""" - return await self.async_step_enable_bluetooth(user_input) - - @staticmethod - @callback - def async_get_options_flow( - config_entry: ConfigEntry, - ) -> OptionsFlowHandler: - """Get the options flow for this handler.""" - return OptionsFlowHandler(config_entry) - - -class OptionsFlowHandler(OptionsFlow): - """Handle the option flow for bluetooth.""" - - def __init__(self, config_entry: ConfigEntry) -> None: - """Initialize options flow.""" - self.config_entry = config_entry - - async def async_step_init( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle options flow.""" - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - if not (adapters := await async_get_bluetooth_adapters()): - return self.async_abort(reason="no_adapters") - - data_schema = vol.Schema( - { - vol.Required( - CONF_ADAPTER, - default=self.config_entry.options.get(CONF_ADAPTER, adapters[0]), - ): vol.In(adapters), - } - ) - return self.async_show_form(step_id="init", data_schema=data_schema) + return await self.async_step_multiple_adapters() diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py index 04581b841b9..0cd02bcbb8d 100644 --- a/homeassistant/components/bluetooth/const.py +++ b/homeassistant/components/bluetooth/const.py @@ -2,18 +2,27 @@ from __future__ import annotations from datetime import timedelta -from typing import Final +from typing import Final, TypedDict DOMAIN = "bluetooth" -DEFAULT_NAME = "Bluetooth" CONF_ADAPTER = "adapter" +CONF_DETAILS = "details" -MACOS_DEFAULT_BLUETOOTH_ADAPTER = "CoreBluetooth" +WINDOWS_DEFAULT_BLUETOOTH_ADAPTER = "bluetooth" +MACOS_DEFAULT_BLUETOOTH_ADAPTER = "Core Bluetooth" UNIX_DEFAULT_BLUETOOTH_ADAPTER = "hci0" DEFAULT_ADAPTERS = {MACOS_DEFAULT_BLUETOOTH_ADAPTER, UNIX_DEFAULT_BLUETOOTH_ADAPTER} +DEFAULT_ADAPTER_BY_PLATFORM = { + "Windows": WINDOWS_DEFAULT_BLUETOOTH_ADAPTER, + "Darwin": MACOS_DEFAULT_BLUETOOTH_ADAPTER, +} + +# Some operating systems hide the adapter address for privacy reasons (ex MacOS) +DEFAULT_ADDRESS: Final = "00:00:00:00:00:00" + SOURCE_LOCAL: Final = "local" DATA_MANAGER: Final = "bluetooth_manager" @@ -22,3 +31,16 @@ UNAVAILABLE_TRACK_SECONDS: Final = 60 * 5 START_TIMEOUT = 12 SCANNER_WATCHDOG_TIMEOUT: Final = 60 * 5 SCANNER_WATCHDOG_INTERVAL: Final = timedelta(seconds=SCANNER_WATCHDOG_TIMEOUT) + + +class AdapterDetails(TypedDict, total=False): + """Adapter details.""" + + address: str + sw_version: str + hw_version: str + + +ADAPTER_ADDRESS: Final = "address" +ADAPTER_SW_VERSION: Final = "sw_version" +ADAPTER_HW_VERSION: Final = "hw_version" diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 0fba2d2aae1..0b588e71681 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -20,7 +20,12 @@ from homeassistant.core import ( from homeassistant.helpers import discovery_flow from homeassistant.helpers.event import async_track_time_interval -from .const import SOURCE_LOCAL, UNAVAILABLE_TRACK_SECONDS +from .const import ( + ADAPTER_ADDRESS, + SOURCE_LOCAL, + UNAVAILABLE_TRACK_SECONDS, + AdapterDetails, +) from .match import ( ADDRESS, BluetoothCallbackMatcher, @@ -29,6 +34,7 @@ from .match import ( ) from .models import BluetoothCallback, BluetoothChange, BluetoothServiceInfoBleak from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher +from .util import async_get_bluetooth_adapters if TYPE_CHECKING: from bleak.backends.device import BLEDevice @@ -39,7 +45,7 @@ if TYPE_CHECKING: FILTER_UUIDS: Final = "UUIDs" -RSSI_SWITCH_THRESHOLD = 10 +RSSI_SWITCH_THRESHOLD = 6 STALE_ADVERTISEMENT_SECONDS = 180 _LOGGER = logging.getLogger(__name__) @@ -132,6 +138,26 @@ class BluetoothManager: ] = [] self.history: dict[str, AdvertisementHistory] = {} self._scanners: list[HaScanner] = [] + self._adapters: dict[str, AdapterDetails] = {} + + def _find_adapter_by_address(self, address: str) -> str | None: + for adapter, details in self._adapters.items(): + if details[ADAPTER_ADDRESS] == address: + return adapter + return None + + async def async_get_bluetooth_adapters(self) -> dict[str, AdapterDetails]: + """Get bluetooth adapters.""" + if not self._adapters: + self._adapters = await async_get_bluetooth_adapters() + return self._adapters + + async def async_get_adapter_from_address(self, address: str) -> str | None: + """Get adapter from address.""" + if adapter := self._find_adapter_by_address(address): + return adapter + self._adapters = await async_get_bluetooth_adapters() + return self._find_adapter_by_address(address) @hass_callback def async_setup(self) -> None: diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index f3828db5d10..ff99bd3d97d 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/bluetooth", "dependencies": ["websocket_api"], "quality_scale": "internal", - "requirements": ["bleak==0.15.1", "bluetooth-adapters==0.1.3"], + "requirements": ["bleak==0.15.1", "bluetooth-adapters==0.2.0"], "codeowners": ["@bdraco"], "config_flow": true, "iot_class": "local_push" diff --git a/homeassistant/components/bluetooth/strings.json b/homeassistant/components/bluetooth/strings.json index beff2fd8312..269995192a8 100644 --- a/homeassistant/components/bluetooth/strings.json +++ b/homeassistant/components/bluetooth/strings.json @@ -2,9 +2,6 @@ "config": { "flow_title": "{name}", "step": { - "enable_bluetooth": { - "description": "Do you want to setup Bluetooth?" - }, "user": { "description": "Choose a device to setup", "data": { @@ -13,20 +10,20 @@ }, "bluetooth_confirm": { "description": "Do you want to setup {name}?" + }, + "multiple_adapters": { + "description": "Select a Bluetooth adapter to setup", + "data": { + "adapter": "Adapter" + } + }, + "single_adapter": { + "description": "Do you want to setup the Bluetooth adapter {name}?" } }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", - "no_adapters": "No Bluetooth adapters found" - } - }, - "options": { - "step": { - "init": { - "data": { - "adapter": "The Bluetooth Adapter to use for scanning" - } - } + "no_adapters": "No unconfigured Bluetooth adapters found" } } } diff --git a/homeassistant/components/bluetooth/translations/en.json b/homeassistant/components/bluetooth/translations/en.json index 4b53822b771..ac80cfb620e 100644 --- a/homeassistant/components/bluetooth/translations/en.json +++ b/homeassistant/components/bluetooth/translations/en.json @@ -2,15 +2,21 @@ "config": { "abort": { "already_configured": "Service is already configured", - "no_adapters": "No Bluetooth adapters found" + "no_adapters": "No unconfigured Bluetooth adapters found" }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Do you want to setup {name}?" }, - "enable_bluetooth": { - "description": "Do you want to setup Bluetooth?" + "multiple_adapters": { + "data": { + "adapter": "Adapter" + }, + "description": "Select a Bluetooth adapter to setup" + }, + "single_adapter": { + "description": "Do you want to setup the Bluetooth adapter {name}?" }, "user": { "data": { @@ -19,14 +25,5 @@ "description": "Choose a device to setup" } } - }, - "options": { - "step": { - "init": { - "data": { - "adapter": "The Bluetooth Adapter to use for scanning" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/util.py b/homeassistant/components/bluetooth/util.py index 68920050748..3133b2f210d 100644 --- a/homeassistant/components/bluetooth/util.py +++ b/homeassistant/components/bluetooth/util.py @@ -3,25 +3,65 @@ from __future__ import annotations import platform -from .const import MACOS_DEFAULT_BLUETOOTH_ADAPTER, UNIX_DEFAULT_BLUETOOTH_ADAPTER +from homeassistant.core import callback + +from .const import ( + DEFAULT_ADAPTER_BY_PLATFORM, + DEFAULT_ADDRESS, + MACOS_DEFAULT_BLUETOOTH_ADAPTER, + UNIX_DEFAULT_BLUETOOTH_ADAPTER, + WINDOWS_DEFAULT_BLUETOOTH_ADAPTER, + AdapterDetails, +) -async def async_get_bluetooth_adapters() -> list[str]: +async def async_get_bluetooth_adapters() -> dict[str, AdapterDetails]: """Return a list of bluetooth adapters.""" - if platform.system() == "Windows": # We don't have a good way to detect on windows - return [] - if platform.system() == "Darwin": # CoreBluetooth is built in on MacOS hardware - return [MACOS_DEFAULT_BLUETOOTH_ADAPTER] + if platform.system() == "Windows": + return { + WINDOWS_DEFAULT_BLUETOOTH_ADAPTER: AdapterDetails( + address=DEFAULT_ADDRESS, + sw_version=platform.release(), + ) + } + if platform.system() == "Darwin": + return { + MACOS_DEFAULT_BLUETOOTH_ADAPTER: AdapterDetails( + address=DEFAULT_ADDRESS, + sw_version=platform.release(), + ) + } from bluetooth_adapters import ( # pylint: disable=import-outside-toplevel - get_bluetooth_adapters, + get_bluetooth_adapter_details, ) - adapters = await get_bluetooth_adapters() - if ( - UNIX_DEFAULT_BLUETOOTH_ADAPTER in adapters - and adapters[0] != UNIX_DEFAULT_BLUETOOTH_ADAPTER - ): - # The default adapter always needs to be the first in the list - # because that is how bleak works. - adapters.insert(0, adapters.pop(adapters.index(UNIX_DEFAULT_BLUETOOTH_ADAPTER))) + adapters: dict[str, AdapterDetails] = {} + adapter_details = await get_bluetooth_adapter_details() + for adapter, details in adapter_details.items(): + adapter1 = details["org.bluez.Adapter1"] + adapters[adapter] = AdapterDetails( + address=adapter1["Address"], + sw_version=adapter1["Name"], # This is actually the BlueZ version + hw_version=adapter1["Modalias"], + ) return adapters + + +@callback +def async_default_adapter() -> str: + """Return the default adapter for the platform.""" + return DEFAULT_ADAPTER_BY_PLATFORM.get( + platform.system(), UNIX_DEFAULT_BLUETOOTH_ADAPTER + ) + + +@callback +def adapter_human_name(adapter: str, address: str) -> str: + """Return a human readable name for the adapter.""" + return adapter if address == DEFAULT_ADDRESS else f"{adapter} ({address})" + + +@callback +def adapter_unique_name(adapter: str, address: str) -> str: + """Return a unique name for the adapter.""" + return adapter if address == DEFAULT_ADDRESS else address diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8381e204a77..b6e5dbc4119 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ attrs==21.2.0 awesomeversion==22.6.0 bcrypt==3.1.7 bleak==0.15.1 -bluetooth-adapters==0.1.3 +bluetooth-adapters==0.2.0 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==37.0.4 diff --git a/requirements_all.txt b/requirements_all.txt index 322d67a94df..733d82c9668 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -424,7 +424,7 @@ blockchain==1.4.4 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.1.3 +bluetooth-adapters==0.2.0 # homeassistant.components.bond bond-async==0.1.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c67a756157..2a5a6c3f6cc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -335,7 +335,7 @@ blebox_uniapi==2.0.2 blinkpy==0.19.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.1.3 +bluetooth-adapters==0.2.0 # homeassistant.components.bond bond-async==0.1.22 diff --git a/tests/components/bluetooth/__init__.py b/tests/components/bluetooth/__init__.py index 44da1a60f03..220432c46c2 100644 --- a/tests/components/bluetooth/__init__.py +++ b/tests/components/bluetooth/__init__.py @@ -6,8 +6,13 @@ from unittest.mock import patch from bleak.backends.scanner import AdvertisementData, BLEDevice -from homeassistant.components.bluetooth import SOURCE_LOCAL, models +from homeassistant.components.bluetooth import DOMAIN, SOURCE_LOCAL, models +from homeassistant.components.bluetooth.const import DEFAULT_ADDRESS from homeassistant.components.bluetooth.manager import BluetoothManager +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry def _get_manager() -> BluetoothManager: @@ -48,3 +53,24 @@ def patch_discovered_devices(mock_discovered: list[BLEDevice]) -> None: return patch.object( manager, "async_discovered_devices", return_value=mock_discovered ) + + +async def async_setup_with_default_adapter(hass: HomeAssistant) -> MockConfigEntry: + """Set up the Bluetooth integration with a default adapter.""" + return await _async_setup_with_adapter(hass, DEFAULT_ADDRESS) + + +async def async_setup_with_one_adapter(hass: HomeAssistant) -> MockConfigEntry: + """Set up the Bluetooth integration with one adapter.""" + return await _async_setup_with_adapter(hass, "00:00:00:00:00:01") + + +async def _async_setup_with_adapter( + hass: HomeAssistant, address: str +) -> MockConfigEntry: + """Set up the Bluetooth integration with any adapter.""" + entry = MockConfigEntry(domain="bluetooth", unique_id=address) + entry.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + return entry diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py index 760500fe7a1..5ddd0fbc15f 100644 --- a/tests/components/bluetooth/conftest.py +++ b/tests/components/bluetooth/conftest.py @@ -1 +1,71 @@ """Tests for the bluetooth component.""" + +from unittest.mock import patch + +import pytest + + +@pytest.fixture(name="macos_adapter") +def macos_adapter(): + """Fixture that mocks the macos adapter.""" + with patch( + "homeassistant.components.bluetooth.util.platform.system", return_value="Darwin" + ): + yield + + +@pytest.fixture(name="windows_adapter") +def windows_adapter(): + """Fixture that mocks the windows adapter.""" + with patch( + "homeassistant.components.bluetooth.util.platform.system", + return_value="Windows", + ): + yield + + +@pytest.fixture(name="one_adapter") +def one_adapter_fixture(): + """Fixture that mocks one adapter on Linux.""" + with patch( + "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" + ), patch( + "bluetooth_adapters.get_bluetooth_adapter_details", + return_value={ + "hci0": { + "org.bluez.Adapter1": { + "Address": "00:00:00:00:00:01", + "Name": "BlueZ 4.63", + "Modalias": "usbid:1234", + } + }, + }, + ): + yield + + +@pytest.fixture(name="two_adapters") +def two_adapters_fixture(): + """Fixture that mocks two adapters on Linux.""" + with patch( + "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" + ), patch( + "bluetooth_adapters.get_bluetooth_adapter_details", + return_value={ + "hci0": { + "org.bluez.Adapter1": { + "Address": "00:00:00:00:00:01", + "Name": "BlueZ 4.63", + "Modalias": "usbid:1234", + } + }, + "hci1": { + "org.bluez.Adapter1": { + "Address": "00:00:00:00:00:02", + "Name": "BlueZ 4.63", + "Modalias": "usbid:1234", + } + }, + }, + ): + yield diff --git a/tests/components/bluetooth/test_config_flow.py b/tests/components/bluetooth/test_config_flow.py index 1053133cac9..e16208b3d70 100644 --- a/tests/components/bluetooth/test_config_flow.py +++ b/tests/components/bluetooth/test_config_flow.py @@ -5,38 +5,88 @@ from unittest.mock import patch from homeassistant import config_entries from homeassistant.components.bluetooth.const import ( CONF_ADAPTER, + CONF_DETAILS, + DEFAULT_ADDRESS, DOMAIN, - MACOS_DEFAULT_BLUETOOTH_ADAPTER, + AdapterDetails, ) from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -async def test_async_step_user(hass): - """Test setting up manually.""" +async def test_async_step_user_macos(hass, macos_adapter): + """Test setting up manually with one adapter on MacOS.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data={}, ) assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "enable_bluetooth" + assert result["step_id"] == "single_adapter" with patch( + "homeassistant.components.bluetooth.async_setup", return_value=True + ), patch( "homeassistant.components.bluetooth.async_setup_entry", return_value=True ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Bluetooth" + assert result2["title"] == "Core Bluetooth" assert result2["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 -async def test_async_step_user_only_allows_one(hass): +async def test_async_step_user_linux_one_adapter(hass, one_adapter): + """Test setting up manually with one adapter on Linux.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "single_adapter" + with patch( + "homeassistant.components.bluetooth.async_setup", return_value=True + ), patch( + "homeassistant.components.bluetooth.async_setup_entry", return_value=True + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "00:00:00:00:00:01" + assert result2["data"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_async_step_user_linux_two_adapters(hass, two_adapters): + """Test setting up manually with two adapters on Linux.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "multiple_adapters" + with patch( + "homeassistant.components.bluetooth.async_setup", return_value=True + ), patch( + "homeassistant.components.bluetooth.async_setup_entry", return_value=True + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_ADAPTER: "hci1"} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "00:00:00:00:00:02" + assert result2["data"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_async_step_user_only_allows_one(hass, macos_adapter): """Test setting up manually with an existing entry.""" - entry = MockConfigEntry(domain=DOMAIN) + entry = MockConfigEntry(domain=DOMAIN, unique_id=DEFAULT_ADDRESS) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, @@ -44,34 +94,48 @@ async def test_async_step_user_only_allows_one(hass): data={}, ) assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "already_configured" + assert result["reason"] == "no_adapters" async def test_async_step_integration_discovery(hass): """Test setting up from integration discovery.""" + + details = AdapterDetails( + address="00:00:00:00:00:01", sw_version="1.23.5", hw_version="1.2.3" + ) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={}, + data={CONF_ADAPTER: "hci0", CONF_DETAILS: details}, ) assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "enable_bluetooth" + assert result["step_id"] == "single_adapter" with patch( + "homeassistant.components.bluetooth.async_setup", return_value=True + ), patch( "homeassistant.components.bluetooth.async_setup_entry", return_value=True ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Bluetooth" + assert result2["title"] == "00:00:00:00:00:01" assert result2["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 -async def test_async_step_integration_discovery_during_onboarding(hass): +async def test_async_step_integration_discovery_during_onboarding_one_adapter( + hass, one_adapter +): """Test setting up from integration discovery during onboarding.""" + details = AdapterDetails( + address="00:00:00:00:00:01", sw_version="1.23.5", hw_version="1.2.3" + ) with patch( + "homeassistant.components.bluetooth.async_setup", return_value=True + ), patch( "homeassistant.components.bluetooth.async_setup_entry", return_value=True ) as mock_setup_entry, patch( "homeassistant.components.onboarding.async_is_onboarded", @@ -80,10 +144,77 @@ async def test_async_step_integration_discovery_during_onboarding(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={}, + data={CONF_ADAPTER: "hci0", CONF_DETAILS: details}, ) assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "Bluetooth" + assert result["title"] == "00:00:00:00:00:01" + assert result["data"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_onboarding.mock_calls) == 1 + + +async def test_async_step_integration_discovery_during_onboarding_two_adapters( + hass, two_adapters +): + """Test setting up from integration discovery during onboarding.""" + details1 = AdapterDetails( + address="00:00:00:00:00:01", sw_version="1.23.5", hw_version="1.2.3" + ) + details2 = AdapterDetails( + address="00:00:00:00:00:02", sw_version="1.23.5", hw_version="1.2.3" + ) + + with patch( + "homeassistant.components.bluetooth.async_setup", return_value=True + ), patch( + "homeassistant.components.bluetooth.async_setup_entry", return_value=True + ) as mock_setup_entry, patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=False, + ) as mock_onboarding: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={CONF_ADAPTER: "hci0", CONF_DETAILS: details1}, + ) + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={CONF_ADAPTER: "hci1", CONF_DETAILS: details2}, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "00:00:00:00:00:01" + assert result["data"] == {} + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "00:00:00:00:00:02" + assert result2["data"] == {} + + assert len(mock_setup_entry.mock_calls) == 2 + assert len(mock_onboarding.mock_calls) == 2 + + +async def test_async_step_integration_discovery_during_onboarding(hass, macos_adapter): + """Test setting up from integration discovery during onboarding.""" + details = AdapterDetails( + address=DEFAULT_ADDRESS, sw_version="1.23.5", hw_version="1.2.3" + ) + + with patch( + "homeassistant.components.bluetooth.async_setup", return_value=True + ), patch( + "homeassistant.components.bluetooth.async_setup_entry", return_value=True + ) as mock_setup_entry, patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=False, + ) as mock_onboarding: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={CONF_ADAPTER: "Core Bluetooth", CONF_DETAILS: details}, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Core Bluetooth" assert result["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_onboarding.mock_calls) == 1 @@ -91,150 +222,16 @@ async def test_async_step_integration_discovery_during_onboarding(hass): async def test_async_step_integration_discovery_already_exists(hass): """Test setting up from integration discovery when an entry already exists.""" - entry = MockConfigEntry(domain=DOMAIN) + details = AdapterDetails( + address="00:00:00:00:00:01", sw_version="1.23.5", hw_version="1.2.3" + ) + + entry = MockConfigEntry(domain=DOMAIN, unique_id="00:00:00:00:00:01") entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={}, + data={CONF_ADAPTER: "hci0", CONF_DETAILS: details}, ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" - - -async def test_async_step_import(hass): - """Test setting up from integration discovery.""" - with patch( - "homeassistant.components.bluetooth.async_setup_entry", return_value=True - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={}, - ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "Bluetooth" - assert result["data"] == {} - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_async_step_import_already_exists(hass): - """Test setting up from yaml when an entry already exists.""" - entry = MockConfigEntry(domain=DOMAIN) - entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={}, - ) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "already_configured" - - -@patch("homeassistant.components.bluetooth.util.platform.system", return_value="Linux") -async def test_options_flow_linux(mock_system, hass, mock_bleak_scanner_start): - """Test options on Linux.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options={}, - unique_id="DOMAIN", - ) - entry.add_to_hass(hass) - - # Verify we can keep it as hci0 - with patch( - "bluetooth_adapters.get_bluetooth_adapters", return_value=["hci0", "hci1"] - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "init" - assert result["errors"] is None - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - CONF_ADAPTER: "hci0", - }, - ) - await hass.async_block_till_done() - - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["data"][CONF_ADAPTER] == "hci0" - - # Verify we can change it to hci1 - with patch( - "bluetooth_adapters.get_bluetooth_adapters", return_value=["hci0", "hci1"] - ): - await hass.async_block_till_done() - - result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "init" - assert result["errors"] is None - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - CONF_ADAPTER: "hci1", - }, - ) - await hass.async_block_till_done() - - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["data"][CONF_ADAPTER] == "hci1" - - -@patch("homeassistant.components.bluetooth.util.platform.system", return_value="Darwin") -async def test_options_flow_macos(mock_system, hass, mock_bleak_scanner_start): - """Test options on MacOS.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options={}, - unique_id="DOMAIN", - ) - entry.add_to_hass(hass) - - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "init" - assert result["errors"] is None - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - CONF_ADAPTER: MACOS_DEFAULT_BLUETOOTH_ADAPTER, - }, - ) - await hass.async_block_till_done() - - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["data"][CONF_ADAPTER] == MACOS_DEFAULT_BLUETOOTH_ADAPTER - - -@patch( - "homeassistant.components.bluetooth.util.platform.system", return_value="Windows" -) -async def test_options_flow_windows(mock_system, hass, mock_bleak_scanner_start): - """Test options on Windows.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options={}, - unique_id="DOMAIN", - ) - entry.add_to_hass(hass) - - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "no_adapters" diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 84c37300dc4..57fcb8402a0 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -19,6 +19,7 @@ from homeassistant.components.bluetooth import ( scanner, ) from homeassistant.components.bluetooth.const import ( + DEFAULT_ADDRESS, SOURCE_LOCAL, UNAVAILABLE_TRACK_SECONDS, ) @@ -28,7 +29,12 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from . import _get_manager, inject_advertisement, patch_discovered_devices +from . import ( + _get_manager, + async_setup_with_default_adapter, + inject_advertisement, + patch_discovered_devices, +) from tests.common import MockConfigEntry, async_fire_time_changed @@ -52,7 +58,7 @@ async def test_setup_and_stop(hass, mock_bleak_scanner_start, enable_bluetooth): assert len(mock_bleak_scanner_start.mock_calls) == 1 -async def test_setup_and_stop_no_bluetooth(hass, caplog): +async def test_setup_and_stop_no_bluetooth(hass, caplog, macos_adapter): """Test we fail gracefully when bluetooth is not available.""" mock_bt = [ {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"} @@ -63,10 +69,7 @@ async def test_setup_and_stop_no_bluetooth(hass, caplog): ) as mock_ha_bleak_scanner, patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -76,7 +79,7 @@ async def test_setup_and_stop_no_bluetooth(hass, caplog): assert "Failed to initialize Bluetooth" in caplog.text -async def test_setup_and_stop_broken_bluetooth(hass, caplog): +async def test_setup_and_stop_broken_bluetooth(hass, caplog, macos_adapter): """Test we fail gracefully when bluetooth/dbus is broken.""" mock_bt = [] with patch( @@ -85,10 +88,7 @@ async def test_setup_and_stop_broken_bluetooth(hass, caplog): ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -98,7 +98,7 @@ async def test_setup_and_stop_broken_bluetooth(hass, caplog): assert len(bluetooth.async_discovered_service_info(hass)) == 0 -async def test_setup_and_stop_broken_bluetooth_hanging(hass, caplog): +async def test_setup_and_stop_broken_bluetooth_hanging(hass, caplog, macos_adapter): """Test we fail gracefully when bluetooth/dbus is hanging.""" mock_bt = [] @@ -111,10 +111,7 @@ async def test_setup_and_stop_broken_bluetooth_hanging(hass, caplog): ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -123,7 +120,7 @@ async def test_setup_and_stop_broken_bluetooth_hanging(hass, caplog): assert "Timed out starting Bluetooth" in caplog.text -async def test_setup_and_retry_adapter_not_yet_available(hass, caplog): +async def test_setup_and_retry_adapter_not_yet_available(hass, caplog, macos_adapter): """Test we retry if the adapter is not yet available.""" mock_bt = [] with patch( @@ -132,10 +129,7 @@ async def test_setup_and_retry_adapter_not_yet_available(hass, caplog): ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -159,7 +153,7 @@ async def test_setup_and_retry_adapter_not_yet_available(hass, caplog): await hass.async_block_till_done() -async def test_no_race_during_manual_reload_in_retry_state(hass, caplog): +async def test_no_race_during_manual_reload_in_retry_state(hass, caplog, macos_adapter): """Test we can successfully reload when the entry is in a retry state.""" mock_bt = [] with patch( @@ -168,10 +162,7 @@ async def test_no_race_during_manual_reload_in_retry_state(hass, caplog): ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -196,7 +187,9 @@ async def test_no_race_during_manual_reload_in_retry_state(hass, caplog): await hass.async_block_till_done() -async def test_calling_async_discovered_devices_no_bluetooth(hass, caplog): +async def test_calling_async_discovered_devices_no_bluetooth( + hass, caplog, macos_adapter +): """Test we fail gracefully when asking for discovered devices and there is no blueooth.""" mock_bt = [] with patch( @@ -205,9 +198,7 @@ async def test_calling_async_discovered_devices_no_bluetooth(hass, caplog): ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) + await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -228,9 +219,7 @@ async def test_discovery_match_by_service_uuid( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) + await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -256,16 +245,15 @@ async def test_discovery_match_by_service_uuid( assert mock_config_flow.mock_calls[0][1][0] == "switchbot" -async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start): +async def test_discovery_match_by_local_name( + hass, mock_bleak_scanner_start, macos_adapter +): """Test bluetooth discovery match by local_name.""" mock_bt = [{"domain": "switchbot", "local_name": "wohand"}] with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -292,7 +280,7 @@ async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start): async def test_discovery_match_by_manufacturer_id_and_manufacturer_data_start( - hass, mock_bleak_scanner_start + hass, mock_bleak_scanner_start, macos_adapter ): """Test bluetooth discovery match by manufacturer_id and manufacturer_data_start.""" mock_bt = [ @@ -305,10 +293,7 @@ async def test_discovery_match_by_manufacturer_id_and_manufacturer_data_start( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -371,7 +356,7 @@ async def test_discovery_match_by_manufacturer_id_and_manufacturer_data_start( async def test_discovery_match_by_service_data_uuid_then_others( - hass, mock_bleak_scanner_start + hass, mock_bleak_scanner_start, macos_adapter ): """Test bluetooth discovery match by service_data_uuid and then other fields.""" mock_bt = [ @@ -391,10 +376,7 @@ async def test_discovery_match_by_service_data_uuid_then_others( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -526,7 +508,7 @@ async def test_discovery_match_by_service_data_uuid_then_others( async def test_discovery_match_first_by_service_uuid_and_then_manufacturer_id( - hass, mock_bleak_scanner_start + hass, mock_bleak_scanner_start, macos_adapter ): """Test bluetooth discovery matches twice for service_uuid and then manufacturer_id.""" mock_bt = [ @@ -542,10 +524,7 @@ async def test_discovery_match_first_by_service_uuid_and_then_manufacturer_id( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -600,9 +579,7 @@ async def test_rediscovery(hass, mock_bleak_scanner_start, enable_bluetooth): with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) + await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -631,7 +608,9 @@ async def test_rediscovery(hass, mock_bleak_scanner_start, enable_bluetooth): assert mock_config_flow.mock_calls[1][1][0] == "switchbot" -async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): +async def test_async_discovered_device_api( + hass, mock_bleak_scanner_start, macos_adapter +): """Test the async_discovered_device API.""" mock_bt = [] with patch( @@ -642,10 +621,7 @@ async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): ): assert not bluetooth.async_discovered_service_info(hass) assert not bluetooth.async_address_present(hass, "44:44:22:22:11:22") - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -738,9 +714,8 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start, enable_bluetoo with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ), patch.object(hass.config_entries.flow, "async_init"): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) + await async_setup_with_default_adapter(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -821,10 +796,7 @@ async def test_register_callback_by_address( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -913,10 +885,7 @@ async def test_register_callback_survives_reload( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -933,7 +902,7 @@ async def test_register_callback_survives_reload( switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") switchbot_adv = AdvertisementData( local_name="wohand", - service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + service_uuids=["zba20d00-224d-11e6-9fb8-0002a5d5c51b"], manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) @@ -1063,10 +1032,7 @@ async def test_wrapped_instance_with_filter( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -1132,10 +1098,7 @@ async def test_wrapped_instance_with_service_uuids( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -1184,10 +1147,7 @@ async def test_wrapped_instance_with_broken_callbacks( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] ), patch.object(hass.config_entries.flow, "async_init"): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -1231,10 +1191,7 @@ async def test_wrapped_instance_changes_uuids( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -1283,10 +1240,7 @@ async def test_wrapped_instance_changes_filters( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -1335,10 +1289,7 @@ async def test_wrapped_instance_unsupported_filter( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -1354,7 +1305,9 @@ async def test_wrapped_instance_unsupported_filter( assert "Only UUIDs filters are supported" in caplog.text -async def test_async_ble_device_from_address(hass, mock_bleak_scanner_start): +async def test_async_ble_device_from_address( + hass, mock_bleak_scanner_start, macos_adapter +): """Test the async_ble_device_from_address api.""" mock_bt = [] with patch( @@ -1369,9 +1322,8 @@ async def test_async_ble_device_from_address(hass, mock_bleak_scanner_start): bluetooth.async_ble_device_from_address(hass, "44:44:33:11:23:45") is None ) - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) + await async_setup_with_default_adapter(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -1394,26 +1346,14 @@ async def test_async_ble_device_from_address(hass, mock_bleak_scanner_start): ) -async def test_setup_without_bluetooth_in_configuration_yaml(hass, mock_bluetooth): - """Test setting up without bluetooth in configuration.yaml does not create the config entry.""" - assert await async_setup_component(hass, bluetooth.DOMAIN, {}) - await hass.async_block_till_done() - assert not hass.config_entries.async_entries(bluetooth.DOMAIN) - - -async def test_setup_with_bluetooth_in_configuration_yaml(hass, mock_bluetooth): - """Test setting up with bluetooth in configuration.yaml creates the config entry.""" - assert await async_setup_component(hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}) - await hass.async_block_till_done() - assert hass.config_entries.async_entries(bluetooth.DOMAIN) - - -async def test_can_unsetup_bluetooth(hass, mock_bleak_scanner_start, enable_bluetooth): +async def test_can_unsetup_bluetooth_single_adapter_macos( + hass, mock_bleak_scanner_start, enable_bluetooth, macos_adapter +): """Test we can setup and unsetup bluetooth.""" - entry = MockConfigEntry(domain=bluetooth.DOMAIN, data={}) + entry = MockConfigEntry(domain=bluetooth.DOMAIN, data={}, unique_id=DEFAULT_ADDRESS) entry.add_to_hass(hass) - for _ in range(2): + for _ in range(2): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -1421,35 +1361,80 @@ async def test_can_unsetup_bluetooth(hass, mock_bleak_scanner_start, enable_blue await hass.async_block_till_done() -async def test_auto_detect_bluetooth_adapters_linux(hass): +async def test_can_unsetup_bluetooth_single_adapter_linux( + hass, mock_bleak_scanner_start, enable_bluetooth, one_adapter +): + """Test we can setup and unsetup bluetooth.""" + entry = MockConfigEntry( + domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:01" + ) + entry.add_to_hass(hass) + + for _ in range(2): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_can_unsetup_bluetooth_multiple_adapters( + hass, mock_bleak_scanner_start, enable_bluetooth, two_adapters +): + """Test we can setup and unsetup bluetooth with multiple adapters.""" + entry1 = MockConfigEntry( + domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:01" + ) + entry1.add_to_hass(hass) + + entry2 = MockConfigEntry( + domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:02" + ) + entry2.add_to_hass(hass) + + for _ in range(2): + for entry in (entry1, entry2): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_three_adapters_one_missing( + hass, mock_bleak_scanner_start, enable_bluetooth, two_adapters +): + """Test three adapters but one is missing results in a retry on setup.""" + entry = MockConfigEntry( + domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:03" + ) + entry.add_to_hass(hass) + assert not await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_auto_detect_bluetooth_adapters_linux(hass, one_adapter): """Test we auto detect bluetooth adapters on linux.""" - with patch( - "bluetooth_adapters.get_bluetooth_adapters", return_value=["hci0"] - ), patch( - "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" - ): - assert await async_setup_component(hass, bluetooth.DOMAIN, {}) - await hass.async_block_till_done() + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() assert not hass.config_entries.async_entries(bluetooth.DOMAIN) assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 1 -async def test_auto_detect_bluetooth_adapters_linux_multiple(hass): +async def test_auto_detect_bluetooth_adapters_linux_multiple(hass, two_adapters): """Test we auto detect bluetooth adapters on linux with multiple adapters.""" - with patch( - "bluetooth_adapters.get_bluetooth_adapters", return_value=["hci1", "hci0"] - ), patch( - "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" - ): - assert await async_setup_component(hass, bluetooth.DOMAIN, {}) - await hass.async_block_till_done() + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() assert not hass.config_entries.async_entries(bluetooth.DOMAIN) - assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 1 + assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 2 async def test_auto_detect_bluetooth_adapters_linux_none_found(hass): """Test we auto detect bluetooth adapters on linux with no adapters found.""" - with patch("bluetooth_adapters.get_bluetooth_adapters", return_value=set()), patch( + with patch( + "bluetooth_adapters.get_bluetooth_adapter_details", return_value={} + ), patch( "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" ): assert await async_setup_component(hass, bluetooth.DOMAIN, {}) @@ -1485,3 +1470,23 @@ async def test_getting_the_scanner_returns_the_wrapped_instance(hass, enable_blu """Test getting the scanner returns the wrapped instance.""" scanner = bluetooth.async_get_scanner(hass) assert isinstance(scanner, models.HaBleakScannerWrapper) + + +async def test_migrate_single_entry_macos( + hass, mock_bleak_scanner_start, macos_adapter +): + """Test we can migrate a single entry on MacOS.""" + entry = MockConfigEntry(domain=bluetooth.DOMAIN, data={}) + entry.add_to_hass(hass) + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + assert entry.unique_id == DEFAULT_ADDRESS + + +async def test_migrate_single_entry_linux(hass, mock_bleak_scanner_start, one_adapter): + """Test we can migrate a single entry on Linux.""" + entry = MockConfigEntry(domain=bluetooth.DOMAIN, data={}) + entry.add_to_hass(hass) + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + assert entry.unique_id == "00:00:00:00:00:01" diff --git a/tests/components/bluetooth/test_scanner.py b/tests/components/bluetooth/test_scanner.py index 032b67662df..bde1dbd1696 100644 --- a/tests/components/bluetooth/test_scanner.py +++ b/tests/components/bluetooth/test_scanner.py @@ -11,23 +11,20 @@ from dbus_next import InvalidMessageError from homeassistant.components import bluetooth from homeassistant.components.bluetooth.const import ( - CONF_ADAPTER, SCANNER_WATCHDOG_INTERVAL, SCANNER_WATCHDOG_TIMEOUT, - UNIX_DEFAULT_BLUETOOTH_ADAPTER, ) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP -from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from . import _get_manager +from . import _get_manager, async_setup_with_one_adapter -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import async_fire_time_changed async def test_config_entry_can_be_reloaded_when_stop_raises( - hass, caplog, enable_bluetooth + hass, caplog, enable_bluetooth, macos_adapter ): """Test we can reload if stopping the scanner raises.""" entry = hass.config_entries.async_entries(bluetooth.DOMAIN)[0] @@ -44,31 +41,7 @@ async def test_config_entry_can_be_reloaded_when_stop_raises( assert "Error stopping scanner" in caplog.text -async def test_changing_the_adapter_at_runtime(hass): - """Test we can change the adapter at runtime.""" - entry = MockConfigEntry( - domain=bluetooth.DOMAIN, - data={}, - options={CONF_ADAPTER: UNIX_DEFAULT_BLUETOOTH_ADAPTER}, - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start" - ), patch("homeassistant.components.bluetooth.scanner.OriginalBleakScanner.stop"): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - entry.options = {CONF_ADAPTER: "hci1"} - - await hass.config_entries.async_reload(entry.entry_id) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() - - -async def test_dbus_socket_missing_in_container(hass, caplog): +async def test_dbus_socket_missing_in_container(hass, caplog, one_adapter): """Test we handle dbus being missing in the container.""" with patch( @@ -77,10 +50,8 @@ async def test_dbus_socket_missing_in_container(hass, caplog): "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", side_effect=FileNotFoundError, ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_one_adapter(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -90,7 +61,7 @@ async def test_dbus_socket_missing_in_container(hass, caplog): assert "docker" in caplog.text -async def test_dbus_socket_missing(hass, caplog): +async def test_dbus_socket_missing(hass, caplog, one_adapter): """Test we handle dbus being missing.""" with patch( @@ -99,10 +70,8 @@ async def test_dbus_socket_missing(hass, caplog): "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", side_effect=FileNotFoundError, ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_one_adapter(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -112,7 +81,7 @@ async def test_dbus_socket_missing(hass, caplog): assert "docker" not in caplog.text -async def test_dbus_broken_pipe_in_container(hass, caplog): +async def test_dbus_broken_pipe_in_container(hass, caplog, one_adapter): """Test we handle dbus broken pipe in the container.""" with patch( @@ -121,10 +90,8 @@ async def test_dbus_broken_pipe_in_container(hass, caplog): "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", side_effect=BrokenPipeError, ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_one_adapter(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -135,7 +102,7 @@ async def test_dbus_broken_pipe_in_container(hass, caplog): assert "container" in caplog.text -async def test_dbus_broken_pipe(hass, caplog): +async def test_dbus_broken_pipe(hass, caplog, one_adapter): """Test we handle dbus broken pipe.""" with patch( @@ -144,10 +111,8 @@ async def test_dbus_broken_pipe(hass, caplog): "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", side_effect=BrokenPipeError, ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_one_adapter(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -158,17 +123,15 @@ async def test_dbus_broken_pipe(hass, caplog): assert "container" not in caplog.text -async def test_invalid_dbus_message(hass, caplog): +async def test_invalid_dbus_message(hass, caplog, one_adapter): """Test we handle invalid dbus message.""" with patch( "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", side_effect=InvalidMessageError, ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_one_adapter(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -177,7 +140,7 @@ async def test_invalid_dbus_message(hass, caplog): assert "dbus" in caplog.text -async def test_recovery_from_dbus_restart(hass): +async def test_recovery_from_dbus_restart(hass, one_adapter): """Test we can recover when DBus gets restarted out from under us.""" called_start = 0 @@ -213,10 +176,8 @@ async def test_recovery_from_dbus_restart(hass): "homeassistant.components.bluetooth.scanner.OriginalBleakScanner", return_value=scanner, ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_one_adapter(hass) + assert called_start == 1 start_time_monotonic = 1000 diff --git a/tests/conftest.py b/tests/conftest.py index 4c268206805..0c0a654059b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -876,7 +876,7 @@ async def mock_enable_bluetooth( hass, mock_bleak_scanner_start, mock_bluetooth_adapters ): """Fixture to mock starting the bleak scanner.""" - entry = MockConfigEntry(domain="bluetooth") + entry = MockConfigEntry(domain="bluetooth", unique_id="00:00:00:00:00:01") entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -885,7 +885,20 @@ async def mock_enable_bluetooth( @pytest.fixture(name="mock_bluetooth_adapters") def mock_bluetooth_adapters(): """Fixture to mock bluetooth adapters.""" - with patch("bluetooth_adapters.get_bluetooth_adapters", return_value=[]): + with patch( + "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" + ), patch( + "bluetooth_adapters.get_bluetooth_adapter_details", + return_value={ + "hci0": { + "org.bluez.Adapter1": { + "Address": "00:00:00:00:00:01", + "Name": "BlueZ 4.63", + "Modalias": "usbid:1234", + } + }, + }, + ): yield