Skip to content

fix: stop re-fetching masked usercodes every poll cycle#590

Merged
tykeal merged 1 commit intoFutureTense:mainfrom
tykeal:fix-masked-polling
Apr 1, 2026
Merged

fix: stop re-fetching masked usercodes every poll cycle#590
tykeal merged 1 commit intoFutureTense:mainfrom
tykeal:fix-masked-polling

Conversation

@tykeal
Copy link
Copy Markdown
Collaborator

@tykeal tykeal commented Mar 26, 2026

Summary

Schlage BE469 locks return masked usercodes (e.g. **********) with in_use=True on every poll. The refresh logic detected this as needing a re-fetch, but the lock always returns the same masked value — creating an infinite polling loop that sends Z-Wave UserCodeCCGet commands every 60 seconds, draining the lock battery.

Root Cause

In _sync_usercode(), when in_use=True and the code appears masked, async_refresh_usercode() is called. But if the lock always masks its responses, the refresh returns the same masked value, and the next poll cycle triggers the same refresh again.

Fix

  • Add _is_masked_code() helper that specifically checks for known mask patterns (all *, all 0, or None) — avoids false positives on valid PINs like 1111
  • Track per-slot masked state via masked_code_slots on KeymasterLock (transient, not persisted) so a masked slot is only refreshed once, then skipped on subsequent polls
  • After refresh, if code is still masked and slot is in-use, fall back to locally stored PIN
  • If refresh reveals in_use=False, let downstream empty-slot handling run instead of forcing local PIN fallback
  • Exclude transient init=False fields from persistence in both _dict_to_kmlocks and _kmlocks_to_dict

Tests

10 new tests in TestSyncUsercodeRefreshMasked covering:

  • Refresh returns real code (existing behavior preserved)
  • Refresh still masked with/without local PIN fallback
  • Refresh returns None (code or entire object)
  • No provider available
  • All-zeros masked code handling
  • Subsequent poll skips refresh (battery drain fix)
  • Valid PIN 1111 not mistaken for masked code
  • Refresh reveals slot actually empty (in_use=False)

Closes #589

@github-actions github-actions Bot added the bugfix Fixes a bug label Mar 26, 2026
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Mar 26, 2026

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.09%. Comparing base (cdb4922) to head (a421775).
⚠️ Report is 70 commits behind head on main.
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #590      +/-   ##
==========================================
+ Coverage   84.14%   89.09%   +4.95%     
==========================================
  Files          10       27      +17     
  Lines         801     3210    +2409     
==========================================
+ Hits          674     2860    +2186     
- Misses        127      350     +223     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes an infinite masked-usercode refresh loop for Schlage BE469 locks by adjusting _sync_usercode() refresh handling and adding targeted tests to cover masked refresh outcomes (issue #589).

Changes:

  • Add post-refresh masking detection and local-PIN fallback logic in _sync_usercode() to avoid repeated refresh attempts for persistently masked locks.
  • Add a new test suite covering refresh outcomes when the provider returns masked/None/real codes.

Reviewed changes

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

File Description
custom_components/keymaster/coordinator.py Updates _sync_usercode() refresh logic to handle persistently masked usercodes and optionally fall back to a local PIN.
tests/test_coordinator.py Adds a new test class covering masked refresh behaviors and fallback scenarios.

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

Comment thread tests/test_coordinator.py Outdated
Comment thread custom_components/keymaster/coordinator.py Outdated
Comment thread custom_components/keymaster/coordinator.py Outdated
Comment thread custom_components/keymaster/coordinator.py Outdated
Comment thread custom_components/keymaster/coordinator.py Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 3 out of 3 changed files in this pull request and generated 2 comments.


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

Comment thread custom_components/keymaster/lock.py Outdated
Comment thread custom_components/keymaster/coordinator.py
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 3 out of 3 changed files in this pull request and generated 3 comments.


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

Comment thread tests/test_coordinator.py Outdated
Comment thread custom_components/keymaster/coordinator.py
Comment thread tests/test_coordinator.py
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 3 out of 3 changed files in this pull request and generated 1 comment.


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

Comment thread tests/test_coordinator.py
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 3 out of 3 changed files in this pull request and generated 3 comments.


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

Comment thread custom_components/keymaster/coordinator.py
Comment thread tests/test_coordinator.py Outdated
Comment thread custom_components/keymaster/coordinator.py Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 no new comments.


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

Schlage BE469 locks return masked usercodes (e.g. '**********') with
in_use=True on every poll. The refresh logic detected this as needing
a re-fetch but the lock always returns the same masked value, creating
an infinite polling loop that drains battery via repeated Z-Wave
UserCodeCCGet commands every 60 seconds.

After async_refresh_usercode(), check if the result is still masked.
If so, fall back to the locally stored PIN rather than accepting the
masked value that will trigger another refresh next cycle.

Closes FutureTense#589

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 1 comment.


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

Comment thread tests/test_coordinator.py
Copy link
Copy Markdown
Collaborator

@firstof9 firstof9 left a comment

Choose a reason for hiding this comment

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

Logic behind this seems sound, looks good.

@tykeal tykeal merged commit 38d240d into FutureTense:main Apr 1, 2026
13 checks passed
@tykeal tykeal deleted the fix-masked-polling branch April 1, 2026 15:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Fixes a bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ISSUE: Schlage BE469 UserCodeCCGet issued every minute

4 participants