diff --git a/tests/test_tuya_motion.py b/tests/test_tuya_motion.py index 5cc220ef8c..8510499d67 100644 --- a/tests/test_tuya_motion.py +++ b/tests/test_tuya_motion.py @@ -3,6 +3,7 @@ import asyncio import pytest +from zigpy.types import Bool from zigpy.zcl import foundation from zigpy.zcl.clusters.measurement import IlluminanceMeasurement, OccupancySensing from zigpy.zcl.clusters.security import IasZone @@ -21,6 +22,29 @@ ZCL_TUYA_MOTION_V7 = b"\tL\x01\x00\x05\x01\x01\x00\x01\x00" # DP 1, Inv ZCL_TUYA_MOTION_V8 = b"\tL\x01\x00\x05\x65\x01\x00\x01\x00" # DP 101, Inv +# ZG-204ZV configuration datapoint messages +ZCL_ZG204ZV_SENSITIVITY = ( + b"\tL\x01\x00\x05\x02\x02\x00\x04\x00\x00\x00\x0a" # DP 2, sensitivity=10 +) +ZCL_ZG204ZV_FADING_TIME = ( + b"\tL\x01\x00\x05\x66\x02\x00\x04\x00\x00\x00\x3c" # DP 102, fading_time=60s +) +ZCL_ZG204ZV_HUMIDITY_OFFSET = ( + b"\tL\x01\x00\x05\x68\x02\x00\x04\x00\x00\x00\x05" # DP 104, humidity_offset=5 +) +ZCL_ZG204ZV_TEMP_OFFSET = ( + b"\tL\x01\x00\x05\x69\x02\x00\x04\x00\x00\x00\x0a" # DP 105, temp_offset=10 (1.0°C) +) +ZCL_ZG204ZV_ILLUM_INTERVAL = ( + b"\tL\x01\x00\x05\x6b\x02\x00\x04\x00\x00\x00\x0a" # DP 107, illum_interval=10min +) +ZCL_ZG204ZV_LED_ON = b"\tL\x01\x00\x05\x6c\x01\x00\x01\x01" # DP 108, LED indicator ON +ZCL_ZG204ZV_LED_OFF = ( + b"\tL\x01\x00\x05\x6c\x01\x00\x01\x00" # DP 108, LED indicator OFF +) +ZCL_ZG204ZV_TEMP_UNIT_C = b"\tL\x01\x00\x05\x6d\x04\x00\x01\x00" # DP 109, Celsius +ZCL_ZG204ZV_TEMP_UNIT_F = b"\tL\x01\x00\x05\x6d\x04\x00\x01\x01" # DP 109, Fahrenheit + zhaquirks.setup() @@ -67,6 +91,11 @@ ("_TZE200_2aaelwxk", "TS0601", ZCL_TUYA_MOTION), ("_TZE200_kb5noeto", "TS0601", ZCL_TUYA_MOTION), ("_TZE204_ex3rcdha", "TS0601", ZCL_TUYA_MOTION_V8), + ("HOBEIAN", "ZG-204ZV", ZCL_TUYA_MOTION), + ("_TZE200_uli8wasj", "TS0601", ZCL_TUYA_MOTION), + ("_TZE200_grgol3xp", "TS0601", ZCL_TUYA_MOTION), + ("_TZE200_rhgsbacq", "TS0601", ZCL_TUYA_MOTION), + ("_TZE200_y8jijhba", "TS0601", ZCL_TUYA_MOTION), ], ) async def test_tuya_motion_quirk_occ(zigpy_device_from_v2_quirk, model, manuf, occ_msg): @@ -172,3 +201,111 @@ async def test_tuya_motion_quirk_enum_illum( assert len(illum_listener.attribute_updates) == 1 assert illum_listener.attribute_updates[0][0] == zcl_illum_id assert illum_listener.attribute_updates[0][1] == exp_value + + +@pytest.mark.parametrize( + "model,manuf", + [ + ("HOBEIAN", "ZG-204ZV"), + ("_TZE200_uli8wasj", "TS0601"), + ("_TZE200_grgol3xp", "TS0601"), + ("_TZE200_rhgsbacq", "TS0601"), + ("_TZE200_y8jijhba", "TS0601"), + ], +) +async def test_zg204zv_tuya_config_datapoints(zigpy_device_from_v2_quirk, model, manuf): + """Test ZG-204ZV Tuya configuration datapoints. + + Note: ZG-204ZV reports sensor data (battery, temperature, humidity, illuminance) + via standard Zigbee clusters (0x0001, 0x0402, 0x0405, 0x0400) natively. + This test focuses on Tuya datapoints used for occupancy detection and device + configuration (sensitivity, fading time, offsets, intervals, LED, temp unit). + """ + quirked_device = zigpy_device_from_v2_quirk(model, manuf) + ep = quirked_device.endpoints[1] + + assert ep.tuya_manufacturer is not None + assert isinstance(ep.tuya_manufacturer, TuyaMCUCluster) + + tmcu_listener = ClusterListener(ep.tuya_manufacturer) + assert len(tmcu_listener.attribute_updates) == 0 + + # Test DP 2: motion_detection_sensitivity (0-19) + hdr, data = ep.tuya_manufacturer.deserialize(ZCL_ZG204ZV_SENSITIVITY) + status = ep.tuya_manufacturer.handle_get_data(data.data) + assert status == foundation.Status.SUCCESS + assert ep.tuya_manufacturer.get((0xEF << 8) | 2) == 10 + assert len(tmcu_listener.attribute_updates) == 1 + assert tmcu_listener.attribute_updates[0][0] == (0xEF << 8) | 2 + assert tmcu_listener.attribute_updates[0][1] == 10 + + # Test DP 102: fading_time (0-28800 seconds) + hdr, data = ep.tuya_manufacturer.deserialize(ZCL_ZG204ZV_FADING_TIME) + status = ep.tuya_manufacturer.handle_get_data(data.data) + assert status == foundation.Status.SUCCESS + assert ep.tuya_manufacturer.get((0xEF << 8) | 102) == 60 + assert len(tmcu_listener.attribute_updates) == 2 + assert tmcu_listener.attribute_updates[1][0] == (0xEF << 8) | 102 + assert tmcu_listener.attribute_updates[1][1] == 60 + + # Test DP 104: humidity_offset (-30 to 30) + hdr, data = ep.tuya_manufacturer.deserialize(ZCL_ZG204ZV_HUMIDITY_OFFSET) + status = ep.tuya_manufacturer.handle_get_data(data.data) + assert status == foundation.Status.SUCCESS + assert ep.tuya_manufacturer.get((0xEF << 8) | 104) == 5 + assert len(tmcu_listener.attribute_updates) == 3 + assert tmcu_listener.attribute_updates[2][0] == (0xEF << 8) | 104 + assert tmcu_listener.attribute_updates[2][1] == 5 + + # Test DP 105: temperature_offset (-2.0 to 2.0°C, step 0.1, multiplier 0.1) + hdr, data = ep.tuya_manufacturer.deserialize(ZCL_ZG204ZV_TEMP_OFFSET) + status = ep.tuya_manufacturer.handle_get_data(data.data) + assert status == foundation.Status.SUCCESS + # Device sends raw value 10, which with multiplier 0.1 = 1.0°C + assert ep.tuya_manufacturer.get((0xEF << 8) | 105) == 10 + assert len(tmcu_listener.attribute_updates) == 4 + assert tmcu_listener.attribute_updates[3][0] == (0xEF << 8) | 105 + assert tmcu_listener.attribute_updates[3][1] == 10 + + # Test DP 107: illuminance_interval (1-720 minutes) + hdr, data = ep.tuya_manufacturer.deserialize(ZCL_ZG204ZV_ILLUM_INTERVAL) + status = ep.tuya_manufacturer.handle_get_data(data.data) + assert status == foundation.Status.SUCCESS + assert ep.tuya_manufacturer.get((0xEF << 8) | 107) == 10 + assert len(tmcu_listener.attribute_updates) == 5 + assert tmcu_listener.attribute_updates[4][0] == (0xEF << 8) | 107 + assert tmcu_listener.attribute_updates[4][1] == 10 + + # Test DP 108: led_indicator (bool) + hdr, data = ep.tuya_manufacturer.deserialize(ZCL_ZG204ZV_LED_ON) + status = ep.tuya_manufacturer.handle_get_data(data.data) + assert status == foundation.Status.SUCCESS + assert ep.tuya_manufacturer.get((0xEF << 8) | 108) == 1 + assert len(tmcu_listener.attribute_updates) == 6 + assert tmcu_listener.attribute_updates[5][0] == (0xEF << 8) | 108 + assert tmcu_listener.attribute_updates[5][1] == Bool.true + + hdr, data = ep.tuya_manufacturer.deserialize(ZCL_ZG204ZV_LED_OFF) + status = ep.tuya_manufacturer.handle_get_data(data.data) + assert status == foundation.Status.SUCCESS + assert ep.tuya_manufacturer.get((0xEF << 8) | 108) == 0 + assert len(tmcu_listener.attribute_updates) == 7 + assert tmcu_listener.attribute_updates[6][0] == (0xEF << 8) | 108 + assert tmcu_listener.attribute_updates[6][1] == Bool.false + + # Test DP 109: temperature_unit_convert (enum: 0=Celsius, 1=Fahrenheit) + hdr, data = ep.tuya_manufacturer.deserialize(ZCL_ZG204ZV_TEMP_UNIT_C) + status = ep.tuya_manufacturer.handle_get_data(data.data) + assert status == foundation.Status.SUCCESS + assert ep.tuya_manufacturer.get((0xEF << 8) | 109) == 0 + assert len(tmcu_listener.attribute_updates) == 8 + assert tmcu_listener.attribute_updates[7][0] == (0xEF << 8) | 109 + assert tmcu_listener.attribute_updates[7][1] == 0 + + hdr, data = ep.tuya_manufacturer.deserialize(ZCL_ZG204ZV_TEMP_UNIT_F) + status = ep.tuya_manufacturer.handle_get_data(data.data) + assert status == foundation.Status.SUCCESS + assert ep.tuya_manufacturer.get((0xEF << 8) | 109) == 1 + assert len(tmcu_listener.attribute_updates) == 9 + assert tmcu_listener.attribute_updates[8][0] == (0xEF << 8) | 109 + assert tmcu_listener.attribute_updates[8][1] == 1 diff --git a/zhaquirks/tuya/tuya_motion.py b/zhaquirks/tuya/tuya_motion.py index c04e8645a7..154759e2c7 100644 --- a/zhaquirks/tuya/tuya_motion.py +++ b/zhaquirks/tuya/tuya_motion.py @@ -13,6 +13,7 @@ from zhaquirks.tuya import TuyaLocalCluster from zhaquirks.tuya.builder import TuyaQuirkBuilder +from zhaquirks.tuya.tuya_sensor import TuyaTempUnitConvert class TuyaOccupancySensing(OccupancySensing, TuyaLocalCluster): @@ -1519,6 +1520,98 @@ class TuyaMotionDetectionMode(t.enum8): ) +# HOBEIAN ZG-204ZV, 10GHz mmWave presence sensor with temp/humidity/illuminance +# Sensor data (battery, temperature, humidity, illuminance) reported via standard +# Zigbee clusters (0x0001, 0x0402, 0x0405, 0x0400) by the device itself +# Tuya datapoints used for: occupancy detection (DP 1) and device configuration +# (sensitivity, fading time, sensor offsets, illuminance interval, LED, temp unit) +( + TuyaQuirkBuilder("HOBEIAN", "ZG-204ZV") + .applies_to("_TZE200_uli8wasj", "TS0601") + .applies_to("_TZE200_grgol3xp", "TS0601") + .applies_to("_TZE200_rhgsbacq", "TS0601") + .applies_to("_TZE200_y8jijhba", "TS0601") + .tuya_dp( + dp_id=1, + ep_attribute=TuyaOccupancySensing.ep_attribute, + attribute_name=OccupancySensing.AttributeDefs.occupancy.name, + converter=lambda x: x == 1, + ) + .adds(TuyaOccupancySensing) + .tuya_number( + dp_id=2, + attribute_name="motion_detection_sensitivity", + type=t.uint16_t, + min_value=0, + max_value=19, + step=1, + translation_key="motion_detection_sensitivity", + fallback_name="Motion detection sensitivity", + ) + .tuya_number( + dp_id=102, + attribute_name="fading_time", + type=t.uint16_t, + device_class=SensorDeviceClass.DURATION, + unit=UnitOfTime.SECONDS, + min_value=0, + max_value=28800, + step=1, + translation_key="fading_time", + fallback_name="Motion keep time", + ) + .tuya_number( + dp_id=104, + attribute_name="humidity_offset", + type=t.int16s, + min_value=-30, + max_value=30, + step=1, + translation_key="humidity_offset", + fallback_name="Humidity offset", + ) + .tuya_number( + dp_id=105, + attribute_name="temperature_offset", + type=t.int16s, + min_value=-2, + max_value=2, + step=0.1, + multiplier=0.1, + translation_key="temperature_offset", + fallback_name="Temperature offset", + ) + .tuya_number( + dp_id=107, + attribute_name="illuminance_interval", + type=t.uint16_t, + device_class=SensorDeviceClass.DURATION, + unit=UnitOfTime.MINUTES, + min_value=1, + max_value=720, + step=1, + translation_key="illuminance_interval", + fallback_name="Illuminance interval", + ) + .tuya_switch( + dp_id=108, + attribute_name="led_indicator", + entity_type=EntityType.STANDARD, + translation_key="led_indicator", + fallback_name="LED indicator", + ) + .tuya_enum( + dp_id=109, + attribute_name="temperature_unit_convert", + enum_class=TuyaTempUnitConvert, + translation_key="temperature_unit_convert", + fallback_name="Temperature unit", + ) + .skip_configuration() + .add_to_registry() +) + + # Tuya mmWave radar 5.8GHz, ZY_HPS01 ( TuyaQuirkBuilder("_TZE204_ex3rcdha", "TS0601")