diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 20120dce..c1c82241 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,11 +13,12 @@ and this project adheres to `Semantic Versioning "BleakScanner": + await self.start() return self async def __aexit__(self, exc_type, exc_val, exc_tb): - await self._backend.stop() + await self.stop() def register_detection_callback( self, callback: Optional[AdvertisementDataCallback] @@ -184,6 +185,7 @@ async def start(self): async def stop(self): """Stop scanning for devices""" await self._backend.stop() + self._backend.new_devices = None def set_scanning_filter(self, **kwargs): """ @@ -204,6 +206,55 @@ def set_scanning_filter(self, **kwargs): ) self._backend.set_scanning_filter(**kwargs) + def __aiter__(self) -> AsyncIterator[Tuple[BLEDevice, AdvertisementData]]: + self._backend.new_devices = asyncio.Queue() + return self + + async def __anext__(self) -> Tuple[BLEDevice, AdvertisementData]: + return await self._backend.new_devices.get() + + @overload + def devices( + self, timeout: Optional[float] = None, return_adv: Literal[False] = False + ) -> AsyncIterator[BLEDevice]: + ... + + @overload + def devices( + self, timeout: Optional[float] = None, return_adv: Literal[True] = True + ) -> AsyncIterator[Tuple[BLEDevice, AdvertisementData]]: + ... + + async def devices(self, timeout: Optional[float] = None, return_adv: bool = False): + """ + Continuously scan for devices and yield them. + + Args: + timeout: + The maximum duration, in seconds, to scan for each individual device. + This is not a total scan duration. Each invocation of ``next()`` will + wait for up to ``timeout`` seconds for a new device to be discovered + before stopping the iteration. + return_adv: + If ``True``, the return value will also include advertising data. + + Returns: + An async iterator that yields instances of :class:`BLEDevice` if ``return_adv`` + parameter is ``False`` or tuple of (:class:`BLEDevice`, :class:`AdvertisementData`) + if the parameter is ``True``. + """ + self._backend.new_devices = asyncio.Queue() + while True: + try: + bd, ad = await asyncio.wait_for( + self._backend.new_devices.get(), timeout=timeout + ) + yield (bd, ad) if return_adv else bd + except asyncio.TimeoutError: + break + finally: + self._backend.new_devices = None + @overload @classmethod async def discover( @@ -360,16 +411,12 @@ async def find_device_by_filter( the timeout. """ - found_device_queue: asyncio.Queue[BLEDevice] = asyncio.Queue() - - def apply_filter(d: BLEDevice, ad: AdvertisementData): - if filterfunc(d, ad): - found_device_queue.put_nowait(d) - - async with cls(detection_callback=apply_filter, **kwargs): + async with cls(**kwargs) as scanner: try: async with async_timeout(timeout): - return await found_device_queue.get() + async for d, ad in scanner: + if filterfunc(d, ad): + return d except asyncio.TimeoutError: return None diff --git a/bleak/backends/scanner.py b/bleak/backends/scanner.py index 0ae33641..d13ea743 100644 --- a/bleak/backends/scanner.py +++ b/bleak/backends/scanner.py @@ -141,6 +141,9 @@ def __init__( ) self.seen_devices = {} + self.new_devices: Optional[ + asyncio.Queue[Tuple[BLEDevice, AdvertisementData]] + ] = None def register_detection_callback( self, callback: Optional[AdvertisementDataCallback] @@ -215,7 +218,10 @@ def create_or_update_device( **metadata, ) - self.seen_devices[address] = (device, adv) + dev = (device, adv) + self.seen_devices[address] = dev + if self.new_devices: + self.new_devices.put_nowait(dev) return device diff --git a/examples/scan_iterator.py b/examples/scan_iterator.py new file mode 100644 index 00000000..91a203c1 --- /dev/null +++ b/examples/scan_iterator.py @@ -0,0 +1,35 @@ +""" +Scan/Discovery Async Iterator +-------------- + +Example showing how to scan for BLE devices using async iterator instead of callback function + +Created on 2023-07-27 by bojanpotocnik + +""" +import asyncio + +from bleak import BleakScanner + + +async def main(): + x = 5 + print(f"Scanning for {x} devices...") + async with BleakScanner() as scanner: + async for bd, ad in scanner: + print(f"{x}. {bd!r} with {ad!r}") + x -= 1 + if x == 0: + break + + x = 6 + print(f"\nScanning for a device with name longer than {x} characters...") + async with BleakScanner() as scanner: + async for bd, ad in scanner.devices(timeout=10, return_adv=True): + print(f"Found{' it' if len(bd.name) > x else ''} {bd!r} with {ad!r}") + if len(bd.name or "") > x or len(ad.local_name or "") > x: + break + + +if __name__ == "__main__": + asyncio.run(main())