Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/devices/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ import {definitions as nodieby} from "./nodieby";
import {definitions as nodon} from "./nodon";
import {definitions as nordtronic} from "./nordtronic";
import {definitions as nous} from "./nous";
import {definitions as novaDigital} from "./nova_digital";
import {definitions as novo} from "./novo";
import {definitions as nue3a} from "./nue_3a";
import {definitions as nyce} from "./nyce";
Expand Down Expand Up @@ -606,6 +607,7 @@ const definitions: DefinitionWithExtend[] = [
...nordtronic,
...nous,
...nobo,
...novaDigital,
...novo,
...nue3a,
...nyce,
Expand Down
35 changes: 35 additions & 0 deletions src/devices/nova_digital.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as reporting from "../lib/reporting";
import * as tuya from "../lib/tuya";
import type {DefinitionWithExtend} from "../lib/types";

export const definitions: DefinitionWithExtend[] = [
{
fingerprint: tuya.fingerprint("TS0002", ["_TZ3210_5ksufhqi"]),

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.

Please add this to the already existing TS0002 definition in tuya.ts

@trzenaro trzenaro Jul 13, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks for the review @Koenkk . I initially created a separate definition because this device behaves differently from the other devices handled by the generic TS0002 definition:

  • Physical on/off changes are only reported after configuring onOff reporting for both endpoints.
  • switchType and powerOnBehavior2 return UNSUPPORTED_ATTRIBUTE.
  • The device supports the legacy powerOutageMemory implementation instead.

I considered adding the following mutually exclusive conditions to the existing tuyaOnOff extend:

powerOnBehavior2: (manufacturerName) => manufacturerName !== "_TZ3210_5ksufhqi",
powerOutageMemory: (manufacturerName) => manufacturerName === "_TZ3210_5ksufhqi",

However, tuyaOnOff currently handles these options through an if / else if branch. Since the predicate function itself is truthy, the powerOutageMemory branch is always selected while building the definition, and the powerOnBehavior2 converters are not registered for the existing models.

This is why I originally used a separate definition: to avoid adding several manufacturer-specific conditions to the generic definition and, more importantly, to avoid changing the behavior of existing TS0002 devices.

I have prepared the following integrated implementation for the existing TS0002 definition. It conditionally disables the unsupported options, exposes power_outage_memory only for _TZ3210_5ksufhqi, restricts the legacy converter to the power_outage_memory key, and configures on/off reporting only for this manufacturer:

{
    // TS0002 2 gang switch module with all available features. This is the default for TS0002 devices.
    model: "TS0002",
    zigbeeModel: ["TS0002"],
    vendor: "Tuya",
    description: "2-Gang switch with backlight, countdown and inching",
    fromZigbee: [tuya.fz.power_outage_memory],
    toZigbee: [{...tuya.tz.power_on_behavior_1, key: ["power_outage_memory"]}],
    exposes: (device) => (device.manufacturerName === "_TZ3210_5ksufhqi" ? [te.powerOutageMemory()] : []),
    extend: [
        tuya.modernExtend.tuyaBase(),
        tuya.modernExtend.tuyaOnOff({
            switchType: (m) => m !== "_TZ3210_5ksufhqi",
            powerOnBehavior2: (m) => m !== "_TZ3210_5ksufhqi",
            backlightModeOffOn: (m) => m !== "_TZ3000_criiahcg",
            indicatorMode: true,
            onOffCountdown: true,
            inchingSwitch: true,
            endpoints: ["l1", "l2"],
        }),
        tuya.clusters.addTuyaCommonPrivateCluster(),
    ],
    endpoint: (device) => {
        return {l1: 1, l2: 2};
    },
    meta: {multiEndpoint: true},
    configure: async (device, coordinatorEndpoint) => {
        await tuya.configureMagicPacket(device, coordinatorEndpoint);
        await reporting.bind(device.getEndpoint(1), coordinatorEndpoint, ["genOnOff"]);
        await reporting.bind(device.getEndpoint(2), coordinatorEndpoint, ["genOnOff"]);
        if (device.manufacturerName === "_TZ3210_5ksufhqi") {
            await reporting.onOff(device.getEndpoint(1));
            await reporting.onOff(device.getEndpoint(2));
        }
    },
    whiteLabel: [
        tuya.whitelabel("Zemismart", "TB26-2", "2 Gang switch with backlight, countdown, inching", ["_TZ3000_ywubfuvt"]),
        {vendor: "Zemismart", model: "ZM-CSW002-D_switch"},
        {vendor: "Lonsonho", model: "X702"},
        {vendor: "AVATTO", model: "ZTS02"},
        tuya.whitelabel("PSMART", "T462", "2 Gang switch with backlight, countdown, inching", ["_TZ3000_wnzoyohq"]),
        tuya.whitelabel("Nova Digital", "FZB-2", "2-Gang switch with backlight, countdown and inching", ["_TZ3000_5ksufhqi"]),
        tuya.whitelabel("Nova Digital", "NFZB-2", "2-Gang switch with backlight, countdown and inching", ["_TZ3210_5ksufhqi"]),
        tuya.whitelabel("iHseno", "_TZ3000_zxrfobzw", "2-gang touch switch", ["_TZ3000_zxrfobzw"]),
        tuya.whitelabel("Moes", "ZM4LT2", "2-gang switch module", ["_TZ3000_criiahcg"]),
        tuya.whitelabel("Tuya", "ZG-2002-RF", "Three mode Zigbee Switch", [
            "_TZ3000_lugaswf8",
            "_TZ3000_nuenzetq",
            "_TZ3000_ruldv5dt",
            "_TZ3210_nuenzetq",
        ]),
    ],
}

What approach would you prefer here: keeping the dedicated definition, or integrating these exceptions into the existing TS0002 definition? Could you also please check whether the integrated implementation above is correct? I am not very familiar with adding model-specific behavior to shared definitions without affecting the existing models.

model: "NFZB-2",
vendor: "Nova Digital",
description: "2-Gang switch with backlight, countdown and inching",
extend: [
tuya.modernExtend.tuyaBase(),
tuya.modernExtend.tuyaOnOff({
powerOutageMemory: true,
backlightModeOffOn: true,
indicatorMode: true,
onOffCountdown: true,
inchingSwitch: true,
endpoints: ["l1", "l2"],
}),
tuya.clusters.addTuyaCommonPrivateCluster(),
],
endpoint: () => {
return {l1: 1, l2: 2};
},
meta: {multiEndpoint: true},
configure: async (device, coordinatorEndpoint) => {
await tuya.configureMagicPacket(device, coordinatorEndpoint);
await reporting.bind(device.getEndpoint(1), coordinatorEndpoint, ["genOnOff"]);
await reporting.bind(device.getEndpoint(2), coordinatorEndpoint, ["genOnOff"]);
await reporting.onOff(device.getEndpoint(1));
await reporting.onOff(device.getEndpoint(2));
},
},
];