In a previous post, I recommended a Zigbee motion sensor, which utilizes infrared technology (PIR). This time, I want to introduce you to a human presence sensor based on millimeter wave (mmWave) technology.
Understanding PIR and mmWave Sensors
PIR Sensors
Passive Infrared (PIR) sensors are excellent for detecting motion. They are ideal for triggering actions, such as turning on a light when you enter a room. However, they have limitations. PIR sensors can miss detecting someone who is sitting still or sleeping, as they rely on movement.
mmWave Sensors
Millimeter wave (mmWave) sensors, on the other hand, are capable of detecting human presence even if the person is not moving at all. This makes them perfect for applications where you need to know if a room is occupied, such as turning off the air conditioner when no one is present. Unlike PIR sensors, mmWave sensors do not suffer from false detections caused by stillness.
Recommended mmWave Sensor
The model I recommend is in the link below, which is recognized in Home Assistant as TS0601 _TZE204_ijxvkhd0, _TZE204_qasjif9e, or _TZE204_dapwryy7. This sensor works flawlessly with Zigbee Home Automation (ZHA) but requires a specific quirk to function correctly.
Setting Up the mmWave Sensor with ZHA
To integrate the mmWave sensor with Home Assistant using ZHA, you need to add a quirk.
Zigbee ZHA (Zigbee Home Automation) quirks are custom configurations and adaptations used to ensure that Zigbee devices work correctly with the ZHA integration in Home Assistant. Many Zigbee devices, especially those from different manufacturers, may have unique features or report data in slightly different formats. Quirks are essentially small pieces of code that “translate” the device’s communication into a standard format that ZHA can understand, enabling seamless integration and functionality within Home Assistant. By using quirks, users can ensure that their diverse range of Zigbee devices work harmoniously together, regardless of the manufacturer or model.
To install quirks, follow my guide for the irrigation valve.
Here’s the required code:
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,
MODELS_INFO,
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, 10)
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, 000)
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, 2000)
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, 118
) # 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),
0xEF09: ("target_distance", t.uint32_t, True),
0xEF65: ("detection_delay", t.uint32_t, True),
0xEF66: ("fading_time", t.uint32_t, 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,
),
4: DPToAttributeMapping(
TuyaMmwRadarMaxRange.ep_attribute,
"present_value",
endpoint_id=3,
),
9: DPToAttributeMapping(
TuyaMmwRadarTargetDistance.ep_attribute,
"present_value",
),
101: DPToAttributeMapping(
TuyaMmwRadarDetectionDelay.ep_attribute,
"present_value",
converter=lambda x: x * 100,
dp_converter=lambda x: round(x / 100),
endpoint_id=4,
),
102: DPToAttributeMapping(
TuyaMmwRadarFadingTime.ep_attribute,
"present_value",
converter=lambda x: x * 1000,
dp_converter=lambda x: round(x / 1000),
endpoint_id=5,
),
360: DPToAttributeMapping(
TuyaIlluminanceMeasurement.ep_attribute,
"measured_value",
converter=lambda x: x * 10,
),
}
class TuyaRadarFillerCluster(LocalDataCluster, MotionOnEvent):
"""Filler cluster for motion event relay."""
cluster_id = TuyaMmwRadarCluster.cluster_id
def __init__(self, *args, **kwargs):
"""Init."""
super().__init__(*args, **kwargs)
self.endpoint.device.motion_bus.add_listener(self)
def attribute_updated(self, attrid, value):
"""Attribute updated."""
if attrid == 0xEF01:
self.endpoint.device.motion_bus.listener_event(MOTION_EVENT)
class TuyaRadar(CustomDevice):
"""Tuya radar device."""
def __init__(self, *args, **kwargs):
"""Init device."""
self.motion_bus = Bus()
super().__init__(*args, **kwargs)
signature = {
MODELS_INFO: [
("_TZE204_dapwryy7", "TS0601"),
("_TZE204_qasjif9e", "TS0601"),
("_TZE204_ijxvkhd0", "TS0601"),
],
ENDPOINTS: {
1: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR,
INPUT_CLUSTERS: [
Basic.cluster_id,
Groups.cluster_id,
Identify.cluster_id,
Scenes.cluster_id,
TuyaMmwRadarCluster.cluster_id,
TuyaOccupancySensing.cluster_id,
TuyaIlluminanceMeasurement.cluster_id,
TuyaMmwRadarSensitivity.cluster_id,
TuyaMmwRadarTargetDistance.cluster_id,
],
OUTPUT_CLUSTERS: [
Ota.cluster_id,
Time.cluster_id,
TuyaRadarFillerCluster.cluster_id,
],
},
2: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.ANALOG_OUTPUT,
INPUT_CLUSTERS: [
TuyaMmwRadarMinRange.cluster_id,
],
OUTPUT_CLUSTERS: [],
},
3: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.ANALOG_OUTPUT,
INPUT_CLUSTERS: [
TuyaMmwRadarMaxRange.cluster_id,
],
OUTPUT_CLUSTERS: [],
},
4: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.ANALOG_OUTPUT,
INPUT_CLUSTERS: [
TuyaMmwRadarDetectionDelay.cluster_id,
],
OUTPUT_CLUSTERS: [],
},
5: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.ANALOG_OUTPUT,
INPUT_CLUSTERS: [
TuyaMmwRadarFadingTime.cluster_id,
],
OUTPUT_CLUSTERS: [],
},
},
}
replacement = {
ENDPOINTS: {
1: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR,
INPUT_CLUSTERS: [
Basic.cluster_id,
Groups.cluster_id,
Identify.cluster_id,
Scenes.cluster_id,
TuyaOccupancySensing,
TuyaIlluminanceMeasurement,
TuyaMmwRadarSensitivity,
TuyaMmwRadarTargetDistance,
],
OUTPUT_CLUSTERS: [
Ota.cluster_id,
Time.cluster_id,
TuyaRadarFillerCluster,
],
},
2: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.ANALOG_OUTPUT,
INPUT_CLUSTERS: [
TuyaMmwRadarMinRange,
],
OUTPUT_CLUSTERS: [],
},
3: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.ANALOG_OUTPUT,
INPUT_CLUSTERS: [
TuyaMmwRadarMaxRange,
],
OUTPUT_CLUSTERS: [],
},
4: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.ANALOG_OUTPUT,
INPUT_CLUSTERS: [
TuyaMmwRadarDetectionDelay,
],
OUTPUT_CLUSTERS: [],
},
5: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.ANALOG_OUTPUT,
INPUT_CLUSTERS: [
TuyaMmwRadarFadingTime,
],
OUTPUT_CLUSTERS: [],
},
},
}
Customization Options
This sensor provides customization options such as sensitivity, detection delay, and maximum detection distance, allowing you to tailor it to your specific needs.
Conclusion
The mmWave sensor brings significant advancements in smart home automation, offering reliable presence detection even when motion is not present. This makes it a perfect addition to your smart home ecosystem.
Stay tuned for more insights and updates on smart home technologies!
source: https://github.com/zigpy/zha-device-handlers/issues/2551#issuecomment-1795920085