Skip to content

feat(lock): add Quick Key support for Lock Ultra#515

Open
mayerwin wants to merge 2 commits into
sblibs:mainfrom
mayerwin:lock-ultra-quick-key
Open

feat(lock): add Quick Key support for Lock Ultra#515
mayerwin wants to merge 2 commits into
sblibs:mainfrom
mayerwin:lock-ultra-quick-key

Conversation

@mayerwin

Copy link
Copy Markdown

Summary

Adds support for the Quick Key setting on the SwitchBot Lock Ultra. All three of its sub-settings live in a single config byte on the lock, read/written over BLE:

  • SwitchbotLock.get_quick_key() — reads + parses the byte into {"enabled": bool, "double_press": bool, "function": QuickKeyFunction}.
  • SwitchbotLock.set_quick_key(*, enabled=None, double_press=None, function=None) — a masked write that changes only the fields you pass (the rest keep their current value) and confirms the lock echoed the requested bits.
  • A QuickKeyFunction enum (LOCK_AND_UNLOCK / UNLOCK_ONLY / LOCK_ONLY).

Scoped to LOCK_ULTRA; other lock models raise SwitchbotOperationError (the command set is untested there).

Protocol

read : 57 0f 4f 04 01                         -> <status> <CFG> ...
write: 57 0f 4e 04 01 00 <mask> <value> ff     (masked write of the low nibble)

CFG low nibble: bit 3 = enabled, bit 2 = double-press, bits 1-0 = function.

Testing

  • Hardware: verified on a SwitchBot Lock Ultra (firmware V2.6) — every enabled / single-vs-double / function combination writes and reads back correctly.
  • Unit tests: added to tests/test_lock.py (get, masked single + multi-field set, no-op guard, unsupported-model guard). The full tests/test_lock.py passes (203 tests).

Note

As an interim solution I've published a Home Assistant custom integration that uses this protocol (via the existing connection) for anyone who needs it before this merges: https://github.com/mayerwin/ha-switchbot-lock-quick-key-support — it can be retired once this lands.

@codecov

codecov Bot commented Jun 10, 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% <ø> (ø)
switchbot/const/__init__.py 100.00% <100.00%> (ø)
switchbot/const/lock.py 100.00% <100.00%> (ø)
switchbot/devices/lock.py 100.00% <100.00%> (ø)

... and 12 files with indirect coverage changes

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

@mayerwin mayerwin force-pushed the lock-ultra-quick-key branch from 0a0cd06 to ff04a06 Compare June 10, 2026 04:38
Expose the Lock Ultra "Quick Key" setting. All three of its settings live in a
single config byte: get_quick_key() reads and parses it, set_quick_key() does a
masked write of any subset of enabled / single-vs-double press / function
(Lock & Unlock, Unlock Only, Lock Only). Adds a QuickKeyFunction enum and unit
tests. Scoped to LOCK_ULTRA (other lock models are untested). Tested on a SwitchBot
Lock Ultra (firmware V2.6).

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds Lock Ultra support for reading and updating the “Quick Key” configuration over BLE, including a new enum to represent the 2-bit function field and corresponding unit tests.

Changes:

  • Introduces QuickKeyFunction enum for the Quick Key function field.
  • Adds SwitchbotLock.get_quick_key() and SwitchbotLock.set_quick_key() (masked write) for Lock Ultra.
  • Adds unit tests covering read, write (single/multi-field), no-op guard, and unsupported-model guard.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
switchbot/devices/lock.py Adds Quick Key BLE command constants and get/set APIs with bitmask parsing/verification.
switchbot/const/lock.py Defines QuickKeyFunction enum used by the new Quick Key APIs.
tests/test_lock.py Adds unit tests validating Quick Key read/parse behavior and masked-write command generation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread switchbot/devices/lock.py
Comment on lines +185 to +192
@staticmethod
def _parse_quick_key(cfg: int) -> dict[str, Any]:
"""Parse the Quick Key config byte."""
return {
"enabled": bool(cfg & QUICK_KEY_ENABLED_BIT),
"double_press": bool(cfg & QUICK_KEY_DOUBLE_PRESS_BIT),
"function": QuickKeyFunction(cfg & QUICK_KEY_FUNCTION_MASK),
}
@bluetoothbot

Copy link
Copy Markdown
Collaborator

PR Review — feat(lock): add Quick Key support for Lock Ultra

Clean, well-tested Quick Key feature for Lock Ultra; one defensive-parsing gap worth fixing before merge.

Strengths:

  • Masked-write design is correct — mask/value are built only from passed fields, and the echo confirmation (result[1] & mask) == (value & mask) correctly validates only the touched bits, including the all-zero LOCK_ONLY case.
  • Hardware-verified protocol, scoped strictly to LOCK_ULTRA with SwitchbotOperationError on other models, matching the existing COMMAND_HALF_LOCK pattern.
  • Thorough tests (get, masked single/multi-field set, no-op ValueError guard, unsupported-model, short-response, rejected-write) with 100% coverage; length guards (len(result) < 2) mirror the repo's get_basic_info audit work.

Needs attention:

  • warning: _parse_quick_key calls QuickKeyFunction(cfg & 0x03) on a 2-bit field with only 3 defined members — a 0b11 reading raises an uncaught ValueError out of get_quick_key, breaking the return-None-on-malformed contract every other path here honors.
  • suggestion: QuickKeyFunction isn't exported from switchbot/__init__.py though sibling LockStatus is — callers need it for set_quick_key(function=...).

🟡 Important

1. Undefined 4th function value crashes get_quick_key with ValueError
switchbot/devices/lock.py:191

The function field is 2 bits (QUICK_KEY_FUNCTION_MASK = 0x03), so cfg & 0x03 can yield any of 4 values (0b00–0b11), but QuickKeyFunction only defines three members (0b00, 0b01, 0b10). If the lock ever reports 0b11 in that field, QuickKeyFunction(cfg & 0x03) raises ValueError.

Why it matters: unlike the other failure paths in get_quick_key (non-success status, short payload), this exception is not caught — it propagates uncaught out of get_quick_key() and crashes the caller (Home Assistant's coordinator update). This breaks the repo's consistent defensive-parsing contract for lock command responses: every other malformed-payload path here returns None and logs, rather than throwing. A firmware variant, a reserved/4th function, or a corrupted BLE-proxy byte would turn a settings read into a traceback.

Fix: guard the conversion, e.g.

func_bits = cfg & QUICK_KEY_FUNCTION_MASK
try:
    function = QuickKeyFunction(func_bits)
except ValueError:
    _LOGGER.error("Unknown Quick Key function bits: %s", func_bits)
    return None

(and have _parse_quick_key signal the failure to get_quick_key), or add the 0b11 member if its meaning is known. The high-bit status flags (0xC0) are already masked off, so this is the one remaining unbounded field.

"function": QuickKeyFunction(cfg & QUICK_KEY_FUNCTION_MASK),

🟢 Suggestions

1. QuickKeyFunction not exported from package root
switchbot/const/lock.py:16-26

LockStatus (the sibling enum in this file) is re-exported from switchbot/__init__.py and listed in __all__, but QuickKeyFunction is not. Callers of set_quick_key(function=...) need this enum, and will have to reach into switchbot.const.lock directly while every other public lock type is available from switchbot.

Why it matters: it's an avoidable inconsistency in the public API surface — CLAUDE.md lists "an export in __init__.py" as part of adding a device-facing type. Add QuickKeyFunction to the imports and __all__ in switchbot/__init__.py alongside LockStatus.


Checklist

  • Command response payloads length-guarded before indexing
  • Malformed-payload paths return None instead of raising — warning #1
  • Feature scoped to supported model with clear error
  • Public types exported consistently — suggestion #2
  • Edge cases tested (empty/short/rejected/no-op)
  • No mutable default args / is-vs-== misuse

To rebase specific severity levels, mention me: @bluetoothbot rebase critical (fixes 🔴 only), @bluetoothbot rebase important (fixes 🔴 + 🟡), or just @bluetoothbot rebase for all.


Automated review by Kōan (Claude) HEAD=1f1e33f 3 min 1s

@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.

Blocking issues found.

  • Undefined 4th function value crashes get_quick_key with ValueError

Address PR review feedback:

- get_quick_key() no longer raises on an undefined function value. The
  2-bit function field has 4 possible values but only 3 are defined, so a
  0b11 reading (firmware variant or corrupted BLE byte) now logs and
  returns None, matching the return-None-on-malformed contract that the
  other parse paths already honor.
- Export QuickKeyFunction from switchbot/__init__.py and the const package
  alongside LockStatus, so callers of set_quick_key(function=...) can
  import it from the package root.
- Add a test for the undefined-function-value path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mayerwin

Copy link
Copy Markdown
Author

Thanks for the review. Both points are addressed in 08a5feb:

  1. Undefined function value. _parse_quick_key now checks the 2-bit function field against the set of defined QuickKeyFunction values before building the enum. An undefined value (e.g. 0b11 from a firmware variant or a corrupted BLE-proxy byte) logs and returns None, so get_quick_key() keeps the return-None-on-malformed contract the other parse paths follow instead of raising. I used a membership check rather than a try/except so the failure path does not emit a traceback on every poll (which would spam the HA coordinator). Added a test for that path.

  2. Export. QuickKeyFunction is now re-exported from switchbot/__init__.py and the const package alongside LockStatus, so callers of set_quick_key(function=...) can import it from the package root.

Full test suite passes (1222) and ruff is clean.

mayerwin added a commit to mayerwin/ha-switchbot-lock-quick-key-support that referenced this pull request Jun 22, 2026
The 2-bit function field has 4 possible values but only 3 are defined.
A firmware variant or a corrupted BLE byte reporting the undefined 4th
(0b11) now logs and surfaces the function as unknown, keeping the
well-defined enable and trigger bits, instead of passing a stray value
to the select. Mirrors the same fix in the upstream pySwitchbot PR
(sblibs/pySwitchbot#515). Bumps version to 0.3.1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

3 participants