Skip to content

Stop LocalDataCluster reads from re-applying quirk value conversions - #5287

Open
rasmusbe wants to merge 1 commit into
zigpy:devfrom
rasmusbe:fix-local-cluster-read-echo
Open

Stop LocalDataCluster reads from re-applying quirk value conversions#5287
rasmusbe wants to merge 1 commit into
zigpy:devfrom
rasmusbe:fix-local-cluster-read-echo

Conversation

@rasmusbe

@rasmusbe rasmusbe commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Proposed change

LocalDataCluster.read_attributes_raw serves values straight from the attribute
cache, and zigpy feeds every successful read result back through
_update_attribute (via _legacy_apply_quirk_attribute_update in
Cluster.read_attributes). On a cluster that converts values in
_update_attribute, that converts an already converted value a second time and
stores the result, so every read corrupts the value further.

LocalIlluminanceMeasurementCluster applies 10000 * log10(v) + 1, so a reading
of 100 lx walks towards the fixed point of that function on each read:

100 -> 20001 -> 43011 -> 46336 -> 46660

20001 is the correct stored value for 100 lx. 46660 decodes to roughly
46 300 lx — direct sunlight. I observed this on an Aqara Curtain Driver E1 after
four reads of measured_value in the "Manage Zigbee device" UI, but nothing
about it is specific to that device: it affects every quirk pairing
LocalDataCluster with a converting _update_attribute, which includes all
Xiaomi quirks using LocalIlluminanceMeasurementCluster.

The cache is the source of truth for a local cluster, so an echo of a value it
just returned carries no new information. This records the attribute ids handed
back by read_attributes_raw and drops the matching _update_attribute call.

Only the attribute id is compared, not the value. A subclass overriding
_update_attribute sits ahead of LocalDataCluster in the MRO and has already
transformed the value by the time it gets there — comparing values works for
LocalIlluminanceMeasurementCluster but not for
IlluminanceMeasurementClusterP1, which overrides _update_attribute itself.

Additional information

Reproduction: on any device using LocalIlluminanceMeasurementCluster, read
measured_value repeatedly on the local cluster and watch the value climb.

Two regression tests, both verified to fail without the change:

  • test_local_data_cluster_read_does_not_reconvert covers the pattern generally
    with a synthetic converting local cluster
  • test_xiaomi_local_illuminance_survives_repeated_reads covers the real
    LocalIlluminanceMeasurementCluster via LumiMotionAC02

Side effects worth flagging for review: reads of _CONSTANT_ATTRIBUTES and
_DEFAULT_VALUES no longer populate the attribute cache as a side effect of the
echo. The read itself still returns those values, and get() still falls back to
_DEFAULT_VALUES, so I could not find behaviour that depends on the old
caching — but it is the one observable difference beyond the fix itself.

There is a narrow window between read_attributes_raw and the echo in which a
genuine update to the same attribute id would be dropped. read_attributes_raw
on a LocalDataCluster performs no I/O, and updates to a local cluster come
from quirk code running synchronously inside a report handler, so I do not
believe this is reachable in practice.

No separate issue was filed; this PR is the report.

Device diagnostics

Not applicable — this is a fix in LocalDataCluster, not a new or changed quirk.
The behaviour is covered by the two tests above rather than by diagnostics data.

Checklist

  • The changes are tested and work correctly
  • pre-commit checks pass / the code has been formatted using Black
  • Tests have been added to verify that the new code works
  • Device diagnostics data has been attached (not applicable, see above)

`LocalDataCluster.read_attributes_raw` serves values straight from the
attribute cache, and zigpy feeds every successful read result back through
`_update_attribute`. On a cluster that converts values there, that converts an
already converted value a second time and stores the result, so every read
corrupts the value further.

`LocalIlluminanceMeasurementCluster` applies `10000 * log10(v) + 1`, so a
reading of 100 lx walks towards the fixed point of that function on each read:

    100 -> 20001 -> 43011 -> 46336 -> 46660

46660 decodes to roughly 46 300 lx. Observed on an Aqara Curtain Driver E1
after four reads of `measured_value` in the Manage Zigbee device UI, but this
affects every quirk combining `LocalDataCluster` with a converting
`_update_attribute`.

The cache is the source of truth for a local cluster, so an echo of a value it
just returned carries no new information. Record the attribute ids handed back
by `read_attributes_raw` and drop the matching `_update_attribute` call. Only
the id is compared, not the value: a subclass overriding `_update_attribute`
sits ahead of `LocalDataCluster` in the MRO and has already transformed the
value by the time it gets there.
@codecov

codecov Bot commented Aug 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.60%. Comparing base (6a3822c) to head (12ba5b3).

Additional details and impacted files
@@           Coverage Diff           @@
##              dev    #5287   +/-   ##
=======================================
  Coverage   92.60%   92.60%           
=======================================
  Files         424      424           
  Lines       14667    14676    +9     
=======================================
+ Hits        13582    13591    +9     
  Misses       1085     1085           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.

1 participant