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:
J. Nick Koston 2022-07-22 13:19:53 -05:00 committed by GitHub
parent 20b6c4c48e
commit 38bccadaa6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 755 additions and 339 deletions

View File

@ -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()

View 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)

View File

@ -1,3 +1,4 @@
"""Constants for the Bluetooth integration.""" """Constants for the Bluetooth integration."""
DOMAIN = "bluetooth" DOMAIN = "bluetooth"
DEFAULT_NAME = "Bluetooth"

View File

@ -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"
} }

View File

@ -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(

View File

@ -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%]"
} }
} }
} }

View File

@ -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"

View File

@ -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

View File

@ -5,6 +5,7 @@
"dependencies": [ "dependencies": [
"application_credentials", "application_credentials",
"automation", "automation",
"bluetooth",
"cloud", "cloud",
"counter", "counter",
"dhcp", "dhcp",

View File

@ -47,6 +47,7 @@ FLOWS = {
"balboa", "balboa",
"blebox", "blebox",
"blink", "blink",
"bluetooth",
"bmw_connected_drive", "bmw_connected_drive",
"bond", "bond",
"bosch_shc", "bosch_shc",

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View File

@ -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(

View File

@ -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)

View File

@ -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."""

View File

@ -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"})

View File

@ -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."""

View File

@ -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."""

View File

@ -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."""