@@ -91,6 +91,14 @@ class DeviceRemovedCallbackAndState(NamedTuple):
9191 """
9292
9393
94+ DiscoveryStoppedCallback = Callable [[], None ]
95+
96+
97+ class DiscoveryStoppedCallbackAndState (NamedTuple ):
98+ callback : DiscoveryStoppedCallback
99+ adapter_path : str
100+
101+
94102DeviceConnectedChangedCallback = Callable [[bool ], None ]
95103"""
96104A callback that is called when a device's "Connected" property changes.
@@ -167,6 +175,7 @@ def __init__(self):
167175
168176 self ._advertisement_callbacks : List [CallbackAndState ] = []
169177 self ._device_removed_callbacks : List [DeviceRemovedCallbackAndState ] = []
178+ self ._discovery_stopped_callbacks : List [DiscoveryStoppedCallbackAndState ] = []
170179 self ._device_watchers : Set [DeviceWatcher ] = set ()
171180 self ._condition_callbacks : Set [Callable ] = set ()
172181 self ._services_cache : Dict [str , BleakGATTServiceCollection ] = {}
@@ -312,6 +321,7 @@ async def active_scan(
312321 filters : Dict [str , Variant ],
313322 advertisement_callback : AdvertisementCallback ,
314323 device_removed_callback : DeviceRemovedCallback ,
324+ discovery_stopped_callback : DiscoveryStoppedCallback ,
315325 ) -> Callable [[], Coroutine ]:
316326 """
317327 Configures the advertisement data filters and starts scanning.
@@ -323,6 +333,9 @@ async def active_scan(
323333 A callable that will be called when new advertisement data is received.
324334 device_removed_callback:
325335 A callable that will be called when a device is removed from BlueZ.
336+ discovery_stopped_callback:
337+ A callable that will be called if discovery is stopped early
338+ (before stop was requested by calling the return value).
326339
327340 Returns:
328341 An async function that is used to stop scanning and remove the filters.
@@ -342,6 +355,13 @@ async def active_scan(
342355 )
343356 self ._device_removed_callbacks .append (device_removed_callback_and_state )
344357
358+ discovery_stopped_callback_and_state = DiscoveryStoppedCallbackAndState (
359+ discovery_stopped_callback , adapter_path
360+ )
361+ self ._discovery_stopped_callbacks .append (
362+ discovery_stopped_callback_and_state
363+ )
364+
345365 try :
346366 # Apply the filters
347367 reply = await self ._bus .call (
@@ -375,6 +395,9 @@ async def stop() -> None:
375395 self ._device_removed_callbacks .remove (
376396 device_removed_callback_and_state
377397 )
398+ self ._discovery_stopped_callbacks .remove (
399+ discovery_stopped_callback_and_state
400+ )
378401
379402 async with self ._bus_lock :
380403 reply = await self ._bus .call (
@@ -413,6 +436,7 @@ async def passive_scan(
413436 filters : List [OrPatternLike ],
414437 advertisement_callback : AdvertisementCallback ,
415438 device_removed_callback : DeviceRemovedCallback ,
439+ discovery_stopped_callback : DiscoveryStoppedCallback ,
416440 ) -> Callable [[], Coroutine ]:
417441 """
418442 Configures the advertisement data filters and starts scanning.
@@ -444,7 +468,7 @@ async def passive_scan(
444468 self ._device_removed_callbacks .append (device_removed_callback_and_state )
445469
446470 try :
447- monitor = AdvertisementMonitor (filters )
471+ monitor = AdvertisementMonitor (filters , discovery_stopped_callback )
448472
449473 # this should be a unique path to allow multiple python interpreters
450474 # running bleak and multiple scanners within a single interpreter
@@ -828,7 +852,14 @@ def _parse_msg(self, message: Message):
828852 # then call any callbacks so they will be called with the
829853 # updated state
830854
831- if interface == defs .DEVICE_INTERFACE :
855+ if interface == defs .ADAPTER_INTERFACE :
856+ if "Discovering" in changed and not self_interface ["Discovering" ]:
857+ for (
858+ discovery_stopped_callback ,
859+ _ ,
860+ ) in self ._discovery_stopped_callbacks :
861+ discovery_stopped_callback ()
862+ elif interface == defs .DEVICE_INTERFACE :
832863 # handle advertisement watchers
833864
834865 self ._run_advertisement_callbacks (
0 commit comments