Siddhant Kumar
Project 005 · Smart Home

Smart Plug Energy Monitor.

A mains socket that measures true power — not the fiction you get from multiplying RMS voltage by RMS current — logs kilowatt-hours per appliance, and switches the load on a schedule or from your phone.

Advanced 10–15 hours 42 min read EnergyWiFiApp
Jump to source Bill of materials
Smart Plug Energy Monitor — reference build illustration MCU VCC · GND · SIG · NC
Difficulty
Advanced
Build time
10–15 hours
Indicative cost
₹3,800 – ₹4,900
Platform
ESP32 DevKit V1 (ESP-WROOM-32)
Category
Smart Home
Last updated
28 July 2026
Contents — 26 sections

Project Overview

A mains socket that measures true power — not the fiction you get from multiplying RMS voltage by RMS current — logs kilowatt-hours per appliance, and switches the load on a schedule or from your phone.

Most "energy monitor" projects measure current with an ACS712 or a clamp, assume the mains is a clean 230 V sine, multiply, and report a number. For a resistive load such as a heater that number is roughly right. For anything with a switch-mode supply — a laptop charger, an LED bulb, a fridge — it can be wrong by 40 % or more, because the current is not in phase with the voltage and is not sinusoidal.

This build uses a PZEM-004T v3, which samples voltage and current simultaneously and integrates their product over each cycle. That is the definition of real power, and it is the only measurement that corresponds to what your electricity meter bills you for. The module also reports apparent power, power factor and frequency, so you can see exactly how badly the naive calculation would have failed for each appliance.

The safety architecture matters more here than in any other project in this catalogue, because the measurement side sits at mains potential. The PZEM's UART is opto-isolated, and that isolation boundary is the only thing between 230 V and your ESP32 — and, through the USB cable, your laptop. Respect it: never bridge the grounds, never probe the mains side with an oscilloscope that is earthed, and never work on this powered.

The result is per-appliance data you can actually act on. Standby draw is usually the surprise: a television that reads 0.4 W on paper often measures 8 W in reality, which is 70 kWh a year doing nothing.

An ESP32 development board with the ESP-WROOM-32 module and USB connector
An ESP32 development board — the metering controller, kept strictly on the isolated side of the design. Photograph sourced from Wikimedia Commons — ESP32 Espressif ESP-WROOM-32 Dev Board.jpg. Reused under the licence stated on that page; please check it before republishing.

What this project does

  • Measures true RMS voltage, current, real power, apparent power, power factor, frequency and cumulative energy.
  • Switches the load through a 16 A relay with a schedule, a phone command, or a standby-power auto-off rule.
  • Publishes a full metering payload over MQTT once per second and integrates kilowatt-hours locally.
  • Detects appliance state — off, standby, active — from the power signature and reports it as a category.
  • Estimates running cost from a configurable tariff, including a two-tier slab structure.
  • Retains the energy counter through power cuts by writing to NVS.
  • Alerts on over-current, over-voltage and unexpected standby draw.

Real-World Applications

SettingHow it is used
Appliance auditingFind out what is actually consuming your electricity. The results are usually not what people expect.
Standby eliminationAutomatically cut power when a device drops below its standby threshold for ten minutes.
Solar self-consumptionSchedule high-draw appliances into the hours when your panels are producing.
Rental and shared-space billingPer-socket kilowatt-hour records give a defensible basis for splitting a bill.
Fault detectionA motor whose power draw creeps up over months is a bearing failing. This catches it before it fails.
Server and lab equipmentPower-cycle a hung device remotely, with a record of how much it drew before it hung.

Deployment contexts where a build of this kind earns its keep.

Features & Capabilities

  • True power measurement via the PZEM-004T v3, not a current-only approximation.
  • Opto-isolated Modbus-RTU link at 9600 baud — the one safety boundary in the design.
  • 16 A relay with a snubber network across the contacts to suppress arcing on inductive loads.
  • Non-volatile energy accumulation that survives power loss without wearing out flash.
  • Appliance state classification from power thresholds with hysteresis.
  • Tariff-aware cost estimation with slab rates.
  • Home Assistant discovery for switch, power, energy, voltage, current and power-factor entities.
  • Over-current trip in firmware that opens the relay well before the fuse would blow.

Difficulty, Time & Required Skills

AttributeValue
Difficulty levelAdvanced
Estimated completion time10–15 hours
Indicative build cost₹3,800 – ₹4,900
Primary disciplineSmart Home
Reference platformESP32 DevKit V1 (ESP-WROOM-32)

Skills you should have (or will pick up)

  • Mains wiring competence and genuine respect for it — this is not a beginner project
  • Understanding of real, apparent and reactive power and power factor
  • UART and the basics of Modbus-RTU register reads
  • Arduino C++ with non-blocking timing
  • MQTT and Home Assistant discovery
  • Enclosure and creepage/clearance basics

Bill of Materials

Every part below is commonly available from Indian and international hobby-electronics suppliers. Prices are indicative 2026 retail figures in Indian rupees and will drift — treat them as a budgeting guide, not a quotation.

ComponentKey specificationQtyApprox. cost
ESP32 DevKit V1 (ESP-WROOM-32)
Wi-Fi transmit bursts peak near 500 mA — size the regulator accordingly.
Dual-core Xtensa LX6 @ 240 MHz, 520 KB SRAM, 4 MB flash, Wi-Fi 802.11 b/g/n + BLE 4.2, 34 GPIO, 18× 12-bit ADC, 2× 8-bit DAC1₹450
PZEM-004T v3 AC energy meter (100 A CT)
The measurement side sits at mains potential — the opto-isolated UART is the only safe boundary.
80–260 VAC, 0–100 A, voltage/current/power/energy/PF/frequency, ±0.5 %1₹950
5 V single-channel opto-isolated relay module
Cut the JD-VCC jumper and feed the coil separately for true isolation.
SPDT contacts rated 10 A @ 250 VAC / 10 A @ 30 VDC, opto-isolated input1₹90
0.96″ SSD1306 OLED display
Static images burn in — invert or scroll the screen periodically.
128 × 64 monochrome, 1.3–3.3 V logic, 100 kHz–400 kHz I²C1₹250
LM2596 adjustable buck converter module
Set the output voltage with no load connected before wiring the board.
4.5–40 V in, 1.25–37 V out, 2 A (3 A peak), ~92 % efficiency1₹90
Double-sided perfboard 7 × 9 cm + headers
Solder female headers so the MCU can be swapped without desoldering.
FR-4, 0.1″ pitch, plated through-holes, 24 × 18 grid1₹60
IP65 ABS junction enclosure 158 × 90 × 60 mm
Fit cable glands, not drilled holes, or the IP rating means nothing.
IP65, ABS, −20 to +80 °C, transparent lid, wall-mount lugs1₹260
IEC/mains inlet + socket outlet pair16 A rated, screw terminals1₹320
5 V 1 A isolated AC-DC module (HLK-PM01)
Replaces the buck converter and a separate adapter — one mains feed powers everything.
100–264 VAC in, 5 V 0.6 A out, 3 kV isolation1₹380
Snubber network (100 nF X2 + 100 Ω 2 W)
Suppresses contact arcing on inductive loads; roughly triples relay life.
RC across the relay contacts1₹60
MOV (275 V varistor) + 16 A fuse and holderS14K275 varistor, ceramic fuse1₹120
Cable glands and 1.5 mm² mains cablePG9 glands, 3-core1₹220

Estimated total: ₹3,250, excluding tools, shipping and consumables.

Tools and consumables

  • Soldering iron (temperature controlled, 350 °C) with 0.8 mm 60/40 or lead-free solder
  • Digital multimeter — continuity, DC volts and current ranges
  • Wire strippers, flush cutters and a small set of precision screwdrivers
  • Heat-shrink tubing and a heat gun (or a lighter, carefully)
  • A laptop with a USB port and the toolchain listed above

Hardware Specifications

PartSpecificationSupplyInterfaceReference
ESP32 DevKit V1 (ESP-WROOM-32)Dual-core Xtensa LX6 @ 240 MHz, 520 KB SRAM, 4 MB flash, Wi-Fi 802.11 b/g/n + BLE 4.2, 34 GPIO, 18× 12-bit ADC, 2× 8-bit DAC3.3 V logic / 5 V USBUART, SPI, I²C, I²S, CAN, PWMDatasheet
PZEM-004T v3 AC energy meter (100 A CT)80–260 VAC, 0–100 A, voltage/current/power/energy/PF/frequency, ±0.5 %5 V logic sideUART Modbus-RTU 9600Datasheet
5 V single-channel opto-isolated relay moduleSPDT contacts rated 10 A @ 250 VAC / 10 A @ 30 VDC, opto-isolated input5 V coilDigital (active-low)Datasheet
0.96″ SSD1306 OLED display128 × 64 monochrome, 1.3–3.3 V logic, 100 kHz–400 kHz I²C3.3–5 VI²C (0x3C)Datasheet
LM2596 adjustable buck converter module4.5–40 V in, 1.25–37 V out, 2 A (3 A peak), ~92 % efficiency4.5–40 VScrew terminals + trimmerDatasheet
Double-sided perfboard 7 × 9 cm + headersFR-4, 0.1″ pitch, plated through-holes, 24 × 18 gridDatasheet
IP65 ABS junction enclosure 158 × 90 × 60 mmIP65, ABS, −20 to +80 °C, transparent lid, wall-mount lugsDatasheet

Consolidated electrical and interface specifications for every active part in the build.

Power Budget & Supply Sizing

Add up the typical active current of every part, then size the supply with at least 50 % headroom so transmit bursts and motor inrush never brown out the controller.

LoadSupply railTypical current (mA)Notes
ESP32 DevKit V1 (ESP-WROOM-32)3.3 V logic / 5 V USB160Wi-Fi transmit bursts peak near 500 mA — size the regulator accordingly.
PZEM-004T v3 AC energy meter (100 A CT)5 V logic side20The measurement side sits at mains potential — the opto-isolated UART is the only safe boundary.
5 V single-channel opto-isolated relay module5 V coil70Cut the JD-VCC jumper and feed the coil separately for true isolation.
0.96″ SSD1306 OLED display3.3–5 V20Static images burn in — invert or scroll the screen periodically.
LM2596 adjustable buck converter module4.5–40 V8Set the output voltage with no load connected before wiring the board.

Summed typical draw is 278 mA. With a 1.5× design margin the supply should deliver at least 500 mA continuously at the stated rail voltage.

Software Requirements & Development Environment

Reference toolchain: Arduino IDE 2.3.x with the ESP32 board package 3.x (or PlatformIO on VS Code). Anything newer normally works; anything older may lack the board definitions used here.

  • Install the Arduino IDE 2.3.x (or PlatformIO if you prefer a real editor and dependency locking).
  • Add https://espressif.github.io/arduino-esp32/package_esp32_index.json under File → Preferences → Additional Board Manager URLs, then install esp32 from the Boards Manager.
  • Set the correct port under Tools → Port. On Linux add yourself to the dialout group: sudo usermod -aG dialout $USER and log out and back in.
  • Open the Serial Monitor at 115200 baud — every sketch here logs its state there.
  • Keep File → Preferences → Show verbose output during: compilation switched on while you are debugging build errors.

Required libraries

LibraryWhy it is neededInstall
WiFi (ESP32 core) bundledStation/AP connection management for the ESP32.Bundled with the ESP32 Arduino core
PubSubClient 2.8Lightweight MQTT 3.1.1 client for constrained devices.Library Manager → "PubSubClient" by Nick O'Leary
ArduinoJson 7.xZero-allocation JSON serialisation and parsing.Library Manager → "ArduinoJson" by Benoit Blanchon
ModbusMaster 2.0.1Modbus-RTU master framing for RS-485 meters and drives.Library Manager → "ModbusMaster" by Doc Walker
Adafruit SSD1306 + GFX 2.5.xFramebuffer and text/graphics primitives for the OLED.Library Manager → "Adafruit SSD1306"
Preferences (NVS) bundledWear-levelled key/value storage in ESP32 flash for settings.Bundled with the ESP32 core

Block Diagram

The block diagram shows the functional decomposition of the system — what senses, what decides, what acts, and where the data ends up.

Smart Plug Energy Monitor — system block diagramFunctional block diagram of the Smart Plug Energy Monitor system. MainsInlet + fuse + MOV16 A protectionCurrent transformerlive conductor onlyMeteringPZEM-004T v3true power, PFOpto-isolated UARTsafety boundaryControlESP32integrate kWhRules engineschedule, standbyOutput16 A relayswitches liveMQTT + OLEDtelemetryV and IModbusdecisions
Smart Plug Energy Monitor — system block diagram

Circuit Diagram & Wiring

Every signal line in the build is shown below, followed by a pin-by-pin connection table you can work through with a multimeter in hand.

Smart Plug Energy Monitor — wiring schematicConnection schematic showing which controller pin drives each peripheral. Sensors / InputsControllerActuators / OutputsESP32 DevKit V1(ESP-WROOM-32)3.3 V logic / 5 V USBPZEM-004T v3GPIO 16 (RX2)Modbus-RTU 9600 8N1PZEM-004T v3GPIO 17 (TX2)Through theopto-isolatorPZEM-004T v35 V / GNDLogic side onlySSD1306 OLEDGPIO 21 / 22I²C at 0x3CRelay module → liveconductorGPIO 26Active-lowStatus LEDGPIO 2Through 330 Ω
Smart Plug Energy Monitor — wiring schematic
PeripheralPeripheral pinController pinSignal
PZEM-004T v3TXGPIO 16 (RX2)Modbus-RTU 9600 8N1
PZEM-004T v3RXGPIO 17 (TX2)Through the opto-isolator
PZEM-004T v35V / GND5 V / GNDLogic side only
SSD1306 OLEDSDA / SCLGPIO 21 / 22I²C at 0x3C
Relay module → live conductorINGPIO 26Active-low
Status LEDAnodeGPIO 2Through 330 Ω

Wire one row at a time and tick it off — most "it does not work" reports trace back to a single swapped pair.

Wiring explanation

  • The PZEM current transformer goes around the live conductor only. Passing both live and neutral through the CT gives a reading of zero, because the two currents cancel. This catches almost everyone the first time.
  • The PZEM voltage sense terminals connect across live and neutral upstream of the relay, so voltage is still measured when the load is switched off.
  • The relay switches the live conductor, never the neutral. Switching neutral leaves the appliance live at its terminals when "off", which is exactly the hazard the switch is supposed to remove.
  • Maintain at least 6 mm of creepage between mains tracks and low-voltage tracks on perfboard, and route a slot in the board between the two domains if you can. Do not run a mains track under the ESP32.
  • Fit the MOV between live and neutral upstream of everything, and the fuse in the live line before the MOV. A varistor without an upstream fuse fails as a short circuit and then as a fire.
  • The HLK-PM01 supplies both the ESP32 and the PZEM logic side from mains, so there is only one cable into the box. Its output is isolated from mains, but treat its input terminals as live at all times.
A single-channel relay module with screw terminals
A relay module. In this build the relay switches the live conductor only, with an RC snubber across the contacts. Photograph sourced from Wikimedia Commons — Relay module.jpg. Reused under the licence stated on that page; please check it before republishing.

System Architecture

Read the stack from the bottom up: physical hardware, the firmware that drives it, the transport that moves data off the device, and the software a human actually looks at.

Smart Plug Energy Monitor — architecture stackLayered architecture from hardware to user interface. Hardware layerESP32 DevKit V1 (ESP-WROOM-32) · sensors and actuatorsDriver layerwifi · pubsub · arduinojson · modbusApplication logicsampling loop · filtering · thresholds · state machinePresentation layerlocal display · serial console · logged output
Smart Plug Energy Monitor — architecture stack

Working Principle

Instantaneous power is p(t) = v(t)·i(t). Real power is its average over a whole number of cycles. For a purely resistive load, current follows voltage exactly and that average equals V_rms × I_rms. For everything else it does not, and the ratio between the two is the power factor.

Two separate effects reduce power factor. Displacement is a phase shift between voltage and current, caused by inductance (motors) or capacitance. Distortion is current that is not sinusoidal at all, which is what every switch-mode power supply produces: it draws current only near the voltage peaks, in short high spikes. A laptop charger can have a power factor of 0.55 purely from distortion, with no phase shift whatsoever.

This is why a current-only measurement fails. An ACS712 tells you I_rms. Multiplying by an assumed 230 V gives apparent power in volt-amperes, not watts. For that laptop charger you would report almost twice its real consumption. The PZEM avoids this by digitising both channels and computing the true average of the product — the same thing your utility meter does.

Energy is the time integral of real power: E = ∫P dt, in joules, or in kilowatt-hours if you divide by 3.6 million. The firmware integrates numerically using the trapezoidal rule at 1 Hz, which for a signal whose meaningful changes happen over seconds is more than accurate enough. The PZEM keeps its own energy counter too, and comparing the two is a useful sanity check on your integration.

Appliance state classification exploits the fact that most devices have three distinct power regimes with large gaps between them. A television might draw 0.1 W truly off, 8 W in standby, and 90 W active. Two thresholds with hysteresis separate them robustly, and the classification is far more useful than raw watts when you are looking at a month of data.

The snubber across the relay contacts deserves a note because it is routinely omitted. When a relay opens an inductive load, the collapsing field drives the voltage across the opening contacts high enough to strike an arc, which erodes the contact material. An RC network — typically 100 nF in series with 100 Ω — gives the energy somewhere to go during the microseconds while the gap is still small. It roughly triples contact life on a motor load.

The maths behind it

Real, apparent and reactive power

plainReal, apparent and reactive power
P (real, W)      = (1/T) ∫ v(t)·i(t) dt
S (apparent, VA) = V_rms × I_rms
Q (reactive, var)= √(S² − P²)
PF               = P / S

Example — 65 W laptop charger measured:
  V_rms = 231.4 V
  I_rms = 0.51 A
  S     = 118.0 VA
  P     = 64.8 W        (measured, not derived)
  PF    = 0.55

A current-only monitor would report 118 W — 82 % too high.

Energy integration

plainEnergy integration
E(kWh) = Σ [ (P_n + P_(n−1)) / 2 ] × Δt / 3 600 000

At 1 Hz with P in watts and Δt = 1 s:
  ΔE = P_avg / 3 600 000  kWh per sample

A 100 W load for one hour:
  3600 samples × 100 / 3 600 000 = 0.100 kWh  ✓

Relay and conductor sizing

plainRelay and conductor sizing
Load          = 16 A resistive at 230 V = 3680 W
Relay rating  = 16 A @ 250 VAC  → utilisation 100 %  ✗

Derate: a relay rated 16 A resistive should carry
≤ 10 A continuously in an enclosed box at 40 °C.

Safe continuous load: 10 A → 2300 W
Cable: 1.5 mm² is rated ~16 A in free air,
       ~13 A enclosed — adequate for 10 A.
Fuse: 12 A, so it opens before the relay welds.

Program Flowchart

The firmware is a single cooperative loop. Nothing blocks for long, so networking, sensing and the user interface all stay responsive.

Smart Plug Energy Monitor — firmware flowchartControl flow through the main program loop. Boot: restore kWh counter fromNVSRead PZEM registers overModbus (1 Hz)Read valid?yesretry, flag comms faultIntegrate energy, classifyappliance stateOver-current orover-voltage?trip relay opencontinueApply schedule and standbyrulesPublish metering payload overMQTTPersist kWh every 10 minutes
Smart Plug Energy Monitor — firmware flowchart

Assembly Instructions

Build on a breadboard first and only commit to solder once the whole system has run for an hour without a fault.

Step-by-Step Implementation Guide

Work through these in order. Each step ends in something you can observe, so a failure is always localised to the step you just finished.

  1. Read the PZEM before touching anything else

    Wire only the PZEM and the ESP32, with the PZEM measuring a known load such as a 60 W lamp on a proper extension lead. Confirm the readings are sane before you build anything into an enclosure.

    cpp01-pzem-read.ino
    #include <ModbusMaster.h>
    
    #define PZEM_RX 16     // ESP32 RX2  <- PZEM TX
    #define PZEM_TX 17     // ESP32 TX2  -> PZEM RX
    #define PZEM_ADDR 0xF8 // factory default broadcast address
    
    ModbusMaster pzem;
    
    struct Meter { float v, i, w, wh, hz, pf; bool ok; };
    
    void meterBegin() {
      Serial2.begin(9600, SERIAL_8N1, PZEM_RX, PZEM_TX);
      pzem.begin(PZEM_ADDR, Serial2);
    }
    
    Meter meterRead() {
      Meter m = {0, 0, 0, 0, 0, 0, false};
    
      // Input registers 0x0000-0x0009 hold every measurement in one read.
      uint8_t rc = pzem.readInputRegisters(0x0000, 10);
      if (rc != pzem.ku8MBSuccess) return m;
    
      uint16_t r[10];
      for (int i = 0; i < 10; i++) r[i] = pzem.getResponseBuffer(i);
    
      m.v  =  r[0] / 10.0f;                                  // 0.1 V
      m.i  = ((uint32_t)r[2] << 16 | r[1]) / 1000.0f;        // 0.001 A, 32-bit
      m.w  = ((uint32_t)r[4] << 16 | r[3]) / 10.0f;          // 0.1 W,  32-bit
      m.wh = ((uint32_t)r[6] << 16 | r[5]);                  // 1 Wh,   32-bit
      m.hz =  r[7] / 10.0f;                                  // 0.1 Hz
      m.pf =  r[8] / 100.0f;                                 // 0.01
      m.ok = true;
      return m;
    }
    readInputRegisters(0x0000, 10)One transaction fetches everything. Reading registers individually multiplies the bus traffic by ten and makes 1 Hz sampling marginal at 9600 baud.
    (uint32_t)r[2] << 16 | r[1]Current, power and energy are 32-bit values split across two 16-bit registers, low word first. Getting the word order backwards produces values that look almost plausible, which makes it a nasty bug.
    PZEM_ADDR 0xF8The default broadcast address works with a single meter on the bus. If you put several on one RS-485 segment, each needs a unique address written to holding register 0x0002 first.
    m.ok = false on failureNever treat a failed Modbus read as zero watts. It will corrupt your energy total and, worse, make a running appliance look switched off to the standby rule.
  2. Integrate energy and persist it safely

    Flash has a finite erase-cycle budget. Writing the counter every second would exhaust an NVS partition in months.

    cpp02-energy-accumulate.ino
    #include <Preferences.h>
    Preferences prefs;
    
    double   energyKwh   = 0;
    float    lastWatts   = 0;
    uint32_t lastSample  = 0, lastPersist = 0;
    
    void energyBegin() {
      prefs.begin("meter", false);
      energyKwh = prefs.getDouble("kwh", 0.0);
    }
    
    void energyAccumulate(float watts) {
      uint32_t now = millis();
      if (lastSample) {
        float dt = (now - lastSample) / 1000.0f;             // seconds
        if (dt > 0 && dt < 10) {                             // reject huge gaps
          double avg = (watts + lastWatts) / 2.0;            // trapezoidal
          energyKwh += avg * dt / 3600000.0;
        }
      }
      lastSample = now;
      lastWatts  = watts;
    
      // Persist every 10 minutes, and only if the value actually moved.
      if (now - lastPersist > 600000UL) {
        lastPersist = now;
        double stored = prefs.getDouble("kwh", 0.0);
        if (fabs(energyKwh - stored) > 0.0005) prefs.putDouble("kwh", energyKwh);
      }
    }
    
    float costOf(double kwh, float slab1Rate, float slab1Units, float slab2Rate) {
      if (kwh <= slab1Units) return kwh * slab1Rate;
      return slab1Units * slab1Rate + (kwh - slab1Units) * slab2Rate;
    }
    dt > 0 && dt < 10Guards against both a millis() rollover and a long stall (a blocked Wi-Fi reconnect). Without it a single ten-second hiccup silently adds a fictitious chunk of energy.
    double, not floatA float has about seven significant digits. After a few thousand kilowatt-hours, adding a 0.00003 kWh increment to a float total becomes a no-op — the increment is smaller than the representable step. A double pushes that failure past any realistic lifetime.
    fabs(energyKwh - stored) > 0.0005Skips the flash write when nothing meaningful changed, which matters for a socket that is switched off for weeks.

Complete Source Code

The listing below is complete and compiles as written — there are no elided sections. Read the annotations under each block before you upload it.

cppsmart-plug-monitor.ino
/* ═══════════════════════════════════════════════════════════════
   Smart Plug Energy Monitor — ESP32 + PZEM-004T v3 + 16 A relay

   True-power metering over an opto-isolated Modbus link, local kWh
   integration with NVS persistence, appliance state classification,
   over-current trip, and Home Assistant discovery.

   MAINS VOLTAGE. Build it, close it, and never open it powered.
   ══════════════════════════════════════════════════════════════════ */

#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <ModbusMaster.h>
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <Preferences.h>
#include <math.h>

#define WIFI_SSID "YOUR_WIFI"
#define WIFI_PASS "YOUR_PASSWORD"
#define MQTT_HOST "192.168.1.50"
#define DEVICE_ID "plug-office"

#define PZEM_RX 16
#define PZEM_TX 17
#define PIN_RELAY 26
#define PIN_LED    2

#define TRIP_AMPS      10.0f     // firmware trip, below the 12 A fuse
#define OVERVOLT       260.0f
#define STANDBY_W       15.0f    // below this and above OFF_W = standby
#define OFF_W            1.0f
#define STANDBY_CUT_MS 600000UL  // auto-off after 10 min in standby

#define TARIFF_1   4.50f         // INR per kWh, first slab
#define SLAB_1   100.0f          // units
#define TARIFF_2   6.80f

ModbusMaster     pzem;
Adafruit_SSD1306 oled(128, 64, &Wire, -1);
WiFiClient       net;
PubSubClient     mqtt(net);
Preferences      prefs;

struct Meter { float v, i, w, va, wh, hz, pf; bool ok; };
Meter meter = {};

double   energyKwh = 0;
float    lastWatts = 0;
uint32_t lastSample = 0, lastPersist = 0, standbySince = 0, lastPub = 0;
bool     relayOn = true, tripped = false, autoStandbyCut = true;
const char *applianceState = "off";

/* ── metering ───────────────────────────────────────────────── */
Meter meterRead() {
  Meter m = {};
  if (pzem.readInputRegisters(0x0000, 10) != pzem.ku8MBSuccess) return m;
  uint16_t r[10];
  for (int i = 0; i < 10; i++) r[i] = pzem.getResponseBuffer(i);

  m.v  =  r[0] / 10.0f;
  m.i  = ((uint32_t)r[2] << 16 | r[1]) / 1000.0f;
  m.w  = ((uint32_t)r[4] << 16 | r[3]) / 10.0f;
  m.wh = ((uint32_t)r[6] << 16 | r[5]);
  m.hz =  r[7] / 10.0f;
  m.pf =  r[8] / 100.0f;
  m.va =  m.v * m.i;
  m.ok = true;
  return m;
}

void energyAccumulate(float watts) {
  uint32_t now = millis();
  if (lastSample) {
    float dt = (now - lastSample) / 1000.0f;
    if (dt > 0 && dt < 10) energyKwh += ((watts + lastWatts) / 2.0) * dt / 3600000.0;
  }
  lastSample = now;
  lastWatts = watts;

  if (now - lastPersist > 600000UL) {
    lastPersist = now;
    if (fabs(energyKwh - prefs.getDouble("kwh", 0.0)) > 0.0005)
      prefs.putDouble("kwh", energyKwh);
  }
}

/* ── protection and rules ───────────────────────────────────── */
void setRelay(bool on) {
  relayOn = on;
  digitalWrite(PIN_RELAY, on ? LOW : HIGH);
  digitalWrite(PIN_LED, on);
}

void protectionService() {
  if (!meter.ok) return;
  if (meter.i > TRIP_AMPS || meter.v > OVERVOLT) {
    tripped = true;
    setRelay(false);
    mqtt.publish("home/plug/" DEVICE_ID "/alert",
                 meter.i > TRIP_AMPS ? "overcurrent" : "overvoltage", true);
  }
}

void classifyAndRule() {
  if (!meter.ok) return;

  const char *prev = applianceState;
  if      (meter.w < OFF_W)      applianceState = "off";
  else if (meter.w < STANDBY_W)  applianceState = "standby";
  else                           applianceState = "active";

  if (strcmp(applianceState, "standby") != 0) { standbySince = 0; return; }
  if (prev != applianceState || !standbySince) standbySince = millis();

  if (autoStandbyCut && millis() - standbySince > STANDBY_CUT_MS && relayOn) {
    setRelay(false);
    mqtt.publish("home/plug/" DEVICE_ID "/alert", "standby-auto-off", false);
  }
}

/* ── MQTT ───────────────────────────────────────────────────── */
void publishDiscovery() {
  const char *base = "home/plug/" DEVICE_ID;

  JsonDocument sw;
  sw["name"] = "Office Plug"; sw["unique_id"] = DEVICE_ID "_sw";
  sw["command_topic"] = "home/plug/" DEVICE_ID "/set";
  sw["state_topic"]   = "home/plug/" DEVICE_ID "/state";
  sw["value_template"] = "{{ 'ON' if value_json.relay else 'OFF' }}";
  char b[512]; size_t n = serializeJson(sw, b, sizeof(b));
  mqtt.publish("homeassistant/switch/" DEVICE_ID "/config", (uint8_t *)b, n, true);

  struct { const char *id, *name, *unit, *devcls, *field; } sensors[] = {
    { "power",   "Power",        "W",   "power",       "watts"   },
    { "energy",  "Energy",       "kWh", "energy",      "kwh"     },
    { "voltage", "Voltage",      "V",   "voltage",     "volts"   },
    { "current", "Current",      "A",   "current",     "amps"    },
    { "pf",      "Power Factor", "",    "power_factor","pf"      },
  };
  for (auto &s : sensors) {
    JsonDocument d;
    d["name"] = s.name;
    d["unique_id"] = String(DEVICE_ID) + "_" + s.id;
    d["state_topic"] = String(base) + "/state";
    d["unit_of_measurement"] = s.unit;
    d["device_class"] = s.devcls;
    d["value_template"] = String("{{ value_json.") + s.field + " }}";
    if (!strcmp(s.id, "energy")) d["state_class"] = "total_increasing";
    else                          d["state_class"] = "measurement";
    char buf[512]; size_t k = serializeJson(d, buf, sizeof(buf));
    mqtt.publish((String("homeassistant/sensor/") + DEVICE_ID + "_" + s.id + "/config").c_str(),
                 (uint8_t *)buf, k, true);
  }
}

void publishState() {
  JsonDocument d;
  d["volts"]   = meter.v;
  d["amps"]    = meter.i;
  d["watts"]   = meter.w;
  d["va"]      = roundf(meter.va * 10) / 10.0f;
  d["pf"]      = meter.pf;
  d["hz"]      = meter.hz;
  d["kwh"]     = roundf(energyKwh * 1000) / 1000.0;
  d["cost"]    = roundf((energyKwh <= SLAB_1 ? energyKwh * TARIFF_1
                        : SLAB_1 * TARIFF_1 + (energyKwh - SLAB_1) * TARIFF_2) * 100) / 100.0;
  d["relay"]   = relayOn;
  d["tripped"] = tripped;
  d["state"]   = applianceState;
  d["comms"]   = meter.ok;
  char buf[320]; size_t n = serializeJson(d, buf, sizeof(buf));
  mqtt.publish("home/plug/" DEVICE_ID "/state", (uint8_t *)buf, n, true);
}

void onMessage(char *topic, byte *payload, unsigned int len) {
  char v[16] = {0};
  memcpy(v, payload, len < 15 ? len : 15);
  if (!strcmp(v, "ON"))    { tripped = false; setRelay(true);  }
  if (!strcmp(v, "OFF"))   { setRelay(false); }
  if (!strcmp(v, "RESET")) { energyKwh = 0; prefs.putDouble("kwh", 0.0); }
}

/* ── setup / loop ───────────────────────────────────────────── */
void setup() {
  Serial.begin(115200);
  pinMode(PIN_RELAY, OUTPUT); pinMode(PIN_LED, OUTPUT);
  setRelay(true);

  Serial2.begin(9600, SERIAL_8N1, PZEM_RX, PZEM_TX);
  pzem.begin(0xF8, Serial2);

  Wire.begin(21, 22);
  oled.begin(SSD1306_SWITCHCAPVCC, 0x3C);

  prefs.begin("meter", false);
  energyKwh = prefs.getDouble("kwh", 0.0);

  WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASS);
  for (int i = 0; i < 40 && WiFi.status() != WL_CONNECTED; i++) delay(250);
  mqtt.setServer(MQTT_HOST, 1883);
  mqtt.setCallback(onMessage);
  mqtt.setBufferSize(768);
  Serial.println("Smart plug metering");
}

void loop() {
  if (!mqtt.connected() && WiFi.status() == WL_CONNECTED) {
    if (mqtt.connect(DEVICE_ID, NULL, NULL,
                     "home/plug/" DEVICE_ID "/status", 0, true, "offline")) {
      mqtt.publish("home/plug/" DEVICE_ID "/status", "online", true);
      mqtt.subscribe("home/plug/" DEVICE_ID "/set");
      publishDiscovery();
    }
  }
  mqtt.loop();

  static uint32_t lastRead = 0;
  if (millis() - lastRead >= 1000) {
    lastRead = millis();
    meter = meterRead();
    if (meter.ok) {
      energyAccumulate(meter.w);
      protectionService();
      classifyAndRule();
    }

    oled.clearDisplay();
    oled.setTextColor(SSD1306_WHITE);
    oled.setTextSize(2); oled.setCursor(0, 2);
    oled.printf("%.0f W", meter.w);
    oled.setTextSize(1);
    oled.setCursor(0, 26); oled.printf("%.1f V  %.3f A  pf %.2f", meter.v, meter.i, meter.pf);
    oled.setCursor(0, 38); oled.printf("%.3f kWh", energyKwh);
    oled.setCursor(0, 50); oled.printf("%s  %s%s", applianceState,
                                       relayOn ? "ON" : "OFF", tripped ? " TRIP" : "");
    oled.display();
  }

  if (millis() - lastPub > 1000) { lastPub = millis(); publishState(); }
}
state_class = "total_increasing"This tells Home Assistant the energy value only ever goes up, so its statistics engine can compute daily and monthly consumption correctly and handle the reset when you zero the counter.
tripped flag cleared only by ONAn over-current trip is latched. It must not clear itself when the current falls, because a fault that trips and re-closes repeatedly is far more dangerous than one that stays open.
meter.va = m.v * m.iApparent power is computed rather than read, so you can display it next to real power. Seeing 118 VA and 65 W on the same screen makes the power-factor concept concrete in a way no explanation does.
d["comms"] = meter.okPublishing the communication health explicitly means a dashboard can distinguish "the appliance is off" from "the meter stopped talking" — two situations that otherwise both look like zero watts.
LWT on the status topicCombined with the retained state topic, this makes a dead plug visibly dead rather than a plug reporting stale figures forever.

Configuration & Calibration

Electronics: PCB, Ratings & Pin Detail

PCB information

  • Keep mains and low-voltage on physically separate regions of the board with a routed slot between them if you have the tooling — the slot raises the effective creepage distance far beyond what the surface alone gives.
  • Minimum 6 mm creepage and 3 mm clearance between mains and SELV circuits for 230 V basic insulation in a pollution-degree-2 environment. More is better and costs nothing on a hand-built board.
  • Mains traces carrying 10 A need generous copper. At 1 oz copper, a 10 A trace wants roughly 5 mm width for a 10 °C rise — most people undersize this by a factor of three.
  • Put the fuse first in the live path, then the MOV, then everything else. A MOV upstream of a fuse is a fire waiting for a surge.
  • Use screw terminals rated for the conductor size, torqued properly. A loose mains terminal is the single most common cause of fires in DIY electrical work.

Power calculations

Trace width for 10 A

plainTrace width for 10 A
IPC-2221 external layer, 10 °C rise, 1 oz copper:
  A (mils²) = (I / (k · ΔT^0.44))^(1/0.725),  k = 0.048
  A = (10 / (0.048 × 10^0.44))^(1/0.725)
    = (10 / 0.1325)^1.379 ≈ 528 mils²
  width = 528 / 1.378 (mils thickness) ≈ 383 mils ≈ 9.7 mm

Use 10 mm of copper, or reinforce the track with
solder or a soldered bus wire.

Relay contact derating

plainRelay contact derating
Rating       : 16 A @ 250 VAC resistive
Enclosed box : derate 40 %  → 9.6 A
Inductive    : derate a further 30 % → 6.7 A

Continuous design load: 6 A (1380 W) inductive,
                        9 A (2070 W) resistive.
Fuse at 12 A so the fuse opens before the contacts weld.

Snubber sizing

plainSnubber sizing
Rule of thumb for a 230 VAC contact snubber:
  C = I_load / 10   (µF, I in amperes)
  R = V_load / (10 × I_load)   (Ω)

For a 2 A inductive load:
  C = 0.2 µF  →  use 100–220 nF X2-rated
  R = 230 / 20 = 11.5 Ω → use 100 Ω, 2 W

The capacitor MUST be X2 safety rated. A general
purpose film capacitor across mains can fail short.
ParameterRatingDesign margin
Mains voltage230 V ±10 % (207–253 V)MOV clamps at 275 V; firmware trips at 260 V.
Continuous current10 A design, 16 A relay rating38 % below rating — accounts for enclosure temperature rise.
Fuse12 A ceramic, HRCAbove the 10 A design load, below the relay welding threshold.
Creepage, mains to SELV≥ 6 mmBasic insulation, pollution degree 2, per IEC 60664-1.
PZEM CT range0–100 AVastly over-specified for a 10 A socket, which keeps it in its linear region.
HLK-PM01 supply5 V, 600 mA, 3 kV isolationESP32 plus PZEM logic draws about 200 mA — a third of capacity.

Never operate a part beyond about 70 % of its absolute maximum rating.

Pin diagram

PinFunctionNote
PZEM TXModbus response to ESP32 RX2 (GPIO 16)Opto-isolated on the module
PZEM RXModbus request from ESP32 TX2 (GPIO 17)Opto-isolated
PZEM CTSplit-core current transformerAround the LIVE conductor only
PZEM V+/V−Voltage senseUpstream of the relay
Relay COM/NOSwitches the live conductorNever switch neutral

Testing Procedure & Expected Output

Test from the bottom up. Confirm power, then each sensor in isolation, then the integrated loop — the first failing step tells you exactly where to look.

TestWhat you should see
Read the meter with no loadVoltage 220–250 V, frequency 49.8–50.2 Hz, current under 0.02 A, power under 1 W.
Plug in a 60 W incandescent lampAbout 60 W with a power factor above 0.98 — a resistive load, so real and apparent power nearly match.
Plug in a laptop chargerPower factor between 0.5 and 0.7, with apparent power well above real power. This is the demonstration that makes the whole project worthwhile.
Run a known load for exactly one hourIntegrated kWh within about 1 % of load watts ÷ 1000, and matching the PZEM's own counter.
Publish OFF over MQTTThe relay clicks, power drops to zero, and voltage continues to be reported (it is sensed upstream).
Leave a television in standby for eleven minutesThe standby rule fires and the plug switches itself off, with an alert published.
Power-cycle the plugThe kWh counter resumes within 0.001 kWh of its previous value.
Disconnect the PZEM UARTcomms goes false and the display shows stale values — it must not report zero watts.

Bench-test checklist. If a row fails, stop and fix it before moving on.

Expected output

With everything wired and the firmware uploaded, the Serial Monitor at 115200 baud should look similar to the trace below. Values will differ; the shape of the output should not.

A Grafana time-series dashboard
A dashboard of the kind used to chart per-appliance power and cumulative energy. Photograph sourced from Wikimedia Commons — Grafana dashboard.png. Reused under the licence stated on that page; please check it before republishing.

Troubleshooting: Common Errors & Fixes

Current always reads zero but voltage is correct

Likely cause. The current transformer is around both conductors, or around neither.

Fix. The CT must enclose the live conductor only. With both live and neutral inside, the magnetic fields cancel exactly and you measure nothing. Open the split core, pass only the live wire through, and click it shut.

Modbus reads always fail

Likely cause. TX and RX swapped, wrong baud rate, or the wrong UART pins.

Fix. PZEM TX goes to ESP32 RX (GPIO 16) and PZEM RX to ESP32 TX (GPIO 17) — crossed, not straight. Baud is fixed at 9600 8N1. Verify with a USB-serial adapter first if you have one.

Power readings are wildly wrong or negative

Likely cause. The 32-bit registers are being assembled with the words in the wrong order.

Fix. The PZEM sends the low word first: (uint32_t)r[n+1] << 16 | r[n]. Swapping this gives plausible-looking nonsense, which is why it takes so long to spot.

The ESP32 resets when the relay switches

Likely cause. Contact arcing is coupling into the low-voltage supply, or the relay coil transient is reaching the regulator.

Fix. Fit the RC snubber across the contacts, add a 470 µF capacitor at the ESP32 5 V input, and increase the physical separation between the relay and the microcontroller. If it persists, drive the relay from a genuinely separate isolated supply.

The energy total resets after a power cut

Likely cause. NVS was written too rarely, or not at all before the cut.

Fix. The ten-minute persist interval means you can lose up to ten minutes of accumulation, which is normally acceptable. If it matters, add a supercapacitor across the 5 V rail and write on brown-out detection.

Home Assistant shows the entities but the energy dashboard rejects them

Likely cause. Missing state_class or device_class in the discovery payload.

Fix. Energy entities need device_class: energy, state_class: total_increasing and unit_of_measurement: kWh. All three are required before Home Assistant will accept the sensor into its energy dashboard.

The sketch will not upload — "Failed to connect" or "avrdude: stk500_recv()"

Likely cause. The bootloader is not being reached: wrong port, wrong board, a serial monitor holding the port open, or a USB cable that only carries power.

Fix. Close every serial monitor, confirm Tools → Board and Port, and swap to a known data-capable USB cable. On an ESP32 hold BOOT while the IDE prints "Connecting…", then release. If a peripheral is wired to the UART pins (GPIO 1/3 on ESP32, D0/D1 on Uno) unplug it — it fights the programmer.

The board resets in a loop, or the serial monitor prints "Brownout detector was triggered"

Likely cause. The supply cannot deliver peak current. Wi-Fi transmit bursts, relay coils and servos all pull far more than their average draw.

Fix. Power peripherals from a separate regulated supply with a common ground rather than from the board 5 V pin. Add a 470–1000 µF electrolytic capacitor across the supply near the load, and use a real power adapter rather than a laptop USB port.

Serial monitor shows garbage characters

Likely cause. Baud rate mismatch between Serial.begin() and the monitor, or a floating/shared UART line.

Fix. Set the monitor to 115200 to match the sketch. If it still garbles, the crystal or the USB bridge is being confused by noise — shorten the cable and keep motor wiring away from the USB lead.

An I²C device is not detected

Likely cause. Wrong address, missing pull-ups, swapped SDA/SCL, or a bus too long for the pull-up value.

Fix. Run an I²C scanner sketch first — it should print the device address. Most breakout boards include 4.7 kΩ pull-ups, but if you have chained four of them the parallel resistance is too low; remove the pull-ups from all but one board. Keep the bus under 30 cm at 100 kHz.

Performance Optimisation

  • Read all ten registers in one Modbus transaction. Ten separate reads at 9600 baud take about 300 ms and make 1 Hz sampling unreliable.
  • Publish at 1 Hz but persist at 1/600 Hz. Matching those rates is what wears out flash.
  • Use a double for the energy accumulator. Float precision loss after a few thousand kilowatt-hours is a real, silent failure.
  • Replace every delay() with a millis() comparison — blocking delays are the single most common cause of dropped readings.
  • Sample sensors on a fixed cadence and publish on a slower one; you almost never need to transmit at the sampling rate.
  • Move networking into its own FreeRTOS task so a slow DNS lookup cannot stall the control loop.
  • Use uint8_t / uint16_t where the range allows; on an 8-bit AVR a 32-bit add costs four times as much.
  • For battery builds use deep sleep between samples: an ESP32 drops from ~160 mA awake to about 10 µA asleep, which is the difference between days and months of runtime.
  • Profile before optimising — print micros() deltas around each stage and fix the slowest one first.

Safety Precautions

  • This project handles mains voltage. If you are not competent and confident with 230 V wiring, build the metering half only and use a certified smart plug for switching.
  • Never open the enclosure while it is plugged in. Never probe the mains side with an earthed oscilloscope — you will short live to earth through the probe ground.
  • Fit the fuse in the live conductor, upstream of everything including the MOV.
  • Use only an X2-rated capacitor in the snubber. A general-purpose film capacitor across mains can fail short circuit.
  • The finished unit must be fully enclosed with cable glands and no accessible conductive parts. A test build on a bench with exposed terminals is a genuine electrocution hazard, not a theoretical one.
  • Have the finished assembly checked by a qualified electrician before it carries a real load unattended.
  • Mains voltage kills. Anything on the load side of the relay is at 230 V. Do not work on a powered circuit, and never leave exposed mains wiring on a bench where someone could touch it.
  • Keep at least 6 mm of creepage between the mains and low-voltage sides of any board you make, and never route mains tracks under the microcontroller.
  • Have a qualified electrician do the final installation into a consumer unit or wall fitting. In most jurisdictions this is a legal requirement, not a suggestion.
  • Fit an RCD/RCBO upstream and fuse the load appropriately for its rating.
  • Wear eye protection when soldering or cutting, and solder in a ventilated space — rosin flux fumes are a respiratory irritant.
  • Power the circuit through a bench supply with a current limit while you are testing. A 300 mA limit turns a wiring mistake into a beep instead of a dead board.
  • Disconnect power before changing any wiring. Hot-plugging a sensor onto a live bus is the fastest way to lose a controller.

Maintenance

  • Re-check every screw terminal and header after the first week — thermal cycling loosens connections that felt tight on day one.
  • Recalibrate at the interval given in the calibration section, and keep the constants in a text file next to the firmware — not only in flash.
  • Keep a short logbook of firmware versions and what changed. Six months later you will not remember why that constant is 1.083.

Future Improvements & Upgrades

A working v1 is a platform, not a finish line. These are the upgrades that add the most capability for the least rework.

  • Add harmonic analysis with a dedicated energy IC such as the ATM90E32AS, which reports individual harmonics rather than just power factor.
  • Add appliance disaggregation — a small classifier on the power signature can identify which appliance is plugged in from its startup transient.
  • Add a zero-cross detector and switch the relay at the voltage zero crossing, which nearly eliminates contact arcing.
  • Replace the relay with a triac and opto-isolated driver for silent, unlimited-cycle switching (accepting the leakage current and heatsinking that brings).
  • Add local storage so a month of per-minute data survives a broker outage.
  • Design a proper PCB. Once the breadboard version has run for a month, moving to a two-layer board removes the intermittent-contact failures that dominate prototype faults.
  • Add connectivity — an ESP32 and an MQTT publish turn a local gadget into something you can graph, alert on and analyse over months.
  • Add persistent local storage (microSD or the on-chip flash) so a network outage does not create a hole in your data.
  • Move configuration out of the source: a captive-portal setup page or a JSON config file makes the build reusable without a recompile.
  • Add a battery and solar option so the unit survives a power cut and can be sited away from a socket.
  • Write a small test harness that feeds synthetic sensor values through the decision logic, so you can validate thresholds without physically triggering the event.

Frequently Asked Questions

Why not just use an ACS712 or a current clamp?

Because current alone is not power. For a resistive load the approximation holds; for anything with a switch-mode supply it can be wrong by 80 %. The PZEM costs about ₹950 and measures the same quantity your utility meter does. If you only want to know when a motor is running, a clamp is fine — if you want watts, it is not.

Is the PZEM accurate enough to be trusted?

It is specified at ±0.5 % for voltage, current and power, which is comparable to a class-1 utility meter. It is not legally traceable and you cannot bill from it, but for auditing your own appliances it is far more accurate than anything else at the price.

Can I measure a whole house with this?

Yes — the PZEM-004T comes in a 100 A version with a larger split-core CT that clamps around the incoming main. The firmware is unchanged. The relay obviously is not, and you should never put a hobby relay in a main feed.

Why does my LED bulb show such a bad power factor?

Because its driver is a switch-mode supply that draws current in short pulses near the voltage peaks. That is distortion, not phase shift, and it is completely normal. It costs you nothing directly — domestic customers are billed on real power, not apparent — but it does load the grid disproportionately, which is why commercial tariffs include power-factor penalties.

How dangerous is this really?

Genuinely dangerous if done carelessly and entirely manageable if done properly. The specific risks are: touching a live terminal on an open unit; an under-rated conductor or loose terminal causing a fire; and bridging the isolation barrier so mains reaches your USB-connected laptop. All three are avoided by procedure rather than skill — never open it powered, torque the terminals, and never connect USB while it is plugged into mains.

References & Learning Resources

These are the primary sources worth reading in full. Manufacturer datasheets always outrank forum posts when the two disagree.

  1. PZEM-004T v3.0 — user manual and Modbus register mapPeacefair / Innovators Guru
  2. Modbus Application Protocol Specification V1.1b3Modbus Organization
  3. IEC 60664-1 — insulation coordination, creepage and clearanceIEC
  4. IPC-2221 generic standard on printed board design — trace current capacityIPC
  5. Real, reactive and apparent power — AC power theoryWikipedia
  6. Home Assistant MQTT Sensor — device_class and state_class referenceHome Assistant
  7. Relay contact protection and snubber designTE Connectivity application note