feat: Add requests to verify device bindings and reporting - #32858
pedrolamas wants to merge 6 commits into
Conversation
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>
|
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. |
|
All three points taken — marking this draft while I rework it. Sync duplicating logic: you're right. Binding logic belongs in ZH: agreed. The before/after snapshot only exists here because Does that shape work for you, or would you rather it hung off Error reporting: will wrap the call so a device that doesn't support One thing I'd keep independent of all this: This stays draft until the herdsman side is released. |
`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
Addresses the diagnostic half of #32857, where a plug silently stopped reporting while Zigbee2MQTT showed it as healthy throughout: availability
online,last_seencurrent, LQI normal. Commands, reads and availability pings all kept working, because none of them depend on the binding table.Zigbee2MQTT treats its cached
bindsas 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/readcallsDevice.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 resultingbindingsandmissing_bindings.missing_bindingslists 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, sincebindings: []only means something if you also know reporting was configured for those clusters. The same composition is currently hand-rolled inlib/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_changewas typednumber, but it is an array for attributes wider than 32 bit —seMetering.currentSummDeliveredis uint48 and really does produce one. Widened tonumber | number[]inZigbee2MQTTDeviceEndpointConfiguredReporting. This is a correctness fix that stands on its own; happy to split it out if preferred.lib/extension/bind.tshad an unanchored topic regex, sodevice/bindprefix-matcheddevice/binds/readand the bind extension answered a request that was not its own. Anchored, with a regression test.Review feedback applied
device/reporting/syncremoved.Endpoint.readReportingConfigalready reconciles the cache with the device's response, so the existingdevice/reporting/readperforms the sync. All that request added was device-wide enumeration plus a diff, which does not justify a separate request.Mgmt_Bind_req.Scope
This makes the divergence visible and fixable in one action. It does not detect it automatically —
configure.tsstill trustsmeta.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