Templates and Automations for Garage Door with TS0603 by _TZE608_xxxxx

Hi everyone,

I wanted to share a complete working example for anyone using the garage door opener TS0603 by _TZE608_xxxxx(often found on AliExpress). Out of the box, this device is unstable and unsafe to use. With a combination of a custom quirk, template helpers, and automations, I now have a setup that works reliably in Home Assistant and integrates cleanly with HomeKit. Hopefully this post saves others a lot of trial and error.

Problem statement

Out of the box, the controller didn’t behave in a stable way. Clicking too fast caused strange behavior: delayed commands piling up, the door staying open, or even closing while the car was still underneath. Clearly, the stock setup wasn’t safe or usable.

A lot of what I did is based on the excellent discussion here: GitHub issue #3263. That post was the starting point for my work, but I refined it to make it fit seamlessly into Home Assistant.

Summary of my solution

  1. Added a custom quirk → improved things a bit, but not enough.
  2. Created template helpers → provided stable abstractions for stop/go and close commands.
  3. Built automations → handled pulse timing, stop/go behavior, and reliable close loops.

With these in place, the door can now open, close, or stop safely and consistently.

Step 1 – Quirk

Under /config/zha_quirks/ I added a file called TS0603_quirk.py.

Here’s the content of the quirk:

TS0603_quirk.py ``` """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, Identify, 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,
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",
    ),
    2: DPToAttributeMapping(
        TuyaMCUCluster.ep_attribute,
        "dp_2",
    ),
    3: DPToAttributeMapping(
        ContactSwitchCluster.ep_attribute,
        "zone_status",
        converter=lambda x: IasZone.ZoneStatus.Alarm_1 if x else 0,
        endpoint_id=2,
    ),
    4: DPToAttributeMapping(
        TuyaMCUCluster.ep_attribute,
        "dp_4",
    ),
    5: DPToAttributeMapping(
        TuyaMCUCluster.ep_attribute,
        "dp_5",
    ),
    11: DPToAttributeMapping(
        TuyaMCUCluster.ep_attribute,
        "dp_11",
    ),
    # garage door status (open, closed, ...)
    12: DPToAttributeMapping(
        TuyaMCUCluster.ep_attribute,
        "dp_12",
    ),
}

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: [
        ("_TZE608_xkr8gep3", "TS0603"),
                                       
                                       
    ],
    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,
                Identify.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.SMART_PLUG,
            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],
        },
    },
}
```

Don’t forget to link it in your configuration.yaml:

zha:
  database_path: /config/zigbee.db
  enable_quirks: true
  custom_quirks_path: /config/zha_quirks/

Restart Home Assistant afterwards.

Step 2 – Templates (helpers)

I added two input booleans and a template cover. You can put these directly into configuration.yaml:

input_boolean:
  garage_door_stop_go:
    name: Garage Door – Stop/Go
    icon: mdi:garage

  garage_door_close:
    name: Garage Door – Close
    icon: mdi:arrow-down-bold-box

cover:
  - platform: template
    covers:
      garage_door:
        unique_id: garage_door
        friendly_name: "Garage Door"
        value_template: >
          {{ is_state('binary_sensor.garage_door_opening', 'on') }}
        open_cover:
          service: input_boolean.turn_on
          target:
            entity_id: input_boolean.garage_door_stop_go
        stop_cover:
          service: input_boolean.turn_on
          target:
            entity_id: input_boolean.garage_door_stop_go
        close_cover:
          service: input_boolean.turn_on
          target:
            entity_id: input_boolean.garage_door_close
        icon_template: >-
          {% if is_state('binary_sensor.garage_door_opening', 'off') %}
            mdi:garage
          {% elif is_state('input_boolean.garage_door_close', 'on') %}
            mdi:transfer-down
          {% elif is_state('binary_sensor.garage_door_opening', 'on') %}
            mdi:garage-open
          {% else %}
            mdi:garage-alert
          {% endif %}

These helpers provide the stable control logic that the stock device was missing.

Step 3 – Automations

Garage Door – Move (Stop/Go pulse)

alias: Garage Door - Move
description: Pulse relay based on current door state; reset *_stop_go* helper
mode: single
triggers:
  - entity_id: input_boolean.garage_door_stop_go
    to: "on"
    trigger: state
conditions: []
actions:
  - if:
      - condition: template
        value_template: "{{ trigger.to_state.context.user_id is not none }}"
    then:
      - target:
          entity_id: input_boolean.garage_door_close
        action: input_boolean.turn_off
  - choose:
      - conditions:
          - condition: state
            entity_id: binary_sensor.garage_door_opening
            state: "off"
        sequence:
          - target:
              entity_id: switch.garage_door
            action: switch.turn_on
      - conditions:
          - condition: state
            entity_id: binary_sensor.garage_door_opening
            state: "on"
        sequence:
          - target:
              entity_id: switch.garage_door
            action: switch.turn_off
  - delay: "00:00:01"
  - target:
      entity_id: input_boolean.garage_door_stop_go
    action: input_boolean.turn_off

Garage Door – Close (repeat pulses until closed)

alias: Garage Door - Close
description: Repeat pulses via *_stop_go* until contact reports closed
mode: parallel
max: 3
triggers:
  - entity_id: input_boolean.garage_door_close
    to: "on"
    trigger: state
conditions:
  - condition: state
    entity_id: binary_sensor.garage_door_opening
    state: "on"
actions:
  - repeat:
      until:
        - condition: state
          entity_id: binary_sensor.garage_door_opening
          state: "off"
      sequence:
        - alias: Impulse via *_stop_go*
          target:
            entity_id: input_boolean.garage_door_stop_go
          action: input_boolean.turn_on
        - wait_for_trigger:
            - value_template: "{{ is_state('binary_sensor.garage_door_opening', 'off') }}"
              trigger: template
          timeout: "00:00:22"
          continue_on_timeout: true
        - choose:
            - conditions:
                - condition: state
                  entity_id: binary_sensor.garage_door_opening
                  state: "off"
              sequence:
                - alias: Stop close loop
                  target:
                    entity_id: input_boolean.garage_door_close
                  action: input_boolean.turn_off

Final notes

Make sure to adapt the entity IDs (switch.garage_door, binary_sensor.garage_door_opening, etc.) to match your setup.

With this setup, I can now:

  • Open the door
  • Stop mid-operation
  • Close reliably (looping until contact sensor confirms closed)

I’ve been using this for weeks without a single glitch. It also integrates nicely into HomeKit (via HomeKit Bridge), so I can control the door with Siri voice commands.

Hope this helps anyone else struggling with the TS0603 garage door opener!

1 Like

Hi,
I’m having a problem with this sensor. Over time, it stops responding correctly. I’m using a Zigbee 3.0 Sonoff USB stick. My first issue occurred when pairing a new device: I had to keep the sensor very close to the Sonoff stick in order for it to register.
The second issue is low signal, even though the sensor is about 5 meters away from the Sonoff USB stick. I’ve integrated it with Zigbee2MQTT.
Could your solution help me with these problems?

cannot say, at any rate try the quirk at least, it might help, from your discretion it seems more an issue with the signal strength. Is your observation only limited to the garage door controller? I mean could it be the USB Zigbee stick?

Hi

Can I ask what the trigger switch is like on your garage door motor?
I have implemented all the changes you mention above but I am still having trouble with this, it seems to work fine on the bench when I test it with a multi meter but as soon as I put it on the door it just acts weired, keeps opening the door about 6 inches and then closes it
One thing I have noticed though is that I initially though the button on my motor was just triggering a ground signal but it actually triggers a 5v signal so I am thinking it may be the 5v that is messing with the TS0603 device
I was thinking to order a small relay and just get the device to trigger 5v through that and see how it goes

Cheers

Hi there,

I also tried to get this little item up an running.
First I created the quirk.
This went fine.
I got 2 new entities: 1 switch and 1 binary.sensor.
The garage door was opening and closing by using the new entity switch and the other entity opening was correctly reporting the state.

Then I wanted to integrate your templates and automations what ended in a greyed out switch that is not responding anymore.

I tried to delete the template and the automations, but this did not make any difference.

I made hardware resets and tried to integrate the item as new. Now even the switch disappeared.

I reloaded a backup.

The switch was here again but still greyed out and not usable.

I don’t know anymore what I could do with this.

The problem started with the changes in configuration.yaml. A shame I did that, because the door was already working…

Any advice?