zigpy / zha-device-handlers

ZHA device handlers bridge the functionality gap created when manufacturers deviate from the ZCL specification, handling deviations and exceptions by parsing custom messages to and from Zigbee devices.
Apache License 2.0
743 stars 679 forks source link

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

Open matt-sullivan opened 6 months ago

matt-sullivan commented 6 months ago

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 ```json { "node_descriptor": "NodeDescriptor(logical_type=, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=, mac_capability_flags=, manufacturer_code=4098, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=, *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 ```json { "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=, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=, mac_capability_flags=, manufacturer_code=4098, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=, *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: ```python 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=, is_manufacturer_specific=True, direction=, disable_default_response=0, reserved=0, *is_cluster=True, *is_general=False), manufacturer=4098, tsn=52, command_id=0, *direction=) 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=, address=0x7106), src_ep=1, dst=AddrModeAddress(addr_mode=, 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=, 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=, is_manufacturer_specific=True, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4098, tsn=52, command_id=11, *direction=) 2024-03-19 22:39:04.516 DEBUG (MainThread) [zigpy.zcl] [0x7106:1:0xef00] Decoded ZCL frame: TuyaManufacturerWindowCover:Default_Response(command_id=0, status=) 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=, address=0x7106), src_ep=1, dst=AddrModeAddress(addr_mode=, 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=, 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=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=True, *is_general=False), tsn=102, command_id=1, *direction=) 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=, address=0x7106), src_ep=1, dst=AddrModeAddress(addr_mode=, 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=, 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=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=True, *is_general=False), tsn=103, command_id=2, *direction=) 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: ```python 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= 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=, function=0, raw=b'\x02', *payload=))])] 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=, is_manufacturer_specific=True, direction=, disable_default_response=0, reserved=0, *is_cluster=True, *is_general=False), manufacturer=4098, tsn=4, command_id=0, *direction=) 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=, function=0, raw=b'\x02', *payload=))])) 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=, address=0x4C47), src_ep=1, dst=AddrModeAddress(addr_mode=, 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=, 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=, is_manufacturer_specific=True, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4098, tsn=4, command_id=11, *direction=) 2024-03-25 23:55:34.380 DEBUG (MainThread) [zigpy.zcl] [0x4C47:1:0xef00] Decoded ZCL frame: TuyaMCUClusterForWindowCover:Default_Response(command_id=0, status=) 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=, address=0x4C47), src_ep=1, dst=AddrModeAddress(addr_mode=, 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=, 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=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=True, *is_general=False), tsn=28, command_id=1, *direction=) 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=, function=0, raw=b'\x02', *payload=))])) 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=, function=0, raw=b'\x02', *payload=))])) 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=, address=0x4C47), src_ep=1, dst=AddrModeAddress(addr_mode=, 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=, 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=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=True, *is_general=False), tsn=29, command_id=2, *direction=) 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=, function=0, raw=b'\x01', *payload=)), TuyaDatapointData(dp=7, data=TuyaData(dp_type=, function=0, raw=b'\x01', *payload=)), TuyaDatapointData(dp=3, data=TuyaData(dp_type=, function=0, raw=b'\x00\x00\x00\x00', *payload=0)), TuyaDatapointData(dp=5, data=TuyaData(dp_type=, function=0, raw=b'\x01', *payload=)), TuyaDatapointData(dp=13, data=TuyaData(dp_type=, 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=, function=0, raw=b'\x01', *payload=)), TuyaDatapointData(dp=7, data=TuyaData(dp_type=, function=0, raw=b'\x01', *payload=)), TuyaDatapointData(dp=3, data=TuyaData(dp_type=, function=0, raw=b'\x00\x00\x00\x00', *payload=0)), TuyaDatapointData(dp=5, data=TuyaData(dp_type=, function=0, raw=b'\x01', *payload=)), TuyaDatapointData(dp=13, data=TuyaData(dp_type=, 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=, is_manufacturer_specific=False, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=29, command_id=, *direction=) 2024-03-25 23:55:35.755 DEBUG (MainThread) [zigpy.zcl] [0x4C47:1:0xef00] Sending reply: Default_Response(command_id=2, status=) ```

Custom quirk

Custom quirk ```python """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

github-actions[bot] commented 5 days ago

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.