fix: Preserve genBasic attributes when re-read returns undefined#1753
Conversation
`Device.updateGenBasic` used `Object.assign`, which overwrote known values with `undefined` when a `readRsp` record came back with a non-success status (e.g. `UNSUPPORTED_ATTRIBUTE`). During an `interview(true)` re-read, the per-endpoint loop in `interviewInternal` walks every endpoint that advertises `genBasic`; on multi-endpoint devices like Inovelli VZM3x, secondary endpoints reply with unsupported for `swBuildId`, which then wiped the value just read successfully from endpoint 1. This in turn broke firmware-gated exposes/configure in zigbee-herdsman-converters. Skip `undefined` values in `updateGenBasic` so an absent attribute can never erase a previously known one. Successful reads with real values still overwrite as before. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Strange, smells like a regression... zigbee-herdsman/src/controller/helpers/zclFrameConverter.ts Lines 31 to 37 in b7bf626 wrap the whole try/catch with something like if (!("status" in item) || item.status === Zcl.Status.SUCCESS).Which would be proper spec behavior (not supposed to assume any value from non-success response). @Koenkk that would be fine with converters, right? |
I think this is the proper solution indeed. @rohankapoorcom can you check if that works for this particular device? |
…ned" This reverts the `updateGenBasic` change in favor of a fix in `ZclFrameConverter.attributeKeyValue` (per maintainer feedback in Koenkk#1753): non-success `readRsp` records should be dropped at the converter rather than masked at the genBasic chokepoint, matching ZCL spec behavior and preventing the same class of bug for any cluster. Co-authored-by: Cursor <cursoragent@cursor.com>
Per ZCL spec, a `readRsp` record with `status != SUCCESS` carries no
`dataType`/`attrData`. Previously `attributeKeyValue` would still set
`payload[attribute.name] = undefined` for such records, which then
flowed into `Device.updateGenBasic` (and `saveClusterAttributeKeyValue`)
and could overwrite a previously known value.
This was visible on multi-endpoint devices like Inovelli VZM3x: the
per-endpoint genBasic re-read in `interviewInternal` (triggered by
`device.zh.interview(true)` from the zigbee2mqtt frontend re-interview
path) succeeds on endpoint 1 for `swBuildId`, then endpoint 2 replies
with UNSUPPORTED_ATTRIBUTE, and the resulting `{swBuildId: undefined}`
clobbered the value via `Object.assign`. Downstream this broke
firmware-gated exposes/configure in zigbee-herdsman-converters.
Skip non-success records at the converter so the rest of the stack
never sees a synthetic value for them.
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
|
Tested it on my end, the new fix seems to work fine! Docker image: |
|
Thanks! |
Summary
Device.updateGenBasicpreviously calledObject.assign(this.#genBasic, data), which overwrites known values withundefinedwhen the source object contains the key. That happens whenever areadRsprecord comes back with a non-success status (e.g.UNSUPPORTED_ATTRIBUTE): thereadRspparser leavesattrDataundefined,ZclFrameConverter.attributeKeyValuestill setspayload[attribute.name] = undefined, and the downstreamupdateGenBasicthen erases the existing value.This change makes
updateGenBasicskip undefined values so an absent attribute can never wipe a previously known one. Successful reads with real values still overwrite as before.Bug scenario (the trigger I hit)
device.zh.interview(true)(ignoreCache=true, introduced in Koenkk/zigbee2mqtt#23328).interviewInternalruns the genBasic re-read loop:genBasicper the ZCL spec but only endpoint 1 has a meaningfulswBuildId. Endpoint 2/3 reply toswBuildIdreads withUNSUPPORTED_ATTRIBUTE.swBuildIdis set successfully, then the endpoint-2 read returns{ swBuildId: undefined }, andObject.assignwipes the value.device.softwareBuildIDisundefined. In zigbee-herdsman-converters this breaks the InovellidynamicExposes/configurepath (src/lib/inovelli.ts:656,673), which usessoftwareBuildIDto compute firmware-gated exposes —configurethenchunkedReads attributes the firmware doesn't actually support.Root cause
Two pre-existing facts combined to make the bug visible:
genBasic(always has).attributeKeyValuepropagatesundefinedfor non-success records (because the readRsp parser leavesattrDataundefined whenstatus !== SUCCESS):updateGenBasicis the chokepoint where every genBasic mutation funnels through (interview reads, Tuya quick-reads, and runtimereadRsp/attributeReportincontroller.ts). Fixing it here covers all paths and there's no scenario where overwriting a known value withundefinedis desirable.Change
Regression test
test/device.test.tsnow pins the contract: a real value is set, a follow-up call withundefinedfor the same keys does not clobber, and a third call with new real values still overwrites normally.Validation
pnpm run build(tsc) — cleanpnpm run check(biome) — cleanpnpm test— 1740 passed, 1 skipped (was 1739/1)Test image (zigbee2mqtt + this ZH branch)
For users who want to verify the fix end-to-end, a custom z2m Docker image bundling this branch is available:
Repro:
softwareBuildID) becomes empty and the Inovelli converter loses firmware-gated exposes / errors during configure.softwareBuildIDis preserved and the converter behaves as before.Test plan
softwareBuildIDintact.Made with Cursor