diff --git a/tests/common.py b/tests/common.py index 319cd19dff..11bad7736f 100644 --- a/tests/common.py +++ b/tests/common.py @@ -12,6 +12,7 @@ def __init__(self, cluster): """Init instance.""" self.cluster_commands = [] self.attribute_updates = [] + self.events_sent = [] cluster.add_listener(self) def attribute_updated(self, attr_id, value): @@ -21,3 +22,7 @@ def attribute_updated(self, attr_id, value): def cluster_command(self, tsn, commdand_id, args): """Command received listener.""" self.cluster_commands.append((tsn, commdand_id, args)) + + def zha_send_event(self, action, args): + """Send event listener.""" + self.events_sent.append((action, args)) diff --git a/tests/test_tuya.py b/tests/test_tuya.py index 43ad38d95a..8c0c12d562 100644 --- a/tests/test_tuya.py +++ b/tests/test_tuya.py @@ -9,23 +9,26 @@ from zigpy.quirks import CustomDevice, get_device import zigpy.types as t from zigpy.zcl import foundation - +from zigpy.zcl.clusters.general import Groups, OnOff import zhaquirks from zhaquirks.const import ( DEVICE_TYPE, ENDPOINTS, INPUT_CLUSTERS, + LONG_PRESS, MODELS_INFO, OFF, ON, OUTPUT_CLUSTERS, PROFILE_ID, + SHORT_PRESS, ZONE_STATE, ) from zhaquirks.tuya import Data, TuyaManufClusterAttributes import zhaquirks.tuya.ts0042 import zhaquirks.tuya.ts0043 import zhaquirks.tuya.ts0601_electric_heating +import zhaquirks.tuya.ts1001 import zhaquirks.tuya.ts0601_motion import zhaquirks.tuya.ts0601_siren import zhaquirks.tuya.ts0601_trv @@ -39,6 +42,8 @@ ZCL_TUYA_MOTION = b"\tL\x01\x00\x05\x03\x04\x00\x01\x02" ZCL_TUYA_SWITCH_ON = b"\tQ\x02\x006\x01\x01\x00\x01\x01" ZCL_TUYA_SWITCH_OFF = b"\tQ\x02\x006\x01\x01\x00\x01\x00" +ZCL_TUYA_1001_SWITCH_ON = b'\x01\x1c\xfd\x00' +ZCL_TUYA_1001_SWITCH_OFF = b'\x19\xde\x02\xff\x00' ZCL_TUYA_ATTRIBUTE_617_TO_179 = b"\tp\x02\x00\x02i\x02\x00\x04\x00\x00\x00\xb3" ZCL_TUYA_SIREN_TEMPERATURE = ZCL_TUYA_ATTRIBUTE_617_TO_179 ZCL_TUYA_SIREN_HUMIDITY = b"\tp\x02\x00\x02j\x02\x00\x04\x00\x00\x00U" @@ -132,6 +137,41 @@ async def test_singleswitch_state_report(zigpy_device_from_quirk, quirk): assert switch_listener.attribute_updates[1][0] == 0x0000 assert switch_listener.attribute_updates[1][1] == OFF +@pytest.mark.parametrize("quirk", (zhaquirks.tuya.ts1001.TuyaDimRemote1001,)) +async def test_ts1001_state_report(zigpy_device_from_quirk, quirk): + """Test ts1001 4 button switch.""" + + switch_dev = zigpy_device_from_quirk(quirk) + + switch_cluster = switch_dev.endpoints[1].out_clusters[OnOff.cluster_id] + switch_listener = ClusterListener(switch_cluster) + + # tuya_cluster = switch_dev.endpoints[1].tuya_manufacturer + + hdr, args = switch_cluster.deserialize(ZCL_TUYA_1001_SWITCH_ON) + switch_cluster.handle_message(hdr, args) + + # We expect to have seen an event sent and a command (response to device) + assert len(switch_listener.cluster_commands) == 1 + assert len(switch_listener.events_sent) == 1 + assert len(switch_listener.attribute_updates) == 0 + assert switch_listener.events_sent[0][0] == SHORT_PRESS + + # Test the off switch. This comes in as command 0x02 on + # cluster 4 (groups) + groups_cluster = switch_dev.endpoints[1].out_clusters[Groups.cluster_id] + groups_listener = ClusterListener(groups_cluster) + + # tuya_cluster = switch_dev.endpoints[1].tuya_manufacturer + + hdr, args = groups_cluster.deserialize(ZCL_TUYA_1001_SWITCH_OFF) + groups_cluster.handle_message(hdr, args) + + assert len(groups_listener.cluster_commands) == 1 + assert len(groups_listener.events_sent) == 1 + assert len(groups_listener.attribute_updates) == 0 + assert groups_listener.events_sent[0][0] == SHORT_PRESS + @pytest.mark.parametrize("quirk", (zhaquirks.tuya.ts0601_switch.TuyaDoubleSwitchTO,)) async def test_doubleswitch_state_report(zigpy_device_from_quirk, quirk): @@ -1208,6 +1248,7 @@ async def async_success(*args, **kwargs): (zhaquirks.tuya.ts0044.TuyaSmartRemote0044TO, "_TZ3400_cdyjhasw"), (zhaquirks.tuya.ts0044.TuyaSmartRemote0044TO, "_TZ3400_pdyjhapl"), (zhaquirks.tuya.ts0044.TuyaSmartRemote0044TO, "_some_random_manuf"), + (zhaquirks.tuya.ts1001.TuyaDimRemote1001, "_TZ3000_ztrfrcsu"), ), ) async def test_tuya_wildcard_manufacturer(zigpy_device_from_quirk, quirk, manufacturer): diff --git a/zhaquirks/tuya/ts1001.py b/zhaquirks/tuya/ts1001.py new file mode 100644 index 0000000000..ce0f62c59c --- /dev/null +++ b/zhaquirks/tuya/ts1001.py @@ -0,0 +1,157 @@ +"""Tuya 4 Button Remote.""" + +from typing import Any, List, Optional, Union + +from zhaquirks import GroupBoundCluster +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +import zigpy.types as t +from zigpy.zcl import foundation +from zigpy.zcl.clusters.general import Basic, Identify, LevelControl, OnOff, Ota, PowerConfiguration, Scenes, Time, Groups +from zigpy.zcl.clusters.lightlink import LightLink + +from zhaquirks.const import ( + TURN_ON, + TURN_OFF, + DIM_UP, + DIM_DOWN, + COMMAND, + DEVICE_TYPE, + DOUBLE_PRESS, + ENDPOINT_ID, + CLUSTER_ID, + ENDPOINTS, + INPUT_CLUSTERS, + LONG_PRESS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, + SHORT_PRESS, + ZHA_SEND_EVENT, +) +from zhaquirks.tuya import TuyaManufClusterAttributes, TuyaOnOff, TuyaSmartRemoteOnOffCluster, TuyaSwitch + +class TuyaRemote1001OnOffCluster(TuyaSmartRemoteOnOffCluster): + name = "TS1001_cluster" + ep_attribute = "TS1001_cluster" + cluster_id = 4 + + manufacturer_client_commands = { + 0x0002: ("press_type", (t.uint8_t,), True), + } + + manufacturer_server_commands = { + 0xFD: ("press_type", (t.uint8_t,), False), + } + + def handle_cluster_request( + self, + hdr: foundation.ZCLHeader, + args: List[Any], + *, + dst_addressing: Optional[ + Union[t.Addressing.Group, t.Addressing.IEEE, t.Addressing.NWK] + ] = None, + ): + prev_tsn = self.last_tsn + super().handle_cluster_request(hdr, args=args, dst_addressing=dst_addressing) + + if self.last_tsn == prev_tsn: + # No command was processed + return + if hdr.command_id == 0x02: + press_type = args[0] + self.listener_event( + #ZHA_SEND_EVENT, self.press_type.get(press_type, "unknown"), [] + # Still need to work out different press types + ZHA_SEND_EVENT, "remote_button_short_press", [] + ) + +class TuyaDimRemote1001(TuyaSwitch): + """Tuya 4-button remote device.""" + + signature = { + MODELS_INFO: [("_TZ3000_ztrfrcsu", "TS1001")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.DIMMER_SWITCH, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + LightLink.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Time.cluster_id, + Ota.cluster_id, + LightLink.cluster_id, + ], + }, + }, + } + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.REMOTE_CONTROL, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + LightLink.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + TuyaRemote1001OnOffCluster, + Scenes.cluster_id, + TuyaSmartRemoteOnOffCluster, + LevelControl.cluster_id, + Time.cluster_id, + Ota.cluster_id, + LightLink.cluster_id, + ], + }, + }, + } + device_automation_triggers = { + (SHORT_PRESS, TURN_ON): {CLUSTER_ID: 6, COMMAND: SHORT_PRESS}, + (LONG_PRESS, TURN_ON): {CLUSTER_ID: 6, COMMAND: LONG_PRESS}, + (DOUBLE_PRESS, TURN_ON): {CLUSTER_ID: 6, COMMAND: DOUBLE_PRESS}, + (SHORT_PRESS, TURN_OFF): {CLUSTER_ID: 6, COMMAND: SHORT_PRESS}, + (LONG_PRESS, TURN_OFF): {CLUSTER_ID: 6, COMMAND: LONG_PRESS}, + (DOUBLE_PRESS, TURN_OFF): {CLUSTER_ID: 6, COMMAND: DOUBLE_PRESS}, + (SHORT_PRESS, DIM_UP): {CLUSTER_ID: 8, COMMAND: SHORT_PRESS}, + (LONG_PRESS, DIM_UP): {CLUSTER_ID: 8, COMMAND: LONG_PRESS}, + (DOUBLE_PRESS, DIM_UP): {CLUSTER_ID: 8, COMMAND: DOUBLE_PRESS}, + (SHORT_PRESS, DIM_DOWN): {CLUSTER_ID: 8, COMMAND: SHORT_PRESS}, + (LONG_PRESS, DIM_DOWN): {CLUSTER_ID: 8, COMMAND: LONG_PRESS}, + (DOUBLE_PRESS, DIM_DOWN): {CLUSTER_ID: 8, COMMAND: DOUBLE_PRESS}, + } + +# ON Button +# [bellows.uart] Data frame: b'631bb157546f15b658924a24ab5593499cf0e767ca0b9874f9c77f74fc7c40447e' +# [bellows.uart] Sending: b'87009f7e' +# [bellows.ezsp.protocol] Application frame 69 (incomingMessageHandler) received: b'0004010600010100010000bec0cc27c5ffff04011cfd0002' +# [bellows.zigbee.application] Received incomingMessageHandler frame with [, EmberApsFrame(profileId=260, clusterId=6, sourceEndpoint=1, destinationEndpoint=1, options=, groupId=0, sequence=190), 192, -52, 0xc527, 255, 255, b'\x01\x1c\xfd\x00'] +# [zigpy.zcl] [0xc527:1:0x0006] ZCL deserialize: manufacturer=None tsn=28 command_id=253> +# [zigpy.zcl] [0xc527:1:0x0006] Unknown cluster-specific command 253 +# [zigpy.zcl] [0xc527:1:0x0006] ZCL request 0x00fd: b'\x00' +# [zigpy.zcl] [0xc527:1:0x0006] No handler for cluster command 253 + +# OFF button +# [bellows.zigbee.application] Received incomingMessageHandler frame with [, EmberApsFrame(profileId=260, clusterId=4, sourceEndpoint=1, destinationEndpoint=1, options=, groupId=0, sequence=193), 168, -58, 0xc527, 255, 255, b'\x19\xde\x02\xff\x00'] +# [zigpy.zcl] [0xc527:1:0x0004] ZCL deserialize: manufacturer=None tsn=222 command_id=2> +# [zigpy.zcl] [0xc527:1:0x0004] ZCL request 0x0002: [255, []] +# [zigpy.zcl] [0xc527:1:0x0004] No handler for cluster command 2 + +# Dim down button +# [bellows.ezsp.protocol] Application frame 84 (customFrameHandler) received: b'1200100c00000800040101be01030201330a00' +# [bellows.zigbee.application] Received customFrameHandler frame with [b'\x00\x10\x0c\x00\x00\x08\x00\x04\x01\x01\xbe\x01\x03\x02\x013\n\x00']