From 667f4b1ca83bd08d6777a44104abd9c693607739 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 26 Sep 2023 05:29:46 -0500 Subject: [PATCH] Mark Bluetooth scanner as not scanning when watchdog timeout is reached (#100738) --- .../components/bluetooth/base_scanner.py | 4 + homeassistant/components/bluetooth/scanner.py | 3 + .../components/bluetooth/test_base_scanner.py | 81 +++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/homeassistant/components/bluetooth/base_scanner.py b/homeassistant/components/bluetooth/base_scanner.py index 455619182ab..240610e4868 100644 --- a/homeassistant/components/bluetooth/base_scanner.py +++ b/homeassistant/components/bluetooth/base_scanner.py @@ -131,6 +131,9 @@ class BaseHaScanner(ABC): self.name, SCANNER_WATCHDOG_TIMEOUT, ) + self.scanning = False + return + self.scanning = not self._connecting @contextmanager def connecting(self) -> Generator[None, None, None]: @@ -302,6 +305,7 @@ class BaseHaRemoteScanner(BaseHaScanner): advertisement_monotonic_time: float, ) -> None: """Call the registered callback.""" + self.scanning = not self._connecting self._last_detection = advertisement_monotonic_time try: prev_discovery = self._discovered_device_advertisement_datas[address] diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index eb3ce11b644..896d9dc7958 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -329,6 +329,9 @@ class HaScanner(BaseHaScanner): self.name, SCANNER_WATCHDOG_TIMEOUT, ) + # Immediately mark the scanner as not scanning + # since the restart task will have to wait for the lock + self.scanning = False self.hass.async_create_task(self._async_restart_scanner()) async def _async_restart_scanner(self) -> None: diff --git a/tests/components/bluetooth/test_base_scanner.py b/tests/components/bluetooth/test_base_scanner.py index 5662bc6324b..fc870f2bfe3 100644 --- a/tests/components/bluetooth/test_base_scanner.py +++ b/tests/components/bluetooth/test_base_scanner.py @@ -23,6 +23,8 @@ from homeassistant.components.bluetooth.advertisement_tracker import ( from homeassistant.components.bluetooth.const import ( CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, + SCANNER_WATCHDOG_INTERVAL, + SCANNER_WATCHDOG_TIMEOUT, UNAVAILABLE_TRACK_SECONDS, ) from homeassistant.core import HomeAssistant, callback @@ -557,3 +559,82 @@ async def test_device_with_ten_minute_advertising_interval( cancel() unsetup() + + +async def test_scanner_stops_responding( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, enable_bluetooth: None +) -> None: + """Test we mark a scanner are not scanning when it stops responding.""" + manager = _get_manager() + + class FakeScanner(BaseHaRemoteScanner): + """A fake remote scanner.""" + + def inject_advertisement( + self, device: BLEDevice, advertisement_data: AdvertisementData + ) -> 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"}, + MONOTONIC_TIME(), + ) + + new_info_callback = manager.scanner_adv_received + connector = ( + HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), + ) + scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, False) + unsetup = scanner.async_setup() + cancel = manager.async_register_scanner(scanner, True) + + start_time_monotonic = time.monotonic() + + assert scanner.scanning is True + failure_reached_time = ( + start_time_monotonic + + SCANNER_WATCHDOG_TIMEOUT + + SCANNER_WATCHDOG_INTERVAL.total_seconds() + ) + # We hit the timer with no detections, so we reset the adapter and restart the scanner + with patch( + "homeassistant.components.bluetooth.base_scanner.MONOTONIC_TIME", + return_value=failure_reached_time, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() + + assert scanner.scanning is False + + bparasite_device = generate_ble_device( + "44:44:33:11:23:45", + "bparasite", + {}, + rssi=-100, + ) + bparasite_device_adv = generate_advertisement_data( + local_name="bparasite", + service_uuids=[], + manufacturer_data={1: b"\x01"}, + rssi=-100, + ) + + failure_reached_time += 1 + + with patch( + "homeassistant.components.bluetooth.base_scanner.MONOTONIC_TIME", + return_value=failure_reached_time, + ): + scanner.inject_advertisement(bparasite_device, bparasite_device_adv) + + # As soon as we get a detection, we know the scanner is working again + assert scanner.scanning is True + + cancel() + unsetup()