Skip to content

[Device Support Request] CurtainSwitch _TZE20C_xbexmf8h TS130F - no ZHA TuyaCoveringCluster BUT is in Z2M #4615

@jstimmer

Description

@jstimmer

Problem description

_TZE20C_xbexmf8h TS130F Only has 0xeb00 / 0xef00 clusters (No Covering Cluster) So I can't control anything from HA with other TS130F quirks and have been unsuccessful at guessing DPs to write it myself (but I'm definitely an amateur).

Solution description

It does exist in Z2M so I'm hoping someone with access can port over/build a quirk that'll work for the ZHA folks.

Screenshots/Video

Screenshots/Video

[Paste/upload your media here]

Diagnostics information

zha-01JAZ8V12KZKPTF3Z8CXA4AXWM-_TZE20C_xbexmf8h TS130F-00bce2ba663f56df213332ba9bce39da.json

Device signature

Device signature
{
  "node_descriptor": {
    "logical_type": 1,
    "complex_descriptor_available": 0,
    "user_descriptor_available": 0,
    "reserved": 0,
    "aps_flags": 0,
    "frequency_band": 8,
    "mac_capability_flags": 142,
    "manufacturer_code": 4417,
    "maximum_buffer_size": 74,
    "maximum_incoming_transfer_size": 80,
    "server_mask": 10752,
    "maximum_outgoing_transfer_size": 80,
    "descriptor_capability_field": 0
  },
  "endpoints": {
    "1": {
      "profile_id": "0x0104",
      "device_type": "0x0301",
      "input_clusters": [
        "0x0000",
        "0x0004",
        "0x0005",
        "0xeb00",
        "0xef00"
      ],
      "output_clusters": [
        "0x000a",
        "0x0019"
      ]
    },
    "242": {
      "profile_id": "0xa1e0",
      "device_type": "0x0061",
      "input_clusters": [],
      "output_clusters": [
        "0x0021"
      ]
    }
  },
  "manufacturer": "_TZE20C_xbexmf8h",
  "model": "TS130F",
  "class": "zigpy.device.Device"
}

Logs

Logs
[Paste the logs here]

Custom quirk

Custom quirk
"""Device handler for loratap TS130F smart curtain switch."""

from typing import Final

from zigpy.profiles import zgp, zha
from zigpy.quirks import CustomCluster, CustomDevice
import zigpy.types as t
from zigpy.zcl.clusters.closures import WindowCovering
from zigpy.zcl.clusters.general import (
    Basic,
    GreenPowerProxy,
    Groups,
    Identify,
    OnOff,
    Ota,
    Scenes,
    Time,
)
from zigpy.zcl.foundation import ZCLAttributeDef

from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODEL,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
)
from zhaquirks.tuya import SwitchBackLight, TuyaZBExternalSwitchTypeCluster


ATTR_CURRENT_POSITION_LIFT_PERCENTAGE = 0x0008
CMD_GO_TO_LIFT_PERCENTAGE = 0x0005


class TuyaWithBacklightOnOffCluster(CustomCluster, OnOff):
    """Tuya Zigbee On Off cluster with extra attributes."""

    class AttributeDefs(OnOff.AttributeDefs):
        """Attribute definitions."""

        backlight_mode: Final = ZCLAttributeDef(id=0x8001, type=SwitchBackLight)


class MotorMode(t.enum8):
    """Tuya motor mode enum."""

    STRONG_MOTOR = 0x00
    WEAK_MOTOR = 0x01


class TuyaCoveringCluster(CustomCluster, WindowCovering):
    """TuyaSmartCurtainWindowCoveringCluster: Allow to setup Window covering tuya devices."""

    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=t.enum8)
        motor_reversal: Final = ZCLAttributeDef(id=0xF002, type=t.enum8)
        calibration_time: Final = ZCLAttributeDef(id=0xF003, type=t.uint16_t)

    def _update_attribute(self, attrid, value):
        if attrid == ATTR_CURRENT_POSITION_LIFT_PERCENTAGE:
            # Invert the percentage value (cf https://github.com/dresden-elektronik/deconz-rest-plugin/issues/3757)
            value = 100 - value
        super()._update_attribute(attrid, value)

    async def command(
        self, command_id, *args, manufacturer=None, expect_reply=True, tsn=None
    ):
        """Override default command to invert percent lift value."""
        if command_id == CMD_GO_TO_LIFT_PERCENTAGE:
            percent = args[0]
            # Invert the percentage value
            percent = 100 - percent
            v = (percent,)
            return await super().command(command_id, *v)
        return await super().command(
            command_id,
            *args,
            manufacturer=manufacturer,
            expect_reply=expect_reply,
            tsn=tsn,
        )


class TuyaTS130FNoButtons(CustomDevice):
    """Tuya TS130F curtain module without physical buttons (_TZ3000_niezp8zq)."""

    signature = {
        MODEL: "TS130F",
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Identify.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    WindowCovering.cluster_id,
                ],
                OUTPUT_CLUSTERS: [
                    Ota.cluster_id,
                ],
            },
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Identify.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaCoveringCluster,  # custom cluster with calibration attrs
                ],
                OUTPUT_CLUSTERS: [
                    Ota.cluster_id,
                ],
            },
        },
    }


class TuyaTS130F_XBEXMF8H(CustomDevice):
    """Tuya TS130F curtain module variant (_TZE20C_xbexmf8h)."""

    # Matches the signature you pasted:
    # - manufacturer: "_TZE20C_xbexmf8h"
    # - model: "TS130F"
    # - endpoint 1: profile 0x0104, device_type 0x0301,
    #   in: 0x0000,0x0004,0x0005,0xEB00,0xEF00
    #   out: 0x000A,0x0019
    # - endpoint 242: profile 0xA1E0, device_type 0x0061,
    #   out: 0x0021
    signature = {
        MODEL: "TS130F",
        "manufacturer": "_TZE20C_xbexmf8h",
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: 0x0301,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,   # 0x0000
                    Groups.cluster_id,  # 0x0004
                    Scenes.cluster_id,  # 0x0005
                    0xEB00,
                    0xEF00,
                ],
                OUTPUT_CLUSTERS: [
                    Time.cluster_id,  # 0x000A
                    Ota.cluster_id,   # 0x0019
                ],
            },
            242: {
                PROFILE_ID: 0xA1E0,
                DEVICE_TYPE: 0x0061,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [0x0021],
            },
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaCoveringCluster,  # gives you motor_reversal, calibration, etc.
                    TuyaZBExternalSwitchTypeCluster.cluster_id,
                ],
                OUTPUT_CLUSTERS: [
                    Time.cluster_id,
                    Ota.cluster_id,
                ],
            },
            242: {
                PROFILE_ID: zgp.PROFILE_ID,
                DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        },
    }

Additional information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions