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
684 stars 636 forks source link

[Device Support Request] Moes Tuya sliding windows pusher #3233

Open tribela opened 1 week ago

tribela commented 1 week ago

Problem description

I got this(https://www.aliexpress.com/item/1005006924617514.html) product and connected it to Home Assistant. But it detected as Smoke detector, Not windows cover variant.

Solution description

It should detected as windows cover, Not a smoke detector

Screenshots/Video

Screenshots/Video ![image](https://github.com/zigpy/zha-device-handlers/assets/5047683/2a596984-e29b-4420-a21c-25e6d7e6e933)

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": "0x0402", "input_clusters": [ "0x0000", "0x0001", "0x0003", "0x0500", "0xef00" ], "output_clusters": [ "0x000a", "0x0019" ] } }, "manufacturer": "_TZ3210_5rta89nj", "model": "TS0601", "class": "zigpy.device.Device" } ```

Diagnostic information

Diagnostic information ```json { "home_assistant": { "installation_type": "Home Assistant Container", "version": "2024.6.4", "dev": false, "hassio": false, "virtualenv": false, "python_version": "3.12.2", "docker": true, "arch": "x86_64", "timezone": "Asia/Seoul", "os_name": "Linux", "os_version": "5.15.0-107-generic", "run_as_root": true }, "custom_components": { "config_editor": { "documentation": "https://github.com/htmltiger/config-editor-card", "version": "4.3", "requirements": [] }, "twitch_helix": { "documentation": "https://github.com/Radioh/ha_twitch_helix", "version": "0.5.3", "requirements": [ "twitchAPI==2.5.1" ] }, "hacs": { "documentation": "https://hacs.xyz/docs/configuration/start", "version": "1.34.0", "requirements": [ "aiogithubapi>=22.10.1" ] }, "imou_life": { "documentation": "https://github.com/user2684/imou_life", "version": "1.0.13", "requirements": [ "imouapi==1.0.13" ] }, "ical": { "documentation": "https://www.home-assistant.io/integrations/ical", "version": "1.6.7", "requirements": [ "icalendar==5.0.7" ] }, "yeelight_bt": { "documentation": "https://github.com/hcoohb/hass-yeelightbt", "version": "1.4.0", "requirements": [ "bleak>=0.18.0", "bleak-retry-connector>=2.1.3" ] }, "sonoff": { "documentation": "https://github.com/AlexxIT/SonoffLAN", "version": "3.7.3", "requirements": [ "pycryptodome>=3.6.6" ] }, "spotcast": { "documentation": "https://github.com/fondberg/spotcast", "version": "v3.6.30", "requirements": [] } }, "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", "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.39.1", "pyserial==3.5", "zha-quirks==0.0.116", "zigpy-deconz==0.23.1", "zigpy==0.64.1", "zigpy-xbee==0.20.1", "zigpy-zigate==0.12.0", "zigpy-znp==0.12.1", "universal-silabs-flasher==0.0.20", "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 }, "setup_times": { "null": { "setup": 3.454601392149925e-05 }, "a5311a9701c2c820e86c04d9279db967": { "wait_import_platforms": -9.543681517243385e-05, "wait_base_component": -0.0005097817629575729, "config_entry_setup": 16.72964466921985 } }, "data": { "ieee": "**REDACTED**", "nwk": 47367, "manufacturer": "_TZ3210_5rta89nj", "model": "TS0601", "name": "_TZ3210_5rta89nj TS0601", "quirk_applied": false, "quirk_class": "zigpy.device.Device", "quirk_id": null, "manufacturer_code": 4098, "power_source": "Battery or Unknown", "lqi": 156, "rssi": null, "last_seen": "2024-06-28T18:46:39", "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": "0x0402", "input_clusters": [ "0x0000", "0x0001", "0x0003", "0x0500", "0xef00" ], "output_clusters": [ "0x000a", "0x0019" ] } }, "manufacturer": "_TZ3210_5rta89nj", "model": "TS0601" }, "active_coordinator": false, "entities": [ { "entity_id": "binary_sensor.tz3210_5rta89nj_ts0601_smoke", "name": "_TZ3210_5rta89nj TS0601" }, { "entity_id": "button.tz3210_5rta89nj_ts0601_identify", "name": "_TZ3210_5rta89nj TS0601" }, { "entity_id": "sensor.tz3210_5rta89nj_ts0601_battery", "name": "_TZ3210_5rta89nj TS0601" }, { "entity_id": "update.tz3210_5rta89nj_ts0601_firmware", "name": "_TZ3210_5rta89nj TS0601" } ], "neighbors": [], "routes": [], "endpoint_names": [ { "name": "IAS_ZONE" } ], "user_given_name": null, "device_reg_id": "1697ead490a519507a62095a02be92a0", "area_id": null, "cluster_details": { "1": { "device_type": { "name": "IAS_ZONE", "id": 1026 }, "profile_id": 260, "in_clusters": { "0x0000": { "endpoint_attribute": "basic", "attributes": { "0x0004": { "attribute_name": "manufacturer", "value": "_TZ3210_5rta89nj" }, "0x0005": { "attribute_name": "model", "value": "TS0601" }, "0x0007": { "attribute_name": "power_source", "value": 3 } }, "unsupported_attributes": { "0x0008": { "attribute_name": "generic_device_class" }, "0x0009": { "attribute_name": "generic_device_type" }, "0x000a": { "attribute_name": "product_code" } } }, "0x0003": { "endpoint_attribute": "identify", "attributes": {}, "unsupported_attributes": {} }, "0x0001": { "endpoint_attribute": "power", "attributes": { "0x0021": { "attribute_name": "battery_percentage_remaining", "value": 200 }, "0x0020": { "attribute_name": "battery_voltage", "value": 0 } }, "unsupported_attributes": { "0x0031": { "attribute_name": "battery_size" }, "0x0033": { "attribute_name": "battery_quantity" } } }, "0x0500": { "endpoint_attribute": "ias_zone", "attributes": { "0x0010": { "attribute_name": "cie_addr", "value": [ 85, 126, 139, 37, 0, 75, 18, 0 ] }, "0x0000": { "attribute_name": "zone_state", "value": 0 }, "0x0002": { "attribute_name": "zone_status", "value": 0 }, "0x0001": { "attribute_name": "zone_type", "value": 40 } }, "unsupported_attributes": {} }, "0xef00": { "endpoint_attribute": null, "attributes": {}, "unsupported_attributes": {} } }, "out_clusters": { "0x000a": { "endpoint_attribute": "time", "attributes": {}, "unsupported_attributes": {} }, "0x0019": { "endpoint_attribute": "ota", "attributes": { "0x0002": { "attribute_name": "current_file_version", "value": 74 } }, "unsupported_attributes": { "0x0002": { "attribute_name": "current_file_version" } } } } } } } } ```

Logs

Logs ```python [Paste the logs here] ```

Custom quirk

Custom quirk I am using this quirk got from this repo's another issue ```python """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 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, } 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", # for Slider movement # dp_type=TuyaDPType.VALUE, ), 3: DPToAttributeMapping( TuyaWindowCovering.ep_attribute, "current_position_lift_percentage", # for Slider updates # 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=, complex_descriptor_available=0, user_descriptor_available=0, # reserved=0, aps_flags=0, frequency_band=, # mac_capability_flags=, # manufacturer_code=4417, maximum_buffer_size=66, maximum_incoming_transfer_size=66, server_mask=10752, # maximum_outgoing_transfer_size=66, descriptor_capability_field=, # *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: [ ("_TZE204_r0jdjrvi", "TS0601"), ("_TZ3210_5rta89nj", "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], }, } } ```

Additional information

No response

AlbertoV8 commented 6 days ago

I have the same issue...