Skip to content

fix: track lock _notifications_enabled state on enable#497

Draft
bluetoothbot wants to merge 4 commits into
sblibs:mainfrom
bluetoothbot:koan/lock-notifications-state-tracking
Draft

fix: track lock _notifications_enabled state on enable#497
bluetoothbot wants to merge 4 commits into
sblibs:mainfrom
bluetoothbot:koan/lock-notifications-state-tracking

Conversation

@bluetoothbot

@bluetoothbot bluetoothbot commented May 15, 2026

Copy link
Copy Markdown
Collaborator

What

Set _notifications_enabled = True on a successful _enable_notifications call (and skip the BLE write when already enabled). Tighten the existing test and add a failure-path test.

Why

SwitchbotLock._enable_notifications only returned the command result — it never updated self._notifications_enabled. The flag therefore stayed False for the lifetime of the device. That has two consequences:

  • _disable_notifications early-returns on if not self._notifications_enabled: return True, so the disable command would never actually be sent to the lock.
  • Every lock() / unlock() / unlock_without_unlatch() / half_lock() re-issues the enable command rather than recognising notifications are already on.

Surfaced while looking at #489 (lock event subscription). The notification-tracking gap is unrelated to that question's root cause but is a real bug in its own right.

How

  • _enable_notifications: short-circuit when the flag is already True; on a successful command, set the flag; on failure, return False and leave the flag False.
  • Test changes:
    • test_enable_notifications now asserts the flag flips to True and that a second call is a no-op (call count stays at 1).
    • New test_enable_notifications_failure_keeps_state_false covers the failure path.

Testing

  • .venv/bin/python -m pytest -q → 1085 passed.

🤖 Generated with Claude Code


Quality Report

Changes: 2 files changed, 38 insertions(+), 2 deletions(-)

Code scan: clean

Tests: failed (FAILED)

Branch hygiene: clean

Generated by Kōan post-mission quality pipeline

@codecov

codecov Bot commented May 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

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

@bdraco

bdraco commented May 15, 2026

Copy link
Copy Markdown
Member

Looks like a real bug. I'm traveling so can't test on a real device.

@bdraco bdraco requested a review from zerzhang May 15, 2026 17:11
@zerzhang

Copy link
Copy Markdown
Collaborator

I am currently away on a business trip; once I return tomorrow, I will perform the testing on the device.

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

The purpose of the _enable_notifications function is to enable the device to periodically report messages. For instance, when the device is being unlocked, its status transitions sequentially through "unlocking" and "unlocked"; conversely, when the BLE connection is lost, this status is reset.

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

This PR fixes notification state tracking for SwitchbotLock by updating _notifications_enabled after a successful enable, and avoiding redundant enable writes once notifications are already enabled. It also strengthens and expands tests to validate both success and failure paths for notification enabling.

Changes:

  • Short-circuit _enable_notifications() when notifications are already enabled.
  • Set self._notifications_enabled = True only when the enable command succeeds, and return False on an unsuccessful result.
  • Update the existing enable-notifications test to assert state changes and no-op behavior, and add a failure-path test to ensure state remains False.

Reviewed changes

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

File Description
switchbot/devices/lock.py Tracks _notifications_enabled on successful enable and skips re-sending the enable command when already enabled.
tests/test_lock.py Tightens _enable_notifications test expectations and adds a failure-path test to confirm state is not flipped on unsuccessful enable.

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

@bluetoothbot

Copy link
Copy Markdown
Collaborator Author

@zerzhang: I am currently away on a business trip; once I return tomorrow, I will perform the testing on the de...

Test re-enable after disconnect specifically. Flag never reset on connection loss. Verified: _execute_disconnect_with_lock leaves _notifications_enabled True. Short-circuit then skips the enable write on the next lock/unlock. Per your note, device resets reporting on disconnect. So transitional statuses may stop firing after the 8.5s disconnect timer. Confirm unlocking→unlocked still reports on a second operation.

@bluetoothbot

Copy link
Copy Markdown
Collaborator Author

@bdraco: Looks like a real bug. I'm traveling so can't test on a real device.

Real bug confirmed: flag never set True. But two parts. Setter fix is correct. Short-circuit is risky. Flag not reset on disconnect, so re-enable is skipped after the disconnect timer. Recommend dropping the short-circuit, keeping only the setter. Also _disable_notifications has no production callers — that motivation is theoretical.

@bluetoothbot

Copy link
Copy Markdown
Collaborator Author

PR Review — fix: track lock _notifications_enabled state on enable

The _notifications_enabled = True setter fix is correct, but the accompanying short-circuit introduces a re-enable regression because the flag is never reset on disconnect.

Strengths:

  • Correctly identifies that _enable_notifications never set the tracking flag — a real gap.
  • Sets the flag only on a successful command result, and the new failure-path test (test_enable_notifications_failure_keeps_state_false) covers the previously-untested branch.
  • 100% line coverage on the changed code; tests actually pass locally (197 passed in tests/test_lock.py — the quality report's "Tests: failed" is a false negative, make test isn't wired in this workspace).

Needs attention:

  • The short-circuit if self._notifications_enabled: return True is unsafe: nothing resets the flag on disconnect (verified — device.py _execute_disconnect_with_lock leaves it True), so after the 8.5s disconnect timer the next lock/unlock skips re-sending the enable command. Per @zerzhang, the device resets reporting on connection loss, so transitional statuses stop firing after the first cycle. Regression vs master.
  • The PR's stated motivation about _disable_notifications is largely moot — it has no production callers (only tests), so the disable-never-fires consequence is theoretical today.
  • The tightened test asserts the no-op second call, baking in the unsafe behavior; it'll need updating with the fix.

🟡 Important

1. Short-circuit skips re-enable after a disconnect — flag is never reset on connection loss
switchbot/devices/lock.py:239-240

The new if self._notifications_enabled: return True short-circuit assumes _notifications_enabled tracks whether the device currently has reporting armed. It doesn't — it only tracks whether the enable command has ever succeeded, and nothing ever sets it back to False on disconnect.

I verified the lifecycle:

  • _notifications_enabled appears only in lock.py (decl at line 87; written at 243/247). It is never referenced in device.py.
  • The disconnect path device.py:634 _execute_disconnect_with_lock() tears down self._client, _read_char, _write_char but leaves the flag at True.
  • The disconnect timer fires DISCONNECT_DELAY (8.5s) after the last activity, so a steady-state lock disconnects between operations.

Consequence: first lock()/unlock() enables notifications and sets the flag True. After the disconnect timer fires, the next lock()/unlock() reconnects (re-running GATT _start_notify() at device.py:556) but _enable_notifications() now short-circuits and never re-sends COMMAND_ENABLE_NOTIFICATIONS. Per @zerzhang's comment, the device resets its app-level status reporting when the BLE link drops — so the transitional statuses (unlocking → unlocked) stop being pushed after the first disconnect cycle. That is a regression versus current master, which re-sends the enable command on every _lock_unlock.

The self._notifications_enabled = True assignment itself is correct and worth keeping (it makes the flag honest). The unsafe part is the short-circuit.

Two ways to fix:

  • Simplest: drop the short-circuit, keep the = True assignment. Master's always-re-send behavior is preserved and the flag becomes accurate.
  • Or, if you want the optimization, reset self._notifications_enabled = False in the disconnect path so the flag actually follows the connection lifecycle (requires hooking lock into _execute_disconnect).
if self._notifications_enabled:
    return True

🟢 Suggestions

1. Test locks in the no-op-on-second-call behavior
tests/test_lock.py:419-420

test_enable_notifications now asserts mock_send.call_count == 1 after a second call — encoding the short-circuit as expected behavior. If you adopt either fix above (drop the short-circuit, or reset the flag on disconnect), this assertion has to change.

The failure-path test (test_enable_notifications_failure_keeps_state_false) is a genuine improvement and worth keeping regardless — it's the failure case the original test missed.

assert mock_send.call_count == 1

Checklist

  • Correct state tracking across connection lifecycle — warning #1
  • No untested branches introduced
  • Tests verify observable behavior, not source text
  • No backward-incompatible behavior regression — warning #1
  • PR description aligns with diff — suggestion #2

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=6b91932 3 min 33s

bluetoothbot and others added 3 commits June 20, 2026 14:51
_enable_notifications never set self._notifications_enabled = True,
so the flag stayed False forever. Two consequences:

- _disable_notifications short-circuits on `if not self._notifications_enabled`,
  meaning the disable command would never actually fire on the lock.
- Every lock/unlock invocation re-sent the enable command instead of
  recognising notifications were already on.

Set the flag on a successful response, return False on failure, and
no-op when already enabled. Existing test only checked the return
value; expand it to assert state plus a no-op second call, and add a
failure-path test that the flag stays False if the lock rejects the
command.

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

Copy link
Copy Markdown
Collaborator Author

Rebase with requested adjustments

Branch koan/lock-notifications-state-tracking was rebased onto master and review feedback was applied.

Changes applied

  • Done.
  • Changes:
  • lock.py: Dropped _enable_notifications short-circuit. Per reviewer warning press #1, the if self._notifications_enabled: return True skip broke device status reporting after a disconnect — flag never resets on connection loss, so enable command would never re-send. Kept the self._notifications_enabled = True assignment on success (makes flag honest) and return False on failure. Restores master's always-re-send behavior.
  • test_lock.py: Removed no-op second-call assertion (suggestion press #1) that baked in the unsafe short-circuit. Kept tightened success-path checks and the new failure-path test.

Stats

2 files changed, 30 insertions(+), 2 deletions(-)
Actions performed
  • Already-solved check: negative (confidence=high, reasoning=No commit on master touches SwitchbotLock._enable_notifications or the _notifications_enabled tracki)
  • Warning: pending (unsubmitted) review comments detected
  • Rebased koan/lock-notifications-state-tracking onto upstream/master
  • Applied review feedback
  • Pre-push CI check: previous run passed
  • Force-pushed koan/lock-notifications-state-tracking to origin
  • CI check enqueued in ## CI (async)

CI status

CI will be checked asynchronously.


Automated by Kōan

@bluetoothbot bluetoothbot force-pushed the koan/lock-notifications-state-tracking branch from 6b91932 to 4bfd306 Compare June 20, 2026 14:52
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.

4 participants