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
4 changes: 2 additions & 2 deletions device_db.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2847,7 +2847,7 @@ REMOTE_MOES_SWITCH_TS0044:
tuya_module: ZT3L
mcu_family: Telink
mcu: TLSR8258
config_str: zgyzgdua;TS0044-MOES;SD2d;SC3d;SC2d;SB4d;LD4i;BTC5;M;
config_str: zgyzgdua;TS0044-MOES;SD2d;IC4i;SC3d;IA0i;SC2d;ID7i;SB4d;ID4i;BTC5;M;D0;
alt_config_str: null
old_manufacturer_names: null
old_zb_models: null
Expand All @@ -2856,7 +2856,7 @@ REMOTE_MOES_SWITCH_TS0044:
firmware_image_type: 43572
build: yes
status: in_progress
info: Huge battery drain! LEDs on C4, A0, D7 and D4, WIP for led control as press confirmation, no relays.
info: no relay
threads: https://github.com/romasku/tuya-zigbee-switch/issues/342
store: https://moeshouse.com/products/zigbee-wireless-scene-switch-battery-powered?variant=43700045283643
REMOTE_TUYA_TS0041:
Expand Down
7 changes: 7 additions & 0 deletions docs/changelog_fw.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ Please describe what you are working on:
- **Push-button without relay** support for battery-powered scene switches
- Switches auto-detect absence of relays and default to detached mode
- New device: Moes 4-gang scene switch (`REMOTE_MOES_SWITCH_TS0044`)
- **Multi-press action reporting** — firmware detects single, double, triple (and beyond) presses and reports them as a Z2M `action` event (`1_single`, `2_double`, `1_hold`…). Automations are handled entirely in Z2M/ZHA.
- ⚠️ **Breaking change**: per-gang `switch_N_press_action` attributes are replaced by a single `action` field. Update your automations.
- Action values: `{n}_single`, `{n}_single_hold`, `{n}_single_release`, `{n}_double`, `{n}_double_hold`, `{n}_double_release`, `{n}_triple`, `{n}_triple_hold`, `{n}_triple_release`, `{n}_position_on`, `{n}_position_off`
- Two new configurable attributes per switch endpoint:
- **`confirm_release_ms`** (0xFF06) — window to wait for a follow-up press before confirming the count (default 200 ms)
- **`max_press_count`** (0xFF07) — maximum number of presses to detect; set to 3 for triple-press support (default 2)
- Fully backward-compatible at firmware level: firmware `long_press` (value 2) maps to `{n}_single_hold`
- **Battery measurement & reporting** (Zigbee `genPowerCfg` cluster) for Telink devices
- **Deep retention sleep** for Telink end devices

Expand Down
64 changes: 54 additions & 10 deletions helper_scripts/templates/switch_custom.js.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const {assertString} = require("zigbee-herdsman-converters/lib/utils");
const reporting = require("zigbee-herdsman-converters/lib/reporting");
const constants = require("zigbee-herdsman-converters/lib/constants");
const Zcl = require('zigbee-herdsman').Zcl;
const e = require("zigbee-herdsman-converters/lib/exposes");
{% if z2m_v1 %}
const ota = require("zigbee-herdsman-converters/lib/ota");
{% endif%}
Expand Down Expand Up @@ -107,16 +108,27 @@ const romasku = {
valueMax: 255,
entityCategory: "config",
}),
pressAction: (name, endpointName) =>
enumLookup({
confirmReleaseMs: (name, endpointName) =>
numeric({
name,
endpointName,
access: "STATE_GET",
lookup: { released: 0, press: 1, long_press: 2, position_on: 3, position_off: 4 },
cluster: "genMultistateInput",
attribute: "presentValue",
description: "Action of the switch: 'released' or 'press' or 'long_press'",
entityCategory: "diagnostic",
endpointNames: [endpointName],
cluster: "genOnOffSwitchCfg",
attribute: { ID: 0xff06, type: 0x21 }, // uint16
description: "Time (ms) to wait after release before confirming press count (single vs double etc.)",
valueMin: 0,
valueMax: 2000,
entityCategory: "config",
}),
maxPressCount: (name, endpointName) =>
numeric({
name,
endpointNames: [endpointName],
cluster: "genOnOffSwitchCfg",
attribute: { ID: 0xff07, type: 0x20 }, // uint8
description: "Maximum number of consecutive presses to differentiate (1=single only, 2=single+double, 3=+triple)",
valueMin: 1,
valueMax: 3,
entityCategory: "config",
}),
relayIndicatorMode: (name, endpointName) =>
enumLookup({
Expand Down Expand Up @@ -360,6 +372,14 @@ const romasku = {
}),
};

// Multistate presentValue → action suffix mapping (values 0 and 1 are not published)
const SWITCH_MULTISTATE_ACTION_MAP = {
2: 'single_hold', 3: 'position_on', 4: 'position_off',
5: 'single', 6: 'single_release',
7: 'double', 8: 'double_hold', 9: 'double_release',
10: 'triple', 11: 'triple_hold', 12: 'triple_release',
};

const definitions = [
{% for device in devices %}
{
Expand Down Expand Up @@ -424,8 +444,30 @@ const definitions = [
{% if device.relayNames %}
onOff({ endpointNames: {{device.relayNames | tojson }} }),
{% endif %}
{% if device.switchNames %}
{%- set ns = device.switchNames | length -%}
{%- set action_values = [] -%}
{%- for i in range(1, ns + 1) -%}
{%- for s in ['single', 'single_hold', 'single_release', 'double', 'double_hold', 'double_release', 'triple', 'triple_hold', 'triple_release', 'position_on', 'position_off'] -%}
{%- set _ = action_values.append(i ~ '_' ~ s) -%}
{%- endfor -%}
{%- endfor -%}
{
fromZigbee: [{
cluster: 'genMultistateInput',
type: ['attributeReport', 'readResponse'],
convert: (model, msg, publish, options, meta) => {
if (msg.endpoint.ID > {{ns}}) return;
const suffix = SWITCH_MULTISTATE_ACTION_MAP[msg.data['presentValue']];
if (suffix === undefined) return;
return {action: `${msg.endpoint.ID}_${suffix}`};
},
}],
toZigbee: [],
exposes: [e.action({{ action_values | tojson }})],
},
{% endif %}
{% for switchName in device.switchNames %}
romasku.pressAction("{{switchName}}_press_action", "{{switchName}}"),
romasku.switchMode("{{switchName}}_mode", "{{switchName}}"),
romasku.switchAction("{{switchName}}_action_mode", "{{switchName}}"),
{% if device.relayNames %}
Expand All @@ -435,6 +477,8 @@ const definitions = [
romasku.bindedMode("{{switchName}}_binded_mode", "{{switchName}}"),
romasku.longPressDuration("{{switchName}}_long_press_duration", "{{switchName}}"),
romasku.levelMoveRate("{{switchName}}_level_move_rate", "{{switchName}}"),
romasku.confirmReleaseMs("{{switchName}}_confirm_release_ms", "{{switchName}}"),
romasku.maxPressCount("{{switchName}}_max_press_count", "{{switchName}}"),
{% endfor %}
{% for relayName in device.relayIndicatorNames %}
romasku.relayIndicatorMode("{{relayName}}_indicator_mode", "{{relayName}}"),
Expand Down
21 changes: 11 additions & 10 deletions src/device_config/config_parser.c
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ void parse_config() {
memcpy(basic_cluster.modelId + 1, zb_model, basic_cluster.modelId[0]);

bool has_dedicated_status_led = false;
uint8_t switch_indicator_cnt = 0;
uint16_t debounce_ms = DEBOUNCE_DELAY_MS;
char * entry;
for (entry = extract_next_entry(&cursor); *entry != '\0';
Expand All @@ -119,7 +120,7 @@ void parse_config() {
// Simultaneous Latching Pulses == SLP
allow_simultaneous_latching_pulses = 1;
} else if (entry[0] == 'D' && entry[1] >= '0' && entry[1] <= '9') {
// D<N> sets the global debounce duration in milliseconds.
// D<N> global debounce duration in ms (e.g. D0 to disable)
debounce_ms = (uint16_t)parse_int(entry + 1);
for (int i = 0; i < buttons_cnt; i++) {
buttons[i].debounce_delay_ms = debounce_ms;
Expand Down Expand Up @@ -161,18 +162,18 @@ void parse_config() {
leds[leds_cnt].on_high = entry[3] != 'i';
led_init(&leds[leds_cnt]);

for (int index = 0; index < 4; index++) {
if (relay_clusters[index].indicator_led == NULL) {
relay_clusters[index].indicator_led = &leds[leds_cnt];
break;
if (relay_clusters_cnt > 0) {
for (int index = 0; index < 4; index++) {
if (relay_clusters[index].indicator_led == NULL) {
relay_clusters[index].indicator_led = &leds[leds_cnt];
break;
}
}
}

for (int index = 0; index < 4; index++) {
if (switch_clusters[index].indicator_led == NULL) {
switch_clusters[index].indicator_led = &leds[leds_cnt];
break;
}
if (switch_indicator_cnt < switch_clusters_cnt) {
switch_clusters[switch_indicator_cnt].indicator_led = &leds[leds_cnt];
switch_indicator_cnt++;
}

if (!has_dedicated_status_led) {
Expand Down
18 changes: 10 additions & 8 deletions src/zigbee/consts.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,16 @@

// OnOff configuration cluster

#define ZCL_ATTR_ONOFF_CONFIGURATION_SWITCH_TYPE 0x0000
#define ZCL_ATTR_ONOFF_CONFIGURATION_SWITCH_ACTIONS 0x0010
#define ZCL_ATTR_ONOFF_CONFIGURATION_SWITCH_MODE 0xff00
#define ZCL_ATTR_ONOFF_CONFIGURATION_SWITCH_RELAY_MODE 0xff01
#define ZCL_ATTR_ONOFF_CONFIGURATION_SWITCH_RELAY_INDEX 0xff02
#define ZCL_ATTR_ONOFF_CONFIGURATION_SWITCH_LONG_PRESS_DUR 0xff03
#define ZCL_ATTR_ONOFF_CONFIGURATION_SWITCH_LEVEL_MOVE_RATE 0xff04
#define ZCL_ATTR_ONOFF_CONFIGURATION_SWITCH_BINDING_MODE 0xff05
#define ZCL_ATTR_ONOFF_CONFIGURATION_SWITCH_TYPE 0x0000
#define ZCL_ATTR_ONOFF_CONFIGURATION_SWITCH_ACTIONS 0x0010
#define ZCL_ATTR_ONOFF_CONFIGURATION_SWITCH_MODE 0xff00
#define ZCL_ATTR_ONOFF_CONFIGURATION_SWITCH_RELAY_MODE 0xff01
#define ZCL_ATTR_ONOFF_CONFIGURATION_SWITCH_RELAY_INDEX 0xff02
#define ZCL_ATTR_ONOFF_CONFIGURATION_SWITCH_LONG_PRESS_DUR 0xff03
#define ZCL_ATTR_ONOFF_CONFIGURATION_SWITCH_LEVEL_MOVE_RATE 0xff04
#define ZCL_ATTR_ONOFF_CONFIGURATION_SWITCH_BINDING_MODE 0xff05
#define ZCL_ATTR_ONOFF_CONFIGURATION_SWITCH_CONFIRM_RELEASE_DUR 0xff06
#define ZCL_ATTR_ONOFF_CONFIGURATION_SWITCH_MAX_PRESS_COUNT 0xff07


// Multistate cluster
Expand Down
Loading
Loading