Skip to content

fix(zigbee2mqtt): read PIN responses from the users payload; gate cached republications#1336

Merged
raman325 merged 3 commits into
mainfrom
fix/z2m-users-payload-reads
Jul 11, 2026
Merged

fix(zigbee2mqtt): read PIN responses from the users payload; gate cached republications#1336
raman325 merged 3 commits into
mainfrom
fix/z2m-users-payload-reads

Conversation

@raman325

Copy link
Copy Markdown
Owner

Proposed change

Fixes the read-timeout / out_of_sync reprogramming loop reported in #1335 (Yale YMF40A RL). Verified against the zigbee-herdsman-converters source: the Yale lockExtend() definition answers GetPinCode via fz.lock_pin_code_response, which publishes the result inside the users object — there is no code path in current Zigbee2MQTT that emits the {"pin_code": {user, user_enabled, pin_code}} response shape the provider's pending-read futures were waiting on. Three coupled fixes:

  1. Resolve pending slot reads from users payloads. Each GET is answered by a full-state publish containing the freshly merged entry for the queried user; the pending future now resolves from it instead of burning a guaranteed 10-second timeout per slot (the 40-second refreshes in the issue's log). At most one read is pending at a time (async_get_users queries sequentially), so cached entries for other slots can't satisfy a future they don't belong to. The legacy pin_code response branch is kept.

  2. Fix the status projection (shared by reads and pushes, per the lockUserStatus vocabulary available/enabled/disabled):

    • enabled without a usable PIN value → unreadable() (occupied-but-withheld — expose_pin off hides the code). Previously the push path skipped these entirely, and the read path had no way to represent them. Treating occupancy-with-hidden-code as anything but unreadable makes sync reprogram slots that already hold the right code — the same universal masked projection the Z-Wave and Matter providers use.
    • explicit pin_code: null on an enabled user still projects to empty() (the broker exposes the field and reports no code — existing behavior).
    • unrecognized statuses (not_supported_*) → unreadable(), not empty().
  3. Delta-gate users pushes. Zigbee2MQTT republishes its entire cached state on every attribute change, so most users payloads restate stale entries rather than report changes. Applying them verbatim let a stale cache entry overwrite the optimistic push from a write the device had already accepted — flipping the slot back to its pre-write state and reprogramming it forever (the loop in [ISSUE] Yale YMF40A RL with Zigbee2MQTT constantly polls PIN codes and keeps slots out_of_sync #1335). Entries now reach the coordinator (via _confirm_slot, matching the other push providers) only when they differ from the previous payload; fresh GetPinCode responses always differ when state actually changed, and the read path stays authoritative regardless of the gate.

One aspect intentionally awaits the reporter's raw payload (requested on the issue): which exact status string their lock reports. The projection above is safe for every value in the converter's vocabulary plus its not_supported_* fallback, so the fix does not depend on the answer.

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

Checklist

  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • Tests have been added to verify that the new code works.

Full suite passes locally (1258 passed, 3 skipped); prek clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_015tvvgVhy5F12XVyv6RQBv7

…hed republications

Zigbee2MQTT answers GetPinCode through the users object
(fz.lock_pin_code_response), not a pin_code response payload, so every
slot read timed out. Resolve pending reads from users entries, project
enabled-but-withheld and unrecognized statuses to unreadable instead of
empty, and only push entries that changed since the previous payload --
Zigbee2MQTT republishes its full cached state on every attribute
change, and applying stale entries verbatim reprograms slots forever
(#1335).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015tvvgVhy5F12XVyv6RQBv7
Entire-Checkpoint: dfff3689de8a
Copilot AI review requested due to automatic review settings July 10, 2026 19:07
@github-actions github-actions Bot added python Pull requests that update Python code bug Something isn't working labels Jul 10, 2026

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 a Zigbee2MQTT provider sync loop where PIN reads were timing out (causing repeated out_of_sync reprogramming) by resolving pending reads from the users payload, improving status-to-credential projection (including unreadable/withheld PIN cases), and gating cached-state republications so stale cached entries don’t overwrite optimistic writes.

Changes:

  • Resolve pending GET PIN reads from Zigbee2MQTT users payload entries (while keeping legacy pin_code response handling).
  • Project users status/PIN combinations into SlotCredential with explicit handling for “enabled but PIN withheld” and unknown statuses.
  • Delta-gate users republishes so only changed slot states are confirmed/pushed, preventing stale cache churn.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
custom_components/lock_code_manager/providers/zigbee2mqtt.py Updates payload handling to resolve pending reads from users, improve state projection, and gate/confirm push updates to avoid stale-cache loops.
tests/providers/zigbee2mqtt/test_payload.py Adds/updates tests covering unreadable projections, delta-gating behavior, and pending-read resolution via users.

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

Comment thread custom_components/lock_code_manager/providers/zigbee2mqtt.py Outdated
@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.97%. Comparing base (9a2408a) to head (ffa74a2).
✅ All tests successful. No failed tests found.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main    #1336   +/-   ##
=======================================
  Coverage   96.97%   96.97%           
=======================================
  Files          53       53           
  Lines        6441     6454   +13     
  Branches      473      473           
=======================================
+ Hits         6246     6259   +13     
  Misses        195      195           
Flag Coverage Δ
python 97.51% <100.00%> (+<0.01%) ⬆️

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

Files with missing lines Coverage Δ
...om_components/lock_code_manager/providers/_util.py 100.00% <100.00%> (ø)
...ponents/lock_code_manager/providers/zigbee2mqtt.py 96.24% <100.00%> (+0.14%) ⬆️
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…e coordinator attach

Retained/live MQTT messages can arrive between async_setup's
subscription and coordinator attach; recording that snapshot would
gate out the first post-attach republication that should seed initial
state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015tvvgVhy5F12XVyv6RQBv7

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

Comment thread custom_components/lock_code_manager/providers/zigbee2mqtt.py
int() coerces True/False to 1/0, so a malformed boolean slot
identifier in a payload would silently address slot 1 or 0. Hoisted
into the shared helper so every provider parsing external slot
identifiers gets the guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015tvvgVhy5F12XVyv6RQBv7
Entire-Checkpoint: 37e07409d865

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

@raman325 raman325 merged commit f4eebd3 into main Jul 11, 2026
19 checks passed
@raman325 raman325 deleted the fix/z2m-users-payload-reads branch July 11, 2026 01:55
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.

[ISSUE] Yale YMF40A RL with Zigbee2MQTT constantly polls PIN codes and keeps slots out_of_sync

2 participants