diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 8568724c0b1..3559adfd976 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -57,6 +57,7 @@ from .api import ( _get_manager, async_address_present, async_ble_device_from_address, + async_current_scanners, async_discovered_service_info, async_get_advertisement_callback, async_get_fallback_availability_interval, @@ -114,6 +115,7 @@ __all__ = [ "HomeAssistantRemoteScanner", "async_address_present", "async_ble_device_from_address", + "async_current_scanners", "async_discovered_service_info", "async_get_advertisement_callback", "async_get_fallback_availability_interval", diff --git a/homeassistant/components/bluetooth/api.py b/homeassistant/components/bluetooth/api.py index 00e585fa266..f12d22cc8b5 100644 --- a/homeassistant/components/bluetooth/api.py +++ b/homeassistant/components/bluetooth/api.py @@ -66,6 +66,22 @@ def async_scanner_count(hass: HomeAssistant, connectable: bool = True) -> int: return _get_manager(hass).async_scanner_count(connectable) +@hass_callback +def async_current_scanners(hass: HomeAssistant) -> list[BaseHaScanner]: + """Return the list of currently active scanners. + + This method returns a list of all active Bluetooth scanners registered + with Home Assistant, including both connectable and non-connectable scanners. + + Args: + hass: Home Assistant instance + + Returns: + List of all active scanner instances + """ + return _get_manager(hass).async_current_scanners() + + @hass_callback def async_discovered_service_info( hass: HomeAssistant, connectable: bool = True diff --git a/tests/components/bluetooth/test_api.py b/tests/components/bluetooth/test_api.py index 74373da6865..2afd59e83cf 100644 --- a/tests/components/bluetooth/test_api.py +++ b/tests/components/bluetooth/test_api.py @@ -9,6 +9,7 @@ from homeassistant.components import bluetooth from homeassistant.components.bluetooth import ( MONOTONIC_TIME, BaseHaRemoteScanner, + BluetoothScanningMode, HaBluetoothConnector, async_scanner_by_source, async_scanner_devices_by_address, @@ -16,6 +17,7 @@ from homeassistant.components.bluetooth import ( from homeassistant.core import HomeAssistant from . import ( + FakeRemoteScanner, FakeScanner, MockBleakClient, _get_manager, @@ -161,3 +163,68 @@ async def test_async_scanner_devices_by_address_non_connectable( assert devices[0].ble_device.name == switchbot_device.name assert devices[0].advertisement.local_name == switchbot_device_adv.local_name cancel() + + +@pytest.mark.usefixtures("enable_bluetooth") +async def test_async_current_scanners(hass: HomeAssistant) -> None: + """Test getting the list of current scanners.""" + # The enable_bluetooth fixture registers one scanner + initial_scanners = bluetooth.async_current_scanners(hass) + assert len(initial_scanners) == 1 + initial_scanner_count = len(initial_scanners) + + # Verify current_mode is accessible on the initial scanner + for scanner in initial_scanners: + assert hasattr(scanner, "current_mode") + # The mode might be None or a BluetoothScanningMode enum value + + # Register additional connectable scanners + hci0_scanner = FakeScanner("hci0", "hci0") + hci1_scanner = FakeScanner("hci1", "hci1") + cancel_hci0 = bluetooth.async_register_scanner(hass, hci0_scanner) + cancel_hci1 = bluetooth.async_register_scanner(hass, hci1_scanner) + + # Test that the new scanners are added + scanners = bluetooth.async_current_scanners(hass) + assert len(scanners) == initial_scanner_count + 2 + assert hci0_scanner in scanners + assert hci1_scanner in scanners + + # Verify current_mode is accessible on all scanners + for scanner in scanners: + assert hasattr(scanner, "current_mode") + # Verify it's None or the correct type (BluetoothScanningMode) + assert scanner.current_mode is None or isinstance( + scanner.current_mode, BluetoothScanningMode + ) + + # Register non-connectable scanner + connector = HaBluetoothConnector( + MockBleakClient, "mock_bleak_client", lambda: False + ) + hci2_scanner = FakeRemoteScanner("hci2", "hci2", connector, False) + cancel_hci2 = bluetooth.async_register_scanner(hass, hci2_scanner) + + # Test that all scanners are returned (both connectable and non-connectable) + all_scanners = bluetooth.async_current_scanners(hass) + assert len(all_scanners) == initial_scanner_count + 3 + assert hci0_scanner in all_scanners + assert hci1_scanner in all_scanners + assert hci2_scanner in all_scanners + + # Verify current_mode is accessible on all scanners including non-connectable + for scanner in all_scanners: + assert hasattr(scanner, "current_mode") + # The mode should be None or a BluetoothScanningMode instance + assert scanner.current_mode is None or isinstance( + scanner.current_mode, BluetoothScanningMode + ) + + # Clean up our scanners + cancel_hci0() + cancel_hci1() + cancel_hci2() + + # Verify we're back to the initial scanner + final_scanners = bluetooth.async_current_scanners(hass) + assert len(final_scanners) == initial_scanner_count