Skip to content

Instantly share code, notes, and snippets.

@crstian19
Last active March 12, 2026 16:47
Show Gist options
  • Select an option

  • Save crstian19/fceda5c8bd032b2971c82ffc7127a808 to your computer and use it in GitHub Desktop.

Select an option

Save crstian19/fceda5c8bd032b2971c82ffc7127a808 to your computer and use it in GitHub Desktop.
ZHA quirk for Tuya TS130F curtain module - manufacturer _TZ3000_bs93npae
"""ZHA quirk for Tuya TS130F curtain module - manufacturer _TZ3000_bs93npae.
Device: QS-Zigbee-C01 / Tuya TS130F blind/shutter switch
Manufacturer: _TZ3000_bs93npae
Controls exposed in HA device panel:
- motor_reversal (switch) : invert motor direction
- calibration_time (number) : total travel time in 0.1s (e.g. 300 = 30s)
- Start Calibration (button) : starts calibration mode (writes 0 to 0xF001)
- Save Calibration (button) : saves calibration (writes 1 to 0xF001)
"""
from typing import Final
from zigpy.quirks import CustomCluster
from zigpy.quirks.v2 import EntityType, QuirkBuilder
import zigpy.types as t
from zigpy.zcl.clusters.closures import WindowCovering
from zigpy.zcl.foundation import ZCLAttributeDef
ATTR_CURRENT_POSITION_LIFT_PERCENTAGE = 0x0008
CMD_GO_TO_LIFT_PERCENTAGE = 0x0005
class MotorMode(t.enum8):
"""Tuya motor mode."""
STRONG_MOTOR = 0x00
WEAK_MOTOR = 0x01
class CalibrationMode(t.enum8):
"""Tuya calibration mode."""
CALIBRATE = 0x00
STORE = 0x01
class MotorReversal(t.enum8):
"""Tuya motor reversal."""
DEFAULT = 0x00
REVERSED = 0x01
class TuyaCoveringCluster(CustomCluster, WindowCovering):
"""Tuya TS130F window covering cluster with calibration attributes."""
class AttributeDefs(WindowCovering.AttributeDefs):
"""Attribute definitions."""
motor_mode: Final = ZCLAttributeDef(id=0x8000, type=MotorMode)
tuya_moving_state: Final = ZCLAttributeDef(id=0xF000, type=t.enum8)
calibration: Final = ZCLAttributeDef(id=0xF001, type=CalibrationMode)
motor_reversal: Final = ZCLAttributeDef(id=0xF002, type=MotorReversal)
calibration_time: Final = ZCLAttributeDef(id=0xF003, type=t.uint16_t)
def _update_attribute(self, attrid, value):
if attrid == ATTR_CURRENT_POSITION_LIFT_PERCENTAGE:
# Tuya reports 0=open, HA expects 0=closed
value = 100 - value
super()._update_attribute(attrid, value)
async def command(
self, command_id, *args, manufacturer=None, expect_reply=True, tsn=None
):
"""Invert lift percentage when sending commands."""
if command_id == CMD_GO_TO_LIFT_PERCENTAGE:
percent = args[0]
percent = 100 - percent
return await super().command(command_id, percent)
return await super().command(
command_id,
*args,
manufacturer=manufacturer,
expect_reply=expect_reply,
tsn=tsn,
)
(
QuirkBuilder("_TZ3000_bs93npae", "TS130F")
.replaces(TuyaCoveringCluster)
# Motor reversal: switch ON = reversed, OFF = normal
.switch(
"motor_reversal",
WindowCovering.cluster_id,
on_value=MotorReversal.REVERSED,
off_value=MotorReversal.DEFAULT,
translation_key="motor_reversal",
fallback_name="Motor Reversal",
entity_type=EntityType.CONFIG,
)
# Total motor travel time in 0.1s units
.number(
"calibration_time",
WindowCovering.cluster_id,
min_value=0,
max_value=60000,
step=1,
unit="0.1s",
translation_key="calibration_time",
fallback_name="Calibration Time",
entity_type=EntityType.CONFIG,
)
# Button: start calibration (writes 0 to calibration attribute)
.write_attr_button(
"calibration",
CalibrationMode.CALIBRATE,
WindowCovering.cluster_id,
unique_id_suffix="start",
translation_key="calibration_start",
fallback_name="Start Calibration",
entity_type=EntityType.CONFIG,
)
# Button: save calibration (writes 1 to calibration attribute)
.write_attr_button(
"calibration",
CalibrationMode.STORE,
WindowCovering.cluster_id,
unique_id_suffix="save",
translation_key="calibration_save",
fallback_name="Save Calibration",
entity_type=EntityType.CONFIG,
)
.add_to_registry()
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment