mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 03:07:37 +00:00
Add support for multiple Bluetooth adapters (#76963)
This commit is contained in:
parent
a434d755b3
commit
cd59d3ab81
@ -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:
|
||||
|
@ -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()
|
||||
|
@ -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"
|
||||
|
@ -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:
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user