From 326e05dcf1fdc524abdde92ccae98612bdfe15bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 22 Jul 2022 18:12:08 -0500 Subject: [PATCH] Fix async_get_scanner to return the correct bluetooth scanner (#75637) --- .../components/bluetooth/__init__.py | 15 +-- tests/components/bluetooth/__init__.py | 7 ++ tests/components/bluetooth/test_init.py | 95 ++++++++++--------- .../test_passive_update_coordinator.py | 5 +- 4 files changed, 70 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 3c7f23932576..a50be7f4aced 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -35,7 +35,7 @@ from homeassistant.loader import ( from . import models from .const import DOMAIN -from .models import HaBleakScanner +from .models import HaBleakScanner, HaBleakScannerWrapper from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher _LOGGER = logging.getLogger(__name__) @@ -117,8 +117,12 @@ BluetoothCallback = Callable[ @hass_callback -def async_get_scanner(hass: HomeAssistant) -> HaBleakScanner: - """Return a HaBleakScanner.""" +def async_get_scanner(hass: HomeAssistant) -> HaBleakScannerWrapper: + """Return a HaBleakScannerWrapper. + + This is a wrapper around our BleakScanner singleton that allows + multiple integrations to share the same BleakScanner. + """ if DOMAIN not in hass.data: raise RuntimeError("Bluetooth integration not loaded") manager: BluetoothManager = hass.data[DOMAIN] @@ -320,10 +324,9 @@ class BluetoothManager: models.HA_BLEAK_SCANNER = self.scanner = HaBleakScanner() @hass_callback - def async_get_scanner(self) -> HaBleakScanner: + def async_get_scanner(self) -> HaBleakScannerWrapper: """Get the scanner.""" - assert self.scanner is not None - return self.scanner + return HaBleakScannerWrapper() async def async_start(self, scanning_mode: BluetoothScanningMode) -> None: """Set up BT Discovery.""" diff --git a/tests/components/bluetooth/__init__.py b/tests/components/bluetooth/__init__.py index 6bf53afcd1e2..3dc80d555906 100644 --- a/tests/components/bluetooth/__init__.py +++ b/tests/components/bluetooth/__init__.py @@ -1 +1,8 @@ """Tests for the Bluetooth integration.""" + +from homeassistant.components.bluetooth import models + + +def _get_underlying_scanner(): + """Return the underlying scanner that has been wrapped.""" + return models.HA_BLEAK_SCANNER diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 8aef5f3ddbb7..5d932c56349d 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -12,7 +12,6 @@ from homeassistant.components.bluetooth import ( UNAVAILABLE_TRACK_SECONDS, BluetoothChange, BluetoothServiceInfo, - async_get_scanner, async_track_unavailable, models, ) @@ -21,6 +20,8 @@ from homeassistant.core import callback from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util +from . import _get_underlying_scanner + from tests.common import MockConfigEntry, async_fire_time_changed @@ -135,7 +136,7 @@ async def test_discovery_match_by_service_uuid( wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name") wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) - async_get_scanner(hass)._callback(wrong_device, wrong_adv) + _get_underlying_scanner()._callback(wrong_device, wrong_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -145,7 +146,7 @@ async def test_discovery_match_by_service_uuid( local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] ) - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 @@ -172,7 +173,7 @@ async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start): wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name") wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) - async_get_scanner(hass)._callback(wrong_device, wrong_adv) + _get_underlying_scanner()._callback(wrong_device, wrong_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -180,7 +181,7 @@ async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start): switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 @@ -219,7 +220,7 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte( manufacturer_data={76: b"\x06\x02\x03\x99"}, ) - async_get_scanner(hass)._callback(hkc_device, hkc_adv) + _get_underlying_scanner()._callback(hkc_device, hkc_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 @@ -227,7 +228,7 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte( mock_config_flow.reset_mock() # 2nd discovery should not generate another flow - async_get_scanner(hass)._callback(hkc_device, hkc_adv) + _get_underlying_scanner()._callback(hkc_device, hkc_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -238,7 +239,7 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte( local_name="lock", service_uuids=[], manufacturer_data={76: b"\x02"} ) - async_get_scanner(hass)._callback(not_hkc_device, not_hkc_adv) + _get_underlying_scanner()._callback(not_hkc_device, not_hkc_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -247,7 +248,7 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte( local_name="lock", service_uuids=[], manufacturer_data={21: b"\x02"} ) - async_get_scanner(hass)._callback(not_apple_device, not_apple_adv) + _get_underlying_scanner()._callback(not_apple_device, not_apple_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -279,10 +280,10 @@ async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): wrong_device = BLEDevice("44:44:33:11:23:42", "wrong_name") wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) - async_get_scanner(hass)._callback(wrong_device, wrong_adv) + _get_underlying_scanner()._callback(wrong_device, wrong_adv) switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) wrong_device_went_unavailable = False switchbot_device_went_unavailable = False @@ -316,8 +317,8 @@ async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): assert wrong_device_went_unavailable is True # See the devices again - async_get_scanner(hass)._callback(wrong_device, wrong_adv) - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(wrong_device, wrong_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) # Cancel the callbacks wrong_device_unavailable_cancel() switchbot_device_unavailable_cancel() @@ -382,25 +383,25 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start, enable_bluetoo service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - async_get_scanner(hass)._callback(empty_device, empty_adv) + _get_underlying_scanner()._callback(empty_device, empty_adv) await hass.async_block_till_done() empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") # 3rd callback raises ValueError but is still tracked - async_get_scanner(hass)._callback(empty_device, empty_adv) + _get_underlying_scanner()._callback(empty_device, empty_adv) await hass.async_block_till_done() cancel() # 4th callback should not be tracked since we canceled - async_get_scanner(hass)._callback(empty_device, empty_adv) + _get_underlying_scanner()._callback(empty_device, empty_adv) await hass.async_block_till_done() assert len(callbacks) == 3 @@ -467,25 +468,25 @@ async def test_register_callback_by_address( service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - async_get_scanner(hass)._callback(empty_device, empty_adv) + _get_underlying_scanner()._callback(empty_device, empty_adv) await hass.async_block_till_done() empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") # 3rd callback raises ValueError but is still tracked - async_get_scanner(hass)._callback(empty_device, empty_adv) + _get_underlying_scanner()._callback(empty_device, empty_adv) await hass.async_block_till_done() cancel() # 4th callback should not be tracked since we canceled - async_get_scanner(hass)._callback(empty_device, empty_adv) + _get_underlying_scanner()._callback(empty_device, empty_adv) await hass.async_block_till_done() # Now register again with a callback that fails to @@ -549,15 +550,15 @@ async def test_wrapped_instance_with_filter( empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - assert async_get_scanner(hass) is not None + assert _get_underlying_scanner() is not None scanner = models.HaBleakScannerWrapper( filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]} ) scanner.register_detection_callback(_device_detected) mock_discovered = [MagicMock()] - type(async_get_scanner(hass)).discovered_devices = mock_discovered - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + type(_get_underlying_scanner()).discovered_devices = mock_discovered + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() discovered = await scanner.discover(timeout=0) @@ -572,17 +573,17 @@ async def test_wrapped_instance_with_filter( # We should get a reply from the history when we register again assert len(detected) == 3 - type(async_get_scanner(hass)).discovered_devices = [] + type(_get_underlying_scanner()).discovered_devices = [] discovered = await scanner.discover(timeout=0) assert len(discovered) == 0 assert discovered == [] - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) assert len(detected) == 4 # The filter we created in the wrapped scanner with should be respected # and we should not get another callback - async_get_scanner(hass)._callback(empty_device, empty_adv) + _get_underlying_scanner()._callback(empty_device, empty_adv) assert len(detected) == 4 @@ -620,22 +621,22 @@ async def test_wrapped_instance_with_service_uuids( empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - assert async_get_scanner(hass) is not None + assert _get_underlying_scanner() is not None scanner = models.HaBleakScannerWrapper( service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] ) scanner.register_detection_callback(_device_detected) - type(async_get_scanner(hass)).discovered_devices = [MagicMock()] + type(_get_underlying_scanner()).discovered_devices = [MagicMock()] for _ in range(2): - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(detected) == 2 # The UUIDs list we created in the wrapped scanner with should be respected # and we should not get another callback - async_get_scanner(hass)._callback(empty_device, empty_adv) + _get_underlying_scanner()._callback(empty_device, empty_adv) assert len(detected) == 2 @@ -673,15 +674,15 @@ async def test_wrapped_instance_with_broken_callbacks( service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) - assert async_get_scanner(hass) is not None + assert _get_underlying_scanner() is not None scanner = models.HaBleakScannerWrapper( service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] ) scanner.register_detection_callback(_device_detected) - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(detected) == 1 @@ -719,23 +720,23 @@ async def test_wrapped_instance_changes_uuids( empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - assert async_get_scanner(hass) is not None + assert _get_underlying_scanner() is not None scanner = models.HaBleakScannerWrapper() scanner.set_scanning_filter( service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] ) scanner.register_detection_callback(_device_detected) - type(async_get_scanner(hass)).discovered_devices = [MagicMock()] + type(_get_underlying_scanner()).discovered_devices = [MagicMock()] for _ in range(2): - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(detected) == 2 # The UUIDs list we created in the wrapped scanner with should be respected # and we should not get another callback - async_get_scanner(hass)._callback(empty_device, empty_adv) + _get_underlying_scanner()._callback(empty_device, empty_adv) assert len(detected) == 2 @@ -772,23 +773,23 @@ async def test_wrapped_instance_changes_filters( empty_device = BLEDevice("11:22:33:44:55:62", "empty") empty_adv = AdvertisementData(local_name="empty") - assert async_get_scanner(hass) is not None + assert _get_underlying_scanner() is not None scanner = models.HaBleakScannerWrapper() scanner.set_scanning_filter( filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]} ) scanner.register_detection_callback(_device_detected) - type(async_get_scanner(hass)).discovered_devices = [MagicMock()] + type(_get_underlying_scanner()).discovered_devices = [MagicMock()] for _ in range(2): - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(detected) == 2 # The UUIDs list we created in the wrapped scanner with should be respected # and we should not get another callback - async_get_scanner(hass)._callback(empty_device, empty_adv) + _get_underlying_scanner()._callback(empty_device, empty_adv) assert len(detected) == 2 @@ -807,7 +808,7 @@ async def test_wrapped_instance_unsupported_filter( with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - assert async_get_scanner(hass) is not None + assert _get_underlying_scanner() is not None scanner = models.HaBleakScannerWrapper() scanner.set_scanning_filter( filters={ @@ -845,7 +846,7 @@ async def test_async_ble_device_from_address(hass, mock_bleak_scanner_start): switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert ( @@ -935,3 +936,9 @@ async def test_raising_runtime_error_when_no_bluetooth(hass): """Test we raise an exception if we try to get the scanner when its not there.""" with pytest.raises(RuntimeError): bluetooth.async_get_scanner(hass) + + +async def test_getting_the_scanner_returns_the_wrapped_instance(hass, enable_bluetooth): + """Test getting the scanner returns the wrapped instance.""" + scanner = bluetooth.async_get_scanner(hass) + assert isinstance(scanner, models.HaBleakScannerWrapper) diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index 010989628e10..48f2e8edf06d 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -12,7 +12,6 @@ from homeassistant.components.bluetooth import ( DOMAIN, UNAVAILABLE_TRACK_SECONDS, BluetoothChange, - async_get_scanner, ) from homeassistant.components.bluetooth.passive_update_coordinator import ( PassiveBluetoothCoordinatorEntity, @@ -27,6 +26,8 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util +from . import _get_underlying_scanner + from tests.common import MockEntityPlatform, async_fire_time_changed _LOGGER = logging.getLogger(__name__) @@ -208,7 +209,7 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert len(mock_add_entities.mock_calls) == 1 assert coordinator.available is True - scanner = async_get_scanner(hass) + scanner = _get_underlying_scanner() with patch( "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices",