diff --git a/tests/test_xiaomi.py b/tests/test_xiaomi.py index 318dd393c7..7b8f49a800 100644 --- a/tests/test_xiaomi.py +++ b/tests/test_xiaomi.py @@ -90,9 +90,9 @@ ZCL_PORTIONS_DISPENSED, ZCL_SERVING_SIZE, ZCL_WEIGHT_DISPENSED, - AqaraFeederAcn001, FeedingMode, FeedingSource, + OppleCluster, ) from zhaquirks.xiaomi.aqara.light_acn import AqaraLightT1M, LumiPowerOnStateMode import zhaquirks.xiaomi.aqara.magnet_ac01 @@ -1074,11 +1074,20 @@ async def test_xiaomi_total_active_power_clear(zigpy_device_from_quirk): ], ) async def test_aqara_feeder_write_attrs( - zigpy_device_from_quirk, attribute, value, expected_bytes + zigpy_device_from_v2_quirk, attribute, value, expected_bytes ): """Test Aqara C1 pet feeder attr writing.""" - device = zigpy_device_from_quirk(AqaraFeederAcn001) + device = zigpy_device_from_v2_quirk( + None, + "aqara.feeder.acn001", + cluster_ids={ + 1: { + OnOff.cluster_id: ClusterType.Server, + OppleCluster.cluster_id: ClusterType.Server, + } + }, + ) opple_cluster = device.endpoints[1].opple_cluster opple_cluster._write_attributes = mock.AsyncMock( return_value=[ @@ -1117,7 +1126,9 @@ async def test_aqara_feeder_write_attrs( 3, [ mock.call(ZCL_LAST_FEEDING_SIZE, 3, mock.ANY), - mock.call(ZCL_LAST_FEEDING_SOURCE, FeedingSource.Remote, mock.ANY), + mock.call( + ZCL_LAST_FEEDING_SOURCE, FeedingSource.HomeAssistant, mock.ANY + ), mock.call( FEEDER_ATTR, b"\x00\x05\xd0\x04\x15\x02\xbc\x040203", mock.ANY ), @@ -1210,10 +1221,19 @@ async def test_aqara_feeder_write_attrs( ], ) async def test_aqara_feeder_attr_reports( - zigpy_device_from_quirk, bytes_received, call_count, calls + zigpy_device_from_v2_quirk, bytes_received, call_count, calls ): """Test Aqara C1 pet feeder attr reports and parsing.""" - device = zigpy_device_from_quirk(AqaraFeederAcn001) + device = zigpy_device_from_v2_quirk( + None, + "aqara.feeder.acn001", + cluster_ids={ + 1: { + OnOff.cluster_id: ClusterType.Server, + OppleCluster.cluster_id: ClusterType.Server, + } + }, + ) opple_cluster = device.endpoints[1].opple_cluster attribute_updates: list[tuple[int, Any]] = [] diff --git a/zhaquirks/xiaomi/aqara/feeder_acn001.py b/zhaquirks/xiaomi/aqara/feeder_acn001.py index cf2ef74feb..dce7e3ba4b 100644 --- a/zhaquirks/xiaomi/aqara/feeder_acn001.py +++ b/zhaquirks/xiaomi/aqara/feeder_acn001.py @@ -1,4 +1,4 @@ -"""Quirk for Aqara aqara.feeder.acn001.""" +"""Quirk V2 for Aqara aqara.feeder.acn001.""" from __future__ import annotations @@ -6,30 +6,19 @@ from typing import Any, Final from zigpy import types -from zigpy.profiles import zgp, zha from zigpy.zcl import AttributeReportedEvent, AttributeUpdatedEvent, foundation -from zigpy.zcl.clusters.general import ( - Basic, - GreenPowerProxy, - Groups, - Identify, - OnOff, - Ota, - Scenes, - Time, -) +from zigpy.zcl.clusters.general import OnOff, Time from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef -from zhaquirks.const import ( - DEVICE_TYPE, - ENDPOINTS, - INPUT_CLUSTERS, - MANUFACTURER, - MODEL, - OUTPUT_CLUSTERS, - PROFILE_ID, +from zhaquirks.builder import ( + BinarySensorDeviceClass, + EntityPlatform, + EntityType, + QuirkBuilder, + SensorStateClass, + UnitOfMass, ) -from zhaquirks.xiaomi import XiaomiAqaraE1Cluster, XiaomiCustomDevice +from zhaquirks.xiaomi import XiaomiAqaraE1Cluster # 32 bit signed integer values that are encoded in FEEDER_ATTR = 0xFFF1 FEEDING = 0x04150055 @@ -86,8 +75,9 @@ class FeedingSource(types.enum8): """Feeding source.""" + Schedule = 0x00 Feeder = 0x01 - Remote = 0x02 + HomeAssistant = 0x02 class FeedingMode(types.enum8): @@ -274,65 +264,112 @@ async def write_attributes( return await super().write_attributes(attrs, update_cache=False, **kwargs) -class AqaraFeederAcn001(XiaomiCustomDevice): - """Aqara aqara.feeder.acn001 custom device implementation.""" - - signature = { - MODEL: "aqara.feeder.acn001", - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.ON_OFF_OUTPUT, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - Groups.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - OppleCluster.cluster_id, - ], - OUTPUT_CLUSTERS: [ - Identify.cluster_id, - Ota.cluster_id, - ], - }, - 242: { - PROFILE_ID: zgp.PROFILE_ID, - DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, - INPUT_CLUSTERS: [], - OUTPUT_CLUSTERS: [ - GreenPowerProxy.cluster_id, - ], - }, - }, - } - - replacement = { - MANUFACTURER: "Aqara", - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.ON_OFF_OUTPUT, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - Groups.cluster_id, - Scenes.cluster_id, - OppleCluster, - Time.cluster_id, - ], - OUTPUT_CLUSTERS: [ - Identify.cluster_id, - Ota.cluster_id, - ], - }, - 242: { - PROFILE_ID: zgp.PROFILE_ID, - DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, - INPUT_CLUSTERS: [], - OUTPUT_CLUSTERS: [ - GreenPowerProxy.cluster_id, - ], - }, - }, - } +( + QuirkBuilder(None, "aqara.feeder.acn001") + # device does not provide manufacturer during interview + # The v2 entities only take over cleanly once the + # native classes are removed (zigpy/zha#883) if the model string matches. + .friendly_name(manufacturer="Aqara", model="aqara.feeder.acn001") + .removes(OnOff.cluster_id) + .replaces(Time) + .replaces(OppleCluster) + .enum( + attribute_name=OppleCluster.AttributeDefs.last_feeding_source.name, + enum_class=FeedingSource, + cluster_id=OppleCluster.cluster_id, + entity_platform=EntityPlatform.SENSOR, + entity_type=EntityType.STANDARD, + unique_id_suffix=f"{OppleCluster.cluster_id}-last_feeding_source", + translation_key="last_feeding_source", + fallback_name="Last feeding source", + ) + .sensor( + attribute_name=OppleCluster.AttributeDefs.last_feeding_size.name, + cluster_id=OppleCluster.cluster_id, + unit="portions", + unique_id_suffix=f"{OppleCluster.cluster_id}-last_feeding_size", + translation_key="last_feeding_size", + fallback_name="Last feeding size", + ) + .sensor( + attribute_name=OppleCluster.AttributeDefs.portions_dispensed.name, + cluster_id=OppleCluster.cluster_id, + state_class=SensorStateClass.TOTAL_INCREASING, + unit="portions", + unique_id_suffix=f"{OppleCluster.cluster_id}-portions_dispensed", + translation_key="portions_dispensed_today", + fallback_name="Portions dispensed today", + ) + .sensor( + attribute_name=OppleCluster.AttributeDefs.weight_dispensed.name, + cluster_id=OppleCluster.cluster_id, + unit=UnitOfMass.GRAMS, + state_class=SensorStateClass.TOTAL_INCREASING, + unique_id_suffix=f"{OppleCluster.cluster_id}-weight_dispensed", + translation_key="weight_dispensed_today", + fallback_name="Weight dispensed today", + ) + .switch( + attribute_name=OppleCluster.AttributeDefs.disable_led_indicator.name, + cluster_id=OppleCluster.cluster_id, + force_inverted=True, + unique_id_suffix=f"{OppleCluster.cluster_id}-disable_led_indicator", + translation_key="led_indicator", + fallback_name="LED indicator", + ) + .switch( + attribute_name=OppleCluster.AttributeDefs.child_lock.name, + cluster_id=OppleCluster.cluster_id, + unique_id_suffix=f"{OppleCluster.cluster_id}-child_lock", + translation_key="child_lock", + fallback_name="Child lock", + ) + .enum( + attribute_name=OppleCluster.AttributeDefs.feeding_mode.name, + enum_class=FeedingMode, + cluster_id=OppleCluster.cluster_id, + unique_id_suffix=f"{OppleCluster.cluster_id}-feeding_mode", + translation_key="feeding_mode", + fallback_name="Feeding mode", + ) + .number( + attribute_name=OppleCluster.AttributeDefs.serving_size.name, + cluster_id=OppleCluster.cluster_id, + min_value=1, + max_value=10, + mode="box", + unit="portions", + unique_id_suffix=f"{OppleCluster.cluster_id}-serving_size", + translation_key="serving_size", + fallback_name="Serving size", + ) + .number( + attribute_name=OppleCluster.AttributeDefs.portion_weight.name, + cluster_id=OppleCluster.cluster_id, + min_value=1, + max_value=100, + unit=UnitOfMass.GRAMS, + mode="box", + unique_id_suffix=f"{OppleCluster.cluster_id}-portion_weight", + translation_key="portion_weight", + fallback_name="Portion weight", + ) + .binary_sensor( + attribute_name=OppleCluster.AttributeDefs.error_detected.name, + cluster_id=OppleCluster.cluster_id, + entity_type=EntityType.STANDARD, + device_class=BinarySensorDeviceClass.PROBLEM, + unique_id_suffix=f"{OppleCluster.cluster_id}-error_detected", + fallback_name="Error detected", + ) + .write_attr_button( + attribute_name=OppleCluster.AttributeDefs.feeding.name, + attribute_value=1, + cluster_id=OppleCluster.cluster_id, + entity_type=EntityType.STANDARD, + unique_id_suffix=f"{OppleCluster.cluster_id}-feeding", + translation_key="feed", + fallback_name="Feed", + ) + .add_to_registry() +)