Skip to content

Commit

Permalink
WIP: Add ts1001 (4 way switch for lights)
Browse files Browse the repository at this point in the history
I was testing this against an older version of zhaquirks. At the
time, some of the buttons did not reliably send events.

I'm just uploading the current state in case someone else wants to
look at this before I return to it.
  • Loading branch information
challs committed Jan 11, 2022
1 parent 44c4924 commit 14a96a4
Show file tree
Hide file tree
Showing 3 changed files with 204 additions and 1 deletion.
5 changes: 5 additions & 0 deletions tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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))
43 changes: 42 additions & 1 deletion tests/test_tuya.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
157 changes: 157 additions & 0 deletions zhaquirks/tuya/ts1001.py
Original file line number Diff line number Diff line change
@@ -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 [<EmberIncomingMessageType.INCOMING_UNICAST: 0>, EmberApsFrame(profileId=260, clusterId=6, sourceEndpoint=1, destinationEndpoint=1, options=<EmberApsOption.APS_OPTION_ENABLE_ROUTE_DISCOVERY: 256>, groupId=0, sequence=190), 192, -52, 0xc527, 255, 255, b'\x01\x1c\xfd\x00']
# [zigpy.zcl] [0xc527:1:0x0006] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=False disable_default_response=False> 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 [<EmberIncomingMessageType.INCOMING_UNICAST: 0>, EmberApsFrame(profileId=260, clusterId=4, sourceEndpoint=1, destinationEndpoint=1, options=<EmberApsOption.APS_OPTION_ENABLE_ROUTE_DISCOVERY|APS_OPTION_RETRY: 320>, groupId=0, sequence=193), 168, -58, 0xc527, 255, 255, b'\x19\xde\x02\xff\x00']
# [zigpy.zcl] [0xc527:1:0x0004] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=True> 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']

0 comments on commit 14a96a4

Please sign in to comment.