Skip to content

fix: treat occupied-but-valueless Z-Wave slots as unreadable, not empty - #1408

Merged
raman325 merged 1 commit into
mainfrom
fix/1397-uc-occupied-slot-unreadable
Aug 12, 2026
Merged

fix: treat occupied-but-valueless Z-Wave slots as unreadable, not empty#1408
raman325 merged 1 commit into
mainfrom
fix/1397-uc-occupied-slot-unreadable

Conversation

@raman325

@raman325 raman325 commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Proposed change

Closes a parity gap between node-zwave-js's fresh and cached User Code CC reads, which LCM's slot projection turns into a wrong answer.

Three paths in AccessControl.ts answer "is this slot occupied?", and the cached one differs from the other two:

  • getCredential (fresh, single slot, :960) rejects only Available/StatusNotAvailable, then returns data: result.userCode — including "".
  • #getAllCredentials_UC (fresh, bulk branch, :1884) does the same, pushing data: entry.userCode regardless of whether a value came back.
  • #getCredentialCached_UC (:2081) adds if (data == undefined) return undefined, collapsing "slot is empty" and "slot is occupied, value unknown" into one return value.

LCM reads through the cached path (get_all_credentials_cached), and _project_users_to_slots starts every managed slot at SlotCredential.empty() and only overlays credentials. So an occupied-but-valueless slot projects to empty() — LCM's strongest possible claim, "the lock definitively has no code here" — on the weakest possible evidence, an omission.

That matters because empty() is load-bearing for sync. A slot with an outstanding OPTIMISTIC write that reads back empty stays pending and unverified, and calculate_in_sync returns False at the is_verified gate before reaching the _last_set_pin escape hatch that would otherwise tolerate it. The result is a write/verify loop that ends at the circuit breaker. (A CONFIRMED write cannot loop this way — it pops the pending entry and marks the slot verified.)

This PR takes userIdStatus as the occupancy authority and the credential as the value, so an occupied slot with no credential projects to unreadable(). _pin_state already mapped a withheld value to unreadable correctly — the credential simply never reached it.

Deliberately applied in the slot projection (async_get_usercodes) rather than in async_get_users: the user list also drives the write paths (delete-owner resolution and async_set_user's adoption scan). An earlier revision put the fallback in async_get_users and the synthesized owner was picked up by the legacy-adoption scan, silently turning a user CREATE into an UPDATE. Read-repair belongs to the read.

Behavioral notes, each covered by a test:

  • A cleared slot reports AVAILABLE, so clears still confirm.
  • Only empty entries are upgraded; a readable credential always outranks the fallback, so a healthy lock is untouched.
  • in_use is compared against True explicitly — None means the value database holds no status for the slot, which is absence of evidence, not evidence of occupancy.
  • Inert on pure User Credential CC locks, which report their own credentials natively.

Two existing projection tests moved to slots 3/4, which the Schlage fixture reports AVAILABLE; they previously asserted "empty" on slots the fixture reports ENABLED, which is the state this PR corrects.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New feature (which adds functionality)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

This is deliberately not marked as fixing #1397. It was found while investigating that issue and it removes a real failure mode, but whether it is the cause there is unconfirmed. Reaching the state it fixes requires userIdStatus occupied while the cached userCode is absent, and tracing node-zwave-js master did not turn up a normal path that produces it: persistUserCode writes status and code together or removes both, #persistCachedUserCode writes both, setUser on a User Code CC lock throws rather than create a codeless user, and secret: true on userCode only suppresses logging rather than excluding the value from the disk cache. If that state is unreachable, the reporter's slot 5 is reading Available instead, in which case this change does not fire there. Confirming needs the userIdStatus value from Z-Wave JS device diagnostics.

🤖 Generated with Claude Code

When a lock reports userIdStatus occupied while withholding the code
value, node-zwave-js's User Code CC adapter drops the credential from
the unified read rather than returning it with no value. The slot then
projected to empty(), telling the sync layer the write definitively did
not land -- so it rewrote, failed to confirm again, and the circuit
breaker suspended a slot whose PIN works on the keypad.

Only the OPTIMISTIC write path could loop this way: a CONFIRMED write
pops the pending entry and marks the slot verified, so the
_last_set_pin escape hatch in calculate_in_sync tolerates an empty
read-back. An OPTIMISTIC write stays pending and unverified, and
calculate_in_sync returns False at the is_verified gate before ever
reaching that hatch.

Take userIdStatus as the occupancy authority and the credential as the
value, so an occupied slot with no credential is unreadable rather than
empty. Applied in the slot projection rather than async_get_users: the
user list also drives the write paths (delete-owner resolution and
async_set_user's adoption scan), and an occupancy inferred from the
value database must not redirect which user a write targets.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 23adf956e0cc
Copilot AI lite review requested due to automatic review settings August 11, 2026 18:35

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot added python Pull requests that update Python code bug Something isn't working labels Aug 11, 2026
@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.95%. Comparing base (68492f1) to head (c4b2850).
⚠️ Report is 3 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main    #1408   +/-   ##
=======================================
  Coverage   98.95%   98.95%           
=======================================
  Files          53       53           
  Lines        6598     6611   +13     
  Branches      470      470           
=======================================
+ Hits         6529     6542   +13     
  Misses         69       69           
Flag Coverage Δ
python 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...components/lock_code_manager/providers/zwave_js.py 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@raman325
raman325 merged commit 988a5fb into main Aug 12, 2026
22 checks passed
@raman325
raman325 deleted the fix/1397-uc-occupied-slot-unreadable branch August 12, 2026 15:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working python Pull requests that update Python code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants