From 1d1c553d9be847c0e2fe25b93b963d1d9480f1fc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Feb 2023 21:02:52 -0600 Subject: [PATCH] Avoid starting a bluetooth poll when Home Assistant is stopping (#88819) * Avoid starting a bluetooth poll when Home Assistant is stopping * tests --- .../bluetooth/active_update_coordinator.py | 2 + .../bluetooth/active_update_processor.py | 2 + .../test_active_update_coordinator.py | 57 ++++++++++++++++- .../bluetooth/test_active_update_processor.py | 64 ++++++++++++++++++- 4 files changed, 123 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bluetooth/active_update_coordinator.py b/homeassistant/components/bluetooth/active_update_coordinator.py index 78e713ce5e6..d5cf65d8724 100644 --- a/homeassistant/components/bluetooth/active_update_coordinator.py +++ b/homeassistant/components/bluetooth/active_update_coordinator.py @@ -106,6 +106,8 @@ class ActiveBluetoothDataUpdateCoordinator( def needs_poll(self, service_info: BluetoothServiceInfoBleak) -> bool: """Return true if time to try and poll.""" + if self.hass.is_stopping: + return False poll_age: float | None = None if self._last_poll: poll_age = monotonic_time_coarse() - self._last_poll diff --git a/homeassistant/components/bluetooth/active_update_processor.py b/homeassistant/components/bluetooth/active_update_processor.py index b91ac2cbf4d..aabc27ff14e 100644 --- a/homeassistant/components/bluetooth/active_update_processor.py +++ b/homeassistant/components/bluetooth/active_update_processor.py @@ -99,6 +99,8 @@ class ActiveBluetoothProcessorCoordinator( def needs_poll(self, service_info: BluetoothServiceInfoBleak) -> bool: """Return true if time to try and poll.""" + if self.hass.is_stopping: + return False poll_age: float | None = None if self._last_poll: poll_age = monotonic_time_coarse() - self._last_poll diff --git a/tests/components/bluetooth/test_active_update_coordinator.py b/tests/components/bluetooth/test_active_update_coordinator.py index 26697219ae5..2686138d724 100644 --- a/tests/components/bluetooth/test_active_update_coordinator.py +++ b/tests/components/bluetooth/test_active_update_coordinator.py @@ -19,7 +19,7 @@ from homeassistant.components.bluetooth.active_update_coordinator import ( _T, ActiveBluetoothDataUpdateCoordinator, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import CoreState, HomeAssistant from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.setup import async_setup_component @@ -395,3 +395,58 @@ async def test_polling_rejecting_the_first_time( cancel() unregister_listener() + + +async def test_no_polling_after_stop_event( + hass: HomeAssistant, + mock_bleak_scanner_start: MagicMock, + mock_bluetooth_adapters: None, +) -> None: + """Test we do not poll after the stop event.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + needs_poll_calls = 0 + + def _needs_poll( + service_info: BluetoothServiceInfoBleak, seconds_since_last_poll: float | None + ) -> bool: + nonlocal needs_poll_calls + needs_poll_calls += 1 + return True + + async def _poll_method(service_info: BluetoothServiceInfoBleak) -> dict[str, Any]: + return {"fake": "data"} + + coordinator = MyCoordinator( + hass=hass, + logger=_LOGGER, + address="aa:bb:cc:dd:ee:ff", + mode=BluetoothScanningMode.ACTIVE, + needs_poll_method=_needs_poll, + poll_method=_poll_method, + ) + assert coordinator.available is False # no data yet + + mock_listener = MagicMock() + unregister_listener = coordinator.async_add_listener(mock_listener) + + cancel = coordinator.async_start() + assert needs_poll_calls == 0 + + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) + await hass.async_block_till_done() + assert coordinator.passive_data == {"rssi": GENERIC_BLUETOOTH_SERVICE_INFO.rssi} + assert coordinator.data == {"fake": "data"} + + assert needs_poll_calls == 1 + + hass.state = CoreState.stopping + await hass.async_block_till_done() + assert needs_poll_calls == 1 + + # Should not generate a poll now + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO_2) + await hass.async_block_till_done() + assert needs_poll_calls == 1 + + cancel() + unregister_listener() diff --git a/tests/components/bluetooth/test_active_update_processor.py b/tests/components/bluetooth/test_active_update_processor.py index a8dec3cca27..83ad809016a 100644 --- a/tests/components/bluetooth/test_active_update_processor.py +++ b/tests/components/bluetooth/test_active_update_processor.py @@ -16,7 +16,7 @@ from homeassistant.components.bluetooth import ( from homeassistant.components.bluetooth.active_update_processor import ( ActiveBluetoothProcessorCoordinator, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import CoreState, HomeAssistant from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.setup import async_setup_component @@ -384,3 +384,65 @@ async def test_rate_limit( assert async_handle_update.mock_calls[-1] == call({"testdata": 1}) cancel() + + +async def test_no_polling_after_stop_event( + hass: HomeAssistant, + mock_bleak_scanner_start: MagicMock, + mock_bluetooth_adapters: None, +) -> None: + """Test we do not poll after the stop event.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + needs_poll_calls = 0 + + def _update_method(service_info: BluetoothServiceInfoBleak): + return {"testdata": 0} + + def _poll_needed(*args, **kwargs): + nonlocal needs_poll_calls + needs_poll_calls += 1 + return True + + 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, + ) + assert coordinator.available is False # no data yet + + processor = MagicMock() + coordinator.async_register_processor(processor) + async_handle_update = processor.async_handle_update + + cancel = coordinator.async_start() + + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) + await hass.async_block_till_done() + assert needs_poll_calls == 1 + + assert coordinator.available is True + + # async_handle_update should have been called twice + # The first time, it was passed the data from parsing the advertisement + # The second time, it was passed the data from polling + assert len(async_handle_update.mock_calls) == 2 + assert async_handle_update.mock_calls[0] == call({"testdata": 0}) + assert async_handle_update.mock_calls[1] == call({"testdata": 1}) + + hass.state = CoreState.stopping + await hass.async_block_till_done() + assert needs_poll_calls == 1 + + # Should not generate a poll now that CoreState is stopping + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO_2) + await hass.async_block_till_done() + assert needs_poll_calls == 1 + + cancel()