Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions tests/test_tuya_motion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
93 changes: 93 additions & 0 deletions zhaquirks/tuya/tuya_motion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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")
Expand Down
Loading