Curtain Switch TS130F doesn't work correctly, no calibration

Hello,

I’m a complete newby to Home Assistant but I know it’s time change to a correct Smart Home!

I bough 4 curtain switches on Amazon. Here is the signature

{
  "node_descriptor": "NodeDescriptor(logical_type=<LogicalType.Router: 1>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.FullFunctionDevice|MainsPowered|RxOnWhenIdle|AllocateAddress: 142>, manufacturer_code=4417, maximum_buffer_size=66, maximum_incoming_transfer_size=66, server_mask=10752, maximum_outgoing_transfer_size=66, descriptor_capability_field=<DescriptorCapability.NONE: 0>, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=False, *is_full_function_device=True, *is_mains_powered=True, *is_receiver_on_when_idle=True, *is_router=True, *is_security_capable=False)",
  "endpoints": {
    "1": {
      "profile_id": "0x0104",
      "device_type": "0x0202",
      "input_clusters": [
        "0x0000",
        "0x0003",
        "0x0004",
        "0x0005",
        "0x0006",
        "0x0102"
      ],
      "output_clusters": [
        "0x000a"
      ]
    }
  },
  "manufacturer": "_TZ3000_ltiqubue",
  "model": "TS130F",
  "class": "zigpy.device.Device"
}

I tried a lot since a week to get them working properly but I didn’t manage it. I Installed ZHA Toolkit, activated it in the configuration.yaml by adding:

zha_toolkit:

zha:
  enable_quirks: true
  custom_quirks_path: /config/zha_quirks/

I used the the TS130F.py file and I have the following lines:

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

from zigpy.profiles import 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, Groups, OnOff, Ota, Scenes, Time

from zhaquirks.const import (

    DEVICE_TYPE,

    ENDPOINTS,

    INPUT_CLUSTERS,

    MODELS_INFO,

    OUTPUT_CLUSTERS,

    PROFILE_ID,

)

ATTR_CURRENT_POSITION_LIFT_PERCENTAGE = 0x0008

CMD_GO_TO_LIFT_PERCENTAGE = 0x0005

class TuyaWithBacklightOnOffCluster(CustomCluster):

    """TuyaSmartCurtainOnOffCluster: fire events corresponding to press type."""

    cluster_id = OnOff.cluster_id

    LIGHT_MODE_1 = {0x8001: 0}

    LIGHT_MODE_2 = {0x8001: 1}

    LIGHT_MODE_3 = {0x8001: 2}

    attributes = {0x8001: ("backlight_mode", t.enum8)}

class TuyaCoveringCluster(CustomCluster, WindowCovering):

    """TuyaSmartCurtainWindowCoveringCluster: Allow to setup Window covering tuya devices."""

    attributes = WindowCovering.attributes.copy()

    attributes.update({0xF000: ("tuya_moving_state", t.enum8)})

    attributes.update({0xF001: ("calibration", t.enum8)})

    attributes.update({0xF002: ("motor_reversal", t.enum8)})

    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 (cf https://github.com/dresden-elektronik/deconz-rest-plugin/issues/3757)

            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 TuyaTS130F(CustomDevice):

    """Tuya smart curtain roller shutter."""

    signature = {

        # SizePrefixedSimpleDescriptor(endpoint=1, profile=260, device_type=0x0202, device_version=1, input_clusters=[0, 4, 5, 6, 10, 0x0102], output_clusters=[25]))

        MODELS_INFO: [

            ("_TZ3000_8kzqqzu4", "TS130F"),

            ("_TZ3000_egq7y6pr", "TS130F"),

        ],

        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,

                    Time.cluster_id,

                    OnOff.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,

                    Groups.cluster_id,

                    Scenes.cluster_id,

                    Time.cluster_id,

                    TuyaWithBacklightOnOffCluster,

                    TuyaCoveringCluster,

                ],

                OUTPUT_CLUSTERS: [Ota.cluster_id],

            },

        },

    }

class TuyaZemismartTS130F(CustomDevice):

    """Tuya ZemiSmart smart curtain roller shutter."""

    signature = {

        # SizePrefixedSimpleDescriptor(endpoint=1, profile=260, device_type=0x0202, device_version=1, input_clusters=[0x0000, 0x0004, 0x0005, 0x0006, 0x0102], output_clusters=[0x000a, 0x0019]))

        MODELS_INFO: [("_TZ3000_ltiqubue", "TS130F")],

        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,

                    OnOff.cluster_id,

                    WindowCovering.cluster_id,

                ],

                OUTPUT_CLUSTERS: [

                    Time.cluster_id,

                    Ota.cluster_id,

                ],

            },

        },

    }

    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,

                    TuyaWithBacklightOnOffCluster,

                    TuyaCoveringCluster,

                ],

                OUTPUT_CLUSTERS: [

                    Time.cluster_id,

                    Ota.cluster_id,

                ],

            },

        },

    }

Can somebody tell me why the quirk isn’t used?

I cannot calibrate the endpoints of the switch so the curtain doesn’t close completely. Option calibration is not present. Also open and close is reversed.

It would be very nice is somebody could tell me what I am doing wrong and If there is a way to get those switches working properly as I need to install 15 of them…

Thanks in advance!

That question is best asked and answered in a new device feature request issue to the ZHA Device Handlers repository. Read here about posting a new device feature request issue to the ZHA Device Handlers repository → https://www.home-assistant.io/integrations/zha#how-to-add-support-for-new-and-unsupported-devices

As we are in 2024 now I got the calibration done relatively easy, but only after reading after several outdated posts. In my HA version 2024.5 the quirks was loaded automatically and after reading the .zigbee2mqtt explanation it was quite easy to do. TuYa TS130F control via MQTT | Zigbee2MQTT

Quirk: zhaquirks.tuya.ts130f.TuyaTS130FTI2

Hello community,

I also have two of these curtain switches. Here is my signature:

{
  "node_descriptor": "NodeDescriptor(logical_type=<LogicalType.Router: 1>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.FullFunctionDevice|MainsPowered|RxOnWhenIdle|AllocateAddress: 142>, manufacturer_code=4417, maximum_buffer_size=66, maximum_incoming_transfer_size=66, server_mask=10752, maximum_outgoing_transfer_size=66, descriptor_capability_field=<DescriptorCapability.NONE: 0>, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=False, *is_full_function_device=True, *is_mains_powered=True, *is_receiver_on_when_idle=True, *is_router=True, *is_security_capable=False)",
  "endpoints": {
    "1": {
      "profile_id": "0x0104",
      "device_type": "0x0202",
      "input_clusters": [
        "0x0000",
        "0x0003",
        "0x0004",
        "0x0005",
        "0x0006",
        "0x0102"
      ],
      "output_clusters": [
        "0x000a"
      ]
    }
  },
  "manufacturer": "_TZ3000_ltiqubue",
  "model": "TS130F",
  "class": "zigpy.device.Device"
}

No quirks were loaded automatically for me. I made the settings as in the post from @jopeyyyy and saved the script as well. I restarted HA and reconnected the switches but the settings for setting the calibration mode are still not available to me.

After a restart, the log file says:

WARNING (ImportExecutor_0) [zhaquirks] Loaded custom quirks. Please contribute them to GitHub - zigpy/zha-device-handlers: ZHA device handlers bridge the functionality gap created when manufacturers deviate from the ZCL specification, handling deviations and exceptions by parsing custom messages to and from Zigbee devices.

I can’t find any other messages in the log.

What else can I do to calibrate the switch in ZHA?

Thank you for your support!

Hello again,
I wrote my own quirks file today. I copied the actual adjustments from the TS130F.py from the github repository. I adapted the signature to the signature of my switch. Now the quirks file is used when adding the switch and I find the attributes calibration and calibration_time in the TuyaCoveringCluster. I can also query the attributes. When writing the new values, a green check mark is displayed but the new value does not seem to be actually written. If I read the attribute again straight away, the old value is still there (or again). I have the same behavior with all other attributes, e.g. in the TuyaWithBacklightOnOffCluster.

Here is my adapted quirks file:

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

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,
    Identify,
    Groups,
    Scenes,
    OnOff,
    Time,
)

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."""

    attributes = OnOff.attributes.copy()
    attributes.update({0x8001: ("backlight_mode", 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."""

    attributes = WindowCovering.attributes.copy()
    attributes.update({0x8000: ("motor_mode", MotorMode)})
    attributes.update({0xF000: ("tuya_moving_state", t.enum8)})
    attributes.update({0xF001: ("calibration", t.enum8)})
    attributes.update({0xF002: ("motor_reversal", t.enum8)})
    attributes.update({0xF003: ("calibration_time", 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 (cf https://github.com/dresden-elektronik/deconz-rest-plugin/issues/3757)
            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 TuyaTS130F(CustomDevice):
    """Tuya smart curtain roller shutter."""

    signature = {
        # <SimpleDescriptor endpoint=1 profile=260 device_type=0x0202
        # device_version=1
        # input_clusters=[0x0000, 0x0003, 0x0004, 0x0005, 0x0006, 0x0102]
        # output_clusters=[0x000a]>
        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,
                    OnOff.cluster_id,
                    WindowCovering.cluster_id,
                ],
                OUTPUT_CLUSTERS: [
                    Time.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,
                    TuyaWithBacklightOnOffCluster,
                    TuyaCoveringCluster,
                ],
                OUTPUT_CLUSTERS: [
                    Time.cluster_id,
                ],
            },
        },
    }

Does anyone have any idea what I need to do to be able to describe the attributes?

Thank you for supporting an quirks newbie :slight_smile: