mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
Defensively validate ZHA quirks v2 supplied entity metadata (#112643)
This commit is contained in:
parent
65230908c6
commit
c518acfef3
@ -3,8 +3,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
|
import logging
|
||||||
|
|
||||||
from zigpy.quirks.v2 import BinarySensorMetadata, EntityMetadata
|
from zigpy.quirks.v2 import BinarySensorMetadata
|
||||||
import zigpy.types as t
|
import zigpy.types as t
|
||||||
from zigpy.zcl.clusters.general import OnOff
|
from zigpy.zcl.clusters.general import OnOff
|
||||||
from zigpy.zcl.clusters.security import IasZone
|
from zigpy.zcl.clusters.security import IasZone
|
||||||
@ -27,11 +28,11 @@ from .core.const import (
|
|||||||
CLUSTER_HANDLER_OCCUPANCY,
|
CLUSTER_HANDLER_OCCUPANCY,
|
||||||
CLUSTER_HANDLER_ON_OFF,
|
CLUSTER_HANDLER_ON_OFF,
|
||||||
CLUSTER_HANDLER_ZONE,
|
CLUSTER_HANDLER_ZONE,
|
||||||
QUIRK_METADATA,
|
ENTITY_METADATA,
|
||||||
SIGNAL_ADD_ENTITIES,
|
SIGNAL_ADD_ENTITIES,
|
||||||
SIGNAL_ATTR_UPDATED,
|
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 .core.registries import ZHA_ENTITIES
|
||||||
from .entity import ZhaEntity
|
from .entity import ZhaEntity
|
||||||
|
|
||||||
@ -51,6 +52,8 @@ CONFIG_DIAGNOSTIC_MATCH = functools.partial(
|
|||||||
ZHA_ENTITIES.config_diagnostic_match, Platform.BINARY_SENSOR
|
ZHA_ENTITIES.config_diagnostic_match, Platform.BINARY_SENSOR
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -79,15 +82,21 @@ class BinarySensor(ZhaEntity, BinarySensorEntity):
|
|||||||
def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs) -> None:
|
def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs) -> None:
|
||||||
"""Initialize the ZHA binary sensor."""
|
"""Initialize the ZHA binary sensor."""
|
||||||
self._cluster_handler = cluster_handlers[0]
|
self._cluster_handler = cluster_handlers[0]
|
||||||
if QUIRK_METADATA in kwargs:
|
if ENTITY_METADATA in kwargs:
|
||||||
self._init_from_quirks_metadata(kwargs[QUIRK_METADATA])
|
self._init_from_quirks_metadata(kwargs[ENTITY_METADATA])
|
||||||
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
|
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."""
|
"""Init this entity from the quirks metadata."""
|
||||||
super()._init_from_quirks_metadata(entity_metadata)
|
super()._init_from_quirks_metadata(entity_metadata)
|
||||||
binary_sensor_metadata: BinarySensorMetadata = entity_metadata.entity_metadata
|
self._attribute_name = entity_metadata.attribute_name
|
||||||
self._attribute_name = binary_sensor_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:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Run when about to be added to hass."""
|
"""Run when about to be added to hass."""
|
||||||
|
@ -6,11 +6,7 @@ import functools
|
|||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any, Self
|
from typing import TYPE_CHECKING, Any, Self
|
||||||
|
|
||||||
from zigpy.quirks.v2 import (
|
from zigpy.quirks.v2 import WriteAttributeButtonMetadata, ZCLCommandButtonMetadata
|
||||||
EntityMetadata,
|
|
||||||
WriteAttributeButtonMetadata,
|
|
||||||
ZCLCommandButtonMetadata,
|
|
||||||
)
|
|
||||||
|
|
||||||
from homeassistant.components.button import ButtonDeviceClass, ButtonEntity
|
from homeassistant.components.button import ButtonDeviceClass, ButtonEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
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 homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .core import discovery
|
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.helpers import get_zha_data
|
||||||
from .core.registries import ZHA_ENTITIES
|
from .core.registries import ZHA_ENTITIES
|
||||||
from .entity import ZhaEntity
|
from .entity import ZhaEntity
|
||||||
@ -76,17 +72,18 @@ class ZHAButton(ZhaEntity, ButtonEntity):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Init this button."""
|
"""Init this button."""
|
||||||
self._cluster_handler: ClusterHandler = cluster_handlers[0]
|
self._cluster_handler: ClusterHandler = cluster_handlers[0]
|
||||||
if QUIRK_METADATA in kwargs:
|
if ENTITY_METADATA in kwargs:
|
||||||
self._init_from_quirks_metadata(kwargs[QUIRK_METADATA])
|
self._init_from_quirks_metadata(kwargs[ENTITY_METADATA])
|
||||||
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
|
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."""
|
"""Init this entity from the quirks metadata."""
|
||||||
super()._init_from_quirks_metadata(entity_metadata)
|
super()._init_from_quirks_metadata(entity_metadata)
|
||||||
button_metadata: ZCLCommandButtonMetadata = entity_metadata.entity_metadata
|
self._command_name = entity_metadata.command_name
|
||||||
self._command_name = button_metadata.command_name
|
self._args = entity_metadata.args
|
||||||
self._args = button_metadata.args
|
self._kwargs = entity_metadata.kwargs
|
||||||
self._kwargs = button_metadata.kwargs
|
|
||||||
|
|
||||||
def get_args(self) -> list[Any]:
|
def get_args(self) -> list[Any]:
|
||||||
"""Return the arguments to use in the command."""
|
"""Return the arguments to use in the command."""
|
||||||
@ -148,16 +145,17 @@ class ZHAAttributeButton(ZhaEntity, ButtonEntity):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Init this button."""
|
"""Init this button."""
|
||||||
self._cluster_handler: ClusterHandler = cluster_handlers[0]
|
self._cluster_handler: ClusterHandler = cluster_handlers[0]
|
||||||
if QUIRK_METADATA in kwargs:
|
if ENTITY_METADATA in kwargs:
|
||||||
self._init_from_quirks_metadata(kwargs[QUIRK_METADATA])
|
self._init_from_quirks_metadata(kwargs[ENTITY_METADATA])
|
||||||
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
|
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."""
|
"""Init this entity from the quirks metadata."""
|
||||||
super()._init_from_quirks_metadata(entity_metadata)
|
super()._init_from_quirks_metadata(entity_metadata)
|
||||||
button_metadata: WriteAttributeButtonMetadata = entity_metadata.entity_metadata
|
self._attribute_name = entity_metadata.attribute_name
|
||||||
self._attribute_name = button_metadata.attribute_name
|
self._attribute_value = entity_metadata.attribute_value
|
||||||
self._attribute_value = button_metadata.attribute_value
|
|
||||||
|
|
||||||
async def async_press(self) -> None:
|
async def async_press(self) -> None:
|
||||||
"""Write attribute with defined value."""
|
"""Write attribute with defined value."""
|
||||||
|
@ -220,6 +220,8 @@ DISCOVERY_KEY = "zha_discovery_info"
|
|||||||
|
|
||||||
DOMAIN = "zha"
|
DOMAIN = "zha"
|
||||||
|
|
||||||
|
ENTITY_METADATA = "entity_metadata"
|
||||||
|
|
||||||
GROUP_ID = "group_id"
|
GROUP_ID = "group_id"
|
||||||
GROUP_IDS = "group_ids"
|
GROUP_IDS = "group_ids"
|
||||||
GROUP_NAME = "group_name"
|
GROUP_NAME = "group_name"
|
||||||
@ -233,8 +235,6 @@ PRESET_SCHEDULE = "Schedule"
|
|||||||
PRESET_COMPLEX = "Complex"
|
PRESET_COMPLEX = "Complex"
|
||||||
PRESET_TEMP_MANUAL = "Temporary manual"
|
PRESET_TEMP_MANUAL = "Temporary manual"
|
||||||
|
|
||||||
QUIRK_METADATA = "quirk_metadata"
|
|
||||||
|
|
||||||
ZCL_INIT_ATTRS = "ZCL_INIT_ATTRS"
|
ZCL_INIT_ATTRS = "ZCL_INIT_ATTRS"
|
||||||
|
|
||||||
ZHA_ALARM_OPTIONS = "zha_alarm_options"
|
ZHA_ALARM_OPTIONS = "zha_alarm_options"
|
||||||
|
@ -85,12 +85,18 @@ QUIRKS_ENTITY_META_TO_ENTITY_CLASS = {
|
|||||||
WriteAttributeButtonMetadata,
|
WriteAttributeButtonMetadata,
|
||||||
EntityType.CONFIG,
|
EntityType.CONFIG,
|
||||||
): button.ZHAAttributeButton,
|
): button.ZHAAttributeButton,
|
||||||
|
(
|
||||||
|
Platform.BUTTON,
|
||||||
|
WriteAttributeButtonMetadata,
|
||||||
|
EntityType.STANDARD,
|
||||||
|
): button.ZHAAttributeButton,
|
||||||
(Platform.BUTTON, ZCLCommandButtonMetadata, EntityType.CONFIG): button.ZHAButton,
|
(Platform.BUTTON, ZCLCommandButtonMetadata, EntityType.CONFIG): button.ZHAButton,
|
||||||
(
|
(
|
||||||
Platform.BUTTON,
|
Platform.BUTTON,
|
||||||
ZCLCommandButtonMetadata,
|
ZCLCommandButtonMetadata,
|
||||||
EntityType.DIAGNOSTIC,
|
EntityType.DIAGNOSTIC,
|
||||||
): button.ZHAButton,
|
): button.ZHAButton,
|
||||||
|
(Platform.BUTTON, ZCLCommandButtonMetadata, EntityType.STANDARD): button.ZHAButton,
|
||||||
(
|
(
|
||||||
Platform.BINARY_SENSOR,
|
Platform.BINARY_SENSOR,
|
||||||
BinarySensorMetadata,
|
BinarySensorMetadata,
|
||||||
@ -111,6 +117,7 @@ QUIRKS_ENTITY_META_TO_ENTITY_CLASS = {
|
|||||||
(Platform.SENSOR, ZCLSensorMetadata, EntityType.DIAGNOSTIC): sensor.Sensor,
|
(Platform.SENSOR, ZCLSensorMetadata, EntityType.DIAGNOSTIC): sensor.Sensor,
|
||||||
(Platform.SENSOR, ZCLSensorMetadata, EntityType.STANDARD): sensor.Sensor,
|
(Platform.SENSOR, ZCLSensorMetadata, EntityType.STANDARD): sensor.Sensor,
|
||||||
(Platform.SELECT, ZCLEnumMetadata, EntityType.CONFIG): select.ZCLEnumSelectEntity,
|
(Platform.SELECT, ZCLEnumMetadata, EntityType.CONFIG): select.ZCLEnumSelectEntity,
|
||||||
|
(Platform.SELECT, ZCLEnumMetadata, EntityType.STANDARD): select.ZCLEnumSelectEntity,
|
||||||
(
|
(
|
||||||
Platform.SELECT,
|
Platform.SELECT,
|
||||||
ZCLEnumMetadata,
|
ZCLEnumMetadata,
|
||||||
@ -224,7 +231,7 @@ class ProbeEndpoint:
|
|||||||
|
|
||||||
for (
|
for (
|
||||||
cluster_details,
|
cluster_details,
|
||||||
quirk_metadata_list,
|
entity_metadata_list,
|
||||||
) in zigpy_device.exposes_metadata.items():
|
) in zigpy_device.exposes_metadata.items():
|
||||||
endpoint_id, cluster_id, cluster_type = cluster_details
|
endpoint_id, cluster_id, cluster_type = cluster_details
|
||||||
|
|
||||||
@ -265,11 +272,11 @@ class ProbeEndpoint:
|
|||||||
)
|
)
|
||||||
assert cluster_handler
|
assert cluster_handler
|
||||||
|
|
||||||
for quirk_metadata in quirk_metadata_list:
|
for entity_metadata in entity_metadata_list:
|
||||||
platform = Platform(quirk_metadata.entity_platform.value)
|
platform = Platform(entity_metadata.entity_platform.value)
|
||||||
metadata_type = type(quirk_metadata.entity_metadata)
|
metadata_type = type(entity_metadata)
|
||||||
entity_class = QUIRKS_ENTITY_META_TO_ENTITY_CLASS.get(
|
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:
|
if entity_class is None:
|
||||||
@ -280,7 +287,7 @@ class ProbeEndpoint:
|
|||||||
device.name,
|
device.name,
|
||||||
{
|
{
|
||||||
zha_const.CLUSTER_DETAILS: cluster_details,
|
zha_const.CLUSTER_DETAILS: cluster_details,
|
||||||
zha_const.QUIRK_METADATA: quirk_metadata,
|
zha_const.ENTITY_METADATA: entity_metadata,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
@ -288,13 +295,13 @@ class ProbeEndpoint:
|
|||||||
# automatically add the attribute to ZCL_INIT_ATTRS for the cluster
|
# automatically add the attribute to ZCL_INIT_ATTRS for the cluster
|
||||||
# handler if it is not already in the list
|
# handler if it is not already in the list
|
||||||
if (
|
if (
|
||||||
hasattr(quirk_metadata.entity_metadata, "attribute_name")
|
hasattr(entity_metadata, "attribute_name")
|
||||||
and quirk_metadata.entity_metadata.attribute_name
|
and entity_metadata.attribute_name
|
||||||
not in cluster_handler.ZCL_INIT_ATTRS
|
not in cluster_handler.ZCL_INIT_ATTRS
|
||||||
):
|
):
|
||||||
init_attrs = cluster_handler.ZCL_INIT_ATTRS.copy()
|
init_attrs = cluster_handler.ZCL_INIT_ATTRS.copy()
|
||||||
init_attrs[quirk_metadata.entity_metadata.attribute_name] = (
|
init_attrs[entity_metadata.attribute_name] = (
|
||||||
quirk_metadata.attribute_initialized_from_cache
|
entity_metadata.attribute_initialized_from_cache
|
||||||
)
|
)
|
||||||
cluster_handler.__dict__[zha_const.ZCL_INIT_ATTRS] = init_attrs
|
cluster_handler.__dict__[zha_const.ZCL_INIT_ATTRS] = init_attrs
|
||||||
|
|
||||||
@ -303,7 +310,7 @@ class ProbeEndpoint:
|
|||||||
entity_class,
|
entity_class,
|
||||||
endpoint.unique_id,
|
endpoint.unique_id,
|
||||||
[cluster_handler],
|
[cluster_handler],
|
||||||
quirk_metadata=quirk_metadata,
|
entity_metadata=entity_metadata,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
|
@ -14,7 +14,7 @@ from dataclasses import dataclass
|
|||||||
import enum
|
import enum
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar
|
from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar, overload
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
import zigpy.exceptions
|
import zigpy.exceptions
|
||||||
@ -24,8 +24,33 @@ import zigpy.zcl
|
|||||||
from zigpy.zcl.foundation import CommandSchema
|
from zigpy.zcl.foundation import CommandSchema
|
||||||
import zigpy.zdo.types as zdo_types
|
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.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.core import HomeAssistant, State, callback
|
||||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||||
from homeassistant.helpers.typing import ConfigType
|
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."""
|
"""Determine if a device containing the specified in cluster is paired."""
|
||||||
zha_gateway = get_zha_gateway(hass)
|
zha_gateway = get_zha_gateway(hass)
|
||||||
zha_devices = zha_gateway.devices.values()
|
zha_devices = zha_gateway.devices.values()
|
||||||
@ -424,3 +449,80 @@ def get_zha_gateway(hass: HomeAssistant) -> ZHAGateway:
|
|||||||
raise ValueError("No gateway object exists")
|
raise ValueError("No gateway object exists")
|
||||||
|
|
||||||
return zha_gateway
|
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:
|
if entity_metadata.initially_disabled:
|
||||||
self._attr_entity_registry_enabled_default = False
|
self._attr_entity_registry_enabled_default = False
|
||||||
|
|
||||||
|
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:
|
if entity_metadata.translation_key:
|
||||||
self._attr_translation_key = entity_metadata.translation_key
|
self._attr_translation_key = entity_metadata.translation_key
|
||||||
|
elif has_attribute_name:
|
||||||
if hasattr(entity_metadata.entity_metadata, "attribute_name"):
|
self._attr_translation_key = entity_metadata.attribute_name
|
||||||
if not entity_metadata.translation_key:
|
elif has_command_name:
|
||||||
self._attr_translation_key = (
|
self._attr_translation_key = entity_metadata.command_name
|
||||||
entity_metadata.entity_metadata.attribute_name
|
if has_attribute_name:
|
||||||
)
|
self._unique_id_suffix = entity_metadata.attribute_name
|
||||||
self._unique_id_suffix = entity_metadata.entity_metadata.attribute_name
|
elif has_command_name:
|
||||||
elif hasattr(entity_metadata.entity_metadata, "command_name"):
|
self._unique_id_suffix = 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
|
|
||||||
if entity_metadata.entity_type is EntityType.CONFIG:
|
if entity_metadata.entity_type is EntityType.CONFIG:
|
||||||
self._attr_entity_category = EntityCategory.CONFIG
|
self._attr_entity_category = EntityCategory.CONFIG
|
||||||
elif entity_metadata.entity_type is EntityType.DIAGNOSTIC:
|
elif entity_metadata.entity_type is EntityType.DIAGNOSTIC:
|
||||||
self._attr_entity_category = EntityCategory.DIAGNOSTIC
|
self._attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||||
|
else:
|
||||||
|
self._attr_entity_category = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
"pyserial-asyncio==0.6",
|
"pyserial-asyncio==0.6",
|
||||||
"zha-quirks==0.0.112",
|
"zha-quirks==0.0.112",
|
||||||
"zigpy-deconz==0.23.1",
|
"zigpy-deconz==0.23.1",
|
||||||
"zigpy==0.63.4",
|
"zigpy==0.63.5",
|
||||||
"zigpy-xbee==0.20.1",
|
"zigpy-xbee==0.20.1",
|
||||||
"zigpy-zigate==0.12.0",
|
"zigpy-zigate==0.12.0",
|
||||||
"zigpy-znp==0.12.1",
|
"zigpy-znp==0.12.1",
|
||||||
|
@ -6,10 +6,10 @@ import functools
|
|||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any, Self
|
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 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.config_entries import ConfigEntry
|
||||||
from homeassistant.const import EntityCategory, Platform, UnitOfMass, UnitOfTemperature
|
from homeassistant.const import EntityCategory, Platform, UnitOfMass, UnitOfTemperature
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
@ -26,11 +26,11 @@ from .core.const import (
|
|||||||
CLUSTER_HANDLER_LEVEL,
|
CLUSTER_HANDLER_LEVEL,
|
||||||
CLUSTER_HANDLER_OCCUPANCY,
|
CLUSTER_HANDLER_OCCUPANCY,
|
||||||
CLUSTER_HANDLER_THERMOSTAT,
|
CLUSTER_HANDLER_THERMOSTAT,
|
||||||
QUIRK_METADATA,
|
ENTITY_METADATA,
|
||||||
SIGNAL_ADD_ENTITIES,
|
SIGNAL_ADD_ENTITIES,
|
||||||
SIGNAL_ATTR_UPDATED,
|
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 .core.registries import ZHA_ENTITIES
|
||||||
from .entity import ZhaEntity
|
from .entity import ZhaEntity
|
||||||
|
|
||||||
@ -403,7 +403,7 @@ class ZHANumberConfigurationEntity(ZhaEntity, NumberEntity):
|
|||||||
Return entity if it is a supported configuration, otherwise return None
|
Return entity if it is a supported configuration, otherwise return None
|
||||||
"""
|
"""
|
||||||
cluster_handler = cluster_handlers[0]
|
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
|
cls._attribute_name in cluster_handler.cluster.unsupported_attributes
|
||||||
or cls._attribute_name not in cluster_handler.cluster.attributes_by_name
|
or cls._attribute_name not in cluster_handler.cluster.attributes_by_name
|
||||||
or cluster_handler.cluster.get(cls._attribute_name) is None
|
or cluster_handler.cluster.get(cls._attribute_name) is None
|
||||||
@ -426,26 +426,34 @@ class ZHANumberConfigurationEntity(ZhaEntity, NumberEntity):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Init this number configuration entity."""
|
"""Init this number configuration entity."""
|
||||||
self._cluster_handler: ClusterHandler = cluster_handlers[0]
|
self._cluster_handler: ClusterHandler = cluster_handlers[0]
|
||||||
if QUIRK_METADATA in kwargs:
|
if ENTITY_METADATA in kwargs:
|
||||||
self._init_from_quirks_metadata(kwargs[QUIRK_METADATA])
|
self._init_from_quirks_metadata(kwargs[ENTITY_METADATA])
|
||||||
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
|
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."""
|
"""Init this entity from the quirks metadata."""
|
||||||
super()._init_from_quirks_metadata(entity_metadata)
|
super()._init_from_quirks_metadata(entity_metadata)
|
||||||
number_metadata: NumberMetadata = entity_metadata.entity_metadata
|
self._attribute_name = entity_metadata.attribute_name
|
||||||
self._attribute_name = number_metadata.attribute_name
|
|
||||||
|
|
||||||
if number_metadata.min is not None:
|
if entity_metadata.min is not None:
|
||||||
self._attr_native_min_value = number_metadata.min
|
self._attr_native_min_value = entity_metadata.min
|
||||||
if number_metadata.max is not None:
|
if entity_metadata.max is not None:
|
||||||
self._attr_native_max_value = number_metadata.max
|
self._attr_native_max_value = entity_metadata.max
|
||||||
if number_metadata.step is not None:
|
if entity_metadata.step is not None:
|
||||||
self._attr_native_step = number_metadata.step
|
self._attr_native_step = entity_metadata.step
|
||||||
if number_metadata.unit is not None:
|
if entity_metadata.multiplier is not None:
|
||||||
self._attr_native_unit_of_measurement = number_metadata.unit
|
self._attr_multiplier = entity_metadata.multiplier
|
||||||
if number_metadata.multiplier is not None:
|
if entity_metadata.device_class is not None:
|
||||||
self._attr_multiplier = number_metadata.multiplier
|
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
|
@property
|
||||||
def native_value(self) -> float:
|
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.magnet_ac01 import OppleCluster as MagnetAC01OppleCluster
|
||||||
from zhaquirks.xiaomi.aqara.switch_acn047 import OppleCluster as T2RelayOppleCluster
|
from zhaquirks.xiaomi.aqara.switch_acn047 import OppleCluster as T2RelayOppleCluster
|
||||||
from zigpy import types
|
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.general import OnOff
|
||||||
from zigpy.zcl.clusters.security import IasWd
|
from zigpy.zcl.clusters.security import IasWd
|
||||||
|
|
||||||
@ -29,7 +29,7 @@ from .core.const import (
|
|||||||
CLUSTER_HANDLER_INOVELLI,
|
CLUSTER_HANDLER_INOVELLI,
|
||||||
CLUSTER_HANDLER_OCCUPANCY,
|
CLUSTER_HANDLER_OCCUPANCY,
|
||||||
CLUSTER_HANDLER_ON_OFF,
|
CLUSTER_HANDLER_ON_OFF,
|
||||||
QUIRK_METADATA,
|
ENTITY_METADATA,
|
||||||
SIGNAL_ADD_ENTITIES,
|
SIGNAL_ADD_ENTITIES,
|
||||||
SIGNAL_ATTR_UPDATED,
|
SIGNAL_ATTR_UPDATED,
|
||||||
Strobe,
|
Strobe,
|
||||||
@ -179,7 +179,7 @@ class ZCLEnumSelectEntity(ZhaEntity, SelectEntity):
|
|||||||
Return entity if it is a supported configuration, otherwise return None
|
Return entity if it is a supported configuration, otherwise return None
|
||||||
"""
|
"""
|
||||||
cluster_handler = cluster_handlers[0]
|
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
|
cls._attribute_name in cluster_handler.cluster.unsupported_attributes
|
||||||
or cls._attribute_name not in cluster_handler.cluster.attributes_by_name
|
or cls._attribute_name not in cluster_handler.cluster.attributes_by_name
|
||||||
or cluster_handler.cluster.get(cls._attribute_name) is None
|
or cluster_handler.cluster.get(cls._attribute_name) is None
|
||||||
@ -202,17 +202,16 @@ class ZCLEnumSelectEntity(ZhaEntity, SelectEntity):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Init this select entity."""
|
"""Init this select entity."""
|
||||||
self._cluster_handler: ClusterHandler = cluster_handlers[0]
|
self._cluster_handler: ClusterHandler = cluster_handlers[0]
|
||||||
if QUIRK_METADATA in kwargs:
|
if ENTITY_METADATA in kwargs:
|
||||||
self._init_from_quirks_metadata(kwargs[QUIRK_METADATA])
|
self._init_from_quirks_metadata(kwargs[ENTITY_METADATA])
|
||||||
self._attr_options = [entry.name.replace("_", " ") for entry in self._enum]
|
self._attr_options = [entry.name.replace("_", " ") for entry in self._enum]
|
||||||
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
|
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."""
|
"""Init this entity from the quirks metadata."""
|
||||||
super()._init_from_quirks_metadata(entity_metadata)
|
super()._init_from_quirks_metadata(entity_metadata)
|
||||||
zcl_enum_metadata: ZCLEnumMetadata = entity_metadata.entity_metadata
|
self._attribute_name = entity_metadata.attribute_name
|
||||||
self._attribute_name = zcl_enum_metadata.attribute_name
|
self._enum = entity_metadata.enum
|
||||||
self._enum = zcl_enum_metadata.enum
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_option(self) -> str | None:
|
def current_option(self) -> str | None:
|
||||||
|
@ -13,7 +13,7 @@ import random
|
|||||||
from typing import TYPE_CHECKING, Any, Self
|
from typing import TYPE_CHECKING, Any, Self
|
||||||
|
|
||||||
from zigpy import types
|
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.state import Counter, State
|
||||||
from zigpy.zcl.clusters.closures import WindowCovering
|
from zigpy.zcl.clusters.closures import WindowCovering
|
||||||
from zigpy.zcl.clusters.general import Basic
|
from zigpy.zcl.clusters.general import Basic
|
||||||
@ -71,11 +71,11 @@ from .core.const import (
|
|||||||
CLUSTER_HANDLER_TEMPERATURE,
|
CLUSTER_HANDLER_TEMPERATURE,
|
||||||
CLUSTER_HANDLER_THERMOSTAT,
|
CLUSTER_HANDLER_THERMOSTAT,
|
||||||
DATA_ZHA,
|
DATA_ZHA,
|
||||||
QUIRK_METADATA,
|
ENTITY_METADATA,
|
||||||
SIGNAL_ADD_ENTITIES,
|
SIGNAL_ADD_ENTITIES,
|
||||||
SIGNAL_ATTR_UPDATED,
|
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 .core.registries import SMARTTHINGS_HUMIDITY_CLUSTER, ZHA_ENTITIES
|
||||||
from .entity import BaseZhaEntity, ZhaEntity
|
from .entity import BaseZhaEntity, ZhaEntity
|
||||||
|
|
||||||
@ -154,7 +154,7 @@ class Sensor(ZhaEntity, SensorEntity):
|
|||||||
Return entity if it is a supported configuration, otherwise return None
|
Return entity if it is a supported configuration, otherwise return None
|
||||||
"""
|
"""
|
||||||
cluster_handler = cluster_handlers[0]
|
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
|
cls._attribute_name in cluster_handler.cluster.unsupported_attributes
|
||||||
or cls._attribute_name not in cluster_handler.cluster.attributes_by_name
|
or cls._attribute_name not in cluster_handler.cluster.attributes_by_name
|
||||||
):
|
):
|
||||||
@ -176,21 +176,29 @@ class Sensor(ZhaEntity, SensorEntity):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Init this sensor."""
|
"""Init this sensor."""
|
||||||
self._cluster_handler: ClusterHandler = cluster_handlers[0]
|
self._cluster_handler: ClusterHandler = cluster_handlers[0]
|
||||||
if QUIRK_METADATA in kwargs:
|
if ENTITY_METADATA in kwargs:
|
||||||
self._init_from_quirks_metadata(kwargs[QUIRK_METADATA])
|
self._init_from_quirks_metadata(kwargs[ENTITY_METADATA])
|
||||||
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
|
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."""
|
"""Init this entity from the quirks metadata."""
|
||||||
super()._init_from_quirks_metadata(entity_metadata)
|
super()._init_from_quirks_metadata(entity_metadata)
|
||||||
sensor_metadata: ZCLSensorMetadata = entity_metadata.entity_metadata
|
self._attribute_name = entity_metadata.attribute_name
|
||||||
self._attribute_name = sensor_metadata.attribute_name
|
if entity_metadata.divisor is not None:
|
||||||
if sensor_metadata.divisor is not None:
|
self._divisor = entity_metadata.divisor
|
||||||
self._divisor = sensor_metadata.divisor
|
if entity_metadata.multiplier is not None:
|
||||||
if sensor_metadata.multiplier is not None:
|
self._multiplier = entity_metadata.multiplier
|
||||||
self._multiplier = sensor_metadata.multiplier
|
if entity_metadata.device_class is not None:
|
||||||
if sensor_metadata.unit is not None:
|
self._attr_device_class = validate_device_class(
|
||||||
self._attr_native_unit_of_measurement = sensor_metadata.unit
|
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:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Run when about to be added to hass."""
|
"""Run when about to be added to hass."""
|
||||||
@ -355,12 +363,22 @@ class EnumSensor(Sensor):
|
|||||||
_attr_device_class: SensorDeviceClass = SensorDeviceClass.ENUM
|
_attr_device_class: SensorDeviceClass = SensorDeviceClass.ENUM
|
||||||
_enum: type[enum.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."""
|
"""Init this entity from the quirks metadata."""
|
||||||
ZhaEntity._init_from_quirks_metadata(self, entity_metadata) # pylint: disable=protected-access
|
ZhaEntity._init_from_quirks_metadata(self, entity_metadata) # pylint: disable=protected-access
|
||||||
sensor_metadata: ZCLEnumMetadata = entity_metadata.entity_metadata
|
self._attribute_name = entity_metadata.attribute_name
|
||||||
self._attribute_name = sensor_metadata.attribute_name
|
self._enum = entity_metadata.enum
|
||||||
self._enum = sensor_metadata.enum
|
|
||||||
|
|
||||||
def formatter(self, value: int) -> str | None:
|
def formatter(self, value: int) -> str | None:
|
||||||
"""Use name of enum."""
|
"""Use name of enum."""
|
||||||
|
@ -7,7 +7,7 @@ import logging
|
|||||||
from typing import TYPE_CHECKING, Any, Self
|
from typing import TYPE_CHECKING, Any, Self
|
||||||
|
|
||||||
from zhaquirks.quirk_ids import TUYA_PLUG_ONOFF
|
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.closures import ConfigStatus, WindowCovering, WindowCoveringMode
|
||||||
from zigpy.zcl.clusters.general import OnOff
|
from zigpy.zcl.clusters.general import OnOff
|
||||||
from zigpy.zcl.foundation import Status
|
from zigpy.zcl.foundation import Status
|
||||||
@ -25,7 +25,7 @@ from .core.const import (
|
|||||||
CLUSTER_HANDLER_COVER,
|
CLUSTER_HANDLER_COVER,
|
||||||
CLUSTER_HANDLER_INOVELLI,
|
CLUSTER_HANDLER_INOVELLI,
|
||||||
CLUSTER_HANDLER_ON_OFF,
|
CLUSTER_HANDLER_ON_OFF,
|
||||||
QUIRK_METADATA,
|
ENTITY_METADATA,
|
||||||
SIGNAL_ADD_ENTITIES,
|
SIGNAL_ADD_ENTITIES,
|
||||||
SIGNAL_ATTR_UPDATED,
|
SIGNAL_ATTR_UPDATED,
|
||||||
)
|
)
|
||||||
@ -192,7 +192,7 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity):
|
|||||||
Return entity if it is a supported configuration, otherwise return None
|
Return entity if it is a supported configuration, otherwise return None
|
||||||
"""
|
"""
|
||||||
cluster_handler = cluster_handlers[0]
|
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
|
cls._attribute_name in cluster_handler.cluster.unsupported_attributes
|
||||||
or cls._attribute_name not in cluster_handler.cluster.attributes_by_name
|
or cls._attribute_name not in cluster_handler.cluster.attributes_by_name
|
||||||
or cluster_handler.cluster.get(cls._attribute_name) is None
|
or cluster_handler.cluster.get(cls._attribute_name) is None
|
||||||
@ -215,21 +215,20 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Init this number configuration entity."""
|
"""Init this number configuration entity."""
|
||||||
self._cluster_handler: ClusterHandler = cluster_handlers[0]
|
self._cluster_handler: ClusterHandler = cluster_handlers[0]
|
||||||
if QUIRK_METADATA in kwargs:
|
if ENTITY_METADATA in kwargs:
|
||||||
self._init_from_quirks_metadata(kwargs[QUIRK_METADATA])
|
self._init_from_quirks_metadata(kwargs[ENTITY_METADATA])
|
||||||
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
|
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."""
|
"""Init this entity from the quirks metadata."""
|
||||||
super()._init_from_quirks_metadata(entity_metadata)
|
super()._init_from_quirks_metadata(entity_metadata)
|
||||||
switch_metadata: SwitchMetadata = entity_metadata.entity_metadata
|
self._attribute_name = entity_metadata.attribute_name
|
||||||
self._attribute_name = switch_metadata.attribute_name
|
if entity_metadata.invert_attribute_name:
|
||||||
if switch_metadata.invert_attribute_name:
|
self._inverter_attribute_name = entity_metadata.invert_attribute_name
|
||||||
self._inverter_attribute_name = switch_metadata.invert_attribute_name
|
if entity_metadata.force_inverted:
|
||||||
if switch_metadata.force_inverted:
|
self._force_inverted = entity_metadata.force_inverted
|
||||||
self._force_inverted = switch_metadata.force_inverted
|
self._off_value = entity_metadata.off_value
|
||||||
self._off_value = switch_metadata.off_value
|
self._on_value = entity_metadata.on_value
|
||||||
self._on_value = switch_metadata.on_value
|
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Run when about to be added to hass."""
|
"""Run when about to be added to hass."""
|
||||||
|
@ -2952,7 +2952,7 @@ zigpy-zigate==0.12.0
|
|||||||
zigpy-znp==0.12.1
|
zigpy-znp==0.12.1
|
||||||
|
|
||||||
# homeassistant.components.zha
|
# homeassistant.components.zha
|
||||||
zigpy==0.63.4
|
zigpy==0.63.5
|
||||||
|
|
||||||
# homeassistant.components.zoneminder
|
# homeassistant.components.zoneminder
|
||||||
zm-py==0.5.4
|
zm-py==0.5.4
|
||||||
|
@ -2281,7 +2281,7 @@ zigpy-zigate==0.12.0
|
|||||||
zigpy-znp==0.12.1
|
zigpy-znp==0.12.1
|
||||||
|
|
||||||
# homeassistant.components.zha
|
# homeassistant.components.zha
|
||||||
zigpy==0.63.4
|
zigpy==0.63.5
|
||||||
|
|
||||||
# homeassistant.components.zwave_js
|
# homeassistant.components.zwave_js
|
||||||
zwave-js-server-python==0.55.3
|
zwave-js-server-python==0.55.3
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
"""Test ZHA device discovery."""
|
"""Test ZHA device discovery."""
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
import enum
|
||||||
|
import itertools
|
||||||
import re
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest import mock
|
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
|
from zigpy.const import SIG_ENDPOINTS, SIG_MANUFACTURER, SIG_MODEL, SIG_NODE_DESC
|
||||||
import zigpy.profiles.zha
|
import zigpy.profiles.zha
|
||||||
import zigpy.quirks
|
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
|
from zigpy.quirks.v2.homeassistant import UnitOfTime
|
||||||
import zigpy.types
|
import zigpy.types
|
||||||
from zigpy.zcl import ClusterType
|
from zigpy.zcl import ClusterType
|
||||||
@ -40,6 +51,7 @@ from homeassistant.const import STATE_OFF, Platform
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
from homeassistant.helpers.entity_platform import EntityPlatform
|
from homeassistant.helpers.entity_platform import EntityPlatform
|
||||||
|
from homeassistant.util.json import load_json
|
||||||
|
|
||||||
from .common import find_entity_id, update_attribute_cache
|
from .common import find_entity_id, update_attribute_cache
|
||||||
from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE
|
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,
|
step=1,
|
||||||
unit=UnitOfTime.SECONDS,
|
unit=UnitOfTime.SECONDS,
|
||||||
multiplier=1,
|
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_platform=Platform.SENSOR,
|
||||||
entity_type=EntityType.DIAGNOSTIC,
|
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)
|
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
|
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(
|
zigpy_device = zigpy_device_mock(
|
||||||
{
|
{
|
||||||
1: {
|
1: {
|
||||||
@ -703,7 +726,7 @@ def _get_test_device(zigpy_device_mock, manufacturer: str, model: str):
|
|||||||
model=model,
|
model=model,
|
||||||
)
|
)
|
||||||
|
|
||||||
(
|
v2_quirk = (
|
||||||
add_to_registry_v2(manufacturer, model, zigpy.quirks._DEVICE_REGISTRY)
|
add_to_registry_v2(manufacturer, model, zigpy.quirks._DEVICE_REGISTRY)
|
||||||
.replaces(PowerConfig1CRCluster)
|
.replaces(PowerConfig1CRCluster)
|
||||||
.replaces(ScenesCluster, cluster_type=ClusterType.Client)
|
.replaces(ScenesCluster, cluster_type=ClusterType.Client)
|
||||||
@ -716,6 +739,7 @@ def _get_test_device(zigpy_device_mock, manufacturer: str, model: str):
|
|||||||
step=1,
|
step=1,
|
||||||
unit=UnitOfTime.SECONDS,
|
unit=UnitOfTime.SECONDS,
|
||||||
multiplier=1,
|
multiplier=1,
|
||||||
|
translation_key="on_off_transition_time",
|
||||||
)
|
)
|
||||||
.number(
|
.number(
|
||||||
zigpy.zcl.clusters.general.OnOff.AttributeDefs.off_wait_time.name,
|
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,
|
step=1,
|
||||||
unit=UnitOfTime.SECONDS,
|
unit=UnitOfTime.SECONDS,
|
||||||
multiplier=1,
|
multiplier=1,
|
||||||
|
translation_key="on_off_transition_time",
|
||||||
)
|
)
|
||||||
.sensor(
|
.sensor(
|
||||||
zigpy.zcl.clusters.general.OnOff.AttributeDefs.off_wait_time.name,
|
zigpy.zcl.clusters.general.OnOff.AttributeDefs.off_wait_time.name,
|
||||||
zigpy.zcl.clusters.general.OnOff.cluster_id,
|
zigpy.zcl.clusters.general.OnOff.cluster_id,
|
||||||
entity_type=EntityType.CONFIG,
|
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 = zigpy.quirks._DEVICE_REGISTRY.get_device(zigpy_device)
|
||||||
zigpy_device.endpoints[1].power.PLUGGED_ATTR_READS = {
|
zigpy_device.endpoints[1].power.PLUGGED_ATTR_READS = {
|
||||||
"battery_voltage": 3,
|
"battery_voltage": 3,
|
||||||
@ -792,14 +821,13 @@ async def test_quirks_v2_entity_discovery_errors(
|
|||||||
|
|
||||||
# fmt: off
|
# fmt: off
|
||||||
entity_details = (
|
entity_details = (
|
||||||
"{'cluster_details': (1, 6, <ClusterType.Server: 0>), "
|
"{'cluster_details': (1, 6, <ClusterType.Server: 0>), 'entity_metadata': "
|
||||||
"'quirk_metadata': EntityMetadata(entity_metadata=ZCLSensorMetadata("
|
"ZCLSensorMetadata(entity_platform=<EntityPlatform.SENSOR: 'sensor'>, "
|
||||||
"attribute_name='off_wait_time', divisor=1, multiplier=1, unit=None, "
|
"entity_type=<EntityType.CONFIG: 'config'>, cluster_id=6, endpoint_id=1, "
|
||||||
"device_class=None, state_class=None), entity_platform=<EntityPlatform."
|
"cluster_type=<ClusterType.Server: 0>, initially_disabled=False, "
|
||||||
"SENSOR: 'sensor'>, entity_type=<EntityType.CONFIG: 'config'>, "
|
"attribute_initialized_from_cache=True, translation_key='analog_input', "
|
||||||
"cluster_id=6, endpoint_id=1, cluster_type=<ClusterType.Server: 0>, "
|
"attribute_name='off_wait_time', divisor=1, multiplier=1, "
|
||||||
"initially_disabled=False, attribute_initialized_from_cache=True, "
|
"unit=None, device_class=None, state_class=None)}"
|
||||||
"translation_key=None)}"
|
|
||||||
)
|
)
|
||||||
# fmt: on
|
# 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 - "
|
m2 = f"details: {entity_details} that does not have an entity class mapping - "
|
||||||
m3 = "unable to create entity"
|
m3 = "unable to create entity"
|
||||||
assert f"{m1}{m2}{m3}" in caplog.text
|
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."""
|
"""Tests for ZHA helpers."""
|
||||||
|
|
||||||
|
import enum
|
||||||
import logging
|
import logging
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import voluptuous_serialize
|
import voluptuous_serialize
|
||||||
import zigpy.profiles.zha as zha
|
import zigpy.profiles.zha as zha
|
||||||
|
from zigpy.quirks.v2.homeassistant import UnitOfPower as QuirksUnitOfPower
|
||||||
from zigpy.types.basic import uint16_t
|
from zigpy.types.basic import uint16_t
|
||||||
import zigpy.zcl.clusters.general as general
|
import zigpy.zcl.clusters.general as general
|
||||||
import zigpy.zcl.clusters.lighting as lighting
|
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 (
|
from homeassistant.components.zha.core.helpers import (
|
||||||
cluster_command_schema_to_vol_schema,
|
cluster_command_schema_to_vol_schema,
|
||||||
convert_to_zcl_values,
|
convert_to_zcl_values,
|
||||||
|
validate_unit,
|
||||||
)
|
)
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform, UnitOfPower
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
@ -40,7 +43,7 @@ def light_platform_only():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@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."""
|
"""Test light."""
|
||||||
|
|
||||||
zigpy_device = zigpy_device_mock(
|
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
|
# No flags are passed through
|
||||||
assert converted_data["update_flags"] == 0
|
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",
|
"motion_sensitivity_disabled",
|
||||||
AqaraMotionSensitivities,
|
AqaraMotionSensitivities,
|
||||||
MotionSensitivityQuirk.OppleCluster.cluster_id,
|
MotionSensitivityQuirk.OppleCluster.cluster_id,
|
||||||
translation_key="motion_sensitivity_translation_key",
|
translation_key="motion_sensitivity",
|
||||||
initially_disabled=True,
|
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
|
assert hass.states.get(entity_id).state == AqaraMotionSensitivities.Low.name
|
||||||
|
|
||||||
entity_registry = er.async_get(hass)
|
entity_registry = er.async_get(hass)
|
||||||
# none in id because the translation key does not exist
|
entity_entry = entity_registry.async_get(entity_id)
|
||||||
entity_entry = entity_registry.async_get("select.fake_manufacturer_fake_model_none")
|
|
||||||
assert entity_entry
|
assert entity_entry
|
||||||
assert entity_entry.entity_category == EntityCategory.CONFIG
|
assert entity_entry.entity_category == EntityCategory.CONFIG
|
||||||
assert entity_entry.disabled is True
|
assert entity_entry.disabled is False
|
||||||
assert entity_entry.translation_key == "motion_sensitivity_translation_key"
|
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
|
assert entity_id is not None
|
||||||
|
|
||||||
await send_attributes_report(hass, cluster, {0x010C: 1})
|
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})
|
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
|
@pytest.fixture
|
||||||
|
Loading…
x
Reference in New Issue
Block a user