Skip to content

TS110E on_off fix add logic to use the correct state for multi switches#12657

Draft
phyzical wants to merge 15 commits into
Koenkk:masterfrom
phyzical:feature/32246-fix-TS110E-ON-OFF-handling
Draft

TS110E on_off fix add logic to use the correct state for multi switches#12657
phyzical wants to merge 15 commits into
Koenkk:masterfrom
phyzical:feature/32246-fix-TS110E-ON-OFF-handling

Conversation

@phyzical

@phyzical phyzical commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

resolves Koenkk/zigbee2mqtt#32246

Basically when you send a same brightness command moveToLevelTuya will only turn on the device if the brightness changes, this is due to the helper assuming the state is always state but with multi switches we need to handle state_l1 for example

this workaround just does a lookup, if we have a state_ blah then assume we need it to work like that

I'm unsure if other parts of the device suffer a similar fate assuming that it should be i.e brightness vs brightness_l1 but it may?

i assume the reason device would still turn on when the brightness changes is a side effect of the device itself and how it handles moveToLevelTuya

@phyzical

Copy link
Copy Markdown
Contributor Author

ive also decided to add logic to only perform this when the brightness is unchanged given the moveToLevelTuya seems to handle things as expected

@phyzical

phyzical commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

Hmm not sure why the CI doesnt like it i am able to confirm this works via an external converter

import * as modernExtend from "zigbee-herdsman-converters/lib/modernExtend";
import * as tuya from "zigbee-herdsman-converters/lib/tuya";
import * as utils from 'zigbee-herdsman-converters/lib/utils'
import * as fz from "zigbee-herdsman-converters/converters/fromZigbee";
import * as tz from "zigbee-herdsman-converters/converters/toZigbee";
import * as exposes from "zigbee-herdsman-converters/lib/exposes";
const e = exposes.presets;
const {tuyaBase} = tuya.modernExtend;

const TS110E_onoff_brightness_2 = {
         key: ["state", "brightness"],
        convertSet: async (entity, key, value, meta) => {
            const {message, state} = meta;

            if (message.state === "OFF" || (message.state != null && message.brightness == null)) {
                return await tz.on_off.convertSet(entity, key, value, meta);
            }
            if (message.brightness != null) {
                // If state includes state_l1 assume we need to use a custom lookup
                const stateKey = Object.keys(state).find((k) => k.startsWith("state_l1")) ? "state_l" + entity.ID : "state";
                const brightnessKey = Object.keys(state).find((k) => k.startsWith("brightness_l1")) ? "brightness_l" + entity.ID : "brightness";


                const brightness = utils.toNumber(message.brightness, "brightness");
                 // we allow at most 1 incase its a rounding/ float precision issue
                const brightnessUnchanged = Math.abs(utils.mapNumberRange(brightness, 0, 254,0, 254) - state[brightnessKey]) <= 1;
console.log("state is",message, state,stateKey , brightnessKey )
                // if the brightness is unchanged then we need to force it on due to weirdness with moveToLevelTuya

                if (state[stateKey] === "OFF" && brightnessUnchanged) {
                    await entity.command("genOnOff", "on", {}, utils.getOptions(meta.mapped, entity));
                }

                const level = utils.mapNumberRange(brightness, 0, 254, 0, 1000);

                // set brightness
                await entity.command(
                    "genLevelCtrl",
                    "moveToLevelTuya",
                    {level, transtime: 100},
                    utils.getOptions(meta.mapped, entity)
                );
                return {state: {[stateKey]: "ON", brightness}};
          }
        },
        convertGet: async (entity, key, meta) => {
            if (key === "state") await tz.on_off.convertGet(entity, key, meta);
            if (key === "brightness") await entity.read("genLevelCtrl", [61440]);
        },
    };


export default [  {
      fingerprint: tuya.fingerprint("TS110E", [ "_TZ3210_3mpwqzuu"]),
        model: "TS110E_2gang_2",
        vendor: "Tuya",
        description: "2 channel dimmer",
        whiteLabel: [tuya.whitelabel("Nedis", "ZBWD20RD", "SmartLife Triac Dimmer", ["_TZ3210_mt5xjoy6"])],
        extend: [tuyaBase()],
        fromZigbee: [tuya.fz.TS110E, tuya.fz.TS110E_switch_type, tuya.fz.power_on_behavior_1, fz.on_off],
        toZigbee: [TS110E_onoff_brightness_2, tuya.tz.TS110E_options, tuya.tz.power_on_behavior_1, tz.light_brightness_move],
        meta: {multiEndpoint: true},
        exposes: [
            e.power_on_behavior(),
            e.light_brightness().withMinBrightness().withMaxBrightness().withEndpoint("l1"),
            e.light_brightness().withMinBrightness().withMaxBrightness().withEndpoint("l2"),
            tuya.exposes.switchType().withEndpoint("l1"),
            tuya.exposes.switchType().withEndpoint("l2"),
        ],
        configure: async (device, coordinatorEndpoint) => {
            await tuya.configureMagicPacket(device, coordinatorEndpoint);
            const endpoint = device.getEndpoint(1);
            await reporting.bind(endpoint, coordinatorEndpoint, ["genOnOff", "genLevelCtrl"]);
            await reporting.onOff(endpoint);
        },
        endpoint: (device) => {
            return {l1: 1, l2: 2};
        },
    }];



phyzical and others added 2 commits July 11, 2026 18:09
Fix brightness lookups, as max brightness was always 1 behind max causing the force on work around never to fire
@phyzical

Copy link
Copy Markdown
Contributor Author

Added some more logic to handle the clamping to reference the min max instead of a static 254, as it resulted in brightnessUnchanged to always be false

@phyzical

Copy link
Copy Markdown
Contributor Author

had an issue where i was still getting precision errors i.e 31 setting vs 30 in state so instead keep the old logic of hardcoded 254 and allow a variance of 1 as a unchanged bool to force the lights on

Comment thread src/lib/tuya.ts Outdated
const brightnessUnchanged = Math.abs(utils.mapNumberRange(brightness, 0, 254, 0, 254) - state[brightnessKey]) <= 1;

// if the brightness is unchanged then we need to force it on due to weirdness with moveToLevelTuya
if (state[stateKey] === "OFF" && brightnessUnchanged) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we check if the target state is ON here? Because what if {state: OFF} is recevied?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean in the message payload?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did have this thought, but I doubt anyone will send a brightness command with an off right?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes indeed, it might depends on e.g. HA automations used by people so would say it's a valid use-case.

@phyzical phyzical Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh actually i just realised that will be handled by the early return above https://github.com/Koenkk/zigbee-herdsman-converters/pull/12657/changes#diff-d45f20d30a43dc3e172b16bd55872492f4867c8831c5e03a8be9be65f1cd72d5R3314

did we instead want to handle brightness first and then perform an on/off

and avoid all the early returns?

nah i think keep it as it is in this regard what do you think?

@phyzical

phyzical commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Okay so i missunderstood some of the mechanisms going on, turns out moveToLevelTuya will not actually send the ON to the state (i thought the return was handling that oops)

so we need to simply always send the on always if its off (maybe this is what you were implying yesterday?)

So tldr the issue was simply that we did not check the correct state before telling the light to turn on which is what caused the problem in state. and the bug around same brightness not turning on the light should naturally be resolved by the fact we turn it on if its off

sadly this does result in twice as many events firing out to the network but at this time i couldnt get an all in one similar to the moveToLevelWithOnOff as this didnt seem to work

Comment thread src/lib/tuya.ts Outdated
Comment thread src/lib/tuya.ts
Comment thread src/lib/tuya.ts Outdated
will review again but i think the state key being returned is a mistake and there is translation occuring down the line and the only issue was the state check to turn on

Co-authored-by: Jack <5182053+phyzical@users.noreply.github.com>
Comment thread src/lib/tuya.ts
Comment thread src/lib/tuya.ts Outdated
Co-authored-by: Jack <5182053+phyzical@users.noreply.github.com>
@phyzical phyzical marked this pull request as draft July 14, 2026 02:15
@phyzical

Copy link
Copy Markdown
Contributor Author

i will reping you when i feel confident, something wasnt right i think this is it but will retest

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

issue with TS110E - _TZ3210_3mpwqzuu dimmer switches

2 participants