Koenkk / Koenkk/zigbee2mqtt

[External Converter]: Ledron YK-16 (TS1002) Scene Buttons

Open
#32,301 5 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

external converter
Dominant language
TypeScript
Stars
15.7k
Forks
2k
Avg merge
18h 55m
Merged PRs (30d)
35

Description

Link

https://www.zigbee2mqtt.io/devices/YK-16.html

Database entry

{"id":2,"type":"EndDevice","ieeeAddr":"0xa4c138e0727e1b1a","nwkAddr":24297,"manufId":4417,"manufName":"_TZ3000_wbsqjvh3","powerSource":"Battery","modelId":"TS1002","epList":[1],"endpoints":{"1":{"profId":260,"epId":1,"devId":261,"inClusterList":[1,3,4,4096,57345,0],"outClusterList":[3,4,5,6,8,768,4096,25,10],"clusters":{"genBasic":{"attributes":{"modelId":"TS1002","manufacturerName":"_TZ3000_wbsqjvh3","powerSource":3,"zclVersion":3,"appVersion":65,"stackVersion":0,"hwVersion":1,"dateCode":""}},"genPowerCfg":{"attributes":{"batteryPercentageRemaining":200}}},"binds":[{"cluster":1,"type":"endpoint","deviceIeeeAddress":"0x00124b0021b7775e","endpointID":1},{"cluster":6,"type":"endpoint","deviceIeeeAddress":"0x00124b0021b7775e","endpointID":1},{"cluster":8,"type":"endpoint","deviceIeeeAddress":"0x00124b0021b7775e","endpointID":1},{"cluster":768,"type":"endpoint","deviceIeeeAddress":"0x00124b0021b7775e","endpointID":1}],"configuredReportings":[{"cluster":1,"attrId":33,"minRepIntval":3600,"maxRepIntval":65000,"repChange":10}],"meta":{}}},"appVersion":65,"stackVersion":0,"hwVersion":1,"dateCode":"","zclVersion":3,"interviewCompleted":true,"interviewState":"SUCCESSFUL","meta":{"configured":"0.0.0"},"lastSeen":1781419690553}

Zigbee2MQTT version

2.12.0

External converter
const fz = require('zigbee-herdsman-converters/converters/fromZigbee');
const exposes = require('zigbee-herdsman-converters/lib/exposes');
const e = exposes.presets;

// Cache to prevent duplicate triggers from remote transmission retries
const deviceCache = {};

const fzLocal = {
    yk16_raw_scene: {
        cluster: 'genOnOff',
        type: ['raw', 'commandTuyaAction'],
        convert: (model, msg, publish, options, meta) => {
            const ieeeAddr = msg.device?.ieeeAddr || 'generic';
            const now = Date.now();

            // Extract from raw fallback format
            if (msg.type === 'raw' && Buffer.isBuffer(msg.data)) {
                const data = msg.data;
                // Look for Tuya command 253 (0xFD)
                if (data.length >= 6 && data[2] === 253) {
                    const seq = data[1]; // ZCL transaction sequence number
                    const sceneNum = data[5];

                    // Deduplicate retries caused by the UNSUP_COMMAND status (remote retries up to 5 times)
                    if (deviceCache[ieeeAddr] && deviceCache[ieeeAddr].seq === seq && (now - deviceCache[ieeeAddr].time) < 3000) {
                        return;
                    }
                    deviceCache[ieeeAddr] = { seq, time: now };
                    return { action: `scene_${sceneNum}` };
                }
            } 
            // Fallback in case a future herdsman update parses it to commandTuyaAction natively
            else if (msg.type === 'commandTuyaAction') {
                const data = msg.data?.data;
                if (Array.isArray(data) && data.length >= 6) {
                    const seq = msg.data?.seq || 0;
                    const sceneNum = data[5];

                    if (deviceCache[ieeeAddr] && deviceCache[ieeeAddr].seq === seq && (now - deviceCache[ieeeAddr].time) < 3000) {
                        return;
                    }
                    deviceCache[ieeeAddr] = { seq, time: now };
                    return { action: `scene_${sceneNum}` };
                }
            }
        },
    },
};

const definition = {
    fingerprint: [
        {modelID: 'TS1002', manufacturerName: '_TZ3000_zwszqdpy', priority: 5},
        {modelID: 'TS1002', manufacturerName: '_TZ3000_xa9g7rxs', priority: 5},
        {modelID: 'TS1002', manufacturerName: '_TZ3000_xwh1e22x', priority: 5},
        {modelID: 'TS1002', manufacturerName: '_TZ3000_jhqcsfp3', priority: 5},
        {modelID: 'TS1002', priority: 5}
    ],
    zigbeeModel: ['TS1002'],
    model: 'YK-16',
    vendor: 'Ledron',
    description: 'RGB+CCT Remote with custom scene button support',
    fromZigbee: [
        fz.battery, 
        fz.command_on, 
        fz.command_off, 
        fz.command_move_to_level, 
        fz.command_move_to_color_temp, 
        fz.command_step_color_temperature, 
        fz.command_step,
        // Standard color control converters
        fz.command_move_to_hue_and_saturation,
        fz.command_move_to_color,
        fz.command_move_to_hue,
        fz.command_move_to_saturation,
        // Custom raw handler for scene buttons
        fzLocal.yk16_raw_scene
    ],
    toZigbee: [],
    exposes: [
        e.battery(), 
        e.battery_voltage(), 
        e.action([
            'on', 'off', 'brightness_move_to_level', 'color_temperature_move',
            'move_to_hue_and_saturation', 'move_to_color', 'move_to_hue', 'move_to_saturation',
            'scene_1', 'scene_2', 'scene_3', 'scene_4', 
            'scene_5', 'scene_6', 'scene_7', 'scene_8',
            'scene_9', 'scene_10', 'scene_11', 'scene_12',
            'scene_13', 'scene_14', 'scene_15', 'scene_16'
        ])
    ],
};

module.exports = definition;
What does/doesn't work with the external definition?

I am using the Ledron YK-16 remote (TS1002) and while basic features (on, off, brightness, battery) are supported, pressing the scene buttons (S1–S8) and the RGB button fails with unmapped cluster errors.

Issue Description

The scene buttons on the Ledron YK-16 (TS1002) send a manufacturer-specific command 253 over the genOnOff cluster. Because of strict ZCL schema parsing in recent versions of zigbee-herdsman, the coordinator logs:
zh:controller: Failed to parse frame: Error: Status 'UNSUP_COMMAND' genOnOff:253

Consequently, the message falls back as a raw message:
z2m: Received Zigbee message from 'TUY_BTN_REM_161', type 'raw', cluster 'genOnOff', data '{"data":[1,9,253,0,0,8],"type":"Buffer"}'

Additionally, because the command fails standard ZCL parsing, no success acknowledgment (Default Response) is returned to the remote. This causes the remote to assume the message was lost and retry, transmitting the same command up to 5 times for a single physical button press.

Furthermore, the native converter seems to be missing standard color-control converters (such as lightingColorCtrl:moveToHueAndSaturation), meaning the RGB button is unmapped by default.

Possible Solution

I have constructed an external converter that seems to solve both issues:

  • It captures the raw fallback messages, decodes the button ID, and implements sequence-number-based deduplication (using the transaction sequence byte at index 1) to block the duplicate transmission retries.
  • It restores the missing standard colour control converters so the colour wheel and RGB modes are fully mapped.

Here are the initial logs when pressing scene button S8 (without the converter):

Jun 13 16:03:02 BTN node[100271]: [2026/06/13 16:03:02] debug:         zh:controller: Failed to parse frame: Error: Status 'UNSUP_COMMAND' genOnOff:253
Jun 13 16:03:02 BTN node[100271]: [2026/06/13 16:03:02] debug:         z2m: Received Zigbee message from 'TUY_BTN_REM_161', type 'raw', cluster 'genOnOff', data '{"data":[1,5,253,0,0,8],"type":"Buffer"}' from endpoint 1 with groupID 0
Jun 13 16:03:02 BTN node[100271]: [2026/06/13 16:03:02] debug:         z2m: No converter available for 'TS1002' with cluster 'genOnOff' and type 'raw' and data '{"data":[1,5,253,0,0,8],"type":"Buffer"}'

With the external converter, the raw message is intercepted, mapped, and duplicate retries are safely ignored:

Jun 13 16:34:00 BTN node[126444]: debug: z2m: Received Zigbee message from 'TUY_BTN_REM_161', type 'raw', cluster 'genOnOff', data '{"data":[1,45,253,0,0,8],"type":"Buffer"}'
Jun 13 16:34:00 BTN node[126444]: info:  z2m: yk16_raw_scene: Decoded button press -> scene_8 (seq: 45)
Jun 13 16:34:00 BTN node[126444]: info:  z2m:mqtt: MQTT publish: topic 'mqtt/TUY_BTN_REM_161', payload '{"action":"scene_8","battery":100,"linkquality":185}'
Jun 13 16:34:01 BTN node[126444]: debug: z2m: yk16_raw_scene: ignoring duplicate retry (seq: 45)
Jun 13 16:34:02 BTN node[126444]: debug: z2m: yk16_raw_scene: ignoring duplicate retry (seq: 45)

Natively, zigbee-herdsman-converters seems to use fz.tuya_on_off_action (which converts commandTuyaAction on genOnOff). However, because the parser throws UNSUP_COMMAND on command 253 and fails with an undefined frame, it falls back to a 'raw' type message instead of a structured 'commandTuyaAction'.

Could this fallback handling (or a native ZCL command 253 definition on genOnOff for TS1002 devices) be added natively to zigbee-herdsman-converters?

Thanks a lot.

Notes

No response

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at the native TS1002 device definition and the fromZigbee converter entry points, comparing them with the external converter's fzLocal.yk16_raw_scene handler. Reproduce the raw genOnOff command 253 and the missing color-control actions, then verify that S1–S8 and RGB presses publish the expected actions without duplicate retries.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, typescript
Domain
backend, embedded-iot
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.