-
-
Save rabin-io/a279f93903a3574aedc0c4d7f860febc to your computer and use it in GitHub Desktop.
ZHA support for MoesHouse (Tuya) Zigbee Presence Sensor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""Tuya mmw radar occupancy sensor.""" | |
import math | |
from typing import Dict, Optional, Tuple, Union | |
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, | |
Identify, | |
Ota, | |
Scenes, | |
Time, | |
) | |
from zigpy.zcl.clusters.measurement import ( | |
IlluminanceMeasurement, | |
OccupancySensing | |
) | |
from zigpy.zcl.clusters.security import IasZone | |
from zhaquirks import Bus, LocalDataCluster, MotionOnEvent | |
from zhaquirks.const import ( | |
DEVICE_TYPE, | |
ENDPOINTS, | |
INPUT_CLUSTERS, | |
MODEL, | |
MOTION_EVENT, | |
OUTPUT_CLUSTERS, | |
PROFILE_ID, | |
) | |
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 TuyaOccupancySensing(OccupancySensing, TuyaLocalCluster): | |
"""Tuya local OccupancySensing cluster.""" | |
class TuyaIlluminanceMeasurement(IlluminanceMeasurement, TuyaLocalCluster): | |
"""Tuya local IlluminanceMeasurement cluster.""" | |
class TuyaMmwRadarSensitivity(TuyaAttributesCluster, AnalogOutput): | |
"""AnalogOutput cluster for sensitivity.""" | |
def __init__(self, *args, **kwargs): | |
"""Init.""" | |
super().__init__(*args, **kwargs) | |
self._update_attribute( | |
self.attributes_by_name["description"].id, "Sensitivity" | |
) | |
self._update_attribute(self.attributes_by_name["min_present_value"].id, 1) | |
self._update_attribute(self.attributes_by_name["max_present_value"].id, 9) | |
self._update_attribute(self.attributes_by_name["resolution"].id, 1) | |
class TuyaMmwRadarMinRange(TuyaAttributesCluster, AnalogOutput): | |
"""AnalogOutput cluster for min range.""" | |
def __init__(self, *args, **kwargs): | |
"""Init.""" | |
super().__init__(*args, **kwargs) | |
self._update_attribute( | |
self.attributes_by_name["description"].id, "Min range" | |
) | |
self._update_attribute(self.attributes_by_name["min_present_value"].id, 0) | |
self._update_attribute(self.attributes_by_name["max_present_value"].id, 950) | |
self._update_attribute(self.attributes_by_name["resolution"].id, 10) | |
self._update_attribute( | |
self.attributes_by_name["engineering_units"].id, 118 | |
) # 31: meters | |
class TuyaMmwRadarMaxRange(TuyaAttributesCluster, AnalogOutput): | |
"""AnalogOutput cluster for max range.""" | |
def __init__(self, *args, **kwargs): | |
"""Init.""" | |
super().__init__(*args, **kwargs) | |
self._update_attribute( | |
self.attributes_by_name["description"].id, "Max range" | |
) | |
self._update_attribute(self.attributes_by_name["min_present_value"].id, 0) | |
self._update_attribute(self.attributes_by_name["max_present_value"].id, 950) | |
self._update_attribute(self.attributes_by_name["resolution"].id, 10) | |
self._update_attribute( | |
self.attributes_by_name["engineering_units"].id, 118 | |
) # 31: meters | |
class TuyaMmwRadarDetectionDelay(TuyaAttributesCluster, AnalogOutput): | |
"""AnalogOutput cluster for detection delay.""" | |
def __init__(self, *args, **kwargs): | |
"""Init.""" | |
super().__init__(*args, **kwargs) | |
self._update_attribute( | |
self.attributes_by_name["description"].id, "Detection delay" | |
) | |
self._update_attribute(self.attributes_by_name["min_present_value"].id, 100) | |
self._update_attribute(self.attributes_by_name["max_present_value"].id, 20000) | |
self._update_attribute(self.attributes_by_name["resolution"].id, 100) | |
self._update_attribute( | |
self.attributes_by_name["engineering_units"].id, 159 | |
) # 73: seconds | |
class TuyaMmwRadarFadingTime(TuyaAttributesCluster, AnalogOutput): | |
"""AnalogOutput cluster for fading time.""" | |
def __init__(self, *args, **kwargs): | |
"""Init.""" | |
super().__init__(*args, **kwargs) | |
self._update_attribute( | |
self.attributes_by_name["description"].id, "Fading time" | |
) | |
self._update_attribute(self.attributes_by_name["min_present_value"].id, 1000) | |
self._update_attribute(self.attributes_by_name["max_present_value"].id, 200000) | |
self._update_attribute(self.attributes_by_name["resolution"].id, 1000) | |
self._update_attribute( | |
self.attributes_by_name["engineering_units"].id, 159 | |
) # 73: seconds | |
class TuyaMmwRadarTargetDistance(TuyaAttributesCluster, AnalogInput): | |
"""AnalogInput cluster for target distance.""" | |
def __init__(self, *args, **kwargs): | |
"""Init.""" | |
super().__init__(*args, **kwargs) | |
self._update_attribute( | |
self.attributes_by_name["description"].id, "Target distance" | |
) | |
self._update_attribute( | |
self.attributes_by_name["engineering_units"].id, 31 | |
) # 31: meters | |
class TuyaMmwRadarCluster(NoManufacturerCluster, TuyaMCUCluster): | |
"""Mmw radar cluster.""" | |
attributes = TuyaMCUCluster.attributes.copy() | |
attributes.update( | |
{ | |
# ramdom 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), | |
} | |
) | |
dp_to_attribute: Dict[int, DPToAttributeMapping] = { | |
1: DPToAttributeMapping( | |
TuyaOccupancySensing.ep_attribute, | |
"occupancy", | |
), | |
2: DPToAttributeMapping( | |
TuyaMmwRadarSensitivity.ep_attribute, | |
"present_value", | |
), | |
3: DPToAttributeMapping( | |
TuyaMmwRadarMinRange.ep_attribute, | |
"present_value", | |
endpoint_id=2, | |
# converter=lambda x: x / 100, | |
# dp_converter=lambda x: x * 100, | |
), | |
4: DPToAttributeMapping( | |
TuyaMmwRadarMaxRange.ep_attribute, | |
"present_value", | |
endpoint_id=3, | |
# converter=lambda x: x / 100, | |
# dp_converter=lambda x: x * 100, | |
), | |
6: DPToAttributeMapping( | |
TuyaMCUCluster.ep_attribute, | |
"self_test", | |
), | |
9: DPToAttributeMapping( | |
TuyaMmwRadarTargetDistance.ep_attribute, | |
"present_value", | |
# converter=lambda x: x / 100, | |
), | |
101: DPToAttributeMapping( | |
TuyaMmwRadarDetectionDelay.ep_attribute, | |
"present_value", | |
converter=lambda x: x * 100, | |
dp_converter=lambda x: x // 100, | |
endpoint_id=4, | |
), | |
102: DPToAttributeMapping( | |
TuyaMmwRadarFadingTime.ep_attribute, | |
"present_value", | |
converter=lambda x: x * 100, | |
dp_converter=lambda x: x // 100, | |
endpoint_id=5, | |
), | |
103: DPToAttributeMapping( | |
TuyaMCUCluster.ep_attribute, | |
"cli", | |
), | |
104: DPToAttributeMapping( | |
TuyaIlluminanceMeasurement.ep_attribute, | |
"measured_value", | |
converter=lambda x: 10000 * math.log10(x) + 1 if x != 0 else 0, | |
), | |
} | |
data_point_handlers = { | |
1: "_dp_2_attr_update", | |
2: "_dp_2_attr_update", | |
3: "_dp_2_attr_update", | |
4: "_dp_2_attr_update", | |
6: "_dp_2_attr_update", | |
9: "_dp_2_attr_update", | |
101: "_dp_2_attr_update", | |
102: "_dp_2_attr_update", | |
103: "_dp_2_attr_update", | |
104: "_dp_2_attr_update", | |
} | |
class TuyaMmwRadarOccupancy(CustomDevice): | |
"""Millimeter wave occupancy sensor.""" | |
signature = { | |
# endpoint=1, profile=260, device_type=81, device_version=1, | |
# input_clusters=[0, 4, 5, 61184], output_clusters=[25, 10] | |
"models_info": [ | |
("_TZE200_ar0slwnd", "TS0601"), | |
("_TZE200_sfiy5tfs", "TS0601"), | |
("_TZE200_mrf6vtua", "TS0601"), | |
("_TZE200_ztc6ggyl", "TS0601"), | |
("_TZE204_ztc6ggyl", "TS0601"), | |
("_TZE200_ikvncluo", "TS0601"), | |
("_TZE204_qasjif9e", "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], | |
}, | |
}, | |
} | |
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, | |
TuyaMmwRadarTargetDistance, | |
TuyaMmwRadarSensitivity, | |
], | |
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: [ | |
TuyaMmwRadarMaxRange, | |
], | |
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: [ | |
TuyaMmwRadarFadingTime, | |
], | |
OUTPUT_CLUSTERS: [], | |
}, | |
} | |
} |
@NotaPress did you ever get this working? I have the same device model.
It works, in terms of usage on line 266 you add the seller at a higher level ("_TZE204_qasjif9e", "TS0601")
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
2023.11.26
Device: TS0601 / _TZE204_qasjif9e
System:
"home_assistant": {
"installation_type": "Home Assistant OS",
"version": "2023.11.3",
"dev": false,
"hassio": true,
"virtualenv": false,
"python_version": "3.11.6",
"docker": true,
"arch": "x86_64",
"timezone": "Europe/Warsaw",
"os_name": "Linux",
"os_version": "6.1.59",
"supervisor": "2023.11.3",
"host_os": "Home Assistant OS 11.1",
"docker_version": "24.0.6",
"chassis": "vm",
"run_as_root": true
}
This script doesn't work.
The device is visible in the HA, but no sensors are visible.