TUYA TS0601 Zigbee garage door opener

Sure thing @kimme1024! I should have come back and posted here (all in one place) anyway! So… to be clear, this would need to be the exact same device, as quirks depend on the Zigbee signature. My device shows up as:
Manufacturer: _TZE200_nklqjk62
Model: TS0601

  • In your Home Assistant /config folder, create a subfolder named zhaquirks.
  • In your configuration.yaml file, insert the following two lines somewhere below the “default_config” line in the file, and save.
zha:
  custom_quirks_path: /config/zhaquirks
  • Copy and paste the following code, exactly as it is, into a text file, and name that file “ts0601_garage.py” (no quotation marks).
"""Tuya based cover and blinds."""
from typing import Dict

from zigpy.profiles import zha
from zigpy.quirks import CustomDevice
import zigpy.types as t
from zigpy.zcl.clusters.general import Basic, GreenPowerProxy, Groups, Ota, Scenes, Time
from zigpy.zcl.clusters.security import IasZone

from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
)
from zhaquirks.tuya import TuyaLocalCluster
from zhaquirks.tuya.mcu import (
    DPToAttributeMapping,
    TuyaDPType,
    TuyaMCUCluster,
    TuyaOnOff
)

from zhaquirks.tuya.ts0601_dimmer import TuyaOnOffNM


ZONE_TYPE = 0x0001


class ContactSwitchCluster(TuyaLocalCluster, IasZone):
    """Tuya ContactSwitch Sensor."""

    _CONSTANT_ATTRIBUTES = {ZONE_TYPE: IasZone.ZoneType.Contact_Switch}

    def _update_attribute(self, attrid, value):
        self.debug("_update_attribute '%s': %s", attrid, value)
        super()._update_attribute(attrid, value)


class TuyaGarageManufCluster(TuyaMCUCluster):
    """Tuya garage door opener."""

    attributes = TuyaMCUCluster.attributes.copy()
    attributes.update(
        {
            # ramdom attribute IDs
            0xEF02: ("dp_2", t.uint32_t, True),
            0xEF04: ("dp_4", t.uint32_t, True),
            0xEF05: ("dp_5", t.uint32_t, True),
            0xEF0B: ("dp_11", t.Bool, True),
            0xEF0C: ("dp_12", t.enum8, True),
        }
    )

    dp_to_attribute: Dict[int, DPToAttributeMapping] = {
        # garage door trigger ¿on movement, on open, on closed?
        1: DPToAttributeMapping(
            TuyaOnOffNM.ep_attribute,
            "on_off",
            dp_type=TuyaDPType.BOOL,
        ),
        2: DPToAttributeMapping(
            TuyaMCUCluster.ep_attribute,
            "dp_2",
            dp_type=TuyaDPType.VALUE,
        ),
        3: DPToAttributeMapping(
            ContactSwitchCluster.ep_attribute,
            "zone_status",
            dp_type=TuyaDPType.BOOL,
            converter=lambda x: IasZone.ZoneStatus.Alarm_1 if x else 0,
            endpoint_id=2,
        ),
        4: DPToAttributeMapping(
            TuyaMCUCluster.ep_attribute,
            "dp_4",
            dp_type=TuyaDPType.VALUE,
        ),
        5: DPToAttributeMapping(
            TuyaMCUCluster.ep_attribute,
            "dp_5",
            dp_type=TuyaDPType.VALUE,
        ),
        11: DPToAttributeMapping(
            TuyaMCUCluster.ep_attribute,
            "dp_11",
            dp_type=TuyaDPType.BOOL,
        ),
        # garage door status (open, closed, ...)
        12: DPToAttributeMapping(
            TuyaMCUCluster.ep_attribute,
            "dp_12",
            dp_type=TuyaDPType.ENUM,
        ),
    }

    data_point_handlers = {
        1: "_dp_2_attr_update",
        2: "_dp_2_attr_update",
        3: "_dp_2_attr_update",
        4: "_dp_2_attr_update",
        5: "_dp_2_attr_update",
        11: "_dp_2_attr_update",
        12: "_dp_2_attr_update",
    }


class TuyaGarageSwitchTO(CustomDevice):
    """Tuya Garage switch."""

    signature = {
        MODELS_INFO: [
            ("_TZE200_nklqjk62", "TS0601"),
        ],
        ENDPOINTS: {
            # <SimpleDescriptor endpoint=1 profile=260 device_type=0x0051
            # input_clusters=[0, 4, 5, 61184]
            # output_clusters=[10, 25]>
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaGarageManufCluster.cluster_id,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            # <SimpleDescriptor endpoint=242 profile=41440 device_type=97
            # input_clusters=[]
            # output_clusters=[33]
            242: {
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaGarageManufCluster,
                    TuyaOnOffNM,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            2: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.IAS_ZONE,
                INPUT_CLUSTERS: [
                    ContactSwitchCluster
                ],
                OUTPUT_CLUSTERS: [],
            },
            242: {
                PROFILE_ID: 0xA1E0,
                DEVICE_TYPE: 0x0061,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [0x0021],
            },
        },
    }
  • Save the .py file into the /config/zhaquirks folder you created in the earlier step.
  • If you haven’t already, delete the garage door opener device from Home Assistant.
  • Restart Home Assistant.
  • Re-discover the garage door opener.
  • If you did it all perfectly, you can go into the device and see that it has a control and a sensor (they’re not named “switch” and “sensor”… I did that, and cannot recall what they were named at first. I also chose new icons for them.

You can also expand the Zigbee properties with the down arrow and you should see that it used the newly created quirk (.py file) to define the device (bottom line)

So… you have a switch, which is really just supposed to act like a button. You press it, and the garage door opens. You press it again, and it closes. You have a sensor that tells you if the door is opened or closed, but it’s NOT like the normal sensors for doors… just need to make that clear… a door sensor is almost immediate, but you can press the switch control for this, and because garage doors close slowly, the sensor might not say closed for ten seconds or so. It just takes some getting used to is all.

Now the only other thing to know is that the quirk defines it as a switch because I think it has to. But I told you that it really is supposed to act like a button. The only shortcoming of the quirk is that when you activate the switch, it stays on… if you were to go back and turn it off a half-hour later, it doesn’t close the door. It just turns the switch off. So… the unglamorous solution to this is to create an automation to make it act more like a button… I don’t need to post the yaml… here’s the gist:

Hope that all makes sense. In the end, I put it in the dashboard like this, using an Entities card:

When you click click the switch on the card, the automation immediately sees that the switch has been hit, and immediately turns it off, so it only ever “blips” when you tap it. But the sensor shows me current state.

From there, I created three automations (you do you, of course):

  • If garage door was open at time of sunset, wait ten minutes and close it, if not closed already.
  • If garage door was opened while sun is under horizon (dark hours), wait ten minutes and close it, if not closed already.
  • If garage door is opened during daylight hours, and stays open for more than an hour, send a notification asking if I want to close it.

I think there’s probably a way to combine if/else stuff for the first two, but I couldn’t get it and decided to just split them.

I hope this helps! Ask other questions if needed!

-C

3 Likes