From 638cb8708f51b915bef78307834ab1acd69e3c06 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 19 Jun 2025 14:21:12 -0400 Subject: [PATCH 1/2] Allow quirks v2 to specify primary entities --- zha/application/platforms/__init__.py | 10 ++++++++-- zha/zigbee/device.py | 28 +++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/zha/application/platforms/__init__.py b/zha/application/platforms/__init__.py index 805fcf5d2..8200a2cfe 100644 --- a/zha/application/platforms/__init__.py +++ b/zha/application/platforms/__init__.py @@ -123,7 +123,7 @@ class BaseEntity(LogMixin, EventBase): _attr_state_class: str | None = None _attr_enabled: bool = True _attr_always_supported: bool = False - _attr_primary: bool = False + _attr_primary: bool | None = None # When two entities both want to be primary, the one with the higher weight will be # chosen. If there is a tie, both lose. @@ -173,10 +173,13 @@ def enabled(self, value: bool) -> None: @property def primary(self) -> bool: """Return if the entity is the primary device control.""" + if self._attr_primary is None: + return False + return self._attr_primary @primary.setter - def primary(self, value: bool) -> None: + def primary(self, value: bool | None) -> None: """Set the entity as the primary device control.""" self._attr_primary = value @@ -404,6 +407,9 @@ def _init_from_quirks_metadata(self, entity_metadata: EntityMetadata) -> None: else: self._attr_entity_category = None + if entity_metadata.primary is not None: + self._attr_primary = entity_metadata.primary + @cached_property def identifiers(self) -> PlatformEntityIdentifiers: """Return a dict with the information necessary to identify this entity.""" diff --git a/zha/zigbee/device.py b/zha/zigbee/device.py index 38264d337..c29421ffc 100644 --- a/zha/zigbee/device.py +++ b/zha/zigbee/device.py @@ -1207,11 +1207,35 @@ def log(self, level: int, msg: str, *args: Any, **kwargs: Any) -> None: def _compute_primary_entity(self) -> None: """Compute the primary entity for this device.""" - # Only consider non-counter entities + # First, check if any entity is explicitly primary + explicitly_primary = [ + entity for entity in self._platform_entities.values() if entity.primary + ] + + if len(explicitly_primary) == 1: + self.debug( + "Device has a single explicitly primary entity," + " not performing weight matching" + ) + return + elif len(explicitly_primary) > 1: + self.warning( + "Device has multiple explicitly primary entities, this is a bug" + ) + + # If there is a collision, nobody wins + for entity in explicitly_primary: + entity.primary = False + del entity.info_object + + return + + # For weight matching, only consider non-counter entities and entities which are + # not explicitly marked as not primary candidates = [ e for e in self._platform_entities.values() - if e.enabled and hasattr(e, "info_object") + if e.enabled and hasattr(e, "info_object") and e._attr_primary is not False ] candidates.sort(reverse=True, key=lambda e: e.primary_weight) From e4678d59ecb0b572ffaeff1bd61f33d4e499cba6 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 19 Jun 2025 14:27:28 -0400 Subject: [PATCH 2/2] Add a unit test --- tests/test_device.py | 30 ++++++++++++++++++++++++++++++ zha/zigbee/device.py | 12 ++---------- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/tests/test_device.py b/tests/test_device.py index 398579512..765e8b6f6 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -13,6 +13,7 @@ from zigpy.quirks.v2 import DeviceAlertLevel, DeviceAlertMetadata, QuirkBuilder import zigpy.types from zigpy.zcl.clusters import general +from zigpy.zcl.clusters.general import PowerConfiguration from zigpy.zcl.foundation import Status, WriteAttributesResponse import zigpy.zdo.types as zdo_t @@ -895,6 +896,35 @@ async def test_primary_entity_computation( ] +async def test_quirks_v2_primary_entity(zha_gateway: Gateway) -> None: + """Test quirks v2 primary entity.""" + registry = DeviceRegistry() + + ( + QuirkBuilder("CentraLite", "3405-L", registry=registry) + .sensor( + attribute_name=PowerConfiguration.AttributeDefs.battery_quantity.id, + cluster_id=PowerConfiguration.cluster_id, + translation_key="battery_quantity", + fallback_name="Battery quantity", + primary=True, + ) + .add_to_registry() + ) + + zigpy_dev = registry.get_device( + await zigpy_device_from_json( + zha_gateway.application_controller, + "tests/data/devices/centralite-3405-l.json", + ) + ) + + zha_device = await join_zigpy_device(zha_gateway, zigpy_dev) + + (primary,) = [e for e in zha_device.platform_entities.values() if e.primary] + assert primary.translation_key == "battery_quantity" + + async def test_quirks_v2_prevent_default_entities(zha_gateway: Gateway) -> None: """Test quirks v2 can prevent creating default entities.""" registry = DeviceRegistry() diff --git a/zha/zigbee/device.py b/zha/zigbee/device.py index c29421ffc..db190f730 100644 --- a/zha/zigbee/device.py +++ b/zha/zigbee/device.py @@ -1218,17 +1218,9 @@ def _compute_primary_entity(self) -> None: " not performing weight matching" ) return - elif len(explicitly_primary) > 1: - self.warning( - "Device has multiple explicitly primary entities, this is a bug" - ) - - # If there is a collision, nobody wins - for entity in explicitly_primary: - entity.primary = False - del entity.info_object - return + # It should not be possible for there to be more than one + assert not explicitly_primary # For weight matching, only consider non-counter entities and entities which are # not explicitly marked as not primary