Allow bluetooth coordinators to request an active scan cadence (#172015)

This commit is contained in:
J. Nick Koston
2026-05-24 10:43:52 -04:00
committed by GitHub
parent daa60c6d55
commit 2d3a3bf4fc
8 changed files with 96 additions and 13 deletions
@@ -68,9 +68,20 @@ class ActiveBluetoothProcessorCoordinator[_DataT](
| None = None,
poll_debouncer: Debouncer[Coroutine[Any, Any, None]] | None = None,
connectable: bool = True,
scan_interval: float | None = None,
scan_duration: float | None = None,
) -> None:
"""Initialize the processor."""
super().__init__(hass, logger, address, mode, update_method, connectable)
super().__init__(
hass,
logger,
address,
mode,
update_method,
connectable,
scan_interval,
scan_duration,
)
self._needs_poll_method = needs_poll_method
self._poll_method = poll_method
@@ -298,9 +298,13 @@ class PassiveBluetoothProcessorCoordinator[_DataT](BasePassiveBluetoothCoordinat
mode: BluetoothScanningMode,
update_method: Callable[[BluetoothServiceInfoBleak], _DataT],
connectable: bool = False,
scan_interval: float | None = None,
scan_duration: float | None = None,
) -> None:
"""Initialize the coordinator."""
super().__init__(hass, logger, address, mode, connectable)
super().__init__(
hass, logger, address, mode, connectable, scan_interval, scan_duration
)
self._processors: list[PassiveBluetoothDataProcessor[Any, _DataT]] = []
self._update_method = update_method
self.last_update_success = True
@@ -30,6 +30,8 @@ class BasePassiveBluetoothCoordinator(ABC):
address: str,
mode: BluetoothScanningMode,
connectable: bool,
scan_interval: float | None = None,
scan_duration: float | None = None,
) -> None:
"""Initialize the coordinator."""
self.hass = hass
@@ -38,6 +40,8 @@ class BasePassiveBluetoothCoordinator(ABC):
self.connectable = connectable
self._on_stop: list[CALLBACK_TYPE] = []
self.mode = mode
self._scan_interval = scan_interval
self._scan_duration = scan_duration
self._last_unavailable_time = 0.0
self._last_name = address
# Subclasses are responsible for setting _available to True
@@ -92,6 +96,8 @@ class BasePassiveBluetoothCoordinator(ABC):
address=self.address, connectable=self.connectable
),
self.mode,
scan_interval=self._scan_interval,
scan_duration=self._scan_duration,
)
)
self._on_stop.append(
@@ -2,7 +2,7 @@
import asyncio
import logging
from unittest.mock import MagicMock, call
from unittest.mock import MagicMock, call, patch
from bleak import BleakError
import pytest
@@ -15,6 +15,7 @@ from homeassistant.components.bluetooth import (
from homeassistant.components.bluetooth.active_update_processor import (
ActiveBluetoothProcessorCoordinator,
)
from homeassistant.components.bluetooth.manager import HomeAssistantBluetoothManager
from homeassistant.core import CoreState, HomeAssistant
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
@@ -422,3 +423,44 @@ async def test_no_polling_after_stop_event(hass: HomeAssistant) -> None:
assert needs_poll_calls == 1
cancel()
@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters")
async def test_scan_interval_and_duration_forwarded(hass: HomeAssistant) -> None:
"""Test scan_interval and scan_duration reach the bluetooth manager."""
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
def _update_method(service_info: BluetoothServiceInfoBleak):
return {"testdata": 0}
def _poll_needed(*args, **kwargs):
return False
async def _poll(*args, **kwargs):
return {"testdata": 1}
coordinator = ActiveBluetoothProcessorCoordinator(
hass,
_LOGGER,
address="aa:bb:cc:dd:ee:ff",
mode=BluetoothScanningMode.ACTIVE,
update_method=_update_method,
needs_poll_method=_poll_needed,
poll_method=_poll,
scan_interval=165.0,
scan_duration=10.0,
)
cancel_scan = MagicMock()
with patch.object(
HomeAssistantBluetoothManager,
"async_register_active_scan",
return_value=cancel_scan,
) as mock_register:
cancel = coordinator.async_start()
mock_register.assert_called_once_with("aa:bb:cc:dd:ee:ff", 165.0, 10.0)
cancel_scan.assert_not_called()
cancel()
cancel_scan.assert_called_once()
@@ -703,7 +703,9 @@ async def test_exception_from_update_method(
assert coordinator.available is False # no data yet
saved_callback = None
def _async_register_callback(_hass, _callback, _matcher, _mode):
def _async_register_callback(
_hass, _callback, _matcher, _mode, *, scan_interval=None, scan_duration=None
):
nonlocal saved_callback
saved_callback = _callback
return lambda: None
@@ -768,7 +770,9 @@ async def test_bad_data_from_update_method(hass: HomeAssistant) -> None:
assert coordinator.available is False # no data yet
saved_callback = None
def _async_register_callback(_hass, _callback, _matcher, _mode):
def _async_register_callback(
_hass, _callback, _matcher, _mode, *, scan_interval=None, scan_duration=None
):
nonlocal saved_callback
saved_callback = _callback
return lambda: None
+6 -2
View File
@@ -501,7 +501,9 @@ async def test_async_step_reauth(hass: HomeAssistant) -> None:
entry.add_to_hass(hass)
saved_callback = None
def _async_register_callback(_hass, _callback, _matcher, _mode):
def _async_register_callback(
_hass, _callback, _matcher, _mode, *, scan_interval=None, scan_duration=None
):
nonlocal saved_callback
saved_callback = _callback
return lambda: None
@@ -541,7 +543,9 @@ async def test_async_step_reauth_wrong_key(hass: HomeAssistant) -> None:
entry.add_to_hass(hass)
saved_callback = None
def _async_register_callback(_hass, _callback, _matcher, _mode):
def _async_register_callback(
_hass, _callback, _matcher, _mode, *, scan_interval=None, scan_duration=None
):
nonlocal saved_callback
saved_callback = _callback
return lambda: None
+3 -1
View File
@@ -40,7 +40,9 @@ async def _setup_entry(
saved_callback: Callable[[object, BluetoothChange], None] | None = None
def _async_register_callback(_hass, _callback, _matcher, _mode):
def _async_register_callback(
_hass, _callback, _matcher, _mode, *, scan_interval=None, scan_duration=None
):
nonlocal saved_callback
saved_callback = _callback
return lambda: None
@@ -1162,7 +1162,9 @@ async def test_async_step_reauth_legacy(hass: HomeAssistant) -> None:
entry.add_to_hass(hass)
saved_callback = None
def _async_register_callback(_hass, _callback, _matcher, _mode):
def _async_register_callback(
_hass, _callback, _matcher, _mode, *, scan_interval=None, scan_duration=None
):
nonlocal saved_callback
saved_callback = _callback
return lambda: None
@@ -1211,7 +1213,9 @@ async def test_async_step_reauth_legacy_wrong_key(hass: HomeAssistant) -> None:
entry.add_to_hass(hass)
saved_callback = None
def _async_register_callback(_hass, _callback, _matcher, _mode):
def _async_register_callback(
_hass, _callback, _matcher, _mode, *, scan_interval=None, scan_duration=None
):
nonlocal saved_callback
saved_callback = _callback
return lambda: None
@@ -1268,7 +1272,9 @@ async def test_async_step_reauth_v4(hass: HomeAssistant) -> None:
entry.add_to_hass(hass)
saved_callback = None
def _async_register_callback(_hass, _callback, _matcher, _mode):
def _async_register_callback(
_hass, _callback, _matcher, _mode, *, scan_interval=None, scan_duration=None
):
nonlocal saved_callback
saved_callback = _callback
return lambda: None
@@ -1322,7 +1328,9 @@ async def test_async_step_reauth_v4_wrong_key(hass: HomeAssistant) -> None:
entry.add_to_hass(hass)
saved_callback = None
def _async_register_callback(_hass, _callback, _matcher, _mode):
def _async_register_callback(
_hass, _callback, _matcher, _mode, *, scan_interval=None, scan_duration=None
):
nonlocal saved_callback
saved_callback = _callback
return lambda: None
@@ -1384,7 +1392,9 @@ async def test_async_step_reauth_v4_from_cloud(hass: HomeAssistant) -> None:
entry.add_to_hass(hass)
saved_callback = None
def _async_register_callback(_hass, _callback, _matcher, _mode):
def _async_register_callback(
_hass, _callback, _matcher, _mode, *, scan_interval=None, scan_duration=None
):
nonlocal saved_callback
saved_callback = _callback
return lambda: None