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
|
||||||
await bluetooth_discovery.async_setup()
|
# The config entry is responsible for starting the manager
|
||||||
hass.data[DOMAIN] = bluetooth_discovery
|
# 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={}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
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, {}
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
|
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)
|
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop)
|
||||||
await self.scanner.start()
|
|
||||||
|
|
||||||
@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 self.scanner.history.values()
|
||||||
for device_adv in 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]] = {}
|
||||||
super().__init__(*args, **kwargs)
|
# 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)
|
||||||
|
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"
|
File diff suppressed because it is too large
Load Diff
@ -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