mirror of
https://github.com/home-assistant/core.git
synced 2025-07-14 16:57:10 +00:00
Add Bluetooth config entries for remote scanners (#135543)
This commit is contained in:
parent
93b3d76ee2
commit
6e255060c6
@ -78,6 +78,9 @@ from .const import (
|
|||||||
CONF_ADAPTER,
|
CONF_ADAPTER,
|
||||||
CONF_DETAILS,
|
CONF_DETAILS,
|
||||||
CONF_PASSIVE,
|
CONF_PASSIVE,
|
||||||
|
CONF_SOURCE_CONFIG_ENTRY_ID,
|
||||||
|
CONF_SOURCE_DOMAIN,
|
||||||
|
CONF_SOURCE_MODEL,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
|
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
|
||||||
LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS,
|
LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS,
|
||||||
@ -315,6 +318,32 @@ async def async_update_device(
|
|||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up a config entry for a bluetooth scanner."""
|
"""Set up a config entry for a bluetooth scanner."""
|
||||||
|
if source_entry_id := entry.data.get(CONF_SOURCE_CONFIG_ENTRY_ID):
|
||||||
|
if not (source_entry := hass.config_entries.async_get_entry(source_entry_id)):
|
||||||
|
# Cleanup the orphaned entry using a call_soon to ensure
|
||||||
|
# we can return before the entry is removed
|
||||||
|
hass.loop.call_soon(
|
||||||
|
hass_callback(
|
||||||
|
lambda: hass.async_create_task(
|
||||||
|
hass.config_entries.async_remove(entry.entry_id),
|
||||||
|
"remove orphaned bluetooth entry {entry.entry_id}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
address = entry.unique_id
|
||||||
|
assert address is not None
|
||||||
|
assert source_entry is not None
|
||||||
|
await async_update_device(
|
||||||
|
hass,
|
||||||
|
entry,
|
||||||
|
source_entry.title,
|
||||||
|
AdapterDetails(
|
||||||
|
address=address,
|
||||||
|
product=entry.data.get(CONF_SOURCE_MODEL),
|
||||||
|
manufacturer=entry.data[CONF_SOURCE_DOMAIN],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return True
|
||||||
manager = _get_manager(hass)
|
manager = _get_manager(hass)
|
||||||
address = entry.unique_id
|
address = entry.unique_id
|
||||||
assert address is not None
|
assert address is not None
|
||||||
|
@ -178,9 +178,14 @@ def async_register_scanner(
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
scanner: BaseHaScanner,
|
scanner: BaseHaScanner,
|
||||||
connection_slots: int | None = None,
|
connection_slots: int | None = None,
|
||||||
|
source_domain: str | None = None,
|
||||||
|
source_model: str | None = None,
|
||||||
|
source_config_entry_id: str | None = None,
|
||||||
) -> CALLBACK_TYPE:
|
) -> CALLBACK_TYPE:
|
||||||
"""Register a BleakScanner."""
|
"""Register a BleakScanner."""
|
||||||
return _get_manager(hass).async_register_scanner(scanner, connection_slots)
|
return _get_manager(hass).async_register_hass_scanner(
|
||||||
|
scanner, connection_slots, source_domain, source_model, source_config_entry_id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@hass_callback
|
@hass_callback
|
||||||
|
@ -18,7 +18,12 @@ from habluetooth import get_manager
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import onboarding
|
from homeassistant.components import onboarding
|
||||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import (
|
||||||
|
ConfigEntry,
|
||||||
|
ConfigFlow,
|
||||||
|
ConfigFlowResult,
|
||||||
|
OptionsFlow,
|
||||||
|
)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.schema_config_entry_flow import (
|
from homeassistant.helpers.schema_config_entry_flow import (
|
||||||
SchemaFlowFormStep,
|
SchemaFlowFormStep,
|
||||||
@ -26,7 +31,16 @@ from homeassistant.helpers.schema_config_entry_flow import (
|
|||||||
)
|
)
|
||||||
from homeassistant.helpers.typing import DiscoveryInfoType
|
from homeassistant.helpers.typing import DiscoveryInfoType
|
||||||
|
|
||||||
from .const import CONF_ADAPTER, CONF_DETAILS, CONF_PASSIVE, DOMAIN
|
from .const import (
|
||||||
|
CONF_ADAPTER,
|
||||||
|
CONF_DETAILS,
|
||||||
|
CONF_PASSIVE,
|
||||||
|
CONF_SOURCE,
|
||||||
|
CONF_SOURCE_CONFIG_ENTRY_ID,
|
||||||
|
CONF_SOURCE_DOMAIN,
|
||||||
|
CONF_SOURCE_MODEL,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
from .util import adapter_title
|
from .util import adapter_title
|
||||||
|
|
||||||
OPTIONS_SCHEMA = vol.Schema(
|
OPTIONS_SCHEMA = vol.Schema(
|
||||||
@ -63,6 +77,8 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
self, discovery_info: DiscoveryInfoType
|
self, discovery_info: DiscoveryInfoType
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Handle a flow initialized by discovery."""
|
"""Handle a flow initialized by discovery."""
|
||||||
|
if discovery_info and CONF_SOURCE in discovery_info:
|
||||||
|
return await self.async_step_external_scanner(discovery_info)
|
||||||
self._adapter = cast(str, discovery_info[CONF_ADAPTER])
|
self._adapter = cast(str, discovery_info[CONF_ADAPTER])
|
||||||
self._details = cast(AdapterDetails, discovery_info[CONF_DETAILS])
|
self._details = cast(AdapterDetails, discovery_info[CONF_DETAILS])
|
||||||
await self.async_set_unique_id(self._details[ADAPTER_ADDRESS])
|
await self.async_set_unique_id(self._details[ADAPTER_ADDRESS])
|
||||||
@ -167,6 +183,24 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def async_step_external_scanner(
|
||||||
|
self, user_input: dict[str, Any]
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle a flow initialized by an external scanner."""
|
||||||
|
source = user_input[CONF_SOURCE]
|
||||||
|
await self.async_set_unique_id(source)
|
||||||
|
data = {
|
||||||
|
CONF_SOURCE: source,
|
||||||
|
CONF_SOURCE_MODEL: user_input[CONF_SOURCE_MODEL],
|
||||||
|
CONF_SOURCE_DOMAIN: user_input[CONF_SOURCE_DOMAIN],
|
||||||
|
CONF_SOURCE_CONFIG_ENTRY_ID: user_input[CONF_SOURCE_CONFIG_ENTRY_ID],
|
||||||
|
}
|
||||||
|
self._abort_if_unique_id_configured(updates=data)
|
||||||
|
manager = get_manager()
|
||||||
|
scanner = manager.async_scanner_by_source(source)
|
||||||
|
assert scanner is not None
|
||||||
|
return self.async_create_entry(title=scanner.name, data=data)
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
@ -177,8 +211,10 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
@callback
|
@callback
|
||||||
def async_get_options_flow(
|
def async_get_options_flow(
|
||||||
config_entry: ConfigEntry,
|
config_entry: ConfigEntry,
|
||||||
) -> SchemaOptionsFlowHandler:
|
) -> SchemaOptionsFlowHandler | RemoteAdapterOptionsFlowHandler:
|
||||||
"""Get the options flow for this handler."""
|
"""Get the options flow for this handler."""
|
||||||
|
if CONF_SOURCE in config_entry.data:
|
||||||
|
return RemoteAdapterOptionsFlowHandler()
|
||||||
return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW)
|
return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -186,3 +222,13 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
def async_supports_options_flow(cls, config_entry: ConfigEntry) -> bool:
|
def async_supports_options_flow(cls, config_entry: ConfigEntry) -> bool:
|
||||||
"""Return options flow support for this handler."""
|
"""Return options flow support for this handler."""
|
||||||
return bool((manager := get_manager()) and manager.supports_passive_scan)
|
return bool((manager := get_manager()) and manager.supports_passive_scan)
|
||||||
|
|
||||||
|
|
||||||
|
class RemoteAdapterOptionsFlowHandler(OptionsFlow):
|
||||||
|
"""Handle a option flow for remote adapters."""
|
||||||
|
|
||||||
|
async def async_step_init(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle options flow."""
|
||||||
|
return self.async_abort(reason="remote_adapters_not_supported")
|
||||||
|
@ -18,6 +18,12 @@ CONF_DETAILS = "details"
|
|||||||
CONF_PASSIVE = "passive"
|
CONF_PASSIVE = "passive"
|
||||||
|
|
||||||
|
|
||||||
|
CONF_SOURCE: Final = "source"
|
||||||
|
CONF_SOURCE_DOMAIN: Final = "source_domain"
|
||||||
|
CONF_SOURCE_MODEL: Final = "source_model"
|
||||||
|
CONF_SOURCE_CONFIG_ENTRY_ID: Final = "source_config_entry_id"
|
||||||
|
|
||||||
|
|
||||||
SOURCE_LOCAL: Final = "local"
|
SOURCE_LOCAL: Final = "local"
|
||||||
|
|
||||||
DATA_MANAGER: Final = "bluetooth_manager"
|
DATA_MANAGER: Final = "bluetooth_manager"
|
||||||
|
@ -22,7 +22,13 @@ from homeassistant.core import (
|
|||||||
from homeassistant.helpers import discovery_flow
|
from homeassistant.helpers import discovery_flow
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import (
|
||||||
|
CONF_SOURCE,
|
||||||
|
CONF_SOURCE_CONFIG_ENTRY_ID,
|
||||||
|
CONF_SOURCE_DOMAIN,
|
||||||
|
CONF_SOURCE_MODEL,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
from .match import (
|
from .match import (
|
||||||
ADDRESS,
|
ADDRESS,
|
||||||
CALLBACK,
|
CALLBACK,
|
||||||
@ -240,6 +246,39 @@ class HomeAssistantBluetoothManager(BluetoothManager):
|
|||||||
unregister()
|
unregister()
|
||||||
self._async_save_scanner_history(scanner)
|
self._async_save_scanner_history(scanner)
|
||||||
|
|
||||||
|
@hass_callback
|
||||||
|
def async_register_hass_scanner(
|
||||||
|
self,
|
||||||
|
scanner: BaseHaScanner,
|
||||||
|
connection_slots: int | None = None,
|
||||||
|
source_domain: str | None = None,
|
||||||
|
source_model: str | None = None,
|
||||||
|
source_config_entry_id: str | None = None,
|
||||||
|
) -> CALLBACK_TYPE:
|
||||||
|
"""Register a scanner."""
|
||||||
|
cancel = self.async_register_scanner(scanner, connection_slots)
|
||||||
|
if (
|
||||||
|
isinstance(scanner, BaseHaRemoteScanner)
|
||||||
|
and source_domain
|
||||||
|
and source_config_entry_id
|
||||||
|
and not self.hass.config_entries.async_entry_for_domain_unique_id(
|
||||||
|
DOMAIN, scanner.source
|
||||||
|
)
|
||||||
|
):
|
||||||
|
self.hass.async_create_task(
|
||||||
|
self.hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||||
|
data={
|
||||||
|
CONF_SOURCE: scanner.source,
|
||||||
|
CONF_SOURCE_DOMAIN: source_domain,
|
||||||
|
CONF_SOURCE_MODEL: source_model,
|
||||||
|
CONF_SOURCE_CONFIG_ENTRY_ID: source_config_entry_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return cancel
|
||||||
|
|
||||||
def async_register_scanner(
|
def async_register_scanner(
|
||||||
self,
|
self,
|
||||||
scanner: BaseHaScanner,
|
scanner: BaseHaScanner,
|
||||||
@ -257,6 +296,13 @@ class HomeAssistantBluetoothManager(BluetoothManager):
|
|||||||
def async_remove_scanner(self, source: str) -> None:
|
def async_remove_scanner(self, source: str) -> None:
|
||||||
"""Remove a scanner."""
|
"""Remove a scanner."""
|
||||||
self.storage.async_remove_advertisement_history(source)
|
self.storage.async_remove_advertisement_history(source)
|
||||||
|
if entry := self.hass.config_entries.async_entry_for_domain_unique_id(
|
||||||
|
DOMAIN, source
|
||||||
|
):
|
||||||
|
self.hass.async_create_task(
|
||||||
|
self.hass.config_entries.async_remove(entry.entry_id),
|
||||||
|
f"Removing {source} Bluetooth config entry",
|
||||||
|
)
|
||||||
|
|
||||||
@hass_callback
|
@hass_callback
|
||||||
def _handle_config_entry_removed(
|
def _handle_config_entry_removed(
|
||||||
|
@ -33,6 +33,9 @@
|
|||||||
"passive": "Passive scanning"
|
"passive": "Passive scanning"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"remote_adapters_not_supported": "Bluetooth configuration for remote adapters is not supported."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ from bleak_esphome import connect_scanner
|
|||||||
from homeassistant.components.bluetooth import async_register_scanner
|
from homeassistant.components.bluetooth import async_register_scanner
|
||||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
from .entry_data import RuntimeEntryData
|
from .entry_data import RuntimeEntryData
|
||||||
|
|
||||||
|
|
||||||
@ -38,7 +39,13 @@ def async_connect_scanner(
|
|||||||
return partial(
|
return partial(
|
||||||
_async_unload,
|
_async_unload,
|
||||||
[
|
[
|
||||||
async_register_scanner(hass, scanner),
|
async_register_scanner(
|
||||||
|
hass,
|
||||||
|
scanner,
|
||||||
|
source_domain=DOMAIN,
|
||||||
|
source_model=device_info.model,
|
||||||
|
source_config_entry_id=entry_data.entry_id,
|
||||||
|
),
|
||||||
scanner.async_setup(),
|
scanner.async_setup(),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -28,7 +28,13 @@ async def async_connect_scanner(
|
|||||||
source = format_mac(coordinator.mac).upper()
|
source = format_mac(coordinator.mac).upper()
|
||||||
scanner = create_scanner(source, entry.title)
|
scanner = create_scanner(source, entry.title)
|
||||||
unload_callbacks = [
|
unload_callbacks = [
|
||||||
async_register_scanner(hass, scanner),
|
async_register_scanner(
|
||||||
|
hass,
|
||||||
|
scanner,
|
||||||
|
source_domain=entry.domain,
|
||||||
|
source_model=coordinator.model,
|
||||||
|
source_config_entry_id=entry.entry_id,
|
||||||
|
),
|
||||||
scanner.async_setup(),
|
scanner.async_setup(),
|
||||||
coordinator.async_subscribe_events(scanner.async_on_event),
|
coordinator.async_subscribe_events(scanner.async_on_event),
|
||||||
]
|
]
|
||||||
|
@ -14,7 +14,9 @@ from habluetooth import BaseHaScanner, BluetoothManager, get_manager
|
|||||||
|
|
||||||
from homeassistant.components.bluetooth import (
|
from homeassistant.components.bluetooth import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
MONOTONIC_TIME,
|
||||||
SOURCE_LOCAL,
|
SOURCE_LOCAL,
|
||||||
|
BaseHaRemoteScanner,
|
||||||
BluetoothServiceInfo,
|
BluetoothServiceInfo,
|
||||||
BluetoothServiceInfoBleak,
|
BluetoothServiceInfoBleak,
|
||||||
async_get_advertisement_callback,
|
async_get_advertisement_callback,
|
||||||
@ -324,3 +326,26 @@ class FakeScanner(FakeScannerMixin, BaseHaScanner):
|
|||||||
) -> dict[str, tuple[BLEDevice, AdvertisementData]]:
|
) -> dict[str, tuple[BLEDevice, AdvertisementData]]:
|
||||||
"""Return a list of discovered devices and their advertisement data."""
|
"""Return a list of discovered devices and their advertisement data."""
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class FakeRemoteScanner(BaseHaRemoteScanner):
|
||||||
|
"""Fake remote scanner."""
|
||||||
|
|
||||||
|
def inject_advertisement(
|
||||||
|
self,
|
||||||
|
device: BLEDevice,
|
||||||
|
advertisement_data: AdvertisementData,
|
||||||
|
now: float | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Inject an advertisement."""
|
||||||
|
self._async_on_advertisement(
|
||||||
|
device.address,
|
||||||
|
advertisement_data.rssi,
|
||||||
|
device.name,
|
||||||
|
advertisement_data.service_uuids,
|
||||||
|
advertisement_data.service_data,
|
||||||
|
advertisement_data.manufacturer_data,
|
||||||
|
advertisement_data.tx_power,
|
||||||
|
{"scanner_specific_data": "test"},
|
||||||
|
now or MONOTONIC_TIME(),
|
||||||
|
)
|
||||||
|
@ -7,16 +7,12 @@ import time
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from bleak.backends.device import BLEDevice
|
|
||||||
from bleak.backends.scanner import AdvertisementData
|
|
||||||
|
|
||||||
# pylint: disable-next=no-name-in-module
|
# pylint: disable-next=no-name-in-module
|
||||||
from habluetooth.advertisement_tracker import TRACKER_BUFFERING_WOBBLE_SECONDS
|
from habluetooth.advertisement_tracker import TRACKER_BUFFERING_WOBBLE_SECONDS
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components import bluetooth
|
from homeassistant.components import bluetooth
|
||||||
from homeassistant.components.bluetooth import (
|
from homeassistant.components.bluetooth import (
|
||||||
MONOTONIC_TIME,
|
|
||||||
BaseHaRemoteScanner,
|
BaseHaRemoteScanner,
|
||||||
HaBluetoothConnector,
|
HaBluetoothConnector,
|
||||||
storage,
|
storage,
|
||||||
@ -28,12 +24,14 @@ from homeassistant.components.bluetooth.const import (
|
|||||||
SCANNER_WATCHDOG_TIMEOUT,
|
SCANNER_WATCHDOG_TIMEOUT,
|
||||||
UNAVAILABLE_TRACK_SECONDS,
|
UNAVAILABLE_TRACK_SECONDS,
|
||||||
)
|
)
|
||||||
|
from homeassistant.components.bluetooth.manager import HomeAssistantBluetoothManager
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.util.json import json_loads
|
from homeassistant.util.json import json_loads
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
|
FakeRemoteScanner as FakeScanner,
|
||||||
MockBleakClient,
|
MockBleakClient,
|
||||||
_get_manager,
|
_get_manager,
|
||||||
generate_advertisement_data,
|
generate_advertisement_data,
|
||||||
@ -41,30 +39,7 @@ from . import (
|
|||||||
patch_bluetooth_time,
|
patch_bluetooth_time,
|
||||||
)
|
)
|
||||||
|
|
||||||
from tests.common import async_fire_time_changed, load_fixture
|
from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture
|
||||||
|
|
||||||
|
|
||||||
class FakeScanner(BaseHaRemoteScanner):
|
|
||||||
"""Fake scanner."""
|
|
||||||
|
|
||||||
def inject_advertisement(
|
|
||||||
self,
|
|
||||||
device: BLEDevice,
|
|
||||||
advertisement_data: AdvertisementData,
|
|
||||||
now: float | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""Inject an advertisement."""
|
|
||||||
self._async_on_advertisement(
|
|
||||||
device.address,
|
|
||||||
advertisement_data.rssi,
|
|
||||||
device.name,
|
|
||||||
advertisement_data.service_uuids,
|
|
||||||
advertisement_data.service_data,
|
|
||||||
advertisement_data.manufacturer_data,
|
|
||||||
advertisement_data.tx_power,
|
|
||||||
{"scanner_specific_data": "test"},
|
|
||||||
now or MONOTONIC_TIME(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("name_2", [None, "w"])
|
@pytest.mark.parametrize("name_2", [None, "w"])
|
||||||
@ -545,3 +520,53 @@ async def test_scanner_stops_responding(hass: HomeAssistant) -> None:
|
|||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
unsetup()
|
unsetup()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("enable_bluetooth")
|
||||||
|
async def test_remote_scanner_bluetooth_config_entry(hass: HomeAssistant) -> None:
|
||||||
|
"""Test the remote scanner gets a bluetooth config entry."""
|
||||||
|
manager: HomeAssistantBluetoothManager = _get_manager()
|
||||||
|
|
||||||
|
switchbot_device = generate_ble_device(
|
||||||
|
"44:44:33:11:23:45",
|
||||||
|
"wohand",
|
||||||
|
{},
|
||||||
|
rssi=-100,
|
||||||
|
)
|
||||||
|
switchbot_device_adv = generate_advertisement_data(
|
||||||
|
local_name="wohand",
|
||||||
|
service_uuids=[],
|
||||||
|
manufacturer_data={1: b"\x01"},
|
||||||
|
rssi=-100,
|
||||||
|
)
|
||||||
|
|
||||||
|
connector = (
|
||||||
|
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
|
||||||
|
)
|
||||||
|
scanner = FakeScanner("esp32", "esp32", connector, True)
|
||||||
|
unsetup = scanner.async_setup()
|
||||||
|
entry = MockConfigEntry(domain="test")
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
cancel = manager.async_register_hass_scanner(
|
||||||
|
scanner,
|
||||||
|
source_domain="test",
|
||||||
|
source_model="test",
|
||||||
|
source_config_entry_id=entry.entry_id,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
scanner.inject_advertisement(switchbot_device, switchbot_device_adv)
|
||||||
|
assert len(scanner.discovered_devices) == 1
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
unsetup()
|
||||||
|
|
||||||
|
assert hass.config_entries.async_entry_for_domain_unique_id(
|
||||||
|
"bluetooth", scanner.source
|
||||||
|
)
|
||||||
|
|
||||||
|
manager.async_remove_scanner(scanner.source)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert not hass.config_entries.async_entry_for_domain_unique_id(
|
||||||
|
"bluetooth", scanner.source
|
||||||
|
)
|
||||||
|
@ -6,16 +6,23 @@ from bluetooth_adapters import DEFAULT_ADDRESS, AdapterDetails
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components.bluetooth import HaBluetoothConnector
|
||||||
from homeassistant.components.bluetooth.const import (
|
from homeassistant.components.bluetooth.const import (
|
||||||
CONF_ADAPTER,
|
CONF_ADAPTER,
|
||||||
CONF_DETAILS,
|
CONF_DETAILS,
|
||||||
CONF_PASSIVE,
|
CONF_PASSIVE,
|
||||||
|
CONF_SOURCE,
|
||||||
|
CONF_SOURCE_CONFIG_ENTRY_ID,
|
||||||
|
CONF_SOURCE_DOMAIN,
|
||||||
|
CONF_SOURCE_MODEL,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from . import FakeRemoteScanner, MockBleakClient, _get_manager
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
from tests.typing import WebSocketGenerator
|
from tests.typing import WebSocketGenerator
|
||||||
|
|
||||||
@ -450,6 +457,36 @@ async def test_options_flow_enabled_linux(
|
|||||||
await hass.config_entries.async_unload(entry.entry_id)
|
await hass.config_entries.async_unload(entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures(
|
||||||
|
"one_adapter", "mock_bleak_scanner_start", "mock_bluetooth_adapters"
|
||||||
|
)
|
||||||
|
async def test_options_flow_remote_adapter(hass: HomeAssistant) -> None:
|
||||||
|
"""Test options are not available for remote adapters."""
|
||||||
|
source_entry = MockConfigEntry(
|
||||||
|
domain="test",
|
||||||
|
)
|
||||||
|
source_entry.add_to_hass(hass)
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={
|
||||||
|
CONF_SOURCE: "BB:BB:BB:BB:BB:BB",
|
||||||
|
CONF_SOURCE_DOMAIN: "test",
|
||||||
|
CONF_SOURCE_MODEL: "test",
|
||||||
|
CONF_SOURCE_CONFIG_ENTRY_ID: source_entry.entry_id,
|
||||||
|
},
|
||||||
|
options={},
|
||||||
|
unique_id="BB:BB:BB:BB:BB:BB",
|
||||||
|
)
|
||||||
|
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"] is FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "remote_adapters_not_supported"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("one_adapter")
|
@pytest.mark.usefixtures("one_adapter")
|
||||||
async def test_async_step_user_linux_adapter_is_ignored(hass: HomeAssistant) -> None:
|
async def test_async_step_user_linux_adapter_is_ignored(hass: HomeAssistant) -> None:
|
||||||
"""Test we give a hint that the adapter is ignored."""
|
"""Test we give a hint that the adapter is ignored."""
|
||||||
@ -467,3 +504,49 @@ async def test_async_step_user_linux_adapter_is_ignored(hass: HomeAssistant) ->
|
|||||||
assert result["type"] is FlowResultType.ABORT
|
assert result["type"] is FlowResultType.ABORT
|
||||||
assert result["reason"] == "no_adapters"
|
assert result["reason"] == "no_adapters"
|
||||||
assert result["description_placeholders"] == {"ignored_adapters": "1"}
|
assert result["description_placeholders"] == {"ignored_adapters": "1"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("enable_bluetooth")
|
||||||
|
async def test_async_step_integration_discovery_remote_adapter(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
) -> None:
|
||||||
|
"""Test remote adapter configuration via integration discovery."""
|
||||||
|
entry = MockConfigEntry(domain="test")
|
||||||
|
connector = (
|
||||||
|
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
|
||||||
|
)
|
||||||
|
scanner = FakeRemoteScanner("esp32", "esp32", connector, True)
|
||||||
|
manager = _get_manager()
|
||||||
|
cancel_scanner = manager.async_register_scanner(scanner)
|
||||||
|
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
with (
|
||||||
|
patch("homeassistant.components.bluetooth.async_setup", return_value=True),
|
||||||
|
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_INTEGRATION_DISCOVERY},
|
||||||
|
data={
|
||||||
|
CONF_SOURCE: scanner.source,
|
||||||
|
CONF_SOURCE_DOMAIN: "test",
|
||||||
|
CONF_SOURCE_MODEL: "test",
|
||||||
|
CONF_SOURCE_CONFIG_ENTRY_ID: entry.entry_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == "esp32"
|
||||||
|
assert result["data"] == {
|
||||||
|
CONF_SOURCE: scanner.source,
|
||||||
|
CONF_SOURCE_DOMAIN: "test",
|
||||||
|
CONF_SOURCE_MODEL: "test",
|
||||||
|
CONF_SOURCE_CONFIG_ENTRY_ID: entry.entry_id,
|
||||||
|
}
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await hass.config_entries.async_unload(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
cancel_scanner()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
@ -18,6 +18,7 @@ from homeassistant.components.bluetooth import (
|
|||||||
BluetoothChange,
|
BluetoothChange,
|
||||||
BluetoothScanningMode,
|
BluetoothScanningMode,
|
||||||
BluetoothServiceInfo,
|
BluetoothServiceInfo,
|
||||||
|
HaBluetoothConnector,
|
||||||
async_process_advertisements,
|
async_process_advertisements,
|
||||||
async_rediscover_address,
|
async_rediscover_address,
|
||||||
async_track_unavailable,
|
async_track_unavailable,
|
||||||
@ -25,6 +26,10 @@ from homeassistant.components.bluetooth import (
|
|||||||
from homeassistant.components.bluetooth.const import (
|
from homeassistant.components.bluetooth.const import (
|
||||||
BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS,
|
BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS,
|
||||||
CONF_PASSIVE,
|
CONF_PASSIVE,
|
||||||
|
CONF_SOURCE,
|
||||||
|
CONF_SOURCE_CONFIG_ENTRY_ID,
|
||||||
|
CONF_SOURCE_DOMAIN,
|
||||||
|
CONF_SOURCE_MODEL,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS,
|
LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS,
|
||||||
SOURCE_LOCAL,
|
SOURCE_LOCAL,
|
||||||
@ -47,7 +52,9 @@ from homeassistant.setup import async_setup_component
|
|||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
|
FakeRemoteScanner,
|
||||||
FakeScanner,
|
FakeScanner,
|
||||||
|
MockBleakClient,
|
||||||
_get_manager,
|
_get_manager,
|
||||||
async_setup_with_default_adapter,
|
async_setup_with_default_adapter,
|
||||||
async_setup_with_one_adapter,
|
async_setup_with_one_adapter,
|
||||||
@ -3263,3 +3270,33 @@ async def test_title_updated_if_mac_address(
|
|||||||
await hass.config_entries.async_setup(entry.entry_id)
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert entry.title == "ACME Bluetooth Adapter 5.0 (00:00:00:00:00:01)"
|
assert entry.title == "ACME Bluetooth Adapter 5.0 (00:00:00:00:00:01)"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("enable_bluetooth")
|
||||||
|
async def test_cleanup_orphened_remote_scanner_config_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
) -> None:
|
||||||
|
"""Test the remote scanner config entries get cleaned up when orphened."""
|
||||||
|
connector = (
|
||||||
|
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
|
||||||
|
)
|
||||||
|
scanner = FakeRemoteScanner("esp32", "esp32", connector, True)
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={
|
||||||
|
CONF_SOURCE: scanner.source,
|
||||||
|
CONF_SOURCE_DOMAIN: "test",
|
||||||
|
CONF_SOURCE_MODEL: "test",
|
||||||
|
CONF_SOURCE_CONFIG_ENTRY_ID: "no_longer_exists",
|
||||||
|
},
|
||||||
|
unique_id=scanner.source,
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Orphened remote scanner config entry should be cleaned up
|
||||||
|
assert not hass.config_entries.async_entry_for_domain_unique_id(
|
||||||
|
"bluetooth", scanner.source
|
||||||
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user