Skip to content

Fix missing config entity attribute reads - #825

Merged
TheJulianJES merged 5 commits into
devfrom
zigpy-bot/restore-startup-reads-657
Jul 17, 2026
Merged

Fix missing config entity attribute reads#825
TheJulianJES merged 5 commits into
devfrom
zigpy-bot/restore-startup-reads-657

Conversation

@TheJulianJES

@TheJulianJES TheJulianJES commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Proposed change

This PR fixes a regression introduced with the below PR where attributes for several config entities were not read when initially pairing the device, leading to missing entities. This is fixed by adding a cluster config to read the required attributes for those entities.

ZHA refactor PR that caused the issue (note: not the recent ZHA 2.0.0 refactor):

Addresses HA Core issue:

This should also fix a reported issue where the Danfoss entities are missing:

Future

In the future, we may want to move some of the entities to ZHA quirks. I'm not sure about the ultrasonic_*-based entities used by the Sonoff though. Maybe we should make them more generic instead? Similar to the pir_* attributes?

Either way, an initial branch moving all the Sonoff entities to quirks v2 (keeping the same unique ID) can be found here:

I guess we can always move them back to ZHA as generic entities if quirks keeps the cluster ID in the unique ID, so the format is the same as used by ZHA. And we should also move the other entities, like Danfoss and Hue.

AI summary

Problem

LLM-generated text describing the problem with more detail (click to expand)

ZCLEnumSelectEntity, NumberConfigurationEntity and ConfigurableAttributeSwitch skip entity creation when cluster.get(attribute_name) returns None in _is_supported(). Before #657, the cluster handlers read these attributes during device initialization (ZCL_INIT_ATTRS/REPORT_CONFIG), so a value was cached and the entity was created. #657 moved the startup reads to per-entity _server_cluster_config declarations, but several entities never got one. Their backing attribute is now never read, so the entities are silently skipped on freshly-paired devices. Devices paired before the regression keep their entities only because a value is still in the database from a read performed by an older version.

Reported for the Sonoff SNZB-06P in home-assistant/core#173224: the "Detection sensitivity" and "Presence detection timeout" entities are missing for newly-paired sensors, while identical sensors paired earlier still have them. Debug logs from that issue confirm the interview never sends a read for ultrasonic_o_to_u_delay/ultrasonic_u_to_o_threshold, after which both entities are skipped with "… is not supported - skipping … entity creation".

Affected entities

All confirmed against the pre-#657 handlers (each attribute was read at startup via ZCL_INIT_ATTRS):

  • Sonoff SNZB-06P/03P (OccupancySensing): SonoffPresenceSenorTimeout (ultrasonic_o_to_u_delay), SonoffPresenceDetectionSensitivity (ultrasonic_u_to_o_threshold)
  • Hue SML001/SML002/SML003/SML004 (OccupancySensing): HueV1MotionSensitivity and HueV2MotionSensitivity (sensitivity) — the V1 entity has a _server_cluster_config that reads occupancy and the PIR delays, but not its own backing attribute
  • Danfoss Ally (Thermostat): DanfossExerciseDayOfTheWeek, DanfossExerciseTriggerTime, DanfossOrientation, DanfossAdaptationRunControl, DanfossControlAlgorithmScaleFactor, DanfossExternalMeasuredRoomSensor, DanfossLoadRoomMean, DanfossRegulationSetpointOffset

Other cache-gated entities are unaffected: the IKEA air purifier attributes are read via the fan entity's cluster config, and the Tuya 0xEF00 attributes (timer_duration, window_detection_function) are seeded into the attribute cache by the quirk and were never read by ZHA itself.

Fix

LLM-generated text describing the fix in detail (click to expand)

Give each affected entity a _server_cluster_config that reads its backing attribute on startup. read_on_startup mirrors the pre-#657 semantics: attributes marked cached in ZCL_INIT_ATTRS use read_on_startup=False (allow-cache read, still hits the device when nothing is cached), and the two Danfoss attributes previously marked "can change" (external_measured_room_sensor, load_room_mean) use read_on_startup=True.

Device snapshots are unchanged: the recorded devices already carry cached values for these attributes, so they don't exercise the fresh-pair path.

Upgrade note

LLM-generated upgrade note (click to expand)

Affected mains-powered devices (e.g. the SNZB-06P) pick the values up on the next ZHA restart via startup polling, restoring the missing entities. Battery-powered devices (SNZB-03P, Hue motion sensors, Danfoss TRVs) are initialized from cache on restart, so they may additionally need a re-interview (Reconfigure with the device awake) or an unavailable/available cycle.

PR #657 ("Replace cluster handlers with entity attributes") moved
startup attribute reads from cluster handlers' `ZCL_INIT_ATTRS` onto
each entity's `_server_cluster_config`. Several cache-gated config
entities never got one, so their backing attribute is never read on a
fresh pair. Because these entities skip their own creation when
`cluster.get(attribute_name) is None`, they are silently culled on
newly-paired devices (devices paired under an older ZHA keep working
only because the value persisted in the cache from a past read).

Restore the reads by giving each affected entity a `_server_cluster_config`
that reads its backing attribute on startup (mirroring the pre-#657
`ZCL_INIT_ATTRS` cache/fresh semantics):

- Sonoff SNZB-06P/03P: presence timeout (`ultrasonic_o_to_u_delay`) and
  detection sensitivity (`ultrasonic_u_to_o_threshold`)
- Hue SML00x: motion sensitivity (`sensitivity`)
- Danfoss Ally thermostat: exercise day of week, exercise trigger time,
  orientation, adaptation run control, control algorithm scale factor,
  external measured room sensor, load room mean, regulation setpoint offset

Add a discovery invariant test asserting every cache-gated config entity
reads its backing attribute on startup (with a documented allowlist for
Tuya EF00 attributes whose cache is seeded by the quirk), plus functional
tests that the Sonoff entities are created from the startup read.
Drop the Sonoff read-on-join tests and the cache-gate/startup-read
invariant test; a check like the invariant may live in the diagnostics
tooling instead in the future.
@codecov

codecov Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.27%. Comparing base (c38e03e) to head (338f83b).

Additional details and impacted files
@@           Coverage Diff           @@
##              dev     #825   +/-   ##
=======================================
  Coverage   97.27%   97.27%           
=======================================
  Files          55       55           
  Lines       10930    10940   +10     
=======================================
+ Hits        10632    10642   +10     
  Misses        298      298           

☔ 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.

Comment on lines 539 to 544
OccupancySensing.AttributeDefs.pir_o_to_u_delay: AttrConfig(
read_on_startup=False,
),
OccupancySensing.AttributeDefs.pir_u_to_o_delay: AttrConfig(
read_on_startup=False,
),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, I'm not sure how these attributes ended up in the cluster config of this Hue-specific select entity? These are used by a number entity. (Side note: Why does that number entity also include the occupancy attribute for the binary sensor again?)

I'll check to see if other unintentional changes were introduced by that refactor and then remove these in a follow-up PR (to keep this PR as just the fix).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And the binary sensor also re-adds all the same attributes yet again? Why do all three platforms configure the exact same attributes in the cluster config? We merge these, right..?

OccupancySensing.AttributeDefs.occupancy: AttrConfig(
read_on_startup=True,
reporting=ReportingConfig(
min_interval=0, max_interval=900, reportable_change=1
),
),
OccupancySensing.AttributeDefs.pir_o_to_u_delay: AttrConfig(
read_on_startup=False,
),
OccupancySensing.AttributeDefs.pir_u_to_o_delay: AttrConfig(
read_on_startup=False,
),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a wholesale copy of the old OccupancySensingClusterHandler config: #657 translated each pre-refactor cluster handler by copying its full REPORT_CONFIG/ZCL_INIT_ATTRS block onto every entity class that used that handler — the Occupancy binary sensor, the PIR occupied→unoccupied delay number entity, and this Hue select all got the identical occupancy + pir_*_delay block. The handler's dynamic __init__ additions (Hue sensitivity, Sonoff ultrasonic_*) were dropped in that translation, which is exactly the regression this PR fixes.

And yes, they merge: aggregate_cluster_configs() merges all discovered entities' configs per (endpoint, cluster, direction)read_on_startup is OR-ed, the tightest reporting wins, bind is OR-ed — so the duplicates are runtime-harmless, but they hide gaps like this one. Two more side effects of the same copy-paste, for the follow-up:

  • PIRUnoccupiedToOccupiedDelayConfigurationEntity has no cluster config of its own and only works because its sibling's copy happens to include pir_u_to_o_delay.
  • ThermostatLocalTempCalibration (+ Sonoff/Bosch subclasses) and DanfossExerciseDayOfTheWeek carry the entire 22-attribute thermostat handler block (including setpoint_change_source*), and StartupOnOffSelectEntity / OnOffTransitionTimeConfigurationEntity / BegaColorTemperatureChannelSelect carry the OnOff/LevelControl state blocks.

On the entity count: before this PR exactly 12 entity classes are broken on fresh pairing — per device that's 2 missing entities on SNZB-06P/SNZB-03P, 1 on each Hue SML001–SML004, and 8 on a Danfoss Ally. After this PR the only cache-gated entities without a ZHA-side read are the two Tuya 0xEF00 ones, which are quirk-seeded by design. (Full audit in my review.)

I've pushed the dedup as a follow-up basis on top of this branch: zigpy-bot/cluster-config-dedup — 5 commits (OccupancySensing / Thermostat / LevelControl / OnOff dedup, each entity now only declaring the attributes it owns, verified by diffing the aggregated per-(cluster, attribute) config view before/after — byte-identical — plus a full test run; and one restore commit for two more #657 losses: Philips SOC001 0xFC06 bind+reporting and the Legrand cable outlet 0xFC40 bind, details in the follow-up comment). Happy to open it as a PR once this one lands.

@zigpy-review-bot zigpy-review-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve — the fix is correct, and I can confirm the affected-entity list in the description is complete.

I audited this programmatically: enumerate every registered entity class whose _is_supported() gates on cluster.get(<attr>) is None (126 classes across select/number/switch), then check which backing attributes are read at startup by any co-matching entity's _server_cluster_config:

  • Before this PR, exactly the 12 entity classes listed in the description have no startup read (2× Sonoff SNZB-06P/03P, HueV1MotionSensitivity/HueV2MotionSensitivity, 8× Danfoss Ally) — nothing else is missing.
  • After this PR, only the two Tuya 0xEF00 entities remain unread (timer_duration, window_detection_function), matching the description: those were never read by ZHA pre-#657 and rely on the quirk seeding the attribute cache.
  • The read_on_startup choices mirror the pre-#657 ZCL_INIT_ATTRS markings exactly: the old attr: True (allow-cache) entries → read_on_startup=False, and the two Danfoss # Can change entries (external_measured_room_sensor, load_room_mean) → read_on_startup=True (checked against DanfossThermostatClusterHandler / OccupancySensingClusterHandler at e399b13a^).
  • Ordering is right: async_initialize() discovers prospective entities, aggregates and reads the configs, and only then runs the is_supported() gating (zha/zigbee/device.py, async_initialize_add_pending_entities), so the restored reads land before entity creation is decided.

Root-cause note for the record: the pre-#657 OccupancySensingClusterHandler added the Hue sensitivity / Sonoff ultrasonic_* reads dynamically in __init__, and DanfossThermostatClusterHandler extended the base dict — the refactor translated the static handler configs onto entities but dropped these model-conditional additions.

Tests pass locally and mypy (run with project dependencies installed) is clean. A second-opinion Copilot (GPT-5.6 Sol) review of this PR returned no findings.

Pre-existing gap in the same area, not part of this PR: Inovelli VZM36 / VZM32-SN (click to expand)

The same audit shows the Inovelli config entities get no startup read on models without an InovelliVzm3*Init virtual entity: InovelliDimmingModeEntity explicitly matches VZM36, and the unfiltered Inovelli entities match any device exposing 0xFC31 (e.g. VZM32-SN), but Init entities exist only for VZM30-SN/VZM31-SN/VZM35-SN. This is not a #657 regression — the old InovelliConfigEntityClusterHandler.__init__ also only had ZCL_INIT_ATTRS branches for those three models — so these cache-gated entities have plausibly never been created on freshly-paired VZM36/VZM32-SN devices. Worth a follow-up Init entity (or model additions) if those models should expose the config entities.

@zigpy-review-bot

zigpy-review-bot commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

As a follow-up to the review: I re-checked the original refactor (#657) — both its review threads and a fresh pass over the (non-snapshot) diff against the deleted cluster_handlers/ code — for issues that are still present on dev. Nothing below blocks this PR; it's a punch list for follow-ups. A second model (Copilot / GPT-5.6 Sol) did an independent retrospective pass; its two findings are folded in below (verified against the code before including).

Two more lost-in-translation regressions of the same kind as this PR (verified against e399b13a^, still unhandled on dev):

  • Philips SOC001 — cluster 0xFC06: the old PhillipsContactClusterHandler bound the cluster and configured immediate reporting for contact and tamper. No entity cluster config covers 0xFC06 today (the quirk's v2 tamper binary sensor declares no reporting either), so freshly-paired SOC001 sensors are left unbound with no reporting configured.
  • Legrand cable outlet — cluster 0xFC40: the old LegrandCableOutletClusterHandler bound the cluster (handler default BIND = True); nothing binds it today.

Update: the mechanical items from the list below are now fixed on zigpy-bot/657-followup-fixes (based on dev, independent of this PR) — 4 commits: the dual-side duplicate instantiation (with a regression test), the moot platform-override block (behavior-preserving dead-code removal), the unguarded find_attribute + the docstring, and the configure-only prospect leak (prospects torn down after reconfiguring an already-initialized device, with a regression test). Full suite passes (1339) and a GPT-5.6-Sol second-opinion pass over the branch returned no findings. Deliberately not fixed there: reporting-without-bind (a design decision — an assert or auto-bind both change semantics), the discovery+aggregation running in both configure and initialize (inherent — both phases need the data), and the VZM36/VZM32-SN gap (a product decision on which config entities those models should expose — Z2M exposes distinct per-endpoint _1/_2 attribute sets for the VZM36, so it isn't a mechanical port). The SOC001/Legrand restore above is the final commit on the dedup branch (next section). Happy to open PRs from either branch on request.

Still open from the original #657 review threads (verified still present on dev) — click to expand
  • (fixed on the branch) Duplicate entity instantiation when a cluster id is on both endpoint sidesdiscover_entities_for_endpoint() chains in_clusters and out_clusters and evaluates the registry per cluster object, so a cluster id present on both sides matches twice; the duplicate is constructed, on_add()-ed, and only discarded later in _add_pending_entities() (zha/application/discovery.py, the itertools.chain(...) loop).
  • (fixed on the branch) The first platform-override block in discover_entities_for_endpoint() is moot — the second pass rebuilds override_matches from all priorities of matches_by_priority and overwrites selected_matches, discarding the first block's priority filtering (and flattening priority scoring among override matches — probably not intended).
  • (fixed on the branch) Configure-vs-initialize lifecycle: async_configure() and async_initialize() each re-run _discover_new_entities() + aggregate_cluster_configs(); only async_initialize() drains _pending_entities. On a join both run and the duplicate prospects are cleaned up, but a configure-only flow leaves on_add()-subscribed prospects parked in _pending_entities — worth double-checking the listener lifecycle there.
  • (left as-is, needs a design decision) Reporting is configured independently of binding — in configure_cluster_configs() the bind and reporting blocks are decoupled; an entity config with reporting but no bind (none exists today) would silently configure reporting on an unbound cluster.
  • (fixed on the branch) Docstring nit: aggregate_cluster_configs() says the result is keyed by (endpoint_id, cluster_id) — the key is the 3-tuple (endpoint_id, cluster_id, is_server).

One new observation in the same area (✅ also fixed on the branch): configure_cluster_configs() resolves reporting attribute names via agg.cluster.find_attribute(attr_name) outside the surrounding try blocks. All current reporting configs on quirk-added attributes sit behind model/feature filters, but a matched device without the expected quirk (e.g. a custom quirk that drops an attribute) would raise KeyError and abort that device's configure pass. Wrapping the resolution like the read path would make it robust.

Everything else raised in the #657 threads was addressed before or shortly after merge (skip_configuration on the initialize path, _pick_primary_cluster determinism, the OnOffClientCacheSync timer leak, Lixee summation priority, reporting chunking / retries now living in zigpy 2.0.0's read_attributes/configure_reporting_multiple/Device.request).

Accidental config duplication from the #657 translation (the thread above) — click to expand

The refactor copied each old handler's full config block onto every entity that used the handler, so several single-attribute config entities carry unrelated state blocks:

  • Occupancy binary sensor, PIROccupiedToUnoccupiedDelayConfigurationEntity, and HueV1MotionSensitivity each carry the identical occupancy + pir_*_delay block (and PIRUnoccupiedToOccupiedDelayConfigurationEntity has no config of its own — it works only via its sibling's copy).
  • ThermostatLocalTempCalibration (+ Sonoff/Bosch subclasses) and DanfossExerciseDayOfTheWeek carry the entire 22-attribute thermostat block, including setpoint_change_source*.
  • StartupOnOffSelectEntity carries the on_off state block; OnOffTransitionTimeConfigurationEntity and BegaColorTemperatureChannelSelect carry the full LevelControl block; StartUpColorTemperatureConfigurationEntity carries the full Color state block (this one is left as the unfiltered host for now, since Light is device-type-filtered).

These are runtime-harmless (the aggregation merges identical copies) but hide gaps like the one this PR fixes. Not everything that looks duplicated is: the metering tier sensors inherit one shared block from SmartEnergyMetering (single definition), and IkeaFan hosts the STARKVIND block deliberately with a comment.

The dedup is pushed to zigpy-bot/cluster-config-dedup (based on this PR's branch; each entity declares only what it owns, verified by diffing the aggregated per-(cluster, attribute, reporting) view before/after — identical — plus a full test run), including the SOC001/Legrand restore above as the final commit. Happy to open it as a PR once this one lands.

Pre-existing gap (not a #657 regression): Inovelli VZM36 / VZM32-SN — click to expand

The Inovelli startup reads exist only for VZM30-SN/VZM31-SN/VZM35-SN (formerly the model branches in InovelliConfigEntityClusterHandler.__init__, now the InovelliVzm3*Init virtual entities). InovelliDimmingModeEntity matches VZM36, and the unfiltered Inovelli config entities match any device exposing 0xFC31 (e.g. VZM32-SN), but with no Init entity for those models their backing attributes are never read — so the cache-gated config entities have plausibly never been created on freshly-paired VZM36/VZM32-SN. Same behavior before and after #657; needs its own follow-up if those models should expose config entities.

@TheJulianJES
TheJulianJES marked this pull request as ready for review July 16, 2026 22:30
Copilot AI review requested due to automatic review settings July 16, 2026 22:30

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 encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

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 encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@TheJulianJES

Copy link
Copy Markdown
Contributor Author

The gaps mentioned above should be addressed, probably with something like the below branches:

In the future, these manufacturer-specific entities will be moved to zha-quirks anyway. This PR is only for a HA patch release, so it's a more minimal fix. We should also really expand diagnostics here for what we (try to) bind/init/report.

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

OccupancySensing.AttributeDefs.pir_u_to_o_delay: AttrConfig(
read_on_startup=False,
),
"sensitivity": AttrConfig(read_on_startup=False),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, planned to do basically.

@TheJulianJES
TheJulianJES merged commit f316b7f into dev Jul 17, 2026
10 of 12 checks passed
@TheJulianJES
TheJulianJES deleted the zigpy-bot/restore-startup-reads-657 branch July 17, 2026 01:41
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.

4 participants