Skip to content

[Device Support Request] _TZE284_gt5al3bl (Gledopto GL-SPI-206P) #4610

@basilfx

Description

@basilfx

Problem description

Tuya TS0601-based LED strip controller, produced by Gledopto.

Solution description

Support for this device exists in Zigbee2MQTT, so I used this as a start to write a quirk.

However, I cannot get the color part to work, because I need to send a raw payload to DP 61. On/off and level control seem to work.

Quirk V2 lacks some examples on RGB/Lights, so I have no clue how to continue.

Screenshots/Video

None

Diagnostics information

None

Device signature

Device signature
{
  "node_descriptor": {
    "logical_type": 1,
    "complex_descriptor_available": 0,
    "user_descriptor_available": 0,
    "reserved": 0,
    "aps_flags": 0,
    "frequency_band": 8,
    "mac_capability_flags": 142,
    "manufacturer_code": 4417,
    "maximum_buffer_size": 66,
    "maximum_incoming_transfer_size": 66,
    "server_mask": 10752,
    "maximum_outgoing_transfer_size": 66,
    "descriptor_capability_field": 0
  },
  "endpoints": {
    "1": {
      "profile_id": "0x0104",
      "device_type": "0x0051",
      "input_clusters": [
        "0x0000",
        "0x0004",
        "0x0005",
        "0xed00",
        "0xef00"
      ],
      "output_clusters": [
        "0x000a",
        "0x0019"
      ]
    },
    "242": {
      "profile_id": "0xa1e0",
      "device_type": "0x0061",
      "input_clusters": [],
      "output_clusters": [
        "0x0021"
      ]
    }
  },
  "manufacturer": "_TZE284_gt5al3bl",
  "model": "TS0601",
  "class": "zigpy.device.Device"
}

Logs

None

Custom quirk

Custom quirk
"""Tuya SPI Pixel Controller GL-SPI-206P quirk."""

from zigpy.quirks.v2 import EntityType
import zigpy.types as t
from zigpy.zcl.clusters.general import LevelControl
from zigpy.zcl.clusters.lighting import Color

from zhaquirks.tuya import TuyaLocalCluster, NoManufacturerCluster, TuyaData
from zhaquirks.tuya.mcu import (
    TuyaLevelControl,
    TuyaDatapointData,
    TUYA_MCU_COMMAND,
)
from zhaquirks.tuya.builder import TuyaQuirkBuilder
from zigpy.profiles import zha
from zhaquirks.tuya.mcu import DPToAttributeMapping

from zigpy.zcl import foundation

from homeassistant.util.color import color_xy_to_hs
from typing import Any


class WorkMode(t.enum8):
    """Work mode enum."""

    White = 0x00
    Colour = 0x01
    Scene = 0x02
    Music = 0x03


class ChipType(t.enum8):
    """Chip type enum."""

    WS2801 = 0x00
    LPD6803 = 0x01
    LPD8803 = 0x02
    WS2811 = 0x03
    TM1814B = 0x04
    TM1934A = 0x05
    SK6812 = 0x06
    SK9822 = 0x07
    UCS8904B = 0x08
    WS2805 = 0x09


class TuyaColor(Color, TuyaLocalCluster):
    """Tuya MCU Level cluster for color device."""

    _CONSTANT_ATTRIBUTES = {
        Color.AttributeDefs.color_mode.id: Color.ColorMode.Hue_and_saturation
    }

    class AttributeDefs(Color.AttributeDefs):
        """Cluster attributes."""

    def _xy_to_hsv(self, x: int, y: int) -> tuple[int, int, int]:
        """Convert CIE xy to HSV for Tuya."""
        # Normalize x, y (ZCL sends as uint16 where 65535 = 1.0)
        x_norm = x / 65535
        y_norm = y / 65535

        # Use Home Assistant's color utility to convert xy to HS
        hue_deg, sat_percent = color_xy_to_hs(x_norm, y_norm)

        # Convert to Tuya format (0-360 for hue, 0-1000 for saturation)
        hue = max(0, min(360, round(hue_deg)))
        sat = max(0, min(1000, round(sat_percent * 10)))
        val = 1000  # Fixed at 1000 as per TypeScript implementation

        return (hue, sat, val)

    def _send_dp61_color(self, hue: int, sat: int, val: int = 1000):
        """Send DP 61 raw payload with HSV data."""
        # Build the 11-byte payload as per TypeScript implementation
        payload = bytes([
            0x00, 0x01, 0x01, 0x14, 0x00,  # Header
            (hue >> 8) & 0xFF, hue & 0xFF,  # Hue (16-bit big-endian)
            (sat >> 8) & 0xFF, sat & 0xFF,  # Saturation (16-bit)
            (val >> 8) & 0xFF, val & 0xFF,  # Value (16-bit)
        ])

        self.debug(
            "Sending DP 61 color: H=%d S=%d V=%d, payload=%s",
            hue, sat, val, payload.hex()
        )

        # Send via Tuya MCU command bus with the raw payload as attribute value
        cluster_data = TuyaDatapointData(
            dp=61,
            data=TuyaData(raw=payload)
        )
        self.endpoint.device.command_bus.listener_event(
            TUYA_MCU_COMMAND,
            cluster_data,
        )

    async def command(
        self,
        command_id: foundation.GeneralCommand | int | t.uint8_t,
        *args,
        manufacturer: int | t.uint16_t | None = None,
        expect_reply: bool = True,
        tsn: int | t.uint8_t | None = None,
        **kwargs: Any,
    ):
        """Override the default Cluster command."""
        self.debug(
            "Tuya Cluster Command: %x, args=%s, kwargs=%s",
            command_id,
            args,
            kwargs,
        )

        if command_id == Color.ServerCommandDefs.move_to_color.id:
            x = kwargs.get("color_x")
            y = kwargs.get("color_y")

            hue, sat, val = self._xy_to_hsv(x, y)
            self._send_dp61_color(hue, sat, val)
            return foundation.GENERAL_COMMANDS[
                foundation.GeneralCommand.Default_Response
            ].schema(command_id=command_id, status=foundation.Status.SUCCESS)


class TuyaColorNM(NoManufacturerCluster, TuyaColor):
    """Tuya Color cluster for SPI Pixel Controller."""


class TuyaLevelControlNM(NoManufacturerCluster, TuyaLevelControl):
    """Tuya OnOff cluster with NoManufacturerID."""


(
    TuyaQuirkBuilder("_TZE284_gt5al3bl", "TS0601")
    # DP 1: On/Off (works)
    .tuya_onoff(dp_id=1)

    # DP 2: Work mode (not tested)
    .tuya_enum(
        dp_id=2,
        attribute_name="work_mode",
        enum_class=WorkMode,
        entity_type=EntityType.CONFIG,
        translation_key="work_mode",
        fallback_name="Work mode",
    )

    # DP 3: Brightness/Level control (works)
    .tuya_dp(
        dp_id=3,
        ep_attribute=TuyaLevelControlNM.ep_attribute,
        attribute_name=LevelControl.AttributeDefs.current_level.name
    )
    .adds(TuyaLevelControlNM)


    # DP 61: Color control (does not work, needs raw payload support)
    .tuya_dp_multi(
        dp_id=61,
        attribute_mapping=[
            DPToAttributeMapping(
                ep_attribute=TuyaColorNM.ep_attribute,
                attribute_name=Color.AttributeDefs.current_hue.name
            ),
        ])
    .adds(TuyaColorNM)

    # DP 53: Light pixel number setting (works)
    .tuya_number(
        dp_id=53,
        attribute_name="lightpixel_number_set",
        type=t.uint16_t,
        min_value=1,
        max_value=1000,
        step=1,
        translation_key="lightpixel_number_set",
        fallback_name="Pixel count",
    )
    # DP 102: Chip type (works)
    .tuya_enum(
        dp_id=102,
        attribute_name="chip_type",
        enum_class=ChipType,
        entity_type=EntityType.CONFIG,
        translation_key="chip_type",
        fallback_name="Chip type",
    )
    .replaces_endpoint(1, device_type=zha.DeviceType.COLOR_DIMMABLE_LIGHT)
    .skip_configuration()
    .add_to_registry()
)

Additional information

None

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions