diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index 195b0a1dcf5..6ffb6d6f909 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -3,8 +3,9 @@ from __future__ import annotations import functools +import logging -from zigpy.quirks.v2 import BinarySensorMetadata, EntityMetadata +from zigpy.quirks.v2 import BinarySensorMetadata import zigpy.types as t from zigpy.zcl.clusters.general import OnOff from zigpy.zcl.clusters.security import IasZone @@ -27,11 +28,11 @@ from .core.const import ( CLUSTER_HANDLER_OCCUPANCY, CLUSTER_HANDLER_ON_OFF, CLUSTER_HANDLER_ZONE, - QUIRK_METADATA, + ENTITY_METADATA, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, ) -from .core.helpers import get_zha_data +from .core.helpers import get_zha_data, validate_device_class from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity @@ -51,6 +52,8 @@ CONFIG_DIAGNOSTIC_MATCH = functools.partial( ZHA_ENTITIES.config_diagnostic_match, Platform.BINARY_SENSOR ) +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry( hass: HomeAssistant, @@ -79,15 +82,21 @@ class BinarySensor(ZhaEntity, BinarySensorEntity): def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs) -> None: """Initialize the ZHA binary sensor.""" self._cluster_handler = cluster_handlers[0] - if QUIRK_METADATA in kwargs: - self._init_from_quirks_metadata(kwargs[QUIRK_METADATA]) + if ENTITY_METADATA in kwargs: + self._init_from_quirks_metadata(kwargs[ENTITY_METADATA]) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - def _init_from_quirks_metadata(self, entity_metadata: EntityMetadata) -> None: + def _init_from_quirks_metadata(self, entity_metadata: BinarySensorMetadata) -> None: """Init this entity from the quirks metadata.""" super()._init_from_quirks_metadata(entity_metadata) - binary_sensor_metadata: BinarySensorMetadata = entity_metadata.entity_metadata - self._attribute_name = binary_sensor_metadata.attribute_name + self._attribute_name = entity_metadata.attribute_name + if entity_metadata.device_class is not None: + self._attr_device_class = validate_device_class( + BinarySensorDeviceClass, + entity_metadata.device_class, + Platform.BINARY_SENSOR.value, + _LOGGER, + ) async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" diff --git a/homeassistant/components/zha/button.py b/homeassistant/components/zha/button.py index 48b27ee6892..33102062443 100644 --- a/homeassistant/components/zha/button.py +++ b/homeassistant/components/zha/button.py @@ -6,11 +6,7 @@ import functools import logging from typing import TYPE_CHECKING, Any, Self -from zigpy.quirks.v2 import ( - EntityMetadata, - WriteAttributeButtonMetadata, - ZCLCommandButtonMetadata, -) +from zigpy.quirks.v2 import WriteAttributeButtonMetadata, ZCLCommandButtonMetadata from homeassistant.components.button import ButtonDeviceClass, ButtonEntity from homeassistant.config_entries import ConfigEntry @@ -20,7 +16,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .core import discovery -from .core.const import CLUSTER_HANDLER_IDENTIFY, QUIRK_METADATA, SIGNAL_ADD_ENTITIES +from .core.const import CLUSTER_HANDLER_IDENTIFY, ENTITY_METADATA, SIGNAL_ADD_ENTITIES from .core.helpers import get_zha_data from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity @@ -76,17 +72,18 @@ class ZHAButton(ZhaEntity, ButtonEntity): ) -> None: """Init this button.""" self._cluster_handler: ClusterHandler = cluster_handlers[0] - if QUIRK_METADATA in kwargs: - self._init_from_quirks_metadata(kwargs[QUIRK_METADATA]) + if ENTITY_METADATA in kwargs: + self._init_from_quirks_metadata(kwargs[ENTITY_METADATA]) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - def _init_from_quirks_metadata(self, entity_metadata: EntityMetadata) -> None: + def _init_from_quirks_metadata( + self, entity_metadata: ZCLCommandButtonMetadata + ) -> None: """Init this entity from the quirks metadata.""" super()._init_from_quirks_metadata(entity_metadata) - button_metadata: ZCLCommandButtonMetadata = entity_metadata.entity_metadata - self._command_name = button_metadata.command_name - self._args = button_metadata.args - self._kwargs = button_metadata.kwargs + self._command_name = entity_metadata.command_name + self._args = entity_metadata.args + self._kwargs = entity_metadata.kwargs def get_args(self) -> list[Any]: """Return the arguments to use in the command.""" @@ -148,16 +145,17 @@ class ZHAAttributeButton(ZhaEntity, ButtonEntity): ) -> None: """Init this button.""" self._cluster_handler: ClusterHandler = cluster_handlers[0] - if QUIRK_METADATA in kwargs: - self._init_from_quirks_metadata(kwargs[QUIRK_METADATA]) + if ENTITY_METADATA in kwargs: + self._init_from_quirks_metadata(kwargs[ENTITY_METADATA]) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - def _init_from_quirks_metadata(self, entity_metadata: EntityMetadata) -> None: + def _init_from_quirks_metadata( + self, entity_metadata: WriteAttributeButtonMetadata + ) -> None: """Init this entity from the quirks metadata.""" super()._init_from_quirks_metadata(entity_metadata) - button_metadata: WriteAttributeButtonMetadata = entity_metadata.entity_metadata - self._attribute_name = button_metadata.attribute_name - self._attribute_value = button_metadata.attribute_value + self._attribute_name = entity_metadata.attribute_name + self._attribute_value = entity_metadata.attribute_value async def async_press(self) -> None: """Write attribute with defined value.""" diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 8d56652d8ee..74110d390ed 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -220,6 +220,8 @@ DISCOVERY_KEY = "zha_discovery_info" DOMAIN = "zha" +ENTITY_METADATA = "entity_metadata" + GROUP_ID = "group_id" GROUP_IDS = "group_ids" GROUP_NAME = "group_name" @@ -233,8 +235,6 @@ PRESET_SCHEDULE = "Schedule" PRESET_COMPLEX = "Complex" PRESET_TEMP_MANUAL = "Temporary manual" -QUIRK_METADATA = "quirk_metadata" - ZCL_INIT_ATTRS = "ZCL_INIT_ATTRS" ZHA_ALARM_OPTIONS = "zha_alarm_options" diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 75e5b51e599..3c342d14060 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -85,12 +85,18 @@ QUIRKS_ENTITY_META_TO_ENTITY_CLASS = { WriteAttributeButtonMetadata, EntityType.CONFIG, ): button.ZHAAttributeButton, + ( + Platform.BUTTON, + WriteAttributeButtonMetadata, + EntityType.STANDARD, + ): button.ZHAAttributeButton, (Platform.BUTTON, ZCLCommandButtonMetadata, EntityType.CONFIG): button.ZHAButton, ( Platform.BUTTON, ZCLCommandButtonMetadata, EntityType.DIAGNOSTIC, ): button.ZHAButton, + (Platform.BUTTON, ZCLCommandButtonMetadata, EntityType.STANDARD): button.ZHAButton, ( Platform.BINARY_SENSOR, BinarySensorMetadata, @@ -111,6 +117,7 @@ QUIRKS_ENTITY_META_TO_ENTITY_CLASS = { (Platform.SENSOR, ZCLSensorMetadata, EntityType.DIAGNOSTIC): sensor.Sensor, (Platform.SENSOR, ZCLSensorMetadata, EntityType.STANDARD): sensor.Sensor, (Platform.SELECT, ZCLEnumMetadata, EntityType.CONFIG): select.ZCLEnumSelectEntity, + (Platform.SELECT, ZCLEnumMetadata, EntityType.STANDARD): select.ZCLEnumSelectEntity, ( Platform.SELECT, ZCLEnumMetadata, @@ -224,7 +231,7 @@ class ProbeEndpoint: for ( cluster_details, - quirk_metadata_list, + entity_metadata_list, ) in zigpy_device.exposes_metadata.items(): endpoint_id, cluster_id, cluster_type = cluster_details @@ -265,11 +272,11 @@ class ProbeEndpoint: ) assert cluster_handler - for quirk_metadata in quirk_metadata_list: - platform = Platform(quirk_metadata.entity_platform.value) - metadata_type = type(quirk_metadata.entity_metadata) + for entity_metadata in entity_metadata_list: + platform = Platform(entity_metadata.entity_platform.value) + metadata_type = type(entity_metadata) entity_class = QUIRKS_ENTITY_META_TO_ENTITY_CLASS.get( - (platform, metadata_type, quirk_metadata.entity_type) + (platform, metadata_type, entity_metadata.entity_type) ) if entity_class is None: @@ -280,7 +287,7 @@ class ProbeEndpoint: device.name, { zha_const.CLUSTER_DETAILS: cluster_details, - zha_const.QUIRK_METADATA: quirk_metadata, + zha_const.ENTITY_METADATA: entity_metadata, }, ) continue @@ -288,13 +295,13 @@ class ProbeEndpoint: # automatically add the attribute to ZCL_INIT_ATTRS for the cluster # handler if it is not already in the list if ( - hasattr(quirk_metadata.entity_metadata, "attribute_name") - and quirk_metadata.entity_metadata.attribute_name + hasattr(entity_metadata, "attribute_name") + and entity_metadata.attribute_name not in cluster_handler.ZCL_INIT_ATTRS ): init_attrs = cluster_handler.ZCL_INIT_ATTRS.copy() - init_attrs[quirk_metadata.entity_metadata.attribute_name] = ( - quirk_metadata.attribute_initialized_from_cache + init_attrs[entity_metadata.attribute_name] = ( + entity_metadata.attribute_initialized_from_cache ) cluster_handler.__dict__[zha_const.ZCL_INIT_ATTRS] = init_attrs @@ -303,7 +310,7 @@ class ProbeEndpoint: entity_class, endpoint.unique_id, [cluster_handler], - quirk_metadata=quirk_metadata, + entity_metadata=entity_metadata, ) _LOGGER.debug( diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 22efe995954..1a001cab381 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -14,7 +14,7 @@ from dataclasses import dataclass import enum import logging import re -from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar +from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar, overload import voluptuous as vol import zigpy.exceptions @@ -24,8 +24,33 @@ import zigpy.zcl from zigpy.zcl.foundation import CommandSchema import zigpy.zdo.types as zdo_types +from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.components.number import NumberDeviceClass +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.const import ( + Platform, + UnitOfApparentPower, + UnitOfDataRate, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfFrequency, + UnitOfInformation, + UnitOfIrradiance, + UnitOfLength, + UnitOfMass, + UnitOfPower, + UnitOfPrecipitationDepth, + UnitOfPressure, + UnitOfSoundPressure, + UnitOfSpeed, + UnitOfTemperature, + UnitOfTime, + UnitOfVolume, + UnitOfVolumeFlowRate, + UnitOfVolumetricFlux, +) from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.typing import ConfigType @@ -218,7 +243,7 @@ def async_get_zha_config_value( ) -def async_cluster_exists(hass, cluster_id, skip_coordinator=True): +def async_cluster_exists(hass: HomeAssistant, cluster_id, skip_coordinator=True): """Determine if a device containing the specified in cluster is paired.""" zha_gateway = get_zha_gateway(hass) zha_devices = zha_gateway.devices.values() @@ -424,3 +449,80 @@ def get_zha_gateway(hass: HomeAssistant) -> ZHAGateway: raise ValueError("No gateway object exists") return zha_gateway + + +UNITS_OF_MEASURE = { + UnitOfApparentPower.__name__: UnitOfApparentPower, + UnitOfPower.__name__: UnitOfPower, + UnitOfEnergy.__name__: UnitOfEnergy, + UnitOfElectricCurrent.__name__: UnitOfElectricCurrent, + UnitOfElectricPotential.__name__: UnitOfElectricPotential, + UnitOfTemperature.__name__: UnitOfTemperature, + UnitOfTime.__name__: UnitOfTime, + UnitOfLength.__name__: UnitOfLength, + UnitOfFrequency.__name__: UnitOfFrequency, + UnitOfPressure.__name__: UnitOfPressure, + UnitOfSoundPressure.__name__: UnitOfSoundPressure, + UnitOfVolume.__name__: UnitOfVolume, + UnitOfVolumeFlowRate.__name__: UnitOfVolumeFlowRate, + UnitOfMass.__name__: UnitOfMass, + UnitOfIrradiance.__name__: UnitOfIrradiance, + UnitOfVolumetricFlux.__name__: UnitOfVolumetricFlux, + UnitOfPrecipitationDepth.__name__: UnitOfPrecipitationDepth, + UnitOfSpeed.__name__: UnitOfSpeed, + UnitOfInformation.__name__: UnitOfInformation, + UnitOfDataRate.__name__: UnitOfDataRate, +} + + +def validate_unit(quirks_unit: enum.Enum) -> enum.Enum: + """Validate and return a unit of measure.""" + return UNITS_OF_MEASURE[type(quirks_unit).__name__](quirks_unit.value) + + +@overload +def validate_device_class( + device_class_enum: type[BinarySensorDeviceClass], + metadata_value, + platform: str, + logger: logging.Logger, +) -> BinarySensorDeviceClass | None: ... + + +@overload +def validate_device_class( + device_class_enum: type[SensorDeviceClass], + metadata_value, + platform: str, + logger: logging.Logger, +) -> SensorDeviceClass | None: ... + + +@overload +def validate_device_class( + device_class_enum: type[NumberDeviceClass], + metadata_value, + platform: str, + logger: logging.Logger, +) -> NumberDeviceClass | None: ... + + +def validate_device_class( + device_class_enum: type[BinarySensorDeviceClass] + | type[SensorDeviceClass] + | type[NumberDeviceClass], + metadata_value: enum.Enum, + platform: str, + logger: logging.Logger, +) -> BinarySensorDeviceClass | SensorDeviceClass | NumberDeviceClass | None: + """Validate and return a device class.""" + try: + return device_class_enum(metadata_value.value) + except ValueError as ex: + logger.warning( + "Quirks provided an invalid device class: %s for platform %s: %s", + metadata_value, + platform, + ex, + ) + return None diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 842450f9279..f9f63321d44 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -182,25 +182,28 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity): if entity_metadata.initially_disabled: self._attr_entity_registry_enabled_default = False - if entity_metadata.translation_key: - self._attr_translation_key = entity_metadata.translation_key - - if hasattr(entity_metadata.entity_metadata, "attribute_name"): - if not entity_metadata.translation_key: - self._attr_translation_key = ( - entity_metadata.entity_metadata.attribute_name - ) - self._unique_id_suffix = entity_metadata.entity_metadata.attribute_name - elif hasattr(entity_metadata.entity_metadata, "command_name"): - if not entity_metadata.translation_key: - self._attr_translation_key = ( - entity_metadata.entity_metadata.command_name - ) - self._unique_id_suffix = entity_metadata.entity_metadata.command_name + has_device_class = hasattr(entity_metadata, "device_class") + has_attribute_name = hasattr(entity_metadata, "attribute_name") + has_command_name = hasattr(entity_metadata, "command_name") + if not has_device_class or ( + has_device_class and entity_metadata.device_class is None + ): + if entity_metadata.translation_key: + self._attr_translation_key = entity_metadata.translation_key + elif has_attribute_name: + self._attr_translation_key = entity_metadata.attribute_name + elif has_command_name: + self._attr_translation_key = entity_metadata.command_name + if has_attribute_name: + self._unique_id_suffix = entity_metadata.attribute_name + elif has_command_name: + self._unique_id_suffix = entity_metadata.command_name if entity_metadata.entity_type is EntityType.CONFIG: self._attr_entity_category = EntityCategory.CONFIG elif entity_metadata.entity_type is EntityType.DIAGNOSTIC: self._attr_entity_category = EntityCategory.DIAGNOSTIC + else: + self._attr_entity_category = None @property def available(self) -> bool: diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 95a4feadc19..e85966e870f 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -26,7 +26,7 @@ "pyserial-asyncio==0.6", "zha-quirks==0.0.112", "zigpy-deconz==0.23.1", - "zigpy==0.63.4", + "zigpy==0.63.5", "zigpy-xbee==0.20.1", "zigpy-zigate==0.12.0", "zigpy-znp==0.12.1", diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index 3ae261cb572..8af2fe178c8 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -6,10 +6,10 @@ import functools import logging from typing import TYPE_CHECKING, Any, Self -from zigpy.quirks.v2 import EntityMetadata, NumberMetadata +from zigpy.quirks.v2 import NumberMetadata from zigpy.zcl.clusters.hvac import Thermostat -from homeassistant.components.number import NumberEntity, NumberMode +from homeassistant.components.number import NumberDeviceClass, NumberEntity, NumberMode from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory, Platform, UnitOfMass, UnitOfTemperature from homeassistant.core import HomeAssistant, callback @@ -26,11 +26,11 @@ from .core.const import ( CLUSTER_HANDLER_LEVEL, CLUSTER_HANDLER_OCCUPANCY, CLUSTER_HANDLER_THERMOSTAT, - QUIRK_METADATA, + ENTITY_METADATA, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, ) -from .core.helpers import get_zha_data +from .core.helpers import get_zha_data, validate_device_class, validate_unit from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity @@ -403,7 +403,7 @@ class ZHANumberConfigurationEntity(ZhaEntity, NumberEntity): Return entity if it is a supported configuration, otherwise return None """ cluster_handler = cluster_handlers[0] - if QUIRK_METADATA not in kwargs and ( + if ENTITY_METADATA not in kwargs and ( cls._attribute_name in cluster_handler.cluster.unsupported_attributes or cls._attribute_name not in cluster_handler.cluster.attributes_by_name or cluster_handler.cluster.get(cls._attribute_name) is None @@ -426,26 +426,34 @@ class ZHANumberConfigurationEntity(ZhaEntity, NumberEntity): ) -> None: """Init this number configuration entity.""" self._cluster_handler: ClusterHandler = cluster_handlers[0] - if QUIRK_METADATA in kwargs: - self._init_from_quirks_metadata(kwargs[QUIRK_METADATA]) + if ENTITY_METADATA in kwargs: + self._init_from_quirks_metadata(kwargs[ENTITY_METADATA]) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - def _init_from_quirks_metadata(self, entity_metadata: EntityMetadata) -> None: + def _init_from_quirks_metadata(self, entity_metadata: NumberMetadata) -> None: """Init this entity from the quirks metadata.""" super()._init_from_quirks_metadata(entity_metadata) - number_metadata: NumberMetadata = entity_metadata.entity_metadata - self._attribute_name = number_metadata.attribute_name + self._attribute_name = entity_metadata.attribute_name - if number_metadata.min is not None: - self._attr_native_min_value = number_metadata.min - if number_metadata.max is not None: - self._attr_native_max_value = number_metadata.max - if number_metadata.step is not None: - self._attr_native_step = number_metadata.step - if number_metadata.unit is not None: - self._attr_native_unit_of_measurement = number_metadata.unit - if number_metadata.multiplier is not None: - self._attr_multiplier = number_metadata.multiplier + if entity_metadata.min is not None: + self._attr_native_min_value = entity_metadata.min + if entity_metadata.max is not None: + self._attr_native_max_value = entity_metadata.max + if entity_metadata.step is not None: + self._attr_native_step = entity_metadata.step + if entity_metadata.multiplier is not None: + self._attr_multiplier = entity_metadata.multiplier + if entity_metadata.device_class is not None: + self._attr_device_class = validate_device_class( + NumberDeviceClass, + entity_metadata.device_class, + Platform.NUMBER.value, + _LOGGER, + ) + if entity_metadata.device_class is None and entity_metadata.unit is not None: + self._attr_native_unit_of_measurement = validate_unit( + entity_metadata.unit + ).value @property def native_value(self) -> float: diff --git a/homeassistant/components/zha/select.py b/homeassistant/components/zha/select.py index c3c62e6173d..98d5debd999 100644 --- a/homeassistant/components/zha/select.py +++ b/homeassistant/components/zha/select.py @@ -11,7 +11,7 @@ from zhaquirks.quirk_ids import TUYA_PLUG_MANUFACTURER, TUYA_PLUG_ONOFF from zhaquirks.xiaomi.aqara.magnet_ac01 import OppleCluster as MagnetAC01OppleCluster from zhaquirks.xiaomi.aqara.switch_acn047 import OppleCluster as T2RelayOppleCluster from zigpy import types -from zigpy.quirks.v2 import EntityMetadata, ZCLEnumMetadata +from zigpy.quirks.v2 import ZCLEnumMetadata from zigpy.zcl.clusters.general import OnOff from zigpy.zcl.clusters.security import IasWd @@ -29,7 +29,7 @@ from .core.const import ( CLUSTER_HANDLER_INOVELLI, CLUSTER_HANDLER_OCCUPANCY, CLUSTER_HANDLER_ON_OFF, - QUIRK_METADATA, + ENTITY_METADATA, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, Strobe, @@ -179,7 +179,7 @@ class ZCLEnumSelectEntity(ZhaEntity, SelectEntity): Return entity if it is a supported configuration, otherwise return None """ cluster_handler = cluster_handlers[0] - if QUIRK_METADATA not in kwargs and ( + if ENTITY_METADATA not in kwargs and ( cls._attribute_name in cluster_handler.cluster.unsupported_attributes or cls._attribute_name not in cluster_handler.cluster.attributes_by_name or cluster_handler.cluster.get(cls._attribute_name) is None @@ -202,17 +202,16 @@ class ZCLEnumSelectEntity(ZhaEntity, SelectEntity): ) -> None: """Init this select entity.""" self._cluster_handler: ClusterHandler = cluster_handlers[0] - if QUIRK_METADATA in kwargs: - self._init_from_quirks_metadata(kwargs[QUIRK_METADATA]) + if ENTITY_METADATA in kwargs: + self._init_from_quirks_metadata(kwargs[ENTITY_METADATA]) self._attr_options = [entry.name.replace("_", " ") for entry in self._enum] super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - def _init_from_quirks_metadata(self, entity_metadata: EntityMetadata) -> None: + def _init_from_quirks_metadata(self, entity_metadata: ZCLEnumMetadata) -> None: """Init this entity from the quirks metadata.""" super()._init_from_quirks_metadata(entity_metadata) - zcl_enum_metadata: ZCLEnumMetadata = entity_metadata.entity_metadata - self._attribute_name = zcl_enum_metadata.attribute_name - self._enum = zcl_enum_metadata.enum + self._attribute_name = entity_metadata.attribute_name + self._enum = entity_metadata.enum @property def current_option(self) -> str | None: diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 1a11cd99593..91fe302291a 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -13,7 +13,7 @@ import random from typing import TYPE_CHECKING, Any, Self from zigpy import types -from zigpy.quirks.v2 import EntityMetadata, ZCLEnumMetadata, ZCLSensorMetadata +from zigpy.quirks.v2 import ZCLEnumMetadata, ZCLSensorMetadata from zigpy.state import Counter, State from zigpy.zcl.clusters.closures import WindowCovering from zigpy.zcl.clusters.general import Basic @@ -71,11 +71,11 @@ from .core.const import ( CLUSTER_HANDLER_TEMPERATURE, CLUSTER_HANDLER_THERMOSTAT, DATA_ZHA, - QUIRK_METADATA, + ENTITY_METADATA, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, ) -from .core.helpers import get_zha_data +from .core.helpers import get_zha_data, validate_device_class, validate_unit from .core.registries import SMARTTHINGS_HUMIDITY_CLUSTER, ZHA_ENTITIES from .entity import BaseZhaEntity, ZhaEntity @@ -154,7 +154,7 @@ class Sensor(ZhaEntity, SensorEntity): Return entity if it is a supported configuration, otherwise return None """ cluster_handler = cluster_handlers[0] - if QUIRK_METADATA not in kwargs and ( + if ENTITY_METADATA not in kwargs and ( cls._attribute_name in cluster_handler.cluster.unsupported_attributes or cls._attribute_name not in cluster_handler.cluster.attributes_by_name ): @@ -176,21 +176,29 @@ class Sensor(ZhaEntity, SensorEntity): ) -> None: """Init this sensor.""" self._cluster_handler: ClusterHandler = cluster_handlers[0] - if QUIRK_METADATA in kwargs: - self._init_from_quirks_metadata(kwargs[QUIRK_METADATA]) + if ENTITY_METADATA in kwargs: + self._init_from_quirks_metadata(kwargs[ENTITY_METADATA]) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - def _init_from_quirks_metadata(self, entity_metadata: EntityMetadata) -> None: + def _init_from_quirks_metadata(self, entity_metadata: ZCLSensorMetadata) -> None: """Init this entity from the quirks metadata.""" super()._init_from_quirks_metadata(entity_metadata) - sensor_metadata: ZCLSensorMetadata = entity_metadata.entity_metadata - self._attribute_name = sensor_metadata.attribute_name - if sensor_metadata.divisor is not None: - self._divisor = sensor_metadata.divisor - if sensor_metadata.multiplier is not None: - self._multiplier = sensor_metadata.multiplier - if sensor_metadata.unit is not None: - self._attr_native_unit_of_measurement = sensor_metadata.unit + self._attribute_name = entity_metadata.attribute_name + if entity_metadata.divisor is not None: + self._divisor = entity_metadata.divisor + if entity_metadata.multiplier is not None: + self._multiplier = entity_metadata.multiplier + if entity_metadata.device_class is not None: + self._attr_device_class = validate_device_class( + SensorDeviceClass, + entity_metadata.device_class, + Platform.SENSOR.value, + _LOGGER, + ) + if entity_metadata.device_class is None and entity_metadata.unit is not None: + self._attr_native_unit_of_measurement = validate_unit( + entity_metadata.unit + ).value async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" @@ -355,12 +363,22 @@ class EnumSensor(Sensor): _attr_device_class: SensorDeviceClass = SensorDeviceClass.ENUM _enum: type[enum.Enum] - def _init_from_quirks_metadata(self, entity_metadata: EntityMetadata) -> None: + def __init__( + self, + unique_id: str, + zha_device: ZHADevice, + cluster_handlers: list[ClusterHandler], + **kwargs: Any, + ) -> None: + """Init this sensor.""" + super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) + self._attr_options = [e.name for e in self._enum] + + def _init_from_quirks_metadata(self, entity_metadata: ZCLEnumMetadata) -> None: """Init this entity from the quirks metadata.""" ZhaEntity._init_from_quirks_metadata(self, entity_metadata) # pylint: disable=protected-access - sensor_metadata: ZCLEnumMetadata = entity_metadata.entity_metadata - self._attribute_name = sensor_metadata.attribute_name - self._enum = sensor_metadata.enum + self._attribute_name = entity_metadata.attribute_name + self._enum = entity_metadata.enum def formatter(self, value: int) -> str | None: """Use name of enum.""" diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 0561efbb2f2..14da2344cd4 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -7,7 +7,7 @@ import logging from typing import TYPE_CHECKING, Any, Self from zhaquirks.quirk_ids import TUYA_PLUG_ONOFF -from zigpy.quirks.v2 import EntityMetadata, SwitchMetadata +from zigpy.quirks.v2 import SwitchMetadata from zigpy.zcl.clusters.closures import ConfigStatus, WindowCovering, WindowCoveringMode from zigpy.zcl.clusters.general import OnOff from zigpy.zcl.foundation import Status @@ -25,7 +25,7 @@ from .core.const import ( CLUSTER_HANDLER_COVER, CLUSTER_HANDLER_INOVELLI, CLUSTER_HANDLER_ON_OFF, - QUIRK_METADATA, + ENTITY_METADATA, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, ) @@ -192,7 +192,7 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity): Return entity if it is a supported configuration, otherwise return None """ cluster_handler = cluster_handlers[0] - if QUIRK_METADATA not in kwargs and ( + if ENTITY_METADATA not in kwargs and ( cls._attribute_name in cluster_handler.cluster.unsupported_attributes or cls._attribute_name not in cluster_handler.cluster.attributes_by_name or cluster_handler.cluster.get(cls._attribute_name) is None @@ -215,21 +215,20 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity): ) -> None: """Init this number configuration entity.""" self._cluster_handler: ClusterHandler = cluster_handlers[0] - if QUIRK_METADATA in kwargs: - self._init_from_quirks_metadata(kwargs[QUIRK_METADATA]) + if ENTITY_METADATA in kwargs: + self._init_from_quirks_metadata(kwargs[ENTITY_METADATA]) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - def _init_from_quirks_metadata(self, entity_metadata: EntityMetadata) -> None: + def _init_from_quirks_metadata(self, entity_metadata: SwitchMetadata) -> None: """Init this entity from the quirks metadata.""" super()._init_from_quirks_metadata(entity_metadata) - switch_metadata: SwitchMetadata = entity_metadata.entity_metadata - self._attribute_name = switch_metadata.attribute_name - if switch_metadata.invert_attribute_name: - self._inverter_attribute_name = switch_metadata.invert_attribute_name - if switch_metadata.force_inverted: - self._force_inverted = switch_metadata.force_inverted - self._off_value = switch_metadata.off_value - self._on_value = switch_metadata.on_value + self._attribute_name = entity_metadata.attribute_name + if entity_metadata.invert_attribute_name: + self._inverter_attribute_name = entity_metadata.invert_attribute_name + if entity_metadata.force_inverted: + self._force_inverted = entity_metadata.force_inverted + self._off_value = entity_metadata.off_value + self._on_value = entity_metadata.on_value async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" diff --git a/requirements_all.txt b/requirements_all.txt index c3106479617..a22b8169363 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2952,7 +2952,7 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.1 # homeassistant.components.zha -zigpy==0.63.4 +zigpy==0.63.5 # homeassistant.components.zoneminder zm-py==0.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index efaa3a8ad30..de5f355f742 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2281,7 +2281,7 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.1 # homeassistant.components.zha -zigpy==0.63.4 +zigpy==0.63.5 # homeassistant.components.zwave_js zwave-js-server-python==0.55.3 diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index debf233de36..f9242eb1d96 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -1,6 +1,8 @@ """Test ZHA device discovery.""" from collections.abc import Callable +import enum +import itertools import re from typing import Any from unittest import mock @@ -20,7 +22,16 @@ from zhaquirks.xiaomi.aqara.driver_curtain_e1 import ( from zigpy.const import SIG_ENDPOINTS, SIG_MANUFACTURER, SIG_MODEL, SIG_NODE_DESC import zigpy.profiles.zha import zigpy.quirks -from zigpy.quirks.v2 import EntityType, add_to_registry_v2 +from zigpy.quirks.v2 import ( + BinarySensorMetadata, + EntityMetadata, + EntityType, + NumberMetadata, + QuirksV2RegistryEntry, + ZCLCommandButtonMetadata, + ZCLSensorMetadata, + add_to_registry_v2, +) from zigpy.quirks.v2.homeassistant import UnitOfTime import zigpy.types from zigpy.zcl import ClusterType @@ -40,6 +51,7 @@ from homeassistant.const import STATE_OFF, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import EntityPlatform +from homeassistant.util.json import load_json from .common import find_entity_id, update_attribute_cache from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE @@ -520,6 +532,7 @@ async def test_quirks_v2_entity_discovery( step=1, unit=UnitOfTime.SECONDS, multiplier=1, + translation_key="on_off_transition_time", ) ) @@ -618,7 +631,11 @@ async def test_quirks_v2_entity_discovery_e1_curtain( entity_platform=Platform.SENSOR, entity_type=EntityType.DIAGNOSTIC, ) - .binary_sensor("error_detected", FakeXiaomiAqaraDriverE1.cluster_id) + .binary_sensor( + "error_detected", + FakeXiaomiAqaraDriverE1.cluster_id, + translation_key="valve_alarm", + ) ) aqara_E1_device = zigpy.quirks._DEVICE_REGISTRY.get_device(aqara_E1_device) @@ -683,7 +700,13 @@ async def test_quirks_v2_entity_discovery_e1_curtain( assert state.state == STATE_OFF -def _get_test_device(zigpy_device_mock, manufacturer: str, model: str): +def _get_test_device( + zigpy_device_mock, + manufacturer: str, + model: str, + augment_method: Callable[[QuirksV2RegistryEntry], QuirksV2RegistryEntry] + | None = None, +): zigpy_device = zigpy_device_mock( { 1: { @@ -703,7 +726,7 @@ def _get_test_device(zigpy_device_mock, manufacturer: str, model: str): model=model, ) - ( + v2_quirk = ( add_to_registry_v2(manufacturer, model, zigpy.quirks._DEVICE_REGISTRY) .replaces(PowerConfig1CRCluster) .replaces(ScenesCluster, cluster_type=ClusterType.Client) @@ -716,6 +739,7 @@ def _get_test_device(zigpy_device_mock, manufacturer: str, model: str): step=1, unit=UnitOfTime.SECONDS, multiplier=1, + translation_key="on_off_transition_time", ) .number( zigpy.zcl.clusters.general.OnOff.AttributeDefs.off_wait_time.name, @@ -725,14 +749,19 @@ def _get_test_device(zigpy_device_mock, manufacturer: str, model: str): step=1, unit=UnitOfTime.SECONDS, multiplier=1, + translation_key="on_off_transition_time", ) .sensor( zigpy.zcl.clusters.general.OnOff.AttributeDefs.off_wait_time.name, zigpy.zcl.clusters.general.OnOff.cluster_id, entity_type=EntityType.CONFIG, + translation_key="analog_input", ) ) + if augment_method: + v2_quirk = augment_method(v2_quirk) + zigpy_device = zigpy.quirks._DEVICE_REGISTRY.get_device(zigpy_device) zigpy_device.endpoints[1].power.PLUGGED_ATTR_READS = { "battery_voltage": 3, @@ -792,14 +821,13 @@ async def test_quirks_v2_entity_discovery_errors( # fmt: off entity_details = ( - "{'cluster_details': (1, 6, ), " - "'quirk_metadata': EntityMetadata(entity_metadata=ZCLSensorMetadata(" - "attribute_name='off_wait_time', divisor=1, multiplier=1, unit=None, " - "device_class=None, state_class=None), entity_platform=, entity_type=, " - "cluster_id=6, endpoint_id=1, cluster_type=, " - "initially_disabled=False, attribute_initialized_from_cache=True, " - "translation_key=None)}" + "{'cluster_details': (1, 6, ), 'entity_metadata': " + "ZCLSensorMetadata(entity_platform=, " + "entity_type=, cluster_id=6, endpoint_id=1, " + "cluster_type=, initially_disabled=False, " + "attribute_initialized_from_cache=True, translation_key='analog_input', " + "attribute_name='off_wait_time', divisor=1, multiplier=1, " + "unit=None, device_class=None, state_class=None)}" ) # fmt: on @@ -807,3 +835,266 @@ async def test_quirks_v2_entity_discovery_errors( m2 = f"details: {entity_details} that does not have an entity class mapping - " m3 = "unable to create entity" assert f"{m1}{m2}{m3}" in caplog.text + + +DEVICE_CLASS_TYPES = [NumberMetadata, BinarySensorMetadata, ZCLSensorMetadata] + + +def validate_device_class_unit( + quirk: QuirksV2RegistryEntry, + entity_metadata: EntityMetadata, + platform: Platform, + translations: dict, +) -> None: + """Ensure device class and unit are used correctly.""" + if ( + hasattr(entity_metadata, "unit") + and entity_metadata.unit is not None + and hasattr(entity_metadata, "device_class") + and entity_metadata.device_class is not None + ): + m1 = "device_class and unit are both set - unit: " + m2 = f"{entity_metadata.unit} device_class: " + m3 = f"{entity_metadata.device_class} for {platform.name} " + raise ValueError(f"{m1}{m2}{m3}{quirk}") + + +def validate_translation_keys( + quirk: QuirksV2RegistryEntry, + entity_metadata: EntityMetadata, + platform: Platform, + translations: dict, +) -> None: + """Ensure translation keys exist for all v2 quirks.""" + if isinstance(entity_metadata, ZCLCommandButtonMetadata): + default_translation_key = entity_metadata.command_name + else: + default_translation_key = entity_metadata.attribute_name + translation_key = entity_metadata.translation_key or default_translation_key + + if ( + translation_key is not None + and translation_key not in translations["entity"][platform] + ): + raise ValueError( + f"Missing translation key: {translation_key} for {platform.name} {quirk}" + ) + + +def validate_translation_keys_device_class( + quirk: QuirksV2RegistryEntry, + entity_metadata: EntityMetadata, + platform: Platform, + translations: dict, +) -> None: + """Validate translation keys and device class usage.""" + if isinstance(entity_metadata, ZCLCommandButtonMetadata): + default_translation_key = entity_metadata.command_name + else: + default_translation_key = entity_metadata.attribute_name + translation_key = entity_metadata.translation_key or default_translation_key + + metadata_type = type(entity_metadata) + if metadata_type in DEVICE_CLASS_TYPES: + device_class = entity_metadata.device_class + if device_class is not None and translation_key is not None: + m1 = "translation_key and device_class are both set - translation_key: " + m2 = f"{translation_key} device_class: {device_class} for {platform.name} " + raise ValueError(f"{m1}{m2}{quirk}") + + +def validate_metadata(validator: Callable) -> None: + """Ensure v2 quirks metadata does not violate HA rules.""" + all_v2_quirks = itertools.chain.from_iterable( + zigpy.quirks._DEVICE_REGISTRY._registry_v2.values() + ) + translations = load_json("homeassistant/components/zha/strings.json") + for quirk in all_v2_quirks: + for entity_metadata in quirk.entity_metadata: + platform = Platform(entity_metadata.entity_platform.value) + validator(quirk, entity_metadata, platform, translations) + + +def bad_translation_key(v2_quirk: QuirksV2RegistryEntry) -> QuirksV2RegistryEntry: + """Introduce a bad translation key.""" + return v2_quirk.sensor( + zigpy.zcl.clusters.general.OnOff.AttributeDefs.off_wait_time.name, + zigpy.zcl.clusters.general.OnOff.cluster_id, + entity_type=EntityType.CONFIG, + translation_key="missing_translation_key", + ) + + +def bad_device_class_unit_combination( + v2_quirk: QuirksV2RegistryEntry, +) -> QuirksV2RegistryEntry: + """Introduce a bad device class and unit combination.""" + return v2_quirk.sensor( + zigpy.zcl.clusters.general.OnOff.AttributeDefs.off_wait_time.name, + zigpy.zcl.clusters.general.OnOff.cluster_id, + entity_type=EntityType.CONFIG, + unit="invalid", + device_class="invalid", + translation_key="analog_input", + ) + + +def bad_device_class_translation_key_usage( + v2_quirk: QuirksV2RegistryEntry, +) -> QuirksV2RegistryEntry: + """Introduce a bad device class and translation key combination.""" + return v2_quirk.sensor( + zigpy.zcl.clusters.general.OnOff.AttributeDefs.off_wait_time.name, + zigpy.zcl.clusters.general.OnOff.cluster_id, + entity_type=EntityType.CONFIG, + translation_key="invalid", + device_class="invalid", + ) + + +@pytest.mark.parametrize( + ("augment_method", "validate_method", "expected_exception_string"), + [ + ( + bad_translation_key, + validate_translation_keys, + "Missing translation key: missing_translation_key", + ), + ( + bad_device_class_unit_combination, + validate_device_class_unit, + "cannot have both unit and device_class", + ), + ( + bad_device_class_translation_key_usage, + validate_translation_keys_device_class, + "cannot have both a translation_key and a device_class", + ), + ], +) +async def test_quirks_v2_metadata_errors( + hass: HomeAssistant, + zigpy_device_mock, + zha_device_joined, + augment_method: Callable[[QuirksV2RegistryEntry], QuirksV2RegistryEntry], + validate_method: Callable, + expected_exception_string: str, +) -> None: + """Ensure all v2 quirks translation keys exist.""" + + # no error yet + validate_metadata(validate_method) + + # ensure the error is caught and raised + with pytest.raises(ValueError, match=expected_exception_string): + try: + # introduce an error + zigpy_device = _get_test_device( + zigpy_device_mock, + "Ikea of Sweden4", + "TRADFRI remote control4", + augment_method=augment_method, + ) + await zha_device_joined(zigpy_device) + + validate_metadata(validate_method) + # if the device was created we remove it + # so we don't pollute the rest of the tests + zigpy.quirks._DEVICE_REGISTRY.remove(zigpy_device) + except ValueError as e: + # if the device was not created we remove it + # so we don't pollute the rest of the tests + zigpy.quirks._DEVICE_REGISTRY._registry_v2.pop( + ( + "Ikea of Sweden4", + "TRADFRI remote control4", + ) + ) + raise e + + +class BadDeviceClass(enum.Enum): + """Bad device class.""" + + BAD = "bad" + + +def bad_binary_sensor_device_class( + v2_quirk: QuirksV2RegistryEntry, +) -> QuirksV2RegistryEntry: + """Introduce a bad device class on a binary sensor.""" + + return v2_quirk.binary_sensor( + zigpy.zcl.clusters.general.OnOff.AttributeDefs.on_off.name, + zigpy.zcl.clusters.general.OnOff.cluster_id, + device_class=BadDeviceClass.BAD, + ) + + +def bad_sensor_device_class( + v2_quirk: QuirksV2RegistryEntry, +) -> QuirksV2RegistryEntry: + """Introduce a bad device class on a sensor.""" + + return v2_quirk.sensor( + zigpy.zcl.clusters.general.OnOff.AttributeDefs.off_wait_time.name, + zigpy.zcl.clusters.general.OnOff.cluster_id, + device_class=BadDeviceClass.BAD, + ) + + +def bad_number_device_class( + v2_quirk: QuirksV2RegistryEntry, +) -> QuirksV2RegistryEntry: + """Introduce a bad device class on a number.""" + + return v2_quirk.number( + zigpy.zcl.clusters.general.OnOff.AttributeDefs.on_time.name, + zigpy.zcl.clusters.general.OnOff.cluster_id, + device_class=BadDeviceClass.BAD, + ) + + +ERROR_ROOT = "Quirks provided an invalid device class" + + +@pytest.mark.parametrize( + ("augment_method", "expected_exception_string"), + [ + ( + bad_binary_sensor_device_class, + f"{ERROR_ROOT}: BadDeviceClass.BAD for platform binary_sensor", + ), + ( + bad_sensor_device_class, + f"{ERROR_ROOT}: BadDeviceClass.BAD for platform sensor", + ), + ( + bad_number_device_class, + f"{ERROR_ROOT}: BadDeviceClass.BAD for platform number", + ), + ], +) +async def test_quirks_v2_metadata_bad_device_classes( + hass: HomeAssistant, + zigpy_device_mock, + zha_device_joined, + caplog: pytest.LogCaptureFixture, + augment_method: Callable[[QuirksV2RegistryEntry], QuirksV2RegistryEntry], + expected_exception_string: str, +) -> None: + """Test bad quirks v2 device classes.""" + + # introduce an error + zigpy_device = _get_test_device( + zigpy_device_mock, + "Ikea of Sweden4", + "TRADFRI remote control4", + augment_method=augment_method, + ) + await zha_device_joined(zigpy_device) + + assert expected_exception_string in caplog.text + + # remove the device so we don't pollute the rest of the tests + zigpy.quirks._DEVICE_REGISTRY.remove(zigpy_device) diff --git a/tests/components/zha/test_helpers.py b/tests/components/zha/test_helpers.py index b97a2d0fa5d..fed8fe5bb91 100644 --- a/tests/components/zha/test_helpers.py +++ b/tests/components/zha/test_helpers.py @@ -1,11 +1,13 @@ """Tests for ZHA helpers.""" +import enum import logging from unittest.mock import patch import pytest import voluptuous_serialize import zigpy.profiles.zha as zha +from zigpy.quirks.v2.homeassistant import UnitOfPower as QuirksUnitOfPower from zigpy.types.basic import uint16_t import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.lighting as lighting @@ -13,8 +15,9 @@ import zigpy.zcl.clusters.lighting as lighting from homeassistant.components.zha.core.helpers import ( cluster_command_schema_to_vol_schema, convert_to_zcl_values, + validate_unit, ) -from homeassistant.const import Platform +from homeassistant.const import Platform, UnitOfPower from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -40,7 +43,7 @@ def light_platform_only(): @pytest.fixture -async def device_light(hass, zigpy_device_mock, zha_device_joined): +async def device_light(hass: HomeAssistant, zigpy_device_mock, zha_device_joined): """Test light.""" zigpy_device = zigpy_device_mock( @@ -211,3 +214,25 @@ async def test_zcl_schema_conversions(hass: HomeAssistant, device_light) -> None # No flags are passed through assert converted_data["update_flags"] == 0 + + +def test_unit_validation() -> None: + """Test unit validation.""" + + assert validate_unit(QuirksUnitOfPower.WATT) == UnitOfPower.WATT + + class FooUnit(enum.Enum): + """Foo unit.""" + + BAR = "bar" + + class UnitOfMass(enum.Enum): + """UnitOfMass.""" + + BAR = "bar" + + with pytest.raises(KeyError): + validate_unit(FooUnit.BAR) + + with pytest.raises(ValueError): + validate_unit(UnitOfMass.BAR) diff --git a/tests/components/zha/test_select.py b/tests/components/zha/test_select.py index e044281d2a1..bb1c5ca270a 100644 --- a/tests/components/zha/test_select.py +++ b/tests/components/zha/test_select.py @@ -435,7 +435,7 @@ async def test_on_off_select_attribute_report( "motion_sensitivity_disabled", AqaraMotionSensitivities, MotionSensitivityQuirk.OppleCluster.cluster_id, - translation_key="motion_sensitivity_translation_key", + translation_key="motion_sensitivity", initially_disabled=True, ) ) @@ -491,9 +491,8 @@ async def test_on_off_select_attribute_report_v2( assert hass.states.get(entity_id).state == AqaraMotionSensitivities.Low.name entity_registry = er.async_get(hass) - # none in id because the translation key does not exist - entity_entry = entity_registry.async_get("select.fake_manufacturer_fake_model_none") + entity_entry = entity_registry.async_get(entity_id) assert entity_entry assert entity_entry.entity_category == EntityCategory.CONFIG - assert entity_entry.disabled is True - assert entity_entry.translation_key == "motion_sensitivity_translation_key" + assert entity_entry.disabled is False + assert entity_entry.translation_key == "motion_sensitivity" diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index a6c4cfbf4ec..8d0ef8107e3 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -1260,10 +1260,10 @@ async def test_last_feeding_size_sensor_v2( assert entity_id is not None await send_attributes_report(hass, cluster, {0x010C: 1}) - assert_state(hass, entity_id, "1.0", UnitOfMass.GRAMS) + assert_state(hass, entity_id, "1.0", UnitOfMass.GRAMS.value) await send_attributes_report(hass, cluster, {0x010C: 5}) - assert_state(hass, entity_id, "5.0", UnitOfMass.GRAMS) + assert_state(hass, entity_id, "5.0", UnitOfMass.GRAMS.value) @pytest.fixture