From feaa5d3e4e52afa1e149fd9ceb4a1228196723e1 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 27 Dec 2023 11:37:46 -0500 Subject: [PATCH] Add quirk for Aqara T2 relay (#2821) * Add quirk for Aqara T2 relay * fix cluster * rename mode to switch mode * update based on z2m * add test * oops --- tests/test_xiaomi.py | 26 +++ zhaquirks/xiaomi/aqara/switch_acn047.py | 210 ++++++++++++++++++++++++ 2 files changed, 236 insertions(+) create mode 100644 zhaquirks/xiaomi/aqara/switch_acn047.py diff --git a/tests/test_xiaomi.py b/tests/test_xiaomi.py index 451fd298a6..479549546b 100644 --- a/tests/test_xiaomi.py +++ b/tests/test_xiaomi.py @@ -13,6 +13,7 @@ AnalogInput, AnalogOutput, DeviceTemperature, + MultistateInput, MultistateOutput, OnOff, PowerConfiguration, @@ -1608,6 +1609,31 @@ async def test_xiaomi_e1_roller_commands_1(zigpy_device_from_quirk, command, val assert multistate_cluster._write_attributes.call_args[0][0][0].value.value == value +@pytest.mark.parametrize( + "quirk", + (zhaquirks.xiaomi.aqara.switch_acn047.AqaraT2Relay,), +) +async def test_aqara_t2_relay(zigpy_device_from_quirk, quirk): + """Test Aqara T2 relay.""" + + device = zigpy_device_from_quirk(quirk) + mi_cluster = device.endpoints[1].multistate_input + mi_listener = ClusterListener(mi_cluster) + + mi_cluster.update_attribute(MultistateInput.AttributeDefs.present_value.id, 1) + assert len(mi_listener.attribute_updates) == 1 + assert mi_listener.attribute_updates[0][0] == 0 + assert mi_listener.attribute_updates[0][1] == "single" + + mi_cluster.update_attribute(MultistateInput.AttributeDefs.state_text.id, "foo") + assert len(mi_listener.attribute_updates) == 2 + assert ( + mi_listener.attribute_updates[1][0] + == MultistateInput.AttributeDefs.state_text.id + ) + assert mi_listener.attribute_updates[1][1] == "foo" + + @pytest.mark.parametrize( "command, value", [ diff --git a/zhaquirks/xiaomi/aqara/switch_acn047.py b/zhaquirks/xiaomi/aqara/switch_acn047.py new file mode 100644 index 0000000000..e13e9ef89c --- /dev/null +++ b/zhaquirks/xiaomi/aqara/switch_acn047.py @@ -0,0 +1,210 @@ +"""Aqara T2 relay device.""" +from zigpy import types as t +from zigpy.profiles import zha +from zigpy.quirks import CustomCluster +from zigpy.zcl.clusters.general import ( + AnalogInput, + Basic, + DeviceTemperature, + Groups, + Identify, + MultistateInput, + OnOff, + Ota, + Scenes, + Time, +) +from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement +from zigpy.zcl.clusters.smartenergy import Metering + +from zhaquirks.const import ( + ATTR_ID, + COMMAND, + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PRESS_TYPE, + PROFILE_ID, + SHORT_PRESS, + VALUE, + ZHA_SEND_EVENT, +) +from zhaquirks.xiaomi import ( + AnalogInputCluster, + BasicCluster, + ElectricalMeasurementCluster, + MeteringCluster, + XiaomiAqaraE1Cluster, + XiaomiCustomDevice, +) + +PRESS_TYPES = {1: "single"} +SINGLE = "single" +STATUS_TYPE_ATTR = 0x0055 # decimal = 85 + + +class MultistateInputCluster(CustomCluster, MultistateInput): + """Multistate input cluster.""" + + def __init__(self, *args, **kwargs): + """Init.""" + self._current_state = None + super().__init__(*args, **kwargs) + + def _update_attribute(self, attrid, value): + if attrid == STATUS_TYPE_ATTR: + self._current_state = PRESS_TYPES.get(value) + event_args = { + PRESS_TYPE: self._current_state, + ATTR_ID: attrid, + VALUE: value, + } + self.listener_event(ZHA_SEND_EVENT, self, self._current_state, event_args) + super()._update_attribute(0, self._current_state) + else: + super()._update_attribute(attrid, value) + + +class OppleCluster(XiaomiAqaraE1Cluster): + """Opple cluster.""" + + class SwitchType(t.enum8): + """Switch type.""" + + Toggle = 0x01 + Momentary = 0x02 + NoSwitch = 0x03 + + class StartupOnOff(t.enum8): + """Startup mode.""" + + On = 0x00 + Previous = 0x01 + Off = 0x02 + Toggle = 0x03 + + class DecoupledMode(t.enum8): + """Decoupled mode.""" + + Decoupled = 0x00 + ControlRelay = 0x01 + + class SwitchMode(t.enum8): + """Switch Mode.""" + + Power = 0x00 + Pulse = 0x01 + Dry = 0x03 + + attributes = { + 0x000A: ("switch_type", t.uint8_t, True), + 0x0517: ("startup_on_off", t.uint8_t, True), + 0x0200: ("decoupled_mode", t.uint8_t, True), + 0x02D0: ("interlock", t.Bool, True), + 0x0289: ("switch_mode", t.uint8_t, True), + 0x00EB: ("pulse_length", t.uint16_t, True), + } + + +class AqaraT2Relay(XiaomiCustomDevice): + """Aqara T2 in-wall relay device.""" + + signature = { + MODELS_INFO: [("Aqara", "lumi.switch.acn047")], + ENDPOINTS: { + # + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Scenes.cluster_id, + Groups.cluster_id, + OnOff.cluster_id, + MultistateInput.cluster_id, + Metering.cluster_id, + ElectricalMeasurement.cluster_id, + OppleCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + }, + # + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [ + Identify.cluster_id, + Scenes.cluster_id, + Groups.cluster_id, + OnOff.cluster_id, + MultistateInput.cluster_id, + OppleCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [], + }, + # + 21: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [AnalogInput.cluster_id], + OUTPUT_CLUSTERS: [], + }, + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [ + BasicCluster, + Identify.cluster_id, + Scenes.cluster_id, + Groups.cluster_id, + OnOff.cluster_id, + MultistateInputCluster, + DeviceTemperature.cluster_id, + MeteringCluster, + ElectricalMeasurementCluster, + OppleCluster, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + }, + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [ + Identify.cluster_id, + Scenes.cluster_id, + Groups.cluster_id, + OnOff.cluster_id, + MultistateInputCluster, + OppleCluster, + ], + OUTPUT_CLUSTERS: [], + }, + 21: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [AnalogInputCluster], + OUTPUT_CLUSTERS: [], + }, + }, + } + + device_automation_triggers = { + (SHORT_PRESS, SHORT_PRESS): {COMMAND: SINGLE}, + }