Skip to content

Instantly share code, notes, and snippets.

@vinzent
Last active June 21, 2024 15:59
Show Gist options
  • Save vinzent/2cd645b848fd3b6a0c3e5762956ec89f to your computer and use it in GitHub Desktop.
Save vinzent/2cd645b848fd3b6a0c3e5762956ec89f to your computer and use it in GitHub Desktop.
Tuya PIR+MMWaver Presence sensor ZG-204M ZHA Quirk for HomeAssistant
"""
* TS0601 ZG-204ZM
* _TZE200_kb5noeto
* https://de.aliexpress.com/item/1005006174074799.html ("Color": Mmwave PIR)
* https://github.com/13717033460/zigbee-herdsman-converters/blob/6c9cf1b0de836ec2172d569568d3c7fe75268958/src/devices/tuya.ts#L5730-L5762
* https://www.zigbee2mqtt.io/devices/ZG-204ZM.html
* https://smarthomescene.com/reviews/zigbee-battery-powered-presence-sensor-zg-204zm-review/
* https://doc.szalarm.com/zg-205ZL/cntop_zigbee_sensor.js
* https://github.com/Koenkk/zigbee2mqtt/issues/21919
"""
import logging
from typing import Final
from zigpy.quirks.v2 import add_to_registry_v2
import zigpy.types as t
from zigpy.zcl.foundation import ZCLAttributeDef
from zigpy.zcl.clusters.measurement import (
IlluminanceMeasurement,
OccupancySensing,
)
from zigpy.zcl.clusters.security import IasZone
from zigpy.quirks.v2.homeassistant import EntityPlatform, EntityType
from zhaquirks.tuya import (
TuyaLocalCluster,
TuyaPowerConfigurationCluster2AAA,
)
from zhaquirks.tuya.mcu import TuyaMCUCluster, DPToAttributeMapping
class HumanMotionState(t.enum8):
"""Human Motion State values"""
none = 0x00
large_move = 0x01
small_move = 0x02
breathe = 0x03
class MotionDetectionMode(t.enum8):
"""Motion detection mode values"""
Only_PIR: 0x00
PIR_radar: 0x01
Only_radar: 0x02
@staticmethod
def converter(value):
"""" If value is None, Only_PIR should be returned """
if value is None:
return MotionDetectionMode.Only_PIR
return value
class TuyaOccupancySensing(OccupancySensing, TuyaLocalCluster):
"""Tuya local OccupancySensing cluster."""
class TuyaIlluminanceMeasurement(IlluminanceMeasurement, TuyaLocalCluster):
"""Tuya local IlluminanceMeasurement cluster."""
class HumanPresenceSensorManufCluster(TuyaMCUCluster):
"""Human Presence Sensor ZG-204ZM (PIR+mmWave, battery)"""
# Tuya Data points
# "1":"Human Presence State", (presence_state, Enum, none|presence)
# "2":"Stationary detection sensitivity", (sensitivity, Integer, 0-10, unit=x, step=1)
# "3":"Minimum detection distance", (near_detection, Integer, 0-1000, unit=cm, step=1) (NOT AVAILABLE IN TUYA SMART LIFE APP)
# "4":"Stationary detection distance", (far_detection, Integer, 0-1000, unit=cm, step=1)
# "101":"Human Motion State", (human_motion_state, Enum, none|large_move|small_move|breathe)
# "102":"Presence Keep Time", (presence_time, 10-28800, unit=s, step=1)
# "106":"Illuminance Value", (illuminance_value, Integer, 0-6000, unit=lux )
# "107":"Indicator", (indicator, Boolean)
# "112":"Reset setting", (reset_setting, Boolean)
# "121":"Battery", (battery, Integer, -1-100, step=1, unit=%)
# "122":"Motion detection ", (motion_detection_mode, Enum, Only_PIR|PIR_radar|Only_radar) (NOT AVAILABLE IN TUYA SMART LIFE APP)
# "123":"Motion detection sensitivity", (motion_detection_sen, Integer, 0-10, step=1, unit=x) (NOT AVAILABLE IN TUYA SMART LIFE APP)
# "124":"ver" (ver, Integer, 0-100, step=1) (NOT AVAILABLE IN TUYA SMART LIFE APP)
class AttributeDefs(TuyaMCUCluster.AttributeDefs):
"""Tuya DataPoints attributes"""
# Human presence state (mapped to the OccupancySensing cluster)
#presence_state: Final = ZCLAttributeDef(
# id=0xEF01, # DP 1
# type=Occupancy,
# access="rp",
# is_manufacturer_specific=True,
#)
# Stationary detection sensitivity
sensitivity: Final = ZCLAttributeDef(
id=0x0002, # DP 2
type=t.uint16_t,
is_manufacturer_specific=True,
)
# Minimum detection distance
near_detection: Final = ZCLAttributeDef(
id=0x0003, # DP 3
type=t.uint16_t,
is_manufacturer_specific=True,
)
# Stationary detection distance
far_detection: Final = ZCLAttributeDef(
id=0x0004, # DP 4
type=t.uint16_t,
is_manufacturer_specific=True,
)
# Human motion state
human_motion_state: Final = ZCLAttributeDef(
id=0x0101, # DP 101
type=HumanMotionState,
access="rp",
is_manufacturer_specific=True,
)
# Presence keep time
presence_time: Final = ZCLAttributeDef(
id=0x0102, # DP 102
type=t.uint16_t,
is_manufacturer_specific=True,
)
# Illuminance value
illuminance_value: Final = ZCLAttributeDef(
id=0x0106, # DP 106
type=t.uint16_t,
access="rp",
is_manufacturer_specific=True,
)
# Indicator
indicator: Final = ZCLAttributeDef(
id=0x0107, # DP 107
type=t.Bool,
is_manufacturer_specific=True,
)
# Reset setting
reset_setting: Final = ZCLAttributeDef(
id=0x0112, # DP 112
type=t.Bool,
is_manufacturer_specific=True,
)
# Battery (also provided by the TuyaPowerConfigurationCluster2AAA)
battery: Final = ZCLAttributeDef(
id=0x0121, # DP 121
type=t.int16s,
is_manufacturer_specific=True,
)
# Motion detection
motion_detection_mode: Final = ZCLAttributeDef(
id=0x0122, # DP 122
type=MotionDetectionMode,
is_manufacturer_specific=True,
)
# Motion detection sensitivity
motion_detection_sen: Final = ZCLAttributeDef(
id=0x0123, # DP 123
type=t.uint16_t,
is_manufacturer_specific=True,
)
# ver
ver: Final = ZCLAttributeDef(
id=0x0124, # DP 124
type=t.uint16_t,
is_manufacturer_specific=True,
)
dp_to_attribute: dict[int, DPToAttributeMapping] = {
1: DPToAttributeMapping(
TuyaOccupancySensing.ep_attribute,
"occupancy",
),
2: DPToAttributeMapping(
TuyaMCUCluster.ep_attribute,
"sensitivity",
# Value in Tuya App after Factory reset is 6
converter=lambda x: x if x is not None else 6
),
3: DPToAttributeMapping(
TuyaMCUCluster.ep_attribute,
"near_detection",
# Guessing a default of 0
converter=lambda x: x if x is not None else 0
),
4: DPToAttributeMapping(
TuyaMCUCluster.ep_attribute,
"far_detection",
# Value in Tuya App after Factory reset is 600cm
converter=lambda x: x if x is not None else 600
),
101: DPToAttributeMapping(
TuyaMCUCluster.ep_attribute,
"human_motion_state",
converter=HumanMotionState
),
102: DPToAttributeMapping(
TuyaMCUCluster.ep_attribute,
"presence_time",
# Value in Tuya App is 30 after Factory reset
converter=lambda x: x if x is not None else 30
),
106: DPToAttributeMapping(
TuyaMCUCluster.ep_attribute,
"illuminance_value",
),
107: DPToAttributeMapping(
TuyaMCUCluster.ep_attribute,
"indicator",
),
112: DPToAttributeMapping(
TuyaMCUCluster.ep_attribute,
"reset_setting",
),
121: DPToAttributeMapping(
TuyaMCUCluster.ep_attribute,
"battery",
),
122: DPToAttributeMapping(
TuyaMCUCluster.ep_attribute,
"motion_detection_mode",
converter=MotionDetectionMode,
),
123: DPToAttributeMapping(
TuyaMCUCluster.ep_attribute,
"motion_detection_sen",
# Guessing a default of 10
converter=lambda x: x if x is not None else 10
),
124: DPToAttributeMapping(
TuyaMCUCluster.ep_attribute,
"ver",
),
}
data_point_handlers = {
1: "_dp_2_attr_update",
2: "_dp_2_attr_update",
3: "_dp_2_attr_update",
4: "_dp_2_attr_update",
101: "_dp_2_attr_update",
102: "_dp_2_attr_update",
106: "_dp_2_attr_update",
107: "_dp_2_attr_update",
112: "_dp_2_attr_update",
121: "_dp_2_attr_update",
122: "_dp_2_attr_update",
123: "_dp_2_attr_update",
124: "_dp_2_attr_update",
}
(
add_to_registry_v2("_TZE200_kb5noeto", "TS0601")
.skip_configuration()
.removes(IasZone.cluster_id)
.adds(HumanPresenceSensorManufCluster)
.adds(TuyaOccupancySensing)
.replaces(TuyaPowerConfigurationCluster2AAA)
.replaces(TuyaIlluminanceMeasurement)
.number(HumanPresenceSensorManufCluster.AttributeDefs.sensitivity.name, HumanPresenceSensorManufCluster.cluster_id,step=1,min_value=1,max_value=10)
#.number(HumanPresenceSensorManufCluster.AttributeDefs.near_detection.name, HumanPresenceSensorManufCluster.cluster_id,step=1,min_value=0,max_value=1000)
.number(HumanPresenceSensorManufCluster.AttributeDefs.far_detection.name, HumanPresenceSensorManufCluster.cluster_id,step=1,min_value=0,max_value=1000)
.enum(HumanPresenceSensorManufCluster.AttributeDefs.human_motion_state.name,HumanMotionState,HumanPresenceSensorManufCluster.cluster_id,entity_platform=EntityPlatform.SENSOR, entity_type=EntityType.STANDARD)
.number(HumanPresenceSensorManufCluster.AttributeDefs.presence_time.name, HumanPresenceSensorManufCluster.cluster_id,step=1,min_value=10,max_value=28800)
#.binary_sensor(HumanPresenceSensorManufCluster.AttributeDefs.indicator.name,HumanPresenceSensorManufCluster.cluster_id)
#.binary_sensor(HumanPresenceSensorManufCluster.AttributeDefs.reset_setting.name,HumanPresenceSensorManufCluster.cluster_id)
#.enum(HumanPresenceSensorManufCluster.AttributeDefs.motion_detection_mode.name,MotionDetectionMode,HumanPresenceSensorManufCluster.cluster_id)
#.number(HumanPresenceSensorManufCluster.AttributeDefs.motion_detection_sen.name, HumanPresenceSensorManufCluster.cluster_id,step=1,min_value=0,max_value=10)
#.number(HumanPresenceSensorManufCluster.AttributeDefs.ver.name, HumanPresenceSensorManufCluster.cluster_id,step=1,min_value=0,max_value=10)
)
@jdm09
Copy link

jdm09 commented Jun 9, 2024

@vinzent Ok. Now i got the different between what i am using as custom quirk and your linked description :) Thanks for pointing out. I will also try to understand that v2 documentation, but i think i am much more far away from an expert than you :)

@mikosoft83
Copy link

I tried adding this to your quirk:
( add_to_registry_v2("_TZE200_kb5noeto", "TS0601-block") .skip_configuration() .removes(IasZone.cluster_id) .adds(HumanPresenceSensorCluster) .replaces(TuyaPowerConfigurationCluster2AAA) .replaces(TuyaOccupancySensing) .replaces(TuyaIlluminanceMeasurement) .sensor(HumanPresenceSensorCluster.AttributeDefs.human_motion_state.name, HumanPresenceSensorCluster.cluster_id) .number(HumanPresenceSensorCluster.AttributeDefs.sensitivity.name,HumanPresenceSensorCluster.cluster_id,step=1,min_value=1,max_value=10) .number(HumanPresenceSensorCluster.AttributeDefs.far_detection.name,HumanPresenceSensorCluster.cluster_id,unit="cm",step=1,min_value=0,max_value=1000,mode="slider") .number(HumanPresenceSensorCluster.AttributeDefs.presence_time.name,HumanPresenceSensorCluster.cluster_id,unit="s",step=1,min_value=0,max_value=28800,mode="slider") )

In addition to occupancy and illuminance I got one sensor that displayed the motion state (as a number but that could be fixed to show the enum names instead) but the sensor was unnamed (it was shown as "_TZE200_kb5noeto TS0601 None") and the number sliders didn't show at all.

So I guess the quirk v2 support isn't really there yet.

On another note, I made a quirk, or rather I hacked together a quirk based on other Tuya radar sensors and I was at least able to expose the parameter sliders. I made it part of the ts0601_motion.py so if you want to try that out, here it is:
ts0601_motion.py
I'm not sure if I should submit it to the quirks repo as it's not really cleaned up and I don't know python at all to properly clean it up. Also it doesn't expose motion state because no matter what cluster types I tried it would not expose that cluster. I don't have enough knowledge about Zigbee, ZCL, Tuya MCU and Python to further work on this and it's enough for my use as it is now, so if anyone is willing to pick this up, be my guest.

@vinzent
Copy link
Author

vinzent commented Jun 9, 2024

Rev. 6 looks like this:

grafik

Issues:

  • ZHA integration fails to reload when using a custom quirk v2. Full core restart required.(log: ... Multiple matches found for device <Device model='TS0601' manuf='....)
  • some attributes report None instead of (probably) 0 for default (example: near_detection), this leads to disabled config entities
  • custom sensor/config entities have no good names. They all use "None". Don't know how to set the name.

Tuya iot Dev-Platform Screenshot: https://photos.app.goo.gl/Lc5TMy9qFoG7hML39

@mikosoft83
Copy link

I tested rev 6 but none of the controls worked for me. The were all empty and disabled.
But at least they did show up in the device which is a step up from what I was attempting (I wish I understood why I didn't see anything with my config)

@vinzent
Copy link
Author

vinzent commented Jun 9, 2024

@mikosoft83 did you delete and re-add the device? I think some things only get initialized properly with resetting the device. also some entities were disabled at the beginning and worked after few minutes. (probably until the first valid value was reported?)

@greenamit
Copy link

I used the updated quirk and restarted HA and didn't need to delete/add the sensor.
It looks like this now. What should be the correct entity names instead of none?
Screenshot 2024-06-09 at 20 38 30
Screenshot 2024-06-09 at 20 42 22
Screenshot 2024-06-09 at 20 40 32

@vinzent
Copy link
Author

vinzent commented Jun 9, 2024

@greenamit the first one is the human_motion_state tuya datapoint . so it probably should be called like this? the attribute name is passed by HumanPresenceSensorManufCluster.AttributeDefs.human_motion_state.name.

also for the other enum/numbers the name passed should probably be used.

but I don't have much insight in how these things are handled in HA Core. still reading dev docs on developers.home-assistant.io for a better understanding.

@mikosoft83
Copy link

@vinzent I never needed to repair the device when changing quirks. But it's true it takes a while for all the sensors to populate, but I waited several minutes and nothing happened. With my old style quirk it takes just a couple of seconds.
But the device itself is quite unreliable unfortunately (last night it stayed on presence the whole night) so it may have something to do with the unreliable cluster operation.
I have another such sensor, I set it up and will be monitoring it (with my old quirk for now).

@vinzent
Copy link
Author

vinzent commented Jun 10, 2024

Still working on this. learning. :) there are multiple DPToAttributeMapping classes. one in tuya, one in tuya/mcu. the imported DPToAttributeMapping in rev 6 is probably wrong (why are there 2 of them?)

@vinzent
Copy link
Author

vinzent commented Jun 10, 2024

Rev 7:

  • removed the datapoint entities that did not work out of the box
  • import DPToAttributeMapping from zhaquirks.tuya.mcu instead of the probably wrong zhaquirks.tuya.

grafik

  • Sensor None is the human_motion_state datapoint
  • Config first is sensitivity, 2nd far_detection, 3rd presence_time

Filed and issue about the ZHA integration reload issue: zigpy/zigpy#1410

@BambamNZ
Copy link

BambamNZ commented Jun 18, 2024

But the device itself is quite unreliable unfortunately (last night it stayed on presence the whole night) so it may have something to do with the unreliable cluster operation. I have another such sensor, I set it up and will be monitoring it (with my old quirk for now).

I have the same behaviour it would work "fine" changing state between "Large_move" , "breathe" and "none" couple of time that then just gets stuck on "breathe" until I remove the batteries. Not sure if it is a hardware problem, read on other posts, resoldering pins on the pcb improved reliability

See here -> Koenkk/zigbee2mqtt#21919 (comment)

@mikosoft83
Copy link

But the device itself is quite unreliable unfortunately (last night it stayed on presence the whole night) so it may have something to do with the unreliable cluster operation. I have another such sensor, I set it up and will be monitoring it (with my old quirk for now).

I have the same behaviour it would work "fine" changing state between "Large_move" , "breathe" and "none" couple of time that then just gets stuck on "breathe" until I remove the batteries. Not sure if it is a hardware problem, read on other posts, resoldering pins on the pcb improved reliability

See here -> Koenkk/zigbee2mqtt#21919 (comment)

I actually only cleaned those. It was difficult to desolder from the battery terminals so I didn't bother with my second unit.

Also for mine I only need to press the pairing button to reset the device. Last time it lasted more than a week. If it becomes too bothersome I will probably give it some more soldering time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment