The foundation of any reliable ESP32 home automation system is a rock-solid switching node. While cloud-based smart plugs are convenient, a local MQTT-controlled relay gives you sub-50ms latency, zero reliance on external servers, and complete privacy. To build a robust node that won't fry your microcontroller or drop offline when your microwave runs, you need an ESP32-WROOM-32 (30-pin variant) paired with a 5V optocoupler-isolated relay module.

This guide walks through the exact hardware selection, safe mains wiring, and production-ready C++ code to get your first automation node online.

The Decision Path: Which ESP32 Board and Relay Module?

Not all ESP32 boards and relays are created equal. Picking the wrong combination leads to boot failures, back-EMF destruction, or unreliable switching. Use this decision matrix to select your hardware.

Criteria Option A Option B Verdict
Board Variant ESP32-WROOM-32 (30-pin DevKit v1) ESP32-S3 or ESP32-C3 Mini Pick 30-pin WROOM-32. The 30-pin layout avoids the cramped pitch of the 38-pin boards and provides ample GPIOs without the breadboard-splitting issues of the wider variants.
Relay Isolation Raw transistor-driven relay Optocoupler-isolated relay (PC817) Pick Optocoupler. Mains switching generates inductive kickback. An optocoupler physically separates the 5V logic from the 120V/240V coil, protecting the ESP32's sensitive 3.3V pins.
Relay Voltage 3.3V Relay Module 5V Relay Module with jumper Pick 5V Module. The ESP32's 3.3V pin often lacks the current (80mA+) to reliably pull in a 3.3V relay coil. Use a 5V module powered from the ESP32's VIN/5V pin.
Concrete Pick: Buy a standard 30-pin ESP32-WROOM-32 DevKit v1 (brands like HiLetgo or KeeYees are fine, ~$6) and a 5V 2-Channel Optocoupler Relay Module with Songle SRD-05VDC-SL-C relays (~$4).

Parts List and Pin Mapping

Before wiring, map your GPIOs carefully. The ESP32 has several "strapping pins" that dictate boot behavior. If you wire a relay to GPIO 12 and it pulls high on boot, your ESP32 will enter a boot loop. We use GPIO 26 and GPIO 27, which are safe for outputs.

Bill of Materials

  • Microcontroller: ESP32-WROOM-32 30-pin DevKit v1
  • Switching: 5V 2-Channel Optocoupler Relay Module
  • Power: 5V 2A USB-C or Micro-USB power supply (do not rely on your PC's USB port for mains-switching loads)
  • Wiring: 22 AWG solid core for logic, 14 AWG stranded THHN for mains connections
  • Enclosure: DIN-rail mountable plastic project box (e.g., Bud Industries PN-1321)

Pin Mapping Table

ESP32 Pin Relay Module Pin Function Notes
5V (VIN) VCC Relay Coil Power Provides the ~140mA needed for both coils.
GND GND Common Ground Required for the optocoupler LED circuit.
GPIO 26 IN1 Relay 1 Trigger Safe output pin. Active LOW on most modules.
GPIO 27 IN2 Relay 2 Trigger Safe output pin. Active LOW on most modules.

Reference: For a complete list of safe vs. strapping pins, consult the official Espressif GPIO Documentation.

Wiring the Mains Relay (Safety First)

WARNING: Mains Voltage Hazard. Working with 120V/240V AC can be fatal. De-energize the circuit at the breaker panel, verify it is dead with a non-contact voltage tester and a multimeter, and lock out the panel. If you are not comfortable with mains wiring, hire a licensed electrician. Local electrical codes (NEC/IEC) dictate enclosure and wire gauge requirements.
  1. Prep the Mains Wire: Strip 1/2 inch of insulation from your 14 AWG THHN hot (black) and neutral (white) wires.
  2. Wire the Relay COM and NO: Connect the incoming Hot wire to the COM (Common) terminal of Relay 1. Connect the outgoing Hot wire (going to your appliance/light) to the NO (Normally Open) terminal. Never use the NC (Normally Closed) terminal for home automation, or your device will turn on when the ESP32 loses power.
  3. Pass the Neutral: Wire the incoming Neutral directly to the appliance's Neutral using a Wago 221 lever nut. The relay only switches the Hot leg.
  4. Secure the Logic: Connect your 22 AWG jumper wires from the ESP32 to the relay module's low-voltage side (VCC, GND, IN1, IN2).
  5. Verify and Enclose: Double-check that no stray strands of 14 AWG wire are touching the low-voltage side. Close the enclosure before applying mains power.

Complete MQTT Control Code

This code targets the ESP32-WROOM-32 30-pin DevKit v1. It uses the PubSubClient library to maintain a persistent connection to an MQTT broker (like Mosquitto or Home Assistant). It includes non-blocking reconnect logic and explicit error handling.

Install the PubSubClient library via the Arduino IDE Library Manager before compiling.


#include <WiFi.h>
#include <PubSubClient.h>

// --- Hardware Pin Definitions ---
const int RELAY_1_PIN = 26;
const int RELAY_2_PIN = 27;

// --- Network & MQTT Credentials ---
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const char* mqtt_server = "192.168.1.100"; // Your broker IP
const int mqtt_port = 1883;

// --- MQTT Topics ---
const char* topic_relay1_cmd = "home/esp32_node/relay1/set";
const char* topic_relay1_state = "home/esp32_node/relay1/state";
const char* topic_relay2_cmd = "home/esp32_node/relay2/set";
const char* topic_relay2_state = "home/esp32_node/relay2/state";

WiFiClient espClient;
PubSubClient client(espClient);

// Relay state tracking to prevent redundant MQTT publishes
bool relay1_state = false;
bool relay2_state = false;

void setup_wifi() {
  delay(10);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
  }
}

void callback(char* topic, byte* payload, unsigned int length) {
  String message = "";
  for (int i = 0; i < length; i++) {
    message += (char)payload[i];
  }

  if (String(topic) == topic_relay1_cmd) {
    relay1_state = (message == "ON");
    digitalWrite(RELAY_1_PIN, relay1_state ? LOW : HIGH); // Active LOW
    client.publish(topic_relay1_state, relay1_state ? "ON" : "OFF", true);
  }
  else if (String(topic) == topic_relay2_cmd) {
    relay2_state = (message == "ON");
    digitalWrite(RELAY_2_PIN, relay2_state ? LOW : HIGH); // Active LOW
    client.publish(topic_relay2_state, relay2_state ? "ON" : "OFF", true);
  }
}

void reconnect() {
  while (!client.connected()) {
    String clientId = "ESP32-Node-" + String(random(0xffff), HEX);
    if (client.connect(clientId.c_str())) {
      // Subscribe to command topics
      client.subscribe(topic_relay1_cmd);
      client.subscribe(topic_relay2_cmd);
      // Publish initial states
      client.publish(topic_relay1_state, relay1_state ? "ON" : "OFF", true);
      client.publish(topic_relay2_state, relay2_state ? "ON" : "OFF", true);
    } else {
      // Error handling: wait 5 seconds before retrying
      delay(5000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  
  // Initialize pins as outputs and set to HIGH (OFF for active-low relays)
  pinMode(RELAY_1_PIN, OUTPUT);
  pinMode(RELAY_2_PIN, OUTPUT);
  digitalWrite(RELAY_1_PIN, HIGH);
  digitalWrite(RELAY_2_PIN, HIGH);

  setup_wifi();
  client.setServer(mqtt_server, mqtt_port);
  client.setCallback(callback);
  client.setKeepAlive(60); // Prevents router from dropping idle TCP connections
}

void loop() {
  if (!client.connected()) {
    reconnect();
  }
  client.loop();
}

Debugging: "Connection Refused" and WiFi Drops

When your ESP32 home automation node fails, it usually manifests in the Serial Monitor as a specific error string. Here is how to diagnose the two most common failures.

Error 1: MQTT connect failed, rc=-2

This exact string means the PubSubClient library failed to establish a TCP connection to the broker. It is a network-level failure, not an authentication failure.

The First 3 Things to Check:

  1. Broker IP and Port: Verify your mqtt_server IP is correct and port 1883 is open. If using Home Assistant OS, ensure the Mosquitto add-on is running and listening on the local network interface, not just the internal Docker network.
  2. WiFi Signal Strength (RSSI): Add Serial.println(WiFi.RSSI()); to your loop. If the value is below -70, your ESP32 is dropping packets. Move the node closer to the AP or add a 2.4GHz mesh node.
  3. Router Client Isolation: Some IoT or Guest VLANs enable "AP Isolation," preventing devices on the same WiFi from talking to each other. Ensure your ESP32 and MQTT broker are on the same trusted subnet.

Error 2: Node Reboots Randomly When Relay Clicks

If the ESP32 reboots the exact millisecond the relay engages, you are experiencing a brownout caused by coil inrush current or back-EMF.

  • Ranked Cause 1: Powering the relay coil directly from the ESP32's 3.3V regulator. Fix: Use the 5V VIN pin and a 5V relay module.
  • Ranked Cause 2: Missing flyback diode. Fix: Ensure your relay module has the diode soldered across the coil pins (most blue Songle modules do).
  • Ranked Cause 3: Mains noise coupling into the logic lines. Fix: Verify you are using an optocoupler-isolated module, not a cheap transistor-switched module.

Extending or Simplifying the Build

Once you have a single node working, you will inevitably want to scale. Here is how to adapt this architecture based on your specific deployment needs.

To Simplify (Single Appliance Control):
Drop the ESP32 and use an ESP8266 NodeMCU with a 1-channel relay. The ESP8266 is ~$3, has fewer pins to worry about, and the exact same PubSubClient code will compile if you swap <WiFi.h> for <ESP8266WiFi.h>.
To Extend (Whole-Room or Multi-Zone Control):
Upgrade to an ESP32-S3-WROOM-1 paired with a 4-channel or 8-channel relay board. More importantly, implement Home Assistant MQTT Discovery. By publishing a specific JSON payload to the homeassistant/switch/config topic on boot, your ESP32 will automatically create the switch entities in Home Assistant without requiring manual YAML configuration.

Building local ESP32 home automation nodes requires respecting both the physics of inductive loads and the quirks of the ESP32's boot strapping pins. Stick to the 30-pin WROOM-32, use optocoupler isolation, and implement robust MQTT reconnect logic, and your nodes will run for years without intervention.