Skip to content

Instantly share code, notes, and snippets.

@darkliquid
Created January 28, 2024 08:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save darkliquid/5f080bc5322a1483dfcd4788b225f7a6 to your computer and use it in GitHub Desktop.
Save darkliquid/5f080bc5322a1483dfcd4788b225f7a6 to your computer and use it in GitHub Desktop.
zha quirk for tuya presence sensor
## 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