Created
January 28, 2024 08:01
-
-
Save darkliquid/5f080bc5322a1483dfcd4788b225f7a6 to your computer and use it in GitHub Desktop.
zha quirk for tuya 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
## 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], | |
}, | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment