Open logan893 opened 9 months ago
If there's an easy way to convert the implementation into a quirk or something, zigbee2mqtt has support, even though it doesn't seem to work flawlessly yet.
Here is the data from Manage Zigbee Device -> Signature.
It looks identical to the motion sensor already included in the latest ZHA code with class MmwRadarMotionGPP in ts0601_motion.py. Same input clusters and output clusters for endpoints 1 and 242.
{
"node_descriptor": "NodeDescriptor(logical_type=<LogicalType.Router: 1>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.FullFunctionDevice|MainsPowered|RxOnWhenIdle|AllocateAddress: 142>, manufacturer_code=4417, maximum_buffer_size=66, maximum_incoming_transfer_size=66, server_mask=10752, maximum_outgoing_transfer_size=66, descriptor_capability_field=<DescriptorCapability.NONE: 0>, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=False, *is_full_function_device=True, *is_mains_powered=True, *is_receiver_on_when_idle=True, *is_router=True, *is_security_capable=False)",
"endpoints": {
"1": {
"profile_id": "0x0104",
"device_type": "0x0051",
"input_clusters": [
"0x0000",
"0x0004",
"0x0005",
"0xef00"
],
"output_clusters": [
"0x000a",
"0x0019"
]
},
"242": {
"profile_id": "0xa1e0",
"device_type": "0x0061",
"input_clusters": [],
"output_clusters": [
"0x0021"
]
}
},
"manufacturer": "_TZE204_ijxvkhd0",
"model": "TS0601",
"class": "zigpy.device.Device"
}
I added _TZE204_ijxvkhd0 to the MmwRadarMotionGPP, same as used by _TZE204_qasjif9e ( https://github.com/zigpy/zha-device-handlers/issues/2510 ).
I get a sensor for Occupancy but it is always set to Clear and doesn't change. No other sensors or controls that I can see.
I can get light sensor data from TuyaIlluminanceMeasurement (Endpoint id: 1, Id: 0x0400, Type: in), attribute measured_value (id: 0x0000). It changes the value read when I cover the sensor.
All the occupancy sensing output is None (from TuyaOccupancySensing (Endpoint id: 1, Id: 0x0406, Type: in)), except for occupancy (0x0000) which is always zero (0).
MmwRadarManufCluster, mcu_version returns 1.1.8. Firmware is 0x4a (74)
Edit:
For some unclear reason, a few restarts of HA later (finishing up other upgrade changes), I now have a sensor for Illuminance.
Occupancy is still always Clear (occupancy attribute reading 0).
I do however with manual reads of dp_105 get either 0x0, 0x1 or 0x2, which according to the zigbee2mqtt implementation is for Presence states.
[105, 'presence_state', tuya.valueConverterBasic.lookup({'None': tuya.enum(0), 'Presence': tuya.enum(1), 'Move': tuya.enum(2)})],
Occupancy seems to work (can show Clear and Detected as expected) if I switch occupancy from using dp_1 attribute, to using dp_112.
It's used in the zigbee2mqtt implementation for "presence" true/false. [112, 'presence', tuya.valueConverter.trueFalse1],
I don't know how to create a custom sensor for the presence_state type attribute.
I ordered several of these sensors for testing purposes and discovered they didn't work out of the box since I use ZHA. I don't want to migrate to Zigbee2MQTT just because of this issue.
@logan893, can you share how you added '_TZE204_ijxvkhd0' to the MmwRadarMotionGPP? I would like to test this.
Thank you
@visata I don't know if it's all proper, but this is what I have changed and added so far.
--- ts0601_motion.py.latest
+++ ts0601_motion.py
@@ -147,11 +147,16 @@
0xEF6A: ("dp_106", t.enum8, True),
0xEF6B: ("dp_107", t.enum8, True),
0xEF6C: ("dp_108", t.uint32_t, True),
+ 0xEF6D: ("dp_109", t.uint32_t, True),
+ 0xEF6E: ("dp_110", t.uint32_t, True),
+ 0xEF6F: ("dp_111", t.uint32_t, True),
+ #0xEF70: ("occupancy", t.uint32_t, True),
}
)
dp_to_attribute: Dict[int, DPToAttributeMapping] = {
- 1: DPToAttributeMapping(
+ # was: 1: DPToAttributeMapping(
+ 112: DPToAttributeMapping(
TuyaOccupancySensing.ep_attribute,
"occupancy",
),
@@ -209,6 +214,18 @@
TuyaMCUCluster.ep_attribute,
"dp_108",
),
+ 109: DPToAttributeMapping(
+ TuyaMCUCluster.ep_attribute,
+ "dp_109",
+ ),
+ 110: DPToAttributeMapping(
+ TuyaMCUCluster.ep_attribute,
+ "dp_110",
+ ),
+ 111: DPToAttributeMapping(
+ TuyaMCUCluster.ep_attribute,
+ "dp_111",
+ ),
}
data_point_handlers = {
@@ -226,6 +243,10 @@
106: "_dp_2_attr_update",
107: "_dp_2_attr_update",
108: "_dp_2_attr_update",
+ 109: "_dp_2_attr_update",
+ 110: "_dp_2_attr_update",
+ 111: "_dp_2_attr_update",
+ 112: "_dp_2_attr_update",
}
@@ -399,6 +420,7 @@
("_TZE200_sfiy5tfs", "TS0601"),
("_TZE200_mrf6vtua", "TS0601"),
("_TZE204_qasjif9e", "TS0601"),
+ ("_TZE204_ijxvkhd0", "TS0601"),
],
ENDPOINTS: {
1: {
@logan893 I'm adding this quirk from here https://fixtse.com/blog/zy-m100-full-zha-support
Will let you know if it works.
@visata Looks good. Have you gotten the "target distance" to display as a sensor?
I cobbled together a custom quirk based on fixtse's quirk ( https://fixtse.com/blog/zy-m100-full-zha-support ; https://gist.githubusercontent.com/fixtse/b95753b84c34b45f49b3116d23b66342/raw/0f84bd6e9b6c174970c7b5fc21078d4b4da06a15/TZE204_ijxvkhd0_e5m9c5hl.py ) with some influence from https://github.com/zigpy/zha-device-handlers/pull/2525#issuecomment-1826881992 and my own additions.
I haven't seen that there would be any "fade time remaining" parameter for _TZE204_ijxvkhd0, but I have no clue where to begin digging.
I added an Iaszone motion sensor to separately track the motion detected (seems to be a fixed fade time of roughly 5-6 seconds). Distance to target is via a pressure sensor, showing cm to target with unit hPa.
(this custom quirk also pasted in initial problem description)
## originally based on
### https://fixtse.com/blog/zy-m100-full-zha-support
### ( https://gist.githubusercontent.com/fixtse/b95753b84c34b45f49b3116d23b66342/raw/0f84bd6e9b6c174970c7b5fc21078d4b4da06a15/TZE204_ijxvkhd0_e5m9c5hl.py )
## with inspiration from
### https://github.com/zigpy/zha-device-handlers/pull/2525#issuecomment-1826881992
## and my own changes and additions
import math
from typing import Dict
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 (
AnalogInput,
AnalogOutput,
Basic,
GreenPowerProxy,
Groups,
Ota,
Scenes,
Time,
)
from zigpy.zcl.clusters.measurement import (
IlluminanceMeasurement,
OccupancySensing,
PressureMeasurement,
)
from zigpy.zcl.clusters.security import IasZone, ZoneType
from zhaquirks import Bus, LocalDataCluster
from zhaquirks.const import (
DEVICE_TYPE,
ENDPOINTS,
INPUT_CLUSTERS,
MODELS_INFO,
OUTPUT_CLUSTERS,
PROFILE_ID,
CLUSTER_COMMAND,
ZONE_STATUS_CHANGE_COMMAND,
ON,
OFF,
)
from zhaquirks.tuya import (
NoManufacturerCluster,
TuyaLocalCluster,
TuyaNewManufCluster,
)
from zhaquirks.tuya.mcu import (
DPToAttributeMapping,
TuyaAttributesCluster,
TuyaMCUCluster,
)
class TuyaMmwRadarSelfTest(t.enum8):
"""Mmw radar self test values."""
TESTING = 0
TEST_SUCCESS = 1
TEST_FAILURE = 2
OTHER = 3
COMM_FAULT = 4
RADAR_FAULT = 5
class PresenceMotionEnum(t.enum8):
"""Presence and motion enum."""
NONE = 0x00
PRESENCE = 0x01
MOTION = 0x02
class TuyaMmwRadarTargetDistanceAsPressureMeasurement(PressureMeasurement, TuyaLocalCluster): # result in centimeteres expressed as hPa
"""Target Distance."""
class TuyaMmwRadarMotionSensitivity(TuyaAttributesCluster, AnalogOutput):
"""AnalogOutput cluster for motion sensitivity."""
_CONSTANT_ATTRIBUTES = {
AnalogOutput.AttributeDefs.description.id: "motion sensitivity",
AnalogOutput.AttributeDefs.min_present_value.id: 1,
AnalogOutput.AttributeDefs.max_present_value.id: 9,
AnalogOutput.AttributeDefs.resolution.id: 1,
}
class TuyaMmwRadarPresenceSensitivity(TuyaAttributesCluster, AnalogOutput):
"""AnalogOutput cluster for presence sensitivity."""
_CONSTANT_ATTRIBUTES = {
AnalogOutput.AttributeDefs.description.id: "presence sensitivity",
AnalogOutput.AttributeDefs.min_present_value.id: 1,
AnalogOutput.AttributeDefs.max_present_value.id: 9,
AnalogOutput.AttributeDefs.resolution.id: 1,
}
class TuyaMmwRadarFadingTime(TuyaAttributesCluster, AnalogOutput):
"""AnalogOutput cluster for fading time."""
_CONSTANT_ATTRIBUTES = {
AnalogOutput.AttributeDefs.description.id: "fading time",
AnalogOutput.AttributeDefs.min_present_value.id: 1,
AnalogOutput.AttributeDefs.max_present_value.id: 1000,
AnalogOutput.AttributeDefs.resolution.id: 1, # Resolution 1 second
AnalogOutput.AttributeDefs.engineering_units.id: 73, # 73 defines seconds, the expected unit
}
class TuyaMmwRadarMaxRange(TuyaAttributesCluster, AnalogOutput):
"""AnalogOutput cluster for max range."""
_CONSTANT_ATTRIBUTES = {
AnalogOutput.AttributeDefs.description.id: "max detection range",
AnalogOutput.AttributeDefs.min_present_value.id: 150, # min allowed = 150
AnalogOutput.AttributeDefs.max_present_value.id: 550, # max allowed = 550
AnalogOutput.AttributeDefs.resolution.id: 100, #resolution = 100 centermeters (snaps back to 100 cm intervals between 150 and 550)
AnalogOutput.AttributeDefs.engineering_units.id: 118, # 118 defines centimeters, the expected unit
}
class TuyaOccupancySensing(OccupancySensing, TuyaLocalCluster):
"""Tuya local OccupancySensing cluster."""
class TuyaIlluminanceMeasurement(IlluminanceMeasurement, TuyaLocalCluster):
"""Tuya local IlluminanceMeasurement cluster."""
class TuyaOccupancyMotionSensing(OccupancySensing, TuyaLocalCluster):
"""Tuya local OccupancySensing cluster for motion state."""
class TuyaMmwRadarMotionAnalogInputCluster(TuyaLocalCluster, AnalogInput):
"""Analog input cluster, only used to relay motion state information to Iaszone motion sensor."""
cluster_id = AnalogInput.cluster_id
def __init__(self, *args, **kwargs):
"""Init."""
super().__init__(*args, **kwargs)
def _update_attribute(self, attrid, value):
super()._update_attribute(attrid, value)
if attrid == AnalogInput.AttributeDefs.present_value.id: # 0x55 = "present_value" for AnalogInput
if value == PresenceMotionEnum.MOTION:
self.endpoint.device.motion_bus.listener_event("_turn_on")
else:
self.endpoint.device.motion_bus.listener_event("_turn_off")
class TuyaMmwRadarMotionSensing(LocalDataCluster, IasZone):
"""IasZone cluster for motion."""
_CONSTANT_ATTRIBUTES = {
IasZone.AttributeDefs.zone_type.id: ZoneType.Motion_Sensor, # 0x000D, # motion type
}
cluster_id = IasZone.cluster_id
def __init__(self, *args, **kwargs):
"""Init."""
super().__init__(*args, **kwargs)
self.endpoint.device.motion_bus.add_listener(self)
def _turn_off(self):
self.listener_event(
CLUSTER_COMMAND, 253, ZONE_STATUS_CHANGE_COMMAND, [OFF, 0, 0, 0]
)
def _turn_on(self):
self.listener_event(
CLUSTER_COMMAND, 254, ZONE_STATUS_CHANGE_COMMAND, [ON, 0, 0, 0]
)
class TuyaMmwRadarCluster(NoManufacturerCluster, TuyaMCUCluster):
"""Mmw radar cluster."""
attributes = TuyaMCUCluster.attributes.copy()
dp_to_attribute: Dict[int, DPToAttributeMapping] = {
103: DPToAttributeMapping(
TuyaMCUCluster.ep_attribute,
"cli",
),
104: DPToAttributeMapping(
TuyaIlluminanceMeasurement.ep_attribute,
"measured_value",
converter=lambda x: int(math.log10(x) * 10000 + 1) if x > 0 else int(0),
),
105: DPToAttributeMapping(
TuyaMmwRadarMotionAnalogInputCluster.ep_attribute,
"present_value",
converter=lambda x: PresenceMotionEnum(x),
endpoint_id=2,
),
106: DPToAttributeMapping(
TuyaMmwRadarMotionSensitivity.ep_attribute,
"present_value",
endpoint_id=6,
converter=lambda x: x if x < 10 else x / 10,
),
107: DPToAttributeMapping(
TuyaMmwRadarMaxRange.ep_attribute,
"present_value",
endpoint_id=3,
),
109: DPToAttributeMapping(
TuyaMmwRadarTargetDistanceAsPressureMeasurement.ep_attribute,
"measured_value",
),
110: DPToAttributeMapping(
TuyaMmwRadarFadingTime.ep_attribute,
"present_value",
endpoint_id=5,
),
111: DPToAttributeMapping(
TuyaMmwRadarPresenceSensitivity.ep_attribute,
"present_value",
endpoint_id=7,
converter=lambda x: x if x < 10 else x / 10,
),
112: DPToAttributeMapping(
TuyaOccupancySensing.ep_attribute,
"occupancy",
),
}
data_point_handlers = {
103: "_dp_2_attr_update",
104: "_dp_2_attr_update",
105: "_dp_2_attr_update",
106: "_dp_2_attr_update",
107: "_dp_2_attr_update",
109: "_dp_2_attr_update",
110: "_dp_2_attr_update",
111: "_dp_2_attr_update",
112: "_dp_2_attr_update",
}
class TuyaMmwRadarOccupancy(CustomDevice):
"""Millimeter wave occupancy sensor."""
def __init__(self, *args, **kwargs):
"""Init device."""
self.motion_bus = Bus()
super().__init__(*args, **kwargs)
signature = {
# endpoint=1, profile=260, device_type=81, device_version=1,
# input_clusters=[0, 4, 5, 61184], output_clusters=[25, 10]
MODELS_INFO: [
("_TZE204_ijxvkhd0", "TS0601"),
("_TZE204_e5m9c5hl", "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,
TuyaNewManufCluster.cluster_id,
],
OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
},
242: {
# <SimpleDescriptor endpoint=242 profile=41440 device_type=97
# input_clusters=[]
# output_clusters=[33]
PROFILE_ID: 41440,
DEVICE_TYPE: 97,
INPUT_CLUSTERS: [],
OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
},
},
}
replacement = {
ENDPOINTS: {
1: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR,
INPUT_CLUSTERS: [
Basic.cluster_id,
Groups.cluster_id,
Scenes.cluster_id,
TuyaMmwRadarCluster,
TuyaIlluminanceMeasurement,
TuyaOccupancySensing,
TuyaMmwRadarTargetDistanceAsPressureMeasurement,
],
OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
},
2: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR,
INPUT_CLUSTERS: [
TuyaMmwRadarMotionAnalogInputCluster,
TuyaMmwRadarMotionSensing,
],
OUTPUT_CLUSTERS: [],
},
3: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.COMBINED_INTERFACE,
INPUT_CLUSTERS: [
TuyaMmwRadarMaxRange,
],
OUTPUT_CLUSTERS: [],
},
5: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.COMBINED_INTERFACE,
INPUT_CLUSTERS: [
TuyaMmwRadarFadingTime,
],
OUTPUT_CLUSTERS: [],
},
6: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.COMBINED_INTERFACE,
INPUT_CLUSTERS: [
TuyaMmwRadarMotionSensitivity,
],
OUTPUT_CLUSTERS: [],
},
7: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.COMBINED_INTERFACE,
INPUT_CLUSTERS: [
TuyaMmwRadarPresenceSensitivity,
],
OUTPUT_CLUSTERS: [],
},
242: {
PROFILE_ID: 41440,
DEVICE_TYPE: 97,
INPUT_CLUSTERS: [],
OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
},
}
}
Hi @logan893 ,
it works like a charm for _TZE204_ijxvkhd0. Thanks for sharing such a pearl :) One question: how to correlate distance to target in cm's and hPA ? At first glance such units don't correlate. Can you shed some light here ?
Thank you.
I cobbled together a custom quirk based on fixtse's quirk ( https://fixtse.com/blog/zy-m100-full-zha-support ; https://gist.githubusercontent.com/fixtse/b95753b84c34b45f49b3116d23b66342/raw/0f84bd6e9b6c174970c7b5fc21078d4b4da06a15/TZE204_ijxvkhd0_e5m9c5hl.py ) with some influence from #2525 (comment) and my own additions.
I haven't seen that there would be any "fade time remaining" parameter for _TZE204_ijxvkhd0, but I have no clue where to begin digging.
I added an Iaszone motion sensor to separately track the motion detected (seems to be a fixed fade time of roughly 5-6 seconds). Distance to target is via a pressure sensor, showing cm to target with unit hPa.
(this custom quirk also pasted in initial problem description)
## originally based on ### https://fixtse.com/blog/zy-m100-full-zha-support ### ( https://gist.githubusercontent.com/fixtse/b95753b84c34b45f49b3116d23b66342/raw/0f84bd6e9b6c174970c7b5fc21078d4b4da06a15/TZE204_ijxvkhd0_e5m9c5hl.py ) ## with inspiration from ### https://github.com/zigpy/zha-device-handlers/pull/2525#issuecomment-1826881992 ## and my own changes and additions import math from typing import Dict 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 ( AnalogInput, AnalogOutput, Basic, GreenPowerProxy, Groups, Ota, Scenes, Time, ) from zigpy.zcl.clusters.measurement import ( IlluminanceMeasurement, OccupancySensing, PressureMeasurement, ) from zigpy.zcl.clusters.security import IasZone, ZoneType from zhaquirks import Bus, LocalDataCluster from zhaquirks.const import ( DEVICE_TYPE, ENDPOINTS, INPUT_CLUSTERS, MODELS_INFO, OUTPUT_CLUSTERS, PROFILE_ID, CLUSTER_COMMAND, ZONE_STATUS_CHANGE_COMMAND, ON, OFF, ) from zhaquirks.tuya import ( NoManufacturerCluster, TuyaLocalCluster, TuyaNewManufCluster, ) from zhaquirks.tuya.mcu import ( DPToAttributeMapping, TuyaAttributesCluster, TuyaMCUCluster, ) class TuyaMmwRadarSelfTest(t.enum8): """Mmw radar self test values.""" TESTING = 0 TEST_SUCCESS = 1 TEST_FAILURE = 2 OTHER = 3 COMM_FAULT = 4 RADAR_FAULT = 5 class PresenceMotionEnum(t.enum8): """Presence and motion enum.""" NONE = 0x00 PRESENCE = 0x01 MOTION = 0x02 class TuyaMmwRadarTargetDistanceAsPressureMeasurement(PressureMeasurement, TuyaLocalCluster): # result in centimeteres expressed as hPa """Target Distance.""" class TuyaMmwRadarMotionSensitivity(TuyaAttributesCluster, AnalogOutput): """AnalogOutput cluster for motion sensitivity.""" _CONSTANT_ATTRIBUTES = { AnalogOutput.AttributeDefs.description.id: "motion sensitivity", AnalogOutput.AttributeDefs.min_present_value.id: 1, AnalogOutput.AttributeDefs.max_present_value.id: 9, AnalogOutput.AttributeDefs.resolution.id: 1, } class TuyaMmwRadarPresenceSensitivity(TuyaAttributesCluster, AnalogOutput): """AnalogOutput cluster for presence sensitivity.""" _CONSTANT_ATTRIBUTES = { AnalogOutput.AttributeDefs.description.id: "presence sensitivity", AnalogOutput.AttributeDefs.min_present_value.id: 1, AnalogOutput.AttributeDefs.max_present_value.id: 9, AnalogOutput.AttributeDefs.resolution.id: 1, } class TuyaMmwRadarFadingTime(TuyaAttributesCluster, AnalogOutput): """AnalogOutput cluster for fading time.""" _CONSTANT_ATTRIBUTES = { AnalogOutput.AttributeDefs.description.id: "fading time", AnalogOutput.AttributeDefs.min_present_value.id: 1, AnalogOutput.AttributeDefs.max_present_value.id: 1000, AnalogOutput.AttributeDefs.resolution.id: 1, # Resolution 1 second AnalogOutput.AttributeDefs.engineering_units.id: 73, # 73 defines seconds, the expected unit } class TuyaMmwRadarMaxRange(TuyaAttributesCluster, AnalogOutput): """AnalogOutput cluster for max range.""" _CONSTANT_ATTRIBUTES = { AnalogOutput.AttributeDefs.description.id: "max detection range", AnalogOutput.AttributeDefs.min_present_value.id: 150, # min allowed = 150 AnalogOutput.AttributeDefs.max_present_value.id: 550, # max allowed = 550 AnalogOutput.AttributeDefs.resolution.id: 100, #resolution = 100 centermeters (snaps back to 100 cm intervals between 150 and 550) AnalogOutput.AttributeDefs.engineering_units.id: 118, # 118 defines centimeters, the expected unit } class TuyaOccupancySensing(OccupancySensing, TuyaLocalCluster): """Tuya local OccupancySensing cluster.""" class TuyaIlluminanceMeasurement(IlluminanceMeasurement, TuyaLocalCluster): """Tuya local IlluminanceMeasurement cluster.""" class TuyaOccupancyMotionSensing(OccupancySensing, TuyaLocalCluster): """Tuya local OccupancySensing cluster for motion state.""" class TuyaMmwRadarMotionAnalogInputCluster(TuyaLocalCluster, AnalogInput): """Analog input cluster, only used to relay motion state information to Iaszone motion sensor.""" cluster_id = AnalogInput.cluster_id def __init__(self, *args, **kwargs): """Init.""" super().__init__(*args, **kwargs) def _update_attribute(self, attrid, value): super()._update_attribute(attrid, value) if attrid == AnalogInput.AttributeDefs.present_value.id: # 0x55 = "present_value" for AnalogInput if value == PresenceMotionEnum.MOTION: self.endpoint.device.motion_bus.listener_event("_turn_on") else: self.endpoint.device.motion_bus.listener_event("_turn_off") class TuyaMmwRadarMotionSensing(LocalDataCluster, IasZone): """IasZone cluster for motion.""" _CONSTANT_ATTRIBUTES = { IasZone.AttributeDefs.zone_type.id: ZoneType.Motion_Sensor, # 0x000D, # motion type } cluster_id = IasZone.cluster_id def __init__(self, *args, **kwargs): """Init.""" super().__init__(*args, **kwargs) self.endpoint.device.motion_bus.add_listener(self) def _turn_off(self): self.listener_event( CLUSTER_COMMAND, 253, ZONE_STATUS_CHANGE_COMMAND, [OFF, 0, 0, 0] ) def _turn_on(self): self.listener_event( CLUSTER_COMMAND, 254, ZONE_STATUS_CHANGE_COMMAND, [ON, 0, 0, 0] ) class TuyaMmwRadarCluster(NoManufacturerCluster, TuyaMCUCluster): """Mmw radar cluster.""" attributes = TuyaMCUCluster.attributes.copy() dp_to_attribute: Dict[int, DPToAttributeMapping] = { 103: DPToAttributeMapping( TuyaMCUCluster.ep_attribute, "cli", ), 104: DPToAttributeMapping( TuyaIlluminanceMeasurement.ep_attribute, "measured_value", converter=lambda x: int(math.log10(x) * 10000 + 1) if x > 0 else int(0), ), 105: DPToAttributeMapping( TuyaMmwRadarMotionAnalogInputCluster.ep_attribute, "present_value", converter=lambda x: PresenceMotionEnum(x), endpoint_id=2, ), 106: DPToAttributeMapping( TuyaMmwRadarMotionSensitivity.ep_attribute, "present_value", endpoint_id=6, converter=lambda x: x if x < 10 else x / 10, ), 107: DPToAttributeMapping( TuyaMmwRadarMaxRange.ep_attribute, "present_value", endpoint_id=3, ), 109: DPToAttributeMapping( TuyaMmwRadarTargetDistanceAsPressureMeasurement.ep_attribute, "measured_value", ), 110: DPToAttributeMapping( TuyaMmwRadarFadingTime.ep_attribute, "present_value", endpoint_id=5, ), 111: DPToAttributeMapping( TuyaMmwRadarPresenceSensitivity.ep_attribute, "present_value", endpoint_id=7, converter=lambda x: x if x < 10 else x / 10, ), 112: DPToAttributeMapping( TuyaOccupancySensing.ep_attribute, "occupancy", ), } data_point_handlers = { 103: "_dp_2_attr_update", 104: "_dp_2_attr_update", 105: "_dp_2_attr_update", 106: "_dp_2_attr_update", 107: "_dp_2_attr_update", 109: "_dp_2_attr_update", 110: "_dp_2_attr_update", 111: "_dp_2_attr_update", 112: "_dp_2_attr_update", } class TuyaMmwRadarOccupancy(CustomDevice): """Millimeter wave occupancy sensor.""" def __init__(self, *args, **kwargs): """Init device.""" self.motion_bus = Bus() super().__init__(*args, **kwargs) signature = { # endpoint=1, profile=260, device_type=81, device_version=1, # input_clusters=[0, 4, 5, 61184], output_clusters=[25, 10] MODELS_INFO: [ ("_TZE204_ijxvkhd0", "TS0601"), ("_TZE204_e5m9c5hl", "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, TuyaNewManufCluster.cluster_id, ], OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], }, 242: { # <SimpleDescriptor endpoint=242 profile=41440 device_type=97 # input_clusters=[] # output_clusters=[33] PROFILE_ID: 41440, DEVICE_TYPE: 97, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, }, } replacement = { ENDPOINTS: { 1: { PROFILE_ID: zha.PROFILE_ID, DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, INPUT_CLUSTERS: [ Basic.cluster_id, Groups.cluster_id, Scenes.cluster_id, TuyaMmwRadarCluster, TuyaIlluminanceMeasurement, TuyaOccupancySensing, TuyaMmwRadarTargetDistanceAsPressureMeasurement, ], OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], }, 2: { PROFILE_ID: zha.PROFILE_ID, DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, INPUT_CLUSTERS: [ TuyaMmwRadarMotionAnalogInputCluster, TuyaMmwRadarMotionSensing, ], OUTPUT_CLUSTERS: [], }, 3: { PROFILE_ID: zha.PROFILE_ID, DEVICE_TYPE: zha.DeviceType.COMBINED_INTERFACE, INPUT_CLUSTERS: [ TuyaMmwRadarMaxRange, ], OUTPUT_CLUSTERS: [], }, 5: { PROFILE_ID: zha.PROFILE_ID, DEVICE_TYPE: zha.DeviceType.COMBINED_INTERFACE, INPUT_CLUSTERS: [ TuyaMmwRadarFadingTime, ], OUTPUT_CLUSTERS: [], }, 6: { PROFILE_ID: zha.PROFILE_ID, DEVICE_TYPE: zha.DeviceType.COMBINED_INTERFACE, INPUT_CLUSTERS: [ TuyaMmwRadarMotionSensitivity, ], OUTPUT_CLUSTERS: [], }, 7: { PROFILE_ID: zha.PROFILE_ID, DEVICE_TYPE: zha.DeviceType.COMBINED_INTERFACE, INPUT_CLUSTERS: [ TuyaMmwRadarPresenceSensitivity, ], OUTPUT_CLUSTERS: [], }, 242: { PROFILE_ID: 41440, DEVICE_TYPE: 97, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, } }
This worked wonders for me!
Edit: nm ... PEBKAC error
struggling to get it to pick up my model (_TZE204_qasjif9e). I've added that to the sig part of the py script and reloaded but it just keeps on picking up the same (what seems to be the default) Quirk: zhaquirks.tuya.ts0601_motion.MmwRadarMotionGPP
edit ok sorted that, it was just an extra bracket in the code which somehow got in there.
but now it works, it picks up illum values correctly (afaik) but always says clear to the presence/motion. and there doesn't appear to be a minimum distance value nor is there the exposed detected distance (not that detection is working right now). also also, the max distance seems to be locked to 550cm when it should go up to 900cm.
just seen that you mention the 24g model on yours, and mine is not. would that make a difference?
I have a similar issue where the presence remains on detected and never changes to clear. The device is _TZE204_ztc6ggyl TS0601, I tried using the quirk posted, adding the device details to the signature section but get none the adjustment sliders in HA zigbee page. I'm not sure what I'm missing.
anyone still caring about these devices?
Hi @logan893 ,
it works like a charm for _TZE204_ijxvkhd0. Thanks for sharing such a pearl :) One question: how to correlate distance to target in cm's and hPA ? At first glance such units don't correlate. Can you shed some light here ?
Thank you.
The distance is exactly the number of cm reported from the sensor. Minimum distance seems to be 150 cm (i.e. 150 hPa with the sensor), maximum 550 cm.
anyone still caring about these devices?
The _TZE204_qasjif9e variant is the 5.8GHz version and it behaves quite differently. This quirk of mine is for the 24GHz model only.
I also have that 5.8GHz variant with the same model as yours, and I too had problems with the default implementation. I haven't yet posted my updated quirk file that covers both, as I'm not sure it truly covers everything.
With my variant of a quirk for _TZE204_qasjif9e you get access to a trigger-delay-timer and clear-timer, you get distance sensor and a min/max sense range, sensitivity, occupancy, and illumination. There is no extra active-motion sensor like there is for the 24GHz variant. Again, I don't know if this quirk covers it all.
I'll see if I can post it later today.
I have a similar issue where the presence remains on detected and never changes to clear. The device is _TZE204_ztc6ggyl TS0601, I tried using the quirk posted, adding the device details to the signature section but get none the adjustment sliders in HA zigbee page. I'm not sure what I'm missing.
Is _TZE204_ztc6ggyl the ceiling mounted 5.8GHz variant? If it works the same like the wall mount variant then my updated quirk file (will post soon) should work for that also.
Edit: Seems this device is quite different from the two I have, and my quirks won't work.
@thefunkygibbon Try this one for _TZE204_qasjif9e.
## originally based on
### https://fixtse.com/blog/zy-m100-full-zha-support
### ( https://gist.githubusercontent.com/fixtse/b95753b84c34b45f49b3116d23b66342/raw/0f84bd6e9b6c174970c7b5fc21078d4b4da06a15/TZE204_ijxvkhd0_e5m9c5hl.py )
## with inspiration from
### https://github.com/zigpy/zha-device-handlers/pull/2525#issuecomment-1826881992
## and my own changes and additions
## Update: adding a quirk to support USB-C wall mount 5.8GHz variant (_TZE204_qasjif9e) based on zigbee2mqtt implementation (bonus support for _TZE204_ztqnh5cg)
## https://github.com/Koenkk/zigbee-herdsman-converters/blob/master/src/devices/tuya.ts
import math
from typing import Dict
from zigpy.profiles import zgp, zha
from zigpy.quirks import CustomDevice
import zigpy.types as t
from zigpy.zcl import foundation
from zigpy.zcl.clusters.general import (
AnalogInput,
AnalogOutput,
Basic,
GreenPowerProxy,
Groups,
Ota,
Scenes,
Time,
)
from zigpy.zcl.clusters.measurement import (
IlluminanceMeasurement,
OccupancySensing,
PressureMeasurement,
)
from zigpy.zcl.clusters.security import IasZone, ZoneType
from zhaquirks import Bus, LocalDataCluster
from zhaquirks.const import (
DEVICE_TYPE,
ENDPOINTS,
INPUT_CLUSTERS,
MODELS_INFO,
OUTPUT_CLUSTERS,
PROFILE_ID,
CLUSTER_COMMAND,
ZONE_STATUS_CHANGE_COMMAND,
ON,
OFF,
)
from zhaquirks.tuya import (
NoManufacturerCluster,
TuyaLocalCluster,
TuyaNewManufCluster,
)
from zhaquirks.tuya.mcu import (
DPToAttributeMapping,
TuyaAttributesCluster,
TuyaMCUCluster,
)
class TuyaMmwRadarSelfTest(t.enum8):
"""Mmw radar self test values."""
TESTING = 0
TEST_SUCCESS = 1
TEST_FAILURE = 2
OTHER = 3
COMM_FAULT = 4
RADAR_FAULT = 5
class PresenceMotionEnum(t.enum8):
"""Presence and motion enum."""
NONE = 0x00
PRESENCE = 0x01
MOTION = 0x02
class TuyaMmwRadarTargetDistanceAsPressureMeasurement(PressureMeasurement, TuyaLocalCluster): # result in centimeteres expressed as hPa
"""Target Distance."""
class TuyaMmwRadarMotionSensitivity(TuyaAttributesCluster, AnalogOutput):
"""AnalogOutput cluster for motion sensitivity."""
_CONSTANT_ATTRIBUTES = {
AnalogOutput.AttributeDefs.description.id: "motion sensitivity",
AnalogOutput.AttributeDefs.min_present_value.id: 1,
AnalogOutput.AttributeDefs.max_present_value.id: 9,
AnalogOutput.AttributeDefs.resolution.id: 1,
}
class TuyaMmwRadarPresenceSensitivity(TuyaAttributesCluster, AnalogOutput):
"""AnalogOutput cluster for presence sensitivity."""
_CONSTANT_ATTRIBUTES = {
AnalogOutput.AttributeDefs.description.id: "presence sensitivity",
AnalogOutput.AttributeDefs.min_present_value.id: 1,
AnalogOutput.AttributeDefs.max_present_value.id: 9,
AnalogOutput.AttributeDefs.resolution.id: 1,
}
class TuyaMmwRadarFadingTime(TuyaAttributesCluster, AnalogOutput):
"""AnalogOutput cluster for fading time."""
_CONSTANT_ATTRIBUTES = {
AnalogOutput.AttributeDefs.description.id: "fading time",
AnalogOutput.AttributeDefs.min_present_value.id: 1,
AnalogOutput.AttributeDefs.max_present_value.id: 1000,
AnalogOutput.AttributeDefs.resolution.id: 1, # Resolution 1 second
AnalogOutput.AttributeDefs.engineering_units.id: 73, # 73 defines seconds, the expected unit
}
class TuyaMmwRadarMaxRange(TuyaAttributesCluster, AnalogOutput):
"""AnalogOutput cluster for max range."""
_CONSTANT_ATTRIBUTES = {
AnalogOutput.AttributeDefs.description.id: "max detection range",
AnalogOutput.AttributeDefs.min_present_value.id: 150, # min allowed = 150
AnalogOutput.AttributeDefs.max_present_value.id: 550, # max allowed = 550
AnalogOutput.AttributeDefs.resolution.id: 100, #resolution = 100 centermeters (snaps back to 100 cm intervals between 150 and 550)
AnalogOutput.AttributeDefs.engineering_units.id: 118, # 118 defines centimeters, the expected unit
}
class TuyaOccupancySensing(OccupancySensing, TuyaLocalCluster):
"""Tuya local OccupancySensing cluster."""
class TuyaIlluminanceMeasurement(IlluminanceMeasurement, TuyaLocalCluster):
"""Tuya local IlluminanceMeasurement cluster."""
class TuyaOccupancyMotionSensing(OccupancySensing, TuyaLocalCluster):
"""Tuya local OccupancySensing cluster for motion state."""
class TuyaMmwRadarMotionAnalogInputCluster(TuyaLocalCluster, AnalogInput):
"""Analog input cluster, only used to relay motion state information to Iaszone motion sensor."""
cluster_id = AnalogInput.cluster_id
def __init__(self, *args, **kwargs):
"""Init."""
super().__init__(*args, **kwargs)
def _update_attribute(self, attrid, value):
super()._update_attribute(attrid, value)
if attrid == AnalogInput.AttributeDefs.present_value.id: # 0x55 = "present_value" for AnalogInput
if value == PresenceMotionEnum.MOTION:
self.endpoint.device.motion_bus.listener_event("_turn_on")
else:
self.endpoint.device.motion_bus.listener_event("_turn_off")
class TuyaMmwRadarMotionSensing(LocalDataCluster, IasZone):
"""IasZone cluster for motion."""
_CONSTANT_ATTRIBUTES = {
IasZone.AttributeDefs.zone_type.id: ZoneType.Motion_Sensor, # 0x000D, # motion type
}
cluster_id = IasZone.cluster_id
def __init__(self, *args, **kwargs):
"""Init."""
super().__init__(*args, **kwargs)
self.endpoint.device.motion_bus.add_listener(self)
def _turn_off(self):
self.listener_event(
CLUSTER_COMMAND, 253, ZONE_STATUS_CHANGE_COMMAND, [OFF, 0, 0, 0]
)
def _turn_on(self):
self.listener_event(
CLUSTER_COMMAND, 254, ZONE_STATUS_CHANGE_COMMAND, [ON, 0, 0, 0]
)
class TuyaMmwRadarCluster(NoManufacturerCluster, TuyaMCUCluster):
"""Mmw radar cluster."""
attributes = TuyaMCUCluster.attributes.copy()
dp_to_attribute: Dict[int, DPToAttributeMapping] = {
103: DPToAttributeMapping(
TuyaMCUCluster.ep_attribute,
"cli",
),
104: DPToAttributeMapping(
TuyaIlluminanceMeasurement.ep_attribute,
"measured_value",
converter=lambda x: int(math.log10(x) * 10000 + 1) if x > 0 else int(0),
),
105: DPToAttributeMapping(
TuyaMmwRadarMotionAnalogInputCluster.ep_attribute,
"present_value",
converter=lambda x: PresenceMotionEnum(x),
endpoint_id=2,
),
106: DPToAttributeMapping(
TuyaMmwRadarMotionSensitivity.ep_attribute,
"present_value",
endpoint_id=6,
converter=lambda x: x if x < 10 else x / 10,
),
107: DPToAttributeMapping(
TuyaMmwRadarMaxRange.ep_attribute,
"present_value",
endpoint_id=3,
),
109: DPToAttributeMapping(
TuyaMmwRadarTargetDistanceAsPressureMeasurement.ep_attribute,
"measured_value",
),
110: DPToAttributeMapping(
TuyaMmwRadarFadingTime.ep_attribute,
"present_value",
endpoint_id=5,
),
111: DPToAttributeMapping(
TuyaMmwRadarPresenceSensitivity.ep_attribute,
"present_value",
endpoint_id=7,
converter=lambda x: x if x < 10 else x / 10,
),
112: DPToAttributeMapping(
TuyaOccupancySensing.ep_attribute,
"occupancy",
),
}
data_point_handlers = {
103: "_dp_2_attr_update",
104: "_dp_2_attr_update",
105: "_dp_2_attr_update",
106: "_dp_2_attr_update",
107: "_dp_2_attr_update",
109: "_dp_2_attr_update",
110: "_dp_2_attr_update",
111: "_dp_2_attr_update",
112: "_dp_2_attr_update",
}
class TuyaMmwRadarOccupancy(CustomDevice):
"""Millimeter wave occupancy sensor."""
def __init__(self, *args, **kwargs):
"""Init device."""
self.motion_bus = Bus()
super().__init__(*args, **kwargs)
signature = {
# endpoint=1, profile=260, device_type=81, device_version=1,
# input_clusters=[0, 4, 5, 61184], output_clusters=[25, 10]
MODELS_INFO: [
("_TZE204_ijxvkhd0", "TS0601"), # USB-C wall mounted 24GHz
#("_TZE204_e5m9c5hl", "TS0601"), # untested (and it uses a different config according to the zigbee2mqtt implementation)
],
ENDPOINTS: {
1: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
INPUT_CLUSTERS: [
Basic.cluster_id,
Groups.cluster_id,
Scenes.cluster_id,
TuyaNewManufCluster.cluster_id,
],
OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
},
242: {
# <SimpleDescriptor endpoint=242 profile=41440 device_type=97
# input_clusters=[]
# output_clusters=[33]
PROFILE_ID: 41440,
DEVICE_TYPE: 97,
INPUT_CLUSTERS: [],
OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
},
},
}
replacement = {
ENDPOINTS: {
1: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR,
INPUT_CLUSTERS: [
Basic.cluster_id,
Groups.cluster_id,
Scenes.cluster_id,
TuyaMmwRadarCluster,
TuyaIlluminanceMeasurement,
TuyaOccupancySensing,
TuyaMmwRadarTargetDistanceAsPressureMeasurement,
],
OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
},
2: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR,
INPUT_CLUSTERS: [
TuyaMmwRadarMotionAnalogInputCluster,
TuyaMmwRadarMotionSensing,
],
OUTPUT_CLUSTERS: [],
},
3: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.COMBINED_INTERFACE,
INPUT_CLUSTERS: [
TuyaMmwRadarMaxRange,
],
OUTPUT_CLUSTERS: [],
},
5: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.COMBINED_INTERFACE,
INPUT_CLUSTERS: [
TuyaMmwRadarFadingTime,
],
OUTPUT_CLUSTERS: [],
},
6: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.COMBINED_INTERFACE,
INPUT_CLUSTERS: [
TuyaMmwRadarMotionSensitivity,
],
OUTPUT_CLUSTERS: [],
},
7: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.COMBINED_INTERFACE,
INPUT_CLUSTERS: [
TuyaMmwRadarPresenceSensitivity,
],
OUTPUT_CLUSTERS: [],
},
242: {
PROFILE_ID: 41440,
DEVICE_TYPE: 97,
INPUT_CLUSTERS: [],
OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
},
}
}
# another variant
class TuyaMmwRadarSensitivity(TuyaAttributesCluster, AnalogOutput):
"""AnalogOutput cluster for sensitivity."""
_CONSTANT_ATTRIBUTES = {
AnalogOutput.AttributeDefs.description.id: "sensitivity",
AnalogOutput.AttributeDefs.min_present_value.id: 1,
AnalogOutput.AttributeDefs.max_present_value.id: 9,
AnalogOutput.AttributeDefs.resolution.id: 1,
}
class TuyaMmwRadarMinRange(TuyaAttributesCluster, AnalogOutput):
"""AnalogOutput cluster for min range."""
_CONSTANT_ATTRIBUTES = {
AnalogOutput.AttributeDefs.description.id: "min_range",
AnalogOutput.AttributeDefs.min_present_value.id: 0,
AnalogOutput.AttributeDefs.max_present_value.id: 950,
AnalogOutput.AttributeDefs.resolution.id: 10,
AnalogOutput.AttributeDefs.engineering_units.id: 118, # 31: meters
}
class TuyaMmwRadarMaxRangeV2(TuyaAttributesCluster, AnalogOutput):
"""AnalogOutput cluster for max range."""
_CONSTANT_ATTRIBUTES = {
AnalogOutput.AttributeDefs.description.id: "max_range",
AnalogOutput.AttributeDefs.min_present_value.id: 10,
AnalogOutput.AttributeDefs.max_present_value.id: 950,
AnalogOutput.AttributeDefs.resolution.id: 10,
AnalogOutput.AttributeDefs.engineering_units.id: 118, # 31: meters
}
class TuyaMmwRadarDetectionDelay(TuyaAttributesCluster, AnalogOutput):
"""AnalogOutput cluster for detection delay."""
_CONSTANT_ATTRIBUTES = {
AnalogOutput.AttributeDefs.description.id: "detection_delay",
AnalogOutput.AttributeDefs.min_present_value.id: 000,
AnalogOutput.AttributeDefs.max_present_value.id: 20000,
AnalogOutput.AttributeDefs.resolution.id: 100,
AnalogOutput.AttributeDefs.engineering_units.id: 159, # 73: seconds
}
class TuyaMmwRadarFadingTimeV2(TuyaAttributesCluster, AnalogOutput):
"""AnalogOutput cluster for fading time."""
_CONSTANT_ATTRIBUTES = {
AnalogOutput.AttributeDefs.description.id: "fading_time",
AnalogOutput.AttributeDefs.min_present_value.id: 2000,
AnalogOutput.AttributeDefs.max_present_value.id: 200000,
AnalogOutput.AttributeDefs.resolution.id: 1000,
AnalogOutput.AttributeDefs.engineering_units.id: 159, # 73: seconds
}
class TuyaMmwRadarClusterBase(NoManufacturerCluster, TuyaMCUCluster):
"""Mmw radar cluster, base class."""
attributes = TuyaMCUCluster.attributes.copy()
attributes.update(
{
# Tuya attribute IDs
0xEF01: ("occupancy", t.uint32_t, True),
0xEF02: ("sensitivity", t.uint32_t, True),
0xEF03: ("min_range", t.uint32_t, True),
0xEF04: ("max_range", t.uint32_t, True),
0xEF06: ("self_test", TuyaMmwRadarSelfTest, True),
0xEF09: ("target_distance", t.uint32_t, True),
0xEF65: ("detection_delay", t.uint32_t, True),
0xEF66: ("fading_time", t.uint32_t, True),
0xEF67: ("cli", t.CharacterString, True),
0xEF68: ("illuminance", t.uint32_t, True),
}
)
class TuyaMmwRadarClusterV2(TuyaMmwRadarClusterBase):
"""Mmw radar cluster, variant 2 (5.8GHz)."""
dp_to_attribute: Dict[int, DPToAttributeMapping] = {
1: DPToAttributeMapping(
TuyaOccupancySensing.ep_attribute,
"occupancy",
),
2: DPToAttributeMapping(
TuyaMmwRadarSensitivity.ep_attribute,
"present_value",
endpoint_id=6,
),
3: DPToAttributeMapping(
TuyaMmwRadarMinRange.ep_attribute,
"present_value",
endpoint_id=2,
),
4: DPToAttributeMapping(
TuyaMmwRadarMaxRangeV2.ep_attribute,
"present_value",
endpoint_id=3,
),
9: DPToAttributeMapping(
TuyaMmwRadarTargetDistanceAsPressureMeasurement.ep_attribute,
"measured_value",
),
101: DPToAttributeMapping(
TuyaMmwRadarDetectionDelay.ep_attribute,
"present_value",
converter=lambda x: x * 100,
dp_converter=lambda x: x // 100,
endpoint_id=4,
),
102: DPToAttributeMapping(
TuyaMmwRadarFadingTimeV2.ep_attribute,
"present_value",
converter=lambda x: x * 100,
dp_converter=lambda x: x // 100,
endpoint_id=5,
),
104: DPToAttributeMapping(
TuyaIlluminanceMeasurement.ep_attribute,
"measured_value",
converter=lambda x: int(math.log10(x) * 10000 + 1) if x > 0 else int(0),
),
}
data_point_handlers = {
1: "_dp_2_attr_update",
2: "_dp_2_attr_update",
3: "_dp_2_attr_update",
4: "_dp_2_attr_update",
9: "_dp_2_attr_update",
101: "_dp_2_attr_update",
102: "_dp_2_attr_update",
104: "_dp_2_attr_update",
}
class TuyaMmwRadarOccupancyVariant2(CustomDevice):
"""Mini/Ceiling Human Breathe Sensor"""
def __init__(self, *args, **kwargs):
"""Init device."""
self.motion_bus = Bus()
super().__init__(*args, **kwargs)
signature = {
# endpoint=1, profile=260, device_type=81, device_version=1,
# input_clusters=[4, 5, 61184, 0], output_clusters=[25, 10]
MODELS_INFO: [
("_TZE204_qasjif9e", "TS0601"), # USB-C wall mounted 5.8GHz
("_TZE204_ztqnh5cg", "TS0601"), # untested with ZHA but uses same quirk as _TZE204_qasjif9e for zigbee2mqtt
],
ENDPOINTS: {
1: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
INPUT_CLUSTERS: [
Basic.cluster_id,
Groups.cluster_id,
Scenes.cluster_id,
TuyaNewManufCluster.cluster_id,
],
OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
},
242: {
# <SimpleDescriptor endpoint=242, profile=41440, device_type=97, device_version=0, input_clusters=[], output_clusters=[33]
# input_clusters=[]
# output_clusters=[33]
PROFILE_ID: zgp.PROFILE_ID,
DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
INPUT_CLUSTERS: [],
OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
},
},
}
replacement = {
ENDPOINTS: {
1: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR,
INPUT_CLUSTERS: [
Basic.cluster_id,
Groups.cluster_id,
Scenes.cluster_id,
TuyaMmwRadarClusterV2,
TuyaIlluminanceMeasurement,
TuyaOccupancySensing,
TuyaMmwRadarTargetDistanceAsPressureMeasurement,
],
OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
},
2: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.COMBINED_INTERFACE,
INPUT_CLUSTERS: [
TuyaMmwRadarMinRange,
],
OUTPUT_CLUSTERS: [],
},
3: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.COMBINED_INTERFACE,
INPUT_CLUSTERS: [
TuyaMmwRadarMaxRangeV2,
],
OUTPUT_CLUSTERS: [],
},
4: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.COMBINED_INTERFACE,
INPUT_CLUSTERS: [
TuyaMmwRadarDetectionDelay,
],
OUTPUT_CLUSTERS: [],
},
5: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.COMBINED_INTERFACE,
INPUT_CLUSTERS: [
TuyaMmwRadarFadingTimeV2,
],
OUTPUT_CLUSTERS: [],
},
6: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.COMBINED_INTERFACE,
INPUT_CLUSTERS: [
TuyaMmwRadarSensitivity,
],
OUTPUT_CLUSTERS: [],
},
242: {
PROFILE_ID: zgp.PROFILE_ID,
DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
INPUT_CLUSTERS: [],
OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
},
}
}
hi, ah thats great, thank you. I've just tested it and it actually works with the pressure/distance part which i just couldn't get working from hacking around with other quirks. it does seem to be all over the place though but that won't be anything to do with your code and is likely to do with the radar sensor itself. so after all of this its probably going to be a bit tricky to make any automations with the numbers it gives lol
thanks again @logan893 , you're a hero
@logan893 sorry to be a PITA, i've just got another (ceiling, 24ghz model) which has a new model number I can't find any references to. I've tried to use your above 24ghz quirk , added the model number, it picks it up, but it says illuminance and pressure are 'unknown' and motion/presence are always 'Clear' and don't seem to pick any movement up. any ideas? or do you need to have a physical device to be able to advise?
stupidly I forgot to post the model - its _TZE204_bmdsp6bs https://www.aliexpress.com/item/1005006529472748.html
thanks
edit: i've just done a ZHA Toolbox - scan device, once whilst room lit and empty and again when i'm sitting in the room and darker... hoping that the two outputs would list different values for some of the attributes, but alas the only thing differing between the two documents is the timestamps :-( i guess i'm misunderstanding what zha toolbox is doing there then... that or the motion sensor isnt working at all
ok so i managed to get the occupancy being reported by literally using the quirk from
https://raw.githubusercontent.com/zigpy/zha-device-handlers/dev/zhaquirks/tuya/ts0601_motion.py
and adding the signature to it.
Not sure how to go about getting the illuminance and the settings (max distance etc) to work at all. any ideas?
ugh, ok so i REALLY need to be able to configure the settings on this since it seems to "clear" after no movement within like 10 seconds. it really shouldnt do that should it? i thought the whole point in radar was that it detected presence despite no movement!! :-(
@thefunkygibbon I believe you'll need to either be very good or lucky at guessing, or have a Tuya gateway, to figure out how these new devices work. I don't have a gateway myself, so most of the quirks I posted here are based on information others have already discovered, and all things considered just a tiny bit of guesswork and trial and error.
Zigbee2MQTT has a tutorial for how to extract the device data points from logs using a Tuya gateway and the Tuya app.
https://www.zigbee2mqtt.io/advanced/support-new-devices/03_find_tuya_data_points.html
don't suppose anyone has managed to get a decent quirk working for these devices have they? I really need to get these working else i'll have to find some other ceiling mounted, water resistant radar devices (which aren't tuya) ... and to be honest, there isn't a lot out there
Problem description
Pairing with Home Assistant but no sensors or controls available. Advertised as Presence sensor for use with Tuya zigbee hub/gateway.
Model: ZY-M100-1 (Side wall version); there is also the ZY-M100-2 Ceiling version, which I do not have myself.
Shows up in ZHA as TS0601 by _TZE204_ijxvkhd0
This is a mains powered 24 GHz presence sensor, zigbee and side wall edition.
https://www.aliexpress.com/item/1005006128737558.html
Solution description
Presence Sensor functionality
Screenshots/Video
Screenshots/Video
[Paste/upload your media here]Device signature
Device signature
```json [Paste the device signature here] ```Diagnostic information
Diagnostic information
```json [Paste the diagnostic information here] ```Logs
Logs
```python [Paste the logs here] ```Custom quirk
Custom quirk
```python ## originally based on ### https://fixtse.com/blog/zy-m100-full-zha-support ### ( https://gist.githubusercontent.com/fixtse/b95753b84c34b45f49b3116d23b66342/raw/0f84bd6e9b6c174970c7b5fc21078d4b4da06a15/TZE204_ijxvkhd0_e5m9c5hl.py ) ## with inspiration from ### https://github.com/zigpy/zha-device-handlers/pull/2525#issuecomment-1826881992 ## and my own changes and additions import math from typing import Dict 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 ( AnalogInput, AnalogOutput, Basic, GreenPowerProxy, Groups, Ota, Scenes, Time, ) from zigpy.zcl.clusters.measurement import ( IlluminanceMeasurement, OccupancySensing, PressureMeasurement, ) from zigpy.zcl.clusters.security import IasZone, ZoneType from zhaquirks import Bus, LocalDataCluster from zhaquirks.const import ( DEVICE_TYPE, ENDPOINTS, INPUT_CLUSTERS, MODELS_INFO, OUTPUT_CLUSTERS, PROFILE_ID, CLUSTER_COMMAND, ZONE_STATUS_CHANGE_COMMAND, ON, OFF, ) from zhaquirks.tuya import ( NoManufacturerCluster, TuyaLocalCluster, TuyaNewManufCluster, ) from zhaquirks.tuya.mcu import ( DPToAttributeMapping, TuyaAttributesCluster, TuyaMCUCluster, ) class TuyaMmwRadarSelfTest(t.enum8): """Mmw radar self test values.""" TESTING = 0 TEST_SUCCESS = 1 TEST_FAILURE = 2 OTHER = 3 COMM_FAULT = 4 RADAR_FAULT = 5 class PresenceMotionEnum(t.enum8): """Presence and motion enum.""" NONE = 0x00 PRESENCE = 0x01 MOTION = 0x02 class TuyaMmwRadarTargetDistanceAsPressureMeasurement(PressureMeasurement, TuyaLocalCluster): # result in centimeteres expressed as hPa """Target Distance.""" class TuyaMmwRadarMotionSensitivity(TuyaAttributesCluster, AnalogOutput): """AnalogOutput cluster for motion sensitivity.""" _CONSTANT_ATTRIBUTES = { AnalogOutput.AttributeDefs.description.id: "motion sensitivity", AnalogOutput.AttributeDefs.min_present_value.id: 1, AnalogOutput.AttributeDefs.max_present_value.id: 9, AnalogOutput.AttributeDefs.resolution.id: 1, } class TuyaMmwRadarPresenceSensitivity(TuyaAttributesCluster, AnalogOutput): """AnalogOutput cluster for presence sensitivity.""" _CONSTANT_ATTRIBUTES = { AnalogOutput.AttributeDefs.description.id: "presence sensitivity", AnalogOutput.AttributeDefs.min_present_value.id: 1, AnalogOutput.AttributeDefs.max_present_value.id: 9, AnalogOutput.AttributeDefs.resolution.id: 1, } class TuyaMmwRadarFadingTime(TuyaAttributesCluster, AnalogOutput): """AnalogOutput cluster for fading time.""" _CONSTANT_ATTRIBUTES = { AnalogOutput.AttributeDefs.description.id: "fading time", AnalogOutput.AttributeDefs.min_present_value.id: 1, AnalogOutput.AttributeDefs.max_present_value.id: 1000, AnalogOutput.AttributeDefs.resolution.id: 1, # Resolution 1 second AnalogOutput.AttributeDefs.engineering_units.id: 73, # 73 defines seconds, the expected unit } class TuyaMmwRadarMaxRange(TuyaAttributesCluster, AnalogOutput): """AnalogOutput cluster for max range.""" _CONSTANT_ATTRIBUTES = { AnalogOutput.AttributeDefs.description.id: "max detection range", AnalogOutput.AttributeDefs.min_present_value.id: 150, # min allowed = 150 AnalogOutput.AttributeDefs.max_present_value.id: 550, # max allowed = 550 AnalogOutput.AttributeDefs.resolution.id: 100, #resolution = 100 centermeters (snaps back to 100 cm intervals between 150 and 550) AnalogOutput.AttributeDefs.engineering_units.id: 118, # 118 defines centimeters, the expected unit } class TuyaOccupancySensing(OccupancySensing, TuyaLocalCluster): """Tuya local OccupancySensing cluster.""" class TuyaIlluminanceMeasurement(IlluminanceMeasurement, TuyaLocalCluster): """Tuya local IlluminanceMeasurement cluster.""" class TuyaOccupancyMotionSensing(OccupancySensing, TuyaLocalCluster): """Tuya local OccupancySensing cluster for motion state.""" class TuyaMmwRadarMotionAnalogInputCluster(TuyaLocalCluster, AnalogInput): """Analog input cluster, only used to relay motion state information to Iaszone motion sensor.""" cluster_id = AnalogInput.cluster_id def __init__(self, *args, **kwargs): """Init.""" super().__init__(*args, **kwargs) def _update_attribute(self, attrid, value): super()._update_attribute(attrid, value) if attrid == AnalogInput.AttributeDefs.present_value.id: # 0x55 = "present_value" for AnalogInput if value == PresenceMotionEnum.MOTION: self.endpoint.device.motion_bus.listener_event("_turn_on") else: self.endpoint.device.motion_bus.listener_event("_turn_off") class TuyaMmwRadarMotionSensing(LocalDataCluster, IasZone): """IasZone cluster for motion.""" _CONSTANT_ATTRIBUTES = { IasZone.AttributeDefs.zone_type.id: ZoneType.Motion_Sensor, # 0x000D, # motion type } cluster_id = IasZone.cluster_id def __init__(self, *args, **kwargs): """Init.""" super().__init__(*args, **kwargs) self.endpoint.device.motion_bus.add_listener(self) def _turn_off(self): self.listener_event( CLUSTER_COMMAND, 253, ZONE_STATUS_CHANGE_COMMAND, [OFF, 0, 0, 0] ) def _turn_on(self): self.listener_event( CLUSTER_COMMAND, 254, ZONE_STATUS_CHANGE_COMMAND, [ON, 0, 0, 0] ) class TuyaMmwRadarCluster(NoManufacturerCluster, TuyaMCUCluster): """Mmw radar cluster.""" attributes = TuyaMCUCluster.attributes.copy() dp_to_attribute: Dict[int, DPToAttributeMapping] = { 103: DPToAttributeMapping( TuyaMCUCluster.ep_attribute, "cli", ), 104: DPToAttributeMapping( TuyaIlluminanceMeasurement.ep_attribute, "measured_value", converter=lambda x: int(math.log10(x) * 10000 + 1) if x > 0 else int(0), ), 105: DPToAttributeMapping( TuyaMmwRadarMotionAnalogInputCluster.ep_attribute, "present_value", converter=lambda x: PresenceMotionEnum(x), endpoint_id=2, ), 106: DPToAttributeMapping( TuyaMmwRadarMotionSensitivity.ep_attribute, "present_value", endpoint_id=6, converter=lambda x: x if x < 10 else x / 10, ), 107: DPToAttributeMapping( TuyaMmwRadarMaxRange.ep_attribute, "present_value", endpoint_id=3, ), 109: DPToAttributeMapping( TuyaMmwRadarTargetDistanceAsPressureMeasurement.ep_attribute, "measured_value", ), 110: DPToAttributeMapping( TuyaMmwRadarFadingTime.ep_attribute, "present_value", endpoint_id=5, ), 111: DPToAttributeMapping( TuyaMmwRadarPresenceSensitivity.ep_attribute, "present_value", endpoint_id=7, converter=lambda x: x if x < 10 else x / 10, ), 112: DPToAttributeMapping( TuyaOccupancySensing.ep_attribute, "occupancy", ), } data_point_handlers = { 103: "_dp_2_attr_update", 104: "_dp_2_attr_update", 105: "_dp_2_attr_update", 106: "_dp_2_attr_update", 107: "_dp_2_attr_update", 109: "_dp_2_attr_update", 110: "_dp_2_attr_update", 111: "_dp_2_attr_update", 112: "_dp_2_attr_update", } class TuyaMmwRadarOccupancy(CustomDevice): """Millimeter wave occupancy sensor.""" def __init__(self, *args, **kwargs): """Init device.""" self.motion_bus = Bus() super().__init__(*args, **kwargs) signature = { # endpoint=1, profile=260, device_type=81, device_version=1, # input_clusters=[0, 4, 5, 61184], output_clusters=[25, 10] MODELS_INFO: [ ("_TZE204_ijxvkhd0", "TS0601"), ("_TZE204_e5m9c5hl", "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, TuyaNewManufCluster.cluster_id, ], OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], }, 242: { #Additional information
No response