Skip to content

feat: Add requests to verify device bindings and reporting - #32858

Open
pedrolamas wants to merge 6 commits into
Koenkk:devfrom
pedrolamas:feat/verify-device-bindings-and-reporting
Open

pedrolamas wants to merge 6 commits into
Koenkk:devfrom
pedrolamas:feat/verify-device-bindings-and-reporting

Conversation

@pedrolamas

@pedrolamas pedrolamas commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Addresses the diagnostic half of #32857, where a plug silently stopped reporting while Zigbee2MQTT showed it as healthy throughout: availability online, last_seen current, LQI normal. Commands, reads and availability pings all kept working, because none of them depend on the binding table.

Zigbee2MQTT treats its cached binds as the truth and never reads them back, so there is no way to tell a correctly configured device from one that silently dropped everything.

The change

bridge/request/device/binds/read calls Device.bindingTable(), which already reads the device's own binding table over ZDO and reconciles the cached binds of every endpoint with it — the API was there, it just was not reachable from Zigbee2MQTT. Per endpoint the response reports the resulting bindings and missing_bindings.

missing_bindings lists clusters that have a configured reporting but no binding to the coordinator, so the device cannot report them. It is derived from current state rather than a cache diff, which is what makes it useful: once the read has reconciled the cache, nothing else distinguishes a healthy device from a broken one on a second call, since bindings: [] only means something if you also know reporting was configured for those clusters. The same composition is currently hand-rolled in lib/extension/bind.ts (getSetupReportingEndpoints).

Support of the underlying request is optional, so a failure is reported as Failed to read the binding table of '<name>' (<reason>) rather than a bare adapter message.

Result on the reported failure

The plug recurred and the binding table read answered directly: bindingTableEntries: 0, with all three coordinator bindings gone, on a device that was still fully reachable. Details in #32857 (comment). Reconfigure then restored bindings and reporting in one step, verified by reading both back.

Independent of the above

reportable_change was typed number, but it is an array for attributes wider than 32 bit — seMetering.currentSummDelivered is uint48 and really does produce one. Widened to number | number[] in Zigbee2MQTTDeviceEndpointConfiguredReporting. This is a correctness fix that stands on its own; happy to split it out if preferred.

lib/extension/bind.ts had an unanchored topic regex, so device/bind prefix-matched device/binds/read and the bind extension answered a request that was not its own. Anchored, with a regression test.

Review feedback applied

  • device/reporting/sync removed. Endpoint.readReportingConfig already reconciles the cache with the device's response, so the existing device/reporting/read performs the sync. All that request added was device-wide enumeration plus a diff, which does not justify a separate request.
  • No herdsman changes. The earlier version diffed the cache here and would have needed a new zigbee-herdsman API to do it upstream. Both are gone; this now calls the existing one. feat: Add request to verify device bindings zigbee-herdsman#1850 is obsolete as a result.
  • Errors reported properly, per the note that many devices do not support Mgmt_Bind_req.

Scope

This makes the divergence visible and fixable in one action. It does not detect it automatically — configure.ts still trusts meta.configured, and there is no watchdog, so a device that silently unbinds still looks healthy until someone asks. That felt like a separate change.

There is no frontend for the request yet; it is reachable over MQTT only. If this direction is welcome, a button on the Bind tab would be the natural follow-up in zigbee2mqtt-frontend.

Testing

Six tests covering the request: happy path, a device that dropped its bindings, missing bindings, read failure, malformed payload, and unknown device, plus the bind-extension regression test. Full suite passes at 100% coverage. Exercised against a live 34 device network.

🤖 Generated with Claude Code

pedrolamas and others added 4 commits August 15, 2026 18:32
Zigbee2MQTT treats its cached `binds` and `configuredReportings` as the truth
and never reads them back, so a device that silently dropped its bindings or
its reporting configuration keeps looking correctly configured while it no
longer reports anything. Availability, `last_seen` and LQI are all unaffected
because commands, reads and pings do not depend on the binding table.

Add two requests that ask the device itself:

- `bridge/request/device/binds/read` issues a ZDO `Mgmt_Bind_req` via
  `Device.bindingTable()`, which reconciles the cached bindings, and reports
  per endpoint which bindings were added and which the device does not hold.
- `bridge/request/device/reporting/sync` reads back every cached configured
  reporting of the device and reports each as unchanged, updated, removed or
  failed. This is the existing per-attribute `reporting/read` applied to the
  whole device in one request.

Both log a warning when the device diverges from the cache, and only republish
the devices when something actually changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two problems found while testing against a live network:

- The bind extension's topic regex was not anchored, so a
  `bridge/request/device/binds/read` request also matched its `bind` branch
  and answered with an `Invalid payload` error on
  `bridge/response/device/bind`. Anchor it to the topics it handles;
  `binds/clear` is dispatched before the regex is consulted.
- `reportableChange` is an array for attributes wider than 32 bit, e.g.
  `seMetering.currentSummDelivered`, so comparing it by identity reported
  every such attribute as diverged and republished the devices for nothing.
  Compare by value and correct the type, which never allowed the array form
  Zigbee2MQTT has always published.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The `added`/`removed` diff of `bridge/request/device/binds/read` compares the
device against the cached bindings, so reading it reconciles the cache and a
second read of a still broken device reports no divergence at all.

Add `missing_bindings`, the clusters of an endpoint that have a configured
reporting but no binding to the coordinator. That is derived from the current
state instead of a cache diff, so it keeps reporting the fault until the device
is reconfigured. The `removed` diff is kept, it is the only signal for bindings
to a target other than the coordinator.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The repository enforces 100% coverage, three paths of `deviceReportingSync`
were not exercised: skipping an endpoint without configured reporting, adding
an attribute to an existing cluster group, and reading a manufacturer specific
attribute with its manufacturer code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Nerivec

Nerivec commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Sync is already done automatically when the read API for reporting is called. This is duplicating logic?

For binding, I think it was omitted because many devices don't support that request, though I guess it doesn't hurt to add an API for it, just have to make sure we properly report back any error. Most of that logic should not be in Z2M though, this is ZH-level logic. On Z2M side (should be a simple call to ZH once properly implemented), it should also roughly match the existing read for reporting to keep things consistent.

@pedrolamas

Copy link
Copy Markdown
Contributor Author

All three points taken — marking this draft while I rework it.

Sync duplicating logic: you're right. readReportingConfig already calls saveClusterAttributeReportConfig on the response, so the cache reconciliation isn't something this request adds; what it added was device-wide enumeration plus a diff, which is a much smaller claim than I made for it. Removing device/reporting/sync entirely.

Binding logic belongs in ZH: agreed. The before/after snapshot only exists here because Device.bindingTable() overwrites each endpoint's binds through saveBindings() and returns nothing about what changed. I'll open a herdsman PR that keeps bindingTable() as-is and adds a verifyBindings() returning, per endpoint, {bindings, added, removed, unreportableClusters} — with saveBindings() returning its own diff so it's computed where the replacement happens. unreportableClusters is the cross-reference of configured reportings against coordinator bindings; the same composition is currently hand-rolled here in lib/extension/bind.ts (getSetupReportingEndpoints), so it seems worth having upstream. Z2M then becomes one call plus a shape mapping, matching device/reporting/read.

Does that shape work for you, or would you rather it hung off bindingTable() directly?

Error reporting: will wrap the call so a device that doesn't support Mgmt_Bind_req returns Failed to read the binding table of '<name>' (<reason>) rather than a bare adapter message.

One thing I'd keep independent of all this: reportable_change is typed number but is an array for attributes wider than 32 bit (seMetering.currentSummDelivered is uint48). I'll call that out separately in the description rather than leaving it buried.

This stays draft until the herdsman side is released.

pedrolamas and others added 2 commits August 16, 2026 12:27
`Endpoint.readReportingConfig` already reconciles the cache with the
device's response, so the existing `device/reporting/read` request
performs the sync. All this added was device-wide enumeration and a diff,
which does not justify a separate request.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GapxDbTFAEd67eCtDj1y6B
`Device.bindingTable()` already reconciles the cached binds of every
endpoint with the device's own table, so the request no longer diffs the
cache itself and only reports what the device holds. Failures now name the
device, support of the underlying request being optional.

`missing_bindings` stays: it is derived from current state rather than a
cache diff, so it keeps flagging a device that cannot report until it is
reconfigured.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GapxDbTFAEd67eCtDj1y6B
@pedrolamas
pedrolamas marked this pull request as ready for review August 16, 2026 12:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants