z2m: ESP32-C6 Zigbee Button Multi-Gesture-Erkennung (Single/Double/Triple/Hold)

Begonnen von TomLee, 17 Juni 2026, 17:03:54

Vorheriges Thema - Nächstes Thema

TomLee

Hi,

getestet mit ESP32-C6 Super Mini, Arduino Framework, Zigbee2MQTT.

#include <Arduino.h>

#ifndef ZIGBEE_MODE_ZCZR
#error "Select Zigbee ZCZR mode in Tools -> Zigbee mode"
#endif

#include "Zigbee.h"

#define BUTTON_PIN BOOT_PIN

#define ACTION_SINGLE 1
#define ACTION_DOUBLE 2
#define ACTION_TRIPLE 3
#define ACTION_HOLD   4

ZigbeeMultistate buttonDevice(1);

bool lastState = HIGH;
uint32_t pressStart = 0;
uint32_t lastRelease = 0;
uint8_t clickCount = 0;
bool holdSent = false;

void sendAction(uint8_t action)
{
    buttonDevice.setMultistateInput(action);
    buttonDevice.reportMultistateInput();
    Serial.printf("Action sent: %u\n", action);
}

void setup()
{
    Serial.begin(115200);
    pinMode(BUTTON_PIN, INPUT_PULLUP);

    // Factory Reset: BOOT-Button beim Start gedrückt halten
    delay(200);
    if (digitalRead(BUTTON_PIN) == LOW) {
        Serial.println("Factory reset triggered!");
        Zigbee.factoryReset();
    }

    buttonDevice.setManufacturerAndModel("Custom", "ESP32-Zigbee-Button");
    buttonDevice.addMultistateInput();
    buttonDevice.setMultistateInputStates(4);

    Zigbee.addEndpoint(&buttonDevice);

    Serial.println("Starting Zigbee...");

    if (!Zigbee.begin(ZIGBEE_ROUTER))
    {
        Serial.println("Zigbee start failed!");
        ESP.restart();
    }

    // Timeout nach 30s – läuft auch ohne Verbindung weiter
    uint32_t timeout = millis();
    while (!Zigbee.connected() && millis() - timeout < 30000)
    {
        Serial.print(".");
        delay(100);
    }

    if (Zigbee.connected()) {
        Serial.println("\nConnected");
    } else {
        Serial.println("\nNot connected, running anyway...");
    }
}

void loop()
{
    bool state = digitalRead(BUTTON_PIN);

    if (state == LOW && lastState == HIGH)
    {
        pressStart = millis();
        holdSent = false;
    }

    if (state == LOW && !holdSent && millis() - pressStart > 1000)
    {
        sendAction(ACTION_HOLD);
        holdSent = true;
        clickCount = 0;
    }

    if (state == HIGH && lastState == LOW)
    {
        if (!holdSent)
        {
            clickCount++;
            lastRelease = millis();
        }
    }

    if (clickCount > 0 && millis() - lastRelease > 500)
    {
        if (clickCount == 1) sendAction(ACTION_SINGLE);
        else if (clickCount == 2) sendAction(ACTION_DOUBLE);
        else sendAction(ACTION_TRIPLE);
        clickCount = 0;
    }

    lastState = state;
    delay(10);
}

Arduino IDE Einstellungen:

  • Board ESP32C6 Dev Module
  • USB CDC On Boot Enabled
  • CPU Frequency 160MHz (WiFi)
  • Core Debug Level None
  • Erase All Flash Before Sketch Upload Enabled
  • Flash Frequency 80MHz
  • Flash Mode QIO
  • Flash Size 4MB (32Mb)
  • JTAG Adapter Disabled
  • Partition SchemeZigbee ZCZR 4MB with spiffs
  • Upload Speed 921600
  • Zigbee ModeZigbee ZCZR (coordinator/router)

Z2M External Converter:
Datei /opt/zigbee2mqtt/data/external_converters/custom_devices.js:

const e = require('zigbee-herdsman-converters/lib/exposes');

const lastSeen = {};

const definition = {
    zigbeeModel: ['ESP32-Zigbee-Button'],
    model: 'ESP32-Zigbee-Button',
    vendor: 'Custom',
    description: 'ESP32-C6 Zigbee Button',
    fromZigbee: [
        {
            cluster: 'genMultistateInput',
            type: ['attributeReport', 'readResponse'],
            convert: (model, msg, publish, options, meta) => {
                const actionMap = {1: 'single', 2: 'double', 3: 'triple', 4: 'hold'};
                const value = msg.data['presentValue'];
                if (!actionMap[value]) return;

                // Duplikat-Filter für Mesh-Routing
                const key = `${meta.device.ieeeAddr}_${value}`;
                const now = Date.now();
                if (lastSeen[key] && now - lastSeen[key] < 1000) return;
                lastSeen[key] = now;

                return {action: actionMap[value]};
            },
        },
    ],
    toZigbee: [],
    exposes: [e.enum('action', e.access.STATE, ['single', 'double', 'triple', 'hold'])
        .withDescription('Button action')],
    configure: async (device, coordinatorEndpoint) => {
        const endpoint = device.getEndpoint(1);
        await endpoint.bind('genMultistateInput', coordinatorEndpoint);
        await endpoint.configureReporting('genMultistateInput', [{
            attribute: 'presentValue',
            minimumReportInterval: 0,
            maximumReportInterval: 0,
            reportableChange: 1,
        }]);
    },
};

module.exports = definition;

z2m Log:[17.6.2026, 16:33:38] z2m:mqtt: MQTT publish: topic 'zigbee2mqtt/0x543204fffe3d996c', payload '{"action":"single","linkquality":183}'

[17.6.2026, 16:33:40] z2m:mqtt: MQTT publish: topic 'zigbee2mqtt/0x543204fffe3d996c', payload '{"action":"double","linkquality":183}'

[17.6.2026, 16:33:42] z2m:mqtt: MQTT publish: topic 'zigbee2mqtt/0x543204fffe3d996c', payload '{"action":"triple","linkquality":122}'

[17.6.2026, 16:33:44] z2m:mqtt: MQTT publish: topic 'zigbee2mqtt/0x543204fffe3d996c', payload '{"action":"hold","linkquality":191}'

Kein Eintrag in configuration.yaml nötig. Z2M lädt alle Dateien aus dem external_converters-Ordner automatisch.



Hinweise:

  • Als Router gedacht (dauerhaft am Strom), erweitert das Zigbee-Mesh
  • Factory Reset: BOOT-Button beim Einschalten gedrückt halten
  • Beim ersten Pairing: in Z2M permit_join aktivieren, dann ESP starten
  • Getestet mit Zigbee2MQTT v2.9.1 / zigbee-herdsman-converters v26.12.0

Hat ne Weile gedauert bis alles lief, aber der Weg war das Ziel. Spaß hats auf jeden Fall gemacht!
Verbesserungsvorschläge, Ideen oder Erweiterungen sind herzlich willkommen.

Gruss Thomas