Skip to content

Conversation

bdraco
Copy link
Member

@bdraco bdraco commented Sep 10, 2025

Fixes #284

Summary

This PR adds support for discovering Switchbot devices when the Bluetooth adapter is performing passive scans by fetching device information from the Switchbot cloud API and caching MAC address to model mappings.

Problem

When a Bluetooth adapter is configured for passive scanning (not sending scan requests), it only receives passive advertisements from devices. These passive advertisements from Switchbot devices contain only manufacturer data without service data, making it impossible to identify the device model from the advertisement alone. This causes discovery to fail when the adapter is in passive scanning mode.

Solution

Added a new fetch_cloud_devices() function that:

  1. Authenticates with the Switchbot cloud API using existing credentials
  2. Retrieves all devices associated with the account
  3. Populates an internal MAC address to model cache
  4. Returns a dictionary of discovered devices for reference

Once the cache is populated, the advertisement parser can identify passive devices by their MAC address and correctly decode their advertisements.

Usage

import asyncio
import aiohttp
from switchbot import fetch_cloud_devices

async def discover_devices():
    async with aiohttp.ClientSession() as session:
        # Call once before starting discovery
        # This populates the internal cache with your devices
        devices = await fetch_cloud_devices(
            session,
            username="[email protected]", 
            password="your_password"
        )
        
        print(f"Found {len(devices)} devices in cloud")
        
        # Now proceed with normal BLE discovery
        # The advertisement parser will automatically use the cache
        # to identify devices in passive mode
        # ... your existing discovery code ...

How It Works

  1. Before this PR: When adapter is in passive scan mode, advertisements couldn't be decoded

    # Bluetooth adapter in passive scan mode receives limited advertisement data
    # (no active scan requests sent, only passive advertisements received)
    result = parse_advertisement_data(device, adv_data)
    # result would be None - device not recognized
  2. After this PR: Same passive scan advertisements are now decoded

    # First, fetch cloud devices (done once at startup)
    await fetch_cloud_devices(session, username, password)
    
    # Now passive scan advertisements can be decoded
    result = parse_advertisement_data(device, adv_data)
    # result contains full device information!

Implementation Details

New Components

  • switchbot/utils.py: Added format_mac_upper() to normalize MAC addresses
  • switchbot/devices/device.py:
    • Modified get_devices() to return dict[str, SwitchbotModel] with formatted MACs
    • Added fetch_cloud_devices() as the public API
    • Added _extract_region() helper to reduce code duplication
  • switchbot/adv_parser.py:
    • Added _MODEL_TO_MAC_CACHE for storing MAC to model mappings
    • Added populate_model_to_mac_cache() helper function
    • Parser now checks cache when model cannot be determined from advertisement

API Model Mappings

Added mappings for all known Switchbot device types:

  • WoHand → BOT
  • WoCurtain → CURTAIN
  • WoLock → LOCK
  • WoBulb → COLOR_BULB
  • WoStrip → LIGHT_STRIP
  • WoMeterPlus → METER_PRO
  • And many more...

Unknown models are logged with their full payload to help identify new devices.

Testing

Added comprehensive test coverage:

  • tests/test_device.py: Tests for get_devices() and fetch_cloud_devices()
  • tests/test_utils.py: Tests for format_mac_upper()
  • tests/test_adv_parser.py: Tests showing cache enables passive device discovery

Benefits

  • Devices are now discoverable when Bluetooth adapter is in passive scan mode
  • Reduces Bluetooth traffic (no active scan requests needed)
  • Better for environments with many Bluetooth devices
  • Automatic region detection (US, EU, JP, etc.)
  • Logs unknown device models for future support

Breaking Changes

None. This is purely additive - existing code continues to work as before.

Example Use Case

For Home Assistant or other integrations:

from switchbot import fetch_cloud_devices
from switchbot.const import (
    SwitchbotAccountConnectionError,
    SwitchbotAuthenticationError,
)

async def setup_switchbot_discovery(username, password):
    """Setup Switchbot discovery with cloud device fetching."""
    async with aiohttp.ClientSession() as session:
        try:
            # Fetch and cache cloud devices once at startup
            cloud_devices = await fetch_cloud_devices(session, username, password)
            _LOGGER.info(f"Cached {len(cloud_devices)} Switchbot devices from cloud")
        except SwitchbotAuthenticationError as e:
            _LOGGER.warning(f"Invalid Switchbot credentials: {e}")
            # Discovery will still work for active devices
        except SwitchbotAccountConnectionError as e:
            _LOGGER.warning(f"Could not connect to Switchbot cloud: {e}")
            # Discovery will still work for active devices
        
        # Continue with normal BLE discovery
        # Passive devices will now be recognized automatically
        await start_ble_discovery()

Notes

  • Cloud fetch is optional - discovery still works without it for actively advertising devices
  • Cache persists for the lifetime of the process
  • Credentials are only used for the initial cloud fetch, not stored
  • Works with all regions (automatically detects US, EU, JP, etc.)

@bdraco bdraco changed the title Discover device models from account Fix device discovery when Bluetooth adapter is in passive scanning mode Sep 10, 2025
@bdraco bdraco marked this pull request as ready for review September 10, 2025 17:36
@bdraco bdraco merged commit 60c2725 into master Sep 10, 2025
4 checks passed
@bdraco bdraco deleted the discover_devices_models branch September 10, 2025 17:37
Copy link

codecov bot commented Sep 10, 2025

Codecov Report

❌ Patch coverage is 83.56164% with 12 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
switchbot/devices/device.py 77.35% 12 Missing ⚠️
Files with missing lines Coverage Δ
switchbot/__init__.py 100.00% <ø> (ø)
switchbot/adv_parser.py 94.05% <100.00%> (+0.44%) ⬆️
switchbot/utils.py 100.00% <100.00%> (ø)
switchbot/devices/device.py 63.35% <77.35%> (+2.36%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.

SwitchBot devices require active scans to be discovered
1 participant