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
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
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={}
)
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
@ -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, {}
)
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop)
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)
@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
assert self.scanner is not None
return [
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."""
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()

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."""
DOMAIN = "bluetooth"
DEFAULT_NAME = "Bluetooth"

View File

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

View File

@ -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]] = {}
# 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(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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"

View File

@ -4,6 +4,7 @@ from unittest.mock import MagicMock, patch
from bleak import BleakError
from bleak.backends.scanner import AdvertisementData, BLEDevice
import pytest
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
@ -11,6 +12,7 @@ from homeassistant.components.bluetooth import (
UNAVAILABLE_TRACK_SECONDS,
BluetoothChange,
BluetoothServiceInfo,
async_get_scanner,
async_track_unavailable,
models,
)
@ -19,10 +21,10 @@ from homeassistant.core import callback
from homeassistant.setup import async_setup_component
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."""
mock_bt = [
{"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"}
]
with patch(
"homeassistant.components.bluetooth.HaBleakScanner", side_effect=BleakError
"homeassistant.components.bluetooth.HaBleakScanner.async_setup",
side_effect=BleakError,
) as mock_ha_bleak_scanner, patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
), patch.object(
hass.config_entries.flow, "async_init"
):
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 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):
"""Test we fail gracefully when asking for discovered devices and there is no blueooth."""
mock_bt = []
with patch(
"homeassistant.components.bluetooth.HaBleakScanner", side_effect=BleakError
) as mock_ha_bleak_scanner, patch(
"homeassistant.components.bluetooth.HaBleakScanner.async_setup",
side_effect=FileNotFoundError,
), patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
), patch.object(
hass.config_entries.flow, "async_init"
):
assert await async_setup_component(
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)
await hass.async_block_till_done()
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
assert not bluetooth.async_discovered_service_info(hass)
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."""
mock_bt = [
{"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_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()
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"]
)
models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv)
async_get_scanner(hass)._callback(switchbot_device, switchbot_adv)
await hass.async_block_till_done()
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"}]
with patch(
"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(
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)
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_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()
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_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()
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(
"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(
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)
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"},
)
models.HA_BLEAK_SCANNER._callback(hkc_device, hkc_adv)
async_get_scanner(hass)._callback(hkc_device, hkc_adv)
await hass.async_block_till_done()
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()
# 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()
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"}
)
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()
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"}
)
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()
assert len(mock_config_flow.mock_calls) == 0
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 = []
with patch(
"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_address_present(hass, "44:44:22:22:11:22")
assert await async_setup_component(
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)
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_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_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
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
# See the devices again
models.HA_BLEAK_SCANNER._callback(wrong_device, wrong_adv)
models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv)
async_get_scanner(hass)._callback(wrong_device, wrong_adv)
async_get_scanner(hass)._callback(switchbot_device, switchbot_adv)
# Cancel the callbacks
wrong_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
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."""
mock_bt = []
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"},
)
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_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()
empty_device = BLEDevice("11:22:33:44:55:66", "empty")
empty_adv = AdvertisementData(local_name="empty")
# 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()
cancel()
# 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()
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
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."""
mock_bt = []
callbacks = []
@ -404,10 +441,13 @@ async def test_register_callback_by_address(hass, mock_bleak_scanner_start):
with patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
), patch.object(hass.config_entries.flow, "async_init"):
):
assert await async_setup_component(
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)
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"},
)
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_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()
empty_device = BLEDevice("11:22:33:44:55:66", "empty")
empty_adv = AdvertisementData(local_name="empty")
# 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()
cancel()
# 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()
# 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
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."""
with patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=[]
), patch.object(hass.config_entries.flow, "async_init"):
):
assert await async_setup_component(
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)
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_adv = AdvertisementData(local_name="empty")
assert models.HA_BLEAK_SCANNER is not None
assert async_get_scanner(hass) is not None
scanner = models.HaBleakScannerWrapper(
filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]}
)
scanner.register_detection_callback(_device_detected)
mock_discovered = [MagicMock()]
type(models.HA_BLEAK_SCANNER).discovered_devices = mock_discovered
models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv)
type(async_get_scanner(hass)).discovered_devices = mock_discovered
async_get_scanner(hass)._callback(switchbot_device, switchbot_adv)
await hass.async_block_till_done()
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
assert len(detected) == 3
type(models.HA_BLEAK_SCANNER).discovered_devices = []
type(async_get_scanner(hass)).discovered_devices = []
discovered = await scanner.discover(timeout=0)
assert len(discovered) == 0
assert discovered == []
models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv)
async_get_scanner(hass)._callback(switchbot_device, switchbot_adv)
assert len(detected) == 4
# The filter we created in the wrapped scanner with should be respected
# 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
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."""
with patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=[]
), patch.object(hass.config_entries.flow, "async_init"):
):
assert await async_setup_component(
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)
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_adv = AdvertisementData(local_name="empty")
assert models.HA_BLEAK_SCANNER is not None
assert async_get_scanner(hass) is not None
scanner = models.HaBleakScannerWrapper(
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
)
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):
models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv)
async_get_scanner(hass)._callback(switchbot_device, switchbot_adv)
await hass.async_block_till_done()
assert len(detected) == 2
# The UUIDs list we created in the wrapped scanner with should be respected
# 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
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."""
with patch(
"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(
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)
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"},
)
assert models.HA_BLEAK_SCANNER is not None
assert async_get_scanner(hass) is not None
scanner = models.HaBleakScannerWrapper(
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
)
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()
models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv)
async_get_scanner(hass)._callback(switchbot_device, switchbot_adv)
await hass.async_block_till_done()
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."""
with patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=[]
), patch.object(hass.config_entries.flow, "async_init"):
):
assert await async_setup_component(
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
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 = []
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_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.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)
type(models.HA_BLEAK_SCANNER).discovered_devices = [MagicMock()]
type(async_get_scanner(hass)).discovered_devices = [MagicMock()]
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()
assert len(detected) == 2
# The UUIDs list we created in the wrapped scanner with should be respected
# 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
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."""
with patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=[]
), patch.object(hass.config_entries.flow, "async_init"):
):
assert await async_setup_component(
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
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 = []
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_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.set_scanning_filter(
filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]}
)
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):
models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv)
async_get_scanner(hass)._callback(switchbot_device, switchbot_adv)
await hass.async_block_till_done()
assert len(detected) == 2
# The UUIDs list we created in the wrapped scanner with should be respected
# 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
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."""
with patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=[]
), patch.object(hass.config_entries.flow, "async_init"):
):
assert await async_setup_component(
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
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.set_scanning_filter(
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_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()
assert (
@ -789,3 +856,82 @@ async def test_async_ble_device_from_address(hass, mock_bleak_scanner_start):
assert (
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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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