Add support for multiple Bluetooth adapters (#76963)

This commit is contained in:
J. Nick Koston 2022-08-18 15:41:07 -10:00 committed by GitHub
parent a434d755b3
commit cd59d3ab81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 738 additions and 489 deletions

View File

@ -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:

View File

@ -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()

View File

@ -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"

View File

@ -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:

View File

@ -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"

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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"

View File

@ -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

View File

@ -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