Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] Curtain motor _TZE200_r0jdjrvi has no entities #1953

Open
caradas opened this issue Nov 22, 2022 · 61 comments
Open

[BUG] Curtain motor _TZE200_r0jdjrvi has no entities #1953

caradas opened this issue Nov 22, 2022 · 61 comments
Labels
custom quirk available A custom quirk is available to solve the issue, but it's not merged in the repo yet Tuya Request/PR regarding a Tuya device

Comments

@caradas
Copy link

caradas commented Nov 22, 2022

Describe the bug
Curtain motor has no entities.

Expected behavior
Entities for opening and closing curtain and if possible slider to set partially closed

{
  "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.AllocateAddress|RxOnWhenIdle|MainsPowered|FullFunctionDevice: 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": 260,
      "device_type": "0x0051",
      "in_clusters": [
        "0x0000",
        "0x0004",
        "0x0005",
        "0xef00"
      ],
      "out_clusters": [
        "0x000a",
        "0x0019"
      ]
    },
    "242": {
      "profile_id": 41440,
      "device_type": "0x0061",
      "in_clusters": [],
      "out_clusters": [
        "0x0021"
      ]
    }
  },
  "manufacturer": "_TZE200_r0jdjrvi",
  "model": "TS0601",
  "class": "zigpy.device.Device"
}
{
  "home_assistant": {
    "installation_type": "Home Assistant OS",
    "version": "2022.11.4",
    "dev": false,
    "hassio": true,
    "virtualenv": false,
    "python_version": "3.10.7",
    "docker": true,
    "arch": "x86_64",
    "timezone": "Europe/Berlin",
    "os_name": "Linux",
    "os_version": "5.15.74",
    "supervisor": "2022.10.2",
    "host_os": "Home Assistant OS 9.3",
    "docker_version": "20.10.18",
    "chassis": "vm",
    "run_as_root": true
  },
  "custom_components": {},
  "integration_manifest": {
    "domain": "zha",
    "name": "Zigbee Home Automation",
    "config_flow": true,
    "documentation": "https://www.home-assistant.io/integrations/zha",
    "requirements": [
      "bellows==0.34.2",
      "pyserial==3.5",
      "pyserial-asyncio==0.6",
      "zha-quirks==0.0.86",
      "zigpy-deconz==0.19.0",
      "zigpy==0.51.5",
      "zigpy-xbee==0.16.2",
      "zigpy-zigate==0.10.3",
      "zigpy-znp==0.9.1"
    ],
    "usb": [
      {
        "vid": "10C4",
        "pid": "EA60",
        "description": "*2652*",
        "known_devices": [
          "slae.sh cc2652rb stick"
        ]
      },
      {
        "vid": "1A86",
        "pid": "55D4",
        "description": "*sonoff*plus*",
        "known_devices": [
          "sonoff zigbee dongle plus v2"
        ]
      },
      {
        "vid": "10C4",
        "pid": "EA60",
        "description": "*sonoff*plus*",
        "known_devices": [
          "sonoff zigbee dongle plus"
        ]
      },
      {
        "vid": "10C4",
        "pid": "EA60",
        "description": "*tubeszb*",
        "known_devices": [
          "TubesZB Coordinator"
        ]
      },
      {
        "vid": "1A86",
        "pid": "7523",
        "description": "*tubeszb*",
        "known_devices": [
          "TubesZB Coordinator"
        ]
      },
      {
        "vid": "1A86",
        "pid": "7523",
        "description": "*zigstar*",
        "known_devices": [
          "ZigStar Coordinators"
        ]
      },
      {
        "vid": "1CF1",
        "pid": "0030",
        "description": "*conbee*",
        "known_devices": [
          "Conbee II"
        ]
      },
      {
        "vid": "10C4",
        "pid": "8A2A",
        "description": "*zigbee*",
        "known_devices": [
          "Nortek HUSBZB-1"
        ]
      },
      {
        "vid": "0403",
        "pid": "6015",
        "description": "*zigate*",
        "known_devices": [
          "ZiGate+"
        ]
      },
      {
        "vid": "10C4",
        "pid": "EA60",
        "description": "*zigate*",
        "known_devices": [
          "ZiGate"
        ]
      },
      {
        "vid": "10C4",
        "pid": "8B34",
        "description": "*bv 2010/10*",
        "known_devices": [
          "Bitron Video AV2010/10"
        ]
      }
    ],
    "codeowners": [
      "@dmulcahey",
      "@adminiuga",
      "@puddly"
    ],
    "zeroconf": [
      {
        "type": "_esphomelib._tcp.local.",
        "name": "tube*"
      },
      {
        "type": "_zigate-zigbee-gateway._tcp.local.",
        "name": "*zigate*"
      },
      {
        "type": "_zigstar_gw._tcp.local.",
        "name": "*zigstar*"
      }
    ],
    "dependencies": [
      "file_upload"
    ],
    "after_dependencies": [
      "onboarding",
      "usb",
      "zeroconf"
    ],
    "iot_class": "local_polling",
    "loggers": [
      "aiosqlite",
      "bellows",
      "crccheck",
      "pure_pcapy3",
      "zhaquirks",
      "zigpy",
      "zigpy_deconz",
      "zigpy_xbee",
      "zigpy_zigate",
      "zigpy_znp"
    ],
    "is_built_in": true
  },
  "data": {
    "ieee": "**REDACTED**",
    "nwk": 46434,
    "manufacturer": "_TZE200_r0jdjrvi",
    "model": "TS0601",
    "name": "_TZE200_r0jdjrvi TS0601",
    "quirk_applied": false,
    "quirk_class": "zigpy.device.Device",
    "manufacturer_code": 4417,
    "power_source": "Mains",
    "lqi": 28,
    "rssi": null,
    "last_seen": "2022-11-22T00:55:09",
    "available": true,
    "device_type": "Router",
    "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.AllocateAddress|RxOnWhenIdle|MainsPowered|FullFunctionDevice: 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": 260,
          "device_type": "0x0051",
          "in_clusters": [
            "0x0000",
            "0x0004",
            "0x0005",
            "0xef00"
          ],
          "out_clusters": [
            "0x000a",
            "0x0019"
          ]
        },
        "242": {
          "profile_id": 41440,
          "device_type": "0x0061",
          "in_clusters": [],
          "out_clusters": [
            "0x0021"
          ]
        }
      }
    },
    "active_coordinator": false,
    "entities": [],
    "neighbors": [],
    "endpoint_names": [
      {
        "name": "SMART_PLUG"
      },
      {
        "name": "unknown 97 device_type of 0xa1e0 profile id"
      }
    ],
    "user_given_name": null,
    "device_reg_id": "b43e549ed833f2c0c88589cb86a72fa1",
    "area_id": "schlafzimmer",
    "cluster_details": {
      "1": {
        "device_type": {
          "name": "SMART_PLUG",
          "id": 81
        },
        "profile_id": 260,
        "in_clusters": {
          "0x0004": {
            "endpoint_attribute": "groups",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x0005": {
            "endpoint_attribute": "scenes",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0xef00": {
            "endpoint_attribute": null,
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x0000": {
            "endpoint_attribute": "basic",
            "attributes": {
              "0x0001": {
                "attribute_name": "app_version",
                "value": 70
              },
              "0x0004": {
                "attribute_name": "manufacturer",
                "value": "_TZE200_r0jdjrvi"
              },
              "0x0005": {
                "attribute_name": "model",
                "value": "TS0601"
              }
            },
            "unsupported_attributes": {}
          }
        },
        "out_clusters": {
          "0x0019": {
            "endpoint_attribute": "ota",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x000a": {
            "endpoint_attribute": "time",
            "attributes": {},
            "unsupported_attributes": {}
          }
        }
      },
      "242": {
        "device_type": {
          "name": "unknown",
          "id": 97
        },
        "profile_id": 41440,
        "in_clusters": {},
        "out_clusters": {
          "0x0021": {
            "endpoint_attribute": "green_power",
            "attributes": {},
            "unsupported_attributes": {}
          }
        }
      }
    }
  }
}

According to #1602 (comment) its already fixed.

@javicalle
Copy link
Collaborator

javicalle commented Nov 22, 2022

Sorry for closing, but not, not fixed yet.

If you don't mind, go for a fresh start and focused just in your device.
I have recall all the post and it seems to be a tricky device. If I have read all the info, there must be a posible quirk but is possible that the stop button don't work.

Lets begin...

My proposed quirk:

ts0601_cover_2.py
"""Tuya MCU based cover and blinds."""
from typing import Dict, Optional, Union

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

from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
)
from zhaquirks.tuya import (
    NoManufacturerCluster,
    TUYA_MCU_COMMAND,
    TuyaLocalCluster,
    TuyaManufacturerWindowCover,
    TuyaManufCluster,
    TuyaWindowCover,
    TuyaWindowCoverControl,
)
from zhaquirks.tuya.mcu import (
    DPToAttributeMapping,
    TuyaClusterData,
    TuyaDPType,
    TuyaMCUCluster,
    TuyaOnOffNM,
)

# Maps OPEN/CLOSE/STOP cover commands from Tuya to Zigbee
# https://github.com/zigpy/zigpy/blob/master/zigpy/zcl/clusters/closures.py#L558
# https://developer.tuya.com/en/docs/iot-device-dev/zigbee-curtain-switch-access-standard?id=K9ik6zvra3twv#title-7-DP1%20and%20DP4%20Curtain%20switch%201%20and%202
TUYA2ZB_COMMANDS = {
    0x0000: 0x0000,
    0x0001: 0x0002,
    0x0002: 0x0001,
}


class TuyaWindowCovering(NoManufacturerCluster, WindowCovering, TuyaLocalCluster):
    """Tuya MCU WindowCovering cluster."""

    """Add additional attributes for direction"""
    attributes = WindowCovering.attributes.copy()
    attributes.update(
        {
            0xF000: ("curtain_switch", t.enum8, True),  # 0: open, 1: stop, 2: close
            0xF001: (
                "accurate_calibration",
                t.enum8,
                True,
            ),  # 0: calibration started, 1: calibration finished
            0xF002: ("motor_steering", t.enum8, True),  # 0: default, 1: reverse
            0xF003: ("travel", t.uint16_t, True),  # 30 to 9000 (units of 0.1 seconds)
        }
    )

    async def command(
        self,
        command_id: Union[foundation.GeneralCommand, int, t.uint8_t],
        *args,
        manufacturer: Optional[Union[int, t.uint16_t]] = None,
        expect_reply: bool = True,
        tsn: Optional[Union[int, t.uint8_t]] = None,
    ):
        """Override the default Cluster command."""

        # if manufacturer is None:
        #     manufacturer = self.endpoint.device.manufacturer

        self.debug(
            "Sending Tuya Cluster Command. Cluster Command is %x, Arguments are %s",
            command_id,
            args,
        )

        # (upopen, downclose, stop)
        if command_id in (0x0002):  # ¿0x0003: continue?
            cluster_data = TuyaClusterData(
                endpoint_id=self.endpoint.endpoint_id,
                cluster_attr="curtain_switch",
                attr_value=TUYA2ZB_COMMANDS[command_id],  # convert tuya2zigbee command
                expect_reply=expect_reply,
                manufacturer=manufacturer,
            )
            self.endpoint.device.command_bus.listener_event(
                TUYA_MCU_COMMAND,
                cluster_data,
            )
            return foundation.GENERAL_COMMANDS[
                foundation.GeneralCommand.Default_Response
            ].schema(command_id=command_id, status=foundation.Status.SUCCESS)

        # (go_to_lift_percentage)
        elif command_id in (0x0000, 0x0001, 0x0005):
            if command_id == 0x0000:
                lift_value = 100
            elif command_id == 0x0001:
                lift_value = 0
            elif command_id == 0x0005:
                lift_value = args[0]

            cluster_data = TuyaClusterData(
                endpoint_id=self.endpoint.endpoint_id,
                cluster_attr="current_position_lift_percentage",
                attr_value=lift_value,
                expect_reply=expect_reply,
                manufacturer=manufacturer,
            )
            self.endpoint.device.command_bus.listener_event(
                TUYA_MCU_COMMAND,
                cluster_data,
            )
            return foundation.GENERAL_COMMANDS[
                foundation.GeneralCommand.Default_Response
            ].schema(command_id=command_id, status=foundation.Status.SUCCESS)

        # # Custom Command
        # elif command_id == 0x0006:  # ¿doc reference?
        #     tuya_payload.status = args[0]
        #     tuya_payload.tsn = args[1]
        #     tuya_payload.command_id = args[2]
        #     tuya_payload.function = args[3]
        #     tuya_payload.data = args[4]

        self.warning("Unsupported command_id: %s", command_id)
        return foundation.GENERAL_COMMANDS[
            foundation.GeneralCommand.Default_Response
        ].schema(command_id=command_id, status=foundation.Status.UNSUP_CLUSTER_COMMAND)


class TuyaWindowCoverManufCluster(TuyaMCUCluster):
    """Tuya with WindowCover data points."""

    attributes = TuyaMCUCluster.attributes.copy()
    attributes.update(
        {
            0x5000: ("backlight_mode", t.enum8, True),  # 0: off, 1: on
            0x8001: (
                "indicator_status",
                t.enum8,
                True,
            ),  # 0: status, 1: position, 2: off (¿backlight_mode?)
        }
    )

    dp_to_attribute: Dict[int, DPToAttributeMapping] = {
        1: DPToAttributeMapping(
            TuyaWindowCovering.ep_attribute,
            "on_off",
            dp_type=TuyaDPType.ENUM,
        ),
        2: DPToAttributeMapping(
            TuyaWindowCovering.ep_attribute,
            "current_position_lift_percentage",
            dp_type=TuyaDPType.VALUE,
        ),
        3: DPToAttributeMapping(
            TuyaWindowCovering.ep_attribute,
            "accurate_calibration",
            dp_type=TuyaDPType.ENUM,
        ),
        4: DPToAttributeMapping(
            TuyaWindowCovering.ep_attribute,
            "on_off",
            dp_type=TuyaDPType.ENUM,
            endpoint_id=2,
        ),
        5: DPToAttributeMapping(
            TuyaWindowCovering.ep_attribute,
            "current_position_lift_percentage",
            dp_type=TuyaDPType.VALUE,
            endpoint_id=2,
        ),
        6: DPToAttributeMapping(
            TuyaWindowCovering.ep_attribute,
            "accurate_calibration",
            dp_type=TuyaDPType.ENUM,
            endpoint_id=2,
        ),
        7: DPToAttributeMapping(
            TuyaMCUCluster.ep_attribute,
            "backlight_mode",
            dp_type=TuyaDPType.ENUM,
        ),
        8: DPToAttributeMapping(
            TuyaWindowCovering.ep_attribute,
            "motor_steering",
            dp_type=TuyaDPType.ENUM,
        ),
        9: DPToAttributeMapping(
            TuyaWindowCovering.ep_attribute,
            "motor_steering",
            dp_type=TuyaDPType.ENUM,
            endpoint_id=2,
        ),
        10: DPToAttributeMapping(
            TuyaWindowCovering.ep_attribute,
            "quick_calibration",
            dp_type=TuyaDPType.ENUM,
        ),
        11: DPToAttributeMapping(
            TuyaWindowCovering.ep_attribute,
            "quick_calibration",
            dp_type=TuyaDPType.ENUM,
            endpoint_id=2,
        ),
        14: DPToAttributeMapping(
            TuyaMCUCluster.ep_attribute,
            "indicator_status",
            dp_type=TuyaDPType.ENUM,
        ),
    }

    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",
        6: "_dp_2_attr_update",
        7: "_dp_2_attr_update",
        8: "_dp_2_attr_update",
        9: "_dp_2_attr_update",
        10: "_dp_2_attr_update",
        11: "_dp_2_attr_update",
        14: "_dp_2_attr_update",
    }


class TuyaCover0601_GP(TuyaWindowCover):
    """Tuya blind controller device."""

    signature = {
        MODELS_INFO: [
            ("_TZE200_r0jdjrvi", "TS0601"),
        ],
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaWindowCoverManufCluster.cluster_id,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: {
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaWindowCoverManufCluster,
                    TuyaOnOffNM,
                    TuyaWindowCovering,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: {
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        }
    }

There is a good guide to create your local quirk:

Just create inside your local quirk folder a new file (ie: ts0601_cover_2.py) and put the code inside.

After any future changes to the quirk you will need to remove any __pycache__ folder, restart HA and repair the device.

I will try to help as much as I can, but it will take a lot of tests and a lot of patience (my time is limited).
It will also be necessary to activate the logs for the integration and collect the relevant information. Instructions here:

If you have a remote control for the cover, it will also be useful to obtain information on the commands.

One last request, please, put the logs and code examples between ``` marks like this:

```
Any code o logs requested
```

@caradas
Copy link
Author

caradas commented Nov 23, 2022

Thanks for your time and help :)
I have changed the if in line 88 to:

        # (upopen, downclose, stop)
        if command_id == (0x0002):  # ¿0x0003: continue?
            cluster_data = TuyaClusterData(

After that i noticed that the dasboard buttons aren't working.

Also shortened the log to the only thing thats happening while the motor is active:

Remote: Open + Stop:

2022-11-23 22:15:16.598 DEBUG (MainThread) [zigpy_znp.api] Received command: ZDO.SrcRtgInd.Callback(DstAddr=0xB562, Relays=[0x7929])
2022-11-23 22:15:16.604 DEBUG (MainThread) [zigpy_znp.api] Received command: AF.IncomingMsg.Callback(GroupId=0x0000, ClusterId=61184, SrcAddr=0xB562, SrcEndpoint=1, DstEndpoint=1, WasBroadcast=<Bool.false: 0>, LQI=49, SecurityUse=<Bool.false: 0>, TimeStamp=15684090, TSN=0, Data=b'\x19\xC0\x01\x00\x01\x03\x02\x00\x04\x00\x00\x00\x51', MacSrcAddr=0x7929, MsgResultRadius=28)
2022-11-23 22:15:16.605 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0xB562), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=0, profile_id=260, cluster_id=61184, data=Serialized[b'\x19\xc0\x01\x00\x01\x03\x02\x00\x04\x00\x00\x00Q'], tx_options=<TransmitOptions.NONE: 0>, radius=28, non_member_radius=0, lqi=49, rssi=None)
2022-11-23 22:15:16.605 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] Received ZCL frame: b'\x19\xc0\x01\x00\x01\x03\x02\x00\x04\x00\x00\x00Q'
2022-11-23 22:15:16.606 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=0, direction=<Direction.Client_to_Server: 1>, disable_default_response=1, reserved=0, *is_cluster=True, *is_general=False, *is_reply=True), tsn=192, command_id=1, *direction=<Direction.Client_to_Server: 1>, *is_reply=True)
2022-11-23 22:15:16.607 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] Decoded ZCL frame: TuyaWindowCoverManufCluster:get_data(data=TuyaCommand(status=0, tsn=1, datapoints=[TuyaDatapointData(dp=3, data=TuyaData(dp_type=<TuyaDPType.VALUE: 2>, function=0, raw=b'Q\x00\x00\x00', *payload=81))]))
2022-11-23 22:15:16.608 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] Received command 0x01 (TSN 192): get_data(data=TuyaCommand(status=0, tsn=1, datapoints=[TuyaDatapointData(dp=3, data=TuyaData(dp_type=<TuyaDPType.VALUE: 2>, function=0, raw=b'Q\x00\x00\x00', *payload=81))]))
2022-11-23 22:15:16.609 DEBUG (MainThread) [homeassistant.components.zha.core.channels.base] [0xB562:1:0x0102]: Attribute report 'Window Covering'[accurate_calibration] = 81

Remote Open without stop till fully open:

2022-11-23 22:18:30.401 DEBUG (MainThread) [zigpy_znp.api] Received command: ZDO.SrcRtgInd.Callback(DstAddr=0xB562, Relays=[0x7929])
2022-11-23 22:18:30.407 DEBUG (MainThread) [zigpy_znp.api] Received command: AF.IncomingMsg.Callback(GroupId=0x0000, ClusterId=61184, SrcAddr=0xB562, SrcEndpoint=1, DstEndpoint=1, WasBroadcast=<Bool.false: 0>, LQI=47, SecurityUse=<Bool.false: 0>, TimeStamp=16289752, TSN=0, Data=b'\x19\xC7\x01\x00\x01\x03\x02\x00\x04\x00\x00\x00\x00', MacSrcAddr=0x7929, MsgResultRadius=28)
2022-11-23 22:18:30.408 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0xB562), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=0, profile_id=260, cluster_id=61184, data=Serialized[b'\x19\xc7\x01\x00\x01\x03\x02\x00\x04\x00\x00\x00\x00'], tx_options=<TransmitOptions.NONE: 0>, radius=28, non_member_radius=0, lqi=47, rssi=None)
2022-11-23 22:18:30.408 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] Received ZCL frame: b'\x19\xc7\x01\x00\x01\x03\x02\x00\x04\x00\x00\x00\x00'
2022-11-23 22:18:30.409 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=0, direction=<Direction.Client_to_Server: 1>, disable_default_response=1, reserved=0, *is_cluster=True, *is_general=False, *is_reply=True), tsn=199, command_id=1, *direction=<Direction.Client_to_Server: 1>, *is_reply=True)
2022-11-23 22:18:30.409 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] Decoded ZCL frame: TuyaWindowCoverManufCluster:get_data(data=TuyaCommand(status=0, tsn=1, datapoints=[TuyaDatapointData(dp=3, data=TuyaData(dp_type=<TuyaDPType.VALUE: 2>, function=0, raw=b'\x00\x00\x00\x00', *payload=0))]))
2022-11-23 22:18:30.409 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] Received command 0x01 (TSN 199): get_data(data=TuyaCommand(status=0, tsn=1, datapoints=[TuyaDatapointData(dp=3, data=TuyaData(dp_type=<TuyaDPType.VALUE: 2>, function=0, raw=b'\x00\x00\x00\x00', *payload=0))]))
2022-11-23 22:18:30.410 DEBUG (MainThread) [homeassistant.components.zha.core.channels.base] [0xB562:1:0x0102]: Attribute report 'Window Covering'[accurate_calibration] = 0

Remote Close + Stop:

Open till completly open:
2022-11-23 22:18:30.401 DEBUG (MainThread) [zigpy_znp.api] Received command: ZDO.SrcRtgInd.Callback(DstAddr=0xB562, Relays=[0x7929])
2022-11-23 22:18:30.407 DEBUG (MainThread) [zigpy_znp.api] Received command: AF.IncomingMsg.Callback(GroupId=0x0000, ClusterId=61184, SrcAddr=0xB562, SrcEndpoint=1, DstEndpoint=1, WasBroadcast=<Bool.false: 0>, LQI=47, SecurityUse=<Bool.false: 0>, TimeStamp=16289752, TSN=0, Data=b'\x19\xC7\x01\x00\x01\x03\x02\x00\x04\x00\x00\x00\x00', MacSrcAddr=0x7929, MsgResultRadius=28)
2022-11-23 22:18:30.408 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0xB562), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=0, profile_id=260, cluster_id=61184, data=Serialized[b'\x19\xc7\x01\x00\x01\x03\x02\x00\x04\x00\x00\x00\x00'], tx_options=<TransmitOptions.NONE: 0>, radius=28, non_member_radius=0, lqi=47, rssi=None)
2022-11-23 22:18:30.408 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] Received ZCL frame: b'\x19\xc7\x01\x00\x01\x03\x02\x00\x04\x00\x00\x00\x00'
2022-11-23 22:18:30.409 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=0, direction=<Direction.Client_to_Server: 1>, disable_default_response=1, reserved=0, *is_cluster=True, *is_general=False, *is_reply=True), tsn=199, command_id=1, *direction=<Direction.Client_to_Server: 1>, *is_reply=True)
2022-11-23 22:18:30.409 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] Decoded ZCL frame: TuyaWindowCoverManufCluster:get_data(data=TuyaCommand(status=0, tsn=1, datapoints=[TuyaDatapointData(dp=3, data=TuyaData(dp_type=<TuyaDPType.VALUE: 2>, function=0, raw=b'\x00\x00\x00\x00', *payload=0))]))
2022-11-23 22:18:30.409 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] Received command 0x01 (TSN 199): get_data(data=TuyaCommand(status=0, tsn=1, datapoints=[TuyaDatapointData(dp=3, data=TuyaData(dp_type=<TuyaDPType.VALUE: 2>, function=0, raw=b'\x00\x00\x00\x00', *payload=0))]))
2022-11-23 22:18:30.410 DEBUG (MainThread) [homeassistant.components.zha.core.channels.base] [0xB562:1:0x0102]: Attribute report 'Window Covering'[accurate_calibration] = 0

Remote Close till fully closed:

2022-11-23 22:17:08.010 DEBUG (MainThread) [zigpy_znp.api] Received command: AF.IncomingMsg.Callback(GroupId=0x0000, ClusterId=61184, SrcAddr=0xB562, SrcEndpoint=1, DstEndpoint=1, WasBroadcast=<Bool.false: 0>, LQI=47, SecurityUse=<Bool.false: 0>, TimeStamp=16032248, TSN=0, Data=b'\x19\xC6\x01\x00\x01\x03\x02\x00\x04\x00\x00\x00\x64', MacSrcAddr=0x7929, MsgResultRadius=28)
2022-11-23 22:17:08.010 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0xB562), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=0, profile_id=260, cluster_id=61184, data=Serialized[b'\x19\xc6\x01\x00\x01\x03\x02\x00\x04\x00\x00\x00d'], tx_options=<TransmitOptions.NONE: 0>, radius=28, non_member_radius=0, lqi=47, rssi=None)
2022-11-23 22:17:08.010 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] Received ZCL frame: b'\x19\xc6\x01\x00\x01\x03\x02\x00\x04\x00\x00\x00d'
2022-11-23 22:17:08.011 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=0, direction=<Direction.Client_to_Server: 1>, disable_default_response=1, reserved=0, *is_cluster=True, *is_general=False, *is_reply=True), tsn=198, command_id=1, *direction=<Direction.Client_to_Server: 1>, *is_reply=True)
2022-11-23 22:17:08.011 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] Decoded ZCL frame: TuyaWindowCoverManufCluster:get_data(data=TuyaCommand(status=0, tsn=1, datapoints=[TuyaDatapointData(dp=3, data=TuyaData(dp_type=<TuyaDPType.VALUE: 2>, function=0, raw=b'd\x00\x00\x00', *payload=100))]))
2022-11-23 22:17:08.012 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] Received command 0x01 (TSN 198): get_data(data=TuyaCommand(status=0, tsn=1, datapoints=[TuyaDatapointData(dp=3, data=TuyaData(dp_type=<TuyaDPType.VALUE: 2>, function=0, raw=b'd\x00\x00\x00', *payload=100))]))
2022-11-23 22:17:08.012 DEBUG (MainThread) [homeassistant.components.zha.core.channels.base] [0xB562:1:0x0102]: Attribute report 'Window Covering'[accurate_calibration] = 100

Dashboard up button:
Motor was not turning here

2022-11-23 22:36:38.857 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0x0102] Sending Tuya Cluster Command. Cluster Command is 0, Arguments are ()
2022-11-23 22:36:38.858 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] tuya_mcu_command: cluster_data=TuyaClusterData(endpoint_id=1, cluster_attr='current_position_lift_percentage', attr_value=100, expect_reply=True)
2022-11-23 22:36:38.858 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] get_dp_mapping --> found DP: 2
2022-11-23 22:36:38.858 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] from_cluster_data: 2, DPToAttributeMapping(ep_attribute='window_covering', attribute_name='current_position_lift_percentage', dp_type=<TuyaDPType.VALUE: 2>, converter=None, dp_converter=None, endpoint_id=None)
2022-11-23 22:36:38.858 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] ztype: 100
2022-11-23 22:36:38.858 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] from_value: [4, 0, 0, 0, 100]
2022-11-23 22:36:38.858 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] raw: b'\x00\x00\x00d'
2022-11-23 22:36:38.858 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] tuya_command: TuyaCommand(status=0, tsn=224, datapoints=[TuyaDatapointData(dp=2, data=TuyaData(dp_type=<TuyaDPType.VALUE: 2>, function=0, raw=b'\x00\x00\x00d', *payload=1677721600))])
2022-11-23 22:36:38.859 DEBUG (MainThread) [homeassistant.components.zha.core.channels.base] [0xB562:1:0x0102]: executed 'up_open' command with args: '()' kwargs: '{}' result: Default_Response(command_id=0, status=<Status.SUCCESS: 0>)
2022-11-23 22:36:38.859 DEBUG (MainThread) [homeassistant.components.zha.cover] state=opening
2022-11-23 22:36:38.860 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] Sending request header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=True, direction=<Direction.Server_to_Client: 0>, disable_default_response=0, reserved=0, *is_cluster=True, *is_general=False, *is_reply=False), manufacturer=4417, tsn=225, command_id=0, *direction=<Direction.Server_to_Client: 0>, *is_reply=False)
2022-11-23 22:36:38.860 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] Sending request: set_data(data=TuyaCommand(status=0, tsn=224, datapoints=[TuyaDatapointData(dp=2, data=TuyaData(dp_type=<TuyaDPType.VALUE: 2>, function=0, raw=b'\x00\x00\x00d', *payload=1677721600))]))
2022-11-23 22:36:38.860 DEBUG (MainThread) [zigpy_znp.zigbee.application] Sending packet ZigbeePacket(src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0xB562), dst_ep=1, source_route=None, extended_timeout=False, tsn=225, profile_id=260, cluster_id=61184, data=Serialized[b'\x05A\x11\xe1\x00\x00\xe0\x02\x02\x00\x04\x00\x00\x00d'], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=None, rssi=None)
2022-11-23 22:36:38.862 DEBUG (MainThread) [zigpy_znp.api] Sending request: AF.DataRequestExt.Req(DstAddrModeAddress=AddrModeAddress(mode=<AddrMode.NWK: 2>, address=0xB562), DstEndpoint=1, DstPanId=0x0000, SrcEndpoint=1, ClusterId=61184, TSN=225, Options=<TransmitOptions.SUPPRESS_ROUTE_DISC_NETWORK: 32>, Radius=0, Data=b'\x05\x41\x11\xE1\x00\x00\xE0\x02\x02\x00\x04\x00\x00\x00\x64')
2022-11-23 22:36:38.875 DEBUG (MainThread) [zigpy_znp.api] Received command: AF.DataRequestExt.Rsp(Status=<Status.SUCCESS: 0>)
2022-11-23 22:36:38.881 DEBUG (MainThread) [zigpy_znp.api] Received command: AF.DataConfirm.Callback(Status=<Status.SUCCESS: 0>, Endpoint=1, TSN=225)
2022-11-23 22:36:38.902 DEBUG (MainThread) [zigpy_znp.api] Received command: AF.IncomingMsg.Callback(GroupId=0x0000, ClusterId=61184, SrcAddr=0xB562, SrcEndpoint=1, DstEndpoint=1, WasBroadcast=<Bool.false: 0>, LQI=47, SecurityUse=<Bool.false: 0>, TimeStamp=2979757, TSN=0, Data=b'\x18\xE1\x0B\x00\x83', MacSrcAddr=0x7929, MsgResultRadius=28)
2022-11-23 22:36:38.902 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0xB562), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=0, profile_id=260, cluster_id=61184, data=Serialized[b'\x18\xe1\x0b\x00\x83'], tx_options=<TransmitOptions.NONE: 0>, radius=28, non_member_radius=0, lqi=47, rssi=None)
2022-11-23 22:36:38.902 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] Received ZCL frame: b'\x18\xe1\x0b\x00\x83'
2022-11-23 22:36:38.903 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.GLOBAL_COMMAND: 0>, is_manufacturer_specific=0, direction=<Direction.Client_to_Server: 1>, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True, *is_reply=True), tsn=225, command_id=11, *direction=<Direction.Client_to_Server: 1>, *is_reply=True)
2022-11-23 22:36:38.903 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] Decoded ZCL frame: TuyaWindowCoverManufCluster:Default_Response(command_id=0, status=<Status.UNSUP_MANUF_CLUSTER_COMMAND: 131>)

Dasboard down button:
Motor was not turning here

2022-11-23 22:35:24.700 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0x0102] Sending Tuya Cluster Command. Cluster Command is 1, Arguments are ()
2022-11-23 22:35:24.700 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] tuya_mcu_command: cluster_data=TuyaClusterData(endpoint_id=1, cluster_attr='current_position_lift_percentage', attr_value=0, expect_reply=True)
2022-11-23 22:35:24.700 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] get_dp_mapping --> found DP: 2
2022-11-23 22:35:24.700 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] from_cluster_data: 2, DPToAttributeMapping(ep_attribute='window_covering', attribute_name='current_position_lift_percentage', dp_type=<TuyaDPType.VALUE: 2>, converter=None, dp_converter=None, endpoint_id=None)
2022-11-23 22:35:24.701 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] ztype: 0
2022-11-23 22:35:24.701 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] from_value: [4, 0, 0, 0, 0]
2022-11-23 22:35:24.701 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] raw: b'\x00\x00\x00\x00'
2022-11-23 22:35:24.701 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] tuya_command: TuyaCommand(status=0, tsn=222, datapoints=[TuyaDatapointData(dp=2, data=TuyaData(dp_type=<TuyaDPType.VALUE: 2>, function=0, raw=b'\x00\x00\x00\x00', *payload=0))])
2022-11-23 22:35:24.701 DEBUG (MainThread) [homeassistant.components.zha.core.channels.base] [0xB562:1:0x0102]: executed 'down_close' command with args: '()' kwargs: '{}' result: Default_Response(command_id=1, status=<Status.SUCCESS: 0>)
2022-11-23 22:35:24.701 DEBUG (MainThread) [homeassistant.components.zha.cover] state=closing
2022-11-23 22:35:24.702 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] Sending request header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=True, direction=<Direction.Server_to_Client: 0>, disable_default_response=0, reserved=0, *is_cluster=True, *is_general=False, *is_reply=False), manufacturer=4417, tsn=223, command_id=0, *direction=<Direction.Server_to_Client: 0>, *is_reply=False)
2022-11-23 22:35:24.702 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] Sending request: set_data(data=TuyaCommand(status=0, tsn=222, datapoints=[TuyaDatapointData(dp=2, data=TuyaData(dp_type=<TuyaDPType.VALUE: 2>, function=0, raw=b'\x00\x00\x00\x00', *payload=0))]))
2022-11-23 22:35:24.703 DEBUG (MainThread) [zigpy_znp.zigbee.application] Sending packet ZigbeePacket(src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0xB562), dst_ep=1, source_route=None, extended_timeout=False, tsn=223, profile_id=260, cluster_id=61184, data=Serialized[b'\x05A\x11\xdf\x00\x00\xde\x02\x02\x00\x04\x00\x00\x00\x00'], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=None, rssi=None)
2022-11-23 22:35:24.708 DEBUG (MainThread) [zigpy_znp.api] Sending request: AF.DataRequestExt.Req(DstAddrModeAddress=AddrModeAddress(mode=<AddrMode.NWK: 2>, address=0xB562), DstEndpoint=1, DstPanId=0x0000, SrcEndpoint=1, ClusterId=61184, TSN=223, Options=<TransmitOptions.SUPPRESS_ROUTE_DISC_NETWORK: 32>, Radius=0, Data=b'\x05\x41\x11\xDF\x00\x00\xDE\x02\x02\x00\x04\x00\x00\x00\x00')
2022-11-23 22:35:24.721 DEBUG (MainThread) [zigpy_znp.api] Received command: AF.DataRequestExt.Rsp(Status=<Status.SUCCESS: 0>)
2022-11-23 22:35:24.727 DEBUG (MainThread) [zigpy_znp.api] Received command: AF.DataConfirm.Callback(Status=<Status.SUCCESS: 0>, Endpoint=1, TSN=223)
2022-11-23 22:35:24.755 DEBUG (MainThread) [zigpy_znp.api] Received command: ZDO.SrcRtgInd.Callback(DstAddr=0xB562, Relays=[0x7929])
2022-11-23 22:35:24.761 DEBUG (MainThread) [zigpy_znp.api] Received command: AF.IncomingMsg.Callback(GroupId=0x0000, ClusterId=61184, SrcAddr=0xB562, SrcEndpoint=1, DstEndpoint=1, WasBroadcast=<Bool.false: 0>, LQI=47, SecurityUse=<Bool.false: 0>, TimeStamp=2748057, TSN=0, Data=b'\x18\xDF\x0B\x00\x83', MacSrcAddr=0x7929, MsgResultRadius=28)
2022-11-23 22:35:24.762 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0xB562), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=0, profile_id=260, cluster_id=61184, data=Serialized[b'\x18\xdf\x0b\x00\x83'], tx_options=<TransmitOptions.NONE: 0>, radius=28, non_member_radius=0, lqi=47, rssi=None)
2022-11-23 22:35:24.762 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] Received ZCL frame: b'\x18\xdf\x0b\x00\x83'
2022-11-23 22:35:24.762 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.GLOBAL_COMMAND: 0>, is_manufacturer_specific=0, direction=<Direction.Client_to_Server: 1>, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True, *is_reply=True), tsn=223, command_id=11, *direction=<Direction.Client_to_Server: 1>, *is_reply=True)
2022-11-23 22:35:24.763 DEBUG (MainThread) [zigpy.zcl] [0xB562:1:0xef00] Decoded ZCL frame: TuyaWindowCoverManufCluster:Default_Response(command_id=0, status=<Status.UNSUP_MANUF_CLUSTER_COMMAND: 131>)

@javicalle
Copy link
Collaborator

javicalle commented Nov 24, 2022

Sorry but this seems to be a completely diferent cover from the quirk. The previous quirk was for a 2 cover device.
I would need the full device description (manufacturer/vendor link).

From your logs I would say that cover reports the status position in some way. I would expect to have one report from every remote command but I only can see one from your logs.

My assumption is that the device will report the 'target status' when pushing the open/close buttons and the stop button will report the current position if stopped halfway. That could lead to a strange behavior in HA, but we'll see about that later.

Let's try with a 'classical' approach. Replace completely the local quirk with this one:

ts0601_cover_2.py
"""Tuya based cover and blinds."""
from zigpy.profiles import zha
from zigpy.zcl.clusters.general import Basic, GreenPowerProxy, Groups, Ota, Scenes, Time

from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
)
from zhaquirks.tuya import (
    TuyaManufacturerWindowCover,
    TuyaManufCluster,
    TuyaWindowCover,
    TuyaWindowCoverControl,
)


class TuyaCover0601_GP(TuyaWindowCover):
    """Tuya cover controller device."""

    signature = {
        MODELS_INFO: [
            ("_TZE200_r0jdjrvi", "TS0601"),
        ],
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaManufCluster.cluster_id,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: {
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaManufacturerWindowCover,
                    TuyaWindowCoverControl,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            }
        }
    }

Save changes, delete any __pycache__ folder, restart HA and repair the device.
Double check if the device report the button press o just when it stop moving. Maybe is a RF remote?
Look for any other reports from the device to see if there are other device attributes there.

@caradas
Copy link
Author

caradas commented Nov 24, 2022

It is this device: https://www.aliexpress.com/item/1005003796260309.html As EU Plug, Motor only variant.
The datasheet says something like "Frequency 2.4G/433Mhz" so yes the remote should be rf, this also explains the Wire antenna hanging out of the device. It also says Product model "CMD900E"
Any log output with the remote happens after the curtains stops regardless if stopped by the remote or track ends.

Pressing the stop button on the remote without the curtain moving no log output is generated.

Log for Stop in HA:


2022-11-24 21:50:48.744 DEBUG (MainThread) [zhaquirks.tuya] a4:c1:38:44:24:da:54:d3 Sending Tuya Cluster Command.. Manufacturer is _TZE200_r0jdjrvi Cluster Command is 0x0002, Arguments are ()
2022-11-24 21:50:48.745 DEBUG (MainThread) [zhaquirks.tuya] a4:c1:38:44:24:da:54:d3 Sending Tuya Command. Paylod values [endpoint_id : 1, Status : 0, TSN: 0, Command: 0x0401, Function: 0, Data: [1, 1]]
2022-11-24 21:50:48.745 DEBUG (MainThread) [zigpy.zcl] [0x6A13:1:0xef00] Sending request header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=True, direction=<Direction.Server_to_Client: 0>, disable_default_response=0, reserved=0, *is_cluster=True, *is_general=False, *is_reply=False), manufacturer=4417, tsn=118, command_id=0, *direction=<Direction.Server_to_Client: 0>, *is_reply=False)
2022-11-24 21:50:48.745 DEBUG (MainThread) [zigpy.zcl] [0x6A13:1:0xef00] Sending request: set_data(param=Command(status=0, tsn=0, command_id=1025, function=0, data=[1, 1]))
2022-11-24 21:50:48.746 DEBUG (MainThread) [zigpy_znp.zigbee.application] Sending packet ZigbeePacket(src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x6A13), dst_ep=1, source_route=None, extended_timeout=False, tsn=118, profile_id=260, cluster_id=61184, data=Serialized[b'\x05A\x11v\x00\x00\x00\x01\x04\x00\x01\x01'], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=None, rssi=None)
2022-11-24 21:50:48.747 DEBUG (MainThread) [zigpy_znp.api] Sending request: AF.DataRequestExt.Req(DstAddrModeAddress=AddrModeAddress(mode=<AddrMode.NWK: 2>, address=0x6A13), DstEndpoint=1, DstPanId=0x0000, SrcEndpoint=1, ClusterId=61184, TSN=118, Options=<TransmitOptions.SUPPRESS_ROUTE_DISC_NETWORK: 32>, Radius=0, Data=b'\x05\x41\x11\x76\x00\x00\x00\x01\x04\x00\x01\x01')
2022-11-24 21:50:48.760 DEBUG (MainThread) [zigpy_znp.api] Received command: AF.DataRequestExt.Rsp(Status=<Status.SUCCESS: 0>)
2022-11-24 21:50:48.765 DEBUG (MainThread) [zigpy_znp.api] Received command: AF.DataConfirm.Callback(Status=<Status.SUCCESS: 0>, Endpoint=1, TSN=118)
2022-11-24 21:50:48.786 DEBUG (MainThread) [zigpy_znp.api] Received command: AF.IncomingMsg.Callback(GroupId=0x0000, ClusterId=61184, SrcAddr=0x6A13, SrcEndpoint=1, DstEndpoint=1, WasBroadcast=<Bool.false: 0>, LQI=39, SecurityUse=<Bool.false: 0>, TimeStamp=3669657, TSN=0, Data=b'\x18\x76\x0B\x00\x83', MacSrcAddr=0x7929, MsgResultRadius=28)
2022-11-24 21:50:48.786 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x6A13), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=0, profile_id=260, cluster_id=61184, data=Serialized[b'\x18v\x0b\x00\x83'], tx_options=<TransmitOptions.NONE: 0>, radius=28, non_member_radius=0, lqi=39, rssi=None)
2022-11-24 21:50:48.786 DEBUG (MainThread) [zigpy.zcl] [0x6A13:1:0xef00] Received ZCL frame: b'\x18v\x0b\x00\x83'
2022-11-24 21:50:48.786 DEBUG (MainThread) [zigpy.zcl] [0x6A13:1:0xef00] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.GLOBAL_COMMAND: 0>, is_manufacturer_specific=0, direction=<Direction.Client_to_Server: 1>, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True, *is_reply=True), tsn=118, command_id=11, *direction=<Direction.Client_to_Server: 1>, *is_reply=True)
2022-11-24 21:50:48.787 DEBUG (MainThread) [zigpy.zcl] [0x6A13:1:0xef00] Decoded ZCL frame: TuyaManufacturerWindowCover:Default_Response(command_id=0, status=<Status.UNSUP_MANUF_CLUSTER_COMMAND: 131>)

Log for UP in HA:

2022-11-24 21:53:42.314 DEBUG (MainThread) [zigpy_znp.api] Received command: AF.IncomingMsg.Callback(GroupId=0x0000, ClusterId=1794, SrcAddr=0x2AD3, SrcEndpoint=1, DstEndpoint=1, WasBroadcast=<Bool.false: 0>, LQI=97, SecurityUse=<Bool.false: 0>, TimeStamp=4211952, TSN=0, Data=b'\x08\x22\x0A\x00\x00\x25\x34\x0A\x00\x00\x00\x00', MacSrcAddr=0x2AD3, MsgResultRadius=29)
2022-11-24 21:53:42.314 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x2AD3), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=0, profile_id=260, cluster_id=1794, data=Serialized[b'\x08"\n\x00\x00%4\n\x00\x00\x00\x00'], tx_options=<TransmitOptions.NONE: 0>, radius=29, non_member_radius=0, lqi=97, rssi=None)
2022-11-24 21:53:42.315 DEBUG (MainThread) [zigpy.zcl] [0x2AD3:1:0x0702] Received ZCL frame: b'\x08"\n\x00\x00%4\n\x00\x00\x00\x00'
2022-11-24 21:53:42.315 DEBUG (MainThread) [zigpy.zcl] [0x2AD3:1:0x0702] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.GLOBAL_COMMAND: 0>, is_manufacturer_specific=0, direction=<Direction.Client_to_Server: 1>, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True, *is_reply=True), tsn=34, command_id=10, *direction=<Direction.Client_to_Server: 1>, *is_reply=True)
2022-11-24 21:53:42.316 DEBUG (MainThread) [zigpy.zcl] [0x2AD3:1:0x0702] Decoded ZCL frame: TuyaZBMeteringCluster:Report_Attributes(attribute_reports=[Attribute(attrid=0x0000, value=TypeValue(type=uint48_t, value=2612))])
2022-11-24 21:53:42.316 DEBUG (MainThread) [zigpy.zcl] [0x2AD3:1:0x0702] Received command 0x0A (TSN 34): Report_Attributes(attribute_reports=[Attribute(attrid=0x0000, value=TypeValue(type=uint48_t, value=2612))])
2022-11-24 21:53:42.316 DEBUG (MainThread) [zigpy.zcl] [0x2AD3:1:0x0702] Attribute report received: current_summ_delivered=2612
2022-11-24 21:53:42.320 DEBUG (MainThread) [zigpy.zcl] [0x2AD3:1:0x0702] Sending reply header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.GLOBAL_COMMAND: 0>, is_manufacturer_specific=False, direction=<Direction.Client_to_Server: 1>, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True, *is_reply=True), tsn=34, command_id=<GeneralCommand.Default_Response: 11>, *direction=<Direction.Client_to_Server: 1>, *is_reply=True)
2022-11-24 21:53:42.321 DEBUG (MainThread) [zigpy.zcl] [0x2AD3:1:0x0702] Sending reply: Default_Response(command_id=10, status=<Status.SUCCESS: 0>)
2022-11-24 21:53:42.321 DEBUG (MainThread) [zigpy_znp.zigbee.application] Sending packet ZigbeePacket(src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x2AD3), dst_ep=1, source_route=None, extended_timeout=False, tsn=34, profile_id=260, cluster_id=1794, data=Serialized[b'\x18"\x0b\n\x00'], tx_options=<TransmitOptions.ACK: 1>, radius=0, non_member_radius=0, lqi=None, rssi=None)
2022-11-24 21:53:42.322 DEBUG (MainThread) [zigpy_znp.api] Sending request: AF.DataRequestExt.Req(DstAddrModeAddress=AddrModeAddress(mode=<AddrMode.NWK: 2>, address=0x2AD3), DstEndpoint=1, DstPanId=0x0000, SrcEndpoint=1, ClusterId=1794, TSN=34, Options=<TransmitOptions.SUPPRESS_ROUTE_DISC_NETWORK|ACK_REQUEST: 48>, Radius=0, Data=b'\x18\x22\x0B\x0A\x00')
2022-11-24 21:53:42.331 DEBUG (MainThread) [zigpy_znp.api] Received command: AF.DataRequestExt.Rsp(Status=<Status.SUCCESS: 0>)
2022-11-24 21:53:42.358 DEBUG (MainThread) [zigpy_znp.api] Received command: AF.DataConfirm.Callback(Status=<Status.SUCCESS: 0>, Endpoint=1, TSN=34)
2022-11-24 21:53:42.406 DEBUG (MainThread) [zhaquirks.tuya] a4:c1:38:44:24:da:54:d3 Sending Tuya Cluster Command.. Manufacturer is _TZE200_r0jdjrvi Cluster Command is 0x0000, Arguments are ()
2022-11-24 21:53:42.406 DEBUG (MainThread) [zhaquirks.tuya] a4:c1:38:44:24:da:54:d3 Sending Tuya Command. Paylod values [endpoint_id : 1, Status : 0, TSN: 0, Command: 0x0401, Function: 0, Data: [1, 0]]
2022-11-24 21:53:42.407 DEBUG (MainThread) [zigpy.zcl] [0x6A13:1:0xef00] Sending request header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=True, direction=<Direction.Server_to_Client: 0>, disable_default_response=0, reserved=0, *is_cluster=True, *is_general=False, *is_reply=False), manufacturer=4417, tsn=131, command_id=0, *direction=<Direction.Server_to_Client: 0>, *is_reply=False)

@RowCZ
Copy link

RowCZ commented Nov 27, 2022

Hi @javicalle,
I also have problem with my cover motor with same designation. :-/
I would love to assist to help you find the solution but because I am not exactly good at this I would appreciate any instruction what you need and how can I help.
I was able to connect it to ZHA, I am catching LQI a RSSI Diagnostics but that is all.
I have latest HA 2022.11.4 and with active built in quirks.
Here is at least what I was able to dig from diagnostics about this device

  "data": {
    "ieee": "**REDACTED**",
    "nwk": 204,
    "manufacturer": "_TZE200_r0jdjrvi",
    "model": "TS0601",
    "name": "_TZE200_r0jdjrvi TS0601",
    "quirk_applied": false,
    "quirk_class": "zigpy.device.Device",
    "manufacturer_code": 4417,
    "power_source": "Mains",
    "lqi": 180,
    "rssi": -55,
    "last_seen": "2022-11-27T00:31:18",
    "available": true,
    "device_type": "Router",
    "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.AllocateAddress|RxOnWhenIdle|MainsPowered|FullFunctionDevice: 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": 260,
          "device_type": "0x0051",
          "in_clusters": [
            "0x0000",
            "0x0004",
            "0x0005",
            "0xef00"
          ],
          "out_clusters": [
            "0x000a",
            "0x0019"
          ]
        },
        "242": {
          "profile_id": 41440,
          "device_type": "0x0061",
          "in_clusters": [],
          "out_clusters": [
            "0x0021"
          ]
        }
      }
    },
    "active_coordinator": false,
    "entities": [
      {
        "entity_id": "sensor.tze200_r0jdjrvi_ts0601_rssi",
        "name": "_TZE200_r0jdjrvi TS0601"
      },
      {
        "entity_id": "sensor.tze200_r0jdjrvi_ts0601_lqi",
        "name": "_TZE200_r0jdjrvi TS0601"
      }
    ],
    "neighbors": [
      {
        "device_type": "Coordinator",
        "rx_on_when_idle": "On",
        "relationship": "Sibling",
        "extended_pan_id": "**REDACTED**",
        "ieee": "**REDACTED**",
        "nwk": "0x0000",
        "permit_joining": "Unknown",
        "depth": "0",
        "lqi": "66"
      }
    ],
    "endpoint_names": [
      {
        "name": "SMART_PLUG"
      },
      {
        "name": "unknown 97 device_type of 0xa1e0 profile id"
      }
    ],
    "user_given_name": null,
    "device_reg_id": "9fd13e33b8e356d821e1b46cde802803",
    "area_id": "loznice",
    "cluster_details": {
      "1": {
        "device_type": {
          "name": "SMART_PLUG",
          "id": 81
        },
        "profile_id": 260,
        "in_clusters": {
          "0x0004": {
            "endpoint_attribute": "groups",
            "attributes": {},
            "unsupported_attributes": {
              "0xfffe": {
                "attribute_name": "attr_reporting_status"
              }
            }
          },
          "0x0005": {
            "endpoint_attribute": "scenes",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0xef00": {
            "endpoint_attribute": null,
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x0000": {
            "endpoint_attribute": "basic",
            "attributes": {
              "0x0001": {
                "attribute_name": "app_version",
                "value": 70
              },
              "0x0004": {
                "attribute_name": "manufacturer",
                "value": "_TZE200_r0jdjrvi"
              },
              "0x0005": {
                "attribute_name": "model",
                "value": "TS0601"
              }
            },
            "unsupported_attributes": {}
          }
        },
        "out_clusters": {
          "0x0019": {
            "endpoint_attribute": "ota",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x000a": {
            "endpoint_attribute": "time",
            "attributes": {},
            "unsupported_attributes": {}
          }
        }
      },
      "242": {
        "device_type": {
          "name": "unknown",
          "id": 97
        },
        "profile_id": 41440,
        "in_clusters": {},
        "out_clusters": {
          "0x0021": {
            "endpoint_attribute": "green_power",
            "attributes": {},
            "unsupported_attributes": {}
          }
        }
      }
    }

With this custom quirk active #1953 (comment)
I at least have open close and stop buttons active but they do nothing. I also have an inactive Switch.

And with this one #1953 (comment)
I have same controls active as above but on a press of any of them I get "Failed to call service cover/open_cover. argument of type 'int' is not iterable" notification in HA.
I also have active switch in OFF position and when I turn it ON, nothing happens and after few seconds it returns to OFF position. But when I turn it ON and OFF again (before it returns itself), motor starts spin in clockwice direction and it runs until some kind of timeout. But when it runs and I switch it to ON position again it stops. It also stops when I start to run motor manually (making few turns in one or another direction, it react to external movement of curtain and continues that direction) and switch it to ON position.
But even if I am able to do something, there are still no records in device Logbook, only "No logbook events found." message.
So in this case, "switch ON" is probably command to stop and "switch OFF" is probably command to open or close curtains and this quirk is close on its way to be functional. (I am almost able to make workaround in node-red :D but still missing second direction :( )

I also setup ZHA logging you mentioned before and tried to check my log but it is pretty overcrowded since I have dozens of devices connected to HA so if you specify which part is interresting for you I will try to find it or be able at least to clean it a little bit.

Thanks a lot for all your work.

@javicalle
Copy link
Collaborator

Probably we need to map the motion commands.
You can look in the logs for your device NMK. It will show as [0x00cc]

@javicalle
Copy link
Collaborator

javicalle commented Nov 27, 2022

After checking it, I will return to the new implementation, so please use this quirk to test:

ts0601_cover_2.py
"""Tuya MCU based cover and blinds."""
from typing import Dict, Optional, Union

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

from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
)
from zhaquirks.tuya import (
    NoManufacturerCluster,
    TUYA_MCU_COMMAND,
    TuyaLocalCluster,
    TuyaManufacturerWindowCover,
    TuyaManufCluster,
    TuyaWindowCover,
    TuyaWindowCoverControl,
)
from zhaquirks.tuya.mcu import (
    DPToAttributeMapping,
    TuyaClusterData,
    TuyaDPType,
    TuyaMCUCluster,
    TuyaOnOffNM,
)

# Maps OPEN/CLOSE/STOP cover commands from Tuya to Zigbee
# https://github.com/zigpy/zigpy/blob/master/zigpy/zcl/clusters/closures.py#L558
# https://developer.tuya.com/en/docs/iot-device-dev/zigbee-curtain-switch-access-standard?id=K9ik6zvra3twv#title-7-DP1%20and%20DP4%20Curtain%20switch%201%20and%202
TUYA2ZB_COMMANDS = {
    0x0000: 0x0000,
    0x0001: 0x0002,
    0x0002: 0x0001,
}


class TuyaWindowCovering(NoManufacturerCluster, WindowCovering, TuyaLocalCluster):
    """Tuya MCU WindowCovering cluster."""

    """Add additional attributes for direction"""
    attributes = WindowCovering.attributes.copy()
    attributes.update(
        {
            0xF000: ("curtain_switch", t.enum8, True),  # 0: open, 1: stop, 2: close
            0xF001: (
                "accurate_calibration",
                t.enum8,
                True,
            ),  # 0: calibration started, 1: calibration finished
            0xF002: ("motor_steering", t.enum8, True),  # 0: default, 1: reverse
            0xF003: ("travel", t.uint16_t, True),  # 30 to 9000 (units of 0.1 seconds)
        }
    )

    async def command(
        self,
        command_id: Union[foundation.GeneralCommand, int, t.uint8_t],
        *args,
        manufacturer: Optional[Union[int, t.uint16_t]] = None,
        expect_reply: bool = True,
        tsn: Optional[Union[int, t.uint8_t]] = None,
    ):
        """Override the default Cluster command."""

        # if manufacturer is None:
        #     manufacturer = self.endpoint.device.manufacturer

        self.debug(
            "Sending Tuya Cluster Command. Cluster Command is %x, Arguments are %s",
            command_id,
            args,
        )

        # (upopen, downclose, stop)
        if command_id == 0x0002:  # ¿0x0003: continue?
            cluster_data = TuyaClusterData(
                endpoint_id=self.endpoint.endpoint_id,
                cluster_attr="curtain_switch",
                attr_value=TUYA2ZB_COMMANDS[command_id],  # convert tuya2zigbee command
                expect_reply=expect_reply,
                manufacturer=manufacturer,
            )
            self.endpoint.device.command_bus.listener_event(
                TUYA_MCU_COMMAND,
                cluster_data,
            )
            return foundation.GENERAL_COMMANDS[
                foundation.GeneralCommand.Default_Response
            ].schema(command_id=command_id, status=foundation.Status.SUCCESS)

        # (go_to_lift_percentage)
        elif command_id in (0x0000, 0x0001, 0x0005):
            if command_id == 0x0000:
                lift_value = 100
            elif command_id == 0x0001:
                lift_value = 0
            elif command_id == 0x0005:
                lift_value = args[0]

            cluster_data = TuyaClusterData(
                endpoint_id=self.endpoint.endpoint_id,
                cluster_attr="current_position_lift_percentage",
                attr_value=lift_value,
                expect_reply=expect_reply,
                manufacturer=manufacturer,
            )
            self.endpoint.device.command_bus.listener_event(
                TUYA_MCU_COMMAND,
                cluster_data,
            )
            return foundation.GENERAL_COMMANDS[
                foundation.GeneralCommand.Default_Response
            ].schema(command_id=command_id, status=foundation.Status.SUCCESS)

        # # Custom Command
        # elif command_id == 0x0006:  # ¿doc reference?
        #     tuya_payload.status = args[0]
        #     tuya_payload.tsn = args[1]
        #     tuya_payload.command_id = args[2]
        #     tuya_payload.function = args[3]
        #     tuya_payload.data = args[4]

        self.warning("Unsupported command_id: %s", command_id)
        return foundation.GENERAL_COMMANDS[
            foundation.GeneralCommand.Default_Response
        ].schema(command_id=command_id, status=foundation.Status.UNSUP_CLUSTER_COMMAND)


class TuyaWindowCoverManufCluster(TuyaMCUCluster):
    """Tuya with WindowCover data points."""

    attributes = TuyaMCUCluster.attributes.copy()
    attributes.update(
        {
            0x5000: ("backlight_mode", t.enum8, True),  # 0: off, 1: on
            0x8001: (
                "indicator_status",
                t.enum8,
                True,
            ),  # 0: status, 1: position, 2: off (¿backlight_mode?)
        }
    )

    dp_to_attribute: Dict[int, DPToAttributeMapping] = {
        1: DPToAttributeMapping(
            TuyaWindowCovering.ep_attribute,
            "on_off",
            dp_type=TuyaDPType.ENUM,
        ),
        3: DPToAttributeMapping(
            TuyaWindowCovering.ep_attribute,
            "current_position_lift_percentage",
            dp_type=TuyaDPType.VALUE,
        ),
        # 3: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "accurate_calibration",
        #     dp_type=TuyaDPType.ENUM,
        # ),
        # 4: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "on_off",
        #     dp_type=TuyaDPType.ENUM,
        #     endpoint_id=2,
        # ),
        # 5: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "current_position_lift_percentage",
        #     dp_type=TuyaDPType.VALUE,
        #     endpoint_id=2,
        # ),
        # 6: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "accurate_calibration",
        #     dp_type=TuyaDPType.ENUM,
        #     endpoint_id=2,
        # ),
        # 7: DPToAttributeMapping(
        #     TuyaMCUCluster.ep_attribute,
        #     "backlight_mode",
        #     dp_type=TuyaDPType.ENUM,
        # ),
        # 8: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "motor_steering",
        #     dp_type=TuyaDPType.ENUM,
        # ),
        # 9: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "motor_steering",
        #     dp_type=TuyaDPType.ENUM,
        #     endpoint_id=2,
        # ),
        # 10: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "quick_calibration",
        #     dp_type=TuyaDPType.ENUM,
        # ),
        # 11: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "quick_calibration",
        #     dp_type=TuyaDPType.ENUM,
        #     endpoint_id=2,
        # ),
        # 14: DPToAttributeMapping(
        #     TuyaMCUCluster.ep_attribute,
        #     "indicator_status",
        #     dp_type=TuyaDPType.ENUM,
        # ),
    }

    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",
        6: "_dp_2_attr_update",
        7: "_dp_2_attr_update",
        8: "_dp_2_attr_update",
        9: "_dp_2_attr_update",
        10: "_dp_2_attr_update",
        11: "_dp_2_attr_update",
        14: "_dp_2_attr_update",
    }


class TuyaCover0601_GP(TuyaWindowCover):
    """Tuya blind controller device."""

    signature = {
        # "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.AllocateAddress|RxOnWhenIdle|MainsPowered|FullFunctionDevice: 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
        # )
        MODELS_INFO: [
            ("_TZE200_r0jdjrvi", "TS0601"),
        ],
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaWindowCoverManufCluster.cluster_id,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: {
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaWindowCoverManufCluster,
                    TuyaOnOffNM,
                    TuyaWindowCovering,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: {
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        }
    }

Most of the functionallity has been commented, I will enable it later.

Is expected that lift_percentage would be updated.

I don't know what switch is this that you refer, any info will be welcomed.
Please, enable the debug logs and attach the related ones:

@RowCZ if you can attach the logs related to the switch operation will be helpfull.

@RowCZ
Copy link

RowCZ commented Nov 27, 2022

@javicalle thanks for quick response
I used your new quirk and tried to catch relevant logs while tried:

switch ON - switch OFF (motor starts runnik CW)
switch OFF - (motor stops)
Up - error occurs
Stop - error occurs
Down - error occurs

rowcz__TZE200_r0jdjrvi_home-assistant.log

With new quirk I also noticed that I have finally some messages in Logbook on device page but there are only messages from yesterday (about 15 hours ago and older) so something in past worked at least that much that made some records there and new quirk was at least able to load that records but wasn't able to made new one.

I hope this info will help.
Thanks again

@caradas
Copy link
Author

caradas commented Nov 27, 2022

Is expected that lift_percentage would be updated.

I can confirm that lift_percantage is now showing up and updating correctly after the curtain moved.

switch ON - switch OFF (motor starts runnik CW) switch OFF - (motor stops) Up - error occurs Stop - error occurs Down - error occurs

I can't confirm that. The Switch entity does nothing for me.

@javicalle
Copy link
Collaborator

I have uppdated this part of the code:

        # (upopen, downclose, stop)
        if command_id == 0x0002:  # ¿0x0003: continue?
            cluster_data = TuyaClusterData(
                endpoint_id=self.endpoint.endpoint_id,
                cluster_attr="curtain_switch",
                attr_value=TUYA2ZB_COMMANDS[command_id],  # convert tuya2zigbee command
                expect_reply=expect_reply,
                manufacturer=manufacturer,
            )

Can you fix it and repeat the tests?

@caradas
Copy link
Author

caradas commented Dec 7, 2022

Thanks for the update.

Percentage update is still working.
None of the Buttons do anything.

UP:

2022-12-07 22:44:45.632 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] tuya_mcu_command: cluster_data=TuyaClusterData(endpoint_id=1, cluster_attr='current_position_lift_percentage', attr_value=100, expect_reply=True)
2022-12-07 22:44:45.632 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] get_dp_mapping --> found DP: 3
2022-12-07 22:44:45.632 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] from_cluster_data: 3, DPToAttributeMapping(ep_attribute='window_covering', attribute_name='current_position_lift_percentage', dp_type=<TuyaDPType.VALUE: 2>, converter=None, dp_converter=None, endpoint_id=None)
2022-12-07 22:44:45.632 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] ztype: 100
2022-12-07 22:44:45.632 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] from_value: [4, 0, 0, 0, 100]
2022-12-07 22:44:45.632 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] raw: b'\x00\x00\x00d'
2022-12-07 22:44:45.632 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] tuya_command: TuyaCommand(status=0, tsn=79, datapoints=[TuyaDatapointData(dp=3, data=TuyaData(dp_type=<TuyaDPType.VALUE: 2>, function=0, raw=b'\x00\x00\x00d', *payload=1677721600))])
2022-12-07 22:44:45.633 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] Sending request header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=True, direction=<Direction.Server_to_Client: 0>, disable_default_response=0, reserved=0, *is_cluster=True, *is_general=False, *is_reply=False), manufacturer=4417, tsn=80, command_id=0, *direction=<Direction.Server_to_Client: 0>, *is_reply=False)
2022-12-07 22:44:45.633 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] Sending request: set_data(data=TuyaCommand(status=0, tsn=79, datapoints=[TuyaDatapointData(dp=3, data=TuyaData(dp_type=<TuyaDPType.VALUE: 2>, function=0, raw=b'\x00\x00\x00d', *payload=1677721600))]))
2022-12-07 22:44:45.693 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] Received ZCL frame: b'\x18P\x0b\x00\x83'
2022-12-07 22:44:45.693 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.GLOBAL_COMMAND: 0>, is_manufacturer_specific=0, direction=<Direction.Client_to_Server: 1>, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True, *is_reply=True), tsn=80, command_id=11, *direction=<Direction.Client_to_Server: 1>, *is_reply=True)
2022-12-07 22:44:45.694 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] Decoded ZCL frame: TuyaWindowCoverManufCluster:Default_Response(command_id=0, status=<Status.UNSUP_MANUF_CLUSTER_COMMAND: 131>)

Down:

2022-12-07 22:49:00.991 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] tuya_mcu_command: cluster_data=TuyaClusterData(endpoint_id=1, cluster_attr='current_position_lift_percentage', attr_value=0, expect_reply=True)
2022-12-07 22:49:00.991 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] get_dp_mapping --> found DP: 3
2022-12-07 22:49:00.991 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] from_cluster_data: 3, DPToAttributeMapping(ep_attribute='window_covering', attribute_name='current_position_lift_percentage', dp_type=<TuyaDPType.VALUE: 2>, converter=None, dp_converter=None, endpoint_id=None)
2022-12-07 22:49:00.992 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] ztype: 0
2022-12-07 22:49:00.992 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] from_value: [4, 0, 0, 0, 0]
2022-12-07 22:49:00.992 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] raw: b'\x00\x00\x00\x00'
2022-12-07 22:49:00.992 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] tuya_command: TuyaCommand(status=0, tsn=101, datapoints=[TuyaDatapointData(dp=3, data=TuyaData(dp_type=<TuyaDPType.VALUE: 2>, function=0, raw=b'\x00\x00\x00\x00', *payload=0))])
2022-12-07 22:49:00.993 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] Sending request header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=True, direction=<Direction.Server_to_Client: 0>, disable_default_response=0, reserved=0, *is_cluster=True, *is_general=False, *is_reply=False), manufacturer=4417, tsn=102, command_id=0, *direction=<Direction.Server_to_Client: 0>, *is_reply=False)
2022-12-07 22:49:00.993 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] Sending request: set_data(data=TuyaCommand(status=0, tsn=101, datapoints=[TuyaDatapointData(dp=3, data=TuyaData(dp_type=<TuyaDPType.VALUE: 2>, function=0, raw=b'\x00\x00\x00\x00', *payload=0))]))
2022-12-07 22:49:01.058 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] Received ZCL frame: b'\x18f\x0b\x00\x83'
2022-12-07 22:49:01.058 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.GLOBAL_COMMAND: 0>, is_manufacturer_specific=0, direction=<Direction.Client_to_Server: 1>, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True, *is_reply=True), tsn=102, command_id=11, *direction=<Direction.Client_to_Server: 1>, *is_reply=True)
2022-12-07 22:49:01.058 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] Decoded ZCL frame: TuyaWindowCoverManufCluster:Default_Response(command_id=0, status=<Status.UNSUP_MANUF_CLUSTER_COMMAND: 131>)

Stop:

2022-12-07 22:49:52.003 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] tuya_mcu_command: cluster_data=TuyaClusterData(endpoint_id=1, cluster_attr='curtain_switch', attr_value=1, expect_reply=True)
2022-12-07 22:49:52.003 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] from_cluster_data: None, None
2022-12-07 22:49:52.003 WARNING (MainThread) [zigpy.zcl] [0xB579:1:0xef00] No cluster_dp found for 1, curtain_switch
2022-12-07 22:49:52.005 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] tuya_command: None
2022-12-07 22:49:52.005 WARNING (MainThread) [zigpy.zcl] [0xB579:1:0xef00] no MCU command for data TuyaClusterData(endpoint_id=1, cluster_attr='curtain_switch', attr_value=1, expect_reply=True)

EDIT:

Switch On (Rotating CW):

2022-12-07 23:21:45.637 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] tuya_mcu_command: cluster_data=TuyaClusterData(endpoint_id=1, cluster_attr='on_off', attr_value=1, expect_reply=True, manufacturer=-1)
2022-12-07 23:21:45.637 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] get_dp_mapping --> found DP: 1
2022-12-07 23:21:45.638 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] from_cluster_data: {1: DPToAttributeMapping(ep_attribute='window_covering', attribute_name='on_off', dp_type=<TuyaDPType.ENUM: 4>, converter=None, dp_converter=None, endpoint_id=None)}
2022-12-07 23:21:45.638 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] ztype: enum8.undefined_0x01
2022-12-07 23:21:45.638 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] from_value: [1, 1]
2022-12-07 23:21:45.638 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] raw: b'\x01'
2022-12-07 23:21:45.638 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] tuya_commands: [TuyaCommand(status=0, tsn=78, datapoints=[TuyaDatapointData(dp=1, data=TuyaData(dp_type=<TuyaDPType.ENUM: 4>, function=0, raw=b'\x01', *payload=<enum8.undefined_0x01: 1>))])]
2022-12-07 23:21:45.639 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] Sending request header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=False, direction=<Direction.Server_to_Client: 0>, disable_default_response=0, reserved=0, *is_cluster=True, *is_general=False, *is_reply=False), tsn=79, command_id=0, *direction=<Direction.Server_to_Client: 0>, *is_reply=False)
2022-12-07 23:21:45.639 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] Sending request: set_data(data=TuyaCommand(status=0, tsn=78, datapoints=[TuyaDatapointData(dp=1, data=TuyaData(dp_type=<TuyaDPType.ENUM: 4>, function=0, raw=b'\x01', *payload=<enum8.undefined_0x01: 1>))]))
2022-12-07 23:21:45.682 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] Received ZCL frame: b'\x18O\x0b\x00\x00'
2022-12-07 23:21:45.683 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.GLOBAL_COMMAND: 0>, is_manufacturer_specific=0, direction=<Direction.Client_to_Server: 1>, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True, *is_reply=True), tsn=79, command_id=11, *direction=<Direction.Client_to_Server: 1>, *is_reply=True)
2022-12-07 23:21:45.683 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] Decoded ZCL frame: TuyaWindowCoverManufCluster:Default_Response(command_id=0, status=<Status.SUCCESS: 0>)
2022-12-07 23:21:45.725 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] Received ZCL frame: b'\x19\xe6\x01\x00N\x01\x04\x00\x01\x01'
2022-12-07 23:21:45.725 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=0, direction=<Direction.Client_to_Server: 1>, disable_default_response=1, reserved=0, *is_cluster=True, *is_general=False, *is_reply=True), tsn=230, command_id=1, *direction=<Direction.Client_to_Server: 1>, *is_reply=True)
2022-12-07 23:21:45.726 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] Decoded ZCL frame: TuyaWindowCoverManufCluster:get_data(data=TuyaCommand(status=0, tsn=78, datapoints=[TuyaDatapointData(dp=1, data=TuyaData(dp_type=<TuyaDPType.ENUM: 4>, function=0, raw=b'\x01', *payload=<enum8.undefined_0x01: 1>))]))
2022-12-07 23:21:45.726 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] Received command 0x01 (TSN 230): get_data(data=TuyaCommand(status=0, tsn=78, datapoints=[TuyaDatapointData(dp=1, data=TuyaData(dp_type=<TuyaDPType.ENUM: 4>, function=0, raw=b'\x01', *payload=<enum8.undefined_0x01: 1>))]))

Off (Stop Rotating CW):

2022-12-07 23:21:45.637 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] tuya_mcu_command: cluster_data=TuyaClusterData(endpoint_id=1, cluster_attr='on_off', attr_value=1, expect_reply=True, manufacturer=-1)
2022-12-07 23:21:45.637 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] get_dp_mapping --> found DP: 1
2022-12-07 23:21:45.638 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] from_cluster_data: {1: DPToAttributeMapping(ep_attribute='window_covering', attribute_name='on_off', dp_type=<TuyaDPType.ENUM: 4>, converter=None, dp_converter=None, endpoint_id=None)}
2022-12-07 23:21:45.638 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] ztype: enum8.undefined_0x01
2022-12-07 23:21:45.638 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] from_value: [1, 1]
2022-12-07 23:21:45.638 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] raw: b'\x01'
2022-12-07 23:21:45.638 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] tuya_commands: [TuyaCommand(status=0, tsn=78, datapoints=[TuyaDatapointData(dp=1, data=TuyaData(dp_type=<TuyaDPType.ENUM: 4>, function=0, raw=b'\x01', *payload=<enum8.undefined_0x01: 1>))])]
2022-12-07 23:21:45.639 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] Sending request header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=False, direction=<Direction.Server_to_Client: 0>, disable_default_response=0, reserved=0, *is_cluster=True, *is_general=False, *is_reply=False), tsn=79, command_id=0, *direction=<Direction.Server_to_Client: 0>, *is_reply=False)
2022-12-07 23:21:45.639 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] Sending request: set_data(data=TuyaCommand(status=0, tsn=78, datapoints=[TuyaDatapointData(dp=1, data=TuyaData(dp_type=<TuyaDPType.ENUM: 4>, function=0, raw=b'\x01', *payload=<enum8.undefined_0x01: 1>))]))
2022-12-07 23:21:45.682 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] Received ZCL frame: b'\x18O\x0b\x00\x00'
2022-12-07 23:21:45.683 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.GLOBAL_COMMAND: 0>, is_manufacturer_specific=0, direction=<Direction.Client_to_Server: 1>, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True, *is_reply=True), tsn=79, command_id=11, *direction=<Direction.Client_to_Server: 1>, *is_reply=True)
2022-12-07 23:21:45.683 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] Decoded ZCL frame: TuyaWindowCoverManufCluster:Default_Response(command_id=0, status=<Status.SUCCESS: 0>)
2022-12-07 23:21:45.725 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] Received ZCL frame: b'\x19\xe6\x01\x00N\x01\x04\x00\x01\x01'
2022-12-07 23:21:45.725 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=0, direction=<Direction.Client_to_Server: 1>, disable_default_response=1, reserved=0, *is_cluster=True, *is_general=False, *is_reply=True), tsn=230, command_id=1, *direction=<Direction.Client_to_Server: 1>, *is_reply=True)
2022-12-07 23:21:45.726 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] Decoded ZCL frame: TuyaWindowCoverManufCluster:get_data(data=TuyaCommand(status=0, tsn=78, datapoints=[TuyaDatapointData(dp=1, data=TuyaData(dp_type=<TuyaDPType.ENUM: 4>, function=0, raw=b'\x01', *payload=<enum8.undefined_0x01: 1>))]))
2022-12-07 23:21:45.726 DEBUG (MainThread) [zigpy.zcl] [0xB579:1:0xef00] Received command 0x01 (TSN 230): get_data(data=TuyaCommand(status=0, tsn=78, datapoints=[TuyaDatapointData(dp=1, data=TuyaData(dp_type=<TuyaDPType.ENUM: 4>, function=0, raw=b'\x01', *payload=<enum8.undefined_0x01: 1>))]))

@RowCZ
Copy link

RowCZ commented Dec 7, 2022

@javicalle hi and thanks for the update
I modified the quirk and here is the results

switch is without any change
switch ON - switch OFF (motor starts runnig CW)
switch OFF - (motor stops)

Up - no error this time, in log book I can see message that Cover is opening but no movement on motor (button change to inactive state)
Stop - failed to call service error occurs as before
Up - no error this time, in log book I can see message that Cover is closing but no movement on motor (button change to inactive state)

log:
rowcz__TZE200_r0jdjrvi_home-assistant_2022-12-07.log

Here is a also a part of the screen with controls and logbook
image

@caradas
Copy link
Author

caradas commented Dec 7, 2022

Good News i got the arrows working with the following:

"""Tuya MCU based cover and blinds."""
from typing import Dict, Optional, Union

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

from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
)
from zhaquirks.tuya import (
    NoManufacturerCluster,
    TUYA_MCU_COMMAND,
    TuyaLocalCluster,
    TuyaManufacturerWindowCover,
    TuyaManufCluster,
    TuyaWindowCover,
    TuyaWindowCoverControl,
)
from zhaquirks.tuya.mcu import (
    DPToAttributeMapping,
    TuyaClusterData,
    TuyaDPType,
    TuyaMCUCluster,
)

# Maps OPEN/CLOSE/STOP cover commands from Tuya to Zigbee
# https://github.com/zigpy/zigpy/blob/master/zigpy/zcl/clusters/closures.py#L558
# https://developer.tuya.com/en/docs/iot-device-dev/zigbee-curtain-switch-access-standard?id=K9ik6zvra3twv#title-7-DP1%20and%20DP4%20Curtain%20switch%201%20and%202
TUYA2ZB_COMMANDS = {
    0x0000: 0x0000,
    0x0001: 0x0002,
    0x0002: 0x0001,
}


class TuyaWindowCovering(NoManufacturerCluster, WindowCovering, TuyaLocalCluster):
    """Tuya MCU WindowCovering cluster."""

    """Add additional attributes for direction"""
    attributes = WindowCovering.attributes.copy()
    attributes.update(
        {
            0xF000: ("curtain_switch", t.enum8, True),  # 0: open, 1: stop, 2: close
            0xF001: ("accurate_calibration", t.enum8, True),  # 0: calibration started, 1: calibration finished
            0xF002: ("motor_steering", t.enum8, True),  # 0: default, 1: reverse
            0xF003: ("travel", t.uint16_t, True),  # 30 to 9000 (units of 0.1 seconds)
        }
    )

    async def command(
        self,
        command_id: Union[foundation.GeneralCommand, int, t.uint8_t],
        *args,
        manufacturer: Optional[Union[int, t.uint16_t]] = None,
        expect_reply: bool = True,
        tsn: Optional[Union[int, t.uint8_t]] = None,
    ):
        """Override the default Cluster command."""

        # if manufacturer is None:
        #     manufacturer = self.endpoint.device.manufacturer

        self.debug(
            "Sending Tuya Cluster Command. Cluster Command is %x, Arguments are %s",
            command_id,
            args,
        )

        # (upopen, downclose, stop)
        if command_id in (0x0002, 0x0000, 0x0001):  # ¿0x0003: continue?
            cluster_data = TuyaClusterData(
                endpoint_id=self.endpoint.endpoint_id,
                cluster_attr="curtain_switch",
                attr_value=TUYA2ZB_COMMANDS[command_id],  # convert tuya2zigbee command
                expect_reply=expect_reply,
                manufacturer=-1,
            )
            self.endpoint.device.command_bus.listener_event(
                TUYA_MCU_COMMAND,
                cluster_data,
            )
            return foundation.GENERAL_COMMANDS[
                foundation.GeneralCommand.Default_Response
            ].schema(command_id=command_id, status=foundation.Status.SUCCESS)

        # (go_to_lift_percentage)
        elif command_id == 0x0005:
            lift_value = args[0]

            cluster_data = TuyaClusterData(
                endpoint_id=self.endpoint.endpoint_id,
                cluster_attr="current_position_lift_percentage",
                attr_value=lift_value,
                expect_reply=expect_reply,
                manufacturer=-1,
            )
            self.endpoint.device.command_bus.listener_event(
                TUYA_MCU_COMMAND,
                cluster_data,
            )
            return foundation.GENERAL_COMMANDS[
                foundation.GeneralCommand.Default_Response
            ].schema(command_id=command_id, status=foundation.Status.SUCCESS)

        # # Custom Command
        # elif command_id == 0x0006:  # ¿doc reference?
        #     tuya_payload.status = args[0]
        #     tuya_payload.tsn = args[1]
        #     tuya_payload.command_id = args[2]
        #     tuya_payload.function = args[3]
        #     tuya_payload.data = args[4]

        self.warning("Unsupported command_id: %s", command_id)
        return foundation.GENERAL_COMMANDS[
            foundation.GeneralCommand.Default_Response
        ].schema(command_id=command_id, status=foundation.Status.UNSUP_CLUSTER_COMMAND)


class TuyaWindowCoverManufCluster(TuyaMCUCluster):
    """Tuya with WindowCover data points."""

    attributes = TuyaMCUCluster.attributes.copy()
    attributes.update(
        {
            0x5000: ("backlight_mode", t.enum8, True),  # 0: off, 1: on
            0x8001: ("indicator_status", t.enum8, True),  # 0: status, 1: position, 2: off (¿backlight_mode?)
        }
    )

    dp_to_attribute: Dict[int, DPToAttributeMapping] = {
        1: DPToAttributeMapping(
            TuyaWindowCovering.ep_attribute,
            "curtain_switch",
            dp_type=TuyaDPType.ENUM,
        ),
        3: DPToAttributeMapping(
            TuyaWindowCovering.ep_attribute,
            "current_position_lift_percentage",
            dp_type=TuyaDPType.VALUE,
        ),
        # 3: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "accurate_calibration",
        #     dp_type=TuyaDPType.ENUM,
        # ),
        # 4: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "on_off",
        #     dp_type=TuyaDPType.ENUM,
        #     endpoint_id=2,
        # ),
        # 5: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "current_position_lift_percentage",
        #     dp_type=TuyaDPType.VALUE,
        #     endpoint_id=2,
        # ),
        # 6: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "accurate_calibration",
        #     dp_type=TuyaDPType.ENUM,
        #     endpoint_id=2,
        # ),
        # 7: DPToAttributeMapping(
        #     TuyaMCUCluster.ep_attribute,
        #     "backlight_mode",
        #     dp_type=TuyaDPType.ENUM,
        # ),
        # 8: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "motor_steering",
        #     dp_type=TuyaDPType.ENUM,
        # ),
        # 9: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "motor_steering",
        #     dp_type=TuyaDPType.ENUM,
        #     endpoint_id=2,
        # ),
        # 10: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "quick_calibration",
        #     dp_type=TuyaDPType.ENUM,
        # ),
        # 11: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "quick_calibration",
        #     dp_type=TuyaDPType.ENUM,
        #     endpoint_id=2,
        # ),
        # 14: DPToAttributeMapping(
        #     TuyaMCUCluster.ep_attribute,
        #     "indicator_status",
        #     dp_type=TuyaDPType.ENUM,
        # ),
    }

    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",
        6: "_dp_2_attr_update",
        7: "_dp_2_attr_update",
        8: "_dp_2_attr_update",
        9: "_dp_2_attr_update",
        10: "_dp_2_attr_update",
        11: "_dp_2_attr_update",
        14: "_dp_2_attr_update",
    }


class TuyaCover0601_GP(TuyaWindowCover):
    """Tuya blind controller device."""
    signature = {
        # "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.AllocateAddress|RxOnWhenIdle|MainsPowered|FullFunctionDevice: 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
        # )
        MODELS_INFO: [
            ("_TZE200_r0jdjrvi", "TS0601"),
        ],
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaWindowCoverManufCluster.cluster_id,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: {
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaWindowCoverManufCluster,
                    TuyaWindowCovering,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: {
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        }
    }

Key was to set following in commands:

manufacturer=-1,

Unfortunally the percentage slider is only a display but doesn't turn the motor:
Log:

2022-12-08 00:48:27.526 DEBUG (MainThread) [zigpy.zcl] [0x50CE:1:0xef00] tuya_mcu_command: cluster_data=TuyaClusterData(endpoint_id=1, cluster_attr='current_position_lift_percentage', attr_value=75, expect_reply=True, manufacturer=-1)
2022-12-08 00:48:27.526 DEBUG (MainThread) [zigpy.zcl] [0x50CE:1:0xef00] get_dp_mapping --> found DP: 3
2022-12-08 00:48:27.526 DEBUG (MainThread) [zigpy.zcl] [0x50CE:1:0xef00] from_cluster_data: {3: DPToAttributeMapping(ep_attribute='window_covering', attribute_name='current_position_lift_percentage', dp_type=<TuyaDPType.VALUE: 2>, converter=None, dp_converter=None, endpoint_id=None)}
2022-12-08 00:48:27.527 DEBUG (MainThread) [zigpy.zcl] [0x50CE:1:0xef00] ztype: 75
2022-12-08 00:48:27.527 DEBUG (MainThread) [zigpy.zcl] [0x50CE:1:0xef00] from_value: [4, 0, 0, 0, 75]
2022-12-08 00:48:27.527 DEBUG (MainThread) [zigpy.zcl] [0x50CE:1:0xef00] raw: b'K\x00\x00\x00'
2022-12-08 00:48:27.527 DEBUG (MainThread) [zigpy.zcl] [0x50CE:1:0xef00] tuya_commands: [TuyaCommand(status=0, tsn=72, datapoints=[TuyaDatapointData(dp=3, data=TuyaData(dp_type=<TuyaDPType.VALUE: 2>, function=0, raw=b'K\x00\x00\x00', *payload=75))])]
2022-12-08 00:48:27.529 DEBUG (MainThread) [zigpy.zcl] [0x50CE:1:0xef00] Sending request header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=False, direction=<Direction.Server_to_Client: 0>, disable_default_response=0, reserved=0, *is_cluster=True, *is_general=False, *is_reply=False), tsn=73, command_id=0, *direction=<Direction.Server_to_Client: 0>, *is_reply=False)
2022-12-08 00:48:27.529 DEBUG (MainThread) [zigpy.zcl] [0x50CE:1:0xef00] Sending request: set_data(data=TuyaCommand(status=0, tsn=72, datapoints=[TuyaDatapointData(dp=3, data=TuyaData(dp_type=<TuyaDPType.VALUE: 2>, function=0, raw=b'K\x00\x00\x00', *payload=75))]))
2022-12-08 00:48:27.596 DEBUG (MainThread) [zigpy.zcl] [0x50CE:1:0xef00] Received ZCL frame: b'\x18I\x0b\x00\x00'
2022-12-08 00:48:27.596 DEBUG (MainThread) [zigpy.zcl] [0x50CE:1:0xef00] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.GLOBAL_COMMAND: 0>, is_manufacturer_specific=0, direction=<Direction.Client_to_Server: 1>, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True, *is_reply=True), tsn=73, command_id=11, *direction=<Direction.Client_to_Server: 1>, *is_reply=True)
2022-12-08 00:48:27.597 DEBUG (MainThread) [zigpy.zcl] [0x50CE:1:0xef00] Decoded ZCL frame: TuyaWindowCoverManufCluster:Default_Response(command_id=0, status=<Status.SUCCESS: 0>)

@RowCZ
Copy link

RowCZ commented Dec 8, 2022

@caradas Nice!
It works for me as well even Stop button works now (but still causes error).
Switch become inactive (but is not needed anymore)
And I have no percentage slider at all 😮 is there way how to add it?

image

Here is log from version that cardas provided:
rowcz__TZE200_r0jdjrvi_home-assistant_2022-12-07-2.log

@caradas
Copy link
Author

caradas commented Dec 8, 2022

@RowCZ my usual way after changing the quirk was:

  1. Delete Device from Zigbee network.
  2. Delete pychache folder from qirk location
  3. Restart HA
  4. Add Device again

The slider is "hidden" in the details of the controls:
grafik

@RowCZ
Copy link

RowCZ commented Dec 8, 2022

weird, I removed device, delete pycache, restarted HA and paired again but I still don't have a progress bar (that mysterious switch is gone now)

image
image

@javicalle
Copy link
Collaborator

A lot of updates here.

The switch wasn't really necessary, just an attempt to understand things. Once resolved it is not necessary and should not return.

That manufacturer=-1 is the same that would try with the NoManufacturerCluster not sure why isn't working. You can get the same with manufacturer=foundation.ZCLHeader.NO_MANUFACTURER_ID.

The lift percentage is mapped against the DP_3. The device don't complain about it but it can be that is not the good one. According to Z2M it could be the DP_2, so I would try to replace the dp_to_attribute mapping from 3 to 2.

Let's see if we can fix the cover functions and then I try to see what happens to the missing slider.

@RowCZ
Copy link

RowCZ commented Dec 8, 2022

@javicalle
I am bit confused, I am not sure if I uderstand it right, I changed it here from 3 to 2 but with no effect

dp_to_attribute: Dict[int, DPToAttributeMapping] = {
        1: DPToAttributeMapping(
            TuyaWindowCovering.ep_attribute,
            "curtain_switch",
            dp_type=TuyaDPType.ENUM,
        ),
        2: DPToAttributeMapping(
            TuyaWindowCovering.ep_attribute,
            "current_position_lift_percentage",
            dp_type=TuyaDPType.VALUE,
        ),

@bezmi
Copy link

bezmi commented Dec 11, 2022

I've only just started with the zigbee stuff, but upon testing zigbee2mqtt, it appears that all of the buttons as well as position control work pretty well. For position control to work, I had to first set endstop positions by letting the motor move in one direction and physically stopping rotation to signal the end of the track. Repeat for the other direction and the position slider starts working.

Is there any way to bring the config from zigbee2mqtt into the custom quirk here so we can get all of the info required to get this working with zha?

@RowCZ
Copy link

RowCZ commented Dec 11, 2022

... I had to first set endstop positions by letting the motor move in one direction and physically stopping rotation to signal the end of the track. Repeat for the other direction and the position slider starts working...

That can be the reason why I have no slider, I didn't bother with instalation since I was unable to make it work at all. :D

@bezmi
Copy link

bezmi commented Dec 12, 2022

clarification: this was position control with zigbee2mqtt. Haven't been able to reliably get the slider to show up in zha.

Edit: using the quirk by cardas, the slider does indeed show up once the endstops have been configured. To do this:

  1. Manually spin the motor so it starts moving
  2. Force it to stop, as if the curtain is hitting the end of the track
  3. repeat steps 1 and 2 for the other rotational direction

The position of the slider will update if you stop the motor, but there is no control via the slider yet.

@sid12345678910
Copy link

sid12345678910 commented Dec 12, 2022

Good News i got the arrows working with the following:

so i have this model _TZE200_65moe6rh

This code works strangely with mine. Mine came with a remote. It seems the code works only if the motor doesn't hit its "endstops" Once is it hits the endstop HA stops responding. But if its in the middle I can control the motor in HA. All the commands work Up, Down and stop but only if its in-between both endstops. Once it hits the endstop nothing responds...

Also the slider reports the correct position but I cant move it around.

Edit:
I think i need to switch the command directions in the code. Anyone have any idea how to do that?

EDIT2 lol
Fixed
changed code to
TUYA2ZB_COMMANDS = {
0x0000: 0x0002,
0x0001: 0x0000,
0x0002: 0x0001,
}

@TheJulianJES TheJulianJES added the Tuya Request/PR regarding a Tuya device label Jan 4, 2023
@kolmakova
Copy link

kolmakova commented Jan 30, 2023

@javicalle
Hi, I've got another Tuya curtain motor with the same signature, tried the quirk here, buttons work, but state and controls are inverted. Slider doesn't work. Using latest HA and this quirk. Here is the log of slider:

2023-01-30 20:28:41.799 DEBUG (MainThread) [zigpy.zcl] [0x23F6:1:0x0102] Sending Tuya Cluster Command. Cluster Command is 5, Arguments are (48,)
2023-01-30 20:28:41.800 DEBUG (MainThread) [zigpy.zcl] [0x23F6:1:0xef00] tuya_mcu_command: cluster_data=TuyaClusterData(endpoint_id=1, cluster_attr='current_position_lift_percentage', attr_value=48, expect_reply=True)
2023-01-30 20:28:41.801 DEBUG (MainThread) [zigpy.zcl] [0x23F6:1:0xef00] get_dp_mapping --> found DP: 3
2023-01-30 20:28:41.801 DEBUG (MainThread) [zigpy.zcl] [0x23F6:1:0xef00] from_cluster_data: {3: DPToAttributeMapping(ep_attribute='window_covering', attribute_name='current_position_lift_percentage', dp_type=<TuyaDPType.VALUE: 2>, converter=None, dp_converter=None, endpoint_id=None)}
2023-01-30 20:28:41.802 DEBUG (MainThread) [zigpy.zcl] [0x23F6:1:0xef00] value: 48
2023-01-30 20:28:41.803 DEBUG (MainThread) [zigpy.zcl] [0x23F6:1:0xef00] raw: b'\x00\x00\x000'
2023-01-30 20:28:41.803 DEBUG (MainThread) [zigpy.zcl] [0x23F6:1:0xef00] tuya_commands: [TuyaCommand(status=0, tsn=10, datapoints=[TuyaDatapointData(dp=3, data=TuyaData(dp_type=<TuyaDPType.VALUE: 2>, function=0, raw=b'\x00\x00\x000', *payload=48))])]
2023-01-30 20:28:41.809 WARNING (MainThread) [zigpy.util] Error calling listener <bound method TuyaMCUCluster.tuya_mcu_command of <ts0601_cover.TuyaWindowCoverManufCluster object at 0xa3fb6c10>> with args (TuyaClusterData(endpoint_id=1, cluster_attr='current_position_lift_percentage', attr_value=48, expect_reply=True),): TypeError('getattr(): attribute name must be string')
2023-01-30 20:28:41.812 DEBUG (MainThread) [zigpy.util] Error calling listener <bound method TuyaMCUCluster.tuya_mcu_command of <ts0601_cover.TuyaWindowCoverManufCluster object at 0xa3fb6c10>> with args (TuyaClusterData(endpoint_id=1, cluster_attr='current_position_lift_percentage', attr_value=48, expect_reply=True),)
Traceback (most recent call last):
File "/usr/local/lib/python3.10/site-packages/zigpy/util.py", line 55, in listener_event
result.append(method(*args))
File "/usr/local/lib/python3.10/site-packages/zhaquirks/tuya/mcu/__init__.py", line 289, in tuya_mcu_command
cluster = getattr(endpoint, cluster_data.cluster_name)
TypeError: getattr(): attribute name must be string

@javicalle
Copy link
Collaborator

That quirk is a little old now.
You can try adding the cluster_name in the TuyaClusterData, more or less like this:

        # (upopen, downclose, stop)
        if command_id in (0x0002, 0x0000, 0x0001):  # ¿0x0003: continue?
            cluster_data = TuyaClusterData(
                endpoint_id=self.endpoint.endpoint_id,
                cluster_name=TuyaMCUCluster.ep_attribute,
                cluster_attr="curtain_switch",
                attr_value=TUYA2ZB_COMMANDS[command_id],  # convert tuya2zigbee command
                expect_reply=expect_reply,
                manufacturer=-1,
            )

and

        # (go_to_lift_percentage)
        elif command_id == 0x0005:
            lift_value = args[0]

            cluster_data = TuyaClusterData(
                endpoint_id=self.endpoint.endpoint_id,
                cluster_name=TuyaMCUCluster.ep_attribute,
                cluster_attr="current_position_lift_percentage",
                attr_value=lift_value,
                expect_reply=expect_reply,
                manufacturer=-1,
            )

IIRC that quirk is very incomplete

@kolmakova
Copy link

@javicalle Thanks! I've fixed a slider control by changing current_position_lift_percentage DP from 3 to 2 as in Tuya documentation and added your solution to fix TypeError: getattr(): attribute name must be string, so slider is working now. Here's the quirk (for my _TZE200_nogaemzt). Don't forget to add another model name to the MODELS_INFO.

ts0601_cover.py
"""Tuya MCU based cover and blinds."""
from typing import Dict, Optional, Union

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

from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
)
from zhaquirks.tuya import (
    NoManufacturerCluster,
    TUYA_MCU_COMMAND,
    TuyaLocalCluster,
    TuyaManufacturerWindowCover,
    TuyaManufCluster,
    TuyaWindowCover,
    TuyaWindowCoverControl,
)
from zhaquirks.tuya.mcu import (
    DPToAttributeMapping,
    TuyaClusterData,
    TuyaDPType,
    TuyaMCUCluster,
)

# Maps OPEN/CLOSE/STOP cover commands from Tuya to Zigbee
# https://github.com/zigpy/zigpy/blob/master/zigpy/zcl/clusters/closures.py#L558
# https://developer.tuya.com/en/docs/iot-device-dev/zigbee-curtain-switch-access-standard?id=K9ik6zvra3twv#title-7-DP1%20and%20DP4%20Curtain%20switch%201%20and%202
TUYA2ZB_COMMANDS = {
    0x0000: 0x0002,
    0x0001: 0x0000,
    0x0002: 0x0001,
}

class TuyaWindowCovering(NoManufacturerCluster, WindowCovering, TuyaLocalCluster):
    """Tuya MCU WindowCovering cluster."""

    """Add additional attributes for direction"""
    attributes = WindowCovering.attributes.copy()
    attributes.update(
        {
            0xF000: ("curtain_switch", t.enum8, True),  # 0: open, 1: stop, 2: close
            0xF001: ("accurate_calibration", t.enum8, True),  # 0: calibration started, 1: calibration finished
            0xF002: ("motor_steering", t.enum8, True),  # 0: default, 1: reverse
            0xF003: ("travel", t.uint16_t, True),  # 30 to 9000 (units of 0.1 seconds)
        }
    )

    async def command(
        self,
        command_id: Union[foundation.GeneralCommand, int, t.uint8_t],
        *args,
        manufacturer: Optional[Union[int, t.uint16_t]] = None,
        expect_reply: bool = True,
        tsn: Optional[Union[int, t.uint8_t]] = None,
    ):
        """Override the default Cluster command."""

        # if manufacturer is None:
        #     manufacturer = self.endpoint.device.manufacturer

        self.debug(
            "Sending Tuya Cluster Command. Cluster Command is %x, Arguments are %s",
            command_id,
            args,
        )

        # (upopen, downclose, stop)
        if command_id in (0x0002, 0x0000, 0x0001):  # ¿0x0003: continue?
            cluster_data = TuyaClusterData(
                endpoint_id=self.endpoint.endpoint_id,
                cluster_name=TuyaMCUCluster.ep_attribute,
                cluster_attr="curtain_switch",
                attr_value=TUYA2ZB_COMMANDS[command_id],  # convert tuya2zigbee command
                expect_reply=expect_reply,
                manufacturer=-1,
            )
            self.endpoint.device.command_bus.listener_event(
                TUYA_MCU_COMMAND,
                cluster_data,
            )
            return foundation.GENERAL_COMMANDS[
                foundation.GeneralCommand.Default_Response
            ].schema(command_id=command_id, status=foundation.Status.SUCCESS)

        # (go_to_lift_percentage)
        elif command_id == 0x0005:
            lift_value = args[0]

            cluster_data = TuyaClusterData(
                endpoint_id=self.endpoint.endpoint_id,
                cluster_name=TuyaMCUCluster.ep_attribute,
                cluster_attr="current_position_lift_percentage",
                attr_value=lift_value,
                expect_reply=expect_reply,
                manufacturer=-1,
            )
            self.endpoint.device.command_bus.listener_event(
                TUYA_MCU_COMMAND,
                cluster_data,
            )
            return foundation.GENERAL_COMMANDS[
                foundation.GeneralCommand.Default_Response
            ].schema(command_id=command_id, status=foundation.Status.SUCCESS)
            

        # # Custom Command
        # elif command_id == 0x0006:  # ¿doc reference?
        #     tuya_payload.status = args[0]
        #     tuya_payload.tsn = args[1]
        #     tuya_payload.command_id = args[2]
        #     tuya_payload.function = args[3]
        #     tuya_payload.data = args[4]

        self.warning("Unsupported command_id: %s", command_id)
        return foundation.GENERAL_COMMANDS[
            foundation.GeneralCommand.Default_Response
        ].schema(command_id=command_id, status=foundation.Status.UNSUP_CLUSTER_COMMAND)


class TuyaWindowCoverManufCluster(TuyaMCUCluster):
    """Tuya with WindowCover data points."""

    attributes = TuyaMCUCluster.attributes.copy()
    attributes.update(
        {
            0x5000: ("backlight_mode", t.enum8, True),  # 0: off, 1: on
            0x8001: ("indicator_status", t.enum8, True),  # 0: status, 1: position, 2: off (¿backlight_mode?)
        }
    )

    dp_to_attribute: Dict[int, DPToAttributeMapping] = {
        1: DPToAttributeMapping(
            TuyaWindowCovering.ep_attribute,
            "curtain_switch",
            dp_type=TuyaDPType.ENUM,
        ),
        2: DPToAttributeMapping(
            TuyaWindowCovering.ep_attribute,
            "current_position_lift_percentage",
            dp_type=TuyaDPType.VALUE,
        ),
        # 3: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "accurate_calibration",
        #     dp_type=TuyaDPType.ENUM,
        # ),
        # 4: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "on_off",
        #     dp_type=TuyaDPType.ENUM,
        #     endpoint_id=2,
        # ),
        # 5: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "current_position_lift_percentage",
        #     dp_type=TuyaDPType.VALUE,
        #     endpoint_id=2,
        # ),
        # 6: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "accurate_calibration",
        #     dp_type=TuyaDPType.ENUM,
        #     endpoint_id=2,
        # ),
        # 7: DPToAttributeMapping(
        #     TuyaMCUCluster.ep_attribute,
        #     "backlight_mode",
        #     dp_type=TuyaDPType.ENUM,
        # ),
        # 8: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "motor_steering",
        #     dp_type=TuyaDPType.ENUM,
        # ),
        # 9: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "motor_steering",
        #     dp_type=TuyaDPType.ENUM,
        #     endpoint_id=2,
        # ),
        # 10: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "quick_calibration",
        #     dp_type=TuyaDPType.ENUM,
        # ),
        # 11: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "quick_calibration",
        #     dp_type=TuyaDPType.ENUM,
        #     endpoint_id=2,
        # ),
        # 14: DPToAttributeMapping(
        #     TuyaMCUCluster.ep_attribute,
        #     "indicator_status",
        #     dp_type=TuyaDPType.ENUM,
        # ),
    }

    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",
        6: "_dp_2_attr_update",
        7: "_dp_2_attr_update",
        8: "_dp_2_attr_update",
        9: "_dp_2_attr_update",
        10: "_dp_2_attr_update",
        11: "_dp_2_attr_update",
        14: "_dp_2_attr_update",
    }


class TuyaCover0601_GP(TuyaWindowCover):
    tuya_cover_inverted_by_default = True
    """Tuya blind controller device."""
    signature = {
        # "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.AllocateAddress|RxOnWhenIdle|MainsPowered|FullFunctionDevice: 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
        # )
        MODELS_INFO: [
            ("_TZE200_nogaemzt", "TS0601"),
        ],
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaWindowCoverManufCluster.cluster_id,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: {
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaWindowCoverManufCluster,
                    TuyaWindowCovering,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: {
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        }
    }

Seems like the only problem now is that controls have the right direction, but the curtains state and slider control are inverted. To open curtains via slider, I need to move a slider to the 0 so state results in Closed (and vice versa). And if the curtain state is Closed (but it's actually Open), Close button is inactive. Also, commands (buttons) don't result in updating a slider value and curtain state (maybe this doesn't relate to the invertion).

Tried to add tuya_cover_inverted_by_default = True to the TuyaCover0601_GP but no result. Any ideas?

@javicalle
Copy link
Collaborator

If you have a physical remote for that cover try to reverse the motor spin according to the manufacturer.
If don't, try updating the motor_steering attribute from the cluster TuyaWindowCovering. Switch between 0 and 1 values to test.

@Drmatterpl
Copy link

Drmatterpl commented Feb 20, 2023

Hi

That can be the reason why I have no slider, I didn't bother with instalation since I was unable to make it work at all. :D

@RowCZ can you share your latest quirk code? I have spend a looooot of time and it seems that this is the only quirk that works with my motor - the one by @caradas

@Dextrik
Copy link

Dextrik commented Feb 20, 2023

Hi, @Drmatterpl
I can share mine, everything is working, but i don't think so this quirk it's optimal.

ts0601_cover.py
"""Tuya MCU based cover and blinds."""
from typing import Dict, Optional, Union

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

from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
)
from zhaquirks.tuya import (
    NoManufacturerCluster,
    TUYA_MCU_COMMAND,
    TuyaLocalCluster,
    TuyaManufacturerWindowCover,
    TuyaManufCluster,
    TuyaWindowCover,
    TuyaWindowCoverControl,
)
from zhaquirks.tuya.mcu import (
    DPToAttributeMapping,
    TuyaClusterData,
    TuyaDPType,
    TuyaMCUCluster,
)

# Maps OPEN/CLOSE/STOP cover commands from Tuya to Zigbee
# https://github.com/zigpy/zigpy/blob/master/zigpy/zcl/clusters/closures.py#L558
# https://developer.tuya.com/en/docs/iot-device-dev/zigbee-curtain-switch-access-standard?id=K9ik6zvra3twv#title-7-DP1%20and%20DP4%20Curtain%20switch%201%20and%202
TUYA2ZB_COMMANDS = {
    0x0000: 0x0000,
    0x0001: 0x0002,
    0x0002: 0x0001,
}


class TuyaWindowCovering(NoManufacturerCluster, WindowCovering, TuyaLocalCluster):
    """Tuya MCU WindowCovering cluster."""

    """Add additional attributes for direction"""
    attributes = WindowCovering.attributes.copy()
    attributes.update(
        {
            0xF000: ("curtain_switch", t.enum8, True),  # 0: open, 1: stop, 2: close
            0xF001: ("accurate_calibration", t.enum8, True),  # 0: calibration started, 1: calibration finished
            0xF002: ("motor_steering", t.enum8, True),  # 0: default, 1: reverse
            0xF003: ("travel", t.uint16_t, True),  # 30 to 9000 (units of 0.1 seconds)
        }
    )

    async def command(
        self,
        command_id: Union[foundation.GeneralCommand, int, t.uint8_t],
        *args,
        manufacturer: Optional[Union[int, t.uint16_t]] = None,
        expect_reply: bool = True,
        tsn: Optional[Union[int, t.uint8_t]] = None,
    ):
        """Override the default Cluster command."""

        # if manufacturer is None:
        #     manufacturer = self.endpoint.device.manufacturer

        self.debug(
            "Sending Tuya Cluster Command. Cluster Command is %x, Arguments are %s",
            command_id,
            args,
        )

        # (upopen, downclose, stop)
        if command_id in (0x0002, 0x0000, 0x0001):  # ¿0x0003: continue?
            cluster_data = TuyaClusterData(
                endpoint_id=self.endpoint.endpoint_id,
                cluster_name=self.ep_attribute,
                cluster_attr="curtain_switch",
                attr_value=TUYA2ZB_COMMANDS[command_id],  # convert tuya2zigbee command
                expect_reply=expect_reply,
                manufacturer=-1,
            )
            self.endpoint.device.command_bus.listener_event(
                TUYA_MCU_COMMAND,
                cluster_data,
            )
            return foundation.GENERAL_COMMANDS[
                foundation.GeneralCommand.Default_Response
            ].schema(command_id=command_id, status=foundation.Status.SUCCESS)

        # (go_to_lift_percentage)
        elif command_id == 0x0005:
            lift_value = args[0]

            cluster_data = TuyaClusterData(
                endpoint_id=self.endpoint.endpoint_id,
                cluster_name=self.ep_attribute,
                cluster_attr="current_position_lift_percentage",
                attr_value=lift_value,
                expect_reply=expect_reply,
                manufacturer=-1,
            )
            self.endpoint.device.command_bus.listener_event(
                TUYA_MCU_COMMAND,
                cluster_data,
            )
            return foundation.GENERAL_COMMANDS[
                foundation.GeneralCommand.Default_Response
            ].schema(command_id=command_id, status=foundation.Status.SUCCESS)

        # # Custom Command
        # elif command_id == 0x0006:  # ¿doc reference?
        #     tuya_payload.status = args[0]
        #     tuya_payload.tsn = args[1]
        #     tuya_payload.command_id = args[2]
        #     tuya_payload.function = args[3]
        #     tuya_payload.data = args[4]

        self.warning("Unsupported command_id: %s", command_id)
        return foundation.GENERAL_COMMANDS[
            foundation.GeneralCommand.Default_Response
        ].schema(command_id=command_id, status=foundation.Status.UNSUP_CLUSTER_COMMAND)


class TuyaWindowCoverManufCluster(TuyaMCUCluster):
    """Tuya with WindowCover data points."""

    attributes = TuyaMCUCluster.attributes.copy()
    attributes.update(
        {
            0x5000: ("backlight_mode", t.enum8, True),  # 0: off, 1: on
            0x8001: ("indicator_status", t.enum8, True),  # 0: status, 1: position, 2: off (¿backlight_mode?)
        }
    )

    dp_to_attribute: Dict[int, DPToAttributeMapping] = {
        1: DPToAttributeMapping(
            TuyaWindowCovering.ep_attribute,
            "curtain_switch",
            dp_type=TuyaDPType.ENUM,
        ),
        2: DPToAttributeMapping(
            TuyaWindowCovering.ep_attribute,
            "current_position_lift_percentage",
            dp_type=TuyaDPType.VALUE,
        ),
        3: DPToAttributeMapping(
            TuyaWindowCovering.ep_attribute,
            "current_position_lift_percentage",
            dp_type=TuyaDPType.ENUM,
        ),
        # 4: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "on_off",
        #     dp_type=TuyaDPType.ENUM,
        #     endpoint_id=2,
        # ),
        # 5: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "current_position_lift_percentage",
        #     dp_type=TuyaDPType.VALUE,
        #     endpoint_id=2,
        # ),
        # 6: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "accurate_calibration",
        #     dp_type=TuyaDPType.ENUM,
        #     endpoint_id=2,
        # ),
        # 7: DPToAttributeMapping(
        #     TuyaMCUCluster.ep_attribute,
        #     "backlight_mode",
        #     dp_type=TuyaDPType.ENUM,
        # ),
        # 8: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "motor_steering",
        #     dp_type=TuyaDPType.ENUM,
        # ),
        # 9: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "motor_steering",
        #     dp_type=TuyaDPType.ENUM,
        #     endpoint_id=2,
        # ),
        # 10: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "quick_calibration",
        #     dp_type=TuyaDPType.ENUM,
        # ),
        # 11: DPToAttributeMapping(
        #     TuyaWindowCovering.ep_attribute,
        #     "quick_calibration",
        #     dp_type=TuyaDPType.ENUM,
        #     endpoint_id=2,
        # ),
        # 14: DPToAttributeMapping(
        #     TuyaMCUCluster.ep_attribute,
        #     "indicator_status",
        #     dp_type=TuyaDPType.ENUM,
        # ),
    }

    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",
        6: "_dp_2_attr_update",
        7: "_dp_2_attr_update",
        8: "_dp_2_attr_update",
        9: "_dp_2_attr_update",
        10: "_dp_2_attr_update",
        11: "_dp_2_attr_update",
        14: "_dp_2_attr_update",
    }


class TuyaCover0601_GP(TuyaWindowCover):
    """Tuya blind controller device."""
    signature = {
        # "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.AllocateAddress|RxOnWhenIdle|MainsPowered|FullFunctionDevice: 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
        # )
        MODELS_INFO: [
            ("_TZE200_r0jdjrvi", "TS0601"),
        ],
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaWindowCoverManufCluster.cluster_id,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: {
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaWindowCoverManufCluster,
                    TuyaWindowCovering,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: {
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        }
    }

@Drmatterpl
Copy link

Thanks! it works for me.
I have all I need - operating slider, no errors when hitting the buttons.
Before I had some errors with stop button - Failed to call service cover/stop_cover....

@javicalle
Copy link
Collaborator

The implementation needs to be fixed to handle the TuyaDPType.ENUM types. I will take a look in the weekend.

About the percentage behavior, does the cover have a remote? Is the control from remote fine?
My first suggestion would be to invert the motor movement (to fix the percentage) and then (if they are wrong) invert the TUYA2ZB_COMMANDS.

@caradas
Copy link
Author

caradas commented Mar 17, 2023

Yes i have a remote, up arrow on the remote opens, same goes for up arrow in ha.
Only the percentage is not intuitive as 100% is fully open

@javicalle
Copy link
Collaborator

First of all, you were right:

Shouldn't it be TuyaDPType.ENUM?

Yes it should, and maybe is the reason of the behavior.

Let's see if this can fix something

My proposal is add and replace this definitions:

class TuyaCC(t.enum8):
    """Tuya cover commands."""

    OPEN = 0x00
    STOP = 0x01
    CLOSE = 0x02


class ZclCC(t.enum8):
    """ZCL cover commands."""

    OPEN = 0x00
    CLOSE = 0x01
    STOP = 0x02


TUYA2ZB_COMMANDS = {
    ZclCC.OPEN: TuyaCC.OPEN,
    ZclCC.CLOSE: TuyaCC.CLOSE,
    ZclCC.STOP: TuyaCC.STOP,
}

Also, remove the int() wrap from #1953 (comment)

            cluster_data = TuyaClusterData(
                endpoint_id=self.endpoint.endpoint_id,
                cluster_name=self.ep_attribute,
                cluster_attr="curtain_switch",
                attr_value=TUYA2ZB_COMMANDS[command_id],  # convert tuya2zigbee command
                expect_reply=expect_reply,
                manufacturer=-1,
            )

Save changes, remove any __pycache__ folder and restart.

@javicalle
Copy link
Collaborator

BTW, do you have any motor_steering (0xF002) attribute in the TuyaWindowCovering cluster? Can you read the value and try to check setting 0/1 values?

@caradas
Copy link
Author

caradas commented Mar 20, 2023

Your Code proposal is working as expected. Thank you.

2023-03-20 13:50:02.029 DEBUG (MainThread) [zigpy.zcl] [0x3F47:1:0xef00] raw: b'\x01'

BTW, do you have any motor_steering (0xF002) attribute in the TuyaWindowCovering cluster? Can you read the value and try to check setting 0/1 values?

grafik
Log:

2023-03-20 13:55:32.972 DEBUG (MainThread) [zigpy.zcl] [0x3F47:1:0x0102] Couldn't normalize 61442 attribute with enum8.undefined_0x00 value
Traceback (most recent call last):
  File "/usr/local/lib/python3.10/site-packages/zigpy/zcl/__init__.py", line 481, in read_attributes
    value = self.attributes[record.attrid].type(record.value.value)
  File "/usr/local/lib/python3.10/site-packages/zigpy/types/basic.py", line 368, in __call__
    value = int(value)
ValueError: invalid literal for int() with base 10: 'enum8.undefined_0x00'

@danidask
Copy link

danidask commented Aug 6, 2023

It works. Let me sum up all the steps and the updated quirk file:
You'll need the HA File editor, can be installed via settings->add-ons
in the file editor, open config/configuration.yaml an add:

zha:
  custom_quirks_path: /config/custom_zha_quirks/

This will look for quirks in that folder, so create the folder custom_zha_quirks inside config (can be also done with File editor) and inside create a file named TZE200_r0jdjrvi.py and copy the content of this file:
TZE200_r0jdjrvi.py.txt
Restart Home Assistant

P.D for pairing mode, press the button in the base of the motor 3 times and then keep it pressed for the forth time for like two seconds. green led will start blinking

@utech-git
Copy link

Hello, guys.

I was having problem pairing and using a similar model curtain motor. But the the quirk file that @danidask posted on his last comment worked like a charm. I just had to change TZE200 to TZE204 on line 261, because my motor model is _TZE204_r0jdjrvi.

The one thing that still isn't working is the curtain state whenever i use the set_position service (either by calling it manually or by selecting the appropriate position on the scroller in the UI).

If click on the half of the bar (50%) and the curtain goes to that position, the state of the curtain keeps on "Opening" forever. I have to click the Stop button for the state to change to Open. The same thing happens with the Closing state.

Is there a way to fix this?

@tbhova
Copy link

tbhova commented Sep 24, 2023 via email

@javicalle
Copy link
Collaborator

Is there a way to fix this?

home-assistant/core#99646

@Onepamopa
Copy link

Onepamopa commented Oct 6, 2023

It works. Let me sum up all the steps and the updated quirk file: You'll need the HA File editor, can be installed via settings->add-ons in the file editor, open config/configuration.yaml an add:

zha:
  custom_quirks_path: /config/custom_zha_quirks/

This will look for quirks in that folder, so create the folder custom_zha_quirks inside config (can be also done with File editor) and inside create a file named TZE200_r0jdjrvi.py and copy the content of this file: TZE200_r0jdjrvi.py.txt Restart Home Assistant

P.D for pairing mode, press the button in the base of the motor 3 times and then keep it pressed for the forth time for like two seconds. green led will start blinking

_TZE200_r0jdjrvi TS0601 -- Added as described, but there is no slider?
Latest ha version...

IEEE: a4:c1:38:3e:e4:4e:76:90
Nwk: 0x431e
Device Type: Router
LQI: 255
RSSI: -75
Power source: Mains
Quirk: TZE200_r0jdjrvi.TuyaCover0601_GP

The motor is currently sitting on my desk (was never installed on the curtain track, was never calibrated (I've no idea how to calibrate it)).

Open / Close do rotate the motor, but when Stop is pressed - Failed to call service cover/stop_cover. '>' not supported between instances of 'NoneType' and 'int':

2023-10-07 22:13:07.392 ERROR (MainThread) [homeassistant.components.websocket_api.http.connection] [546958404032] '>' not supported between instances of 'NoneType' and 'int'
Traceback (most recent call last):
File "/usr/src/homeassistant/homeassistant/components/websocket_api/commands.py", line 226, in handle_call_service
await hass.services.async_call(
File "/usr/src/homeassistant/homeassistant/core.py", line 2012, in async_call
response_data = await coro
^^^^^^^^^^
File "/usr/src/homeassistant/homeassistant/core.py", line 2049, in _execute_service
return await target(service_call)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/src/homeassistant/homeassistant/helpers/entity_component.py", line 235, in handle_service
return await service.entity_service_call(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/src/homeassistant/homeassistant/helpers/service.py", line 876, in entity_service_call
response_data = await _handle_entity_call(
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/src/homeassistant/homeassistant/helpers/service.py", line 948, in _handle_entity_call
result = await task
^^^^^^^^^^
File "/usr/src/homeassistant/homeassistant/components/zha/cover.py", line 170, in async_stop_cover
self._state = STATE_OPEN if self._current_position > 0 else STATE_CLOSED
^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: '>' not supported between instances of 'NoneType' and 'int'

The curtain motor is this one:
IMG_6154
IMG_6155

@bschmidt006
Copy link

In case is helps someone in the future, I was able to use the above quirk to integrate a different model number: _TZE200_xu4a5rhj

I simply changed the model number in the quirk from _TZE200_r0jdjrvi to _TZE200_xu4a5rhj.

Open/Stop/Close seems to work fine.

I did have to set the motor reverse flag to get those going the correct direction.

@KirkKirk
Copy link

Hello, guys.

I was having problem pairing and using a similar model curtain motor. But the the quirk file that @danidask posted on his last comment worked like a charm. I just had to change TZE200 to TZE204 on line 261, because my motor model is _TZE204_r0jdjrvi.

The one thing that still isn't working is the curtain state whenever i use the set_position service (either by calling it manually or by selecting the appropriate position on the scroller in the UI).

If click on the half of the bar (50%) and the curtain goes to that position, the state of the curtain keeps on "Opening" forever. I have to click the Stop button for the state to change to Open. The same thing happens with the Closing state.

Is there a way to fix this?

Hi @utech-git
We have the same model (_TZE204_r0jdjrvi) and have encountered the same issue. I don't use rails and the only solution I have found is to set limits by forcing the motor to stop. However, I have to do this every time after a power outage because the motor forgets the limits. Have you found any other solution to set the limits?

@utech-git
Copy link

Hello, guys.
I was having problem pairing and using a similar model curtain motor. But the the quirk file that @danidask posted on his last comment worked like a charm. I just had to change TZE200 to TZE204 on line 261, because my motor model is _TZE204_r0jdjrvi.
The one thing that still isn't working is the curtain state whenever i use the set_position service (either by calling it manually or by selecting the appropriate position on the scroller in the UI).
If click on the half of the bar (50%) and the curtain goes to that position, the state of the curtain keeps on "Opening" forever. I have to click the Stop button for the state to change to Open. The same thing happens with the Closing state.
Is there a way to fix this?

Hi @utech-git We have the same model (_TZE204_r0jdjrvi) and have encountered the same issue. I don't use rails and the only solution I have found is to set limits by forcing the motor to stop. However, I have to do this every time after a power outage because the motor forgets the limits. Have you found any other solution to set the limits?

Well, every time after a power outage it loses the limits. But I don't do anything to get the limits back. Once the power is back, the first time I open the curtain it goes slowly to the beggining of the rail and when it reachs it automatically knows where the start limit is. The same way when I first close the curtain, it goes to the end of the rail and set the finish limit. And there it goes.

It knows where the end/beggining is when it feels resistance and can't go any further (just like you said when you force the motor to stop).

@utech-git
Copy link

In case is helps someone in the future, I was able to use the above quirk to integrate a different model number: _TZE200_xu4a5rhj

I simply changed the model number in the quirk from _TZE200_r0jdjrvi to _TZE200_xu4a5rhj.

Open/Stop/Close seems to work fine.

I did have to set the motor reverse flag to get those going the correct direction.

Could you please tell me how to set this "reverse flag"? I couldnt find it on the ZHA.
I had to first pair the motor on a tuya zigbee gateway, set the reverse flag on the tuya app, then delete it from tuya and pair on ZHA.

@bschmidt006
Copy link

bschmidt006 commented Oct 26, 2023

In case is helps someone in the future, I was able to use the above quirk to integrate a different model number: _TZE200_xu4a5rhj
I simply changed the model number in the quirk from _TZE200_r0jdjrvi to _TZE200_xu4a5rhj.
Open/Stop/Close seems to work fine.
I did have to set the motor reverse flag to get those going the correct direction.

Could you please tell me how to set this "reverse flag"? I couldnt find it on the ZHA. I had to first pair the motor on a tuya zigbee gateway, set the reverse flag on the tuya app, then delete it from tuya and pair on ZHA.

In ZHA - select the device. Under device info - hit the 3 dots -> Manage zigbee device.

  1. Ensure you're on the CLUSTERS tab.
  2. Select the TuyaWindowCovering cluster.
  3. Select the motor_steering attribute
  4. Read attribute to see what it's set to.
  5. Flip the value if needed and hit Write Attribute.

This is reflect in the quirk here:

            0xF002: ("motor_steering", t.enum8, True),  # 0: default, 1: reverse

@KirkKirk
Copy link

Hello, guys.
I was having problem pairing and using a similar model curtain motor. But the the quirk file that @danidask posted on his last comment worked like a charm. I just had to change TZE200 to TZE204 on line 261, because my motor model is _TZE204_r0jdjrvi.
The one thing that still isn't working is the curtain state whenever i use the set_position service (either by calling it manually or by selecting the appropriate position on the scroller in the UI).
If click on the half of the bar (50%) and the curtain goes to that position, the state of the curtain keeps on "Opening" forever. I have to click the Stop button for the state to change to Open. The same thing happens with the Closing state.
Is there a way to fix this?

Hi @utech-git We have the same model (_TZE204_r0jdjrvi) and have encountered the same issue. I don't use rails and the only solution I have found is to set limits by forcing the motor to stop. However, I have to do this every time after a power outage because the motor forgets the limits. Have you found any other solution to set the limits?

Well, every time after a power outage it loses the limits. But I don't do anything to get the limits back. Once the power is back, the first time I open the curtain it goes slowly to the beggining of the rail and when it reachs it automatically knows where the start limit is. The same way when I first close the curtain, it goes to the end of the rail and set the finish limit. And there it goes.

It knows where the end/beggining is when it feels resistance and can't go any further (just like you said when you force the motor to stop).

Thank you for your response. In my situation, I have vertical curtains that do not have a physical limit when they are lowered, so I need to force them to stop. I was wondering if it is possible to save the limits in Home Assistant or somewhere else, so that they can be uploaded in case of a power outage.

@utech-git
Copy link

In case is helps someone in the future, I was able to use the above quirk to integrate a different model number: _TZE200_xu4a5rhj
I simply changed the model number in the quirk from _TZE200_r0jdjrvi to _TZE200_xu4a5rhj.
Open/Stop/Close seems to work fine.
I did have to set the motor reverse flag to get those going the correct direction.

Could you please tell me how to set this "reverse flag"? I couldnt find it on the ZHA. I had to first pair the motor on a tuya zigbee gateway, set the reverse flag on the tuya app, then delete it from tuya and pair on ZHA.

In ZHA - select the device. Under device info - hit the 3 dots -> Manage zigbee device.

  1. Ensure you're on the CLUSTERS tab.
  2. Select the TuyaWindowCovering cluster.
  3. Select the motor_steering attribute
  4. Read attribute to see what it's set to.
  5. Flip the value if needed and hit Write Attribute.

This is reflect in the quirk here:

            0xF002: ("motor_steering", t.enum8, True),  # 0: default, 1: reverse

I did try to change this "True" to "False" on the quirk, at first. But nothing happened.

Now I tried on the Manage Zigbee Device menu... when I click "Read Attribute" it shows "None". I try to write attribute to 0 or 1, but when I read it again it continues "None" and the motor is still not reversed.

Maybe it doesn't work with this model, but thanks any way! :)

@Onepamopa
Copy link

@utech-git works fine with zigbee2mqtt ... I'm using it until zha gets enough supported devices working properly ....

@ATaylor5596
Copy link

I have a similar motor, model TZE200_idkvxabg. Thanks to everyone here I was able to get it to work with the most recent quirk, however, my open/close percentage stays out of sync with the remote control. Switching the motor direction will always have the remote and HA be opposite controls. Does anyone have any idea how to invert the display percentages?

@IntegersOfK
Copy link

In case is helps someone in the future, I was able to use the above quirk to integrate a different model number: _TZE200_xu4a5rhj
I simply changed the model number in the quirk from _TZE200_r0jdjrvi to _TZE200_xu4a5rhj.
Open/Stop/Close seems to work fine.
I did have to set the motor reverse flag to get those going the correct direction.

Could you please tell me how to set this "reverse flag"? I couldnt find it on the ZHA. I had to first pair the motor on a tuya zigbee gateway, set the reverse flag on the tuya app, then delete it from tuya and pair on ZHA.

In ZHA - select the device. Under device info - hit the 3 dots -> Manage zigbee device.

  1. Ensure you're on the CLUSTERS tab.
  2. Select the TuyaWindowCovering cluster.
  3. Select the motor_steering attribute
  4. Read attribute to see what it's set to.
  5. Flip the value if needed and hit Write Attribute.

This is reflect in the quirk here:

            0xF002: ("motor_steering", t.enum8, True),  # 0: default, 1: reverse

I did try to change this "True" to "False" on the quirk, at first. But nothing happened.

Now I tried on the Manage Zigbee Device menu... when I click "Read Attribute" it shows "None". I try to write attribute to 0 or 1, but when I read it again it continues "None" and the motor is still not reversed.

Maybe it doesn't work with this model, but thanks any way! :)

I have the same issue, _TZE204_r0jdjrvi model doesn't appear to accept any 0, 1, True, or False for reversing the motor. I wonder if there's a way to tell Home Assistant to treat it as the opposite. Did you figure out any other way to deal with that? I do have a remote but there's nothing described on the docs as to how one might reverse it on the device itself.

Copy link

There hasn't been any activity on this issue recently. Due to the high number of incoming GitHub notifications, we have to clean some of the old issues, as many of them have already been resolved with the latest updates. Please make sure to update to the latest version and check if that solves the issue. Let us know if that works for you by adding a comment 👍 This issue has now been marked as stale and will be closed if no further activity occurs. Thank you for your contributions.

@github-actions github-actions bot added stale Issue is inactivate and might get closed soon and removed stale Issue is inactivate and might get closed soon labels Aug 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
custom quirk available A custom quirk is available to solve the issue, but it's not merged in the repo yet Tuya Request/PR regarding a Tuya device
Projects
None yet
Development

No branches or pull requests