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
- Added a custom quirk → improved things a bit, but not enough.
- Created template helpers → provided stable abstractions for stop/go and close commands.
- 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 Dictfrom 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!