Skip to content

Conversation

@puddly
Copy link
Contributor

@puddly puddly commented Jan 6, 2026

As part of a medium-term goal of getting rid of cluster handlers and eventually rewriting entity classes themselves to request attribute reporting config/binding/attribute values, I think a first stepping stone would be to move away from implicit entity registration decorators and have entity objects themselves decide what cluster handlers they want (or if they are not applicable).

Instead of this:

@STRICT_MATCH(
    cluster_handler_names=CLUSTER_HANDLER_ON_OFF,
    aux_cluster_handlers={CLUSTER_HANDLER_COLOR, CLUSTER_HANDLER_LEVEL},
)
class Light(BaseClusterHandlerLight, PlatformEntity):
    ...

We do this:

@register_entity
class Light(BaseClusterHandlerLight, PlatformEntity):
    ...
    _cluster_handler_match = ClusterHandlerMatch(
        cluster_handlers=frozenset({CLUSTER_HANDLER_ON_OFF}),
        optional_cluster_handlers=frozenset(
            {CLUSTER_HANDLER_COLOR, CLUSTER_HANDLER_LEVEL}
        ),
        profile_device_types=LIGHT_PROFILE_DEVICE_TYPES,
        feature_priority=(PlatformFeatureGroup.LIGHT_OR_SWITCH_OR_SHADE, 0),
    )

There is no more complexity in ClusterHandlerMatch. It lists what cluster handlers are required, what cluster handlers are optional, a bit of model/manufacturer filtering, and feature_priority for exclusivity within a group of overlapping clusters. That's it.

This has a few benefits:

  1. It moves unique ID calculation logic into the platform base entity object and makes it 100% explicit, allowing us to more easily migrate.
  2. Allows us to fine-tune cluster handler matching per-entity without adding more matching rules.
  3. Will allow us to move cluster handler ZCL attribute reporting and binding config into the ZCL entity objects themselves, allowing ZHA to granularly merge binding/reporting config when setting up a device.

@puddly
Copy link
Contributor Author

puddly commented Jan 23, 2026

I'm successfully running this branch locally, as a final test. No entity issues to report.

@puddly puddly changed the title [RFC] Move from centralized discovery to entity-driven discovery Move from centralized discovery to entity-driven discovery Jan 26, 2026
@puddly puddly marked this pull request as ready for review January 26, 2026 20:27
Comment on lines 378 to 387
# if the cluster handler is unclaimed, claim it and set BIND accordingly,
# so ZHA configures the cluster handler: reporting + reads attributes
if attribute_initialization_found or reporting_found:
endpoint.claim_cluster_handlers([cluster_handler])
# BIND is True by default, so only set to False if no reporting found.
# We can safely do this, since quirks v2 entities are initialized last,
# so if the cluster handler wasn't claimed by endpoint probing so far,
# only v2 entities need it.
if not reporting_found:
cluster_handler.BIND = False
Copy link
Contributor

@TheJulianJES TheJulianJES Jan 27, 2026

Choose a reason for hiding this comment

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

Hmm, so previously, we only executed this code for unclaimed cluster handlers (as still in the comment).
I think the main reason for doing so was to only run the cluster_handler.BIND = False if ZHA doesn't already use it.
(Running endpoint.claim_cluster_handlers([cluster_handler]) multiple times should be fine though?)

Now, if ZHA already claimed a cluster handler (for one of its entities) to set up both binding and attribute reporting, and we have a v2 quirks entity that's using an attribute from that cluster (for initialization, but not for attribute reporting), we'll set BIND = False here, but the ZHA cluster handler likely wants that cluster to be bound. So, we cause some issues.


This all needs to be redone anyway when init + report config is moving to entities (and I guess something like request_bind to only bind a cluster if at least one entity is discovered and requires it to be bound?).

BIND = True in the base ClusterHandler. It's only selectively disabled in some other cluster handlers (so those are never bound). But for most cluster handlers, the cluster would only be bound (at least before this PR) if it's a claimed cluster handler, e.g. used/claimed by at least one entity.

So, with the code above, we checked if it was an unclaimed cluster handler, so we knew that no ZHA entity relied on it at all (/for binding), letting us safely set BIND = False then, as claiming that cluster handler above would have bound the cluster handler, otherwise (even though we just want to claim it here for ZCL_INIT_ATTRS). But if no reporting is configured by v2 quirks, we can safely disable binding completely for that cluster handler.

With the changes from this PR, we will disable binding for all cluster handlers that would otherwise be bound by ZHA if there's just one quirks v2 entity on that same cluster (with it not having attribute reporting configured, but possibly still with the base cluster handler doing so).
So, I think there's a bug here? Can we just keep the unclaimed cluster handler check here, for now, just so we know that no ZHA entity relies on that cluster handler (being bound)?

.. Does my explanation make sense? 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah. I just changed this a few hours ago. I'll revert the change. We apparently don't have test coverage for this specific scenario 😅

Copy link
Contributor

Choose a reason for hiding this comment

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

Oops, you're right. Should have been done in #548 😅
I can look into adding a test for that tomorrow-ish, but hopefully we'll all make this better soon, anyway.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR replaces the legacy, registry-based entity discovery system with an explicit, entity-centric discovery model driven by per-entity ClusterHandlerMatch declarations. It also tightens unique ID generation and device-type scoping across multiple platforms to support a future move toward entity-driven reporting/binding configuration.

Changes:

  • Introduces a new entity discovery pipeline (ENTITY_REGISTRY, GROUP_ENTITY_REGISTRY, ClusterHandlerMatch, and discover_entities_for_endpoint) and removes the old zha.application.registries matching system.
  • Migrates platform entities (light, switch, fan, cover, climate, siren, lock, alarm_control_panel, select, button, binary_sensor, update, device_tracker) to the new @register_entity / @register_group_entity decorators with explicit _cluster_handler_match definitions and updated unique ID logic.
  • Updates discovery paths for devices and groups (including coordinator-specific entities) and aligns tests and JSON fixtures with the new discovery model and client/server cluster-handler split.

Reviewed changes

Copilot reviewed 76 out of 76 changed files in this pull request and generated no comments.

Show a summary per file
File Description
zha/zigbee/endpoint.py Adds cached cluster_handlers_by_name and client_cluster_handlers_by_name maps and removes unclaimed_cluster_handlers, enabling name-based matching for entity discovery.
zha/zigbee/device.py Updates neighbor typing to zigpy.zdo.types.Neighbor.* and switches device entity discovery to discovery.discover_device_entities / discover_coordinator_device_entities, with a new is_coordinator branch that suppresses non-active coordinator entities.
zha/zigbee/cluster_handlers/security.py Converts IAS ACE support to a client-side IasAceClientClusterHandler registered in CLIENT_CLUSTER_HANDLER_REGISTRY, aligning it with the new client-cluster handling model.
zha/application/registries.py Removes the old PlatformEntityRegistry, MatchRule, and platform/group registry data structures now superseded by ClusterHandlerMatch and the central entity registries.
zha/application/platforms/update.py Replaces CONFIG_DIAGNOSTIC_MATCH with @register_entity(Ota.cluster_id) for OTA update entities, adds _cluster_handler_match based on CLUSTER_HANDLER_OTA, and differentiates client vs. server OTA use while keeping version/progress logic intact.
zha/application/platforms/switch.py Migrates all switch entities to @register_entity / @register_group_entity, defines detailed _cluster_handler_match rules (including Tuya/Aqara/Inovelli/Sinope special cases), and centralizes legacy discovery unique ID selection (including profile/device-type-specific suffixes).
zha/application/platforms/siren.py Changes siren discovery from multipass match to @register_entity(IasWd.cluster_id) + _cluster_handler_match, adds profile-aware legacy_discovery_unique_id computation, and switches to IasWd references directly.
zha/application/platforms/select.py Refactors all select entities to use @register_entity and _cluster_handler_match (for IAS WD, OnOff, Tuya manufacturer, Aqara Opple, Hue occupancy, Thermostat, Danfoss, Sinope, etc.), and adds a separate Tuya-manufacturer power-on-state entity to split server vs. manufacturer-specific cases.
zha/application/platforms/lock/__init__.py Converts DoorLock to @register_entity(DoorLockCluster.cluster_id) with _cluster_handler_match on CLUSTER_HANDLER_DOORLOCK and reduces primary weight for better prioritization among entities.
zha/application/platforms/light/const.py Adds LIGHT_PROFILE_DEVICE_TYPES as a shared set of (profile, device_type) tuples for standard light device types across ZHA and ZLL profiles.
zha/application/platforms/light/__init__.py Replaces strict/group match with entity registration, introduces _cluster_handler_match (including optional color/level handlers, manufacturer filters, and PlatformFeatureGroup.LIGHT_OR_SWITCH_OR_SHADE priorities), and centralizes light unique IDs per endpoint/device-type.
zha/application/platforms/fan/__init__.py Registers fan and fan group entities with the new decorators, introduces PlatformFeatureGroup.THERMOSTAT_FAN priority to let thermostat entities win over fan-only entities, and adds model-specific matching for IKEA air purifiers and King of Fans.
zha/application/platforms/device_tracker.py Moves SmartThings arrival tracker registration to @register_entity(PowerConfiguration.cluster_id) with a _cluster_handler_match constrained by a fake profile/device-type tuple, and standardizes its legacy unique ID to <ieee>-<endpoint>.
zha/application/platforms/cover/__init__.py Converts Cover and Shade entities to @register_entity with ClusterHandlerMatch (including OnOff/Level/SHADE optional handlers and LIGHT_OR_SWITCH_OR_SHADE grouping), adds profile/device-type filtering for shades, and makes cover capabilities conditional on Level cluster presence.
zha/application/platforms/climate/__init__.py Registers Thermostat variants via @register_entity(ThermostatCluster.cluster_id), defines precise _cluster_handler_match (including optional fan handlers and manufacturer/model filters) and uses PlatformFeatureGroup.THERMOSTAT_FAN so thermostats take precedence over fans; also revises thermostat unique ID construction using profile/device-type.
zha/application/platforms/button/__init__.py Switches all button entities to @register_entity with _cluster_handler_match for Identify, Tuya manufacturer, and Aqara Opple clusters, eliminating use of MULTI_MATCH and CONFIG_DIAGNOSTIC_MATCH.
zha/application/platforms/binary_sensor/__init__.py Refactors binary sensors to the new registration model, introduces a PlatformFeatureGroup.BINARY_SENSOR group for OnOff client clusters (with generic Opening as fallback and higher-priority IKEA/Philips motion classes), and adds explicit matches for IAS Zone, Tuya, Aqara, Danfoss, and other manufacturer-/model-specific sensors.
zha/application/platforms/alarm_control_panel/__init__.py Registers AlarmControlPanel on IasAce.cluster_id with a client-cluster _cluster_handler_match (CLUSTER_HANDLER_IAS_ACE), updates it to use IasAceClientClusterHandler, and sets a stable unique ID incorporating the IAS ACE cluster ID.
zha/application/platforms/__init__.py Introduces ENTITY_REGISTRY / GROUP_ENTITY_REGISTRY, PlatformFeatureGroup, ClusterHandlerMatch, and the register_entity / register_group_entity decorators, and updates PlatformEntity to accept an optional legacy_discovery_unique_id with a consistent fallback based on device/endpoint/cluster.
zha/application/gateway.py Updates group entity creation to call discovery.discover_group_entities instead of the removed GROUP_PROBE and uses the new discovery API when groups change.
zha/application/discovery.py Replaces DeviceProbe/EndpointProbe/GroupProbe with top-level discover_device_entities, discover_entities_for_endpoint, discover_coordinator_device_entities, and discover_group_entities, implementing the new match/prioritization algorithm over ENTITY_REGISTRY and ClusterHandlerMatch (including feature-group-based tie-breaking).
tests/test_sensor.py Updates tests that patched the old endpoint probe to patch discover_entities_for_endpoint and adapts attribute-reporting/reading tests to the new discovery path.
tests/test_registries.py Removes tests for the old PlatformEntityRegistry, MatchRule, quirk ID validation, and entity-name checks now superseded by the new discovery model.
tests/test_discover.py Adapts discovery tests to the new APIs, adds explicit tests for device overrides using persisted JSON devices, and keeps quirks v2 discovery tests valid under the new system.
tests/test_device_tracker.py Adjusts imports and constants to reflect SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE’s new location in platforms.device_tracker.
tests/test_device.py Updates expectation counts for platform entities on quirks v2 devices to account for the new discovery behavior.
tests/test_cluster_handlers.py Removes tests around unclaimed_cluster_handlers and endpoint-level discovery that depended on the old registry logic.
tests/test_button.py Updates quirks v2 button tests to patch discover_entities_for_endpoint instead of the removed endpoint probe.
tests/test_alarm_control_panel.py Converts alarm control panel tests to use a JSON fixture device and the new client-side IAS ACE handler and unique ID format.
tests/data/devices/*.json Updates multiple device snapshots to reflect new client-cluster handler classes (e.g., OnOffClientClusterHandler, IasAceClientClusterHandler), removed server OTA handlers where appropriate, new entity classes, and updated unique IDs/statuses aligned with the new discovery system.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

@TheJulianJES TheJulianJES left a comment

Choose a reason for hiding this comment

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

I've not spotted any (obvious) issues either, so here we go 😄

@puddly puddly merged commit 4ae47ac into zigpy:dev Jan 28, 2026
15 checks passed
_unique_id_suffix = "firmware_update"
_cluster_handler_match = ClusterHandlerMatch(
cluster_handlers=frozenset({CLUSTER_HANDLER_OTA})
)
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks strange @puddly. This entity used to match to the server side cluster handler. Now it is identical to the one above. So should match to same.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There are cluster_handlers and client_cluster_handlers. They're registered by name so the names will be the same for both cluster handlers but they now match correctly.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh. I missed that detail. Then it make sense. Looking at rebasing my event series.

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.

3 participants