Skip to content

Add SwitchbotKeypad device class for classic Keypad#488

Open
italo-lombardi wants to merge 14 commits into
sblibs:mainfrom
italo-lombardi:feature/switchbot-keypad
Open

Add SwitchbotKeypad device class for classic Keypad#488
italo-lombardi wants to merge 14 commits into
sblibs:mainfrom
italo-lombardi:feature/switchbot-keypad

Conversation

@italo-lombardi

@italo-lombardi italo-lombardi commented May 6, 2026

Copy link
Copy Markdown

Adds support for SwitchBot Keypad (WoKeypad) — exposes battery level and attempt_state from BLE advertisements.
https://eu.switch-bot.com/products/switchbot-keypad

Tested on a real device — scanner confirmed battery: 100 and attempt_state parsed correctly from live BLE advertisement data.

Summary

The classic SwitchBot Keypad (WoKeypad) is a passive BLE-only device used to unlock doors paired with SwitchBot Lock. The advertisement parser (adv_parsers/keypad.py) already parses battery and attempt_state from BLE advertisements, and SwitchbotModel.KEYPAD is already defined, but no device class existed.

Changes

  • Add SwitchbotKeypad device class in devices/keypad.py, extending SwitchbotDevice (passive-only, no BLE commands)
  • Export SwitchbotKeypad from switchbot/__init__.py
  • Add KEYPAD_INFO test fixture in tests/__init__.py
  • Add tests/test_keypad.py verifying battery and attempt_state are correctly parsed from advertisement data

Test results

All 3 unit tests pass. Verified against real WoKeypad hardware — advertisement data parsed correctly.

italo-lombardi and others added 3 commits May 6, 2026 11:28
The classic Keypad (WoKeypad) is a passive BLE-only device paired
with SwitchBot Lock. The adv parser already exposes battery and
attempt_state from advertisement data. This adds the device class
and exports it so integrations can reference it explicitly.
@codecov

codecov Bot commented May 6, 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/devices/keypad.py 100.00% <100.00%> (+100.00%) ⬆️

... and 1 file with indirect coverage changes

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

@bdraco bdraco requested a review from Copilot May 14, 2026 20:00
@bdraco

bdraco commented May 14, 2026

Copy link
Copy Markdown
Member

@bluetoothbot review

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 a minimal SwitchbotKeypad device class that wraps the existing process_wokeypad advertisement parser, exposes it from the package's public API, and adds a fixture plus three tests verifying battery and attempt_state parsing.

Changes:

  • New switchbot/devices/keypad.py defining SwitchbotKeypad (subclass of SwitchbotDevice, no commands).
  • Public export added in switchbot/__init__.py (SwitchbotKeypad).
  • New KEYPAD_INFO AdvTestCase fixture in tests/__init__.py and a new tests/test_keypad.py exercising advertisement parsing.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
switchbot/devices/keypad.py Adds the new passive SwitchbotKeypad device class.
switchbot/init.py Imports and re-exports SwitchbotKeypad in the public API.
tests/init.py Adds KEYPAD_INFO advertisement fixture used by the new tests.
tests/test_keypad.py Adds tests verifying parsed battery and attempt_state values, including battery=None override.

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

@bluetoothbot

bluetoothbot commented May 14, 2026

Copy link
Copy Markdown
Collaborator

Previous review — superseded by a newer review below.

Tests previously injected pre-parsed values, bypassing the parser.
Now exercises parse_advertisement_data end-to-end so byte-layout
regressions in adv_parsers/keypad.py are caught.

- Assert get_battery_percent() instead of raw dict access
- None test exercises parser's own None branch (empty mfr_data)
- Extract make_advertisement_data to tests/__init__.py; remove
  duplicate in test_keypad_vision.py
- Expand SwitchbotKeypad docstring: passive-only, no commands
@italo-lombardi

Copy link
Copy Markdown
Author

Addressed all suggestions from the automated review:

  1. Tests now exercise the full pipeline — rewrote all three tests to call parse_advertisement_data with real BLE bytes instead of injecting pre-parsed values. Added assertion on get_battery_percent().

  2. Fixed misleading testtest_keypad_advertisement_battery_none_when_no_data now passes empty manufacturer_data so the None comes from the parser's own None-handling branch (adv_parsers/keypad.py:15-16), not a manual override.

  3. Removed copy-paste — moved make_advertisement_data to tests/__init__.py as a shared helper. test_keypad_vision.py now imports it from there.

  4. Added docstring to SwitchbotKeypad — clarifies passive-only BLE, no commands, and points to adv_parsers/keypad.py for data origin.

  5. Fixture bytesKEYPAD_INFO in tests/__init__.py is already the single source of truth; the new tests reference it directly, so test_adv_parser.py could be updated to do the same in a follow-up if desired.

@italo-lombardi

Copy link
Copy Markdown
Author

@bluetoothbot review

@bluetoothbot

Copy link
Copy Markdown
Collaborator

❌ Permission denied. Only users with write access can trigger bot commands.

File was not in scope for this PR. Revert changes to it and
remove the shared make_advertisement_data helper from tests/__init__.py
that was added only to support that refactor.
@bdraco

bdraco commented May 14, 2026

Copy link
Copy Markdown
Member

@bluetoothbot review

italo-lombardi and others added 2 commits May 15, 2026 10:10
Completes coverage of the None-handling branch in process_wokeypad:
both battery and attempt_state are None when manufacturer data absent.
@andreasbrett

Copy link
Copy Markdown

What's the status here? Would love to have this in the upcoming HA release. Seems like the bot did not fire again.

@bdraco bdraco left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The code as-is doesn't do anything. There is nothing to review

@italo-lombardi

Copy link
Copy Markdown
Author

This PR was AI-assisted. The goal is to expose battery % for the classic WoKeypad in Home Assistant.

The pySwitchbot side should be complete: SwitchbotKeypad inherits get_battery_percent() from SwitchbotDevice, which reads the "battery" key that process_wokeypad already parses from the advertisement data. No BLE commands needed — this is a passive device.

A companion HA PR is needed to wire it up (NON_CONNECTABLE_SUPPORTED_MODEL_TYPES + SupportedModels.KEYPAD + CLASS_BY_DEVICE), similar to how LEAK and CONTACT_SENSOR are handled. That PR will come once this merges.

I have the physical device but no experience testing pySwitchbot integrations. Any guidance on how to validate this locally (e.g. with a BLE sniffer or test harness against real hardware) would be appreciated.

@italo-lombardi italo-lombardi requested a review from bdraco May 20, 2026 21:04
italo-lombardi and others added 4 commits May 23, 2026 00:00
Exposes attempt_state via _get_adv_value, consistent with how
get_battery_percent is implemented in the base class.
Replace raw parsed_data dict access with device.attempt_state property
so the new property is exercised by the test suite.
@italo-lombardi

Copy link
Copy Markdown
Author

Update: tested on real hardware.

Ran the parser against a live WoKeypad using BLE scanning on macOS.

Results from advertisement data:

  • battery: 100%
  • attempt_state: 41 (value at time of scan, changes per keypad interaction)

Parser correctly extracts both values from service_data[2] & 0x7f and manufacturer_data[6]. Length guards (>= 3 svc, >= 7 mfr) satisfied by real device advertisements.

All 3 unit tests pass locally.

@CodeFinder2

Copy link
Copy Markdown

Really cool, would love to see this merged! :-)

deece added a commit to deece/pySwitchbot that referenced this pull request Jun 13, 2026
… from PR sblibs#488

Co-authored-by: Alastair D'Silva <alastair@d-silva.org>

Co-authored-by: Antigravity <antigravity@google.com>
deece added a commit to deece/pySwitchbot that referenced this pull request Jun 13, 2026
… Keypad

This Pull Request adds complete local and cloud passcode management and clock
synchronization capabilities for the non-vision SwitchBot Keypad (WoKeypad).

It also integrates the passive `attempt_state` advertisement property and tests proposed in closed PR sblibs#488 (by @italo-lombardi).

Why Cloud Sync is Needed:
- Keypad passcodes are stored and validated locally (allowing the Keypad to directly command the Lock offline without requiring internet connectivity).
- However, for security reasons, the Keypad's BLE protocol is write-only for passcodes. There is no command to read back or list saved codes over BLE.
- Since the SwitchBot app cannot query the Keypad to build its UI passcode list, it relies entirely on the SwitchBot Cloud database to track which codes exist.
- If a passcode is added locally over BLE but not synced to the cloud, it will successfully operate the Lock offline, but will be completely invisible in the official mobile app. Integrating the cloud sync option ensures the app UI stays in sync with the keypad's physical storage.

What Was Implemented:
1. Passcode Management:
   - `add_password`: Sends passcode bytes over BLE to register a new PIN code on the keypad. Parses response to retrieve the device-assigned index.
   - `modify_password`: Modifies an existing passcode by index. Overwrites values and configures active duration ranges.
   - `delete_password`: Instantly deletes a passcode from device memory by index.
   - `get_password_count`: Queries counts of stored PINs, NFC tags, fingerprints, and duress credentials.
2. Clock Synchronization (RTC):
   - `sync_time`: Synchronizes the internal device clock with millisecond precision (sending 8-byte big-endian milliseconds timestamp). Automatically scales second-level timestamps for backwards compatibility.
   - Note: Clock querying (`get_time`) was tested but found to return error 05 (unsupported) by the keypad hardware, indicating it is a write-only clock, so it has been dropped from the implementation.
3. SwitchBot Cloud Sync Option:
   - Added keyword-only parameters to `add_password`: `session`, `token`, `region`, `name`, and `creator`.
   - If provided, automatically triggers an HTTP POST request to API function 4245 to register the passcode in the SwitchBot Cloud database so it appears correctly in the official smartphone app.
4. Passive Advertisement Property Integration (from sblibs#488):
   - Exposes `attempt_state` property via `_get_adv_value` in `SwitchbotKeypad`.
   - Adds the missing `KEYPAD_INFO` test fixture to `tests/__init__.py`.

Testing Methodology:
1. Automated Tests (tests/test_keypad.py):
   - Created full coverage suite for add/modify/delete password, counts, and sync_time.
   - Implemented `test_add_password_with_cloud_sync` mocking `api_request` to verify payload structures, regional routing, and authorization headers.
   - Added advertisement parsing unit tests for battery percentage and attempt state.
   - Verified that all 14 unit tests pass cleanly.
2. Manual/Hardware Verification:
   - Tested using test_hardware.py on a physical WoKeypad and Lock.
   - Verified that syncing clock with millisecond timestamps sets the correct RTC, allowing time-limited (temporary) passcodes to evaluate successfully offline and unlock the lock.

Co-authored-by: Alastair D'Silva <alastair@d-silva.org>
Co-authored-by: Italo Lombardi <italo.lombardi@sap.com>
Co-authored-by: Antigravity <antigravity@google.com>
deece added a commit to deece/pySwitchbot that referenced this pull request Jun 13, 2026
… Keypad

This Pull Request adds complete local and cloud passcode management and clock
synchronization capabilities for the non-vision SwitchBot Keypad (WoKeypad).

It also integrates the passive `attempt_state` advertisement property and tests proposed in closed PR sblibs#488 (by @italo-lombardi).

Why Cloud Sync is Needed:
- Keypad passcodes are stored and validated locally (allowing the Keypad to directly command the Lock offline without requiring internet connectivity).
- However, for security reasons, the Keypad's BLE protocol is write-only for passcodes. There is no command to read back or list saved codes over BLE.
- Since the SwitchBot app cannot query the Keypad to build its UI passcode list, it relies entirely on the SwitchBot Cloud database to track which codes exist.
- If a passcode is added locally over BLE but not synced to the cloud, it will successfully operate the Lock offline, but will be completely invisible in the official mobile app. Integrating the cloud sync option ensures the app UI stays in sync with the keypad's physical storage.

What Was Implemented:
1. Passcode Management:
   - `add_password`: Sends passcode bytes over BLE to register a new PIN code on the keypad. Parses response to retrieve the device-assigned index.
   - `modify_password`: Modifies an existing passcode by index. Overwrites values and configures active duration ranges.
   - `delete_password`: Instantly deletes a passcode from device memory by index.
   - `get_password_count`: Queries counts of stored PINs, NFC tags, fingerprints, and duress credentials.
2. Clock Synchronization (RTC):
   - `sync_time`: Synchronizes the internal device clock with millisecond precision (sending 8-byte big-endian milliseconds timestamp). Automatically scales second-level timestamps for backwards compatibility.
   - Note: Clock querying (`get_time`) was tested but found to return error 05 (unsupported) by the keypad hardware, indicating it is a write-only clock, so it has been dropped from the implementation.
3. SwitchBot Cloud Sync Option:
   - Added keyword-only parameters to `add_password`: `session`, `token`, `region`, `name`, and `creator`.
   - If provided, automatically triggers an HTTP POST request to API function 4245 to register the passcode in the SwitchBot Cloud database so it appears correctly in the official smartphone app.
4. Passive Advertisement Property Integration (from sblibs#488):
   - Exposes `attempt_state` property via `_get_adv_value` in `SwitchbotKeypad`.
   - Adds the missing `KEYPAD_INFO` test fixture to `tests/__init__.py`.

Testing Methodology:
1. Automated Tests (tests/test_keypad.py):
   - Created full coverage suite for add/modify/delete password, counts, and sync_time.
   - Implemented `test_add_password_with_cloud_sync` mocking `api_request` to verify payload structures, regional routing, and authorization headers.
   - Added advertisement parsing unit tests for battery percentage and attempt state.
   - Verified that all 14 unit tests pass cleanly.
2. Manual/Hardware Verification:
   - Tested using test_hardware.py on a physical WoKeypad and Lock.
   - Verified that syncing clock with millisecond timestamps sets the correct RTC, allowing time-limited (temporary) passcodes to evaluate successfully offline and unlock the lock.

Co-authored-by: Alastair D'Silva <alastair@d-silva.org>
Co-authored-by: Italo Lombardi <italo.lombardi@sap.com>
Co-authored-by: Antigravity <antigravity@google.com>
deece added a commit to deece/pySwitchbot that referenced this pull request Jun 13, 2026
… Keypad

This Pull Request adds complete local and cloud passcode management and clock
synchronization capabilities for the non-vision SwitchBot Keypad (WoKeypad).

It also integrates the passive `attempt_state` advertisement property and tests proposed in closed PR sblibs#488 (by @italo-lombardi).

Why Cloud Sync is Needed:
- Keypad passcodes are stored and validated locally (allowing the Keypad to directly command the Lock offline without requiring internet connectivity).
- However, for security reasons, the Keypad's BLE protocol is write-only for passcodes. There is no command to read back or list saved codes over BLE.
- Since the SwitchBot app cannot query the Keypad to build its UI passcode list, it relies entirely on the SwitchBot Cloud database to track which codes exist.
- If a passcode is added locally over BLE but not synced to the cloud, it will successfully operate the Lock offline, but will be completely invisible in the official mobile app. Integrating the cloud sync option ensures the app UI stays in sync with the keypad's physical storage.

What Was Implemented:
1. Passcode Management:
   - `add_password`: Sends passcode bytes over BLE to register a new PIN code on the keypad. Parses response to retrieve the device-assigned index.
   - `modify_password`: Modifies an existing passcode by index. Overwrites values and configures active duration ranges.
   - `delete_password`: Instantly deletes a passcode from device memory by index.
   - `get_password_count`: Queries counts of stored PINs, NFC tags, fingerprints, and duress credentials.
2. Clock Synchronization (RTC):
   - `sync_time`: Synchronizes the internal device clock with millisecond precision (sending 8-byte big-endian milliseconds timestamp). Automatically scales second-level timestamps for backwards compatibility.
   - Note: Clock querying (`get_time`) was tested but found to return error 05 (unsupported) by the keypad hardware, indicating it is a write-only clock, so it has been dropped from the implementation.
3. SwitchBot Cloud Sync Option:
   - Added keyword-only parameters to `add_password`: `session`, `token`, `region`, `name`, and `creator`.
   - If provided, automatically triggers an HTTP POST request to API function 4245 to register the passcode in the SwitchBot Cloud database so it appears correctly in the official smartphone app.
4. Passive Advertisement Property Integration (from sblibs#488):
   - Exposes `attempt_state` property via `_get_adv_value` in `SwitchbotKeypad`.
   - Adds the missing `KEYPAD_INFO` test fixture to `tests/__init__.py`.

Testing Methodology:
1. Automated Tests (tests/test_keypad.py):
   - Created full coverage suite for add/modify/delete password, counts, and sync_time.
   - Implemented `test_add_password_with_cloud_sync` mocking `api_request` to verify payload structures, regional routing, and authorization headers.
   - Added advertisement parsing unit tests for battery percentage and attempt state.
   - Verified that all 14 unit tests pass cleanly.
2. Manual/Hardware Verification:
   - Tested using test_hardware.py on a physical WoKeypad and Lock.
   - Verified that syncing clock with millisecond timestamps sets the correct RTC, allowing time-limited (temporary) passcodes to evaluate successfully offline and unlock the lock.

Co-authored-by: Alastair D'Silva <alastair@d-silva.org>
Co-authored-by: Antigravity <antigravity@google.com>
deece added a commit to deece/pySwitchbot that referenced this pull request Jun 13, 2026
… Keypad

This patch adds complete local and cloud passcode management and clock
synchronization capabilities for the non-vision SwitchBot Keypad (WoKeypad).

What Was Implemented:
1. Passcode Management:
   - `add_password`: Sends passcode bytes over BLE to register a new PIN code on the keypad. Parses response to retrieve the device-assigned index.
   - `modify_password`: Modifies an existing passcode by index. Overwrites values and configures active duration ranges.
   - `delete_password`: Instantly deletes a passcode from device memory by index.
   - `get_password_count`: Queries counts of stored PINs, NFC tags, fingerprints, and duress credentials.
2. Clock Synchronization (RTC):
   - `sync_time`: Synchronizes the internal device clock with millisecond precision (sending 8-byte big-endian milliseconds timestamp). Automatically scales second-level timestamps for backwards compatibility.
   - Note: Clock querying (`get_time`) was tested but found to return error 05 (unsupported) by the keypad hardware, indicating it is a write-only clock, so it has been dropped from the implementation.
3. SwitchBot Cloud Sync Option:
   - Added keyword-only parameters to `add_password`: `session`, `token`, `region`, `name`, and `creator`.
   - If provided, automatically triggers an HTTP POST request to API function 4245 to register the passcode in the SwitchBot Cloud database so it appears correctly in the official smartphone app.
4. Passive Advertisement Property Integration (from sblibs#488):
   - Exposes `attempt_state` property via `_get_adv_value` in `SwitchbotKeypad`.
   - Adds the missing `KEYPAD_INFO` test fixture to `tests/__init__.py`.

Testing Methodology:
1. Automated Tests (tests/test_keypad.py):
   - Created full coverage suite for add/modify/delete password, counts, and sync_time.
   - Implemented `test_add_password_with_cloud_sync` mocking `api_request` to verify payload structures, regional routing, and authorization headers.
   - Added advertisement parsing unit tests for battery percentage and attempt state.
   - Verified that all 14 unit tests pass cleanly.
2. Manual/Hardware Verification:
   - Tested using test_hardware.py on a physical WoKeypad and Lock.
   - Verified that syncing clock with millisecond timestamps sets the correct RTC, allowing time-limited (temporary) passcodes to evaluate successfully offline and unlock the lock.

Why Cloud Sync is Needed:
- Keypad passcodes are stored and validated locally (allowing the Keypad to directly command the Lock offline without requiring internet connectivity).
- However, for security reasons, the Keypad's BLE protocol is write-only for passcodes. There is no command to read back or list saved codes over BLE.
- Since the SwitchBot app cannot query the Keypad to build its UI passcode list, it relies entirely on the SwitchBot Cloud database to track which codes exist.
- If a passcode is added locally over BLE but not synced to the cloud, it will successfully operate the Lock offline, but will be completely invisible in the official mobile app. Integrating the cloud sync option ensures the app UI stays in sync with the keypad's physical storage.

Co-authored-by: Alastair D'Silva <alastair@d-silva.org>
Co-authored-by: Antigravity <antigravity@google.com>
@bdraco bdraco requested a review from Copilot June 20, 2026 14:08

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

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

Comment on lines +8 to +14
class SwitchbotKeypad(SwitchbotDevice):
"""
Representation of a Switchbot Keypad (WoKeypad) device.

Passive BLE-only — no commands. Battery and attempt_state come from
advertisement parsing in adv_parsers/keypad.py.
"""
Comment thread tests/test_keypad.py
Comment on lines +18 to +22
advertisement = parse_advertisement_data(
ble_device, adv_data, SwitchbotModel.KEYPAD
)
device = SwitchbotKeypad(ble_device)
device.update_from_advertisement(advertisement)
Comment thread tests/test_keypad.py
Comment on lines +35 to +39
advertisement = parse_advertisement_data(
ble_device, adv_data, SwitchbotModel.KEYPAD
)
device = SwitchbotKeypad(ble_device)
device.update_from_advertisement(advertisement)
Comment thread tests/test_keypad.py
Comment on lines +52 to +56
advertisement = parse_advertisement_data(
ble_device, adv_data, SwitchbotModel.KEYPAD
)
device = SwitchbotKeypad(ble_device)
device.update_from_advertisement(advertisement)
@bluetoothbot

Copy link
Copy Markdown
Collaborator

@italo-lombardi: This PR was AI-assisted. The goal is to expose battery % for the classic WoKeypad in Home Assistant.

Checked the codebase. Contact/leak/motion have no device class. devices/contact.py, motion.py, meter.py are empty stubs. None exported in __init__.py. Passive sensors expose battery via advertisement dispatch only — process_wokeypad already fills battery/attempt_state for SwitchbotModel.KEYPAD. So no pySwitchbot class precedent exists here. Confirm HA truly needs a class for a non-connectable model before iterating — precedent says it reads battery straight from parsed adverts. That gap explains bdraco "nothing to review."

@bluetoothbot

Copy link
Copy Markdown
Collaborator

@andreasbrett: What's the status here? Would love to have this in the upcoming HA release. Seems like the bot did n...

Status: blocked on maintainer CHANGES_REQUESTED. Core objection structural. Passive Keypad battery already comes from advertisement parsing — no device class required, matching contact/leak/motion (all classless stubs here). HA wiring may not need this class at all. Needs the author to confirm the actual HA code path before merge.

@bluetoothbot

Copy link
Copy Markdown
Collaborator

PR Review — Add SwitchbotKeypad device class for classic Keypad

Re-attempt of the previously-rejected passive Keypad class; the maintainer's structural objection still applies.

What's solid:

  • Tests drive the real parser end-to-end via parse_advertisement_data with live-derived bytes, not injected values, and the fixture math checks out (service_data[2]&0x7f=100, mfr_data[6]=143 against the dispatch in adv_parser.py:356-366).
  • The None-when-missing test exercises the parser's actual guard branch rather than an override — a real improvement over the prior round.
  • Export is alphabetized; code is clean and Python 3.11-compatible.

What needs attention:

  • The device class adds no behavior beyond what the advertisement dispatch already provides. Verified: contact/motion/meter modules are empty stubs and no passive-sensor device class exists or is exported — so the PR's stated precedent (LEAK/CONTACT_SENSOR "handled" via classes) is inaccurate. This is the basis of bdraco's CHANGES_REQUESTED.
  • Class claims passive-only but inherits update()/get_basic_info(), which connect and send 5702 — a latent contradiction.
  • attempt_state is an undocumented raw byte.

Recommendation: confirm whether HA actually needs a pySwitchbot class for a non-connectable model (the existing passive sensors say no) and put that justification in the PR body — otherwise this remains "nothing to review."


🟡 Important

1. Device class duplicates what advertisement dispatch already provides
switchbot/devices/keypad.py:8-15

This is the crux of bdraco's CHANGES_REQUESTED ("the code as-is doesn't do anything") — and it holds up against the codebase.

Passive SwitchBot sensors in this library carry no device class. I verified that switchbot/devices/contact.py, motion.py, and meter.py are all empty stubs (a single from __future__ import annotations line), and there is no SwitchbotContactSensor, SwitchbotLeak, or SwitchbotMotionSensor defined or exported anywhere. For those models, process_* populates battery/state into the advertisement data via the adv_parser.py dispatch, and consumers read it directly — no class needed.

The PR description (and comment [id=4502620942]) states LEAK and CONTACT_SENSOR are "handled" with device classes similar to this one. That is not accurate for the pySwitchbot side — they aren't. process_wokeypad already fills battery and attempt_state for SwitchbotModel.KEYPAD (adv_parser.py:356-366), so a SwitchbotKeypad subclass that only re-surfaces those values adds no behavior the dispatch doesn't already deliver.

Why it matters: this is the second iteration of essentially the same change, and the maintainer's objection is structural, not cosmetic. Before iterating further, confirm whether Home Assistant actually requires a pySwitchbot device class for a non-connectable model — the contact/leak/motion precedent strongly suggests it does not, and HA can read battery straight from the parsed advertisement. If HA genuinely needs a class hook, that justification (with the HA code path it plugs into) belongs in the PR body; if not, this can be dropped.

class SwitchbotKeypad(SwitchbotDevice):
2. Inherited update()/get_basic_info() contradict the passive-only contract
switchbot/devices/keypad.py:8

The docstring states "Passive BLE-only — no commands," but the class inherits SwitchbotDevice.update() and get_basic_info() unchanged.

I confirmed in device.py that update() (line 819) calls get_basic_info() (line 826) → _get_basic_info() (line 791) → _send_command("5702"), which opens a BLE connection and sends the basic-settings command. For a non-connectable keypad that path cannot succeed.

Why it matters: in normal HA usage of non-connectable models update() is not called, so this is latent rather than active — but it's a foot-gun. Any caller that does invoke update() gets a connection attempt that the device can't service, which is the opposite of "passive, no commands."

If the class stays, consider overriding update() to refresh from a scan (get_device_data) or to no-op, so the inherited connect-and-send behavior can't be reached.

async def update(self, interface=None):  # inherited → _send_command("5702")

🟢 Suggestions

1. attempt_state exposed as an undocumented raw int
switchbot/devices/keypad.py:17-19

attempt_state returns the raw mfr_data[6] byte (143 in the fixture, 41 on the author's live device) with no documented meaning or decoding. As a bare integer with shifting values per interaction, it's of limited use to a consumer.

If this value carries a defined semantic (e.g. last-attempt result code, retry count), decode it into something meaningful or document the encoding; otherwise the genuinely useful signal here is battery, which is already inherited via get_battery_percent().

@property
def attempt_state(self) -> int | None:
    return self._get_adv_value("attempt_state")

Checklist

  • Change delivers what the PR description promises — warning #1
  • Consistent with codebase patterns for passive sensors — warning #1
  • Class contract (passive-only) matches behavior — warning #2
  • Tests cover new code and edge cases
  • No hardcoded secrets / injection / unsafe ops
  • Public export wired correctly

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=6bcc3a6 4 min 45s

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

  • Device class duplicates what advertisement dispatch already provides
  • Inherited update()/get_basic_info() contradict the passive-only contract

deece added a commit to deece/pySwitchbot that referenced this pull request Jun 21, 2026
… Keypad

This patch adds complete local and cloud passcode management and clock
synchronization capabilities for the non-vision SwitchBot Keypad (WoKeypad).

What Was Implemented:
1. Passcode Management:
   - `add_password`: Sends passcode bytes over BLE to register a new PIN code on the keypad. Parses response to retrieve the device-assigned index.
   - `modify_password`: Modifies an existing passcode by index. Overwrites values and configures active duration ranges.
   - `delete_password`: Instantly deletes a passcode from device memory by index.
   - `get_password_count`: Queries counts of stored PINs, NFC tags, fingerprints, and duress credentials.
2. Clock Synchronization (RTC):
   - `sync_time`: Synchronizes the internal device clock with millisecond precision (sending 8-byte big-endian milliseconds timestamp). Automatically scales second-level timestamps for backwards compatibility.
   - Note: Clock querying (`get_time`) was tested but found to return error 05 (unsupported) by the keypad hardware, indicating it is a write-only clock, so it has been dropped from the implementation.
3. SwitchBot Cloud Sync Option:
   - Added keyword-only parameters to `add_password`: `session`, `token`, `region`, `name`, and `creator`.
   - If provided, automatically triggers an HTTP POST request to API function 4245 to register the passcode in the SwitchBot Cloud database so it appears correctly in the official smartphone app.
4. Passive Advertisement Property Integration (from sblibs#488):
   - Exposes `attempt_state` property via `_get_adv_value` in `SwitchbotKeypad`.
   - Adds the missing `KEYPAD_INFO` test fixture to `tests/__init__.py`.

Testing Methodology:
1. Automated Tests (tests/test_keypad.py):
   - Created full coverage suite for add/modify/delete password, counts, and sync_time.
   - Implemented `test_add_password_with_cloud_sync` mocking `api_request` to verify payload structures, regional routing, and authorization headers.
   - Added advertisement parsing unit tests for battery percentage and attempt state.
   - Verified that all unit tests pass cleanly.
2. Manual/Hardware Verification:
   - Tested using test_hardware.py on a physical WoKeypad and Lock.
   - Verified that syncing clock with millisecond timestamps sets the correct RTC, allowing time-limited (temporary) passcodes to evaluate successfully offline and unlock the lock.

Why Cloud Sync is Needed:
- Keypad passcodes are stored and validated locally (allowing the Keypad to directly command the Lock offline without requiring internet connectivity).
- However, for security reasons, the Keypad's BLE protocol is write-only for passcodes. There is no command to read back or list saved codes over BLE.
- Since the SwitchBot app cannot query the Keypad to build its UI passcode list, it relies entirely on the SwitchBot Cloud database to track which codes exist.
- If a passcode is added locally over BLE but not synced to the cloud, it will successfully operate the Lock offline, but will be completely invisible in the official mobile app. Integrating the cloud sync option ensures the app UI stays in sync with the keypad's physical storage.

Co-authored-by: Alastair D'Silva <alastair@d-silva.org>
Co-authored-by: Antigravity <antigravity@google.com>
deece added a commit to deece/pySwitchbot that referenced this pull request Jun 21, 2026
… Keypad

This patch adds complete local and cloud passcode management and clock
synchronization capabilities for the non-vision SwitchBot Keypad (WoKeypad).

What Was Implemented:
1. Passcode Management:
   - `add_password`: Sends passcode bytes over BLE to register a new PIN code on the keypad. Parses response to retrieve the device-assigned index.
   - `modify_password`: Modifies an existing passcode by index. Overwrites values and configures active duration ranges.
   - `delete_password`: Instantly deletes a passcode from device memory by index.
   - `get_password_count`: Queries counts of stored PINs, NFC tags, fingerprints, and duress credentials.
2. Clock Synchronization (RTC):
   - `sync_time`: Synchronizes the internal device clock with millisecond precision (sending 8-byte big-endian milliseconds timestamp). Automatically scales second-level timestamps for backwards compatibility.
   - Note: Clock querying (`get_time`) was tested but found to return error 05 (unsupported) by the keypad hardware, indicating it is a write-only clock, so it has been dropped from the implementation.
3. SwitchBot Cloud Sync Option:
   - Added keyword-only parameters to `add_password`: `session`, `token`, `region`, `name`, and `creator`.
   - If provided, automatically triggers an HTTP POST request to API function 4245 to register the passcode in the SwitchBot Cloud database so it appears correctly in the official smartphone app.
4. Passive Advertisement Property Integration (from sblibs#488):
   - Exposes `attempt_state` property via `_get_adv_value` in `SwitchbotKeypad`.
   - Adds the missing `KEYPAD_INFO` test fixture to `tests/__init__.py`.

Testing Methodology:
1. Automated Tests (tests/test_keypad.py):
   - Created full coverage suite for add/modify/delete password, counts, and sync_time.
   - Implemented `test_add_password_with_cloud_sync` mocking `api_request` to verify payload structures, regional routing, and authorization headers.
   - Added advertisement parsing unit tests for battery percentage and attempt state.
   - Verified that all unit tests pass cleanly and achieve 100% test coverage.
2. Manual/Hardware Verification:
   - Tested using test_hardware.py on a physical WoKeypad and Lock.
   - Verified that syncing clock with millisecond timestamps sets the correct RTC, allowing time-limited (temporary) passcodes to evaluate successfully offline and unlock the lock.

Why Cloud Sync is Needed:
- Keypad passcodes are stored and validated locally (allowing the Keypad to directly command the Lock offline without requiring internet connectivity).
- However, for security reasons, the Keypad's BLE protocol is write-only for passcodes. There is no command to read back or list saved codes over BLE.
- Since the SwitchBot app cannot query the Keypad to build its UI passcode list, it relies entirely on the SwitchBot Cloud database to track which codes exist.
- If a passcode is added locally over BLE but not synced to the cloud, it will successfully operate the Lock offline, but will be completely invisible in the official mobile app. Integrating the cloud sync option ensures the app UI stays in sync with the keypad's physical storage.

Co-authored-by: Alastair D'Silva <alastair@d-silva.org>
Co-authored-by: Antigravity <antigravity@google.com>
deece added a commit to deece/pySwitchbot that referenced this pull request Jun 29, 2026
… Keypad

This patch adds complete local and cloud passcode management and clock
synchronization capabilities for the non-vision SwitchBot Keypad (WoKeypad).

What Was Implemented:
1. Passcode Management:
   - `add_password`: Sends passcode bytes over BLE to register a new PIN code on the keypad. Parses response to retrieve the device-assigned index.
   - `modify_password`: Modifies an existing passcode by index. Overwrites values and configures active duration ranges.
   - `delete_password`: Instantly deletes a passcode from device memory by index.
   - `get_password_count`: Queries counts of stored PINs, NFC tags, fingerprints, and duress credentials.
2. Clock Synchronization (RTC):
   - `sync_time`: Synchronizes the internal device clock with millisecond precision (sending 8-byte big-endian milliseconds timestamp). Automatically scales second-level timestamps for backwards compatibility.
   - Note: Clock querying (`get_time`) was tested but found to return error 05 (unsupported) by the keypad hardware, indicating it is a write-only clock, so it has been dropped from the implementation.
3. SwitchBot Cloud Sync Option:
   - Added keyword-only parameters to `add_password`: `session`, `token`, `region`, `name`, and `creator`.
   - If provided, automatically triggers an HTTP POST request to API function 4245 to register the passcode in the SwitchBot Cloud database so it appears correctly in the official smartphone app.
4. Passive Advertisement Property Integration (from sblibs#488):
   - Exposes `attempt_state` property via `_get_adv_value` in `SwitchbotKeypad`.
   - Adds the missing `KEYPAD_INFO` test fixture to `tests/__init__.py`.

Testing Methodology:
1. Automated Tests (tests/test_keypad.py):
   - Created full coverage suite for add/modify/delete password, counts, and sync_time.
   - Implemented `test_add_password_with_cloud_sync` mocking `api_request` to verify payload structures, regional routing, and authorization headers.
   - Added advertisement parsing unit tests for battery percentage and attempt state.
   - Verified that all unit tests pass cleanly and achieve 100% test coverage.
2. Manual/Hardware Verification:
   - Tested using test_hardware.py on a physical WoKeypad and Lock.
   - Verified that syncing clock with millisecond timestamps sets the correct RTC, allowing time-limited (temporary) passcodes to evaluate successfully offline and unlock the lock.

Why Cloud Sync is Needed:
- Keypad passcodes are stored and validated locally (allowing the Keypad to directly command the Lock offline without requiring internet connectivity).
- However, for security reasons, the Keypad's BLE protocol is write-only for passcodes. There is no command to read back or list saved codes over BLE.
- Since the SwitchBot app cannot query the Keypad to build its UI passcode list, it relies entirely on the SwitchBot Cloud database to track which codes exist.
- If a passcode is added locally over BLE but not synced to the cloud, it will successfully operate the Lock offline, but will be completely invisible in the official mobile app. Integrating the cloud sync option ensures the app UI stays in sync with the keypad's physical storage.

Co-authored-by: Alastair D'Silva <alastair@d-silva.org>
Co-authored-by: Antigravity <antigravity@google.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.

6 participants