zigpy / zha

Zigbee Home Automation
Apache License 2.0
26 stars 25 forks source link

Quirks v2 unable to specify UnitOfLength units for sensor and number entities #290

Open jeverley opened 2 weeks ago

jeverley commented 2 weeks ago

I'm currently writing a quirk for the Aquara FP1E presence sensor https://[github.com/zigpy/zha-device-handlers/issues/3294](https://github.com/zigpy/zha-device-handlers/issues/3294) and I've observed that specifying a UnitOfLength unit for number and sensor entities results in an error during their creation.

Logger: zha.application.gateway
Source: /usr/local/lib/python3.12/site-packages/zha/application/gateway.py:339
First occurred: 12:13:13 (2 occurrences)
Last logged: 12:13:13

Failed to create platform entity: <class 'zha.application.platforms.number.NumberConfigurationEntity'> [args=('54:ef:44:10:00:db:47:bb-1', [<zha.zigbee.cluster_handlers.manufacturerspecific.OppleRemoteClusterHandler object at 0x7f734e8da0>], <zha.zigbee.endpoint.Endpoint object at 0x7f734e9fd0>, <CustomDeviceV2 model='lumi.sensor_occupy.agl1' manuf='aqara' nwk=0xBBCE ieee=54:ef:44:10:00:db:47:bb is_initialized=True> - quirk_applied: True - quirk_or_device_class: zigpy.quirks.v2.CustomDeviceV2 - quirk_id: None), kwargs={'entity_metadata': NumberMetadata(entity_platform=<EntityPlatform.NUMBER: 'number'>, entity_type=<EntityType.CONFIG: 'config'>, cluster_id=64704, endpoint_id=1, cluster_type=<ClusterType.Server: 0>, initially_disabled=False, attribute_initialized_from_cache=True, translation_key='approach_distance', fallback_name='Approach distance', attribute_name='approach_distance', reporting_config=None, min=0, max=6, step=0.1, unit=<UnitOfLength.METERS: 'm'>, mode=None, multiplier=0.01, device_class=<NumberDeviceClass.DISTANCE: 'distance'>)}]
Failed to create platform entity: <class 'zha.application.platforms.sensor.Sensor'> [args=('54:ef:44:10:00:db:47:bb-1', [<zha.zigbee.cluster_handlers.manufacturerspecific.OppleRemoteClusterHandler object at 0x7f734e8da0>], <zha.zigbee.endpoint.Endpoint object at 0x7f734e9fd0>, <CustomDeviceV2 model='lumi.sensor_occupy.agl1' manuf='aqara' nwk=0xBBCE ieee=54:ef:44:10:00:db:47:bb is_initialized=True> - quirk_applied: True - quirk_or_device_class: zigpy.quirks.v2.CustomDeviceV2 - quirk_id: None), kwargs={'entity_metadata': ZCLSensorMetadata(entity_platform=<EntityPlatform.SENSOR: 'sensor'>, entity_type=<EntityType.STANDARD: 'standard'>, cluster_id=64704, endpoint_id=1, cluster_type=<ClusterType.Server: 0>, initially_disabled=False, attribute_initialized_from_cache=True, translation_key='motion_distance', fallback_name='Motion distance', attribute_name='motion_distance', reporting_config=None, divisor=1, multiplier=0.01, unit=<UnitOfLength.METERS: 'm'>, device_class=<SensorDeviceClass.DISTANCE: 'distance'>, state_class=<SensorStateClass.MEASUREMENT: 'measurement'>)}]
Traceback (most recent call last):
  File "/usr/local/lib/python3.12/site-packages/zha/application/gateway.py", line 335, in create_platform_entities
    platform_entity = platform_entity_class.create_platform_entity(
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/zha/application/platforms/number/__init__.py", line 229, in create_platform_entity
    return cls(unique_id, cluster_handlers, endpoint, device, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/zha/application/platforms/number/__init__.py", line 242, in __init__
    super().__init__(unique_id, cluster_handlers, endpoint, device, **kwargs)
  File "/usr/local/lib/python3.12/site-packages/zha/application/platforms/__init__.py", line 297, in __init__
    self._init_from_quirks_metadata(entity_metadata)
  File "/usr/local/lib/python3.12/site-packages/zha/application/platforms/number/__init__.py", line 269, in _init_from_quirks_metadata
    self._attr_native_unit_of_measurement = validate_unit(
                                            ^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/zha/units.py", line 167, in validate_unit
    return UNITS_OF_MEASURE[type(external_unit).__name__](external_unit.value)
           ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
KeyError: 'UnitOfLength'

Omitting the unit results in their successful creation, however a warning is logged about None being an invalid unit for DISTANCE device classes.

Logger: homeassistant.components.sensor
Source: components/sensor/__init__.py:733
integration: Sensor (documentation, issues)
First occurred: 12:20:32 (1 occurrences)
Last logged: 12:20:32

Entity sensor.living_room_fp1e_distance (<class 'homeassistant.components.zha.sensor.Sensor'>) is using native unit of measurement 'None' which is not a valid unit for the device class ('distance') it is using; expected one of ['mi', 'nmi', 'km', 'in', 'mm', 'yd', 'm', 'ft', 'cm']; Please update your configuration if your entity is manually configured, otherwise create a bug report at https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+zha%22

I've determined that this is because zha/units.py is currently missing the units for distance outlined in zigpy/quirks/v2/homeassistant/init.py

This can be replicated using the following entity definitions:

from zigpy.quirks.v2 import (
    QuirkBuilder,
    NumberDeviceClass,
    SensorDeviceClass,
    SensorStateClass,
)
from zigpy.quirks.v2.homeassistant import EntityPlatform, EntityType, UnitOfLength

-- snip --

    .number(
        OppleCluster.AttributeDefs.approach_distance.name,
        OppleCluster.cluster_id,
        min_value=0,
        max_value=6,
        step=0.1,
        unit=UnitOfLength.METERS,
        multiplier=0.01,
        device_class=NumberDeviceClass.DISTANCE,
        translation_key="approach_distance",
        fallback_name="Approach distance",
    )
    .sensor(
        OppleCluster.AttributeDefs.motion_distance.name,
        OppleCluster.cluster_id,
        unit=UnitOfLength.METERS,
        multiplier=0.01,
        device_class=SensorDeviceClass.DISTANCE,
        state_class=SensorStateClass.MEASUREMENT,
        fallback_name="Distance",
    )
jeverley commented 2 weeks ago

Further testing has shown that this can also be reproduced if device_class is omitted.