To integrate real-time, non-invasive power monitoring into your next electrical wiring project, the most reliable architecture pairs an ESP32-WROOM-32 with a PZEM-004T v3.0 AC meter via isolated RS485. This setup measures up to 100A and 250V AC, and using RS485 instead of direct TTL UART prevents the ground-loop noise and logic-level frying that plagues beginner builds inside metal breaker panels.

⚠️ MAINS VOLTAGE WARNING: This project requires tapping 120V AC inside a live electrical panel. De-energize the main breaker, verify dead with a CAT III/IV multimeter, and lock out the panel before routing wires. Local codes (NEC-style guidance) may require a licensed electrician for any permanent panel modifications. Never bypass fuses or breakers.

Component Selection and 2026 Pricing

Choosing the exact variant matters. The PZEM-004T v3 comes in both TTL and RS485 versions; for noisy panel environments, the RS485 version paired with a MAX485 transceiver is mandatory for signal integrity over distances up to 1200 meters.

Component Exact Model / Variant Key Specification Approx. Cost (2026)
Microcontroller ESP32 DevKit V1 (30-pin, ESP32-WROOM-32E) Dual-core 240MHz, 3.3V logic, Hardware UART2 $6.50
AC Meter Module Peacefair PZEM-004T v3.0 (RS485 Version) Modbus RTU, 10-100A range, 0.001A resolution $9.00
Current Transformer PAC-01 Split-Core CT (Included with PZEM) 100A max, 2000:1 ratio, 15mm jaw opening (Included)
Transceiver MAX485 TTL to RS485 Module (with filtering) 5V operation, 2.5Mbps max, slew-rate limited $1.50
Overcurrent Protection Panel Mount Fuse Holder + 2A Glass Fuse (Fast-Act) 250V rated, 5x20mm ceramic/glass body $3.00

RS485 Pin Mapping and Panel Wiring

The ESP32 operates at 3.3V logic, while the MAX485 and PZEM-004T RS485 circuits expect 5V. The MAX485 module handles this level shifting safely. We use Hardware UART2 on the ESP32 to avoid conflicts with the USB serial bootloader on UART0.

ESP32 Pin (30-pin DevKit) MAX485 Module Pin Function / Notes
GPIO 16 (RX2) RO (Receiver Output) ESP32 receives data from PZEM
GPIO 17 (TX2) DI (Driver Input) ESP32 sends Modbus poll requests
GPIO 4 DE & RE (Jumpered together) Transmit Enable (HIGH) / Receive Enable (LOW)
3V3 VCC Powers MAX485 logic (3.3V is sufficient for MAX485)
GND GND Common ground reference

Mains Wiring (Inside Panel): Run 18 AWG THHN copper from a 120V breaker terminal through a 2A inline fuse holder to the AC IN terminals on the PZEM-004T. According to NFPA 70 (NEC) Article 240.4(D), 18 AWG copper is limited to 7A, making a 2A fuse highly conservative and safe for this control-circuit tap. Clamp the split-core CT around the single hot conductor of the load you are monitoring—never clamp it around the neutral or the entire Romex cable, or the magnetic fields will cancel out and read zero.

Complete ESP32 Modbus Polling Code

This code targets the ESP32 DevKit V1 (ESP32-WROOM-32E). We use the ModbusMaster library instead of the proprietary PZEM library because it exposes the raw Modbus error codes, which is critical for debugging RS485 bus collisions and timeouts.

#include <ModbusMaster.h>

// Pin definitions for MAX485 DE/RE control
#define MAX485_DE 4
#define MAX485_RE_NEG 4 // Tied to DE for half-duplex control

// Instantiate ModbusMaster object
ModbusMaster node;

// Hardware Serial2 for ESP32 (RX=16, TX=17)
#define RXD2 16
#define TXD2 17

void preTransmission() {
  digitalWrite(MAX485_DE, HIGH); // Enable RS485 Transmit
}

void postTransmission() {
  digitalWrite(MAX485_DE, LOW);  // Enable RS485 Receive
}

void setup() {
  pinMode(MAX485_DE, OUTPUT);
  digitalWrite(MAX485_DE, LOW); // Start in Receive mode

  Serial.begin(115200); // USB Debug Serial
  Serial2.begin(9600, SERIAL_8N1, RXD2, TXD2); // PZEM defaults to 9600 baud

  node.begin(1, Serial2); // PZEM default Modbus address is 1
  node.preTransmission(preTransmission);
  node.postTransmission(postTransmission);

  Serial.println("ESP32 PZEM-004T RS485 Monitor Initialized.");
}

void loop() {
  // Read 10 input registers starting at 0x0000 (Voltage, Current, Power, Energy, Freq, PF, Alarm)
  uint8_t result = node.readInputRegisters(0x0000, 10);

  if (result == node.ku8MBSuccess) {
    float voltage = node.getResponseBuffer(0x00) / 10.0;
    
    // Current is 32-bit (2 registers: low word, high word)
    uint32_t current_raw = (node.getResponseBuffer(0x02) << 16) | node.getResponseBuffer(0x01);
    float current = current_raw / 1000.0;
    
    // Power is 32-bit
    uint32_t power_raw = (node.getResponseBuffer(0x04) << 16) | node.getResponseBuffer(0x03);
    float power = power_raw / 10.0;
    
    float frequency = node.getResponseBuffer(0x07) / 10.0;
    float pf = node.getResponseBuffer(0x08) / 100.0;

    Serial.printf("V: %.1f | A: %.3f | W: %.1f | Freq: %.1f | PF: %.2f\n", 
                  voltage, current, power, frequency, pf);
  } else {
    // Exact error string output for debugging
    Serial.printf("Error: Modbus poll failed with code 0x%02X\n", result);
  }

  delay(1000); // Poll every 1 second
}

Debugging: "0xE2" Timeouts and Bus Failures

When RS485 fails, the ModbusMaster library returns specific hex codes. The most common failure in this build is Error: Modbus poll failed with code 0xE2. This is the exact string for Response Timeout (ku8MBResponseTimedOut). It means the ESP32 transmitted the request, but the PZEM never replied.

The First Three Things to Check When It Fails:

  1. RE/DE Pin Logic State: The MAX485 requires the DE pin to go HIGH before transmission and LOW immediately after. If your postTransmission() callback is missing or delayed, the bus stays in transmit mode, and the ESP32 cannot read the PZEM's reply. Verify GPIO 4 with an oscilloscope or logic analyzer.
  2. RS485 A/B Polarity Swap: RS485 is differential. If the PZEM manual labels the terminals A and B, but the manufacturer swapped the physical traces (a common issue with cheap MAX485 modules), swap the A and B wires at the terminal block. This fixes 80% of "dead bus" issues.
  3. Baud Rate Mismatch: The PZEM-004T v3 defaults to 9600 baud. If you previously used a tool to change the PZEM's baud rate to 115200, the ESP32 Serial2.begin(9600) line will result in a 0xE2 timeout or a 0xE0 (Illegal Function/CRC fail). Hard-reset the PZEM by cycling power while holding the reset button (if equipped) or use the manufacturer's Windows GUI tool to verify the baud rate.
💡 Bench Tip: If you see 0xE1 (Invalid CRC), your wiring is correct, but electromagnetic interference (EMI) from the AC contactors in your panel is corrupting the serial bytes. Move the MAX485 module further away from the AC bus bars, or use a twisted-pair shielded cable (Cat5e works perfectly) for the RS485 A/B lines.

Extending or Simplifying the Build

Depending on your project constraints, you can scale this architecture up or down.

How to Simplify (The TTL Route)

If you are mounting the ESP32 outside the metal breaker panel in a clean environment, you can drop the MAX485 module entirely. Purchase the TTL version of the PZEM-004T v3.0. However, because the PZEM TTL TX line outputs 5V and the ESP32 RX pin is strictly 3.3V tolerant, you must place a bi-directional logic level shifter (like the BSS138-based Adafruit 4-channel shifter) between the PZEM TX and ESP32 GPIO 16. Feeding 5V directly into an ESP32-WROOM-32 GPIO will permanently brick the silicon.

How to Extend (MQTT and Split-Phase)

To push this data to Home Assistant, integrate the PubSubClient library to publish the parsed JSON payload to an MQTT broker over WiFi. For North American split-phase 240V circuits (like EV chargers or dryers), you must use two PZEM-004T modules. Assign them unique Modbus addresses (e.g., 0x01 and 0x02) using the manufacturer's software, daisy-chain their RS485 A/B lines, and poll them sequentially in your loop() with a 50ms delay between requests to prevent bus collisions.

By utilizing isolated RS485 and strictly adhering to NEC wire-sizing limits for panel taps, this monitor provides commercial-grade telemetry for a fraction of the cost of off-the-shelf smart breakers.