Add support for bleak passive scanning on linux (#75542)
parent
7ee47f0f26
commit
d1486d04d9
|
@ -9,8 +9,8 @@ from typing import TYPE_CHECKING, cast
|
||||||
|
|
||||||
import async_timeout
|
import async_timeout
|
||||||
|
|
||||||
from homeassistant import config_entries
|
|
||||||
from homeassistant.components import usb
|
from homeassistant.components import usb
|
||||||
|
from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, ConfigEntry
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
@ -25,6 +25,7 @@ from .const import (
|
||||||
ADAPTER_SW_VERSION,
|
ADAPTER_SW_VERSION,
|
||||||
CONF_ADAPTER,
|
CONF_ADAPTER,
|
||||||
CONF_DETAILS,
|
CONF_DETAILS,
|
||||||
|
CONF_PASSIVE,
|
||||||
DATA_MANAGER,
|
DATA_MANAGER,
|
||||||
DEFAULT_ADDRESS,
|
DEFAULT_ADDRESS,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
@ -51,7 +52,6 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"async_ble_device_from_address",
|
"async_ble_device_from_address",
|
||||||
"async_discovered_service_info",
|
"async_discovered_service_info",
|
||||||
|
@ -213,7 +213,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
manager.async_setup()
|
manager.async_setup()
|
||||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, manager.async_stop)
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, manager.async_stop)
|
||||||
hass.data[DATA_MANAGER] = models.MANAGER = manager
|
hass.data[DATA_MANAGER] = models.MANAGER = manager
|
||||||
|
|
||||||
adapters = await manager.async_get_bluetooth_adapters()
|
adapters = await manager.async_get_bluetooth_adapters()
|
||||||
|
|
||||||
async_migrate_entries(hass, adapters)
|
async_migrate_entries(hass, adapters)
|
||||||
|
@ -249,8 +248,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
|
||||||
@hass_callback
|
@hass_callback
|
||||||
def async_migrate_entries(
|
def async_migrate_entries(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant, adapters: dict[str, AdapterDetails]
|
||||||
adapters: dict[str, AdapterDetails],
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Migrate config entries to support multiple."""
|
"""Migrate config entries to support multiple."""
|
||||||
current_entries = hass.config_entries.async_entries(DOMAIN)
|
current_entries = hass.config_entries.async_entries(DOMAIN)
|
||||||
|
@ -284,15 +282,13 @@ async def async_discover_adapters(
|
||||||
discovery_flow.async_create_flow(
|
discovery_flow.async_create_flow(
|
||||||
hass,
|
hass,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
context={"source": SOURCE_INTEGRATION_DISCOVERY},
|
||||||
data={CONF_ADAPTER: adapter, CONF_DETAILS: details},
|
data={CONF_ADAPTER: adapter, CONF_DETAILS: details},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_update_device(
|
async def async_update_device(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant, entry: ConfigEntry, adapter: str
|
||||||
entry: config_entries.ConfigEntry,
|
|
||||||
adapter: str,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Update device registry entry.
|
"""Update device registry entry.
|
||||||
|
|
||||||
|
@ -314,9 +310,7 @@ async def async_update_device(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
hass: HomeAssistant, entry: config_entries.ConfigEntry
|
|
||||||
) -> bool:
|
|
||||||
"""Set up a config entry for a bluetooth scanner."""
|
"""Set up a config entry for a bluetooth scanner."""
|
||||||
address = entry.unique_id
|
address = entry.unique_id
|
||||||
assert address is not None
|
assert address is not None
|
||||||
|
@ -326,8 +320,10 @@ async def async_setup_entry(
|
||||||
f"Bluetooth adapter {adapter} with address {address} not found"
|
f"Bluetooth adapter {adapter} with address {address} not found"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
passive = entry.options.get(CONF_PASSIVE)
|
||||||
|
mode = BluetoothScanningMode.PASSIVE if passive else BluetoothScanningMode.ACTIVE
|
||||||
try:
|
try:
|
||||||
bleak_scanner = create_bleak_scanner(BluetoothScanningMode.ACTIVE, adapter)
|
bleak_scanner = create_bleak_scanner(mode, adapter)
|
||||||
except RuntimeError as err:
|
except RuntimeError as err:
|
||||||
raise ConfigEntryNotReady(
|
raise ConfigEntryNotReady(
|
||||||
f"{adapter_human_name(adapter, address)}: {err}"
|
f"{adapter_human_name(adapter, address)}: {err}"
|
||||||
|
@ -342,12 +338,16 @@ async def async_setup_entry(
|
||||||
entry.async_on_unload(async_register_scanner(hass, scanner, True))
|
entry.async_on_unload(async_register_scanner(hass, scanner, True))
|
||||||
await async_update_device(hass, entry, adapter)
|
await async_update_device(hass, entry, adapter)
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = scanner
|
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = scanner
|
||||||
|
entry.async_on_unload(entry.add_update_listener(async_update_listener))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(
|
async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
hass: HomeAssistant, entry: config_entries.ConfigEntry
|
"""Handle options update."""
|
||||||
) -> bool:
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
scanner: HaScanner = hass.data[DOMAIN].pop(entry.entry_id)
|
scanner: HaScanner = hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
await scanner.async_stop()
|
await scanner.async_stop()
|
||||||
|
|
|
@ -1,15 +1,24 @@
|
||||||
"""Config flow to configure the Bluetooth integration."""
|
"""Config flow to configure the Bluetooth integration."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import platform
|
||||||
from typing import TYPE_CHECKING, Any, cast
|
from typing import TYPE_CHECKING, Any, cast
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import onboarding
|
from homeassistant.components import onboarding
|
||||||
from homeassistant.config_entries import ConfigFlow
|
from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
|
||||||
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.typing import DiscoveryInfoType
|
from homeassistant.helpers.typing import DiscoveryInfoType
|
||||||
|
|
||||||
from .const import ADAPTER_ADDRESS, CONF_ADAPTER, CONF_DETAILS, DOMAIN, AdapterDetails
|
from .const import (
|
||||||
|
ADAPTER_ADDRESS,
|
||||||
|
CONF_ADAPTER,
|
||||||
|
CONF_DETAILS,
|
||||||
|
CONF_PASSIVE,
|
||||||
|
DOMAIN,
|
||||||
|
AdapterDetails,
|
||||||
|
)
|
||||||
from .util import adapter_human_name, adapter_unique_name, async_get_bluetooth_adapters
|
from .util import adapter_human_name, adapter_unique_name, async_get_bluetooth_adapters
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -112,3 +121,42 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
) -> FlowResult:
|
) -> FlowResult:
|
||||||
"""Handle a flow initialized by the user."""
|
"""Handle a flow initialized by the user."""
|
||||||
return await self.async_step_multiple_adapters()
|
return await self.async_step_multiple_adapters()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@callback
|
||||||
|
def async_get_options_flow(
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
) -> OptionsFlowHandler:
|
||||||
|
"""Get the options flow for this handler."""
|
||||||
|
return OptionsFlowHandler(config_entry)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@callback
|
||||||
|
def async_supports_options_flow(cls, config_entry: ConfigEntry) -> bool:
|
||||||
|
"""Return options flow support for this handler."""
|
||||||
|
return platform.system() == "Linux"
|
||||||
|
|
||||||
|
|
||||||
|
class OptionsFlowHandler(OptionsFlow):
|
||||||
|
"""Handle the option flow for bluetooth."""
|
||||||
|
|
||||||
|
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||||
|
"""Initialize options flow."""
|
||||||
|
self.config_entry = config_entry
|
||||||
|
|
||||||
|
async def async_step_init(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Handle options flow."""
|
||||||
|
if user_input is not None:
|
||||||
|
return self.async_create_entry(title="", data=user_input)
|
||||||
|
|
||||||
|
data_schema = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(
|
||||||
|
CONF_PASSIVE,
|
||||||
|
default=self.config_entry.options.get(CONF_PASSIVE, False),
|
||||||
|
): bool,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return self.async_show_form(step_id="init", data_schema=data_schema)
|
||||||
|
|
|
@ -8,6 +8,7 @@ DOMAIN = "bluetooth"
|
||||||
|
|
||||||
CONF_ADAPTER = "adapter"
|
CONF_ADAPTER = "adapter"
|
||||||
CONF_DETAILS = "details"
|
CONF_DETAILS = "details"
|
||||||
|
CONF_PASSIVE = "passive"
|
||||||
|
|
||||||
WINDOWS_DEFAULT_BLUETOOTH_ADAPTER = "bluetooth"
|
WINDOWS_DEFAULT_BLUETOOTH_ADAPTER = "bluetooth"
|
||||||
MACOS_DEFAULT_BLUETOOTH_ADAPTER = "Core Bluetooth"
|
MACOS_DEFAULT_BLUETOOTH_ADAPTER = "Core Bluetooth"
|
||||||
|
|
|
@ -7,10 +7,14 @@ from datetime import datetime
|
||||||
import logging
|
import logging
|
||||||
import platform
|
import platform
|
||||||
import time
|
import time
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import async_timeout
|
import async_timeout
|
||||||
import bleak
|
import bleak
|
||||||
from bleak import BleakError
|
from bleak import BleakError
|
||||||
|
from bleak.assigned_numbers import AdvertisementDataType
|
||||||
|
from bleak.backends.bluezdbus.advertisement_monitor import OrPattern
|
||||||
|
from bleak.backends.bluezdbus.scanner import BlueZScannerArgs
|
||||||
from bleak.backends.device import BLEDevice
|
from bleak.backends.device import BLEDevice
|
||||||
from bleak.backends.scanner import AdvertisementData
|
from bleak.backends.scanner import AdvertisementData
|
||||||
from dbus_next import InvalidMessageError
|
from dbus_next import InvalidMessageError
|
||||||
|
@ -38,7 +42,15 @@ from .util import adapter_human_name, async_reset_adapter
|
||||||
OriginalBleakScanner = bleak.BleakScanner
|
OriginalBleakScanner = bleak.BleakScanner
|
||||||
MONOTONIC_TIME = time.monotonic
|
MONOTONIC_TIME = time.monotonic
|
||||||
|
|
||||||
|
# or_patterns is a workaround for the fact that passive scanning
|
||||||
|
# needs at least one matcher to be set. The below matcher
|
||||||
|
# will match all devices.
|
||||||
|
PASSIVE_SCANNER_ARGS = BlueZScannerArgs(
|
||||||
|
or_patterns=[
|
||||||
|
OrPattern(0, AdvertisementDataType.FLAGS, b"\x06"),
|
||||||
|
OrPattern(0, AdvertisementDataType.FLAGS, b"\x1a"),
|
||||||
|
]
|
||||||
|
)
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -81,13 +93,19 @@ def create_bleak_scanner(
|
||||||
scanning_mode: BluetoothScanningMode, adapter: str | None
|
scanning_mode: BluetoothScanningMode, adapter: str | None
|
||||||
) -> bleak.BleakScanner:
|
) -> bleak.BleakScanner:
|
||||||
"""Create a Bleak scanner."""
|
"""Create a Bleak scanner."""
|
||||||
scanner_kwargs = {"scanning_mode": SCANNING_MODE_TO_BLEAK[scanning_mode]}
|
scanner_kwargs: dict[str, Any] = {
|
||||||
|
"scanning_mode": SCANNING_MODE_TO_BLEAK[scanning_mode]
|
||||||
|
}
|
||||||
|
if platform.system() == "Linux":
|
||||||
# Only Linux supports multiple adapters
|
# Only Linux supports multiple adapters
|
||||||
if adapter and platform.system() == "Linux":
|
if adapter:
|
||||||
scanner_kwargs["adapter"] = adapter
|
scanner_kwargs["adapter"] = adapter
|
||||||
|
if scanning_mode == BluetoothScanningMode.PASSIVE:
|
||||||
|
scanner_kwargs["bluez"] = PASSIVE_SCANNER_ARGS
|
||||||
_LOGGER.debug("Initializing bluetooth scanner with %s", scanner_kwargs)
|
_LOGGER.debug("Initializing bluetooth scanner with %s", scanner_kwargs)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return OriginalBleakScanner(**scanner_kwargs) # type: ignore[arg-type]
|
return OriginalBleakScanner(**scanner_kwargs)
|
||||||
except (FileNotFoundError, BleakError) as ex:
|
except (FileNotFoundError, BleakError) as ex:
|
||||||
raise RuntimeError(f"Failed to initialize Bluetooth: {ex}") from ex
|
raise RuntimeError(f"Failed to initialize Bluetooth: {ex}") from ex
|
||||||
|
|
||||||
|
|
|
@ -25,5 +25,15 @@
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||||
"no_adapters": "No unconfigured Bluetooth adapters found"
|
"no_adapters": "No unconfigured Bluetooth adapters found"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"description": "Passive listening requires BlueZ 5.63 or later with experimental features enabled.",
|
||||||
|
"data": {
|
||||||
|
"passive": "Passive listening"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,6 @@
|
||||||
"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?"
|
|
||||||
},
|
|
||||||
"multiple_adapters": {
|
"multiple_adapters": {
|
||||||
"data": {
|
"data": {
|
||||||
"adapter": "Adapter"
|
"adapter": "Adapter"
|
||||||
|
@ -33,8 +30,9 @@
|
||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
"data": {
|
"data": {
|
||||||
"adapter": "The Bluetooth Adapter to use for scanning"
|
"passive": "Passive listening"
|
||||||
}
|
},
|
||||||
|
"description": "Passive listening requires BlueZ 5.63 or later with experimental features enabled."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,11 @@ def windows_adapter():
|
||||||
def one_adapter_fixture():
|
def one_adapter_fixture():
|
||||||
"""Fixture that mocks one adapter on Linux."""
|
"""Fixture that mocks one adapter on Linux."""
|
||||||
with patch(
|
with patch(
|
||||||
|
"homeassistant.components.bluetooth.platform.system", return_value="Linux"
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.bluetooth.scanner.platform.system",
|
||||||
|
return_value="Linux",
|
||||||
|
), patch(
|
||||||
"homeassistant.components.bluetooth.util.platform.system", return_value="Linux"
|
"homeassistant.components.bluetooth.util.platform.system", return_value="Linux"
|
||||||
), patch(
|
), patch(
|
||||||
"bluetooth_adapters.get_bluetooth_adapter_details",
|
"bluetooth_adapters.get_bluetooth_adapter_details",
|
||||||
|
|
|
@ -6,11 +6,13 @@ from homeassistant import config_entries
|
||||||
from homeassistant.components.bluetooth.const import (
|
from homeassistant.components.bluetooth.const import (
|
||||||
CONF_ADAPTER,
|
CONF_ADAPTER,
|
||||||
CONF_DETAILS,
|
CONF_DETAILS,
|
||||||
|
CONF_PASSIVE,
|
||||||
DEFAULT_ADDRESS,
|
DEFAULT_ADDRESS,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
AdapterDetails,
|
AdapterDetails,
|
||||||
)
|
)
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
@ -235,3 +237,105 @@ async def test_async_step_integration_discovery_already_exists(hass):
|
||||||
)
|
)
|
||||||
assert result["type"] == FlowResultType.ABORT
|
assert result["type"] == FlowResultType.ABORT
|
||||||
assert result["reason"] == "already_configured"
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_options_flow_linux(hass, mock_bleak_scanner_start, one_adapter):
|
||||||
|
"""Test options on Linux."""
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={},
|
||||||
|
options={},
|
||||||
|
unique_id="00:00:00:00:00:01",
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "init"
|
||||||
|
assert result["errors"] is None
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={
|
||||||
|
CONF_PASSIVE: True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["data"][CONF_PASSIVE] is True
|
||||||
|
|
||||||
|
# Verify we can change it to False
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "init"
|
||||||
|
assert result["errors"] is None
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={
|
||||||
|
CONF_PASSIVE: False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["data"][CONF_PASSIVE] is False
|
||||||
|
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"homeassistant.components.bluetooth.config_flow.platform.system",
|
||||||
|
return_value="Darwin",
|
||||||
|
)
|
||||||
|
async def test_options_flow_disabled_macos(mock_system, hass, hass_ws_client):
|
||||||
|
"""Test options are disabled on MacOS."""
|
||||||
|
await async_setup_component(hass, "config", {})
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={},
|
||||||
|
options={},
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
ws_client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
await ws_client.send_json(
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"type": "config_entries/get",
|
||||||
|
"domain": "bluetooth",
|
||||||
|
"type_filter": "integration",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response = await ws_client.receive_json()
|
||||||
|
assert response["result"][0]["supports_options"] is False
|
||||||
|
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"homeassistant.components.bluetooth.config_flow.platform.system",
|
||||||
|
return_value="Linux",
|
||||||
|
)
|
||||||
|
async def test_options_flow_enabled_linux(mock_system, hass, hass_ws_client):
|
||||||
|
"""Test options are enabled on Linux."""
|
||||||
|
await async_setup_component(hass, "config", {})
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={},
|
||||||
|
options={},
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
ws_client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
await ws_client.send_json(
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"type": "config_entries/get",
|
||||||
|
"domain": "bluetooth",
|
||||||
|
"type_filter": "integration",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response = await ws_client.receive_json()
|
||||||
|
assert response["result"][0]["supports_options"] is True
|
||||||
|
|
|
@ -20,6 +20,7 @@ from homeassistant.components.bluetooth import (
|
||||||
scanner,
|
scanner,
|
||||||
)
|
)
|
||||||
from homeassistant.components.bluetooth.const import (
|
from homeassistant.components.bluetooth.const import (
|
||||||
|
CONF_PASSIVE,
|
||||||
DEFAULT_ADDRESS,
|
DEFAULT_ADDRESS,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SOURCE_LOCAL,
|
SOURCE_LOCAL,
|
||||||
|
@ -61,6 +62,52 @@ async def test_setup_and_stop(hass, mock_bleak_scanner_start, enable_bluetooth):
|
||||||
assert len(mock_bleak_scanner_start.mock_calls) == 1
|
assert len(mock_bleak_scanner_start.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_and_stop_passive(hass, mock_bleak_scanner_start, one_adapter):
|
||||||
|
"""Test we and setup and stop the scanner the passive scanner."""
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=bluetooth.DOMAIN,
|
||||||
|
data={},
|
||||||
|
options={CONF_PASSIVE: True},
|
||||||
|
unique_id="00:00:00:00:00:01",
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
init_kwargs = None
|
||||||
|
|
||||||
|
class MockPassiveBleakScanner:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""Init the scanner."""
|
||||||
|
nonlocal init_kwargs
|
||||||
|
init_kwargs = kwargs
|
||||||
|
|
||||||
|
async def start(self, *args, **kwargs):
|
||||||
|
"""Start the scanner."""
|
||||||
|
|
||||||
|
async def stop(self, *args, **kwargs):
|
||||||
|
"""Stop the scanner."""
|
||||||
|
|
||||||
|
def register_detection_callback(self, *args, **kwargs):
|
||||||
|
"""Register a callback."""
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.bluetooth.scanner.OriginalBleakScanner",
|
||||||
|
MockPassiveBleakScanner,
|
||||||
|
):
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
|
||||||
|
)
|
||||||
|
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 init_kwargs == {
|
||||||
|
"adapter": "hci0",
|
||||||
|
"bluez": scanner.PASSIVE_SCANNER_ARGS,
|
||||||
|
"scanning_mode": "passive",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_setup_and_stop_no_bluetooth(hass, caplog, macos_adapter):
|
async def test_setup_and_stop_no_bluetooth(hass, caplog, macos_adapter):
|
||||||
"""Test we fail gracefully when bluetooth is not available."""
|
"""Test we fail gracefully when bluetooth is not available."""
|
||||||
mock_bt = [
|
mock_bt = [
|
||||||
|
|
Loading…
Reference in New Issue