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

[Device Support Request] Tuya roller blind _TZE200_eevqq1uv TS0601 #3068

Open
matt-sullivan opened this issue Mar 25, 2024 · 0 comments · May be fixed by #3114
Open

[Device Support Request] Tuya roller blind _TZE200_eevqq1uv TS0601 #3068

matt-sullivan opened this issue Mar 25, 2024 · 0 comments · May be fixed by #3114
Labels
Tuya Request/PR regarding a Tuya device

Comments

@matt-sullivan
Copy link

matt-sullivan commented Mar 25, 2024

Problem description

Hi all, I'd like support for this TUYA roller blind, sold here https://www.zemismart.com/products/zm25r3

The blind reports 5 tuya data points in set_data_response and doesn't seem like a good match for the existing tuya cover quirks. (The existing quirks I tried were able to command it to move, but don't decode it's status/position.)

I've made good progress writing a custom quirk (it all looks like it works) but have few questions:

  1. Any idea why battery remaining doesn't work? I can see in the log my quirk is updating the power cluster attribute, but home assistant shows unknown. Resolved: I didn't have a power cluster derived from LocalDataCluster
  2. Is there a WindowCovering cluster attribute that tracks motor status? (I can't see one, but it seems weird there's isn't one to track opening/closing. My draft quirk creates a new one to allow me to use dp_to_attribute mappings to send an open/close command.)
  3. Is this the right way to map from commands to a send DPs to the TUYA device. I copied this from some of the existing TuyaMCUCluster-based examples (e.g. TuyaRCBOMetering.) It seems a bit abstract to override the command method just to map to an attribute, especially when no standard attribute exists. Resolved: seems like the accepted way to do it.
  4. Any idea why the _CONSTANT_ATTRIBUTES don't work? (I was hoping these might fix the battery and show the window cover type, but HA doesn't show values in the entity nor when reading the zigbee attributes. Resolved: same or similar as battery problem.
  5. Do I need to consider using TuyaNoBindPowerConfigurationCluster or the tuya spell variants? (How would I know if I do?) Resolved: appears to work fine without it.
  6. Does this need to be rewritten for quirks v2? I've read what I can find about v2, but from what I can tell v2 wouldn't reduce code or simplify this quirk much.

Solution description

Fix battery percent in my custom quirk
New quirk integrated in zha without needing a custom one.

Screenshots/Video

Screenshots/Video

[Paste/upload your media here]

Device signature

Device signature
{
  "node_descriptor": "NodeDescriptor(logical_type=<LogicalType.EndDevice: 2>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.AllocateAddress: 128>, manufacturer_code=4098, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=<DescriptorCapability.NONE: 0>, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=True, *is_full_function_device=False, *is_mains_powered=False, *is_receiver_on_when_idle=False, *is_router=False, *is_security_capable=False)",
  "endpoints": {
    "1": {
      "profile_id": "0x0104",
      "device_type": "0x0051",
      "input_clusters": [
        "0x0000",
        "0x0004",
        "0x0005",
        "0xef00"
      ],
      "output_clusters": [
        "0x000a",
        "0x0019"
      ]
    }
  },
  "manufacturer": "_TZE200_eevqq1uv",
  "model": "TS0601",
  "class": "zigpy.device.Device"
}

Diagnostic information

Diagnostic information
{
  "home_assistant": {
    "installation_type": "Home Assistant OS",
    "version": "2024.3.1",
    "dev": false,
    "hassio": true,
    "virtualenv": false,
    "python_version": "3.12.2",
    "docker": true,
    "arch": "x86_64",
    "timezone": "Australia/Brisbane",
    "os_name": "Linux",
    "os_version": "6.6.20-haos",
    "supervisor": "2024.03.0",
    "host_os": "Home Assistant OS 12.1",
    "docker_version": "24.0.7",
    "chassis": "vm",
    "run_as_root": true
  },
  "custom_components": {
    "sonoff": {
      "version": "3.6.0",
      "requirements": [
        "pycryptodome>=3.6.6"
      ]
    },
    "meross_lan": {
      "version": "5.0.2",
      "requirements": []
    },
    "hacs": {
      "version": "1.34.0",
      "requirements": [
        "aiogithubapi>=22.10.1"
      ]
    },
    "spotcast": {
      "version": "v3.6.30",
      "requirements": []
    },
    "zha_toolkit": {
      "version": "v1.1.10",
      "requirements": [
        "pytz"
      ]
    }
  },
  "integration_manifest": {
    "domain": "zha",
    "name": "Zigbee Home Automation",
    "after_dependencies": [
      "onboarding",
      "usb"
    ],
    "codeowners": [
      "@dmulcahey",
      "@adminiuga",
      "@puddly",
      "@TheJulianJES"
    ],
    "config_flow": true,
    "dependencies": [
      "file_upload"
    ],
    "documentation": "https://www.home-assistant.io/integrations/zha",
    "import_executor": true,
    "iot_class": "local_polling",
    "loggers": [
      "aiosqlite",
      "bellows",
      "crccheck",
      "pure_pcapy3",
      "zhaquirks",
      "zigpy",
      "zigpy_deconz",
      "zigpy_xbee",
      "zigpy_zigate",
      "zigpy_znp",
      "universal_silabs_flasher"
    ],
    "requirements": [
      "bellows==0.38.1",
      "pyserial==3.5",
      "pyserial-asyncio==0.6",
      "zha-quirks==0.0.112",
      "zigpy-deconz==0.23.1",
      "zigpy==0.63.4",
      "zigpy-xbee==0.20.1",
      "zigpy-zigate==0.12.0",
      "zigpy-znp==0.12.1",
      "universal-silabs-flasher==0.0.18",
      "pyserial-asyncio-fast==0.11"
    ],
    "usb": [
      {
        "vid": "10C4",
        "pid": "EA60",
        "description": "*2652*",
        "known_devices": [
          "slae.sh cc2652rb stick"
        ]
      },
      {
        "vid": "10C4",
        "pid": "EA60",
        "description": "*slzb-07*",
        "known_devices": [
          "smlight slzb-07"
        ]
      },
      {
        "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": "0403",
        "pid": "6015",
        "description": "*conbee*",
        "known_devices": [
          "Conbee III"
        ]
      },
      {
        "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"
        ]
      }
    ],
    "zeroconf": [
      {
        "type": "_esphomelib._tcp.local.",
        "name": "tube*"
      },
      {
        "type": "_zigate-zigbee-gateway._tcp.local.",
        "name": "*zigate*"
      },
      {
        "type": "_zigstar_gw._tcp.local.",
        "name": "*zigstar*"
      },
      {
        "type": "_uzg-01._tcp.local.",
        "name": "uzg-01*"
      },
      {
        "type": "_slzb-06._tcp.local.",
        "name": "slzb-06*"
      }
    ],
    "is_built_in": true
  },
  "data": {
    "ieee": "**REDACTED**",
    "nwk": 19527,
    "manufacturer": "_TZE200_eevqq1uv",
    "model": "TS0601",
    "name": "_TZE200_eevqq1uv TS0601",
    "quirk_applied": false,
    "quirk_class": "zigpy.device.Device",
    "quirk_id": null,
    "manufacturer_code": 4098,
    "power_source": "Battery or Unknown",
    "lqi": 124,
    "rssi": -69,
    "last_seen": "2024-03-25T22:58:25",
    "available": true,
    "device_type": "EndDevice",
    "signature": {
      "node_descriptor": "NodeDescriptor(logical_type=<LogicalType.EndDevice: 2>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.AllocateAddress: 128>, manufacturer_code=4098, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=<DescriptorCapability.NONE: 0>, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=True, *is_full_function_device=False, *is_mains_powered=False, *is_receiver_on_when_idle=False, *is_router=False, *is_security_capable=False)",
      "endpoints": {
        "1": {
          "profile_id": "0x0104",
          "device_type": "0x0051",
          "input_clusters": [
            "0x0000",
            "0x0004",
            "0x0005",
            "0xef00"
          ],
          "output_clusters": [
            "0x000a",
            "0x0019"
          ]
        }
      },
      "manufacturer": "_TZE200_eevqq1uv",
      "model": "TS0601"
    },
    "active_coordinator": false,
    "entities": [
      {
        "entity_id": "update.testblind6_firmware",
        "name": "_TZE200_eevqq1uv TS0601"
      }
    ],
    "neighbors": [],
    "routes": [],
    "endpoint_names": [
      {
        "name": "SMART_PLUG"
      }
    ],
    "user_given_name": "testblind6",
    "device_reg_id": "842d7945e61502c7d61023b65d80e261",
    "area_id": null,
    "cluster_details": {
      "1": {
        "device_type": {
          "name": "SMART_PLUG",
          "id": 81
        },
        "profile_id": 260,
        "in_clusters": {
          "0x0000": {
            "endpoint_attribute": "basic",
            "attributes": {
              "0x0001": {
                "attribute_name": "app_version",
                "value": 65
              },
              "0x0004": {
                "attribute_name": "manufacturer",
                "value": "_TZE200_eevqq1uv"
              },
              "0x0005": {
                "attribute_name": "model",
                "value": "TS0601"
              }
            },
            "unsupported_attributes": {}
          },
          "0x0004": {
            "endpoint_attribute": "groups",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x0005": {
            "endpoint_attribute": "scenes",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0xef00": {
            "endpoint_attribute": null,
            "attributes": {},
            "unsupported_attributes": {}
          }
        },
        "out_clusters": {
          "0x0019": {
            "endpoint_attribute": "ota",
            "attributes": {},
            "unsupported_attributes": {
              "0x0002": {
                "attribute_name": "current_file_version"
              }
            }
          },
          "0x000a": {
            "endpoint_attribute": "time",
            "attributes": {},
            "unsupported_attributes": {}
          }
        }
      }
    }
  }
}

Logs

Logs

When using one of the existing TUYA cover quirks:

Sending a up/down command
2024-03-19 22:39:04.260 DEBUG (MainThread) [zhaquirks.tuya] 84:b4:db:ff:fe:c0:95:98 Sending Tuya Cluster Command.. Manufacturer is _TZE200_eevqq1uv Cluster Command is 0x0001, Arguments are ()
2024-03-19 22:39:04.260 DEBUG (MainThread) [zhaquirks.tuya] 84:b4:db:ff:fe:c0:95:98 Sending Tuya Command. Paylod values [endpoint_id : 1, Status : 0, TSN: 0, Command: 0x0401, Function: 0, Data: [1, 2]]
2024-03-19 22:39:04.260 DEBUG (MainThread) [zigpy.zcl] [0x7106:1:0xef00] Sending request header: ZCLHeader(frame_control=FrameControl<0x05>(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=True, direction=<Direction.Client_to_Server: 0>, disable_default_response=0, reserved=0, *is_cluster=True, *is_general=False), manufacturer=4098, tsn=52, command_id=0, *direction=<Direction.Client_to_Server: 0>)
2024-03-19 22:39:04.261 DEBUG (MainThread) [zigpy.zcl] [0x7106:1:0xef00] Sending request: set_data(param=Command(status=0, tsn=0, command_id=1025, function=0, data=[1, 2]))
2024-03-19 22:39:04.261 DEBUG (MainThread) [zigpy.device] [0x7106] Extending timeout for 0x34 request
2024-03-19 22:39:04.514 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(timestamp=datetime.datetime(2024, 3, 19, 12, 39, 4, 514942, tzinfo=datetime.timezone.utc), src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x7106), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=165, profile_id=260, cluster_id=61184, data=Serialized[b'\x0c\x02\x104\x0b\x00\x00'], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=152, rssi=-62)
2024-03-19 22:39:04.515 DEBUG (MainThread) [zigpy.zcl] [0x7106:1:0xef00] Received ZCL frame: b'\x0c\x02\x104\x0b\x00\x00'
2024-03-19 22:39:04.516 DEBUG (MainThread) [zigpy.zcl] [0x7106:1:0xef00] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x0C>(frame_type=<FrameType.GLOBAL_COMMAND: 0>, is_manufacturer_specific=True, direction=<Direction.Server_to_Client: 1>, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4098, tsn=52, command_id=11, *direction=<Direction.Server_to_Client: 1>)
2024-03-19 22:39:04.516 DEBUG (MainThread) [zigpy.zcl] [0x7106:1:0xef00] Decoded ZCL frame: TuyaManufacturerWindowCover:Default_Response(command_id=0, status=<Status.SUCCESS: 0>)
2024-03-19 22:39:04.527 DEBUG (MainThread) [homeassistant.components.zha.cover] async_update_state=closing
2024-03-19 22:39:04.556 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(timestamp=datetime.datetime(2024, 3, 19, 12, 39, 4, 556759, tzinfo=datetime.timezone.utc), src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x7106), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=166, profile_id=260, cluster_id=61184, data=Serialized[b'\x19f\x01\x00\x00\x01\x04\x00\x01\x02'], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=156, rssi=-61)
2024-03-19 22:39:04.557 DEBUG (MainThread) [zigpy.zcl] [0x7106:1:0xef00] Received ZCL frame: b'\x19f\x01\x00\x00\x01\x04\x00\x01\x02'
2024-03-19 22:39:04.557 DEBUG (MainThread) [zigpy.zcl] [0x7106:1:0xef00] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x19>(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=0, direction=<Direction.Server_to_Client: 1>, disable_default_response=1, reserved=0, *is_cluster=True, *is_general=False), tsn=102, command_id=1, *direction=<Direction.Server_to_Client: 1>)
2024-03-19 22:39:04.558 DEBUG (MainThread) [zigpy.zcl] [0x7106:1:0xef00] Decoded ZCL frame: TuyaManufacturerWindowCover:get_data(param=Command(status=0, tsn=0, command_id=1025, function=0, data=[1, 2]))
2024-03-19 22:39:04.558 DEBUG (MainThread) [zigpy.zcl] [0x7106:1:0xef00] Received command 0x01 (TSN 102): get_data(param=Command(status=0, tsn=0, command_id=1025, function=0, data=[1, 2]))
2024-03-19 22:39:04.558 DEBUG (MainThread) [zhaquirks.tuya] 84:b4:db:ff:fe:c0:95:98 Received Attribute Report. Command is 0x0001, Tuya Paylod values[Status : 0, TSN: 0, Command: 0x0401, Function: 0x00, Data: [1, 2]]

When the blind stops at it's limit:
2024-03-19 22:39:07.200 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(timestamp=datetime.datetime(2024, 3, 19, 12, 39, 7, 200525, tzinfo=datetime.timezone.utc), src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x7106), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=167, profile_id=260, cluster_id=61184, data=Serialized[b'\tg\x02\x00\x00\x01\x04\x00\x01\x01\x07\x04\x00\x01\x00\x03\x02\x00\x04\x00\x00\x00\x00\x05\x04\x00\x01\x01\r\x02\x00\x04\x00\x00\x00^'], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=164, rssi=-59)
2024-03-19 22:39:07.201 DEBUG (MainThread) [zigpy.zcl] [0x7106:1:0xef00] Received ZCL frame: b'\tg\x02\x00\x00\x01\x04\x00\x01\x01\x07\x04\x00\x01\x00\x03\x02\x00\x04\x00\x00\x00\x00\x05\x04\x00\x01\x01\r\x02\x00\x04\x00\x00\x00^'
2024-03-19 22:39:07.201 DEBUG (MainThread) [zigpy.zcl] [0x7106:1:0xef00] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x09>(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=0, direction=<Direction.Server_to_Client: 1>, disable_default_response=0, reserved=0, *is_cluster=True, *is_general=False), tsn=103, command_id=2, *direction=<Direction.Server_to_Client: 1>)
2024-03-19 22:39:07.202 DEBUG (MainThread) [zigpy.zcl] [0x7106:1:0xef00] Decoded ZCL frame: TuyaManufacturerWindowCover:set_data_response(param=Command(status=0, tsn=0, command_id=1025, function=0, data=[1, 1, 7, 4, 0, 1, 0, 3, 2, 0, 4, 0, 0, 0, 0, 5, 4, 0, 1, 1, 13, 2, 0, 4, 0, 0, 0, 94]))
2024-03-19 22:39:07.202 DEBUG (MainThread) [zigpy.zcl] [0x7106:1:0xef00] Received command 0x02 (TSN 103): set_data_response(param=Command(status=0, tsn=0, command_id=1025, function=0, data=[1, 1, 7, 4, 0, 1, 0, 3, 2, 0, 4, 0, 0, 0, 0, 5, 4, 0, 1, 1, 13, 2, 0, 4, 0, 0, 0, 94]))
2024-03-19 22:39:07.202 DEBUG (MainThread) [zhaquirks.tuya] 84:b4:db:ff:fe:c0:95:98 Received Attribute Report. Command is 0x0002, Tuya Paylod values[Status : 0, TSN: 0, Command: 0x0401, Function: 0x00, Data: [1, 1, 7, 4, 0, 1, 0, 3, 2, 0, 4, 0, 0, 0, 0, 5, 4, 0, 1, 1, 13, 2, 0, 4, 0, 0, 0, 94]]

With my custom quirk:

Sending a up/down command
2024-03-25 23:55:33.959 DEBUG (MainThread) [zigpy.zcl] [0x4C47:1:0x0102] Sending Tuya Cluster Command... Cluster Command is 1, Arguments are ()
2024-03-25 23:55:33.959 DEBUG (MainThread) [zigpy.zcl] [0x4C47:1:0xef00] tuya_mcu_command: cluster_data=TuyaClusterData(endpoint_id=1, cluster_name='window_covering', cluster_attr='motor_direction', attr_value=2, expect_reply=True)
2024-03-25 23:55:33.960 DEBUG (MainThread) [zigpy.zcl] [0x4C47:1:0xef00] get_dp_mapping --> found DP: 1
2024-03-25 23:55:33.960 DEBUG (MainThread) [zigpy.zcl] [0x4C47:1:0xef00] from_cluster_data: {1: DPToAttributeMapping(ep_attribute='window_covering', attribute_name='motor_direction', converter=None, dp_converter=<function TuyaMCUClusterForWindowCover.<lambda> at 0x7fc0380bd4e0>, endpoint_id=None)}
2024-03-25 23:55:33.960 DEBUG (MainThread) [zigpy.zcl] [0x4C47:1:0xef00] value: CoverMotionCommandDirection.Close
2024-03-25 23:55:33.960 DEBUG (MainThread) [zigpy.zcl] [0x4C47:1:0xef00] raw: b'\x02'
2024-03-25 23:55:33.960 DEBUG (MainThread) [zigpy.zcl] [0x4C47:1:0xef00] tuya_commands: [TuyaCommand(status=0, tsn=34, datapoints=[TuyaDatapointData(dp=1, data=TuyaData(dp_type=<TuyaDPType.ENUM: 4>, function=0, raw=b'\x02', *payload=<enum8.undefined_0x02: 2>))])]
2024-03-25 23:55:33.961 DEBUG (MainThread) [homeassistant.components.zha.core.cluster_handlers] [0x4C47:1:0x0102]: cluster_handler[window_covering] attribute_updated - cluster[Window Covering] attr[motor_direction] value[2]
2024-03-25 23:55:33.961 DEBUG (MainThread) [homeassistant.components.zha.cover] async_update_state=closing
2024-03-25 23:55:33.962 DEBUG (MainThread) [zigpy.zcl] [0x4C47:1:0xef00] Sending request header: ZCLHeader(frame_control=FrameControl<0x05>(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=True, direction=<Direction.Client_to_Server: 0>, disable_default_response=0, reserved=0, *is_cluster=True, *is_general=False), manufacturer=4098, tsn=4, command_id=0, *direction=<Direction.Client_to_Server: 0>)
2024-03-25 23:55:33.962 DEBUG (MainThread) [zigpy.zcl] [0x4C47:1:0xef00] Sending request: set_data(data=TuyaCommand(status=0, tsn=34, datapoints=[TuyaDatapointData(dp=1, data=TuyaData(dp_type=<TuyaDPType.ENUM: 4>, function=0, raw=b'\x02', *payload=<enum8.undefined_0x02: 2>))]))
2024-03-25 23:55:33.963 DEBUG (MainThread) [zigpy.device] [0x4c47] Extending timeout for 0x04 request
2024-03-25 23:55:34.378 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(timestamp=datetime.datetime(2024, 3, 25, 13, 55, 34, 378394, tzinfo=datetime.timezone.utc), src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x4C47), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=65, profile_id=260, cluster_id=61184, data=Serialized[b'\x0c\x02\x10\x04\x0b\x00\x00'], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=120, rssi=-70)
2024-03-25 23:55:34.379 DEBUG (MainThread) [zigpy.zcl] [0x4C47:1:0xef00] Received ZCL frame: b'\x0c\x02\x10\x04\x0b\x00\x00'
2024-03-25 23:55:34.379 DEBUG (MainThread) [zigpy.zcl] [0x4C47:1:0xef00] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x0C>(frame_type=<FrameType.GLOBAL_COMMAND: 0>, is_manufacturer_specific=True, direction=<Direction.Server_to_Client: 1>, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4098, tsn=4, command_id=11, *direction=<Direction.Server_to_Client: 1>)
2024-03-25 23:55:34.380 DEBUG (MainThread) [zigpy.zcl] [0x4C47:1:0xef00] Decoded ZCL frame: TuyaMCUClusterForWindowCover:Default_Response(command_id=0, status=<Status.SUCCESS: 0>)
2024-03-25 23:55:34.424 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(timestamp=datetime.datetime(2024, 3, 25, 13, 55, 34, 424844, tzinfo=datetime.timezone.utc), src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x4C47), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=66, profile_id=260, cluster_id=61184, data=Serialized[b'\x19\x1c\x01\x00"\x01\x04\x00\x01\x02'], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=124, rssi=-69)
2024-03-25 23:55:34.425 DEBUG (MainThread) [zigpy.zcl] [0x4C47:1:0xef00] Received ZCL frame: b'\x19\x1c\x01\x00"\x01\x04\x00\x01\x02'
2024-03-25 23:55:34.425 DEBUG (MainThread) [zigpy.zcl] [0x4C47:1:0xef00] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x19>(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=0, direction=<Direction.Server_to_Client: 1>, disable_default_response=1, reserved=0, *is_cluster=True, *is_general=False), tsn=28, command_id=1, *direction=<Direction.Server_to_Client: 1>)
2024-03-25 23:55:34.426 DEBUG (MainThread) [zigpy.zcl] [0x4C47:1:0xef00] Decoded ZCL frame: TuyaMCUClusterForWindowCover:get_data(data=TuyaCommand(status=0, tsn=34, datapoints=[TuyaDatapointData(dp=1, data=TuyaData(dp_type=<TuyaDPType.ENUM: 4>, function=0, raw=b'\x02', *payload=<enum8.undefined_0x02: 2>))]))
2024-03-25 23:55:34.427 DEBUG (MainThread) [zigpy.zcl] [0x4C47:1:0xef00] Received command 0x01 (TSN 28): get_data(data=TuyaCommand(status=0, tsn=34, datapoints=[TuyaDatapointData(dp=1, data=TuyaData(dp_type=<TuyaDPType.ENUM: 4>, function=0, raw=b'\x02', *payload=<enum8.undefined_0x02: 2>))]))
2024-03-25 23:55:34.427 DEBUG (MainThread) [homeassistant.components.zha.core.cluster_handlers] [0x4C47:1:0x0102]: cluster_handler[window_covering] attribute_updated - cluster[Window Covering] attr[motor_direction] value[enum8.undefined_0x02]

When it reaches the limit and stops
2024-03-25 23:55:35.749 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(timestamp=datetime.datetime(2024, 3, 25, 13, 55, 35, 749673, tzinfo=datetime.timezone.utc), src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x4C47), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=67, profile_id=260, cluster_id=61184, data=Serialized[b'\t\x1d\x02\x00\x00\x01\x04\x00\x01\x01\x07\x04\x00\x01\x01\x03\x02\x00\x04\x00\x00\x00\x00\x05\x04\x00\x01\x01\r\x02\x00\x04\x00\x00\x00\\'], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=124, rssi=-69)
2024-03-25 23:55:35.750 DEBUG (MainThread) [zigpy.zcl] [0x4C47:1:0xef00] Received ZCL frame: b'\t\x1d\x02\x00\x00\x01\x04\x00\x01\x01\x07\x04\x00\x01\x01\x03\x02\x00\x04\x00\x00\x00\x00\x05\x04\x00\x01\x01\r\x02\x00\x04\x00\x00\x00\\'
2024-03-25 23:55:35.750 DEBUG (MainThread) [zigpy.zcl] [0x4C47:1:0xef00] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x09>(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=0, direction=<Direction.Server_to_Client: 1>, disable_default_response=0, reserved=0, *is_cluster=True, *is_general=False), tsn=29, command_id=2, *direction=<Direction.Server_to_Client: 1>)
2024-03-25 23:55:35.751 DEBUG (MainThread) [zigpy.zcl] [0x4C47:1:0xef00] Decoded ZCL frame: TuyaMCUClusterForWindowCover:set_data_response(data=TuyaCommand(status=0, tsn=0, datapoints=[TuyaDatapointData(dp=1, data=TuyaData(dp_type=<TuyaDPType.ENUM: 4>, function=0, raw=b'\x01', *payload=<enum8.undefined_0x01: 1>)), TuyaDatapointData(dp=7, data=TuyaData(dp_type=<TuyaDPType.ENUM: 4>, function=0, raw=b'\x01', *payload=<enum8.undefined_0x01: 1>)), TuyaDatapointData(dp=3, data=TuyaData(dp_type=<TuyaDPType.VALUE: 2>, function=0, raw=b'\x00\x00\x00\x00', *payload=0)), TuyaDatapointData(dp=5, data=TuyaData(dp_type=<TuyaDPType.ENUM: 4>, function=0, raw=b'\x01', *payload=<enum8.undefined_0x01: 1>)), TuyaDatapointData(dp=13, data=TuyaData(dp_type=<TuyaDPType.VALUE: 2>, function=0, raw=b'\x00\x00\x00\\', *payload=92))]))
2024-03-25 23:55:35.752 DEBUG (MainThread) [zigpy.zcl] [0x4C47:1:0xef00] Received command 0x02 (TSN 29): set_data_response(data=TuyaCommand(status=0, tsn=0, datapoints=[TuyaDatapointData(dp=1, data=TuyaData(dp_type=<TuyaDPType.ENUM: 4>, function=0, raw=b'\x01', *payload=<enum8.undefined_0x01: 1>)), TuyaDatapointData(dp=7, data=TuyaData(dp_type=<TuyaDPType.ENUM: 4>, function=0, raw=b'\x01', *payload=<enum8.undefined_0x01: 1>)), TuyaDatapointData(dp=3, data=TuyaData(dp_type=<TuyaDPType.VALUE: 2>, function=0, raw=b'\x00\x00\x00\x00', *payload=0)), TuyaDatapointData(dp=5, data=TuyaData(dp_type=<TuyaDPType.ENUM: 4>, function=0, raw=b'\x01', *payload=<enum8.undefined_0x01: 1>)), TuyaDatapointData(dp=13, data=TuyaData(dp_type=<TuyaDPType.VALUE: 2>, function=0, raw=b'\x00\x00\x00\\', *payload=92))]))
2024-03-25 23:55:35.753 DEBUG (MainThread) [homeassistant.components.zha.core.cluster_handlers] [0x4C47:1:0x0102]: cluster_handler[window_covering] attribute_updated - cluster[Window Covering] attr[motor_direction] value[enum8.undefined_0x01]
2024-03-25 23:55:35.753 DEBUG (MainThread) [homeassistant.components.zha.core.cluster_handlers] [0x4C47:1:0x0102]: cluster_handler[window_covering] attribute_updated - cluster[Window Covering] attr[current_position_lift_percentage] value[100]
2024-03-25 23:55:35.754 DEBUG (MainThread) [homeassistant.components.zha.core.cluster_handlers] [0x4C47:1:0x0001]: cluster_handler[power] attribute_updated - cluster[Power Configuration] attr[battery_percentage_remaining] value[92]
2024-03-25 23:55:35.755 DEBUG (MainThread) [zigpy.zcl] [0x4C47:1:0xef00] Sending reply header: ZCLHeader(frame_control=FrameControl<0x10>(frame_type=<FrameType.GLOBAL_COMMAND: 0>, is_manufacturer_specific=False, direction=<Direction.Client_to_Server: 0>, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=29, command_id=<GeneralCommand.Default_Response: 11>, *direction=<Direction.Client_to_Server: 0>)
2024-03-25 23:55:35.755 DEBUG (MainThread) [zigpy.zcl] [0x4C47:1:0xef00] Sending reply: Default_Response(command_id=2, status=<Status.SUCCESS: 0>)

Custom quirk

Custom quirk
"""Tuya based cover and blinds."""
import logging
from typing import Any, Optional, Union

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

from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
)
from zhaquirks.tuya import (
    CustomDevice,
    TUYA_MCU_COMMAND,
    TuyaDatapointData,
    TuyaLocalCluster
)

from zhaquirks.tuya.mcu import DPToAttributeMapping, TuyaClusterData, TuyaMCUCluster

# TODO - access these from WindowCover.ServerCommandDefs
WINDOW_COVER_COMMAND_UPOPEN = 0x0000
WINDOW_COVER_COMMAND_DOWNCLOSE = 0x0001
WINDOW_COVER_COMMAND_STOP = 0x0002
WINDOW_COVER_COMMAND_LIFTPERCENT = 0x0005
WINDOW_COVER_COMMAND_CUSTOM = 0x0006
# ---------------------------------------------------------
# TUYA Cover Custom Values
# ---------------------------------------------------------
ATTR_COVER_DIRECTION = 0x8001
ATTR_COVER_DIRECTION_NAME = "motor_direction"
ATTR_COVER_INVERTED = 0x8002
ATTR_COVER_LIFTPERCENT_NAME = "current_position_lift_percentage"
ATTR_COVER_LIFTPERCENT_CONTROL = 0x8003
ATTR_COVER_LIFTPERCENT_CONTROL_NAME = "current_position_lift_percentage_control"

_LOGGER = logging.getLogger(__name__)

class CoverMotionCommandDirection(t.enum8):
    """window cover motion command direction enum."""
    Open = 0x00
    Close = 0x02
    Stop = 0x01

class TuyaNewWindowCoverCluster(WindowCovering, TuyaLocalCluster):
    """Tuya Window Cover Cluster"""
    # Use TuyaLocalCluster to disable attribute writes, compatible with TuyaNewManufCluster (not
    # TuyaManufCluster.)

    attributes = WindowCovering.attributes.copy()
    attributes.update({ATTR_COVER_DIRECTION: ("motor_direction", t.enum8, True)})
    attributes.update({ATTR_COVER_INVERTED: ("cover_inverted", t.Bool)})
    attributes.update({ATTR_COVER_LIFTPERCENT_CONTROL: (ATTR_COVER_LIFTPERCENT_CONTROL_NAME, t.uint16_t)})

    # For most tuya devices 0 = Up/Open, 1 = Stop, 2 = Down/Close
    tuya_cover_command: dict[int, CoverMotionCommandDirection] = {
        WINDOW_COVER_COMMAND_UPOPEN: CoverMotionCommandDirection.Open,
        WINDOW_COVER_COMMAND_DOWNCLOSE: CoverMotionCommandDirection.Close,
        WINDOW_COVER_COMMAND_STOP: CoverMotionCommandDirection.Stop,
    }

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

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

        # Open Close or Stop commands
        if command_id in (
            WINDOW_COVER_COMMAND_UPOPEN,
            WINDOW_COVER_COMMAND_DOWNCLOSE,
            WINDOW_COVER_COMMAND_STOP,
        ):
            # TODO - Copy more functionality from TuyaWindowCoverControl to handle inverted
            # controls here, in set by position, and perhaps decoding the current position
            cluster_data = TuyaClusterData(
                endpoint_id=self.endpoint.endpoint_id,
                cluster_name=self.ep_attribute,
                cluster_attr=ATTR_COVER_DIRECTION_NAME,
                # Map from zigbee command to tuya DP value
                attr_value=self.tuya_cover_command[command_id],
                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)
        elif command_id == WINDOW_COVER_COMMAND_LIFTPERCENT:
            # TODO - This isn't working, sending set_data DP3 doesn't move the blind
            # TODO - Handle inverted lift vs. closed percent
            cluster_data = TuyaClusterData(
                endpoint_id=self.endpoint.endpoint_id,
                cluster_name=self.ep_attribute,
                cluster_attr=ATTR_COVER_LIFTPERCENT_CONTROL_NAME,
                attr_value=args[0],
                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)
        _LOGGER.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 TuyaNewPowerConfigurationCluster(PowerConfiguration, TuyaLocalCluster):
    """PowerConfiguration cluster reliant on TUYA data point decoding."""

class TuyaMCUClusterForWindowCover(TuyaMCUCluster):
    """Tuya translations between attributes and MCU data point commands"""

    dp_to_attribute: dict[int, DPToAttributeMapping] = {
        1: DPToAttributeMapping(
            TuyaNewWindowCoverCluster.ep_attribute,
            ATTR_COVER_DIRECTION_NAME
        ),
        2: DPToAttributeMapping(
            TuyaNewWindowCoverCluster.ep_attribute,
            ATTR_COVER_LIFTPERCENT_CONTROL_NAME,
            # blind reports % closed, cluster attribute is % open
            lambda x: 100 - x,
            lambda x: 100 - x
        ),
        3: DPToAttributeMapping(
            TuyaNewWindowCoverCluster.ep_attribute,
            ATTR_COVER_LIFTPERCENT_NAME,
            # blind reports % closed, cluster attribute is % open
            lambda x: 100 - x,
            lambda x: 100 - x
        ),
        13: DPToAttributeMapping(
            PowerConfiguration.ep_attribute,
            "battery_percentage_remaining",
            # zigbee spec for battery is to return percent x 2
            lambda x: x * 2
        ),
    }

    data_point_handlers = {
        1: "_dp_2_attr_update",
        2: "_dp_2_attr_update",
        3: "_dp_2_attr_update",
        # Define DPs 5 & 7 to avoid warning logs, but I'm not sure what they are so just ignore
        # updates from them
        5: "ignore_update",
        7: "ignore_update",
        13: "_dp_2_attr_update",
    }

    def ignore_update(self, datapoint: TuyaDatapointData) -> None:
        return None

class TuyaCover0601MultipleDataPoints(CustomDevice):
    """Tuya blind controller device, sending multiple data points in set_data_response."""

    signature = {
        MODELS_INFO: [
            ("_TZE200_eevqq1uv", "TS0601"), # Zemismart ZM25R3
        ],
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaMCUCluster.cluster_id,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            }
        },
    }

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

Additional information

No response

@TheJulianJES TheJulianJES added the Tuya Request/PR regarding a Tuya device label Mar 25, 2024
@matt-sullivan matt-sullivan linked a pull request Apr 17, 2024 that will close this issue
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Tuya Request/PR regarding a Tuya device
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants