ZHA quirk for curtain motor MI-P82 + custom card

Hi,
for everyone who need integrate Zigbee curtain motor ( MI-P82 Zigbee ) from Aliexpress

Here is my working quirk for TS0601 TZE204_tgl8i2np

/zhaquirk/ts0601_curtain.py
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,
    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,
    TuyaWindowCover,
)
from zhaquirks.tuya.mcu import (
    DPToAttributeMapping,
    TuyaClusterData,
    TuyaMCUCluster,
)


class TuyaCC(t.enum8):
    OPEN = 0x00
    STOP = 0x01
    CLOSE = 0x02


class ZclCC(t.enum8):
    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):
    attributes = WindowCovering.attributes.copy()
    attributes.update(
        {
            0xF000: ("curtain_switch", t.enum8, True),
            0xF001: ("accurate_calibration", t.enum8, True),
            0xF002: ("motor_steering", t.enum8, True),
            0xF003: ("travel", t.uint16_t, True),
            # Explicitly define moving_state attribute to parse DP 7 cleanly
            0xF004: ("moving_state", t.enum8, True),
        }
    )

    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,
    ):
        if command_id in (0x0002, 0x0000, 0x0001):
            cluster_data = TuyaClusterData(
                endpoint_id=self.endpoint.endpoint_id,
                cluster_name=self.ep_attribute,
                cluster_attr="curtain_switch",
                attr_value=TUYA2ZB_COMMANDS[command_id],
                expect_reply=expect_reply,
                manufacturer=None,
            )
            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 == 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=None,
            )
            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)

        return foundation.GENERAL_COMMANDS[
            foundation.GeneralCommand.Default_Response
        ].schema(command_id=command_id, status=foundation.Status.UNSUP_CLUSTER_COMMAND)


class TuyaWindowCoverManufCluster(TuyaMCUCluster):
    attributes = TuyaMCUCluster.attributes.copy()
    attributes.update(
        {
            0x5000: ("backlight_mode", t.enum8, True),
            0x8001: ("indicator_status", t.enum8, True),
        }
    )
    
    dp_to_attribute: Dict[int, DPToAttributeMapping] = {
        1: DPToAttributeMapping(
            TuyaWindowCovering.ep_attribute,
            "curtain_switch",
        ),
        2: DPToAttributeMapping(
            TuyaWindowCovering.ep_attribute,
            "current_position_lift_percentage",
        ),
        3: DPToAttributeMapping(
            TuyaWindowCovering.ep_attribute,
            "current_position_lift_percentage",
        ),
        7: DPToAttributeMapping(
            TuyaWindowCovering.ep_attribute,
            "moving_state", 
        ),
    }

    # Merged handlers together properly so they map to the correct update parser
    data_point_handlers: Dict[int, str] = {
        1: "_dp_2_attr_update",
        2: "_dp_2_attr_update",
        3: "_dp_2_attr_update",
        7: "_dp_2_attr_update",
    }


class TuyaCover0601_GP(TuyaWindowCover):
    signature = {
        MODELS_INFO: [
            ("_TZE204_r0jdjrvi", "TS0601"),
            ("_TZE204_tgl8i2np", "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],
            },
        }
    }
curtain custom card code:
type: custom:button-card
entity: cover.zaves_zz
name: Curtain winter garden
show_name: false
show_icon: false
grid_options:
  columns: 12
  rows: 3
styles:
  card:
    - border-radius: 15px
    - padding: 12px
    - background-color: var(--card-background-color)
  grid:
    - grid-template-areas: |
        "i title status status"
        "visual visual visual visual"
        "btn1 btn2 btn3 btn4"
    - grid-template-columns: repeat(4, 1fr)
    - grid-template-rows: auto auto auto
    - row-gap: 12px
    - column-gap: 8px
  custom_fields:
    title:
      - grid-column: span 2
      - justify-self: start
      - font-weight: bold
      - font-size: 14px
      - padding-left: 8px
    status:
      - grid-column: span 2
      - justify-self: end
      - font-size: 13px
      - color: var(--secondary-text-color)
    visual:
      - grid-column: span 4
      - background: rgba(0, 0, 0, 0.2)
      - border-radius: 6px
      - height: 40px
      - width: 100%
      - position: relative
      - overflow: hidden
      - border: 1px solid rgba(255, 255, 255, 0.1)
custom_fields:
  title: |
    [[[ return "Curtain winter garden"; ]]]
  status: |
    [[[ 
      if (entity.state === 'closed') return "Closed";
      const pos = entity.attributes.current_position;
      if (pos === 100) return "Open";
      return pos + "% Open";
    ]]]
  visual: |
    [[[
      const pos = entity.attributes.current_position || 0;
      const panelWidth = (50 - (pos / 2)); 
      
      return `
        <div style="
          position: absolute; left: 0; top: 0; bottom: 0;
          width: ${panelWidth}%; background: var(--accent-color);
          transition: width 0.5s ease-in-out; border-right: 1px solid rgba(0,0,0,0.3);
        "></div>
        <div style="
          position: absolute; right: 0; top: 0; bottom: 0;
          width: ${panelWidth}%; background: var(--accent-color);
          transition: width 0.5s ease-in-out; border-left: 1px solid rgba(0,0,0,0.3);
        "></div>
      `;
    ]]]
  btn1:
    card:
      type: custom:button-card
      name: Closed
      tap_action:
        action: call-service
        service: cover.set_cover_position
        service_data:
          entity_id: cover.zaves_zz
          position: 0
      styles:
        card:
          - background-color: >
              [[[ return states['cover.zaves_zz'].attributes.current_position
              === 0 ? 'var(--accent-color)' : 'rgba(255,255,255,0.05)' ]]]
          - color: >
              [[[ return states['cover.zaves_zz'].attributes.current_position
              === 0 ? 'white' : 'var(--primary-text-color)' ]]]
          - padding: 8px 2px
          - border-radius: 6px
          - font-weight: bold
          - font-size: 11px
  btn2:
    card:
      type: custom:button-card
      name: 25%
      tap_action:
        action: call-service
        service: cover.set_cover_position
        service_data:
          entity_id: cover.zaves_zz
          position: 25
      styles:
        card:
          - background-color: >
              [[[ return states['cover.zaves_zz'].attributes.current_position
              === 25 ? 'var(--accent-color)' : 'rgba(255,255,255,0.05)' ]]]
          - color: >
              [[[ return states['cover.zaves_zz'].attributes.current_position
              === 25 ? 'white' : 'var(--primary-text-color)' ]]]
          - padding: 8px 2px
          - border-radius: 6px
          - font-weight: bold
          - font-size: 11px
  btn3:
    card:
      type: custom:button-card
      name: 75%
      tap_action:
        action: call-service
        service: cover.set_cover_position
        service_data:
          entity_id: cover.zaves_zz
          position: 75
      styles:
        card:
          - background-color: >
              [[[ return states['cover.zaves_zz'].attributes.current_position
              === 75 ? 'var(--accent-color)' : 'rgba(255,255,255,0.05)' ]]]
          - color: >
              [[[ return states['cover.zaves_zz'].attributes.current_position
              === 75 ? 'white' : 'var(--primary-text-color)' ]]]
          - padding: 8px 2px
          - border-radius: 6px
          - font-weight: bold
          - font-size: 11px
  btn4:
    card:
      type: custom:button-card
      name: Open
      tap_action:
        action: call-service
        service: cover.set_cover_position
        service_data:
          entity_id: cover.zaves_zz
          position: 100
      styles:
        card:
          - background-color: >
              [[[ return states['cover.zaves_zz'].attributes.current_position
              === 100 ? 'var(--accent-color)' : 'rgba(255,255,255,0.05)' ]]]
          - color: >
              [[[ return states['cover.zaves_zz'].attributes.current_position
              === 100 ? 'white' : 'var(--primary-text-color)' ]]]
          - padding: 8px 2px
          - border-radius: 6px
          - font-weight: bold
          - font-size: 11px