Skip to content

feat: add Remote With Screen (Universal Remote) support#520

Draft
Onero-testdev wants to merge 1 commit into
sblibs:mainfrom
Onero-testdev:feat/universal-remote
Draft

feat: add Remote With Screen (Universal Remote) support#520
Onero-testdev wants to merge 1 commit into
sblibs:mainfrom
Onero-testdev:feat/universal-remote

Conversation

@Onero-testdev

@Onero-testdev Onero-testdev commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Add support for the SwitchBot Universal Remote (internal name Remote With Screen / WoRemoteWithScreen).

This is a new model that exposes its battery level and charging state. Support is added so consumers (e.g. Home Assistant) can surface battery status.

Details

Advertisement parsing

  • New process_woremote_with_screen(data, mfr_data) parser in adv_parsers/remote.py.
  • Battery and charging state are encoded in the manufacturer specific data. ADV byte 14 maps to mfr_data[9] (manufacturer data starts at ADV byte 5):
    • bit 7 → charging state (0 = not charging, 1 = charging)
    • bits 6-0 → battery level (1-100%)
  • Returns {"battery": None, "charging": None} when mfr_data is missing or too short (guards against IndexError).

Device class

  • New SwitchbotRemoteWithScreen device (devices/remote_with_screen.py), exported from the package.
  • get_basic_info() queries the device over BLE using the general basic-info command (5702) and parses res[1] = battery, res[12] = charging.

Registration

  • New SwitchbotModel.REMOTE_WITH_SCREEN = "WoRemoteWithScreen".
  • Registered in SUPPORTED_TYPES for both the pairing-state device-type byte 0x07 and the normal-state byte 0x27 ('), manufacturer id 2409 (UUID 0x0969).
  • Added "WoRemoteWithScreen" to API_MODEL_TO_ENUM.

Tests

  • tests/test_remote_with_screen.pyget_basic_info battery/charging parsing + empty-response handling.
  • tests/test_adv_parser.py — active-scan parsing and charging-bit handling.
  • tests/test_short_payload_guards.py — short/None mfr_data guard.

Full suite passes locally (1222 passed).

Note from SwitchBot team

This change is developed by the official SwitchBot team. The implementation has been verified with real hardware testing on physical devices.

If you have any questions or need clarification, please reach out:

Add support for the SwitchBot Universal Remote (Remote With Screen,
WoRemoteWithScreen). The device advertises its battery level and charging
state in the manufacturer specific data, and the battery level / charging
state can also be queried over BLE via the basic info command.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
switchbot/__init__.py 100.00% <100.00%> (ø)
switchbot/adv_parser.py 97.82% <100.00%> (ø)
switchbot/adv_parsers/remote.py 100.00% <100.00%> (ø)
switchbot/const/__init__.py 100.00% <100.00%> (ø)
switchbot/devices/device.py 69.90% <ø> (ø)
switchbot/devices/remote_with_screen.py 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Onero-testdev Onero-testdev marked this pull request as draft June 17, 2026 03:08
@bluetoothbot

Copy link
Copy Markdown
Collaborator

PR Review — feat: add Remote With Screen (Universal Remote) support

Clean, well-scoped new-device PR that follows the codebase's established patterns. No blocking issues.

Specific strengths:

  • Parser guard is sized correctly: len(mfr_data) < 10 protects the read at mfr_data[9] (max_index + 1), matching the project's parser-guard convention exactly.
  • Battery/charging bit math (& 0b01111111, >> 7) is correct and matches both the docstring and the active-scan tests.
  • Uses service-data first-byte discriminators ('/0x27, \x07) rather than the manufacturer_data_length discovery hint that was previously rejected — the right call. Verified both keys are free on the real PR base (sblibs/master), so no collision.
  • Registration is complete and consistent: const, API_MODEL_TO_ENUM, package export, and SUPPORTED_TYPES all wired up.

Minor, non-blocking:

  • get_basic_info reads _data[12] with no length guard; a short non-empty command reply would IndexError. Consistent with sibling devices (bot/fan don't guard either), but you read a higher index than any of them, so a len(_data) < 13 guard would be more robust.
  • The \x07 pairing-state dispatch branch is registered but not exercised by any parse test (only the ' path is tested).

🟢 Suggestions

1. No length guard before reading _data[12]
switchbot/devices/remote_with_screen.py:22

get_basic_info reads _data[12] (and _data[1]) after only the if not _data check. The base _get_basic_info (device.py:797) filters single-byte error replies (b"\x07"/b"\x00") but lets any longer payload through.

A truncated 2-to-12-byte command response — which happens with BLE-proxy stripping or a firmware error state — would pass that filter and then raise IndexError on _data[12], propagating out of update() and crashing the polling cycle. There is a real report of exactly this crash class for the 5702 path (the short-payload _get_basic_info issue).

This is only a suggestion because the current convention does not guard either — bot.py reads _data[10] and fan.py reads _data[9] unguarded on the same base. So this code is consistent with its siblings. But since you read a higher index ([12]) than any existing device, a cheap guard would be more robust:

if not (_data := await self._get_basic_info()) or len(_data) < 13:
    return None

The existing empty-response test covers None but not the short-but-non-empty payload, so a length-13 guard plus a short-payload test would close the gap.

        return {
            "battery": _data[1],
            "charging": bool(_data[12]),
        }
2. Pairing-state key \x07 has no parse test
switchbot/adv_parser.py:404-409

Two SUPPORTED_TYPES entries are registered for this model: "\x07" (pairing state) and "'" / 0x27 (normal state). Both test_remote_with_screen_active and test_remote_with_screen_charging exercise only the ' path — the \x07 dispatch branch is never parsed in a test.

Why it matters: codecov reporting 100% on the added lines only confirms the dict literal exists, not that the \x07 first-byte dispatch actually resolves to process_woremote_with_screen. If the pairing-state byte or its 0b01111111 masking were ever wrong, no test would catch it.

Low effort to close: add one parse assertion with service_data={...: b"\x07\x00"} to confirm it routes to the same parser. Not blocking.

    "\x07": {
        "modelName": SwitchbotModel.REMOTE_WITH_SCREEN,
        "modelFriendlyName": "Remote With Screen",
        "func": process_woremote_with_screen,
        "manufacturer_id": 2409,
    },

Checklist

  • Short-payload guard on adv parser
  • Device-level index reads guarded — suggestion #1
  • Dispatch keys free of collision
  • Test coverage for new branches — suggestion #2
  • Registration complete (const/export/enum/types)
  • No mutable defaults / is-vs-== misuse

Automated review by Kōan (Claude) HEAD=1328a8f 4 min 6s

@bluetoothbot bluetoothbot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No blocking issues found.

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