Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 26 additions & 6 deletions tests/test_xiaomi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
"Aqara",
"aqara.feeder.acn001",
cluster_ids={
Comment thread
Copilot marked this conversation as resolved.
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=[
Expand Down Expand Up @@ -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
),
Expand Down Expand Up @@ -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(
"Aqara",
"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]] = []
Expand Down
202 changes: 118 additions & 84 deletions zhaquirks/xiaomi/aqara/feeder_acn001.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,24 @@
"""Quirk for Aqara aqara.feeder.acn001."""
"""Quirk V2 for Aqara aqara.feeder.acn001."""

from __future__ import annotations

import logging
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
Expand Down Expand Up @@ -86,8 +75,9 @@
class FeedingSource(types.enum8):
"""Feeding source."""

Schedule = 0x00
Feeder = 0x01
Remote = 0x02
HomeAssistant = 0x02


class FeedingMode(types.enum8):
Expand Down Expand Up @@ -274,65 +264,109 @@ 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")
Comment thread
Copilot marked this conversation as resolved.
# 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,
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,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

state_class=SensorStateClass.TOTAL_INCREASING for portions_dispensed and weight_dispensed matches the existing legacy entities in ZHA.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about this, I could add unit="portions" and keep the legacy state_class=SensorStateClass.TOTAL_INCREASING

    .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",
    )

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",
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()
)
Loading