Restore history from bluetooth stack at startup (#78612)
This commit is contained in:
parent
13d3f4c3b2
commit
18eef5da1f
16 changed files with 151 additions and 55 deletions
|
@ -228,7 +228,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
integration_matcher = IntegrationMatcher(await async_get_bluetooth(hass))
|
integration_matcher = IntegrationMatcher(await async_get_bluetooth(hass))
|
||||||
integration_matcher.async_setup()
|
integration_matcher.async_setup()
|
||||||
manager = BluetoothManager(hass, integration_matcher)
|
manager = BluetoothManager(hass, integration_matcher)
|
||||||
manager.async_setup()
|
await 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()
|
||||||
|
|
|
@ -45,7 +45,7 @@ from .models import (
|
||||||
BluetoothServiceInfoBleak,
|
BluetoothServiceInfoBleak,
|
||||||
)
|
)
|
||||||
from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher
|
from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher
|
||||||
from .util import async_get_bluetooth_adapters
|
from .util import async_get_bluetooth_adapters, async_load_history_from_system
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from bleak.backends.device import BLEDevice
|
from bleak.backends.device import BLEDevice
|
||||||
|
@ -213,10 +213,15 @@ class BluetoothManager:
|
||||||
self._adapters = await async_get_bluetooth_adapters()
|
self._adapters = await async_get_bluetooth_adapters()
|
||||||
return self._find_adapter_by_address(address)
|
return self._find_adapter_by_address(address)
|
||||||
|
|
||||||
@hass_callback
|
async def async_setup(self) -> None:
|
||||||
def async_setup(self) -> None:
|
|
||||||
"""Set up the bluetooth manager."""
|
"""Set up the bluetooth manager."""
|
||||||
install_multiple_bleak_catcher()
|
install_multiple_bleak_catcher()
|
||||||
|
history = await async_load_history_from_system()
|
||||||
|
# Everything is connectable so it fall into both
|
||||||
|
# buckets since the host system can only provide
|
||||||
|
# connectable devices
|
||||||
|
self._history = history.copy()
|
||||||
|
self._connectable_history = history.copy()
|
||||||
self.async_setup_unavailable_tracking()
|
self.async_setup_unavailable_tracking()
|
||||||
|
|
||||||
@hass_callback
|
@hass_callback
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"bleak==0.17.0",
|
"bleak==0.17.0",
|
||||||
"bleak-retry-connector==1.17.1",
|
"bleak-retry-connector==1.17.1",
|
||||||
"bluetooth-adapters==0.4.1",
|
"bluetooth-adapters==0.5.1",
|
||||||
"bluetooth-auto-recovery==0.3.3",
|
"bluetooth-auto-recovery==0.3.3",
|
||||||
"dbus-fast==1.4.0"
|
"dbus-fast==1.4.0"
|
||||||
],
|
],
|
||||||
|
|
|
@ -19,13 +19,7 @@ from bleak.backends.device import BLEDevice
|
||||||
from bleak.backends.scanner import AdvertisementData
|
from bleak.backends.scanner import AdvertisementData
|
||||||
from dbus_fast import InvalidMessageError
|
from dbus_fast import InvalidMessageError
|
||||||
|
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback
|
||||||
from homeassistant.core import (
|
|
||||||
CALLBACK_TYPE,
|
|
||||||
Event,
|
|
||||||
HomeAssistant,
|
|
||||||
callback as hass_callback,
|
|
||||||
)
|
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.event import async_track_time_interval
|
from homeassistant.helpers.event import async_track_time_interval
|
||||||
from homeassistant.util.package import is_docker_env
|
from homeassistant.util.package import is_docker_env
|
||||||
|
@ -133,7 +127,6 @@ class HaScanner(BaseHaScanner):
|
||||||
self.scanner = scanner
|
self.scanner = scanner
|
||||||
self.adapter = adapter
|
self.adapter = adapter
|
||||||
self._start_stop_lock = asyncio.Lock()
|
self._start_stop_lock = asyncio.Lock()
|
||||||
self._cancel_stop: CALLBACK_TYPE | None = None
|
|
||||||
self._cancel_watchdog: CALLBACK_TYPE | None = None
|
self._cancel_watchdog: CALLBACK_TYPE | None = None
|
||||||
self._last_detection = 0.0
|
self._last_detection = 0.0
|
||||||
self._start_time = 0.0
|
self._start_time = 0.0
|
||||||
|
@ -318,9 +311,6 @@ class HaScanner(BaseHaScanner):
|
||||||
break
|
break
|
||||||
|
|
||||||
self._async_setup_scanner_watchdog()
|
self._async_setup_scanner_watchdog()
|
||||||
self._cancel_stop = self.hass.bus.async_listen_once(
|
|
||||||
EVENT_HOMEASSISTANT_STOP, self._async_hass_stopping
|
|
||||||
)
|
|
||||||
|
|
||||||
@hass_callback
|
@hass_callback
|
||||||
def _async_setup_scanner_watchdog(self) -> None:
|
def _async_setup_scanner_watchdog(self) -> None:
|
||||||
|
@ -368,11 +358,6 @@ class HaScanner(BaseHaScanner):
|
||||||
exc_info=True,
|
exc_info=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _async_hass_stopping(self, event: Event) -> None:
|
|
||||||
"""Stop the Bluetooth integration at shutdown."""
|
|
||||||
self._cancel_stop = None
|
|
||||||
await self.async_stop()
|
|
||||||
|
|
||||||
async def _async_reset_adapter(self) -> None:
|
async def _async_reset_adapter(self) -> None:
|
||||||
"""Reset the adapter."""
|
"""Reset the adapter."""
|
||||||
# There is currently nothing the user can do to fix this
|
# There is currently nothing the user can do to fix this
|
||||||
|
@ -396,9 +381,6 @@ class HaScanner(BaseHaScanner):
|
||||||
|
|
||||||
async def _async_stop_scanner(self) -> None:
|
async def _async_stop_scanner(self) -> None:
|
||||||
"""Stop bluetooth discovery under the lock."""
|
"""Stop bluetooth discovery under the lock."""
|
||||||
if self._cancel_stop:
|
|
||||||
self._cancel_stop()
|
|
||||||
self._cancel_stop = None
|
|
||||||
_LOGGER.debug("%s: Stopping bluetooth discovery", self.name)
|
_LOGGER.debug("%s: Stopping bluetooth discovery", self.name)
|
||||||
try:
|
try:
|
||||||
await self.scanner.stop() # type: ignore[no-untyped-call]
|
await self.scanner.stop() # type: ignore[no-untyped-call]
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import platform
|
import platform
|
||||||
|
import time
|
||||||
|
|
||||||
from bluetooth_auto_recovery import recover_adapter
|
from bluetooth_auto_recovery import recover_adapter
|
||||||
|
|
||||||
|
@ -15,6 +16,38 @@ from .const import (
|
||||||
WINDOWS_DEFAULT_BLUETOOTH_ADAPTER,
|
WINDOWS_DEFAULT_BLUETOOTH_ADAPTER,
|
||||||
AdapterDetails,
|
AdapterDetails,
|
||||||
)
|
)
|
||||||
|
from .models import BluetoothServiceInfoBleak
|
||||||
|
|
||||||
|
|
||||||
|
async def async_load_history_from_system() -> dict[str, BluetoothServiceInfoBleak]:
|
||||||
|
"""Load the device and advertisement_data history if available on the current system."""
|
||||||
|
if platform.system() != "Linux":
|
||||||
|
return {}
|
||||||
|
from bluetooth_adapters import ( # pylint: disable=import-outside-toplevel
|
||||||
|
BlueZDBusObjects,
|
||||||
|
)
|
||||||
|
|
||||||
|
bluez_dbus = BlueZDBusObjects()
|
||||||
|
await bluez_dbus.load()
|
||||||
|
now = time.monotonic()
|
||||||
|
return {
|
||||||
|
address: BluetoothServiceInfoBleak(
|
||||||
|
name=history.advertisement_data.local_name
|
||||||
|
or history.device.name
|
||||||
|
or history.device.address,
|
||||||
|
address=history.device.address,
|
||||||
|
rssi=history.device.rssi,
|
||||||
|
manufacturer_data=history.advertisement_data.manufacturer_data,
|
||||||
|
service_data=history.advertisement_data.service_data,
|
||||||
|
service_uuids=history.advertisement_data.service_uuids,
|
||||||
|
source=history.source,
|
||||||
|
device=history.device,
|
||||||
|
advertisement=history.advertisement_data,
|
||||||
|
connectable=False,
|
||||||
|
time=now,
|
||||||
|
)
|
||||||
|
for address, history in bluez_dbus.history.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def async_get_bluetooth_adapters() -> dict[str, AdapterDetails]:
|
async def async_get_bluetooth_adapters() -> dict[str, AdapterDetails]:
|
||||||
|
|
|
@ -12,7 +12,7 @@ awesomeversion==22.9.0
|
||||||
bcrypt==3.1.7
|
bcrypt==3.1.7
|
||||||
bleak-retry-connector==1.17.1
|
bleak-retry-connector==1.17.1
|
||||||
bleak==0.17.0
|
bleak==0.17.0
|
||||||
bluetooth-adapters==0.4.1
|
bluetooth-adapters==0.5.1
|
||||||
bluetooth-auto-recovery==0.3.3
|
bluetooth-auto-recovery==0.3.3
|
||||||
certifi>=2021.5.30
|
certifi>=2021.5.30
|
||||||
ciso8601==2.2.0
|
ciso8601==2.2.0
|
||||||
|
|
|
@ -430,7 +430,7 @@ bluemaestro-ble==0.2.0
|
||||||
# bluepy==1.3.0
|
# bluepy==1.3.0
|
||||||
|
|
||||||
# homeassistant.components.bluetooth
|
# homeassistant.components.bluetooth
|
||||||
bluetooth-adapters==0.4.1
|
bluetooth-adapters==0.5.1
|
||||||
|
|
||||||
# homeassistant.components.bluetooth
|
# homeassistant.components.bluetooth
|
||||||
bluetooth-auto-recovery==0.3.3
|
bluetooth-auto-recovery==0.3.3
|
||||||
|
|
|
@ -341,7 +341,7 @@ blinkpy==0.19.2
|
||||||
bluemaestro-ble==0.2.0
|
bluemaestro-ble==0.2.0
|
||||||
|
|
||||||
# homeassistant.components.bluetooth
|
# homeassistant.components.bluetooth
|
||||||
bluetooth-adapters==0.4.1
|
bluetooth-adapters==0.5.1
|
||||||
|
|
||||||
# homeassistant.components.bluetooth
|
# homeassistant.components.bluetooth
|
||||||
bluetooth-auto-recovery==0.3.3
|
bluetooth-auto-recovery==0.3.3
|
||||||
|
|
|
@ -1,10 +1,20 @@
|
||||||
"""Tests for the bluetooth component."""
|
"""Tests for the bluetooth component."""
|
||||||
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="bluez_dbus_mock")
|
||||||
|
def bluez_dbus_mock():
|
||||||
|
"""Fixture that mocks out the bluez dbus calls."""
|
||||||
|
# Must patch directly since this is loaded on demand only
|
||||||
|
with patch(
|
||||||
|
"bluetooth_adapters.BlueZDBusObjects", return_value=MagicMock(load=AsyncMock())
|
||||||
|
):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="macos_adapter")
|
@pytest.fixture(name="macos_adapter")
|
||||||
def macos_adapter():
|
def macos_adapter():
|
||||||
"""Fixture that mocks the macos adapter."""
|
"""Fixture that mocks the macos adapter."""
|
||||||
|
@ -25,7 +35,7 @@ def windows_adapter():
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="one_adapter")
|
@pytest.fixture(name="one_adapter")
|
||||||
def one_adapter_fixture():
|
def one_adapter_fixture(bluez_dbus_mock):
|
||||||
"""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"
|
"homeassistant.components.bluetooth.platform.system", return_value="Linux"
|
||||||
|
@ -54,7 +64,7 @@ def one_adapter_fixture():
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="two_adapters")
|
@pytest.fixture(name="two_adapters")
|
||||||
def two_adapters_fixture():
|
def two_adapters_fixture(bluez_dbus_mock):
|
||||||
"""Fixture that mocks two adapters on Linux."""
|
"""Fixture that mocks two adapters on Linux."""
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.bluetooth.platform.system", return_value="Linux"
|
"homeassistant.components.bluetooth.platform.system", return_value="Linux"
|
||||||
|
|
|
@ -47,7 +47,9 @@ GENERIC_BLUETOOTH_SERVICE_INFO_2 = BluetoothServiceInfo(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_basic_usage(hass: HomeAssistant, mock_bleak_scanner_start):
|
async def test_basic_usage(
|
||||||
|
hass: HomeAssistant, mock_bleak_scanner_start, mock_bluetooth_adapters
|
||||||
|
):
|
||||||
"""Test basic usage of the ActiveBluetoothProcessorCoordinator."""
|
"""Test basic usage of the ActiveBluetoothProcessorCoordinator."""
|
||||||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
|
|
||||||
|
@ -92,7 +94,9 @@ async def test_basic_usage(hass: HomeAssistant, mock_bleak_scanner_start):
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
|
|
||||||
async def test_poll_can_be_skipped(hass: HomeAssistant, mock_bleak_scanner_start):
|
async def test_poll_can_be_skipped(
|
||||||
|
hass: HomeAssistant, mock_bleak_scanner_start, mock_bluetooth_adapters
|
||||||
|
):
|
||||||
"""Test need_poll callback works and can skip a poll if its not needed."""
|
"""Test need_poll callback works and can skip a poll if its not needed."""
|
||||||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
|
|
||||||
|
@ -151,7 +155,7 @@ async def test_poll_can_be_skipped(hass: HomeAssistant, mock_bleak_scanner_start
|
||||||
|
|
||||||
|
|
||||||
async def test_bleak_error_and_recover(
|
async def test_bleak_error_and_recover(
|
||||||
hass: HomeAssistant, mock_bleak_scanner_start, caplog
|
hass: HomeAssistant, mock_bleak_scanner_start, mock_bluetooth_adapters, caplog
|
||||||
):
|
):
|
||||||
"""Test bleak error handling and recovery."""
|
"""Test bleak error handling and recovery."""
|
||||||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
|
@ -212,7 +216,9 @@ async def test_bleak_error_and_recover(
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
|
|
||||||
async def test_poll_failure_and_recover(hass: HomeAssistant, mock_bleak_scanner_start):
|
async def test_poll_failure_and_recover(
|
||||||
|
hass: HomeAssistant, mock_bleak_scanner_start, mock_bluetooth_adapters
|
||||||
|
):
|
||||||
"""Test error handling and recovery."""
|
"""Test error handling and recovery."""
|
||||||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
|
|
||||||
|
@ -267,7 +273,9 @@ async def test_poll_failure_and_recover(hass: HomeAssistant, mock_bleak_scanner_
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
|
|
||||||
async def test_second_poll_needed(hass: HomeAssistant, mock_bleak_scanner_start):
|
async def test_second_poll_needed(
|
||||||
|
hass: HomeAssistant, mock_bleak_scanner_start, mock_bluetooth_adapters
|
||||||
|
):
|
||||||
"""If a poll is queued, by the time it starts it may no longer be needed."""
|
"""If a poll is queued, by the time it starts it may no longer be needed."""
|
||||||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
|
|
||||||
|
@ -314,7 +322,9 @@ async def test_second_poll_needed(hass: HomeAssistant, mock_bleak_scanner_start)
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
|
|
||||||
async def test_rate_limit(hass: HomeAssistant, mock_bleak_scanner_start):
|
async def test_rate_limit(
|
||||||
|
hass: HomeAssistant, mock_bleak_scanner_start, mock_bluetooth_adapters
|
||||||
|
):
|
||||||
"""Test error handling and recovery."""
|
"""Test error handling and recovery."""
|
||||||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,11 @@ from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
async def test_options_flow_disabled_not_setup(
|
async def test_options_flow_disabled_not_setup(
|
||||||
hass, hass_ws_client, mock_bleak_scanner_start, macos_adapter
|
hass,
|
||||||
|
hass_ws_client,
|
||||||
|
mock_bleak_scanner_start,
|
||||||
|
mock_bluetooth_adapters,
|
||||||
|
macos_adapter,
|
||||||
):
|
):
|
||||||
"""Test options are disabled if the integration has not been setup."""
|
"""Test options are disabled if the integration has not been setup."""
|
||||||
await async_setup_component(hass, "config", {})
|
await async_setup_component(hass, "config", {})
|
||||||
|
@ -38,6 +42,7 @@ async def test_options_flow_disabled_not_setup(
|
||||||
)
|
)
|
||||||
response = await ws_client.receive_json()
|
response = await ws_client.receive_json()
|
||||||
assert response["result"][0]["supports_options"] is False
|
assert response["result"][0]["supports_options"] is False
|
||||||
|
await hass.config_entries.async_unload(entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
async def test_async_step_user_macos(hass, macos_adapter):
|
async def test_async_step_user_macos(hass, macos_adapter):
|
||||||
|
@ -262,7 +267,9 @@ async def test_async_step_integration_discovery_already_exists(hass):
|
||||||
assert result["reason"] == "already_configured"
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
async def test_options_flow_linux(hass, mock_bleak_scanner_start, one_adapter):
|
async def test_options_flow_linux(
|
||||||
|
hass, mock_bleak_scanner_start, mock_bluetooth_adapters, one_adapter
|
||||||
|
):
|
||||||
"""Test options on Linux."""
|
"""Test options on Linux."""
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
|
@ -308,10 +315,15 @@ async def test_options_flow_linux(hass, mock_bleak_scanner_start, one_adapter):
|
||||||
|
|
||||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||||
assert result["data"][CONF_PASSIVE] is False
|
assert result["data"][CONF_PASSIVE] is False
|
||||||
|
await hass.config_entries.async_unload(entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
async def test_options_flow_disabled_macos(
|
async def test_options_flow_disabled_macos(
|
||||||
hass, hass_ws_client, mock_bleak_scanner_start, macos_adapter
|
hass,
|
||||||
|
hass_ws_client,
|
||||||
|
mock_bleak_scanner_start,
|
||||||
|
mock_bluetooth_adapters,
|
||||||
|
macos_adapter,
|
||||||
):
|
):
|
||||||
"""Test options are disabled on MacOS."""
|
"""Test options are disabled on MacOS."""
|
||||||
await async_setup_component(hass, "config", {})
|
await async_setup_component(hass, "config", {})
|
||||||
|
@ -334,10 +346,11 @@ async def test_options_flow_disabled_macos(
|
||||||
)
|
)
|
||||||
response = await ws_client.receive_json()
|
response = await ws_client.receive_json()
|
||||||
assert response["result"][0]["supports_options"] is False
|
assert response["result"][0]["supports_options"] is False
|
||||||
|
await hass.config_entries.async_unload(entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
async def test_options_flow_enabled_linux(
|
async def test_options_flow_enabled_linux(
|
||||||
hass, hass_ws_client, mock_bleak_scanner_start, one_adapter
|
hass, hass_ws_client, mock_bleak_scanner_start, mock_bluetooth_adapters, one_adapter
|
||||||
):
|
):
|
||||||
"""Test options are enabled on Linux."""
|
"""Test options are enabled on Linux."""
|
||||||
await async_setup_component(hass, "config", {})
|
await async_setup_component(hass, "config", {})
|
||||||
|
@ -363,3 +376,4 @@ async def test_options_flow_enabled_linux(
|
||||||
)
|
)
|
||||||
response = await ws_client.receive_json()
|
response = await ws_client.receive_json()
|
||||||
assert response["result"][0]["supports_options"] is True
|
assert response["result"][0]["supports_options"] is True
|
||||||
|
await hass.config_entries.async_unload(entry.entry_id)
|
||||||
|
|
|
@ -2446,7 +2446,7 @@ async def test_auto_detect_bluetooth_adapters_linux_multiple(hass, two_adapters)
|
||||||
assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 2
|
assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 2
|
||||||
|
|
||||||
|
|
||||||
async def test_auto_detect_bluetooth_adapters_linux_none_found(hass):
|
async def test_auto_detect_bluetooth_adapters_linux_none_found(hass, bluez_dbus_mock):
|
||||||
"""Test we auto detect bluetooth adapters on linux with no adapters found."""
|
"""Test we auto detect bluetooth adapters on linux with no adapters found."""
|
||||||
with patch(
|
with patch(
|
||||||
"bluetooth_adapters.get_bluetooth_adapter_details", return_value={}
|
"bluetooth_adapters.get_bluetooth_adapter_details", return_value={}
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
"""Tests for the Bluetooth integration manager."""
|
"""Tests for the Bluetooth integration manager."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
from bleak.backends.scanner import AdvertisementData, BLEDevice
|
from bleak.backends.scanner import AdvertisementData, BLEDevice
|
||||||
|
from bluetooth_adapters import AdvertisementHistory
|
||||||
|
|
||||||
from homeassistant.components import bluetooth
|
from homeassistant.components import bluetooth
|
||||||
from homeassistant.components.bluetooth.manager import STALE_ADVERTISEMENT_SECONDS
|
from homeassistant.components.bluetooth.manager import STALE_ADVERTISEMENT_SECONDS
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
inject_advertisement_with_source,
|
inject_advertisement_with_source,
|
||||||
|
@ -176,3 +179,24 @@ async def test_switching_adapters_based_on_stale(hass, enable_bluetooth):
|
||||||
bluetooth.async_ble_device_from_address(hass, address)
|
bluetooth.async_ble_device_from_address(hass, address)
|
||||||
is switchbot_device_poor_signal_hci1
|
is switchbot_device_poor_signal_hci1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_restore_history_from_dbus(hass, one_adapter):
|
||||||
|
"""Test we can restore history from dbus."""
|
||||||
|
address = "AA:BB:CC:CC:CC:FF"
|
||||||
|
|
||||||
|
ble_device = BLEDevice(address, "name")
|
||||||
|
history = {
|
||||||
|
address: AdvertisementHistory(
|
||||||
|
ble_device, AdvertisementData(local_name="name"), "hci0"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"bluetooth_adapters.BlueZDBusObjects",
|
||||||
|
return_value=MagicMock(load=AsyncMock(), history=history),
|
||||||
|
):
|
||||||
|
assert await async_setup_component(hass, bluetooth.DOMAIN, {})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert bluetooth.async_ble_device_from_address(hass, address) is ble_device
|
||||||
|
|
|
@ -59,7 +59,7 @@ class MyCoordinator(PassiveBluetoothDataUpdateCoordinator):
|
||||||
super()._async_handle_bluetooth_event(service_info, change)
|
super()._async_handle_bluetooth_event(service_info, change)
|
||||||
|
|
||||||
|
|
||||||
async def test_basic_usage(hass, mock_bleak_scanner_start):
|
async def test_basic_usage(hass, mock_bleak_scanner_start, mock_bluetooth_adapters):
|
||||||
"""Test basic usage of the PassiveBluetoothDataUpdateCoordinator."""
|
"""Test basic usage of the PassiveBluetoothDataUpdateCoordinator."""
|
||||||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
coordinator = MyCoordinator(
|
coordinator = MyCoordinator(
|
||||||
|
@ -88,7 +88,7 @@ async def test_basic_usage(hass, mock_bleak_scanner_start):
|
||||||
|
|
||||||
|
|
||||||
async def test_context_compatiblity_with_data_update_coordinator(
|
async def test_context_compatiblity_with_data_update_coordinator(
|
||||||
hass, mock_bleak_scanner_start
|
hass, mock_bleak_scanner_start, mock_bluetooth_adapters
|
||||||
):
|
):
|
||||||
"""Test contexts can be passed for compatibility with DataUpdateCoordinator."""
|
"""Test contexts can be passed for compatibility with DataUpdateCoordinator."""
|
||||||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
|
@ -124,7 +124,7 @@ async def test_context_compatiblity_with_data_update_coordinator(
|
||||||
|
|
||||||
|
|
||||||
async def test_unavailable_callbacks_mark_the_coordinator_unavailable(
|
async def test_unavailable_callbacks_mark_the_coordinator_unavailable(
|
||||||
hass, mock_bleak_scanner_start
|
hass, mock_bleak_scanner_start, mock_bluetooth_adapters
|
||||||
):
|
):
|
||||||
"""Test that the coordinator goes unavailable when the bluetooth stack no longer sees the device."""
|
"""Test that the coordinator goes unavailable when the bluetooth stack no longer sees the device."""
|
||||||
with patch(
|
with patch(
|
||||||
|
@ -165,7 +165,9 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable(
|
||||||
assert coordinator.available is False
|
assert coordinator.available is False
|
||||||
|
|
||||||
|
|
||||||
async def test_passive_bluetooth_coordinator_entity(hass, mock_bleak_scanner_start):
|
async def test_passive_bluetooth_coordinator_entity(
|
||||||
|
hass, mock_bleak_scanner_start, mock_bluetooth_adapters
|
||||||
|
):
|
||||||
"""Test integration of PassiveBluetoothDataUpdateCoordinator with PassiveBluetoothCoordinatorEntity."""
|
"""Test integration of PassiveBluetoothDataUpdateCoordinator with PassiveBluetoothCoordinatorEntity."""
|
||||||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
coordinator = MyCoordinator(
|
coordinator = MyCoordinator(
|
||||||
|
|
|
@ -98,7 +98,7 @@ GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_basic_usage(hass, mock_bleak_scanner_start):
|
async def test_basic_usage(hass, mock_bleak_scanner_start, mock_bluetooth_adapters):
|
||||||
"""Test basic usage of the PassiveBluetoothProcessorCoordinator."""
|
"""Test basic usage of the PassiveBluetoothProcessorCoordinator."""
|
||||||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
|
|
||||||
|
@ -196,7 +196,9 @@ async def test_basic_usage(hass, mock_bleak_scanner_start):
|
||||||
cancel_coordinator()
|
cancel_coordinator()
|
||||||
|
|
||||||
|
|
||||||
async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start):
|
async def test_unavailable_after_no_data(
|
||||||
|
hass, mock_bleak_scanner_start, mock_bluetooth_adapters
|
||||||
|
):
|
||||||
"""Test that the coordinator is unavailable after no data for a while."""
|
"""Test that the coordinator is unavailable after no data for a while."""
|
||||||
with patch(
|
with patch(
|
||||||
"bleak.BleakScanner.discovered_devices", # Must patch before we setup
|
"bleak.BleakScanner.discovered_devices", # Must patch before we setup
|
||||||
|
@ -290,7 +292,9 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start):
|
||||||
cancel_coordinator()
|
cancel_coordinator()
|
||||||
|
|
||||||
|
|
||||||
async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start):
|
async def test_no_updates_once_stopping(
|
||||||
|
hass, mock_bleak_scanner_start, mock_bluetooth_adapters
|
||||||
|
):
|
||||||
"""Test updates are ignored once hass is stopping."""
|
"""Test updates are ignored once hass is stopping."""
|
||||||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
|
|
||||||
|
@ -343,7 +347,9 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start):
|
||||||
cancel_coordinator()
|
cancel_coordinator()
|
||||||
|
|
||||||
|
|
||||||
async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_start):
|
async def test_exception_from_update_method(
|
||||||
|
hass, caplog, mock_bleak_scanner_start, mock_bluetooth_adapters
|
||||||
|
):
|
||||||
"""Test we handle exceptions from the update method."""
|
"""Test we handle exceptions from the update method."""
|
||||||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
|
|
||||||
|
@ -406,7 +412,9 @@ async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_sta
|
||||||
cancel_coordinator()
|
cancel_coordinator()
|
||||||
|
|
||||||
|
|
||||||
async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start):
|
async def test_bad_data_from_update_method(
|
||||||
|
hass, mock_bleak_scanner_start, mock_bluetooth_adapters
|
||||||
|
):
|
||||||
"""Test we handle bad data from the update method."""
|
"""Test we handle bad data from the update method."""
|
||||||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
|
|
||||||
|
@ -758,7 +766,9 @@ GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_integration_with_entity(hass, mock_bleak_scanner_start):
|
async def test_integration_with_entity(
|
||||||
|
hass, mock_bleak_scanner_start, mock_bluetooth_adapters
|
||||||
|
):
|
||||||
"""Test integration of PassiveBluetoothProcessorCoordinator with PassiveBluetoothCoordinatorEntity."""
|
"""Test integration of PassiveBluetoothProcessorCoordinator with PassiveBluetoothCoordinatorEntity."""
|
||||||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
|
|
||||||
|
@ -888,7 +898,9 @@ NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner_start):
|
async def test_integration_with_entity_without_a_device(
|
||||||
|
hass, mock_bleak_scanner_start, mock_bluetooth_adapters
|
||||||
|
):
|
||||||
"""Test integration with PassiveBluetoothCoordinatorEntity with no device."""
|
"""Test integration with PassiveBluetoothCoordinatorEntity with no device."""
|
||||||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
|
|
||||||
|
@ -950,7 +962,7 @@ async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner
|
||||||
|
|
||||||
|
|
||||||
async def test_passive_bluetooth_entity_with_entity_platform(
|
async def test_passive_bluetooth_entity_with_entity_platform(
|
||||||
hass, mock_bleak_scanner_start
|
hass, mock_bleak_scanner_start, mock_bluetooth_adapters
|
||||||
):
|
):
|
||||||
"""Test with a mock entity platform."""
|
"""Test with a mock entity platform."""
|
||||||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
|
@ -1048,7 +1060,9 @@ BINARY_SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_start):
|
async def test_integration_multiple_entity_platforms(
|
||||||
|
hass, mock_bleak_scanner_start, mock_bluetooth_adapters
|
||||||
|
):
|
||||||
"""Test integration of PassiveBluetoothProcessorCoordinator with multiple platforms."""
|
"""Test integration of PassiveBluetoothProcessorCoordinator with multiple platforms."""
|
||||||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
|
|
||||||
|
@ -1138,7 +1152,7 @@ async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_st
|
||||||
|
|
||||||
|
|
||||||
async def test_exception_from_coordinator_update_method(
|
async def test_exception_from_coordinator_update_method(
|
||||||
hass, caplog, mock_bleak_scanner_start
|
hass, caplog, mock_bleak_scanner_start, mock_bluetooth_adapters
|
||||||
):
|
):
|
||||||
"""Test we handle exceptions from the update method."""
|
"""Test we handle exceptions from the update method."""
|
||||||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
|
|
|
@ -991,6 +991,8 @@ def mock_bluetooth_adapters():
|
||||||
"""Fixture to mock bluetooth adapters."""
|
"""Fixture to mock bluetooth adapters."""
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.bluetooth.util.platform.system", return_value="Linux"
|
"homeassistant.components.bluetooth.util.platform.system", return_value="Linux"
|
||||||
|
), patch(
|
||||||
|
"bluetooth_adapters.BlueZDBusObjects", return_value=MagicMock(load=AsyncMock())
|
||||||
), patch(
|
), patch(
|
||||||
"bluetooth_adapters.get_bluetooth_adapter_details",
|
"bluetooth_adapters.get_bluetooth_adapter_details",
|
||||||
return_value={
|
return_value={
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue