mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 19:27:45 +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 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:
|
||||||
|
@ -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)
|
|
||||||
|
@ -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"
|
||||||
|
@ -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:
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user