How I got it working - WZ-M100 Tuya Presence Sensor (TS0601 _TZE204_7gclukjs)

TLDR - I bought a sensor, realised it didn’t work and spent a small fraction of my life getting it to work. I publish a short ‘how-to’ to outline the steps to get the sensor to work. I ask for some further support to get the sensor ‘out of the box’ ready. The code is now more appropriate for the device and has more functionality.

Edit 1 - 27/08/2024 - Updated code block with fix for user input values resetting to default. Intended behaviour would be user input values remain until changed by user.
Edit 2 - 30/08/2024 - Updated code block to actually fix user input values resetting to default. Intended behaviour would be user input values remain until changed by user. Illuminance sensor now working.

After using a couple of the Sonoff SNZB-03’s for a short while I decided it was time for an upgrade to mmWave. I initially got the ZY-M100 wall mount version which integrated seamlessly into HA through ZHA - a great sensor.

Shortly after, I purchased the ceiling mount version (WZ-M100), punched a hole into the ceiling and wired it into the 230V mains. Only then did I decide to begin the setup process - this was quite the mistake!

The ceiling version, or at least the particular variant I had purchased, does not integrate as seamlessly as the wall mount version. In fact it does not function at all out of the box. I should note other people have had better luck using zigbee2mqtt and/or different variants of the same sensor.

After having a slight panic that I had punched a hole in the ceiling for a dud-sensor, I began trawling the forums looking for people in a similar situation - of which there are plenty. A couple hours later and I had managed to get the sensor working perfectly (minus the lux sensor which I am yet to bother with). The particular variant I had purchased isn’t as widespread as some others so I think it best to summarise my experience below. With the exception of the code, you may choose to follow the extremely helpful guide linked here.

The following guide is a collection of steps from various sources, I take no credit for any of this. This guide applies to the ZHA implementation of Zigbee and I use HA OS based on an SD card on a Raspberry Pi 4. I assume that you already have Studio Code Server (or equivalent) installed on your HA instance, and you can follow instructions (you do not need any python knowledge).

If I can do this, as a complete python novice, so can you!

Step 1: Attempt to setup the sensor BEFORE considering using a drill or any other hole making device. This is very important.
Step 2: Realise your device is not out of the box ready and have a panic proportional to the situation.
Step 3: Using Studio Code Server (or equivalent) create a folder within the config folder named ‘custom_zha_quirks’
Step 4: Within the newly created folder create a file named ‘ts0601_radar.py’. I do not know if the name of this file is particularly important, but I do know that the file extension is paramount.
Step 5: Copy the below code into the newly created file and save it. Ensure indentation is identical.
[Last Updated 30/08/2024]

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 (
    # TuyaDPType,
    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, 75)
        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, 75)
        self._update_attribute(self.attributes_by_name["max_present_value"].id, 900)
        self._update_attribute(self.attributes_by_name["resolution"].id, 75)
        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, 1000)
        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),
            0xEF06: ("self_test", TuyaMmwRadarSelfTest, True),
            0xEF09: ("target_distance", t.uint32_t, True),
            0xEF65: ("detection_delay", t.uint32_t, True),
            0xEF66: ("fading_time", t.uint32_t, True),
            0xEF67: ("cli", t.CharacterString, 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,
            # converter=lambda x: x / 100,
            # dp_converter=lambda x: x * 100,
        ),
        4: DPToAttributeMapping(
            TuyaMmwRadarMaxRange.ep_attribute,
            "present_value",
            endpoint_id=3,
            # converter=lambda x: x / 100,
            # dp_converter=lambda x: x * 100,
        ),
        6: DPToAttributeMapping(
            TuyaMCUCluster.ep_attribute,
            "self_test",
        ),
        9: DPToAttributeMapping(
            TuyaMmwRadarTargetDistance.ep_attribute,
            "present_value",
            # converter=lambda x: x / 100,
        ),
        101: DPToAttributeMapping(
            TuyaMmwRadarDetectionDelay.ep_attribute,
            "present_value",
            converter=lambda x: x * 100,
            dp_converter=lambda x: x // 100,
            endpoint_id=4,
        ),
        103: DPToAttributeMapping(
        TuyaIlluminanceMeasurement.ep_attribute,
        "measured_value",
        converter=lambda x: 10000 * math.log10(x) + 1 if x != 0 else 0,
        ),
        #104: DPToAttributeMapping( Unknown what this attribute is at this time
            #TuyaMCUCluster.ep_attribute,
            #"cli",
        #),
        105: DPToAttributeMapping(
            TuyaMmwRadarFadingTime.ep_attribute,
            "present_value",
            converter=lambda x: x * 100,
            dp_converter=lambda x: x // 100,
            endpoint_id=5,
        ),
    }

    data_point_handlers = {
        1: "_dp_2_attr_update",
        2: "_dp_2_attr_update",
        3: "_dp_2_attr_update",
        4: "_dp_2_attr_update",
        6: "_dp_2_attr_update",
        9: "_dp_2_attr_update",
        101: "_dp_2_attr_update",
        103: "_dp_2_attr_update",
        # 104: "_dp_2_attr_update",
        105: "_dp_2_attr_update",
    }

class TuyaMmwRadarOccupancy(CustomDevice):
    """Millimeter wave occupancy sensor."""
    signature = {
        #  endpoint=1, profile=260, device_type=81, device_version=1,
        #  input_clusters=[4, 5, 61184, 0], output_clusters=[25, 10])
        MODELS_INFO: [
            ("_TZE204_7gclukjs", "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,
                    TuyaMmwRadarTargetDistance,
                    TuyaMmwRadarSensitivity,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            2: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.COMBINED_INTERFACE,
                INPUT_CLUSTERS: [
                    TuyaMmwRadarMinRange,
                ],
                OUTPUT_CLUSTERS: [],
            },
            3: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.COMBINED_INTERFACE,
                INPUT_CLUSTERS: [
                    TuyaMmwRadarMaxRange,
                ],
                OUTPUT_CLUSTERS: [],
            },
            4: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.COMBINED_INTERFACE,
                INPUT_CLUSTERS: [
                    TuyaMmwRadarDetectionDelay,
                ],
                OUTPUT_CLUSTERS: [],
            },
            5: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.COMBINED_INTERFACE,
                INPUT_CLUSTERS: [
                    TuyaMmwRadarFadingTime,
                ],
                OUTPUT_CLUSTERS: [],
            },
            242: {
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        }
    }

Step 6: Add the following code to your configuration.yaml entry, again ensuring correct indentation.

zha:
  custom_quirks_path: /config/custom_zha_quirks

Step 7: Restart Home Assistant.
Step 8: Sigh relief that the sensor now works!
Step 9: Realise you now need to actually install the sensor in a suitable location and do that. Exercise caution!

The resources I used to get the sensor working were;
Fixt’s guide on the ZY-M100 sensor. A big thank you to fixt!
Github resource 3112
Github resource 21738 (attribute mappings)
The code was published by KLM30330 over on Github.

It would be beneficial if this code (with some alterations) could be included with ZHA as standard so the ‘kjs’ variant works out of the box for future buyers. How can I go about doing this?
The lux sensor outputs either 0 or 1, therefore the sensor is not fully operational as someone might expect it to be - could someone with a smidge more python experience help me out with getting this to output something useful?

Original code for reference only (Pre-27/08/2024)
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 (
    # TuyaDPType,
    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: x // 100,
            endpoint_id=4,
        ),
        102: DPToAttributeMapping(
            TuyaMmwRadarFadingTime.ep_attribute,
            "present_value",
            converter=lambda x: x * 100,
            dp_converter=lambda x: x // 100,
            endpoint_id=5,
        ),
        104: DPToAttributeMapping(
            TuyaIlluminanceMeasurement.ep_attribute,
            "measured_value",
            converter=lambda x: int(math.log10(x) * 10000 + 1) if x > 0 else int(0),
        ),
    }

    data_point_handlers = {
        1: "_dp_2_attr_update",
        2: "_dp_2_attr_update",
        3: "_dp_2_attr_update",
        4: "_dp_2_attr_update",
        9: "_dp_2_attr_update",
        101: "_dp_2_attr_update",
        102: "_dp_2_attr_update",
        104: "_dp_2_attr_update",
    }

class TuyaMmwRadarOccupancy(CustomDevice):
    """Millimeter wave occupancy sensor."""
    signature = {
        #  endpoint=1, profile=260, device_type=81, device_version=1,
        #  input_clusters=[4, 5, 61184, 0], output_clusters=[25, 10])
        MODELS_INFO: [
            ("_TZE204_7gclukjs", "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,
                    TuyaMmwRadarTargetDistance,
                    TuyaMmwRadarSensitivity,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            2: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.COMBINED_INTERFACE,
                INPUT_CLUSTERS: [
                    TuyaMmwRadarMinRange,
                ],
                OUTPUT_CLUSTERS: [],
            },
            3: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.COMBINED_INTERFACE,
                INPUT_CLUSTERS: [
                    TuyaMmwRadarMaxRange,
                ],
                OUTPUT_CLUSTERS: [],
            },
            4: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.COMBINED_INTERFACE,
                INPUT_CLUSTERS: [
                    TuyaMmwRadarDetectionDelay,
                ],
                OUTPUT_CLUSTERS: [],
            },
            5: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.COMBINED_INTERFACE,
                INPUT_CLUSTERS: [
                    TuyaMmwRadarFadingTime,
                ],
                OUTPUT_CLUSTERS: [],
            },
            242: {
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        }
    }

6 Likes

That is fantastic… thank you for this. I will give it a go this week.

So, I was playing with this today for a bit and the only data coming from the sensor is a 1 or 0. I started by replacing this Class just to see if a value could be written there:

class TuyaIlluminanceMeasurement(IlluminanceMeasurement, TuyaLocalCluster):
    """Tuya local IlluminanceMeasurement cluster."""

    def _update_attribute(self, attrid, value):
        """Update attribute with conversion if necessary."""
        if attrid == self.attributes_by_name["measured_value"].id:
            # Log the raw value received
            logger.debug(f"Raw illuminance value: {value}")
            
            # Test with a manual value
            value = max(1, value)
            if value == 1:
                value = 1000  # Set a manual test value
                logger.debug(f"Manual test illuminance value: {value}")
                
            value = int(math.log10(value) * 10000 + 1)
            logger.debug(f"Converted illuminance value: {value}")
                
        super()._update_attribute(attrid, value)

The value writes fine. I decided to bring ChatGPT onto the team and after writing some custom logging and several tests, we’re just not getting anything other than a 1 or 0 from the sensor.

Something strange to note though, looking at the sensor history in HA, mine is showing yesterday it WAS working for a short time but then stopped. It was nothing I did… just started on its own. Although, I DID just update HA to the latest either yesterday or day before, can’t remember. Now after all this testing, I can’t see that history showing where it WAS breifly working. Oh well… for FUN, open ChatGPT up and paste all your driver code there. It will walk you thru a bunch of troubleshooting you can do yourself.

How curious you got an output, even if only for a moment. I’ve looked through the history of my sensor, no such ‘working’ moment has occurred yet. Max value recorded is a 1. I’ll give Chat GPT a go and see if I can get anywhere, hopefully not breaking it in the process!

Thanks!!!
Do you know why I don’t see the light sensor ?

Glad you got this far! I’m not sure why the light sensor isn’t showing, I’d hazard a guess the code hasn’t been copied into the custom quirk file correctly - likely formatting.
Having said that, the light sensor output is not currently in a usable state with the code in my original post. At this point in time there isn’t much to gain from getting it to show up, unless you’re trying to get it working properly?

yes it isn’t that important feature fore me…
I thought I could use the sensor to identify locations within the room then split it into a different “zones” for automations.

I think you’d be looking for something a bit more premium, along the lines of an Aqara FP2. I’m not sure if there are any ‘aliexpress’ presence sensors which can do zones from a single sensor - I’d be interested to buy one if you come across one!

1 Like

sure
do you know why when I change the parameter : Number fading_time its always back to ZERO.

by the way this looks good:

Worked with HA Versions:
Core
2024.7.3
Supervisor
2024.06.2
Operating System
12.4
Frontend
20240710.0
TS0601
by _TZE204_7gclukjs

I have given up on these devices. Was a fun try. I think you would need to have the code to modify and flash them directly to get them working. IDK… no expert. Moving on :slight_smile:

I have two of the Everything Presence One Kit’s and I just leave the PIR sensor off of it. Made it fit in a small case I 3D printed. These work great but to get one to Canada is very expensive. Better off financially buying the Aqara FP sensors I think.
I am running them both in beta mode (code). For some reason it was more reliable than the production code. I haven’t updated the firmware in months since I got them so not sure even what version I have or if there is even newer version firmware.

Just change dp_to_attribute 104 to 103 for luminance sensor, and it works

If you are referring to ‘104’ here:

),
        104: DPToAttributeMapping(
            TuyaIlluminanceMeasurement.ep_attribute,
            "measured_value",
            #converter=lambda x: int(math.log10(x) * 10000 + 1) if x > 0 else int(0),
        ),

and here:

    data_point_handlers = {
        1: "_dp_2_attr_update",
        2: "_dp_2_attr_update",
        3: "_dp_2_attr_update",
        4: "_dp_2_attr_update",
        9: "_dp_2_attr_update",
        101: "_dp_2_attr_update",
        102: "_dp_2_attr_update",
        104: "_dp_2_attr_update",

I changed those values to ‘103’ and restarted HA with no joys. I’ve also tried getting a raw value output from the sensor using ChatGPT as @aWanderer suggested, but nothing other than a 0 or 1.

I have noticed that all the ‘Controls’ values reset to default after navigating away from the page. Oh well!

Hi Everyone Do you have any idea how to use it with zigbee2mqtt? I have inserted it, but when I set a higher sensitivity value for both presence and movement, after a few seconds they return to 0. Thanks.

1 Like

same problem

Hi everyone, hope you’ve been keeping well! A bit of an update here.
I’ve played around with the code this afternoon and have gotten the custom input values to remain and work. Specifically input values for Fading Time, Delay Time, Min Range, Max Range and Sensitivity.

The indentation was incorrect within the “Mmw radar cluster” which resulted in the code not being interpreted correctly. The cluster begins at line 154, with the errors existing between lines 171 and 213. The revised code here:

  • if you copy the code from here, make sure the indentation remains correct.
    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: x // 100,
        endpoint_id=4,
        ),
        102: DPToAttributeMapping(
        TuyaMmwRadarFadingTime.ep_attribute,
        "present_value",
        converter=lambda x: x * 100,
        dp_converter=lambda x: x // 100,
        endpoint_id=5,
        ),
        104: DPToAttributeMapping(
        TuyaIlluminanceMeasurement.ep_attribute,
        "measured_value",
        converter=lambda x: int(math.log10(x) * 10000 + 1) if x > 0 else int(0),
        ),
    }

I will update my original post with the amended code for those who come across the thread in future.

Now to get the illuminance sensor to work?

I’ve noticed the behaviour is not 100% as expected. Some values reset on their own, but not all. This could be down to the values not ‘writing’ properly still. I’ve noticed, particularly for the min/max range setting, that the value you try to input may reset quite a few times before it sticks. I’ll try to find some time to look into why that keeps happening but for now I’m happy with the small bit of progress.

Illumination now working. Wrong attribute was mapped.
Actual fix implemented for user input fields. Incorrect steps and lower & upper value limits.

See original post for updated code in its entirety, just replacing the below code will not resolve the user input issue.

Illuminance code change:

        103: DPToAttributeMapping(
        TuyaIlluminanceMeasurement.ep_attribute,
        "measured_value",
        converter=lambda x: 10000 * math.log10(x) + 1 if x != 0 else 0,
        ),
    }

    data_point_handlers = {
        1: "_dp_2_attr_update",
        2: "_dp_2_attr_update",
        3: "_dp_2_attr_update",
        4: "_dp_2_attr_update",
        6: "_dp_2_attr_update",
        9: "_dp_2_attr_update",
        101: "_dp_2_attr_update",
        102: "_dp_2_attr_update",
        103: "_dp_2_attr_update",
    }

The change here is to replace ‘104’ with ‘103’ which was previously suggested but didn’t work for me at the time. I revisited it whilst troubleshooting the sliders and have received outputs between 0-2000lux.

1 Like