From adf2f2854cbc623e7fe3a3bbacc1035a7656b41b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Jun 2026 20:03:38 -0500 Subject: [PATCH] Active scan govee_ble only when needed and widen the window to 30s (#174557) --- .../components/govee_ble/__init__.py | 2 - .../components/govee_ble/coordinator.py | 28 +++++++-- tests/components/govee_ble/test_init.py | 62 +++++++++++++++++++ 3 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 tests/components/govee_ble/test_init.py diff --git a/homeassistant/components/govee_ble/__init__.py b/homeassistant/components/govee_ble/__init__.py index d801ec7230ea..9d7a454aed79 100644 --- a/homeassistant/components/govee_ble/__init__.py +++ b/homeassistant/components/govee_ble/__init__.py @@ -5,7 +5,6 @@ import logging from govee_ble import GoveeBluetoothDeviceData -from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -29,7 +28,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoveeBLEConfigEntry) -> hass, _LOGGER, address=address, - mode=BluetoothScanningMode.ACTIVE, update_method=partial(process_service_info, hass, entry), device_data=data, entry=entry, diff --git a/homeassistant/components/govee_ble/coordinator.py b/homeassistant/components/govee_ble/coordinator.py index f75c0c95ff93..c171917aa3c8 100644 --- a/homeassistant/components/govee_ble/coordinator.py +++ b/homeassistant/components/govee_ble/coordinator.py @@ -21,6 +21,12 @@ from .const import CONF_DEVICE_TYPE, DOMAIN type GoveeBLEConfigEntry = ConfigEntry[GoveeBLEBluetoothProcessorCoordinator] +# Models such as the H5074 carry their measurements only in the scan response, +# so the scanner must stay active long enough to capture one; the default 10s +# window misses them most cycles. 30s is the longest active window habluetooth +# allows (AUTO_WINDOW_MAX_DURATION) and reliably spans a full broadcast cycle. +ACTIVE_SCAN_DURATION = 30.0 + def process_service_info( hass: HomeAssistant, @@ -65,18 +71,30 @@ class GoveeBLEBluetoothProcessorCoordinator( hass: HomeAssistant, logger: Logger, address: str, - mode: BluetoothScanningMode, update_method: Callable[[BluetoothServiceInfoBleak], SensorUpdate], device_data: GoveeBluetoothDeviceData, entry: ConfigEntry, ) -> None: """Initialize the Govee BLE Bluetooth Passive Update Processor Coordinator.""" - super().__init__(hass, logger, address, mode, update_method) + self.model_info: ModelInfo | None = None + # Active scanning is only needed for models that carry their payload in + # the scan response; passively broadcasting models would otherwise be + # scanned needlessly, costing fleet radio time and sensor battery. + # When the model is not yet known, scan actively so a scan-response-only + # model can still be discovered. + mode = BluetoothScanningMode.ACTIVE + scan_duration: float | None = None + if device_type := entry.data.get(CONF_DEVICE_TYPE): + self.model_info = model_info = get_model_info(device_type) + if model_info.requires_active_scan: + scan_duration = ACTIVE_SCAN_DURATION + else: + mode = BluetoothScanningMode.PASSIVE + super().__init__( + hass, logger, address, mode, update_method, scan_duration=scan_duration + ) self.device_data = device_data self.entry = entry - self.model_info: ModelInfo | None = None - if device_type := entry.data.get(CONF_DEVICE_TYPE): - self.set_model_info(device_type) def set_model_info(self, device_type: str) -> None: """Set the model info.""" diff --git a/tests/components/govee_ble/test_init.py b/tests/components/govee_ble/test_init.py new file mode 100644 index 000000000000..20402728e54a --- /dev/null +++ b/tests/components/govee_ble/test_init.py @@ -0,0 +1,62 @@ +"""Test the Govee BLE init.""" + +from unittest.mock import patch + +import pytest + +from homeassistant.components.bluetooth import BluetoothScanningMode +from homeassistant.components.govee_ble.const import CONF_DEVICE_TYPE, DOMAIN +from homeassistant.components.govee_ble.coordinator import ACTIVE_SCAN_DURATION +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + ("device_type", "expected_mode", "expected_scan_duration"), + [ + pytest.param( + "H5074", + BluetoothScanningMode.ACTIVE, + ACTIVE_SCAN_DURATION, + id="h5074_scan_response", + ), + pytest.param( + "H5075", + BluetoothScanningMode.ACTIVE, + ACTIVE_SCAN_DURATION, + id="h5075_scan_response", + ), + pytest.param( + "H5179", + BluetoothScanningMode.PASSIVE, + None, + id="primary_advertisement", + ), + ], +) +async def test_active_scan_duration( + hass: HomeAssistant, + device_type: str, + expected_mode: BluetoothScanningMode, + expected_scan_duration: float | None, +) -> None: + """Test only scan-response-only models are scanned actively.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + data={CONF_DEVICE_TYPE: device_type}, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback" + ) as mock_register_callback: + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert mock_register_callback.call_args.args[3] == expected_mode + assert ( + mock_register_callback.call_args.kwargs["scan_duration"] + == expected_scan_duration + )