mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 19:27:45 +00:00
Add support for setting up and removing bluetooth in the UI (#75600)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
parent
20b6c4c48e
commit
38bccadaa6
@ -7,6 +7,7 @@ from datetime import datetime, timedelta
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import logging
|
import logging
|
||||||
|
import platform
|
||||||
from typing import Final, TypedDict, Union
|
from typing import Final, TypedDict, Union
|
||||||
|
|
||||||
from bleak import BleakError
|
from bleak import BleakError
|
||||||
@ -35,7 +36,7 @@ from homeassistant.loader import (
|
|||||||
from . import models
|
from . import models
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .models import HaBleakScanner
|
from .models import HaBleakScanner
|
||||||
from .usage import install_multiple_bleak_catcher
|
from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -115,6 +116,15 @@ BluetoothCallback = Callable[
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@hass_callback
|
||||||
|
def async_get_scanner(hass: HomeAssistant) -> HaBleakScanner:
|
||||||
|
"""Return a HaBleakScanner."""
|
||||||
|
if DOMAIN not in hass.data:
|
||||||
|
raise RuntimeError("Bluetooth integration not loaded")
|
||||||
|
manager: BluetoothManager = hass.data[DOMAIN]
|
||||||
|
return manager.async_get_scanner()
|
||||||
|
|
||||||
|
|
||||||
@hass_callback
|
@hass_callback
|
||||||
def async_discovered_service_info(
|
def async_discovered_service_info(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -178,14 +188,62 @@ def async_track_unavailable(
|
|||||||
return manager.async_track_unavailable(callback, address)
|
return manager.async_track_unavailable(callback, address)
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_has_bluetooth_adapter() -> bool:
|
||||||
|
"""Return if the device has a bluetooth adapter."""
|
||||||
|
if platform.system() == "Darwin": # CoreBluetooth is built in on MacOS hardware
|
||||||
|
return True
|
||||||
|
if platform.system() == "Windows": # We don't have a good way to detect on windows
|
||||||
|
return False
|
||||||
|
from bluetooth_adapters import ( # pylint: disable=import-outside-toplevel
|
||||||
|
get_bluetooth_adapters,
|
||||||
|
)
|
||||||
|
|
||||||
|
return bool(await 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_matchers = await async_get_bluetooth(hass)
|
integration_matchers = await async_get_bluetooth(hass)
|
||||||
bluetooth_discovery = BluetoothManager(
|
manager = BluetoothManager(hass, integration_matchers)
|
||||||
hass, integration_matchers, BluetoothScanningMode.PASSIVE
|
manager.async_setup()
|
||||||
|
hass.data[DOMAIN] = manager
|
||||||
|
# The config entry is responsible for starting the manager
|
||||||
|
# if its enabled
|
||||||
|
|
||||||
|
if hass.config_entries.async_entries(DOMAIN):
|
||||||
|
return True
|
||||||
|
if DOMAIN in config:
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={}
|
||||||
)
|
)
|
||||||
await bluetooth_discovery.async_setup()
|
)
|
||||||
hass.data[DOMAIN] = bluetooth_discovery
|
elif await _async_has_bluetooth_adapter():
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||||
|
data={},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant, entry: config_entries.ConfigEntry
|
||||||
|
) -> bool:
|
||||||
|
"""Set up the bluetooth integration from a config entry."""
|
||||||
|
manager: BluetoothManager = hass.data[DOMAIN]
|
||||||
|
await manager.async_start(BluetoothScanningMode.ACTIVE)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(
|
||||||
|
hass: HomeAssistant, entry: config_entries.ConfigEntry
|
||||||
|
) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
manager: BluetoothManager = hass.data[DOMAIN]
|
||||||
|
await manager.async_stop()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -241,11 +299,9 @@ class BluetoothManager:
|
|||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
integration_matchers: list[BluetoothMatcher],
|
integration_matchers: list[BluetoothMatcher],
|
||||||
scanning_mode: BluetoothScanningMode,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Init bluetooth discovery."""
|
"""Init bluetooth discovery."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.scanning_mode = scanning_mode
|
|
||||||
self._integration_matchers = integration_matchers
|
self._integration_matchers = integration_matchers
|
||||||
self.scanner: HaBleakScanner | None = None
|
self.scanner: HaBleakScanner | None = None
|
||||||
self._cancel_device_detected: CALLBACK_TYPE | None = None
|
self._cancel_device_detected: CALLBACK_TYPE | None = None
|
||||||
@ -258,19 +314,27 @@ class BluetoothManager:
|
|||||||
# an LRU to avoid memory issues.
|
# an LRU to avoid memory issues.
|
||||||
self._matched: LRU = LRU(MAX_REMEMBER_ADDRESSES)
|
self._matched: LRU = LRU(MAX_REMEMBER_ADDRESSES)
|
||||||
|
|
||||||
async def async_setup(self) -> None:
|
@hass_callback
|
||||||
|
def async_setup(self) -> None:
|
||||||
|
"""Set up the bluetooth manager."""
|
||||||
|
models.HA_BLEAK_SCANNER = self.scanner = HaBleakScanner()
|
||||||
|
|
||||||
|
@hass_callback
|
||||||
|
def async_get_scanner(self) -> HaBleakScanner:
|
||||||
|
"""Get the scanner."""
|
||||||
|
assert self.scanner is not None
|
||||||
|
return self.scanner
|
||||||
|
|
||||||
|
async def async_start(self, scanning_mode: BluetoothScanningMode) -> None:
|
||||||
"""Set up BT Discovery."""
|
"""Set up BT Discovery."""
|
||||||
|
assert self.scanner is not None
|
||||||
try:
|
try:
|
||||||
self.scanner = HaBleakScanner(
|
self.scanner.async_setup(
|
||||||
scanning_mode=SCANNING_MODE_TO_BLEAK[self.scanning_mode]
|
scanning_mode=SCANNING_MODE_TO_BLEAK[scanning_mode]
|
||||||
)
|
)
|
||||||
except (FileNotFoundError, BleakError) as ex:
|
except (FileNotFoundError, BleakError) as ex:
|
||||||
_LOGGER.warning(
|
raise RuntimeError(f"Failed to initialize Bluetooth: {ex}") from ex
|
||||||
"Could not create bluetooth scanner (is bluetooth present and enabled?): %s",
|
install_multiple_bleak_catcher()
|
||||||
ex,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
install_multiple_bleak_catcher(self.scanner)
|
|
||||||
self.async_setup_unavailable_tracking()
|
self.async_setup_unavailable_tracking()
|
||||||
# We have to start it right away as some integrations might
|
# We have to start it right away as some integrations might
|
||||||
# need it straight away.
|
# need it straight away.
|
||||||
@ -279,8 +343,11 @@ class BluetoothManager:
|
|||||||
self._cancel_device_detected = self.scanner.async_register_callback(
|
self._cancel_device_detected = self.scanner.async_register_callback(
|
||||||
self._device_detected, {}
|
self._device_detected, {}
|
||||||
)
|
)
|
||||||
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop)
|
try:
|
||||||
await self.scanner.start()
|
await self.scanner.start()
|
||||||
|
except (FileNotFoundError, BleakError) as ex:
|
||||||
|
raise RuntimeError(f"Failed to start Bluetooth: {ex}") from ex
|
||||||
|
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop)
|
||||||
|
|
||||||
@hass_callback
|
@hass_callback
|
||||||
def async_setup_unavailable_tracking(self) -> None:
|
def async_setup_unavailable_tracking(self) -> None:
|
||||||
@ -289,8 +356,8 @@ class BluetoothManager:
|
|||||||
@hass_callback
|
@hass_callback
|
||||||
def _async_check_unavailable(now: datetime) -> None:
|
def _async_check_unavailable(now: datetime) -> None:
|
||||||
"""Watch for unavailable devices."""
|
"""Watch for unavailable devices."""
|
||||||
assert models.HA_BLEAK_SCANNER is not None
|
scanner = self.scanner
|
||||||
scanner = models.HA_BLEAK_SCANNER
|
assert scanner is not None
|
||||||
history = set(scanner.history)
|
history = set(scanner.history)
|
||||||
active = {device.address for device in scanner.discovered_devices}
|
active = {device.address for device in scanner.discovered_devices}
|
||||||
disappeared = history.difference(active)
|
disappeared = history.difference(active)
|
||||||
@ -406,8 +473,8 @@ class BluetoothManager:
|
|||||||
if (
|
if (
|
||||||
matcher
|
matcher
|
||||||
and (address := matcher.get(ADDRESS))
|
and (address := matcher.get(ADDRESS))
|
||||||
and models.HA_BLEAK_SCANNER
|
and self.scanner
|
||||||
and (device_adv_data := models.HA_BLEAK_SCANNER.history.get(address))
|
and (device_adv_data := self.scanner.history.get(address))
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
callback(
|
callback(
|
||||||
@ -424,31 +491,25 @@ class BluetoothManager:
|
|||||||
@hass_callback
|
@hass_callback
|
||||||
def async_ble_device_from_address(self, address: str) -> BLEDevice | None:
|
def async_ble_device_from_address(self, address: str) -> BLEDevice | None:
|
||||||
"""Return the BLEDevice if present."""
|
"""Return the BLEDevice if present."""
|
||||||
if models.HA_BLEAK_SCANNER and (
|
if self.scanner and (ble_adv := self.scanner.history.get(address)):
|
||||||
ble_adv := models.HA_BLEAK_SCANNER.history.get(address)
|
|
||||||
):
|
|
||||||
return ble_adv[0]
|
return ble_adv[0]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@hass_callback
|
@hass_callback
|
||||||
def async_address_present(self, address: str) -> bool:
|
def async_address_present(self, address: str) -> bool:
|
||||||
"""Return if the address is present."""
|
"""Return if the address is present."""
|
||||||
return bool(
|
return bool(self.scanner and address in self.scanner.history)
|
||||||
models.HA_BLEAK_SCANNER and address in models.HA_BLEAK_SCANNER.history
|
|
||||||
)
|
|
||||||
|
|
||||||
@hass_callback
|
@hass_callback
|
||||||
def async_discovered_service_info(self) -> list[BluetoothServiceInfoBleak]:
|
def async_discovered_service_info(self) -> list[BluetoothServiceInfoBleak]:
|
||||||
"""Return if the address is present."""
|
"""Return if the address is present."""
|
||||||
if models.HA_BLEAK_SCANNER:
|
assert self.scanner is not None
|
||||||
history = models.HA_BLEAK_SCANNER.history
|
|
||||||
return [
|
return [
|
||||||
BluetoothServiceInfoBleak.from_advertisement(*device_adv, SOURCE_LOCAL)
|
BluetoothServiceInfoBleak.from_advertisement(*device_adv, SOURCE_LOCAL)
|
||||||
for device_adv in history.values()
|
for device_adv in self.scanner.history.values()
|
||||||
]
|
]
|
||||||
return []
|
|
||||||
|
|
||||||
async def async_stop(self, event: Event) -> None:
|
async def async_stop(self, event: Event | None = None) -> None:
|
||||||
"""Stop bluetooth discovery."""
|
"""Stop bluetooth discovery."""
|
||||||
if self._cancel_device_detected:
|
if self._cancel_device_detected:
|
||||||
self._cancel_device_detected()
|
self._cancel_device_detected()
|
||||||
@ -458,4 +519,4 @@ class BluetoothManager:
|
|||||||
self._cancel_unavailable_tracking = None
|
self._cancel_unavailable_tracking = None
|
||||||
if self.scanner:
|
if self.scanner:
|
||||||
await self.scanner.stop()
|
await self.scanner.stop()
|
||||||
models.HA_BLEAK_SCANNER = None
|
uninstall_multiple_bleak_catcher()
|
||||||
|
37
homeassistant/components/bluetooth/config_flow.py
Normal file
37
homeassistant/components/bluetooth/config_flow.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
"""Config flow to configure the Bluetooth integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigFlow
|
||||||
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
|
|
||||||
|
from .const import DEFAULT_NAME, DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Config flow for Bluetooth."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Handle a flow initialized by the user."""
|
||||||
|
return await self.async_step_enable_bluetooth()
|
||||||
|
|
||||||
|
async def async_step_enable_bluetooth(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Handle a flow initialized by the user or import."""
|
||||||
|
if self._async_current_entries():
|
||||||
|
return self.async_abort(reason="already_configured")
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
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)
|
@ -1,3 +1,4 @@
|
|||||||
"""Constants for the Bluetooth integration."""
|
"""Constants for the Bluetooth integration."""
|
||||||
|
|
||||||
DOMAIN = "bluetooth"
|
DOMAIN = "bluetooth"
|
||||||
|
DEFAULT_NAME = "Bluetooth"
|
||||||
|
@ -4,7 +4,8 @@
|
|||||||
"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.14.3"],
|
"requirements": ["bleak==0.14.3", "bluetooth-adapters==0.1.1"],
|
||||||
"codeowners": ["@bdraco"],
|
"codeowners": ["@bdraco"],
|
||||||
|
"config_flow": true,
|
||||||
"iot_class": "local_push"
|
"iot_class": "local_push"
|
||||||
}
|
}
|
||||||
|
@ -48,13 +48,24 @@ def _dispatch_callback(
|
|||||||
class HaBleakScanner(BleakScanner): # type: ignore[misc]
|
class HaBleakScanner(BleakScanner): # type: ignore[misc]
|
||||||
"""BleakScanner that cannot be stopped."""
|
"""BleakScanner that cannot be stopped."""
|
||||||
|
|
||||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
def __init__( # pylint: disable=super-init-not-called
|
||||||
|
self, *args: Any, **kwargs: Any
|
||||||
|
) -> None:
|
||||||
"""Initialize the BleakScanner."""
|
"""Initialize the BleakScanner."""
|
||||||
self._callbacks: list[
|
self._callbacks: list[
|
||||||
tuple[AdvertisementDataCallback, dict[str, set[str]]]
|
tuple[AdvertisementDataCallback, dict[str, set[str]]]
|
||||||
] = []
|
] = []
|
||||||
self.history: dict[str, tuple[BLEDevice, AdvertisementData]] = {}
|
self.history: dict[str, tuple[BLEDevice, AdvertisementData]] = {}
|
||||||
|
# Init called later in async_setup if we are enabling the scanner
|
||||||
|
# since init has side effects that can throw exceptions
|
||||||
|
self._setup = False
|
||||||
|
|
||||||
|
@hass_callback
|
||||||
|
def async_setup(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
"""Deferred setup of the BleakScanner since __init__ has side effects."""
|
||||||
|
if not self._setup:
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
self._setup = True
|
||||||
|
|
||||||
@hass_callback
|
@hass_callback
|
||||||
def async_register_callback(
|
def async_register_callback(
|
||||||
|
@ -2,6 +2,9 @@
|
|||||||
"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": {
|
||||||
@ -11,6 +14,9 @@
|
|||||||
"bluetooth_confirm": {
|
"bluetooth_confirm": {
|
||||||
"description": "Do you want to setup {name}?"
|
"description": "Do you want to setup {name}?"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Service is already configured"
|
||||||
|
},
|
||||||
"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": {
|
||||||
|
"description": "Do you want to setup Bluetooth?"
|
||||||
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"address": "Device"
|
"address": "Device"
|
||||||
|
@ -3,11 +3,16 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import bleak
|
import bleak
|
||||||
|
|
||||||
from . import models
|
from .models import HaBleakScannerWrapper
|
||||||
from .models import HaBleakScanner, HaBleakScannerWrapper
|
|
||||||
|
ORIGINAL_BLEAK_SCANNER = bleak.BleakScanner
|
||||||
|
|
||||||
|
|
||||||
def install_multiple_bleak_catcher(hass_bleak_scanner: HaBleakScanner) -> None:
|
def install_multiple_bleak_catcher() -> None:
|
||||||
"""Wrap the bleak classes to return the shared instance if multiple instances are detected."""
|
"""Wrap the bleak classes to return the shared instance if multiple instances are detected."""
|
||||||
models.HA_BLEAK_SCANNER = hass_bleak_scanner
|
|
||||||
bleak.BleakScanner = HaBleakScannerWrapper
|
bleak.BleakScanner = HaBleakScannerWrapper
|
||||||
|
|
||||||
|
|
||||||
|
def uninstall_multiple_bleak_catcher() -> None:
|
||||||
|
"""Unwrap the bleak classes."""
|
||||||
|
bleak.BleakScanner = ORIGINAL_BLEAK_SCANNER
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
"dependencies": [
|
"dependencies": [
|
||||||
"application_credentials",
|
"application_credentials",
|
||||||
"automation",
|
"automation",
|
||||||
|
"bluetooth",
|
||||||
"cloud",
|
"cloud",
|
||||||
"counter",
|
"counter",
|
||||||
"dhcp",
|
"dhcp",
|
||||||
|
@ -47,6 +47,7 @@ FLOWS = {
|
|||||||
"balboa",
|
"balboa",
|
||||||
"blebox",
|
"blebox",
|
||||||
"blink",
|
"blink",
|
||||||
|
"bluetooth",
|
||||||
"bmw_connected_drive",
|
"bmw_connected_drive",
|
||||||
"bond",
|
"bond",
|
||||||
"bosch_shc",
|
"bosch_shc",
|
||||||
|
@ -10,6 +10,8 @@ atomicwrites-homeassistant==1.4.1
|
|||||||
attrs==21.2.0
|
attrs==21.2.0
|
||||||
awesomeversion==22.6.0
|
awesomeversion==22.6.0
|
||||||
bcrypt==3.1.7
|
bcrypt==3.1.7
|
||||||
|
bleak==0.14.3
|
||||||
|
bluetooth-adapters==0.1.1
|
||||||
certifi>=2021.5.30
|
certifi>=2021.5.30
|
||||||
ciso8601==2.2.0
|
ciso8601==2.2.0
|
||||||
cryptography==36.0.2
|
cryptography==36.0.2
|
||||||
|
@ -424,6 +424,9 @@ blockchain==1.4.4
|
|||||||
# homeassistant.components.zengge
|
# homeassistant.components.zengge
|
||||||
# bluepy==1.3.0
|
# bluepy==1.3.0
|
||||||
|
|
||||||
|
# homeassistant.components.bluetooth
|
||||||
|
bluetooth-adapters==0.1.1
|
||||||
|
|
||||||
# homeassistant.components.bond
|
# homeassistant.components.bond
|
||||||
bond-async==0.1.22
|
bond-async==0.1.22
|
||||||
|
|
||||||
|
@ -334,6 +334,9 @@ blebox_uniapi==2.0.2
|
|||||||
# homeassistant.components.blink
|
# homeassistant.components.blink
|
||||||
blinkpy==0.19.0
|
blinkpy==0.19.0
|
||||||
|
|
||||||
|
# homeassistant.components.bluetooth
|
||||||
|
bluetooth-adapters==0.1.1
|
||||||
|
|
||||||
# homeassistant.components.bond
|
# homeassistant.components.bond
|
||||||
bond-async==0.1.22
|
bond-async==0.1.22
|
||||||
|
|
||||||
|
106
tests/components/bluetooth/test_config_flow.py
Normal file
106
tests/components/bluetooth/test_config_flow.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
"""Test the bluetooth config flow."""
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components.bluetooth.const import DOMAIN
|
||||||
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_step_user(hass):
|
||||||
|
"""Test setting up manually."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_USER},
|
||||||
|
data={},
|
||||||
|
)
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "enable_bluetooth"
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.bluetooth.async_setup_entry", return_value=True
|
||||||
|
) as mock_setup_entry:
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input={}
|
||||||
|
)
|
||||||
|
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result2["title"] == "Bluetooth"
|
||||||
|
assert result2["data"] == {}
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_step_user_only_allows_one(hass):
|
||||||
|
"""Test setting up manually with an existing entry."""
|
||||||
|
entry = MockConfigEntry(domain=DOMAIN)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_USER},
|
||||||
|
data={},
|
||||||
|
)
|
||||||
|
assert result["type"] == FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_step_integration_discovery(hass):
|
||||||
|
"""Test setting up from integration discovery."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||||
|
data={},
|
||||||
|
)
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "enable_bluetooth"
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.bluetooth.async_setup_entry", return_value=True
|
||||||
|
) as mock_setup_entry:
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input={}
|
||||||
|
)
|
||||||
|
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result2["title"] == "Bluetooth"
|
||||||
|
assert result2["data"] == {}
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_step_integration_discovery_already_exists(hass):
|
||||||
|
"""Test setting up from integration discovery when an entry already exists."""
|
||||||
|
entry = MockConfigEntry(domain=DOMAIN)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||||
|
data={},
|
||||||
|
)
|
||||||
|
assert result["type"] == FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_step_import(hass):
|
||||||
|
"""Test setting up from integration discovery."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.bluetooth.async_setup_entry", return_value=True
|
||||||
|
) as mock_setup_entry:
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_IMPORT},
|
||||||
|
data={},
|
||||||
|
)
|
||||||
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == "Bluetooth"
|
||||||
|
assert result["data"] == {}
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_step_import_already_exists(hass):
|
||||||
|
"""Test setting up from yaml when an entry already exists."""
|
||||||
|
entry = MockConfigEntry(domain=DOMAIN)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_IMPORT},
|
||||||
|
data={},
|
||||||
|
)
|
||||||
|
assert result["type"] == FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
@ -4,6 +4,7 @@ from unittest.mock import MagicMock, patch
|
|||||||
|
|
||||||
from bleak import BleakError
|
from bleak import BleakError
|
||||||
from bleak.backends.scanner import AdvertisementData, BLEDevice
|
from bleak.backends.scanner import AdvertisementData, BLEDevice
|
||||||
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components import bluetooth
|
from homeassistant.components import bluetooth
|
||||||
from homeassistant.components.bluetooth import (
|
from homeassistant.components.bluetooth import (
|
||||||
@ -11,6 +12,7 @@ from homeassistant.components.bluetooth import (
|
|||||||
UNAVAILABLE_TRACK_SECONDS,
|
UNAVAILABLE_TRACK_SECONDS,
|
||||||
BluetoothChange,
|
BluetoothChange,
|
||||||
BluetoothServiceInfo,
|
BluetoothServiceInfo,
|
||||||
|
async_get_scanner,
|
||||||
async_track_unavailable,
|
async_track_unavailable,
|
||||||
models,
|
models,
|
||||||
)
|
)
|
||||||
@ -19,10 +21,10 @@ from homeassistant.core import 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 tests.common import async_fire_time_changed
|
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||||
|
|
||||||
|
|
||||||
async def test_setup_and_stop(hass, mock_bleak_scanner_start):
|
async def test_setup_and_stop(hass, mock_bleak_scanner_start, enable_bluetooth):
|
||||||
"""Test we and setup and stop the scanner."""
|
"""Test we and setup and stop the scanner."""
|
||||||
mock_bt = [
|
mock_bt = [
|
||||||
{"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"}
|
{"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"}
|
||||||
@ -47,33 +49,57 @@ async def test_setup_and_stop_no_bluetooth(hass, caplog):
|
|||||||
{"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"}
|
{"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"}
|
||||||
]
|
]
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.bluetooth.HaBleakScanner", side_effect=BleakError
|
"homeassistant.components.bluetooth.HaBleakScanner.async_setup",
|
||||||
|
side_effect=BleakError,
|
||||||
) 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
|
||||||
), patch.object(
|
|
||||||
hass.config_entries.flow, "async_init"
|
|
||||||
):
|
):
|
||||||
assert await async_setup_component(
|
assert await async_setup_component(
|
||||||
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
|
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()
|
||||||
|
|
||||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert len(mock_ha_bleak_scanner.mock_calls) == 1
|
assert len(mock_ha_bleak_scanner.mock_calls) == 1
|
||||||
assert "Could not create bluetooth scanner" in caplog.text
|
assert "Failed to initialize Bluetooth" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_and_stop_broken_bluetooth(hass, caplog):
|
||||||
|
"""Test we fail gracefully when bluetooth/dbus is broken."""
|
||||||
|
mock_bt = [
|
||||||
|
{"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"}
|
||||||
|
]
|
||||||
|
|
||||||
|
with patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch(
|
||||||
|
"homeassistant.components.bluetooth.HaBleakScanner.start",
|
||||||
|
side_effect=BleakError,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
|
||||||
|
):
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert "Failed to start Bluetooth" in caplog.text
|
||||||
|
assert len(bluetooth.async_discovered_service_info(hass)) == 0
|
||||||
|
|
||||||
|
|
||||||
async def test_calling_async_discovered_devices_no_bluetooth(hass, caplog):
|
async def test_calling_async_discovered_devices_no_bluetooth(hass, caplog):
|
||||||
"""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(
|
||||||
"homeassistant.components.bluetooth.HaBleakScanner", side_effect=BleakError
|
"homeassistant.components.bluetooth.HaBleakScanner.async_setup",
|
||||||
) as mock_ha_bleak_scanner, patch(
|
side_effect=FileNotFoundError,
|
||||||
|
), 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"
|
|
||||||
):
|
):
|
||||||
assert await async_setup_component(
|
assert await async_setup_component(
|
||||||
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
|
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
|
||||||
@ -83,13 +109,14 @@ async def test_calling_async_discovered_devices_no_bluetooth(hass, caplog):
|
|||||||
|
|
||||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert len(mock_ha_bleak_scanner.mock_calls) == 1
|
assert "Failed to initialize Bluetooth" in caplog.text
|
||||||
assert "Could not create bluetooth scanner" in caplog.text
|
|
||||||
assert not bluetooth.async_discovered_service_info(hass)
|
assert not bluetooth.async_discovered_service_info(hass)
|
||||||
assert not bluetooth.async_address_present(hass, "aa:bb:bb:dd:ee:ff")
|
assert not bluetooth.async_address_present(hass, "aa:bb:bb:dd:ee:ff")
|
||||||
|
|
||||||
|
|
||||||
async def test_discovery_match_by_service_uuid(hass, mock_bleak_scanner_start):
|
async def test_discovery_match_by_service_uuid(
|
||||||
|
hass, mock_bleak_scanner_start, enable_bluetooth
|
||||||
|
):
|
||||||
"""Test bluetooth discovery match by service_uuid."""
|
"""Test bluetooth discovery match by service_uuid."""
|
||||||
mock_bt = [
|
mock_bt = [
|
||||||
{"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"}
|
{"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"}
|
||||||
@ -108,7 +135,7 @@ async def test_discovery_match_by_service_uuid(hass, mock_bleak_scanner_start):
|
|||||||
wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name")
|
wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name")
|
||||||
wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[])
|
wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[])
|
||||||
|
|
||||||
models.HA_BLEAK_SCANNER._callback(wrong_device, wrong_adv)
|
async_get_scanner(hass)._callback(wrong_device, wrong_adv)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_config_flow.mock_calls) == 0
|
assert len(mock_config_flow.mock_calls) == 0
|
||||||
@ -118,7 +145,7 @@ async def test_discovery_match_by_service_uuid(hass, mock_bleak_scanner_start):
|
|||||||
local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
|
local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
|
||||||
)
|
)
|
||||||
|
|
||||||
models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv)
|
async_get_scanner(hass)._callback(switchbot_device, switchbot_adv)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_config_flow.mock_calls) == 1
|
assert len(mock_config_flow.mock_calls) == 1
|
||||||
@ -130,10 +157,13 @@ async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start):
|
|||||||
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
|
||||||
), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow:
|
):
|
||||||
assert await async_setup_component(
|
assert await async_setup_component(
|
||||||
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
|
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
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)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
@ -142,7 +172,7 @@ async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start):
|
|||||||
wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name")
|
wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name")
|
||||||
wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[])
|
wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[])
|
||||||
|
|
||||||
models.HA_BLEAK_SCANNER._callback(wrong_device, wrong_adv)
|
async_get_scanner(hass)._callback(wrong_device, wrong_adv)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_config_flow.mock_calls) == 0
|
assert len(mock_config_flow.mock_calls) == 0
|
||||||
@ -150,7 +180,7 @@ async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start):
|
|||||||
switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
|
switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
|
||||||
switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[])
|
switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[])
|
||||||
|
|
||||||
models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv)
|
async_get_scanner(hass)._callback(switchbot_device, switchbot_adv)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_config_flow.mock_calls) == 1
|
assert len(mock_config_flow.mock_calls) == 1
|
||||||
@ -170,10 +200,13 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte(
|
|||||||
]
|
]
|
||||||
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:
|
):
|
||||||
assert await async_setup_component(
|
assert await async_setup_component(
|
||||||
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
|
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
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)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
@ -186,7 +219,7 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte(
|
|||||||
manufacturer_data={76: b"\x06\x02\x03\x99"},
|
manufacturer_data={76: b"\x06\x02\x03\x99"},
|
||||||
)
|
)
|
||||||
|
|
||||||
models.HA_BLEAK_SCANNER._callback(hkc_device, hkc_adv)
|
async_get_scanner(hass)._callback(hkc_device, hkc_adv)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_config_flow.mock_calls) == 1
|
assert len(mock_config_flow.mock_calls) == 1
|
||||||
@ -194,7 +227,7 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte(
|
|||||||
mock_config_flow.reset_mock()
|
mock_config_flow.reset_mock()
|
||||||
|
|
||||||
# 2nd discovery should not generate another flow
|
# 2nd discovery should not generate another flow
|
||||||
models.HA_BLEAK_SCANNER._callback(hkc_device, hkc_adv)
|
async_get_scanner(hass)._callback(hkc_device, hkc_adv)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_config_flow.mock_calls) == 0
|
assert len(mock_config_flow.mock_calls) == 0
|
||||||
@ -205,7 +238,7 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte(
|
|||||||
local_name="lock", service_uuids=[], manufacturer_data={76: b"\x02"}
|
local_name="lock", service_uuids=[], manufacturer_data={76: b"\x02"}
|
||||||
)
|
)
|
||||||
|
|
||||||
models.HA_BLEAK_SCANNER._callback(not_hkc_device, not_hkc_adv)
|
async_get_scanner(hass)._callback(not_hkc_device, not_hkc_adv)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_config_flow.mock_calls) == 0
|
assert len(mock_config_flow.mock_calls) == 0
|
||||||
@ -214,14 +247,14 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte(
|
|||||||
local_name="lock", service_uuids=[], manufacturer_data={21: b"\x02"}
|
local_name="lock", service_uuids=[], manufacturer_data={21: b"\x02"}
|
||||||
)
|
)
|
||||||
|
|
||||||
models.HA_BLEAK_SCANNER._callback(not_apple_device, not_apple_adv)
|
async_get_scanner(hass)._callback(not_apple_device, not_apple_adv)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_config_flow.mock_calls) == 0
|
assert len(mock_config_flow.mock_calls) == 0
|
||||||
|
|
||||||
|
|
||||||
async def test_async_discovered_device_api(hass, mock_bleak_scanner_start):
|
async def test_async_discovered_device_api(hass, mock_bleak_scanner_start):
|
||||||
"""Test the async_discovered_device_api."""
|
"""Test the async_discovered_device API."""
|
||||||
mock_bt = []
|
mock_bt = []
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
|
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
|
||||||
@ -231,10 +264,12 @@ 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(
|
assert await async_setup_component(
|
||||||
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
|
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
with patch.object(hass.config_entries.flow, "async_init"):
|
||||||
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()
|
||||||
|
|
||||||
@ -244,10 +279,10 @@ async def test_async_discovered_device_api(hass, mock_bleak_scanner_start):
|
|||||||
|
|
||||||
wrong_device = BLEDevice("44:44:33:11:23:42", "wrong_name")
|
wrong_device = BLEDevice("44:44:33:11:23:42", "wrong_name")
|
||||||
wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[])
|
wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[])
|
||||||
models.HA_BLEAK_SCANNER._callback(wrong_device, wrong_adv)
|
async_get_scanner(hass)._callback(wrong_device, wrong_adv)
|
||||||
switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
|
switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
|
||||||
switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[])
|
switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[])
|
||||||
models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv)
|
async_get_scanner(hass)._callback(switchbot_device, switchbot_adv)
|
||||||
wrong_device_went_unavailable = False
|
wrong_device_went_unavailable = False
|
||||||
switchbot_device_went_unavailable = False
|
switchbot_device_went_unavailable = False
|
||||||
|
|
||||||
@ -281,8 +316,8 @@ async def test_async_discovered_device_api(hass, mock_bleak_scanner_start):
|
|||||||
assert wrong_device_went_unavailable is True
|
assert wrong_device_went_unavailable is True
|
||||||
|
|
||||||
# See the devices again
|
# See the devices again
|
||||||
models.HA_BLEAK_SCANNER._callback(wrong_device, wrong_adv)
|
async_get_scanner(hass)._callback(wrong_device, wrong_adv)
|
||||||
models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv)
|
async_get_scanner(hass)._callback(switchbot_device, switchbot_adv)
|
||||||
# Cancel the callbacks
|
# Cancel the callbacks
|
||||||
wrong_device_unavailable_cancel()
|
wrong_device_unavailable_cancel()
|
||||||
switchbot_device_unavailable_cancel()
|
switchbot_device_unavailable_cancel()
|
||||||
@ -308,7 +343,7 @@ async def test_async_discovered_device_api(hass, mock_bleak_scanner_start):
|
|||||||
assert bluetooth.async_address_present(hass, "44:44:33:11:23:45") is True
|
assert bluetooth.async_address_present(hass, "44:44:33:11:23:45") is True
|
||||||
|
|
||||||
|
|
||||||
async def test_register_callbacks(hass, mock_bleak_scanner_start):
|
async def test_register_callbacks(hass, mock_bleak_scanner_start, enable_bluetooth):
|
||||||
"""Test registering a callback."""
|
"""Test registering a callback."""
|
||||||
mock_bt = []
|
mock_bt = []
|
||||||
callbacks = []
|
callbacks = []
|
||||||
@ -347,25 +382,25 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start):
|
|||||||
service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
|
service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
|
||||||
)
|
)
|
||||||
|
|
||||||
models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv)
|
async_get_scanner(hass)._callback(switchbot_device, switchbot_adv)
|
||||||
|
|
||||||
empty_device = BLEDevice("11:22:33:44:55:66", "empty")
|
empty_device = BLEDevice("11:22:33:44:55:66", "empty")
|
||||||
empty_adv = AdvertisementData(local_name="empty")
|
empty_adv = AdvertisementData(local_name="empty")
|
||||||
|
|
||||||
models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv)
|
async_get_scanner(hass)._callback(empty_device, empty_adv)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
empty_device = BLEDevice("11:22:33:44:55:66", "empty")
|
empty_device = BLEDevice("11:22:33:44:55:66", "empty")
|
||||||
empty_adv = AdvertisementData(local_name="empty")
|
empty_adv = AdvertisementData(local_name="empty")
|
||||||
|
|
||||||
# 3rd callback raises ValueError but is still tracked
|
# 3rd callback raises ValueError but is still tracked
|
||||||
models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv)
|
async_get_scanner(hass)._callback(empty_device, empty_adv)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
# 4th callback should not be tracked since we canceled
|
# 4th callback should not be tracked since we canceled
|
||||||
models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv)
|
async_get_scanner(hass)._callback(empty_device, empty_adv)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(callbacks) == 3
|
assert len(callbacks) == 3
|
||||||
@ -389,7 +424,9 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start):
|
|||||||
assert service_info.manufacturer_id is None
|
assert service_info.manufacturer_id is None
|
||||||
|
|
||||||
|
|
||||||
async def test_register_callback_by_address(hass, mock_bleak_scanner_start):
|
async def test_register_callback_by_address(
|
||||||
|
hass, mock_bleak_scanner_start, enable_bluetooth
|
||||||
|
):
|
||||||
"""Test registering a callback by address."""
|
"""Test registering a callback by address."""
|
||||||
mock_bt = []
|
mock_bt = []
|
||||||
callbacks = []
|
callbacks = []
|
||||||
@ -404,10 +441,13 @@ async def test_register_callback_by_address(hass, mock_bleak_scanner_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
|
||||||
), patch.object(hass.config_entries.flow, "async_init"):
|
):
|
||||||
assert await async_setup_component(
|
assert await async_setup_component(
|
||||||
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
|
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
with patch.object(hass.config_entries.flow, "async_init"):
|
||||||
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()
|
||||||
|
|
||||||
@ -427,25 +467,25 @@ async def test_register_callback_by_address(hass, mock_bleak_scanner_start):
|
|||||||
service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
|
service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
|
||||||
)
|
)
|
||||||
|
|
||||||
models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv)
|
async_get_scanner(hass)._callback(switchbot_device, switchbot_adv)
|
||||||
|
|
||||||
empty_device = BLEDevice("11:22:33:44:55:66", "empty")
|
empty_device = BLEDevice("11:22:33:44:55:66", "empty")
|
||||||
empty_adv = AdvertisementData(local_name="empty")
|
empty_adv = AdvertisementData(local_name="empty")
|
||||||
|
|
||||||
models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv)
|
async_get_scanner(hass)._callback(empty_device, empty_adv)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
empty_device = BLEDevice("11:22:33:44:55:66", "empty")
|
empty_device = BLEDevice("11:22:33:44:55:66", "empty")
|
||||||
empty_adv = AdvertisementData(local_name="empty")
|
empty_adv = AdvertisementData(local_name="empty")
|
||||||
|
|
||||||
# 3rd callback raises ValueError but is still tracked
|
# 3rd callback raises ValueError but is still tracked
|
||||||
models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv)
|
async_get_scanner(hass)._callback(empty_device, empty_adv)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
# 4th callback should not be tracked since we canceled
|
# 4th callback should not be tracked since we canceled
|
||||||
models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv)
|
async_get_scanner(hass)._callback(empty_device, empty_adv)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
# Now register again with a callback that fails to
|
# Now register again with a callback that fails to
|
||||||
@ -475,14 +515,19 @@ async def test_register_callback_by_address(hass, mock_bleak_scanner_start):
|
|||||||
assert service_info.manufacturer_id == 89
|
assert service_info.manufacturer_id == 89
|
||||||
|
|
||||||
|
|
||||||
async def test_wrapped_instance_with_filter(hass, mock_bleak_scanner_start):
|
async def test_wrapped_instance_with_filter(
|
||||||
|
hass, mock_bleak_scanner_start, enable_bluetooth
|
||||||
|
):
|
||||||
"""Test consumers can use the wrapped instance with a filter as if it was normal BleakScanner."""
|
"""Test consumers can use the wrapped instance with a filter as if it was normal BleakScanner."""
|
||||||
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"):
|
):
|
||||||
assert await async_setup_component(
|
assert await async_setup_component(
|
||||||
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
|
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
with patch.object(hass.config_entries.flow, "async_init"):
|
||||||
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()
|
||||||
|
|
||||||
@ -504,15 +549,15 @@ async def test_wrapped_instance_with_filter(hass, mock_bleak_scanner_start):
|
|||||||
empty_device = BLEDevice("11:22:33:44:55:66", "empty")
|
empty_device = BLEDevice("11:22:33:44:55:66", "empty")
|
||||||
empty_adv = AdvertisementData(local_name="empty")
|
empty_adv = AdvertisementData(local_name="empty")
|
||||||
|
|
||||||
assert models.HA_BLEAK_SCANNER is not None
|
assert async_get_scanner(hass) is not None
|
||||||
scanner = models.HaBleakScannerWrapper(
|
scanner = models.HaBleakScannerWrapper(
|
||||||
filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]}
|
filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]}
|
||||||
)
|
)
|
||||||
scanner.register_detection_callback(_device_detected)
|
scanner.register_detection_callback(_device_detected)
|
||||||
|
|
||||||
mock_discovered = [MagicMock()]
|
mock_discovered = [MagicMock()]
|
||||||
type(models.HA_BLEAK_SCANNER).discovered_devices = mock_discovered
|
type(async_get_scanner(hass)).discovered_devices = mock_discovered
|
||||||
models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv)
|
async_get_scanner(hass)._callback(switchbot_device, switchbot_adv)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
discovered = await scanner.discover(timeout=0)
|
discovered = await scanner.discover(timeout=0)
|
||||||
@ -527,28 +572,33 @@ async def test_wrapped_instance_with_filter(hass, mock_bleak_scanner_start):
|
|||||||
# We should get a reply from the history when we register again
|
# We should get a reply from the history when we register again
|
||||||
assert len(detected) == 3
|
assert len(detected) == 3
|
||||||
|
|
||||||
type(models.HA_BLEAK_SCANNER).discovered_devices = []
|
type(async_get_scanner(hass)).discovered_devices = []
|
||||||
discovered = await scanner.discover(timeout=0)
|
discovered = await scanner.discover(timeout=0)
|
||||||
assert len(discovered) == 0
|
assert len(discovered) == 0
|
||||||
assert discovered == []
|
assert discovered == []
|
||||||
|
|
||||||
models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv)
|
async_get_scanner(hass)._callback(switchbot_device, switchbot_adv)
|
||||||
assert len(detected) == 4
|
assert len(detected) == 4
|
||||||
|
|
||||||
# The filter we created in the wrapped scanner with should be respected
|
# The filter we created in the wrapped scanner with should be respected
|
||||||
# and we should not get another callback
|
# and we should not get another callback
|
||||||
models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv)
|
async_get_scanner(hass)._callback(empty_device, empty_adv)
|
||||||
assert len(detected) == 4
|
assert len(detected) == 4
|
||||||
|
|
||||||
|
|
||||||
async def test_wrapped_instance_with_service_uuids(hass, mock_bleak_scanner_start):
|
async def test_wrapped_instance_with_service_uuids(
|
||||||
|
hass, mock_bleak_scanner_start, enable_bluetooth
|
||||||
|
):
|
||||||
"""Test consumers can use the wrapped instance with a service_uuids list as if it was normal BleakScanner."""
|
"""Test consumers can use the wrapped instance with a service_uuids list as if it was normal BleakScanner."""
|
||||||
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"):
|
):
|
||||||
assert await async_setup_component(
|
assert await async_setup_component(
|
||||||
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
|
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
with patch.object(hass.config_entries.flow, "async_init"):
|
||||||
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()
|
||||||
|
|
||||||
@ -570,26 +620,28 @@ async def test_wrapped_instance_with_service_uuids(hass, mock_bleak_scanner_star
|
|||||||
empty_device = BLEDevice("11:22:33:44:55:66", "empty")
|
empty_device = BLEDevice("11:22:33:44:55:66", "empty")
|
||||||
empty_adv = AdvertisementData(local_name="empty")
|
empty_adv = AdvertisementData(local_name="empty")
|
||||||
|
|
||||||
assert models.HA_BLEAK_SCANNER is not None
|
assert async_get_scanner(hass) is not None
|
||||||
scanner = models.HaBleakScannerWrapper(
|
scanner = models.HaBleakScannerWrapper(
|
||||||
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
|
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
|
||||||
)
|
)
|
||||||
scanner.register_detection_callback(_device_detected)
|
scanner.register_detection_callback(_device_detected)
|
||||||
|
|
||||||
type(models.HA_BLEAK_SCANNER).discovered_devices = [MagicMock()]
|
type(async_get_scanner(hass)).discovered_devices = [MagicMock()]
|
||||||
for _ in range(2):
|
for _ in range(2):
|
||||||
models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv)
|
async_get_scanner(hass)._callback(switchbot_device, switchbot_adv)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(detected) == 2
|
assert len(detected) == 2
|
||||||
|
|
||||||
# The UUIDs list we created in the wrapped scanner with should be respected
|
# The UUIDs list we created in the wrapped scanner with should be respected
|
||||||
# and we should not get another callback
|
# and we should not get another callback
|
||||||
models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv)
|
async_get_scanner(hass)._callback(empty_device, empty_adv)
|
||||||
assert len(detected) == 2
|
assert len(detected) == 2
|
||||||
|
|
||||||
|
|
||||||
async def test_wrapped_instance_with_broken_callbacks(hass, mock_bleak_scanner_start):
|
async def test_wrapped_instance_with_broken_callbacks(
|
||||||
|
hass, mock_bleak_scanner_start, enable_bluetooth
|
||||||
|
):
|
||||||
"""Test broken callbacks do not cause the scanner to fail."""
|
"""Test broken callbacks do not cause the scanner to fail."""
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=[]
|
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=[]
|
||||||
@ -597,6 +649,9 @@ async def test_wrapped_instance_with_broken_callbacks(hass, mock_bleak_scanner_s
|
|||||||
assert await async_setup_component(
|
assert await async_setup_component(
|
||||||
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
|
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
with patch.object(hass.config_entries.flow, "async_init"):
|
||||||
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()
|
||||||
|
|
||||||
@ -618,30 +673,34 @@ async def test_wrapped_instance_with_broken_callbacks(hass, mock_bleak_scanner_s
|
|||||||
service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
|
service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert models.HA_BLEAK_SCANNER is not None
|
assert async_get_scanner(hass) is not None
|
||||||
scanner = models.HaBleakScannerWrapper(
|
scanner = models.HaBleakScannerWrapper(
|
||||||
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
|
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
|
||||||
)
|
)
|
||||||
scanner.register_detection_callback(_device_detected)
|
scanner.register_detection_callback(_device_detected)
|
||||||
|
|
||||||
models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv)
|
async_get_scanner(hass)._callback(switchbot_device, switchbot_adv)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv)
|
async_get_scanner(hass)._callback(switchbot_device, switchbot_adv)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert len(detected) == 1
|
assert len(detected) == 1
|
||||||
|
|
||||||
|
|
||||||
async def test_wrapped_instance_changes_uuids(hass, mock_bleak_scanner_start):
|
async def test_wrapped_instance_changes_uuids(
|
||||||
|
hass, mock_bleak_scanner_start, enable_bluetooth
|
||||||
|
):
|
||||||
"""Test consumers can use the wrapped instance can change the uuids later."""
|
"""Test consumers can use the wrapped instance can change the uuids later."""
|
||||||
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"):
|
):
|
||||||
assert await async_setup_component(
|
assert await async_setup_component(
|
||||||
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
|
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
|
||||||
)
|
)
|
||||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
with patch.object(hass.config_entries.flow, "async_init"):
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||||
|
await hass.async_block_till_done()
|
||||||
detected = []
|
detected = []
|
||||||
|
|
||||||
def _device_detected(
|
def _device_detected(
|
||||||
@ -660,35 +719,41 @@ async def test_wrapped_instance_changes_uuids(hass, mock_bleak_scanner_start):
|
|||||||
empty_device = BLEDevice("11:22:33:44:55:66", "empty")
|
empty_device = BLEDevice("11:22:33:44:55:66", "empty")
|
||||||
empty_adv = AdvertisementData(local_name="empty")
|
empty_adv = AdvertisementData(local_name="empty")
|
||||||
|
|
||||||
assert models.HA_BLEAK_SCANNER is not None
|
assert async_get_scanner(hass) is not None
|
||||||
scanner = models.HaBleakScannerWrapper()
|
scanner = models.HaBleakScannerWrapper()
|
||||||
scanner.set_scanning_filter(service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"])
|
scanner.set_scanning_filter(
|
||||||
|
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
|
||||||
|
)
|
||||||
scanner.register_detection_callback(_device_detected)
|
scanner.register_detection_callback(_device_detected)
|
||||||
|
|
||||||
type(models.HA_BLEAK_SCANNER).discovered_devices = [MagicMock()]
|
type(async_get_scanner(hass)).discovered_devices = [MagicMock()]
|
||||||
for _ in range(2):
|
for _ in range(2):
|
||||||
models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv)
|
async_get_scanner(hass)._callback(switchbot_device, switchbot_adv)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(detected) == 2
|
assert len(detected) == 2
|
||||||
|
|
||||||
# The UUIDs list we created in the wrapped scanner with should be respected
|
# The UUIDs list we created in the wrapped scanner with should be respected
|
||||||
# and we should not get another callback
|
# and we should not get another callback
|
||||||
models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv)
|
async_get_scanner(hass)._callback(empty_device, empty_adv)
|
||||||
assert len(detected) == 2
|
assert len(detected) == 2
|
||||||
|
|
||||||
|
|
||||||
async def test_wrapped_instance_changes_filters(hass, mock_bleak_scanner_start):
|
async def test_wrapped_instance_changes_filters(
|
||||||
|
hass, mock_bleak_scanner_start, enable_bluetooth
|
||||||
|
):
|
||||||
"""Test consumers can use the wrapped instance can change the filter later."""
|
"""Test consumers can use the wrapped instance can change the filter later."""
|
||||||
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"):
|
):
|
||||||
assert await async_setup_component(
|
assert await async_setup_component(
|
||||||
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
|
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
|
||||||
)
|
)
|
||||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
with patch.object(hass.config_entries.flow, "async_init"):
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||||
|
await hass.async_block_till_done()
|
||||||
detected = []
|
detected = []
|
||||||
|
|
||||||
def _device_detected(
|
def _device_detected(
|
||||||
@ -707,40 +772,42 @@ async def test_wrapped_instance_changes_filters(hass, mock_bleak_scanner_start):
|
|||||||
empty_device = BLEDevice("11:22:33:44:55:62", "empty")
|
empty_device = BLEDevice("11:22:33:44:55:62", "empty")
|
||||||
empty_adv = AdvertisementData(local_name="empty")
|
empty_adv = AdvertisementData(local_name="empty")
|
||||||
|
|
||||||
assert models.HA_BLEAK_SCANNER is not None
|
assert async_get_scanner(hass) is not None
|
||||||
scanner = models.HaBleakScannerWrapper()
|
scanner = models.HaBleakScannerWrapper()
|
||||||
scanner.set_scanning_filter(
|
scanner.set_scanning_filter(
|
||||||
filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]}
|
filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]}
|
||||||
)
|
)
|
||||||
scanner.register_detection_callback(_device_detected)
|
scanner.register_detection_callback(_device_detected)
|
||||||
|
|
||||||
type(models.HA_BLEAK_SCANNER).discovered_devices = [MagicMock()]
|
type(async_get_scanner(hass)).discovered_devices = [MagicMock()]
|
||||||
for _ in range(2):
|
for _ in range(2):
|
||||||
models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv)
|
async_get_scanner(hass)._callback(switchbot_device, switchbot_adv)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(detected) == 2
|
assert len(detected) == 2
|
||||||
|
|
||||||
# The UUIDs list we created in the wrapped scanner with should be respected
|
# The UUIDs list we created in the wrapped scanner with should be respected
|
||||||
# and we should not get another callback
|
# and we should not get another callback
|
||||||
models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv)
|
async_get_scanner(hass)._callback(empty_device, empty_adv)
|
||||||
assert len(detected) == 2
|
assert len(detected) == 2
|
||||||
|
|
||||||
|
|
||||||
async def test_wrapped_instance_unsupported_filter(
|
async def test_wrapped_instance_unsupported_filter(
|
||||||
hass, mock_bleak_scanner_start, caplog
|
hass, mock_bleak_scanner_start, caplog, enable_bluetooth
|
||||||
):
|
):
|
||||||
"""Test we want when their filter is ineffective."""
|
"""Test we want when their filter is ineffective."""
|
||||||
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"):
|
):
|
||||||
assert await async_setup_component(
|
assert await async_setup_component(
|
||||||
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
|
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
|
||||||
)
|
)
|
||||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert models.HA_BLEAK_SCANNER is not None
|
with patch.object(hass.config_entries.flow, "async_init"):
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert async_get_scanner(hass) is not None
|
||||||
scanner = models.HaBleakScannerWrapper()
|
scanner = models.HaBleakScannerWrapper()
|
||||||
scanner.set_scanning_filter(
|
scanner.set_scanning_filter(
|
||||||
filters={
|
filters={
|
||||||
@ -778,7 +845,7 @@ async def test_async_ble_device_from_address(hass, mock_bleak_scanner_start):
|
|||||||
|
|
||||||
switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
|
switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
|
||||||
switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[])
|
switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[])
|
||||||
models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv)
|
async_get_scanner(hass)._callback(switchbot_device, switchbot_adv)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
@ -789,3 +856,82 @@ async def test_async_ble_device_from_address(hass, mock_bleak_scanner_start):
|
|||||||
assert (
|
assert (
|
||||||
bluetooth.async_ble_device_from_address(hass, "00:66:33:22:11:22") is None
|
bluetooth.async_ble_device_from_address(hass, "00:66:33:22:11:22") is None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_without_bluetooth_in_configuration_yaml(hass, mock_bluetooth):
|
||||||
|
"""Test setting up without bluetooth in configuration.yaml does not create the config entry."""
|
||||||
|
assert await async_setup_component(hass, bluetooth.DOMAIN, {})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert not hass.config_entries.async_entries(bluetooth.DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_with_bluetooth_in_configuration_yaml(hass, mock_bluetooth):
|
||||||
|
"""Test setting up with bluetooth in configuration.yaml creates the config entry."""
|
||||||
|
assert await async_setup_component(hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert hass.config_entries.async_entries(bluetooth.DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_can_unsetup_bluetooth(hass, mock_bleak_scanner_start, enable_bluetooth):
|
||||||
|
"""Test we can setup and unsetup bluetooth."""
|
||||||
|
entry = MockConfigEntry(domain=bluetooth.DOMAIN, data={})
|
||||||
|
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_auto_detect_bluetooth_adapters_linux(hass):
|
||||||
|
"""Test we auto detect bluetooth adapters on linux."""
|
||||||
|
with patch(
|
||||||
|
"bluetooth_adapters.get_bluetooth_adapters", return_value={"hci0"}
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.bluetooth.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 len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_auto_detect_bluetooth_adapters_linux_none_found(hass):
|
||||||
|
"""Test we auto detect bluetooth adapters on linux with no adapters found."""
|
||||||
|
with patch("bluetooth_adapters.get_bluetooth_adapters", return_value=set()), patch(
|
||||||
|
"homeassistant.components.bluetooth.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 len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_auto_detect_bluetooth_adapters_macos(hass):
|
||||||
|
"""Test we auto detect bluetooth adapters on macos."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.bluetooth.platform.system", return_value="Darwin"
|
||||||
|
):
|
||||||
|
assert await async_setup_component(hass, bluetooth.DOMAIN, {})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert not hass.config_entries.async_entries(bluetooth.DOMAIN)
|
||||||
|
assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_no_auto_detect_bluetooth_adapters_windows(hass):
|
||||||
|
"""Test we auto detect bluetooth adapters on windows."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.bluetooth.platform.system", return_value="Windows"
|
||||||
|
):
|
||||||
|
assert await async_setup_component(hass, bluetooth.DOMAIN, {})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert not hass.config_entries.async_entries(bluetooth.DOMAIN)
|
||||||
|
assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_raising_runtime_error_when_no_bluetooth(hass):
|
||||||
|
"""Test we raise an exception if we try to get the scanner when its not there."""
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
bluetooth.async_get_scanner(hass)
|
||||||
|
@ -12,6 +12,7 @@ from homeassistant.components.bluetooth import (
|
|||||||
DOMAIN,
|
DOMAIN,
|
||||||
UNAVAILABLE_TRACK_SECONDS,
|
UNAVAILABLE_TRACK_SECONDS,
|
||||||
BluetoothChange,
|
BluetoothChange,
|
||||||
|
async_get_scanner,
|
||||||
)
|
)
|
||||||
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
||||||
PassiveBluetoothCoordinatorEntity,
|
PassiveBluetoothCoordinatorEntity,
|
||||||
@ -207,12 +208,14 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start):
|
|||||||
saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
||||||
assert len(mock_add_entities.mock_calls) == 1
|
assert len(mock_add_entities.mock_calls) == 1
|
||||||
assert coordinator.available is True
|
assert coordinator.available is True
|
||||||
|
scanner = async_get_scanner(hass)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices",
|
"homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices",
|
||||||
[MagicMock(address="44:44:33:11:23:45")],
|
[MagicMock(address="44:44:33:11:23:45")],
|
||||||
), patch(
|
), patch.object(
|
||||||
"homeassistant.components.bluetooth.models.HA_BLEAK_SCANNER.history",
|
scanner,
|
||||||
|
"history",
|
||||||
{"aa:bb:cc:dd:ee:ff": MagicMock()},
|
{"aa:bb:cc:dd:ee:ff": MagicMock()},
|
||||||
):
|
):
|
||||||
async_fire_time_changed(
|
async_fire_time_changed(
|
||||||
@ -228,8 +231,9 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start):
|
|||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices",
|
"homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices",
|
||||||
[MagicMock(address="44:44:33:11:23:45")],
|
[MagicMock(address="44:44:33:11:23:45")],
|
||||||
), patch(
|
), patch.object(
|
||||||
"homeassistant.components.bluetooth.models.HA_BLEAK_SCANNER.history",
|
scanner,
|
||||||
|
"history",
|
||||||
{"aa:bb:cc:dd:ee:ff": MagicMock()},
|
{"aa:bb:cc:dd:ee:ff": MagicMock()},
|
||||||
):
|
):
|
||||||
async_fire_time_changed(
|
async_fire_time_changed(
|
||||||
|
@ -1,22 +1,25 @@
|
|||||||
"""Tests for the Bluetooth integration."""
|
"""Tests for the Bluetooth integration."""
|
||||||
|
|
||||||
from unittest.mock import MagicMock
|
|
||||||
|
|
||||||
import bleak
|
import bleak
|
||||||
|
|
||||||
from homeassistant.components.bluetooth import models
|
|
||||||
from homeassistant.components.bluetooth.models import HaBleakScannerWrapper
|
from homeassistant.components.bluetooth.models import HaBleakScannerWrapper
|
||||||
from homeassistant.components.bluetooth.usage import install_multiple_bleak_catcher
|
from homeassistant.components.bluetooth.usage import (
|
||||||
|
install_multiple_bleak_catcher,
|
||||||
|
uninstall_multiple_bleak_catcher,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_multiple_bleak_scanner_instances(hass):
|
async def test_multiple_bleak_scanner_instances(hass):
|
||||||
"""Test creating multiple zeroconf throws without an integration."""
|
"""Test creating multiple BleakScanners without an integration."""
|
||||||
assert models.HA_BLEAK_SCANNER is None
|
install_multiple_bleak_catcher()
|
||||||
mock_scanner = MagicMock()
|
|
||||||
|
|
||||||
install_multiple_bleak_catcher(mock_scanner)
|
|
||||||
|
|
||||||
instance = bleak.BleakScanner()
|
instance = bleak.BleakScanner()
|
||||||
|
|
||||||
assert isinstance(instance, HaBleakScannerWrapper)
|
assert isinstance(instance, HaBleakScannerWrapper)
|
||||||
assert models.HA_BLEAK_SCANNER is mock_scanner
|
|
||||||
|
uninstall_multiple_bleak_catcher()
|
||||||
|
|
||||||
|
instance = bleak.BleakScanner()
|
||||||
|
|
||||||
|
assert not isinstance(instance, HaBleakScannerWrapper)
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
"""Tests for the bluetooth_le_tracker component."""
|
"""Session fixtures."""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def bluetooth_le_tracker_auto_mock_bluetooth(mock_bluetooth):
|
def mock_bluetooth(enable_bluetooth):
|
||||||
"""Mock the bluetooth integration scanner."""
|
"""Auto mock bluetooth."""
|
||||||
|
@ -23,7 +23,7 @@ def recorder_url_mock():
|
|||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
async def test_setup(hass, mock_zeroconf, mock_get_source_ip):
|
async def test_setup(hass, mock_zeroconf, mock_get_source_ip, mock_bluetooth):
|
||||||
"""Test setup."""
|
"""Test setup."""
|
||||||
recorder_helper.async_initialize_recorder(hass)
|
recorder_helper.async_initialize_recorder(hass)
|
||||||
assert await async_setup_component(hass, "default_config", {"foo": "bar"})
|
assert await async_setup_component(hass, "default_config", {"foo": "bar"})
|
||||||
|
@ -4,5 +4,5 @@ import pytest
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def mock_bluetooth(mock_bleak_scanner_start):
|
def mock_bluetooth(enable_bluetooth):
|
||||||
"""Auto mock bluetooth."""
|
"""Auto mock bluetooth."""
|
||||||
|
@ -4,5 +4,5 @@ import pytest
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def auto_mock_bleak_scanner_start(mock_bleak_scanner_start):
|
def mock_bluetooth(enable_bluetooth):
|
||||||
"""Auto mock bleak scanner start."""
|
"""Auto mock bluetooth."""
|
||||||
|
@ -871,6 +871,24 @@ def mock_integration_frame():
|
|||||||
yield correct_frame
|
yield correct_frame
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="enable_bluetooth")
|
||||||
|
async def mock_enable_bluetooth(
|
||||||
|
hass, mock_bleak_scanner_start, mock_bluetooth_adapters
|
||||||
|
):
|
||||||
|
"""Fixture to mock starting the bleak scanner."""
|
||||||
|
entry = MockConfigEntry(domain="bluetooth")
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="mock_bluetooth_adapters")
|
||||||
|
def mock_bluetooth_adapters():
|
||||||
|
"""Fixture to mock bluetooth adapters."""
|
||||||
|
with patch("bluetooth_adapters.get_bluetooth_adapters", return_value=set()):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="mock_bleak_scanner_start")
|
@pytest.fixture(name="mock_bleak_scanner_start")
|
||||||
def mock_bleak_scanner_start():
|
def mock_bleak_scanner_start():
|
||||||
"""Fixture to mock starting the bleak scanner."""
|
"""Fixture to mock starting the bleak scanner."""
|
||||||
@ -900,5 +918,5 @@ def mock_bleak_scanner_start():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="mock_bluetooth")
|
@pytest.fixture(name="mock_bluetooth")
|
||||||
def mock_bluetooth(mock_bleak_scanner_start):
|
def mock_bluetooth(mock_bleak_scanner_start, mock_bluetooth_adapters):
|
||||||
"""Mock out bluetooth from starting."""
|
"""Mock out bluetooth from starting."""
|
||||||
|
Loading…
x
Reference in New Issue
Block a user