diff --git a/tests/test_sensor.py b/tests/test_sensor.py index ac1a0701..76d0c066 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -87,6 +87,7 @@ async def async_test_temperature( zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity ) -> None: """Test temperature sensor.""" + assert entity.extra_state_attribute_names is None await send_attributes_report(zha_gateway, cluster, {1: 1, 0: 2900, 2: 100}) assert_state(entity, 29.0, "°C") @@ -117,6 +118,11 @@ async def async_test_metering( zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity ) -> None: """Test Smart Energy metering sensor.""" + assert entity.extra_state_attribute_names == { + "status", + "device_type", + "zcl_unit_of_measurement", + } await send_attributes_report( zha_gateway, cluster, {1025: 1, 1024: 12345, 1026: 100} ) @@ -166,7 +172,11 @@ async def async_test_smart_energy_summation_delivered( zha_gateway: Gateway, cluster, entity ): """Test SmartEnergy Summation delivered sensor.""" - + assert entity.extra_state_attribute_names == { + "status", + "device_type", + "zcl_unit_of_measurement", + } await send_attributes_report( zha_gateway, cluster, {1025: 1, "current_summ_delivered": 12321, 1026: 100} ) @@ -316,6 +326,12 @@ async def async_test_powerconfiguration( zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity ) -> None: """Test powerconfiguration/battery sensor.""" + assert entity.extra_state_attribute_names == { + "battery_voltage", + "battery_quantity", + "battery_size", + "battery_voltage", + } await send_attributes_report(zha_gateway, cluster, {33: 98}) assert_state(entity, 49, "%") assert entity.state["battery_voltage"] == 2.9 diff --git a/zha/application/platforms/__init__.py b/zha/application/platforms/__init__.py index 9b31c83b..acc6d5ef 100644 --- a/zha/application/platforms/__init__.py +++ b/zha/application/platforms/__init__.py @@ -213,6 +213,17 @@ def state(self) -> dict[str, Any]: "class_name": self.__class__.__name__, } + @cached_property + def extra_state_attribute_names(self) -> set[str] | None: + """Return entity specific state attribute names. + + Implemented by platform classes. Convention for attribute names + is lowercase snake_case. + """ + if hasattr(self, "_attr_extra_state_attribute_names"): + return self._attr_extra_state_attribute_names + return None + async def on_remove(self) -> None: """Cancel tasks and timers this entity owns.""" for handle in self._tracked_handles: diff --git a/zha/application/platforms/climate/__init__.py b/zha/application/platforms/climate/__init__.py index 0c361b0c..0125c089 100644 --- a/zha/application/platforms/climate/__init__.py +++ b/zha/application/platforms/climate/__init__.py @@ -83,6 +83,16 @@ class Thermostat(PlatformEntity): _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_translation_key: str = "thermostat" _enable_turn_on_off_backwards_compatibility = False + _attr_extra_state_attribute_names: set[str] = { + ATTR_SYS_MODE, + ATTR_OCCUPANCY, + ATTR_OCCP_COOL_SETPT, + ATTR_OCCP_HEAT_SETPT, + ATTR_PI_HEATING_DEMAND, + ATTR_PI_COOLING_DEMAND, + ATTR_UNOCCP_COOL_SETPT, + ATTR_UNOCCP_HEAT_SETPT, + } def __init__( self, diff --git a/zha/application/platforms/light/__init__.py b/zha/application/platforms/light/__init__.py index 55fe61c7..7b7d37eb 100644 --- a/zha/application/platforms/light/__init__.py +++ b/zha/application/platforms/light/__init__.py @@ -117,6 +117,10 @@ class BaseLight(BaseEntity, ABC): PLATFORM = Platform.LIGHT _FORCE_ON = False _DEFAULT_MIN_TRANSITION_TIME: float = 0 + _attr_extra_state_attribute_names: set[str] = { + "off_with_transition", + "off_brightness", + } def __init__(self, *args, **kwargs): """Initialize the light.""" diff --git a/zha/application/platforms/sensor/__init__.py b/zha/application/platforms/sensor/__init__.py index dd9ea3e6..e5b126c7 100644 --- a/zha/application/platforms/sensor/__init__.py +++ b/zha/application/platforms/sensor/__init__.py @@ -250,7 +250,11 @@ def handle_cluster_handler_attribute_updated( event: ClusterAttributeUpdatedEvent, # pylint: disable=unused-argument ) -> None: """Handle attribute updates from the cluster handler.""" - if event.attribute_name == self._attribute_name: + if event.attribute_name == self._attribute_name or ( + hasattr(self, "_attr_extra_state_attribute_names") + and event.attribute_name + in getattr(self, "_attr_extra_state_attribute_names") + ): self.maybe_emit_state_changed_event() def formatter(self, value: int | enum.IntEnum) -> int | float | str | None: @@ -489,6 +493,11 @@ class Battery(Sensor): _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_native_unit_of_measurement = PERCENTAGE + _attr_extra_state_attribute_names: set[str] = { + "battery_size", + "battery_quantity", + "battery_voltage", + } @classmethod def create_platform_entity( @@ -549,6 +558,21 @@ class ElectricalMeasurement(PollableSensor): _attr_native_unit_of_measurement: str = UnitOfPower.WATT _div_mul_prefix: str | None = "ac_power" + def __init__( + self, + unique_id: str, + cluster_handlers: list[ClusterHandler], + endpoint: Endpoint, + device: Device, + **kwargs: Any, + ) -> None: + """Init this sensor.""" + super().__init__(unique_id, cluster_handlers, endpoint, device, **kwargs) + self._attr_extra_state_attribute_names: set[str] = { + "measurement_type", + f"{self._attribute_name}_max", + } + @property def state(self) -> dict[str, Any]: """Return the state for this sensor.""" @@ -734,6 +758,11 @@ class SmartEnergyMetering(PollableSensor): _use_custom_polling: bool = False _attribute_name = "instantaneous_demand" _attr_translation_key: str = "instantaneous_demand" + _attr_extra_state_attribute_names: set[str] = { + "device_type", + "status", + "zcl_unit_of_measurement", + } _ENTITY_DESCRIPTION_MAP = { 0x00: SmartEnergyMeteringEntityDescription(