mirror of https://github.com/home-assistant/core
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
|
||||
import fnmatch
|
||||
import logging
|
||||
import platform
|
||||
from typing import Final, TypedDict, Union
|
||||
|
||||
from bleak import BleakError
|
||||
|
@ -35,7 +36,7 @@ from homeassistant.loader import (
|
|||
from . import models
|
||||
from .const import DOMAIN
|
||||
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__)
|
||||
|
||||
|
@ -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
|
||||
def async_discovered_service_info(
|
||||
hass: HomeAssistant,
|
||||
|
@ -178,14 +188,62 @@ def async_track_unavailable(
|
|||
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:
|
||||
"""Set up the bluetooth integration."""
|
||||
integration_matchers = await async_get_bluetooth(hass)
|
||||
bluetooth_discovery = BluetoothManager(
|
||||
hass, integration_matchers, BluetoothScanningMode.PASSIVE
|
||||
)
|
||||
await bluetooth_discovery.async_setup()
|
||||
hass.data[DOMAIN] = bluetooth_discovery
|
||||
manager = BluetoothManager(hass, integration_matchers)
|
||||
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={}
|
||||
)
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
|
@ -241,11 +299,9 @@ class BluetoothManager:
|
|||
self,
|
||||
hass: HomeAssistant,
|
||||
integration_matchers: list[BluetoothMatcher],
|
||||
scanning_mode: BluetoothScanningMode,
|
||||
) -> None:
|
||||
"""Init bluetooth discovery."""
|
||||
self.hass = hass
|
||||
self.scanning_mode = scanning_mode
|
||||
self._integration_matchers = integration_matchers
|
||||
self.scanner: HaBleakScanner | None = None
|
||||
self._cancel_device_detected: CALLBACK_TYPE | None = None
|
||||
|
@ -258,19 +314,27 @@ class BluetoothManager:
|
|||
# an LRU to avoid memory issues.
|
||||
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."""
|
||||
assert self.scanner is not None
|
||||
try:
|
||||
self.scanner = HaBleakScanner(
|
||||
scanning_mode=SCANNING_MODE_TO_BLEAK[self.scanning_mode]
|
||||
self.scanner.async_setup(
|
||||
scanning_mode=SCANNING_MODE_TO_BLEAK[scanning_mode]
|
||||
)
|
||||
except (FileNotFoundError, BleakError) as ex:
|
||||
_LOGGER.warning(
|
||||
"Could not create bluetooth scanner (is bluetooth present and enabled?): %s",
|
||||
ex,
|
||||
)
|
||||
return
|
||||
install_multiple_bleak_catcher(self.scanner)
|
||||
raise RuntimeError(f"Failed to initialize Bluetooth: {ex}") from ex
|
||||
install_multiple_bleak_catcher()
|
||||
self.async_setup_unavailable_tracking()
|
||||
# We have to start it right away as some integrations might
|
||||
# need it straight away.
|
||||
|
@ -279,8 +343,11 @@ class BluetoothManager:
|
|||
self._cancel_device_detected = self.scanner.async_register_callback(
|
||||
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)
|
||||
await self.scanner.start()
|
||||
|
||||
@hass_callback
|
||||
def async_setup_unavailable_tracking(self) -> None:
|
||||
|
@ -289,8 +356,8 @@ class BluetoothManager:
|
|||
@hass_callback
|
||||
def _async_check_unavailable(now: datetime) -> None:
|
||||
"""Watch for unavailable devices."""
|
||||
assert models.HA_BLEAK_SCANNER is not None
|
||||
scanner = models.HA_BLEAK_SCANNER
|
||||
scanner = self.scanner
|
||||
assert scanner is not None
|
||||
history = set(scanner.history)
|
||||
active = {device.address for device in scanner.discovered_devices}
|
||||
disappeared = history.difference(active)
|
||||
|
@ -406,8 +473,8 @@ class BluetoothManager:
|
|||
if (
|
||||
matcher
|
||||
and (address := matcher.get(ADDRESS))
|
||||
and models.HA_BLEAK_SCANNER
|
||||
and (device_adv_data := models.HA_BLEAK_SCANNER.history.get(address))
|
||||
and self.scanner
|
||||
and (device_adv_data := self.scanner.history.get(address))
|
||||
):
|
||||
try:
|
||||
callback(
|
||||
|
@ -424,31 +491,25 @@ class BluetoothManager:
|
|||
@hass_callback
|
||||
def async_ble_device_from_address(self, address: str) -> BLEDevice | None:
|
||||
"""Return the BLEDevice if present."""
|
||||
if models.HA_BLEAK_SCANNER and (
|
||||
ble_adv := models.HA_BLEAK_SCANNER.history.get(address)
|
||||
):
|
||||
if self.scanner and (ble_adv := self.scanner.history.get(address)):
|
||||
return ble_adv[0]
|
||||
return None
|
||||
|
||||
@hass_callback
|
||||
def async_address_present(self, address: str) -> bool:
|
||||
"""Return if the address is present."""
|
||||
return bool(
|
||||
models.HA_BLEAK_SCANNER and address in models.HA_BLEAK_SCANNER.history
|
||||
)
|
||||
return bool(self.scanner and address in self.scanner.history)
|
||||
|
||||
@hass_callback
|
||||
def async_discovered_service_info(self) -> list[BluetoothServiceInfoBleak]:
|
||||
"""Return if the address is present."""
|
||||
if models.HA_BLEAK_SCANNER:
|
||||
history = models.HA_BLEAK_SCANNER.history
|
||||
return [
|
||||
BluetoothServiceInfoBleak.from_advertisement(*device_adv, SOURCE_LOCAL)
|
||||
for device_adv in history.values()
|
||||
]
|
||||
return []
|
||||
assert self.scanner is not None
|
||||
return [
|
||||
BluetoothServiceInfoBleak.from_advertisement(*device_adv, SOURCE_LOCAL)
|
||||
for device_adv in self.scanner.history.values()
|
||||
]
|
||||
|
||||
async def async_stop(self, event: Event) -> None:
|
||||
async def async_stop(self, event: Event | None = None) -> None:
|
||||
"""Stop bluetooth discovery."""
|
||||
if self._cancel_device_detected:
|
||||
self._cancel_device_detected()
|
||||
|
@ -458,4 +519,4 @@ class BluetoothManager:
|
|||
self._cancel_unavailable_tracking = None
|
||||
if self.scanner:
|
||||
await self.scanner.stop()
|
||||
models.HA_BLEAK_SCANNER = None
|
||||
uninstall_multiple_bleak_catcher()
|
||||
|
|
|
@ -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."""
|
||||
|
||||
DOMAIN = "bluetooth"
|
||||
DEFAULT_NAME = "Bluetooth"
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
"documentation": "https://www.home-assistant.io/integrations/bluetooth",
|
||||
"dependencies": ["websocket_api"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["bleak==0.14.3"],
|
||||
"requirements": ["bleak==0.14.3", "bluetooth-adapters==0.1.1"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
}
|
||||
|
|
|
@ -48,13 +48,24 @@ def _dispatch_callback(
|
|||
class HaBleakScanner(BleakScanner): # type: ignore[misc]
|
||||
"""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."""
|
||||
self._callbacks: list[
|
||||
tuple[AdvertisementDataCallback, dict[str, set[str]]]
|
||||
] = []
|
||||
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
|
||||
def async_register_callback(
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
"config": {
|
||||
"flow_title": "{name}",
|
||||
"step": {
|
||||
"enable_bluetooth": {
|
||||
"description": "Do you want to setup Bluetooth?"
|
||||
},
|
||||
"user": {
|
||||
"description": "Choose a device to setup",
|
||||
"data": {
|
||||
|
@ -11,6 +14,9 @@
|
|||
"bluetooth_confirm": {
|
||||
"description": "Do you want to setup {name}?"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Service is already configured"
|
||||
},
|
||||
"flow_title": "{name}",
|
||||
"step": {
|
||||
"bluetooth_confirm": {
|
||||
"description": "Do you want to setup {name}?"
|
||||
},
|
||||
"enable_bluetooth": {
|
||||
"description": "Do you want to setup Bluetooth?"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"address": "Device"
|
||||
|
|
|
@ -3,11 +3,16 @@ from __future__ import annotations
|
|||
|
||||
import bleak
|
||||
|
||||
from . import models
|
||||
from .models import HaBleakScanner, HaBleakScannerWrapper
|
||||
from .models import 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."""
|
||||
models.HA_BLEAK_SCANNER = hass_bleak_scanner
|
||||
bleak.BleakScanner = HaBleakScannerWrapper
|
||||
|
||||
|
||||
def uninstall_multiple_bleak_catcher() -> None:
|
||||
"""Unwrap the bleak classes."""
|
||||
bleak.BleakScanner = ORIGINAL_BLEAK_SCANNER
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
"dependencies": [
|
||||
"application_credentials",
|
||||
"automation",
|
||||
"bluetooth",
|
||||
"cloud",
|
||||
"counter",
|
||||
"dhcp",
|
||||
|
|
|
@ -47,6 +47,7 @@ FLOWS = {
|
|||
"balboa",
|
||||
"blebox",
|
||||
"blink",
|
||||
"bluetooth",
|
||||
"bmw_connected_drive",
|
||||
"bond",
|
||||
"bosch_shc",
|
||||
|
|
|
@ -10,6 +10,8 @@ atomicwrites-homeassistant==1.4.1
|
|||
attrs==21.2.0
|
||||
awesomeversion==22.6.0
|
||||
bcrypt==3.1.7
|
||||
bleak==0.14.3
|
||||
bluetooth-adapters==0.1.1
|
||||
certifi>=2021.5.30
|
||||
ciso8601==2.2.0
|
||||
cryptography==36.0.2
|
||||
|
|
|
@ -424,6 +424,9 @@ blockchain==1.4.4
|
|||
# homeassistant.components.zengge
|
||||
# bluepy==1.3.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-adapters==0.1.1
|
||||
|
||||
# homeassistant.components.bond
|
||||
bond-async==0.1.22
|
||||
|
||||
|
|
|
@ -334,6 +334,9 @@ blebox_uniapi==2.0.2
|
|||
# homeassistant.components.blink
|
||||
blinkpy==0.19.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-adapters==0.1.1
|
||||
|
||||
# homeassistant.components.bond
|
||||
bond-async==0.1.22
|
||||
|
||||
|
|
|
@ -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,
|
||||
UNAVAILABLE_TRACK_SECONDS,
|
||||
BluetoothChange,
|
||||
async_get_scanner,
|
||||
)
|
||||
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
||||
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)
|
||||
assert len(mock_add_entities.mock_calls) == 1
|
||||
assert coordinator.available is True
|
||||
scanner = async_get_scanner(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices",
|
||||
[MagicMock(address="44:44:33:11:23:45")],
|
||||
), patch(
|
||||
"homeassistant.components.bluetooth.models.HA_BLEAK_SCANNER.history",
|
||||
), patch.object(
|
||||
scanner,
|
||||
"history",
|
||||
{"aa:bb:cc:dd:ee:ff": MagicMock()},
|
||||
):
|
||||
async_fire_time_changed(
|
||||
|
@ -228,8 +231,9 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start):
|
|||
with patch(
|
||||
"homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices",
|
||||
[MagicMock(address="44:44:33:11:23:45")],
|
||||
), patch(
|
||||
"homeassistant.components.bluetooth.models.HA_BLEAK_SCANNER.history",
|
||||
), patch.object(
|
||||
scanner,
|
||||
"history",
|
||||
{"aa:bb:cc:dd:ee:ff": MagicMock()},
|
||||
):
|
||||
async_fire_time_changed(
|
||||
|
|
|
@ -1,22 +1,25 @@
|
|||
"""Tests for the Bluetooth integration."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import bleak
|
||||
|
||||
from homeassistant.components.bluetooth import models
|
||||
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):
|
||||
"""Test creating multiple zeroconf throws without an integration."""
|
||||
assert models.HA_BLEAK_SCANNER is None
|
||||
mock_scanner = MagicMock()
|
||||
|
||||
install_multiple_bleak_catcher(mock_scanner)
|
||||
"""Test creating multiple BleakScanners without an integration."""
|
||||
install_multiple_bleak_catcher()
|
||||
|
||||
instance = bleak.BleakScanner()
|
||||
|
||||
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
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def bluetooth_le_tracker_auto_mock_bluetooth(mock_bluetooth):
|
||||
"""Mock the bluetooth integration scanner."""
|
||||
def mock_bluetooth(enable_bluetooth):
|
||||
"""Auto mock bluetooth."""
|
||||
|
|
|
@ -23,7 +23,7 @@ def recorder_url_mock():
|
|||
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."""
|
||||
recorder_helper.async_initialize_recorder(hass)
|
||||
assert await async_setup_component(hass, "default_config", {"foo": "bar"})
|
||||
|
|
|
@ -4,5 +4,5 @@ import pytest
|
|||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_bluetooth(mock_bleak_scanner_start):
|
||||
def mock_bluetooth(enable_bluetooth):
|
||||
"""Auto mock bluetooth."""
|
||||
|
|
|
@ -4,5 +4,5 @@ import pytest
|
|||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def auto_mock_bleak_scanner_start(mock_bleak_scanner_start):
|
||||
"""Auto mock bleak scanner start."""
|
||||
def mock_bluetooth(enable_bluetooth):
|
||||
"""Auto mock bluetooth."""
|
||||
|
|
|
@ -871,6 +871,24 @@ def mock_integration_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")
|
||||
def mock_bleak_scanner_start():
|
||||
"""Fixture to mock starting the bleak scanner."""
|
||||
|
@ -900,5 +918,5 @@ def mock_bleak_scanner_start():
|
|||
|
||||
|
||||
@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."""
|
||||
|
|
Loading…
Reference in New Issue