feat: surface link health in the slot suspension repair - #1410
Merged
Conversation
A suspension repair currently guesses between two very different causes -- "the lock may be rejecting the code silently or experiencing communication issues" -- and Lock Code Manager has no reliable way to tell those apart from the lock's behaviour alone. Both look identical from the slot: a code that was written and never reads back. Providers that can measure their own transport do know, though. Add an optional `describe_link_health` hook, default None, whose sentence is appended to that repair. It is purely descriptive: nothing reads it to make a decision, so a provider that cannot measure anything keeps the existing wording verbatim. The Z-Wave implementation quotes the node's own counters rather than a verdict, because a threshold would have to be right for mains routers, frequently-listening locks and sleeping sensors alike. It states that the counts are cumulative since Z-Wave JS started, since a node that was unreachable last week still reads badly today. Motivated by issue #1397, where a lock left roughly half of the commands sent to it unanswered. The slot looked like it was being rejected, and the numbers that settled it were never shown to anyone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 3447f5908f7e
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1410 +/- ##
=======================================
Coverage 98.95% 98.96%
=======================================
Files 53 53
Lines 6611 6668 +57
Branches 470 470
=======================================
+ Hits 6542 6599 +57
Misses 69 69
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Review of the initial implementation surfaced five ways the sentence could point a reader at the wrong cause, all verified against node-zwave-js `NodeStatistics.ts`: - `commandsTX` counts only commands that were *successfully sent* (:47); one that could not be sent increments `commandsDroppedTX` (:54). A lock that has drifted out of range therefore reported a flawless link -- or nothing at all, since the early return keyed off `commandsTX` alone. Both counters are now reported, and either one is enough to produce a description. - `timeoutResponse` counts only Get-type commands (:55), so dividing it by every command sent understated the failure rate on a lock that is mostly written to. The ratio is gone; the counters stand alone. - `rtt` is an exponential moving average (:59-61), not a single sample, so "the last round trip took" was simply wrong -- one old outlier holds it up long after a link recovers. - The counters reset on demand as well as at startup (:44), so the stated window was narrower than the truth. - `self.node` raises `LockDisconnected` while the zwave_js entry reloads. This runs at sync.py:786, ahead of the tick's own try block and with no handler above it, so a raise there skipped `_suspend_slot` entirely: no repair, state stuck OUT_OF_SYNC, and the same failure every tick. A descriptive helper must never cost the caller its repair. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Entire-Checkpoint: b0d2d3c113d3
Zigbee exposes nothing equivalent to Z-Wave's per-node failure counters -- zigpy keeps counters on the radio application, not per device -- so ZHA reports the other honest signal: the quality of the most recent frame received from the lock, and how long ago it arrived. Staleness carries most of the weight. A lock not heard from in hours while Lock Code Manager is actively writing to it is the Zigbee equivalent of an unanswered-command count, and it stays meaningful on radios that report no signal metrics at all -- so each of the three values is emitted only if present, and a lock with none produces no description rather than an empty one. The wording says "the last frame this lock sent" rather than "signal strength" because that is literally what zigpy stores: lqi and rssi are overwritten from each received packet, so they describe one moment rather than an average. Reached through the already-cached DoorLock cluster (cluster.endpoint.device), so this adds no new device resolution and no new state. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 0d78f73eff15
Validated against real hardware -- an Aqara Smart Lock U300 on Thread -- after the mock fixture showed every Thread Network Diagnostics counter as zero and left it unclear whether they are ever populated. They are: that lock reports 3,608,185 of 3,611,401 acknowledged transmissions confirmed and 228 detachments from the Thread network, with a feature map of 15 (all four counter features present). Quotes two things. Acknowledged transmissions out of those that asked to be acknowledged is safe to state as a proportion because both counters are drawn from the same population -- unlike most pairs in this cluster, where numerator and denominator count different traffic and a ratio would quietly mislead. Detachment count needs no denominator to be legible. Every counter is optional (the cluster gates them behind feature bits), so each is read independently and omitted when absent. That avoids modelling which bit governs which attribute, and a lock keeping none produces no description rather than a sentence full of blanks. The wording states that these are the lock's own view of its radio rather than the hub's view of reaching it, and that they accumulate from the lock's last restart -- counters that survived a healthy month look alarming beside a problem that started an hour ago. Thread only. A Wi-Fi or Ethernet Matter lock exposes a different diagnostics cluster whose semantics are unverified against hardware, and guessing at them is how a reassuring sentence ends up pointing at the wrong cause. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 0b731ef91cbb
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Proposed change
The slot suspension repair currently guesses between two very different causes:
Lock Code Manager has no reliable way to tell those apart from the lock's behaviour alone — both present identically from the slot: a code that was written and never reads back.
Providers that can measure their own transport do know. This adds an optional
describe_link_health()hook onBaseLock(defaultNone) whose sentence is appended to that repair.Descriptive only — nothing reads it to make a decision, no new state, no thresholds, no control-flow change. A provider that cannot measure anything returns
Noneand the repair keeps its wording verbatim. Every implementation reports measurements, never a verdict, states the semantics of what it reports, and never lets a failed read cost the caller its repair.Z-Wave JS
Three counters side by side rather than one ratio, because node-zwave-js counts them over different populations (verified against
NodeStatistics.ts):commandsTXcounts only commands successfully sent (:47) — an out-of-range lock incrementscommandsDroppedTX(:54) instead — whiletimeoutResponsecounts only Get-type commands (:55). Any ratio built from these distorts in the exact case the feature exists for.rttis an exponential moving average (:59-61), so it is reported as an average, and the window wording covers resets as well as restarts (:44).ZHA
Zigbee has no per-node failure counters — zigpy keeps counters on the radio application, not per device — so ZHA reports the other honest signal. Staleness carries most of the weight: a lock unheard-from for hours while LCM is actively writing to it is the Zigbee equivalent of an unanswered-command count, and it stays meaningful on radios reporting no signal metrics.
lqi/rssiare overwritten from each received packet, so the wording says "the last frame this lock sent" rather than implying an average.Matter (Thread)
Validated against real hardware (an Aqara Smart Lock U300 on Thread) after the mock fixture showed every Thread Network Diagnostics counter as zero, leaving it unclear whether they are ever populated. They are — that lock reports a feature map of 15, all four counter features present.
The acknowledgement pair is safe to state as a proportion because both counters are drawn from the same population, unlike most pairs in this cluster. Every counter is optional, so each is read independently and omitted when absent, which avoids modelling which feature bit governs which attribute. The wording is explicit that these are the lock's own view of its radio rather than the hub's view of reaching it.
Thread only: a Wi-Fi or Ethernet Matter lock exposes a different diagnostics cluster whose semantics are unverified against hardware.
Providers with nothing to report
linkquality, but the provider does not retain it; capturing it would mean new per-lock state for a point-in-time value with no staleness signal.Type of change
Additional information
Motivated by #1397, where the reporter's lock left ~49% of the commands sent to it unanswered (126 of 256 cumulative; 19 of 39 during a controlled test). The write reached the lock — the PIN works on the keypad — but the driver's post-write verification read timed out, so the value database kept serving a stale
userIdStatus = Availableand the slot could never confirm. It looked exactly like a silently rejected code.This does not close #1397: the reporter's underlying problem is their Z-Wave mesh, not integration logic. It makes the next report of this shape self-diagnosing.
Coverage remains at 100%.
🤖 Generated with Claude Code