Defensively validate ZHA quirks v2 supplied entity metadata (#112643)
parent
65230908c6
commit
c518acfef3
|
@ -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."""
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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, <ClusterType.Server: 0>), "
|
||||
"'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=<EntityPlatform."
|
||||
"SENSOR: 'sensor'>, entity_type=<EntityType.CONFIG: 'config'>, "
|
||||
"cluster_id=6, endpoint_id=1, cluster_type=<ClusterType.Server: 0>, "
|
||||
"initially_disabled=False, attribute_initialized_from_cache=True, "
|
||||
"translation_key=None)}"
|
||||
"{'cluster_details': (1, 6, <ClusterType.Server: 0>), 'entity_metadata': "
|
||||
"ZCLSensorMetadata(entity_platform=<EntityPlatform.SENSOR: 'sensor'>, "
|
||||
"entity_type=<EntityType.CONFIG: 'config'>, cluster_id=6, endpoint_id=1, "
|
||||
"cluster_type=<ClusterType.Server: 0>, 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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue