ESP32-C3 (WiFi/Homeyduino) + ESP32-C6 (native Zigbee) — my experience as a beginner, surprisingly easy with Claude Code"

Wanted to share my recent experience testing two different approaches to integrate DIY hardware with Homey Pro, in case it helps others getting started:

1) ESP32-C3 Mini + WiFi + Homeyduino app
I set up an on/off LED using the Homeyduino app (see thread Homeyduino @ V2.0), which lets you program the ESP32-C3 from Arduino IDE and integrate it directly as a device in Homey. The process was straightforward: flash via USB, and Homey picked it up without any friction, automatically creating the device title and on/off flow cards, ready to use right away.

2) ESP32-C6 (Waveshare DEV-KIT-NX) + native/generic Zigbee, no ESPHome
This is where I was really surprised. Using Arduino IDE with the ESP32 core 3.x, which now has the Zigbee library built in (standard On/Off cluster from the Zigbee Cluster Library), I programmed the C6 as a Zigbee End Device. When pairing, Homey Pro (acting as native Zigbee coordinator) detected it as a generic Zigbee device, no driver or extra app needed — it got recognized on the first try, easier than some branded commercial devices I’d tried before. It also generated its own title and flow cards automatically, just like a commercial device would.

My takeaway: when you stick to standard clusters (not proprietary/manufacturer-specific ones), Homey doesn’t just recognize the device without friction — it builds the whole integration (name, flow cards, capabilities) automatically.

Worth mentioning: I wrote and set up both projects entirely with Claude Code (Anthropic’s AI coding assistant). It wrote the Arduino sketches, configured the Zigbee endpoint/cluster setup for the C6, and walked me through wiring, board settings, and pairing — I described what I wanted, and it produced working, uploadable code each time. What struck me most was the speed: both projects went from zero to working devices paired in Homey in just a handful of iterations, without me needing to read through ESP-IDF documentation or the Zigbee spec myself. It significantly lowered the barrier to entry for this kind of DIY Zigbee project — something that used to require real embedded/protocol expertise.

I’m now considering switching the C6 to Router mode (since it’s always USB-powered) to help strengthen the Zigbee mesh at home.

If anyone’s thinking about trying “pure” ESP32 + Zigbee (no ESPHome/Z2M) with Homey, I’d definitely recommend it — the learning curve was much lower than I expected, especially with AI-assisted coding doing the heavy lifting.

Interesting findings! I was actually planning on using some ESP32-C6’s to add Zigbee to some IKEA Ädellövskog candles powered by 2 AA batteries.

Very interesting, especially the battery angle - please share how it goes once you test it!

Also, this kind of experience makes me think Homey could really benefit from building more of a maker community around it, similar to what Home Assistant has. With tools like these, everything becomes so much more accessible - you don’t even need to know how to code anymore, which means we don’t really need ESPHome or MQTT in the middle to get a working device.

From here, I’d encourage Homey to keep developing further in this direction.

As for the ESP32-C6 those are also great to create Thread/Matter devices. One of them is a replacement for a Ring Intercom since my building has two doors and it integrates great into any ecosystem. More complex devices on Zigbee tend to need custom clusters which needs to be implemented per ecosystem integration which is why Matter is great since it has more standardized clusters and capabilities out of the box.

EDIT: I can recommend the Olimex ESP32-C5 EVB and ESP32-C6 EVB as they come with relays and opto-inputs ready to go.

I Can you provide a sample code for in the esp32-c3?

C3 doesn’t support Zigbee/Thread.

Thanks! I’m curious about your Homey integration. Did you expose the ESP32-C6 directly as a Matter device, or did you use another middleware? How has the overall experience been?

Also, are you using End Devices or Router Devices?

I’m working on an apartment intercom project for the same reason. Mine needs to interface with a Comelit Simplebus 2 system, so I’m currently evaluating whether Zigbee or Matter is the better approach.

And thanks for the Olimex board recommendation—I hadn’t come across those before.

Yes, exactly as @robertklep said, that board isn’t running Zigbee—it’s using Wi-Fi, and it works really well.

For my first project I’m actually using a standard ESP32 DevKit board. The only code I currently have for the ESP32-C3 is a simple LED on/off example.

I actually have two separate .ino files: one is the LED example for the ESP32-C3 Mini, and the other is for my humidifier project using a standard ESP32 board.

// Sketch for ESP32-C3 mini with Homeyduino
// Built-in LED on GPIO 8, controllable from the Homey app (WiFi)
// and from the Serial Monitor (USB) by typing “on”, “off” or “toggle” + Enter

#include <WiFi.h>
#include <WiFiClient.h>
#include <Homey.h>

// — Network configuration —
// Replace these values with your own WiFi network’s
const char* WIFI_SSID = “MOVISTAR_4A08”;
const char* WIFI_PASSWORD = “TU_CONTRASEÑA_WIFI”;

const int LED_PIN = 8;

// Test pin used to check which GPIOs we’ll use in the humidifier
// project. First we’re testing pin 5.
const int TEST_LED_PIN = 5;

// Current LED state (true = on, false = off)
bool ledState = false;

// Current state of the test LED on pin 5
bool testLedState = false;

// Buffer that accumulates characters received over the Serial Monitor
String serialBuffer = “”;

// Applies the state to the LED’s physical pin
// (the built-in LED is usually active-LOW, hence the inverted signal)
void applyLedState() {
digitalWrite(LED_PIN, ledState ? LOW : HIGH);
}

// Applies the state to the test LED on pin 5
// (standard external LED: active-HIGH, signal not inverted)
void applyTestLedState() {
digitalWrite(TEST_LED_PIN, testLedState ? HIGH : LOW);
}

// Callback called by Homey when the secondary switch (“onoff.led5”) is pressed in the app
// (controls the built-in LED; the capability name is kept for compatibility with Homey,
// even though it no longer controls pin 5)
void onHomeyOnOff() {
ledState = Homey.value.toInt(); // Converts “1”/“0” received from Homey to bool
// (assigning Homey.value directly to a bool
// always evaluates to true, which is why “off” wasn’t working)
applyLedState();
Serial.println(ledState ? “Homey: LED on” : “Homey: LED off”);
}

// Callback called by Homey when the main switch (“onoff”) is pressed in the app
// (controls the test LED on pin 5)
void onHomeyTestLedOnOff() {
testLedState = Homey.value.toInt(); // Same as with the built-in LED: needs toInt() conversion
applyTestLedState();
Serial.println(testLedState ? “Homey: LED pin 5 on” : “Homey: LED pin 5 off”);
}

// Changes the LED state and syncs the switch in the Homey app
void setLedState(bool newState) {
ledState = newState;
applyLedState();
Homey.setCapabilityValue(“onoff.led5”, ledState); // Updates the secondary switch in the app
Serial.println(ledState ? “Serial: LED on” : “Serial: LED off”);
}

// Changes the state of the test LED on pin 5 and syncs its own switch in Homey
void setTestLedState(bool newState) {
testLedState = newState;
applyTestLedState();
Homey.setCapabilityValue(“onoff”, testLedState); // Updates the main switch in the app
Serial.println(testLedState ? “Serial: LED pin 5 on” : “Serial: LED pin 5 off”);
}

// Processes a text command received over the Serial Monitor
void processSerialCommand(String command) {
command.trim();
command.toLowerCase();

if (command == “on”) {
setLedState(true);
} else if (command == “off”) {
setLedState(false);
} else if (command == “toggle”) {
setLedState(!ledState);
} else if (command == “led5 on”) {
setTestLedState(true);
} else if (command == “led5 off”) {
setTestLedState(false);
} else if (command == “led5 toggle”) {
setTestLedState(!testLedState);
} else if (command.length() > 0) {
Serial.println(“Unrecognized command. Use: on / off / toggle / led5 on / led5 off / led5 toggle”);
}
}

// Reads characters from the Serial Monitor without blocking the main loop
void handleSerialInput() {
while (Serial.available() > 0) {
char c = Serial.read();
if (c == ‘\n’ || c == ‘\r’) {
if (serialBuffer.length() > 0) {
processSerialCommand(serialBuffer);
serialBuffer = “”;
}
} else {
serialBuffer += c;
}
}
}

void setup() {
Serial.begin(115200);
delay(500); // gives the native USB Serial time to finish connecting before printing

pinMode(LED_PIN, OUTPUT);
applyLedState(); // LED off at startup

pinMode(TEST_LED_PIN, OUTPUT);
applyTestLedState(); // Test LED (pin 5) off at startup

// WiFi connection
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print(“Connecting to WiFi”);
while (WiFi.status() != WL_CONNECTED) {
delay(500); // Only blocks during the initial connection, not in the loop
Serial.print(“.”);
}
Serial.println();
Serial.print("Connected. IP: ");
Serial.println(WiFi.localIP());

// Register the device with Homey
Homey.begin(“LedTest”);
Homey.setClass(“light”);

// Main capability “onoff”: the one shown as a direct button on the Homey
// card/dashboard. Controls the test LED on pin 5.
Homey.addCapability(“onoff”, onHomeyTestLedOnOff);

// Second capability “onoff.led5”: secondary switch, only visible inside the
// device detail view (Homey doesn’t give it its own dashboard button). Controls the built-in LED.
Homey.addCapability(“onoff.led5”, onHomeyOnOff);

Serial.println(“Ready. Type on / off / toggle in the Serial Monitor.”);
Serial.println(“To test the LED on pin 5: led5 on / led5 off / led5 toggle”);
}

void loop() {
Homey.loop(); // Must be called constantly, no delay() in this loop
handleSerialInput();
}

// Test sketch for the ESP32-WROOM-32 DevKit board (30 pins)
// of the “humidificador dormitorio” project. Controlled via Homey (WiFi) and
// the Serial Monitor.
//
// GPIO2 = opto channel 1 (dry contact, simulates touching the humidifier’s
// original capacitive ON/mode button).
// GPIO18 = trigger for the relay module (Songle SRD-05VDC-SL-C) that cuts and
// restores the humidifier’s 24V supply.
//
// Business logic (fixed 2026-07-19 to go back to what the original brief
// intended): the relay and the opto are NOT independent, they run as a
// single sequence triggered by the “onoff” capability:
// - OFF: the relay is cut briefly and RESTORES itself (non-blocking
// sequence) → the humidifier ends up idle but still WITH power,
// same as if someone had unplugged and replugged it.
// This way the humidifier’s physical/capacitive button keeps working
// after an OFF from Homey (if the relay were left cut
// permanently, the humidifier would be left without power and the
// manual button wouldn’t respond). The opto is never touched on OFF.
// - ON: the relay is NOT touched (the humidifier should already be idle
// after the last OFF). It just waits a short settling margin
// and then activates the opto to simulate touching the
// ON/mode button. If ON arrives while the relay is still
// cut (e.g. ON right after an OFF), it’s restored first.
//
// Both modules can be wired to trigger active-high or active-low depending on
// jumper/design → if either one activates backwards from what’s expected (clicks or
// lights its LED when it shouldn’t), flip the corresponding _ACTIVE_LOW.

#include <WiFi.h>
#include <WiFiClient.h>
#include <ArduinoOTA.h>
#include <Homey.h>

// — Network configuration —
const char* WIFI_SSID = “MOVISTAR_4A08”;
const char* WIFI_PASSWORD = “TU_CONTRASEÑA_WIFI”;

const int RELAY_PIN = 18;
const bool RELAY_ACTIVE_LOW = false; // confirmed 2026-07-18

const int OPTO1_PIN = 2;
const bool OPTO1_ACTIVE_LOW = false; // adjust if the simulated tap doesn’t register

const unsigned long RELAY_PULSE_MS = 500; // duration of the cut during shutdown, then restores itself
const unsigned long OPTO_SETTLE_MS = 300; // wait before tapping the button (in case the relay just restored)
const unsigned long OPTO_PULSE_MS = 250; // duration of the simulated tap on the opto
// OPTO_SETTLE_MS and OPTO_PULSE_MS are a starting point, they need tuning
// by testing whether the humidifier actually turns on with these timings.

bool humidifierOn = false; // logical state reported to Homey

void setRelayCut(bool cut) {
bool pinLevel = RELAY_ACTIVE_LOW ? !cut : cut;
digitalWrite(RELAY_PIN, pinLevel ? HIGH : LOW);
}

bool relayIsCut = false; // reflects the last call to setRelayCut()

void setRelayCutTracked(bool cut) {
relayIsCut = cut;
setRelayCut(cut);
}

void setOptoTap(bool active) {
bool pinLevel = OPTO1_ACTIVE_LOW ? !active : active;
digitalWrite(OPTO1_PIN, pinLevel ? HIGH : LOW);
}

// Non-blocking sequences (no delay() in loop()):
// OFF: cut relay → wait RELAY_PULSE_MS → restore relay → idle
// ON: (if the relay was still cut, restore it now) → wait
// OPTO_SETTLE_MS → tap opto → wait OPTO_PULSE_MS → release opto
enum ActionStage { STAGE_IDLE, STAGE_OFF_CUTTING, STAGE_ON_SETTLING, STAGE_ON_TAPPING };
ActionStage stage = STAGE_IDLE;
unsigned long stageStartMillis = 0;

void startTurnOffSequence() {
stage = STAGE_OFF_CUTTING;
stageStartMillis = millis();
setRelayCutTracked(true);
Serial.println(“OFF: cutting power for 0.5s (restores itself)…”);
}

void startTurnOnSequence() {
if (relayIsCut) {
setRelayCutTracked(false);
}
stage = STAGE_ON_SETTLING;
stageStartMillis = millis();
Serial.println(“ON: waiting to settle before tapping the button…”);
}

void updateActionSequence() {
if (stage == STAGE_IDLE) return;
unsigned long elapsed = millis() - stageStartMillis;

if (stage == STAGE_OFF_CUTTING && elapsed >= RELAY_PULSE_MS) {
setRelayCutTracked(false);
stage = STAGE_IDLE;
Serial.println(“OFF: power restored, humidifier idle.”);
} else if (stage == STAGE_ON_SETTLING && elapsed >= OPTO_SETTLE_MS) {
setOptoTap(true);
stageStartMillis = millis();
stage = STAGE_ON_TAPPING;
Serial.println(“ON: tapping button (opto)…”);
} else if (stage == STAGE_ON_TAPPING && elapsed >= OPTO_PULSE_MS) {
setOptoTap(false);
stage = STAGE_IDLE;
Serial.println(“ON: sequence complete.”);
}
}

void turnOff() {
setOptoTap(false); // cancels any opto tap in progress
humidifierOn = false;
// Confirms the value to Homey; without this Homey doesn’t know the command
// was applied and after several unconfirmed attempts ends up marking
// the device as “not found”.
Homey.setCapabilityValue(“onoff”, humidifierOn);
startTurnOffSequence();
}

void turnOn() {
humidifierOn = true;
Homey.setCapabilityValue(“onoff”, humidifierOn);
startTurnOnSequence();
}

// Callback called by Homey when the “onoff” switch is pressed in the app
void onHomeyOnOff() {
bool wantOn = Homey.value.toInt(); // toInt() to correctly convert “1”/“0” to bool
if (wantOn) {
turnOn();
} else {
turnOff();
}
}

void processSerialCommand(String command) {
command.trim();
command.toLowerCase();

if (command == “on”) {
turnOn();
} else if (command == “off”) {
turnOff();
} else if (command == “rele on”) {
setRelayCutTracked(true);
Serial.println(“Relay: manual cut (no sequence)”);
} else if (command == “rele off”) {
setRelayCutTracked(false);
Serial.println(“Relay: manual restore (no sequence)”);
} else if (command == “opto on”) {
setOptoTap(true);
Serial.println(“Opto: manually activated”);
} else if (command == “opto off”) {
setOptoTap(false);
Serial.println(“Opto: manually deactivated”);
} else if (command.length() > 0) {
Serial.println(“Unrecognized command. Use: on / off / rele on / rele off / opto on / opto off”);
}
}

void setup() {
Serial.begin(115200);
delay(500); // gives the native USB Serial time before printing

pinMode(RELAY_PIN, OUTPUT);
setRelayCutTracked(false); // starts without touching the relay = humidifier keeps whatever power it already had

pinMode(OPTO1_PIN, OUTPUT);
setOptoTap(false);

Serial.println();
Serial.println(“Board started. ESP32-WROOM-32 DevKit (30 pins).”);

WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print(“Connecting to WiFi”);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(“.”);
}
Serial.println();
Serial.print("Connected. IP: ");
Serial.println(WiFi.localIP());

ArduinoOTA.setHostname(“HumidificadorDormitorio”);
ArduinoOTA.setPassword(“0000”); // the IDE requires something in the Password field, doesn’t accept empty
ArduinoOTA.onStart( {
Serial.println(“OTA: update started.”);
});
ArduinoOTA.onEnd( {
Serial.println(“OTA: update complete, restarting.”);
});
ArduinoOTA.onError((ota_error_t error) {
Serial.printf(“OTA: error [%u]\n”, error);
});
ArduinoOTA.begin();
Serial.println(“OTA ready (password 0000). In Arduino IDE: Tools > Port > HumidificadorDormitorio over network.”);

Homey.begin(“HumidificadorDormitorio”);
Homey.setClass(“light”);
Homey.addCapability(“onoff”, onHomeyOnOff);

Serial.println(“Ready (OTA test #1). Use the onoff switch in Homey, or over Serial: on / off / rele on / rele off / opto on / opto off”);
}

void loop() {
ArduinoOTA.handle(); // must be called constantly, no delay() in this loop
Homey.loop(); // must be called constantly, no delay() in this loop
updateActionSequence();

if (Serial.available() > 0) {
String command = Serial.readStringUntil(‘\n’);
processSerialCommand(command);
}
}

Please use Preformatted Text for code, otherwise your code is unreadable due to Discourse formatting

Hi Eladio,

I have modified “Chip.h” to get it through the compiler.

Now it its working.

(Attachment chip.h is missing)

Thanks for looking into the problem.

I did expose the ESP32-C6 directly as a matter device with two locks nodes and one switch node. I soldered the relays to my existing intercoms buttons, so it does the same as manually pressing the buttons and then connected the external bell port to one of the opto inputs which triggers a Matter switch/button press event. This works great on basically any eco system, in contrast to Zigbee where door locks are not that well standardized.

The whole implementation was done with esp-matter and Claude.

I’m using it as a router device as I have it constantly powered. Initially I wanted to just grab some 27V from my intercom, but that caused some static on the headset. So for now I’m just powering it via usb, but I think in theory battery powering it should also be possible.