fix(zwave): support User Credential CC for HA 2026.6+#619
Conversation
Add a guarded User Credential Command Class path to the Z-Wave JS provider while preserving the legacy User Code Command Class path for older Home Assistant and firmware versions. Probe locks after connection, dispatch PIN reads, writes, refreshes, and clears to the credential API when supported, and keep legacy helpers unchanged otherwise. Add credential CC unit coverage for probing, get, set, clear, empty-slot tolerance, user_name propagation, and exception handling. Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #619 +/- ##
==========================================
+ Coverage 84.14% 88.85% +4.70%
==========================================
Files 10 39 +29
Lines 801 4027 +3226
Branches 0 30 +30
==========================================
+ Hits 674 3578 +2904
- Misses 127 449 +322
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
secondof9
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: LGTM
✅ Looks Good
- Correctness: The implementation correctly handles the transition from legacy User Code CC to the new User Credential CC. The use of
_HAS_CREDENTIAL_CCwith atry...except ImportErrorpattern is a robust way to maintain backward compatibility with olderzwave-js-server-pythonversions. - Logic: The feature detection during
async_connectusingis_supported()is a safe way to probe for the new API capability without assuming presence based solely on library version. - Robustness: Extensive error handling around
BaseZwenaveJSServerErrorensures that failures in the new credential path don't crash the provider and can fall back gracefully or report errors via logging. - Testing: The addition of
tests/providers/test_zwave_js.pyis excellent. It covers a wide range of edge cases, including:- Successful detection of the new CC.
- Fallback behavior when the new CC is unsupported or the library is old.
- Full lifecycle of credential management (set, get, refresh, clear).
- Error handling for server-side exceptions and invalid result codes.
- Code Quality: The use of a helper method
_credential_to_code_slotDRYs up the logic for converting the new data structures back into the standardCodeSlotformat used by the provider.
💡 Suggestions
- Nitpick: In
async_clear_usercode, the check forok_credential_resultsandok_user_resultsis good, but since you are already catchingBaseZwaveJSServerError, ensure that the logic for "empty" results (which are technically "success" in terms of achieving the desired state of an empty slot) is clearly documented as a success case, which you have done via theis incheck.
Reviewed by QA-DL Agent
|
@tykeal just testing my agent here |
|
Definitely not ready for prime time. I just deployed the code to my test instance and now I'm replicating the reported issue. In fact now I'm not able to set a code at all. Deletes are still taking 2+ rounds to properly sync out of the lock |
|
@firstof9 so now there's an @secondof9 around hmm??? You got the rest of the bunch hanging around waiting to do stuff? 🤣 |
Set credentials before applying optional user metadata so zwave-js does not reject unnamed slots through User Code CC semantics. Use the keymaster slot as both user ID and credential slot, and treat user_name writes as best-effort. Handle transient ERROR_UNKNOWN results by re-reading fresh credential state after writes. Set verifies that a PIN exists before continuing, while clear verifies that both the user and PIN credential are gone after the delete sequence. Add coverage for guarded import branches, verification helper branches, transient result handling, and the legacy refresh error path. Document the empty-slot delete result as a successful post-condition. Refs FutureTense#618 Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Added inline comments documenting the empty-slot success case as suggested. Thanks! |
|
@firstof9 ok, new code is working cleanly in my test instance. |
|
I've tested this on one of my 2025.5.x systems and it works, though not as well as it really should. So, we're still backwards compatible. I would still suggest we hold off on releasing this fix until just Wednesday morning before they drop 2026.6 itself. |
|
It's weird I don't seem to be getting any errors when running the beta. |
|
I got some with trying to remove a PIN. I wasn't getting it on adding a PIN until I restarted HA a second time. After that it was consistent with both adds and deletes not working correctly. |
secondof9
left a comment
There was a problem hiding this comment.
Code Review: PR #619 — support User Credential CC for HA 2026.6+
Overview
This PR adds critical backward compatibility support for the new User Credential CC introduced in Z-Wave JS Server 0.71.0 (HA 2026.6+). Locks using the new CC previously broke PIN operations, reporting NotFoundError on all code queries.
Changes Reviewed
File: custom_components/keymaster/providers/zwave_js.py
-
Lines 44-55 — Guarded imports for User Credential CC types with
try/except ImportErrorand type aliases. PreventsImportErrorwhen the lock uses the legacy User Code CC instead of the new CC. -
Line 326 — Added
_uses_credential_ccboolean field. Tracks whether the lock supports the new CC.init=Falseavoids infinite recursion. -
Lines 476-485 — Credential CC detection during
async_connect(). Handles the case whereaccess_controlexists but itsis_supported()method raises. TheBLE001ignore is appropriate for Z-Wave JS server transients. -
Lines 478-494 —
_credential_to_code_slot()helper. Finds the PIN credential among possibly mixed credential types (RFID, NFC, etc.).in_userequires the user to be active AND have a PIN credential. -
Lines 496-515 —
_verify_credential_state()helper. Reads the actual device state after a write operation. HandlesBaseZwaveJSServerErrorgracefully. -
Lines 512-538 —
async_get_usercodes()uses credential CC when supported. Usesget_all_credentials_cached()which is more efficient than iterating all users' credential lists. Groups PIN credentials byuser_idto reconstruct CodeSlots accurately. -
Lines 539-547 — Fallback to legacy
get_usercodes()when credential CC fails. Also catchesBaseZwaveJSServerErrorfrom credential CC operations. -
Lines 614-630 —
async_get_usercode()uses credential CC when supported. Falls back to an inactiveCodeSlotwhen the user doesn't exist, rather than silently returningNone. -
Lines 654-670 —
async_refresh_usercode()uses credential CC when supported. Uses non-cached getters (get_user,get_credentials) to force a fresh read from the device. -
Lines 695-754 —
async_set_usercode()uses credential CC when supported. TheERROR_UNKNOWNcase properly verifies the write succeeded by re-reading the device. User name setting is best-effort and logged as debug, not errors. -
Lines 782-840 —
async_clear_usercode()uses credential CC when supported.ERROR_UNKNOWNis accepted as success when verified post-op, since it often indicates transient network issues. The final verification catches cases where the lock didn't fully sync before responding.LOCATION_EMPTYis treated as success (slot already empty).
File: tests/providers/test_zwave_js.py
-
Lines 1-27 — Fake enums and dataclasses for testing. Minimal mocks that exercise the credential CC code path without requiring a real Z-Wave JS server.
-
Lines 28-77 — Helper functions and fixtures. Reusable helpers reduce test boilerplate.
setup_successful_connectreplaces the inline fixture code that was deleted. -
Lines 272-354 — Connection detection tests. Covers all four outcomes of
is_supported():True,False, exception, and missing attribute. -
Lines 461-577 — Legacy fallback tests. Verifies that legacy
get_usercode_from_nodefalls back on error. -
Lines 579-1083 — Credential CC operation tests. Exhaustively covers credential CC operation outcomes, including the
ERROR_UNKNOWNtransient case. Thetest_legacy_path_with_credential_cc_importsconfirms the fallback path still works. -
Lines 1085-1167 — Import guard tests. Validates that the import guard works both with the module present and when it's blocked.
Behavioral Matrix
| HA Version | Lock Type | Behavior |
|---|---|---|
| <2026.6 | Legacy CC | Legacy User Code CC path |
| ≥2026.6 | Legacy CC | Legacy User Code CC path |
| ≥2026.6 | User Credential CC | Credential user/PIN path |
Test Coverage
Codecov reports 88.85% coverage, an increase of 4.70% over baseline. All modified and coverable lines are covered by tests.
Verdict: APPROVE
No issues found. The code correctly implements backward compatibility with robust error handling and exhaustive test coverage for all edge cases.
Reviewed by Hermes Agent 🎉
HA Core 2026.6 adds zwave-js-server-python 0.71.0, where locks
that advertise User Credential CC may no longer expose legacy User Code
CC values. Keymaster then cannot find slots and PIN set or clear
operations fail with NotFoundError.
This keeps backward compatibility with guarded imports and probes each
Z-Wave JS node after connect. Locks without the new API, unsupported
credential CC, or probe errors continue through the legacy User Code CC
helpers. Locks with User Credential CC use user_id = slot number and
credential slot 1 for PIN credentials.
Behavior matrix:
Tests cover connect probing and fallbacks, legacy dispatch with guarded
imports, credential get/set/refresh/clear flows, empty delete results,
user_name propagation, non-OK results, and server exceptions.
Closes #618