From 0e2ea71dd4abb2f94875e91159dd04a0158996a0 Mon Sep 17 00:00:00 2001 From: Roee Mazor Date: Fri, 12 Sep 2025 09:46:32 +0300 Subject: [PATCH 1/3] Add TuyaTripleSwitchDimmer quirk for TS0601 triple-channel dimmers - Add support for Tuya triple-channel dimmer switches (TS0601) - Supports models: _TZE204_znvwzxkq, _TZE284_znvwzxkq, _TZE204_1v1dxkck, _TZE200_vm1gyrso - Implements 3 endpoints for independent channel control - Uses TuyaOnOff and TuyaInWallLevelControl clusters - Add comprehensive tests for command and dimming functionality - Update device summary documentation --- tests/test_tuya_dimmer.py | 107 ++++++++++++++++++++++++++++++++ zhaquirks/tuya/ts0601_dimmer.py | 66 ++++++++++++++++++++ 2 files changed, 173 insertions(+) diff --git a/tests/test_tuya_dimmer.py b/tests/test_tuya_dimmer.py index 7b341d1292..5f76684bd1 100644 --- a/tests/test_tuya_dimmer.py +++ b/tests/test_tuya_dimmer.py @@ -297,3 +297,110 @@ async def test_doubledimmer_state_report(zigpy_device_from_quirk, quirk): assert len(dimmer2_listener.attribute_updates) == 2 assert dimmer2_listener.attribute_updates[1][0] == 0x0000 assert dimmer2_listener.attribute_updates[1][1] == 170 + + +@pytest.mark.parametrize( + "quirk", (zhaquirks.tuya.ts0601_dimmer.TuyaTripleSwitchDimmer,) +) +async def test_triple_command(zigpy_device_from_quirk, quirk): + """Test write cluster attributes for triple switch dimmer.""" + + dimmer_dev = zigpy_device_from_quirk(quirk) + tuya_cluster = dimmer_dev.endpoints[1].tuya_manufacturer + dimmer1_cluster = dimmer_dev.endpoints[1].level + switch1_cluster = dimmer_dev.endpoints[1].on_off + switch2_cluster = dimmer_dev.endpoints[2].on_off + switch3_cluster = dimmer_dev.endpoints[3].on_off + tuya_listener = ClusterListener(tuya_cluster) + + assert len(tuya_listener.cluster_commands) == 0 + assert len(tuya_listener.attribute_updates) == 0 + + with mock.patch.object( + tuya_cluster.endpoint, "request", return_value=foundation.Status.SUCCESS + ) as m1: + rsp = await switch2_cluster.command(0x0001) # turn_on + await wait_for_zigpy_tasks() + + m1.assert_called_with( + cluster=61184, + sequence=1, + data=b"\x01\x01\x00\x00\x01\x07\x01\x00\x01\x01", + command_id=0, + timeout=5, + expect_reply=True, + use_ieee=False, + ask_for_ack=None, + priority=None, + ) + assert rsp.status == foundation.Status.SUCCESS + + rsp = await switch3_cluster.command(0x0001) # turn_on + await wait_for_zigpy_tasks() + + m1.assert_called_with( + cluster=61184, + sequence=2, + data=b"\x01\x02\x00\x00\x02\x0f\x01\x00\x01\x01", + command_id=0, + timeout=5, + expect_reply=True, + use_ieee=False, + ask_for_ack=None, + priority=None, + ) + assert rsp.status == foundation.Status.SUCCESS + + rsp = await dimmer1_cluster.command(0x0000, 225) # move_to_level + await wait_for_zigpy_tasks() + + m1.assert_called_with( + cluster=61184, + sequence=3, + data=b"\x01\x03\x00\x00\x03\x02\x02\x00\x04\x00\x00\x03r", + command_id=0, + timeout=5, + expect_reply=True, + use_ieee=False, + ask_for_ack=None, + priority=None, + ) + assert rsp.status == foundation.Status.SUCCESS + + +@pytest.mark.parametrize( + "quirk", (zhaquirks.tuya.ts0601_dimmer.TuyaTripleSwitchDimmer,) +) +async def test_triple_dim_values(zigpy_device_from_quirk, quirk): + """Test dimming values for triple switch dimmer.""" + + dimmer_dev = zigpy_device_from_quirk(quirk) + + dimmer2_cluster = dimmer_dev.endpoints[2].level + dimmer2_listener = ClusterListener(dimmer2_cluster) + + dimmer3_cluster = dimmer_dev.endpoints[3].level + dimmer3_listener = ClusterListener(dimmer3_cluster) + + tuya_cluster = dimmer_dev.endpoints[1].tuya_manufacturer + + assert len(dimmer2_listener.attribute_updates) == 0 + assert len(dimmer3_listener.attribute_updates) == 0 + + # Test channel 2 dimming + hdr, args = tuya_cluster.deserialize( + b"\tV\x02\x01y\x08\x02\x00\x04\x00\x00\x02\x29" + ) + tuya_cluster.handle_message(hdr, args) + assert len(dimmer2_listener.attribute_updates) == 1 + assert dimmer2_listener.attribute_updates[0][0] == 0x0000 + assert dimmer2_listener.attribute_updates[0][1] == 141 + + # Test channel 3 dimming + hdr, args = tuya_cluster.deserialize( + b"\tV\x02\x01y\x10\x02\x00\x04\x00\x00\x02\xbc" + ) + tuya_cluster.handle_message(hdr, args) + assert len(dimmer3_listener.attribute_updates) == 1 + assert dimmer3_listener.attribute_updates[0][0] == 0x0000 + assert dimmer3_listener.attribute_updates[0][1] == 178 diff --git a/zhaquirks/tuya/ts0601_dimmer.py b/zhaquirks/tuya/ts0601_dimmer.py index eefd78905c..6562e61ced 100644 --- a/zhaquirks/tuya/ts0601_dimmer.py +++ b/zhaquirks/tuya/ts0601_dimmer.py @@ -27,6 +27,7 @@ class TuyaInWallLevelControlNM(NoManufacturerCluster, TuyaInWallLevelControl): # --- DEVICE SUMMARY --- # TuyaSingleSwitchDimmer: 0x00, 0x04, 0x05, 0xEF00; 0x000A, 0x0019 # TuyaDoubleSwitchDimmer: 0x00, 0x04, 0x05, 0xEF00; 0x000A, 0x0019 +# TuyaTripleSwitchDimmer: 0x00, 0x04, 0x05, 0xEF00; 0x000A, 0x0019 # - Dimmer with Green Power Proxy: Endpoint=242 profile=41440 device_type=0x0061, output_clusters: 0x0021 - # TuyaSingleSwitchDimmerGP: 0x00, 0x04, 0x05, 0xEF00; 0x000A, 0x0019 # TuyaDoubleSwitchDimmerGP: 0x00, 0x04, 0x05, 0xEF00; 0x000A, 0x0019 @@ -144,6 +145,71 @@ class TuyaDoubleSwitchDimmer(TuyaDimmerSwitch): } +class TuyaTripleSwitchDimmer(TuyaDimmerSwitch): + """Tuya triple-channel dimmer (TS0601, Zemismart/Moes), non-GP.""" + + signature = { + MODELS_INFO: [ + ("_TZE204_znvwzxkq", "TS0601"), + ("_TZE284_znvwzxkq", "TS0601"), + ("_TZE204_1v1dxkck", "TS0601"), + ("_TZE200_vm1gyrso", "TS0601"), + ], + ENDPOINTS: { + # + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaLevelControlManufCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + }, + }, + } + + replacement = { + ENDPOINTS: { + 1: { + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaLevelControlManufCluster, + TuyaOnOff, + TuyaInWallLevelControl, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + }, + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + TuyaOnOff, + TuyaInWallLevelControl, + ], + OUTPUT_CLUSTERS: [], + }, + 3: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + TuyaOnOff, + TuyaInWallLevelControl, + ], + OUTPUT_CLUSTERS: [], + }, + } + } + + class TuyaSingleSwitchDimmerGP(TuyaDimmerSwitch): """Tuya touch switch device.""" From 06b06ae9f901392d4741c9fd773b93f5ee049592 Mon Sep 17 00:00:00 2001 From: Roee Mazor Date: Fri, 12 Sep 2025 09:55:04 +0300 Subject: [PATCH 2/3] Simplify model identifiers to only include verified device - Remove unverified model identifiers - Keep only _TZE204_znvwzxkq which is the actual device tested --- zhaquirks/tuya/ts0601_dimmer.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/zhaquirks/tuya/ts0601_dimmer.py b/zhaquirks/tuya/ts0601_dimmer.py index 6562e61ced..892b5f128c 100644 --- a/zhaquirks/tuya/ts0601_dimmer.py +++ b/zhaquirks/tuya/ts0601_dimmer.py @@ -151,9 +151,6 @@ class TuyaTripleSwitchDimmer(TuyaDimmerSwitch): signature = { MODELS_INFO: [ ("_TZE204_znvwzxkq", "TS0601"), - ("_TZE284_znvwzxkq", "TS0601"), - ("_TZE204_1v1dxkck", "TS0601"), - ("_TZE200_vm1gyrso", "TS0601"), ], ENDPOINTS: { # Date: Fri, 12 Sep 2025 10:00:49 +0300 Subject: [PATCH 3/3] Fix linting issue: remove unused variable in test - Remove unused switch1_cluster variable from test_triple_command - All pre-commit hooks now pass - Code is properly formatted with ruff --- tests/test_tuya_dimmer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_tuya_dimmer.py b/tests/test_tuya_dimmer.py index 5f76684bd1..1392ab75e5 100644 --- a/tests/test_tuya_dimmer.py +++ b/tests/test_tuya_dimmer.py @@ -308,7 +308,6 @@ async def test_triple_command(zigpy_device_from_quirk, quirk): dimmer_dev = zigpy_device_from_quirk(quirk) tuya_cluster = dimmer_dev.endpoints[1].tuya_manufacturer dimmer1_cluster = dimmer_dev.endpoints[1].level - switch1_cluster = dimmer_dev.endpoints[1].on_off switch2_cluster = dimmer_dev.endpoints[2].on_off switch3_cluster = dimmer_dev.endpoints[3].on_off tuya_listener = ClusterListener(tuya_cluster)