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 asyncio import Future
from collections.abc import Callable from collections.abc import Callable
import platform
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import async_timeout import async_timeout
@ -11,11 +12,22 @@ from homeassistant import config_entries
from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant, callback as hass_callback from homeassistant.core import HomeAssistant, callback as hass_callback
from homeassistant.exceptions import ConfigEntryNotReady 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 homeassistant.loader import async_get_bluetooth
from . import models 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 .manager import BluetoothManager
from .match import BluetoothCallbackMatcher, IntegrationMatcher from .match import BluetoothCallbackMatcher, IntegrationMatcher
from .models import ( from .models import (
@ -28,7 +40,7 @@ from .models import (
ProcessAdvertisementCallback, ProcessAdvertisementCallback,
) )
from .scanner import HaScanner, create_bleak_scanner 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: if TYPE_CHECKING:
from bleak.backends.device import BLEDevice from bleak.backends.device import BLEDevice
@ -164,37 +176,88 @@ def async_rediscover_address(hass: HomeAssistant, address: str) -> None:
manager.async_rediscover_address(address) 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: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the bluetooth integration.""" """Set up the bluetooth integration."""
integration_matcher = IntegrationMatcher(await async_get_bluetooth(hass)) integration_matcher = IntegrationMatcher(await async_get_bluetooth(hass))
manager = BluetoothManager(hass, integration_matcher) manager = BluetoothManager(hass, integration_matcher)
manager.async_setup() manager.async_setup()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, manager.async_stop) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, manager.async_stop)
hass.data[DATA_MANAGER] = models.MANAGER = manager hass.data[DATA_MANAGER] = models.MANAGER = manager
# The config entry is responsible for starting the manager
# if its enabled
if hass.config_entries.async_entries(DOMAIN): adapters = await manager.async_get_bluetooth_adapters()
return True
if DOMAIN in config: async_migrate_entries(hass, adapters)
hass.async_create_task( await async_discover_adapters(hass, adapters)
hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={} 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( discovery_flow.async_create_flow(
hass, hass,
DOMAIN, DOMAIN,
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, 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( async def async_setup_entry(
@ -202,7 +265,12 @@ async def async_setup_entry(
) -> bool: ) -> bool:
"""Set up a config entry for a bluetooth scanner.""" """Set up a config entry for a bluetooth scanner."""
manager: BluetoothManager = hass.data[DATA_MANAGER] 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: try:
bleak_scanner = create_bleak_scanner(BluetoothScanningMode.ACTIVE, adapter) bleak_scanner = create_bleak_scanner(BluetoothScanningMode.ACTIVE, adapter)
except RuntimeError as err: 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)) entry.async_on_unload(scanner.async_register_callback(manager.scanner_adv_received))
await scanner.async_start() await scanner.async_start()
entry.async_on_unload(manager.async_register_scanner(scanner)) 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 hass.data.setdefault(DOMAIN, {})[entry.entry_id] = scanner
return True 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( async def async_unload_entry(
hass: HomeAssistant, entry: config_entries.ConfigEntry hass: HomeAssistant, entry: config_entries.ConfigEntry
) -> bool: ) -> bool:

View File

@ -1,16 +1,16 @@
"""Config flow to configure the Bluetooth integration.""" """Config flow to configure the Bluetooth integration."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any, cast
import voluptuous as vol import voluptuous as vol
from homeassistant.components import onboarding from homeassistant.components import onboarding
from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.config_entries import ConfigFlow
from homeassistant.core import callback from homeassistant.helpers.typing import DiscoveryInfoType
from .const import CONF_ADAPTER, DEFAULT_NAME, DOMAIN from .const import ADAPTER_ADDRESS, CONF_ADAPTER, CONF_DETAILS, DOMAIN, AdapterDetails
from .util import async_get_bluetooth_adapters from .util import adapter_human_name, adapter_unique_name, async_get_bluetooth_adapters
if TYPE_CHECKING: if TYPE_CHECKING:
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
@ -21,60 +21,94 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1 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( async def async_step_user(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> FlowResult: ) -> FlowResult:
"""Handle a flow initialized by the user.""" """Handle a flow initialized by the user."""
return await self.async_step_enable_bluetooth() return await self.async_step_multiple_adapters()
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)

View File

@ -2,18 +2,27 @@
from __future__ import annotations from __future__ import annotations
from datetime import timedelta from datetime import timedelta
from typing import Final from typing import Final, TypedDict
DOMAIN = "bluetooth" DOMAIN = "bluetooth"
DEFAULT_NAME = "Bluetooth"
CONF_ADAPTER = "adapter" 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" UNIX_DEFAULT_BLUETOOTH_ADAPTER = "hci0"
DEFAULT_ADAPTERS = {MACOS_DEFAULT_BLUETOOTH_ADAPTER, UNIX_DEFAULT_BLUETOOTH_ADAPTER} 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" SOURCE_LOCAL: Final = "local"
DATA_MANAGER: Final = "bluetooth_manager" DATA_MANAGER: Final = "bluetooth_manager"
@ -22,3 +31,16 @@ UNAVAILABLE_TRACK_SECONDS: Final = 60 * 5
START_TIMEOUT = 12 START_TIMEOUT = 12
SCANNER_WATCHDOG_TIMEOUT: Final = 60 * 5 SCANNER_WATCHDOG_TIMEOUT: Final = 60 * 5
SCANNER_WATCHDOG_INTERVAL: Final = timedelta(seconds=SCANNER_WATCHDOG_TIMEOUT) 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 import discovery_flow
from homeassistant.helpers.event import async_track_time_interval 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 ( from .match import (
ADDRESS, ADDRESS,
BluetoothCallbackMatcher, BluetoothCallbackMatcher,
@ -29,6 +34,7 @@ from .match import (
) )
from .models import BluetoothCallback, BluetoothChange, BluetoothServiceInfoBleak from .models import BluetoothCallback, BluetoothChange, BluetoothServiceInfoBleak
from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher
from .util import async_get_bluetooth_adapters
if TYPE_CHECKING: if TYPE_CHECKING:
from bleak.backends.device import BLEDevice from bleak.backends.device import BLEDevice
@ -39,7 +45,7 @@ if TYPE_CHECKING:
FILTER_UUIDS: Final = "UUIDs" FILTER_UUIDS: Final = "UUIDs"
RSSI_SWITCH_THRESHOLD = 10 RSSI_SWITCH_THRESHOLD = 6
STALE_ADVERTISEMENT_SECONDS = 180 STALE_ADVERTISEMENT_SECONDS = 180
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -132,6 +138,26 @@ class BluetoothManager:
] = [] ] = []
self.history: dict[str, AdvertisementHistory] = {} self.history: dict[str, AdvertisementHistory] = {}
self._scanners: list[HaScanner] = [] 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 @hass_callback
def async_setup(self) -> None: def async_setup(self) -> None:

View File

@ -4,7 +4,7 @@
"documentation": "https://www.home-assistant.io/integrations/bluetooth", "documentation": "https://www.home-assistant.io/integrations/bluetooth",
"dependencies": ["websocket_api"], "dependencies": ["websocket_api"],
"quality_scale": "internal", "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"], "codeowners": ["@bdraco"],
"config_flow": true, "config_flow": true,
"iot_class": "local_push" "iot_class": "local_push"

View File

@ -2,9 +2,6 @@
"config": { "config": {
"flow_title": "{name}", "flow_title": "{name}",
"step": { "step": {
"enable_bluetooth": {
"description": "Do you want to setup Bluetooth?"
},
"user": { "user": {
"description": "Choose a device to setup", "description": "Choose a device to setup",
"data": { "data": {
@ -13,20 +10,20 @@
}, },
"bluetooth_confirm": { "bluetooth_confirm": {
"description": "Do you want to setup {name}?" "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": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]", "already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"no_adapters": "No Bluetooth adapters found" "no_adapters": "No unconfigured Bluetooth adapters found"
}
},
"options": {
"step": {
"init": {
"data": {
"adapter": "The Bluetooth Adapter to use for scanning"
}
}
} }
} }
} }

View File

@ -2,15 +2,21 @@
"config": { "config": {
"abort": { "abort": {
"already_configured": "Service is already configured", "already_configured": "Service is already configured",
"no_adapters": "No Bluetooth adapters found" "no_adapters": "No unconfigured Bluetooth adapters found"
}, },
"flow_title": "{name}", "flow_title": "{name}",
"step": { "step": {
"bluetooth_confirm": { "bluetooth_confirm": {
"description": "Do you want to setup {name}?" "description": "Do you want to setup {name}?"
}, },
"enable_bluetooth": { "multiple_adapters": {
"description": "Do you want to setup Bluetooth?" "data": {
"adapter": "Adapter"
},
"description": "Select a Bluetooth adapter to setup"
},
"single_adapter": {
"description": "Do you want to setup the Bluetooth adapter {name}?"
}, },
"user": { "user": {
"data": { "data": {
@ -19,14 +25,5 @@
"description": "Choose a device to setup" "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 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.""" """Return a list of bluetooth adapters."""
if platform.system() == "Windows": # We don't have a good way to detect on windows if platform.system() == "Windows":
return [] return {
if platform.system() == "Darwin": # CoreBluetooth is built in on MacOS hardware WINDOWS_DEFAULT_BLUETOOTH_ADAPTER: AdapterDetails(
return [MACOS_DEFAULT_BLUETOOTH_ADAPTER] 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 from bluetooth_adapters import ( # pylint: disable=import-outside-toplevel
get_bluetooth_adapters, get_bluetooth_adapter_details,
) )
adapters = await get_bluetooth_adapters() adapters: dict[str, AdapterDetails] = {}
if ( adapter_details = await get_bluetooth_adapter_details()
UNIX_DEFAULT_BLUETOOTH_ADAPTER in adapters for adapter, details in adapter_details.items():
and adapters[0] != UNIX_DEFAULT_BLUETOOTH_ADAPTER adapter1 = details["org.bluez.Adapter1"]
): adapters[adapter] = AdapterDetails(
# The default adapter always needs to be the first in the list address=adapter1["Address"],
# because that is how bleak works. sw_version=adapter1["Name"], # This is actually the BlueZ version
adapters.insert(0, adapters.pop(adapters.index(UNIX_DEFAULT_BLUETOOTH_ADAPTER))) hw_version=adapter1["Modalias"],
)
return adapters 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 awesomeversion==22.6.0
bcrypt==3.1.7 bcrypt==3.1.7
bleak==0.15.1 bleak==0.15.1
bluetooth-adapters==0.1.3 bluetooth-adapters==0.2.0
certifi>=2021.5.30 certifi>=2021.5.30
ciso8601==2.2.0 ciso8601==2.2.0
cryptography==37.0.4 cryptography==37.0.4

View File

@ -424,7 +424,7 @@ blockchain==1.4.4
# bluepy==1.3.0 # bluepy==1.3.0
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bluetooth-adapters==0.1.3 bluetooth-adapters==0.2.0
# homeassistant.components.bond # homeassistant.components.bond
bond-async==0.1.22 bond-async==0.1.22

View File

@ -335,7 +335,7 @@ blebox_uniapi==2.0.2
blinkpy==0.19.0 blinkpy==0.19.0
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bluetooth-adapters==0.1.3 bluetooth-adapters==0.2.0
# homeassistant.components.bond # homeassistant.components.bond
bond-async==0.1.22 bond-async==0.1.22

View File

@ -6,8 +6,13 @@ from unittest.mock import patch
from bleak.backends.scanner import AdvertisementData, BLEDevice 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.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: def _get_manager() -> BluetoothManager:
@ -48,3 +53,24 @@ def patch_discovered_devices(mock_discovered: list[BLEDevice]) -> None:
return patch.object( return patch.object(
manager, "async_discovered_devices", return_value=mock_discovered 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.""" """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 import config_entries
from homeassistant.components.bluetooth.const import ( from homeassistant.components.bluetooth.const import (
CONF_ADAPTER, CONF_ADAPTER,
CONF_DETAILS,
DEFAULT_ADDRESS,
DOMAIN, DOMAIN,
MACOS_DEFAULT_BLUETOOTH_ADAPTER, AdapterDetails,
) )
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
async def test_async_step_user(hass): async def test_async_step_user_macos(hass, macos_adapter):
"""Test setting up manually.""" """Test setting up manually with one adapter on MacOS."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": config_entries.SOURCE_USER}, context={"source": config_entries.SOURCE_USER},
data={}, data={},
) )
assert result["type"] == FlowResultType.FORM assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "enable_bluetooth" assert result["step_id"] == "single_adapter"
with patch( with patch(
"homeassistant.components.bluetooth.async_setup", return_value=True
), patch(
"homeassistant.components.bluetooth.async_setup_entry", return_value=True "homeassistant.components.bluetooth.async_setup_entry", return_value=True
) as mock_setup_entry: ) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={} result["flow_id"], user_input={}
) )
assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["type"] == FlowResultType.CREATE_ENTRY
assert result2["title"] == "Bluetooth" assert result2["title"] == "Core Bluetooth"
assert result2["data"] == {} assert result2["data"] == {}
assert len(mock_setup_entry.mock_calls) == 1 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.""" """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) entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
@ -44,34 +94,48 @@ async def test_async_step_user_only_allows_one(hass):
data={}, data={},
) )
assert result["type"] == FlowResultType.ABORT assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "already_configured" assert result["reason"] == "no_adapters"
async def test_async_step_integration_discovery(hass): async def test_async_step_integration_discovery(hass):
"""Test setting up from integration discovery.""" """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( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
data={}, data={CONF_ADAPTER: "hci0", CONF_DETAILS: details},
) )
assert result["type"] == FlowResultType.FORM assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "enable_bluetooth" assert result["step_id"] == "single_adapter"
with patch( with patch(
"homeassistant.components.bluetooth.async_setup", return_value=True
), patch(
"homeassistant.components.bluetooth.async_setup_entry", return_value=True "homeassistant.components.bluetooth.async_setup_entry", return_value=True
) as mock_setup_entry: ) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={} result["flow_id"], user_input={}
) )
assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["type"] == FlowResultType.CREATE_ENTRY
assert result2["title"] == "Bluetooth" assert result2["title"] == "00:00:00:00:00:01"
assert result2["data"] == {} assert result2["data"] == {}
assert len(mock_setup_entry.mock_calls) == 1 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.""" """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( with patch(
"homeassistant.components.bluetooth.async_setup", return_value=True
), patch(
"homeassistant.components.bluetooth.async_setup_entry", return_value=True "homeassistant.components.bluetooth.async_setup_entry", return_value=True
) as mock_setup_entry, patch( ) as mock_setup_entry, patch(
"homeassistant.components.onboarding.async_is_onboarded", "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( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
data={}, data={CONF_ADAPTER: "hci0", CONF_DETAILS: details},
) )
assert result["type"] == FlowResultType.CREATE_ENTRY 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 result["data"] == {}
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
assert len(mock_onboarding.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): async def test_async_step_integration_discovery_already_exists(hass):
"""Test setting up from integration discovery when an entry already exists.""" """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) entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
data={}, data={CONF_ADAPTER: "hci0", CONF_DETAILS: details},
) )
assert result["type"] == FlowResultType.ABORT assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "already_configured" 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, scanner,
) )
from homeassistant.components.bluetooth.const import ( from homeassistant.components.bluetooth.const import (
DEFAULT_ADDRESS,
SOURCE_LOCAL, SOURCE_LOCAL,
UNAVAILABLE_TRACK_SECONDS, UNAVAILABLE_TRACK_SECONDS,
) )
@ -28,7 +29,12 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util 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 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 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.""" """Test we fail gracefully when bluetooth is not available."""
mock_bt = [ mock_bt = [
{"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"} {"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( ) as mock_ha_bleak_scanner, patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
): ):
assert await async_setup_component( await async_setup_with_default_adapter(hass)
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() 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 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.""" """Test we fail gracefully when bluetooth/dbus is broken."""
mock_bt = [] mock_bt = []
with patch( with patch(
@ -85,10 +88,7 @@ async def test_setup_and_stop_broken_bluetooth(hass, caplog):
), patch( ), patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
): ):
assert await async_setup_component( await async_setup_with_default_adapter(hass)
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() 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 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.""" """Test we fail gracefully when bluetooth/dbus is hanging."""
mock_bt = [] mock_bt = []
@ -111,10 +111,7 @@ async def test_setup_and_stop_broken_bluetooth_hanging(hass, caplog):
), patch( ), patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
): ):
assert await async_setup_component( await async_setup_with_default_adapter(hass)
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() 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 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.""" """Test we retry if the adapter is not yet available."""
mock_bt = [] mock_bt = []
with patch( with patch(
@ -132,10 +129,7 @@ async def test_setup_and_retry_adapter_not_yet_available(hass, caplog):
), patch( ), patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
): ):
assert await async_setup_component( await async_setup_with_default_adapter(hass)
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() 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() 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.""" """Test we can successfully reload when the entry is in a retry state."""
mock_bt = [] mock_bt = []
with patch( with patch(
@ -168,10 +162,7 @@ async def test_no_race_during_manual_reload_in_retry_state(hass, caplog):
), patch( ), patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
): ):
assert await async_setup_component( await async_setup_with_default_adapter(hass)
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() 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() 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.""" """Test we fail gracefully when asking for discovered devices and there is no blueooth."""
mock_bt = [] mock_bt = []
with patch( with patch(
@ -205,9 +198,7 @@ async def test_calling_async_discovered_devices_no_bluetooth(hass, caplog):
), patch( ), patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
): ):
assert await async_setup_component( await async_setup_with_default_adapter(hass)
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -228,9 +219,7 @@ async def test_discovery_match_by_service_uuid(
with patch( with patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow:
assert await async_setup_component( await async_setup_with_default_adapter(hass)
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() 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" 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.""" """Test bluetooth discovery match by local_name."""
mock_bt = [{"domain": "switchbot", "local_name": "wohand"}] mock_bt = [{"domain": "switchbot", "local_name": "wohand"}]
with patch( with patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
): ):
assert await async_setup_component( await async_setup_with_default_adapter(hass)
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
await hass.async_block_till_done()
with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow:
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) 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( 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.""" """Test bluetooth discovery match by manufacturer_id and manufacturer_data_start."""
mock_bt = [ mock_bt = [
@ -305,10 +293,7 @@ async def test_discovery_match_by_manufacturer_id_and_manufacturer_data_start(
with patch( with patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
): ):
assert await async_setup_component( await async_setup_with_default_adapter(hass)
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
await hass.async_block_till_done()
with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow:
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) 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( 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.""" """Test bluetooth discovery match by service_data_uuid and then other fields."""
mock_bt = [ mock_bt = [
@ -391,10 +376,7 @@ async def test_discovery_match_by_service_data_uuid_then_others(
with patch( with patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
): ):
assert await async_setup_component( await async_setup_with_default_adapter(hass)
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
await hass.async_block_till_done()
with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow:
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) 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( 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.""" """Test bluetooth discovery matches twice for service_uuid and then manufacturer_id."""
mock_bt = [ mock_bt = [
@ -542,10 +524,7 @@ async def test_discovery_match_first_by_service_uuid_and_then_manufacturer_id(
with patch( with patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
): ):
assert await async_setup_component( await async_setup_with_default_adapter(hass)
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
await hass.async_block_till_done()
with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow:
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
@ -600,9 +579,7 @@ async def test_rediscovery(hass, mock_bleak_scanner_start, enable_bluetooth):
with patch( with patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow:
assert await async_setup_component( await async_setup_with_default_adapter(hass)
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() 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" 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.""" """Test the async_discovered_device API."""
mock_bt = [] mock_bt = []
with patch( 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_discovered_service_info(hass)
assert not bluetooth.async_address_present(hass, "44:44:22:22:11:22") assert not bluetooth.async_address_present(hass, "44:44:22:22:11:22")
assert await async_setup_component( await async_setup_with_default_adapter(hass)
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
await hass.async_block_till_done()
with patch.object(hass.config_entries.flow, "async_init"): with patch.object(hass.config_entries.flow, "async_init"):
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) 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( with patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
), patch.object(hass.config_entries.flow, "async_init"): ), patch.object(hass.config_entries.flow, "async_init"):
assert await async_setup_component( await async_setup_with_default_adapter(hass)
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -821,10 +796,7 @@ async def test_register_callback_by_address(
with patch( with patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
): ):
assert await async_setup_component( await async_setup_with_default_adapter(hass)
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
await hass.async_block_till_done()
with patch.object(hass.config_entries.flow, "async_init"): with patch.object(hass.config_entries.flow, "async_init"):
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
@ -913,10 +885,7 @@ async def test_register_callback_survives_reload(
with patch( with patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
): ):
assert await async_setup_component( await async_setup_with_default_adapter(hass)
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() 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_device = BLEDevice("44:44:33:11:23:45", "wohand")
switchbot_adv = AdvertisementData( switchbot_adv = AdvertisementData(
local_name="wohand", 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"}, manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
) )
@ -1063,10 +1032,7 @@ async def test_wrapped_instance_with_filter(
with patch( with patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[]
): ):
assert await async_setup_component( await async_setup_with_default_adapter(hass)
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
await hass.async_block_till_done()
with patch.object(hass.config_entries.flow, "async_init"): with patch.object(hass.config_entries.flow, "async_init"):
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
@ -1132,10 +1098,7 @@ async def test_wrapped_instance_with_service_uuids(
with patch( with patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[]
): ):
assert await async_setup_component( await async_setup_with_default_adapter(hass)
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
await hass.async_block_till_done()
with patch.object(hass.config_entries.flow, "async_init"): with patch.object(hass.config_entries.flow, "async_init"):
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
@ -1184,10 +1147,7 @@ async def test_wrapped_instance_with_broken_callbacks(
with patch( with patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[]
), patch.object(hass.config_entries.flow, "async_init"): ), patch.object(hass.config_entries.flow, "async_init"):
assert await async_setup_component( await async_setup_with_default_adapter(hass)
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
await hass.async_block_till_done()
with patch.object(hass.config_entries.flow, "async_init"): with patch.object(hass.config_entries.flow, "async_init"):
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
@ -1231,10 +1191,7 @@ async def test_wrapped_instance_changes_uuids(
with patch( with patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[]
): ):
assert await async_setup_component( await async_setup_with_default_adapter(hass)
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
await hass.async_block_till_done()
with patch.object(hass.config_entries.flow, "async_init"): with patch.object(hass.config_entries.flow, "async_init"):
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
@ -1283,10 +1240,7 @@ async def test_wrapped_instance_changes_filters(
with patch( with patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[]
): ):
assert await async_setup_component( await async_setup_with_default_adapter(hass)
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
await hass.async_block_till_done()
with patch.object(hass.config_entries.flow, "async_init"): with patch.object(hass.config_entries.flow, "async_init"):
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
@ -1335,10 +1289,7 @@ async def test_wrapped_instance_unsupported_filter(
with patch( with patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[]
): ):
assert await async_setup_component( await async_setup_with_default_adapter(hass)
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
await hass.async_block_till_done()
with patch.object(hass.config_entries.flow, "async_init"): with patch.object(hass.config_entries.flow, "async_init"):
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) 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 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.""" """Test the async_ble_device_from_address api."""
mock_bt = [] mock_bt = []
with patch( 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 bluetooth.async_ble_device_from_address(hass, "44:44:33:11:23:45") is None
) )
assert await async_setup_component( await async_setup_with_default_adapter(hass)
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() 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): async def test_can_unsetup_bluetooth_single_adapter_macos(
"""Test setting up without bluetooth in configuration.yaml does not create the config entry.""" hass, mock_bleak_scanner_start, enable_bluetooth, macos_adapter
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):
"""Test we can setup and unsetup bluetooth.""" """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) entry.add_to_hass(hass)
for _ in range(2):
for _ in range(2):
assert await hass.config_entries.async_setup(entry.entry_id) assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done() 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() 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.""" """Test we auto detect bluetooth adapters on linux."""
with patch( assert await async_setup_component(hass, bluetooth.DOMAIN, {})
"bluetooth_adapters.get_bluetooth_adapters", return_value=["hci0"] await hass.async_block_till_done()
), 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 not hass.config_entries.async_entries(bluetooth.DOMAIN) 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)) == 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.""" """Test we auto detect bluetooth adapters on linux with multiple adapters."""
with patch( assert await async_setup_component(hass, bluetooth.DOMAIN, {})
"bluetooth_adapters.get_bluetooth_adapters", return_value=["hci1", "hci0"] await hass.async_block_till_done()
), 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 not hass.config_entries.async_entries(bluetooth.DOMAIN) 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): async def test_auto_detect_bluetooth_adapters_linux_none_found(hass):
"""Test we auto detect bluetooth adapters on linux with no adapters found.""" """Test we auto detect bluetooth adapters on linux with no adapters found."""
with patch("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" "homeassistant.components.bluetooth.util.platform.system", return_value="Linux"
): ):
assert await async_setup_component(hass, bluetooth.DOMAIN, {}) 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.""" """Test getting the scanner returns the wrapped instance."""
scanner = bluetooth.async_get_scanner(hass) scanner = bluetooth.async_get_scanner(hass)
assert isinstance(scanner, models.HaBleakScannerWrapper) 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 import bluetooth
from homeassistant.components.bluetooth.const import ( from homeassistant.components.bluetooth.const import (
CONF_ADAPTER,
SCANNER_WATCHDOG_INTERVAL, SCANNER_WATCHDOG_INTERVAL,
SCANNER_WATCHDOG_TIMEOUT, SCANNER_WATCHDOG_TIMEOUT,
UNIX_DEFAULT_BLUETOOTH_ADAPTER,
) )
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP 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 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( 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.""" """Test we can reload if stopping the scanner raises."""
entry = hass.config_entries.async_entries(bluetooth.DOMAIN)[0] 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 assert "Error stopping scanner" in caplog.text
async def test_changing_the_adapter_at_runtime(hass): async def test_dbus_socket_missing_in_container(hass, caplog, one_adapter):
"""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):
"""Test we handle dbus being missing in the container.""" """Test we handle dbus being missing in the container."""
with patch( with patch(
@ -77,10 +50,8 @@ async def test_dbus_socket_missing_in_container(hass, caplog):
"homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start",
side_effect=FileNotFoundError, side_effect=FileNotFoundError,
): ):
assert await async_setup_component( await async_setup_with_one_adapter(hass)
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() 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 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.""" """Test we handle dbus being missing."""
with patch( with patch(
@ -99,10 +70,8 @@ async def test_dbus_socket_missing(hass, caplog):
"homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start",
side_effect=FileNotFoundError, side_effect=FileNotFoundError,
): ):
assert await async_setup_component( await async_setup_with_one_adapter(hass)
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -112,7 +81,7 @@ async def test_dbus_socket_missing(hass, caplog):
assert "docker" not in caplog.text 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.""" """Test we handle dbus broken pipe in the container."""
with patch( with patch(
@ -121,10 +90,8 @@ async def test_dbus_broken_pipe_in_container(hass, caplog):
"homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start",
side_effect=BrokenPipeError, side_effect=BrokenPipeError,
): ):
assert await async_setup_component( await async_setup_with_one_adapter(hass)
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() 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 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.""" """Test we handle dbus broken pipe."""
with patch( with patch(
@ -144,10 +111,8 @@ async def test_dbus_broken_pipe(hass, caplog):
"homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start",
side_effect=BrokenPipeError, side_effect=BrokenPipeError,
): ):
assert await async_setup_component( await async_setup_with_one_adapter(hass)
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -158,17 +123,15 @@ async def test_dbus_broken_pipe(hass, caplog):
assert "container" not in caplog.text 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.""" """Test we handle invalid dbus message."""
with patch( with patch(
"homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start",
side_effect=InvalidMessageError, side_effect=InvalidMessageError,
): ):
assert await async_setup_component( await async_setup_with_one_adapter(hass)
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -177,7 +140,7 @@ async def test_invalid_dbus_message(hass, caplog):
assert "dbus" in caplog.text 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.""" """Test we can recover when DBus gets restarted out from under us."""
called_start = 0 called_start = 0
@ -213,10 +176,8 @@ async def test_recovery_from_dbus_restart(hass):
"homeassistant.components.bluetooth.scanner.OriginalBleakScanner", "homeassistant.components.bluetooth.scanner.OriginalBleakScanner",
return_value=scanner, return_value=scanner,
): ):
assert await async_setup_component( await async_setup_with_one_adapter(hass)
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
await hass.async_block_till_done()
assert called_start == 1 assert called_start == 1
start_time_monotonic = 1000 start_time_monotonic = 1000

View File

@ -876,7 +876,7 @@ async def mock_enable_bluetooth(
hass, mock_bleak_scanner_start, mock_bluetooth_adapters hass, mock_bleak_scanner_start, mock_bluetooth_adapters
): ):
"""Fixture to mock starting the bleak scanner.""" """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) entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id) await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -885,7 +885,20 @@ async def mock_enable_bluetooth(
@pytest.fixture(name="mock_bluetooth_adapters") @pytest.fixture(name="mock_bluetooth_adapters")
def mock_bluetooth_adapters(): def mock_bluetooth_adapters():
"""Fixture to 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 yield