Skip to content

Conversation

@hnw
Copy link
Contributor

@hnw hnw commented Jan 6, 2026

Description

This PR adds a callback parameter to the GetSwitchbotDevices class. This allows consumers of the library to process device advertisements in real-time as they are discovered.

Motivation / Real-world Use Case

I maintain an OSS project called switchbot-actions, which relies on pySwitchbot for device scanning. This tool triggers actions (webhooks, MQTT, shell commands) based on sensor updates. A common use case is turning on a light via webhook immediately when a motion sensor detects movement.

Currently, GetSwitchbotDevices.discover() waits for the full scan_timeout (default 5s) to complete before returning results. This introduces an average latency of half the scan duration, causing a noticeable delay in automation.

By adding a callback, applications can process the advertisement packet the moment it is received, enabling near real-time responsiveness without shortening the scan duration.

Changes

  • Updated GetSwitchbotDevices.__init__ to accept a callback.
  • Updated detection_callback to invoke the user-provided callback.
  • Implemented logic to handle both synchronous and asynchronous callables.
  • Added background task management (_background_tasks) for async callbacks to ensure references are kept alive during execution.

Technical Details

  • Uses inspect.iscoroutinefunction to determine if the callback should be scheduled as an asyncio task.
  • Adds exception handling within the callback execution to prevent discovery interruptions due to user-land errors.

Backward Compatibility

  • The callback parameter is optional. Existing implementations using discover() will function exactly as before without any changes.

Example Usage

from switchbot import GetSwitchbotDevices, SwitchBotAdvertisement

async def on_device_discovered(device: SwitchBotAdvertisement):
    print(f"Discovered: {device.address} - {device.data}")

# Pass the callback during initialization
scanner = GetSwitchbotDevices(callback=on_device_discovered)
await scanner.discover()

Tests

  • Added tests/test_discovery_callback.py covering:
    • Synchronous callbacks.
    • Asynchronous callbacks.
    • Exception handling (ensuring discovery continues if the callback fails).

This introduces a `callback` parameter to the `GetSwitchbotDevices` constructor.
It allows users to handle device advertisements immediately upon detection.

Key changes:
- Support both synchronous and asynchronous callback functions.
- Add comprehensive tests for sync/async execution and error handling.
@codecov
Copy link

codecov bot commented Jan 6, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
switchbot/discovery.py 56.09% <100.00%> (+21.43%) ⬆️

... and 8 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment on lines 57 to 63
try:
if inspect.iscoroutinefunction(self._callback):
task = asyncio.create_task(self._callback(discovery))
self._background_tasks.add(task)
task.add_done_callback(self._handle_async_callback_result)
else:
self._callback(discovery)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets keep it simple and only support a sync function. Callers can always make their own tasks

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Following your feedback, I've simplified the callback to be sync-only. Updated the implementation and tests accordingly.

Copy link
Member

@bdraco bdraco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see comments above

Only support sync functions following the feedback. Callers can manage their own tasks for async execution.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants