Skip to content

feat: add Novato ZRM01 and ZRM02 smart relay support#12139

Merged
Koenkk merged 9 commits intoKoenkk:masterfrom
bilgi-source:novato-zrm-relay
May 8, 2026
Merged

feat: add Novato ZRM01 and ZRM02 smart relay support#12139
Koenkk merged 9 commits intoKoenkk:masterfrom
bilgi-source:novato-zrm-relay

Conversation

@bilgi-source
Copy link
Copy Markdown
Contributor

@bilgi-source bilgi-source commented May 7, 2026

Adds support for Novato ZRM01 (1-channel) and ZRM02 (2-channel) smart relays based on the Tuya TS000F module.

Devices added:

  • ZRM01 — Smart relay 1 channel (_TZ3218_n0jsuogs)
  • ZRM02 — Smart relay 2 channel (_TZ3218_sgbsg6mr)

Features:

  • On/off control via genOnOff cluster
  • power_on_behavior attribute (off / on / memory) — manufacturer attribute 0x8002, code 0x1141
  • switch_type attribute (kickback / seesa_toggle / seesaw_sync) — manufacturer attribute 0x8001, code 0x1141
  • ZRM02 supports multi-endpoint (l1, l2)

Link to picture pull request: Koenkk/zigbee2mqtt.io#5098

Comment thread src/devices/tuya.ts
fingerprint: tuya.fingerprint("TS000F", ["_TZ3218_n0jsuogs"]),
model: "ZRM01",
vendor: "Novato",
description: "Smart relay 1 channel",
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.

Instead of from/toZigbee/exposes, can you try with extend: [tuya.modernExtend.tuyaBase(), tuya.modernExtend.tuyaOnOff()],, this also supports the switch type

Copy link
Copy Markdown
Contributor Author

@bilgi-source bilgi-source May 7, 2026

Choose a reason for hiding this comment

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

Hi @Koenkk, thanks for the suggestion. I tried extend: [tuya.modernExtend.tuyaBase(), tuya.modernExtend.tuyaOnOff({switchType: true, powerOnBehavior2: true})] (also tested with just switchType: true), but writing fails on this device:

Publish 'set' 'switch_type' to '0xa4c1387dc97315db' failed:
'Error: ZCL command 0xa4c1387dc97315db/1 manuSpecificTuya3.write({"switchType":1}, ...) failed (Status 'UNSUPPORTED_ATTRIBUTE')'

The helper writes to the manuSpecificTuya3 cluster, but this device exposes switch_type and power_on_behavior as manufacturer-specific attributes on the standard genOnOff cluster (manufacturerCode 0x1141, attributes 0x8001 and 0x8002, ENUM8). On/off works fine because that's just standard genOnOff.

So tuyaOnOff doesn't fit here. Would you prefer I keep the local converters (as in the original PR), or is there another modernExtend helper better suited for genOnOff manuf-specific attributes that I missed?

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.

I see, in that case use m.enumLookup() here

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.

Tested with m.enumLookup() on both ZRM01 and ZRM02 — works correctly. Pushed the updated commit:

Removed the local fzLocalNovatoZrm / tzLocalNovatoSwitchType / tzLocalNovatoPowerOnBehavior converters
Both definitions now use m.onOff({powerOnBehavior: false}) (since power_on_behavior lives on the manufacturer-specific 0x8002 attribute, not the standard one) plus two m.enumLookup calls for switch_type (0x8001) and power_on_behavior (0x8002), targeting genOnOff with manufacturerCode: 0x1141
For ZRM02 the manuf attributes are device-wide and only readable on endpoint 1, so I set endpointName: "l1" on both lookups

Thanks for the pointer to enumLookup — much cleaner than the original.

refactor: use modernExtend enumLookup for Novato ZRM01/ZRM02
Comment thread src/devices/tuya.ts
Comment on lines +26854 to +26856
fingerprint: tuya.fingerprint("TS000F", ["_TZ3218_n0jsuogs"]),
model: "ZRM01",
vendor: "Novato",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please merge ZRM01 with QS-Zigbee-SEC01-DC. It should fix this issue:

Comment thread src/devices/tuya.ts Outdated
Comment on lines +26862 to +26863
name: "switch_type",
lookup: {kickback: 0, seesa_toggle: 1, seesaw_sync: 2},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please use the standard naming: toggle, state, momentary.

Also maybe this can be switchType2 in tuyaOnOff?

Comment thread src/devices/tuya.ts Outdated
Comment on lines +26869 to +26873
m.enumLookup({
name: "power_on_behavior",
lookup: {off: 0, on: 1, memory: 2},
cluster: "genOnOff",
attribute: {ID: 0x8002, type: DataType.ENUM8},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Did you try powerOutageMemory from tuyaOnOff? Seems to be the same attribute

efactor: use tuyaOnOff(powerOutageMemory) and standard naming
@bilgi-source
Copy link
Copy Markdown
Contributor Author

Thanks @andrei-lazarov, you were right about powerOutageMemory — tested it on both ZRM01 (single endpoint) and ZRM02 (multi-endpoint with endpoints: ["l1", "l2"]) and it works correctly. Updated all three definitions:

QS-Zigbee-SEC01-DC — same fix applied, should resolve #27805. Kept it as a separate definition though since SEC01-DC is a dry-contact module while the new ZRM01/ZRM02 are mains AC switching relays — different products despite sharing the TZ3218* SoC family.
Standard naming — switch_type now uses {toggle, state, momentary} matching valueConverters.switchType2. Couldn't use a switchType2 parameter in tuyaOnOff itself — its type signature only exposes switchType / switchTypeCurtain, and that path uses manuSpecificTuya3 which these devices return UNSUPPORTED_ATTRIBUTE for. So m.enumLookup on genOnOff 0x8001 (manuf 0x1141) is the working path.
powerOutageMemory — switched from custom enumLookup for 0x8002 to tuyaOnOff({powerOutageMemory: true}). Exposes power_outage_memory with standard on / off / restore values now.

@andrei-lazarov
Copy link
Copy Markdown
Contributor

Thanks. Did you test the switch type? I think the order is wrong now.

I believe:
kickback = momentary
seesa_toggle = toggle
seesaw_sync = state

So it would be lookup: {momentary: 0, toggle: 1, state: 2},

…type naming

Per @andrei-lazarov review feedback:

- Replace custom enumLookup for 0x8002 with tuyaOnOff({powerOutageMemory: true})
- Rename switch_type values to standard Tuya naming (momentary/toggle/state)

Mapping verified against Tuya's official documentation:
- 0 (kickback / 回弹式) → momentary (spring-loaded push button)
- 1 (seesaw_toggle / 翘板翻转式) → toggle (rocker flip)
- 2 (seesaw_sync / 翘板同步式) → state (rocker sync)

Apply the same fix to QS-Zigbee-SEC01-DC (_TZ3218_hdc8bbha) which uses
the same SoC family and enum semantics. Closes #27805.

Tested on _TZ3218_hdc8bbha by feeding voltage pulses to S1:
- Value 0: latched toggle on each press-release (push button)
- Value 1: relay toggles on each input edge change (rocker flip)
- Value 2: relay tracks input level (rocker sync)
@bilgi-source
Copy link
Copy Markdown
Contributor Author

Thanks. Did you test the switch type? I think the order is wrong now.

I believe: kickback = momentary seesa_toggle = toggle seesaw_sync = state

So it would be lookup: {momentary: 0, toggle: 1, state: 2},

Hi @andrei-lazarov, thanks for pushing on this — the labels turned out to need careful checking. According to Tuya's official documentation, the enum values for these _TZ3218_* devices are:

Value Manufacturer term Chinese Tuya UI label
0 kickback 回弹式开关 (spring-back switch) Button
1 seesaw_toggle 翘板翻转式开关 (rocker flip switch) Flip
2 seesaw_sync 翘板同步式开关 (rocker sync switch) Sync

Mapped to standard zigbee-herdsman-converters labels:

lookup: {momentary: 0, toggle: 1, state: 2}

The "kickback" label is the confusing one in English — it actually refers to the spring-back action of a momentary push button (Chinese 回弹 literally means "spring back"), not "tap and reverse direction." So kickbackmomentary, not toggle.

Verified empirically on _TZ3218_hdc8bbha (SEC01-DC) by feeding voltage pulses to the S1 input:

  • Value 0: each press-release toggles the latched relay (classic push button behavior) ✓
  • Value 1: relay toggles on each input edge change (rocker flip) ✓
  • Value 2: relay tracks input level (rocker sync) ✓

Updated all three definitions in this PR:

  1. ZRM01 / ZRM02 — new entries with tuyaOnOff({powerOutageMemory: true}) and enumLookup for switch_type
  2. QS-Zigbee-SEC01-DC — same fix applied; this also resolves #27805 since switchType: true was hitting manuSpecificTuya3 and returning UNSUPPORTED_ATTRIBUTE. Now writes go to genOnOff 0x8001 with manufacturerCode: 0x1141, which the firmware accepts.

All three share the same SoC family (_TZ3218_*), same manufacturer code, and same enum semantics per Tuya's docs, so the mapping is identical across them.

@Koenkk
Copy link
Copy Markdown
Owner

Koenkk commented May 8, 2026

Thanks!

@Koenkk Koenkk merged commit 31f758b into Koenkk:master May 8, 2026
3 checks passed
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.

3 participants