Skip to content

[Device Support Request] ZY-M100 Series Presence Sensor (TS0601 by _TZE204_ijxvkhd0) #2852

Description

@logan893

Problem description

Pairing with Home Assistant but no sensors or controls available. Advertised as Presence sensor for use with Tuya zigbee hub/gateway.

Model: ZY-M100-1 (Side wall version); there is also the ZY-M100-2 Ceiling version, which I do not have myself.

Shows up in ZHA as TS0601 by _TZE204_ijxvkhd0

This is a mains powered 24 GHz presence sensor, zigbee and side wall edition.

https://www.aliexpress.com/item/1005006128737558.html

Solution description

Presence Sensor functionality

Screenshots/Video

Screenshots/Video

[Paste/upload your media here]

Device signature

Device signature
[Paste the device signature here]

Diagnostic information

Diagnostic information
[Paste the diagnostic information here]

Logs

Logs
[Paste the logs here]

Custom quirk

Custom quirk
## 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],
            },
        }
    }

Additional information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    TuyaRequest/PR regarding a Tuya devicedevice support requestThis requests support for a new devicestaleIssue is inactivate and might get closed soon

    Fields

    No fields configured for Feature.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions