A person standing in a living room with presence sensors detecting his presence

Zigbee Human Presence Detector: A Game Changer for Smart Home Automation

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.

Zigbee Human Presence Detector

Aliexpress

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


Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply