Built-in project · Arduino Uno R3

Home energy monitor with EmonLib on an Arduino Uno

A ZMPT101B and an ACS712 feed EmonLib on an Uno: volts, amps, real power and power factor on an I2C LCD, with the watt hours used adding up. The whole build, an Arduino Uno and 6 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

Teaching? Assign it to your class in one click.

Intermediate About 30 minutes to build and run. Runs in your browser. Free, and no account needed.

The parts retail for about $36. On Mokxi they're free.

The circuit runs here once the page has loaded.

What you'll learn

  • Measuring AC voltage and current with sensor modules
  • Real power versus apparent power, and power factor
  • Calibrating with VOLT_CAL, CURRENT_CAL and PHASE_CAL
  • Adding up energy in watt hours over time
  • Showing readings on an I2C character LCD
  • Difficulty: Intermediate
  • Time: About 30 minutes
  • Board: Arduino Uno R3
  • Parts: 8

How it works

A ZMPT101B senses the mains voltage on A0, and an ACS712 senses the current on A1. The EmonLib library samples both over 20 zero crossings, about 10 mains cycles. It works out volts, amps, real power and power factor from them.

In the simulator, the signal generator and the 3.5 Ω resistor stand in for an appliance, and the switch plugs it in or out.

The sketch adds up watt hours by multiplying power by the time since the last reading. It shows volts, amps, power factor, watts and watt hours on the I2C LCD, and prints the same values to the Serial Monitor.

Parts list

8 parts, plus the jumper wires and a bench instrument. The parts retail for about $36; on Mokxi they're free, and so are the instruments.

Wiring table

Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.

Part and pinConnects to
Character LCD with I2C backpack pin SCLArduino Uno R3 pin SCL
Character LCD with I2C backpack pin SDAArduino Uno R3 pin SDA
Arduino Uno R3 pin GNDGround
AC voltage sensor, ZMPT101B pin VCCArduino Uno R3 pin 5V
AC voltage sensor, ZMPT101B pin GNDArduino Uno R3 pin GND
AC voltage sensor, ZMPT101B pin OUTArduino Uno R3 pin A0
Current sensor, ACS712 pin OUTArduino Uno R3 pin A1
Slide switch pin 1Current sensor, ACS712 pin IP+
Resistor, 3.5 Ω pin 1Current sensor, ACS712 pin IP-
Current sensor, ACS712 pin VCC5 V
Current sensor, ACS712 pin GNDGround
Slide switch pin 2Signal generator pin OUT
Signal generator pin GNDGround
Resistor, 3.5 Ω pin 2Ground
Character LCD with I2C backpack pin GNDGround
Character LCD with I2C backpack pin VCC5 V

The note on the canvas

Word for word, as it sits beside the circuit in the editor.

Try it
1. Press Run. The LCD reads about 230 V, 1.39 A, PF 1.0 and 320 W, and the watt hours start to climb.
2. The generator and the 3.5 ohm resistor stand in for the appliance: about 1.4 A rms, what a 320 W heater draws on 230 V.
3. Flip the switch to unplug it: amps, watts and PF drop to 0 and the watt hours stop.
4. Drag the mains slider on the ZMPT101B to 120 V: the volts follow and the watts halve, since the same current flows at half the voltage.

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · Arduino Uno R3
// Home energy monitor: OpenEnergyMonitor's EmonLib reads the mains voltage
// from a ZMPT101B and the current from an ACS712, and an I2C LCD shows the
// volts, amps, real power, power factor and the energy used so far.
//
//   ZMPT101B   OUT on A0, VCC 5V, GND        (its screw terminals across the mains)
//   ACS712-05B OUT on A1, VCC 5V, GND        (IP+ and IP- in series with the appliance)
//   LCD        16x2 with an I2C backpack at 0x27: SDA A4, SCL A5, VCC 5V, GND
//
// HOW IT WORKS
//   Both sensors give a wave that swings about half the supply. calcVI()
//   samples the two in turn over 20 zero crossings (10 mains cycles), takes
//   each one's middle out, and works out:
//     Vrms and Irms   the root mean square of each wave, times its calibration
//     realPower       the mean of volts times amps, sample by sample: the
//                     watts the meter on the wall charges for
//     powerFactor     real power over Vrms x Irms: 1 for a heater or a kettle,
//                     less for a motor or a phone charger
//   Watt hours add up as real power times the time between readings.
//
// CALIBRATION
//   VOLT_CAL is the ZMPT101B's ratio: mains volts per volt at OUT. Read the
//   mains with a meter you trust, then VOLT_CAL = VOLT_CAL x meter / sketch.
//   CURRENT_CAL is amps per volt at OUT: 1000 / 185 mV per amp = 5.405 for
//   the ACS712-05B (1000 / 100 = 10 for the 20 A board, 1000 / 66 = 15.15
//   for the 30 A). PHASE_CAL moves the voltage samples to line up with the
//   current ones: with a kettle or a heater plugged in, change it until the
//   power factor reads 1.00. EmonLib's own examples use 1.7 for their AC
//   adapter; 1.0 is no correction.
//
// SAFETY
//   The ZMPT101B and the ACS712's IP pins sit on line voltage. Only a
//   qualified person should wire them to the mains, in a closed box. A
//   clip-on current transformer (SCT-013) with EmonLib's calcIrms() keeps
//   the current side off the wires altogether.

#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include "EmonLib.h"

LiquidCrystal_I2C lcd(0x27, 16, 2);
EnergyMonitor emon;

const float VOLT_CAL = 600.0;    // the ZMPT101B's trimmer ratio
const float PHASE_CAL = 1.0;    // no correction: see CALIBRATION
const float CURRENT_CAL = 5.405; // ACS712-05B: 1000 / 185
const float NO_LOAD_A = 0.05;    // below this the ACS712's noise, not a load

double wattHours = 0;
unsigned long lastAt = 0;

void setup() {
  Serial.begin(9600);
  lcd.init();
  lcd.backlight();
  lcd.print("Energy monitor");

  emon.voltage(A0, VOLT_CAL, PHASE_CAL);
  emon.current(A1, CURRENT_CAL);
  // The first window starts with the offsets at mid-scale and no supply
  // reading yet: measure once and throw it away.
  emon.calcVI(20, 2000);
  lastAt = millis();
  Serial.println("energy monitor ready");
}

void loop() {
  emon.calcVI(20, 2000);

  float volts = emon.Vrms;
  float amps = emon.Irms;
  float watts = emon.realPower;
  float pf = emon.powerFactor;
  if (amps < NO_LOAD_A) {   // nothing plugged in
    amps = 0;
    watts = 0;
    pf = 0;
  }
  if (watts < 0) watts = 0; // a sensor turned round reads negative

  unsigned long now = millis();
  wattHours += watts * (now - lastAt) / 3600000.0;
  lastAt = now;

  // 230V 1.41A PF1.0
  //  325W 0.9Wh
  int centiAmps = (int)(amps * 100 + 0.5);
  int tenthsPf = (int)(pf * 10 + 0.5);
  char line[17];
  char wh[10];
  dtostrf(wattHours, 1, 1, wh);
  lcd.setCursor(0, 0);
  snprintf(line, sizeof line, "%3dV %d.%02dA PF%d.%d", (int)(volts + 0.5),
           centiAmps / 100, centiAmps % 100, tenthsPf / 10, tenthsPf % 10);
  lcd.print(line);
  lcd.setCursor(0, 1);
  snprintf(line, sizeof line, "%5dW %sWh      ", (int)(watts + 0.5), wh);
  lcd.print(line);

  Serial.print("V ");
  Serial.print(volts, 1);
  Serial.print("  A ");
  Serial.print(amps, 2);
  Serial.print("  W ");
  Serial.print(watts, 0);
  Serial.print("  PF ");
  Serial.print(pf, 2);
  Serial.print("  Wh ");
  Serial.println(wattHours, 2);

  delay(800);
}

Code walkthrough

The key functions of the sketch, and what each one does.

setup sketch.ino

Starts serial and the LCD, then tells EmonLib which pins to use and the calibration numbers: emon.voltage for A0 and emon.current for A1. It runs one measurement and throws it away, because the first window starts with the offsets at mid-scale.

void setup() {
  Serial.begin(9600);
  lcd.init();
  lcd.backlight();
  lcd.print("Energy monitor");

  emon.voltage(A0, VOLT_CAL, PHASE_CAL);
  emon.current(A1, CURRENT_CAL);
  // The first window starts with the offsets at mid-scale and no supply
  // reading yet: measure once and throw it away.
  emon.calcVI(20, 2000);
  lastAt = millis();
  Serial.println("energy monitor ready");
}

loop sketch.ino

Measures with calcVI, then reads volts, amps, watts and power factor. Below NO_LOAD_A it treats the current as noise and shows zero. It adds to wattHours, writes two lines to the LCD, prints to serial, and waits 800 ms.

void loop() {
  emon.calcVI(20, 2000);

  float volts = emon.Vrms;
  float amps = emon.Irms;
  float watts = emon.realPower;
  float pf = emon.powerFactor;
  if (amps < NO_LOAD_A) {   // nothing plugged in
    amps = 0;
    watts = 0;
    pf = 0;
  }
  if (watts < 0) watts = 0; // a sensor turned round reads negative

  unsigned long now = millis();
  wattHours += watts * (now - lastAt) / 3600000.0;
  lastAt = now;

  // 230V 1.41A PF1.0
  //  325W 0.9Wh
  int centiAmps = (int)(amps * 100 + 0.5);
  int tenthsPf = (int)(pf * 10 + 0.5);
  char line[17];
  char wh[10];
  dtostrf(wattHours, 1, 1, wh);
  lcd.setCursor(0, 0);
  snprintf(line, sizeof line, "%3dV %d.%02dA PF%d.%d", (int)(volts + 0.5),
           centiAmps / 100, centiAmps % 100, tenthsPf / 10, tenthsPf % 10);
  // ... 18 more lines in the editor

Troubleshooting

The usual mistakes with this circuit, and the fix for each.

  • Volts or amps read wrong compared with a meter This project
    Adjust VOLT_CAL and CURRENT_CAL. Scale VOLT_CAL by the meter reading over the sketch reading.
  • Power factor is not close to 1.00 with a heater This project
    Change PHASE_CAL until it is. A value of 1.0 means no correction.
  • The LCD stays blank This project
    Check SDA and SCL and the backpack address. The sketch uses 0x27.
  • The reading sits flat or at a constant number. AC voltage sensor
    The module output is AC centered on about half the supply. Sample quickly over a full cycle, find peak to peak or the RMS, and subtract the center value. One analogRead alone tells nothing.
  • The mains voltage value is wrong. AC voltage sensor
    Calibrate with the trimmer and a known reference, such as a meter, and set a scale factor in code. Use the supply the module is meant for and keep the ADC reference stable.
  • The reading does not sit at zero amps. The output is about half of VCC at no current and moves only 185, 100 or 66 mV per amp, depending on the 5A, 20A or 30A part. Current sensor
    Read the idle value at startup and subtract it. Pick the version that fits your current and use the matching mV per amp. Average many samples to cut the noise.
  • The load is wired in parallel instead of in series, or a 3.3 V board reads wrong. The sensor needs 5 V and its output can be above 3.3 V. Current sensor
    Break the wire and run the load current through the IP+ and IP- terminals. Power the sensor from 5 V, and scale the output with a divider for 3.3 V ADCs.
  • The circuit under test shows clipped or distorted signals. Signal generator
    A sine swinging below zero can exceed what an input allows. Add a DC offset or bias so the signal stays inside the supply range of the part.

Questions and answers

How do I calibrate the voltage and current readings?

VOLT_CAL is the ZMPT101B's ratio, mains volts per volt at OUT: read the mains with a meter you trust and multiply VOLT_CAL by the meter's reading over the sketch's. CURRENT_CAL is 1000 over the ACS712's mV per amp: 5.405 for the 5 A board, 10 for the 20 A and 15.15 for the 30 A.

What is the difference between real power and volts times amps?

Real power is the average of volts times amps sample by sample, which is what the electricity meter charges for. Vrms times Irms is the apparent power, and the power factor is their ratio: 1 for a heater or kettle, less for motors and chargers.

Why does it read 0 A with nothing plugged in?

The ACS712's output noise reads as a few tens of milliamps. Below NO_LOAD_A (0.05 A) the sketch treats it as no load, so the watt hours do not creep up.

Is it safe to connect to mains?

The ZMPT101B's input and the ACS712's IP pins carry line voltage, so only a qualified person should wire them, in a closed box. A clip-on SCT-013 current transformer with EmonLib's calcIrms() keeps the current side off the wires.

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.