Skip to content

feat: surface link health in the slot suspension repair - #1410

Merged
raman325 merged 4 commits into
mainfrom
fix/1397-surface-link-health
Aug 14, 2026
Merged

feat: surface link health in the slot suspension repair#1410
raman325 merged 4 commits into
mainfrom
fix/1397-surface-link-health

Conversation

@raman325

@raman325 raman325 commented Aug 14, 2026

Copy link
Copy Markdown
Owner

Proposed change

The slot suspension repair currently guesses between two very different causes:

The lock may be rejecting the code silently or experiencing communication issues.

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 on BaseLock (default None) 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 None and 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

Z-Wave link, counted since Z-Wave JS started or the statistics were last reset: 256 commands reached this lock, 4 could not be sent at all, and 126 read requests went unanswered; recent round trips averaged 1072 ms.

Three counters side by side rather than one ratio, because node-zwave-js counts them over different populations (verified against NodeStatistics.ts): commandsTX counts only commands successfully sent (:47) — an out-of-range lock increments commandsDroppedTX (:54) instead — while timeoutResponse counts only Get-type commands (:55). Any ratio built from these distorts in the exact case the feature exists for. rtt is 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 link, from the last frame this lock sent: signal quality 12 of 255, -89 dBm, last heard from 3 hours ago.

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/rssi are overwritten from each received packet, so the wording says "the last frame this lock sent" rather than implying an average.

Matter (Thread)

Thread link, from the lock's own radio counters since it last restarted: 3608185 of 3611401 transmissions that asked to be acknowledged were acknowledged, and the lock has dropped off the Thread network 228 times.

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

  • zigbee2mqtt — publishes 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.
  • Schlage / Akuvox — cloud APIs with no radio telemetry.
  • virtual — nothing to measure.

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

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 = Available and 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

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
Copilot AI lite review requested due to automatic review settings August 14, 2026 03:41

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 enhancement New feature or request labels Aug 14, 2026
@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.96%. Comparing base (988a5fb) to head (4dcc050).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files

Impacted file tree graph

@@           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           
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 Δ
custom_components/lock_code_manager/domain/sync.py 100.00% <100.00%> (ø)
...om_components/lock_code_manager/providers/_base.py 100.00% <100.00%> (ø)
...m_components/lock_code_manager/providers/matter.py 100.00% <100.00%> (ø)
...stom_components/lock_code_manager/providers/zha.py 100.00% <100.00%> (ø)
...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 and others added 3 commits August 13, 2026 23:55
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
@raman325
raman325 merged commit 46bd5aa into main Aug 14, 2026
22 checks passed
@raman325
raman325 deleted the fix/1397-surface-link-health branch August 14, 2026 04:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request python Pull requests that update Python code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ISSUE] Sync attempts exceeded

2 participants