This project shows how to build an Arduino based electricity consumption monitor using the Industruino PROTO platform. We can use it to measure the power consumption of an AC appliance such as a water cooker, a TV, a laptop charger, anything you plug into a wall socket. Alternatively you can also use it in your electricity cabinet to measure the power consumption in your entire house (at least one phase).
The challenge is to measure an alternating current (AC) of a relatively high voltage (220-240V) with our direct current 5V Arduino microcontroller. This may seem dangerous, but we will use a non-invasive Current Transformer (CT), so our Arduino remains galvanically isolated from the high voltage AC.
This prototype is based on the excellent open source project OpenEnergyMonitor. It uses parts of the its standard emonTx hardware and software to report the AC apparent power consumption, based on measurements of a Current Transformer as in the picture on the left. The original project also allows to measure 3 phase and/or real power, but for our prototype here we are only measuring the current of one phase, not its voltage which would require an AC/AC adaptor.
The CT, as shown in the picture above, is clamped around 1 wire of the mains (live or neutral, not both!), and it has 2000 windings and a max current of 50mA so it can read up to 100A. More info here.
We want to capture the small alternating current that is proportional (1:2000) to the AC that goes through the main wire. We convert this current into a voltage by putting through a burden resistor, as in the picture on the right.
I used a burden resistor of 30 ohm, and a voltage divider 10K/10K for the DC bias. This creates a voltage around 2.5V proportional to the current flowing through the CT, as explained in above link. The circuit is complete with one 10uF capacitor. The output goes to the PROTO's A6 (=D4).
For a detailed explanation, see OpenEnergyMonitor.org
The CT has a 3.5mm (mono) jack and i put a socket on the Industruino PROTO's baseboard, enlarging the soldering holes to fit the pins.
What you see in the picture on the left is actually all you need to measure the AC power!
The sketch is quite simple, as all calculations are done in the emon library.
The RMS voltage is considered a constant, to be calibrated with a multimeter.
The maximum current of this setup is 100A, so it can measure up to 20kW. This can be adjusted by changing the burden resistor.
The LCD shows the AC power (in Watt) as a product of the calibrated AC voltage (in Volt) and the measured current (in Ampere). The final version also shows the average power consumption since startup (in Watt), and the accumulated energy consumption (in kWh). This kWh number is probably not very accurate as the sketch does not use precise timing.
Although the 32u4 topboard that we used for this prototype only has 28K available, i could use the U8G display library with no problem: the code is quite small and i used subsets of the 3 fonts to reduce the file size.
Below is the full setup:
-
the white cable plugged into the electricity wall socket (out of view)
-
the CT on the blue power line
-
the kettle's black cable plugged into the white socket
-
the CT plugged into the PROTO (without its case)
-
the PROTO powered by a USB power bank
LOGGING YOUR DATA INTO THE CLOUD
There are 2 options:
- using the Industruino Ethernet module
- installing an ESP8266 WiFi module inside the PROTO
The Ethernet option is straightforward: just connect your Ethernet module to the PROTO using the supplied cable, and plug in an ethernet cable connected to your LAN, as in the picture on the left (PROTO and Ethernet modules shown without casings).
See ETHERNET sketch below.
Installing the ESP9266 WiFi module requires a bit of soldering, as in the picture on the right, details below.
Our favourite cloud solution is the open source http://emoncms.org/ where you can create nice visualisations and dashboards.
This is a graph on emoncms.org with 3 hours of data for my laptop charger, which consumes around 40W when the laptop screen is on, and less than 10W when off.
I have also added a counter in the code to keep track of kiloWatt-hours (kWh) since startup of the Industruino, and average power consumption. The timing is not exact; as far as i can tell it's accurate to about 3%.
As the WiFi module has to restart completely, each data transmission takes around 9 seconds. During that period there are no power measurements so i use the last reading for that entire period.
Below is the sketch; it is easy to remove the Ethernet/WiFi part if needed.
PROTO baseboard implementation with ESP8266 WiFi module (ESP-01)
The diagram on the right shows you how to wire the sensor conditioning circuit and ESP8266 module onto the prototyping area of the Industruino.
I used the minimal ESP-01 package in a 4x2 female header on the PROTO board. I used the onboard 3.3V with a large capacitor (1500uF) to deal with the high current requirements during transmission. And i used the Serial1 hardware serial port on D0/D1 at baud rate 115200, with the standard AT firmware on the ESP8266.
When the WiFi connection is active, there is a lot of interference with the analog measurement, so i shutdown the WiFi module during the sampling (by pulling the CH_PD pin to GND), and restart it after a fixed interval, e.g. 1 min, to send the power data.
See WIFI sketch below.
/*
INDUSTRUINO PROTO with Leonardo style 32u4 + ETHERNET MODULE
measuring AC power consumption
based on the emonCMS project's emonTX https://github.com/openenergymonitor/emonTxFirmware/tree/master/emonTxV3
in particular the sketch: emonTxV3_2_DiscreteSampling
Current Transformers: https://openenergymonitor.org/emon/buildingblocks/ct-sensors-interface
possible to read up to 10W even when nothing is connected (ADC resolution) https://openenergymonitor.org/emon/buildingblocks/measurement-implications-of-adc-resolution-at-low-current-values
hardware: 10K/10K voltage divider for DC bias
30 ohm burden resistor and 10uF cap
CT: YHDC SCT-013-000 with 2000 windings, 100A max
Industruino Ethernet module
LCD shows:
> apparent power (W) -- we do not measure the voltage so cannot know the phase shift
> measured current (A) -- RMS
> preset voltage (V) -- RMS
> kWh spent since startup (kWh) -- cumulative per second, stored as Watt-seconds
Tom Tobback, Nov 2016
*/
#include "U8glib.h"
U8GLIB_MINI12864 u8g(21, 20, 19, 22); // SPI Com: SCK = 21, MOSI = 20, CS = 19, A0 = 22
#include "EmonLib.h" // Include Emon Library
EnergyMonitor emon; // Create an instance
#include <avr/wdt.h>
#include <SPI.h>
#include <EthernetIndustruino.h>
// Enter a MAC address and IP address for your controller below.
byte mac[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED};
// Set the static IP address to use if the DHCP fails to assign
IPAddress ip(192, 168, 1, 145);
// Initialize the Ethernet client library
EthernetClient client;
#define DST_IP "80.243.190.58" // emoncms.org
const byte Vrms = 225; // Vrms for apparent power readings (when no AC-AC voltage sample is present)
// measured by Tom in Mui Wo
const int no_of_samples = 1480;
const float Ical = 66.6; // (2000 turns / 30 Ohm burden) = 66.6
float Irms;
unsigned long lcd_timestamp;
unsigned long sending_timestamp;
unsigned long sending_interval = 60; // wifi data sending interval in seconds
unsigned long watt_seconds = 0; // to store the energy measured since startup
unsigned long seconds = 0; // counter since start
////////////////////////////////////////////////////////////////////////////////////////////////////////////
void setup() {
pinMode(13, OUTPUT); // LED
digitalWrite(13, LOW); // LOW = backlight ON -- do not use analogWrite
pinMode(2, OUTPUT); // in case the ESP8266 is installed need to pull CH_PD down
digitalWrite(2, LOW); // ESP off
emon.current(6, Ical); // Current: input pin A6=D4, calibration factor
Serial.begin(9600); // for Serial Monitor
wdt_enable(WDTO_8S); // options: WDTO_1S, WDTO_2S, WDTO_4S, WDTO_8S
u8g.begin();
u8g.setRot180();
drawIntro(); // LCD intro screen
delay(2000);
checkEthernet(); // check Ethernet connection
delay(2000);
sending_timestamp = millis(); // reset counters
lcd_timestamp = millis();
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////
void loop() {
wdt_reset(); // confirm to watchdog timer that all is well
Irms = emon.calcIrms(no_of_samples); // Calculate Irms only
int power = Irms * Vrms;
if (millis() - lcd_timestamp > 925L) { // in theory should be 1000 but needs to be calibrated (accuracy 3% over 15min)
lcd_timestamp = millis(); // reset counter to include the time spent doing the display
watt_seconds += power;
seconds++;
int power_average = watt_seconds / seconds;
Serial.print("Measured power (W): ");
Serial.print(power); // Apparent power
Serial.print("\t");
Serial.print(Irms); // Irms
Serial.print("A");
Serial.print("\t");
Serial.print(watt_seconds); // Wattseconds
Serial.print("Ws");
Serial.print("\t");
Serial.print(seconds); // seconds counter
Serial.print("s");
Serial.print("\t");
Serial.print(power_average); // seconds counter
Serial.println("W average");
drawPower(power, power_average);
}
if (millis() - sending_timestamp > sending_interval * 1000L) {
unsigned long start_sending = millis();
drawSending(power);
Serial.println();
sendData(power); // send data to cloud server
delay(200); // to avoid interference
float seconds_sending = (millis() - start_sending) / 1000.0;
Serial.print("seconds spent sending = ");
Serial.println(seconds_sending);
watt_seconds += power * seconds_sending;
seconds += seconds_sending;
lcd_timestamp = millis(); // reset timers
sending_timestamp = millis();
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////
void drawIntro(void) { // INTRO
u8g.firstPage();
do {
u8g.setFont(u8g_font_6x13r);
u8g.drawStr(10, 10, "Industruino emonTx");
u8g.drawStr(10, 25, "Energy Monitor");
u8g.drawStr(10, 40, "Ethernet version");
// u8g.drawStr(10, 60, "Tom Tobback v2");
} while ( u8g.nextPage() );
}
void drawPower(int p, int avg) { // main screen
u8g.firstPage();
do {
u8g.setFont(u8g_font_6x13r);
u8g.drawStr(10, 10, "Industruino emonTx");
u8g.setPrintPos(10, 45);
u8g.setFont(u8g_font_fub25n);
u8g.print(p);
u8g.setFont(u8g_font_6x13r); // on 1286 board can use nicer font for this line: u8g_font_fub14r (on 32u4 not enough memory)
u8g.print("W");
u8g.setFont(u8g_font_6x13r);
u8g.setPrintPos(10, 60);
u8g.print(Irms, 1);
u8g.print("A");
u8g.setPrintPos(45, 60);
u8g.print(Vrms);
u8g.print("V");
u8g.setPrintPos(80, 60);
u8g.print(float (watt_seconds / (3600.0 * 1000.0)), 1);
u8g.print("kWh");
u8g.setPrintPos(95, 25);
u8g.print("avg:");
u8g.setPrintPos(95, 38);
u8g.print(avg);
u8g.print("W");
} while ( u8g.nextPage() );
}
void checkEthernet(void) { // check ethernet
Serial.println("Checking Ethernet..");
u8g.firstPage();
do {
u8g.setFont(u8g_font_6x13r);
u8g.drawStr(10, 10, "Industruino emonTx");
u8g.drawStr(10, 25, "Ethernet ");
u8g.drawStr(10, 35, "testing connection.. ");
} while ( u8g.nextPage() );
if (Ethernet.begin(mac) == 0) {
Serial.println("Failed to configure Ethernet using DHCP, trying with fixed IP");
u8g.firstPage();
do {
u8g.setFont(u8g_font_6x13r);
u8g.drawStr(10, 10, "Industruino emonTx");
u8g.drawStr(10, 25, "Ethernet ");
u8g.drawStr(10, 45, "DHCP failed, trying");
u8g.setPrintPos(10, 58);
u8g.print(ip);
} while ( u8g.nextPage() );
Ethernet.begin(mac, ip);
}
else {
u8g.firstPage();
do {
u8g.setFont(u8g_font_6x13r);
u8g.drawStr(10, 10, "Industruino emonTx");
u8g.drawStr(10, 25, "Ethernet");
u8g.drawStr(10, 45, "DHCP is OK");
u8g.setPrintPos(10, 58);
u8g.print(Ethernet.localIP());
} while ( u8g.nextPage() );
Serial.print("Ethernet connected via DHCP, IP=");
Serial.println(Ethernet.localIP());
}
}
void drawSending(int p) { // sending screen
u8g.firstPage();
do {
u8g.setFont(u8g_font_6x13r);
u8g.drawStr(10, 10, "Industruino emonTx");
u8g.setPrintPos(10, 45);
u8g.setFont(u8g_font_fub25n);
u8g.print(p);
u8g.setFont(u8g_font_6x13r); // on 1286 board can use nicer font for this line: u8g_font_fub14r (on 32u4 not enough memory)
u8g.print("W");
u8g.setFont(u8g_font_6x13r);
u8g.drawStr(10, 60, "sending data..");
} while ( u8g.nextPage() );
}
///////////////////////////////// ETHERNET /////////////////////////////////////////
void sendData(int p) {
wdt_reset(); // confirm to watchdog timer that all is well
Serial.print("Trying to connect to server.. ");
if (client.connect(DST_IP, 80)) {
Serial.print("connected - ");
// send an HTTP GET request:
client.print("GET /input/post.json?node=29&apikey=xx&csv=");
client.print(p);
client.println(" HTTP/1.0\r\n\r\n");
client.println("Host: www.emoncms.org");
client.println("Connection: close");
client.println();
delay(100); // minimum delay here depends on the speed of your internet connection
if (client.find("200 OK")) { // find function will wait for timeout
Serial.println("server replies OK!");
client.stop();
}
else { // no reply could be due to slow connection:
delay(1000); // wait extra second and find again
if (client.find("200 OK")) {
Serial.println("server replies OK!");
client.stop();
}
else {
Serial.println("no reply from server"); // no confirmation received from server
client.stop();
}
}
}
else {
Serial.println("connection failed"); // failed to connect to server
client.stop();
}
wdt_reset(); // confirm to watchdog timer that all is well
}
/*
INDUSTRUINO PROTO with Leonardo style 32u4 + ESP8266 WIFI
measuring AC power consumption
based on the emonCMS project's emonTX https://github.com/openenergymonitor/emonTxFirmware/tree/master/emonTxV3
in particular the sketch: emonTxV3_2_DiscreteSampling
Current Transformers: https://openenergymonitor.org/emon/buildingblocks/ct-sensors-interface
possible to read up to 10W even when nothing is connected (ADC resolution) https://openenergymonitor.org/emon/buildingblocks/measurement-implications-of-adc-resolution-at-low-current-values
hardware: 10K/10K voltage divider for DC bias
30 ohm burden resistor and 10uF cap
CT: YHDC SCT-013-000 with 2000 windings, 100A max
ESP826-01 connected to hardware serial1, standard firmware (AT commands):
Vcc 3.3V
GND GND
TX D0=RX
RX D1=TX
RST D3
CH_PD D2 (if no power down, a lot of interference with CT measurements
LCD shows:
> apparent power (W) -- we do not measure the voltage so cannot know the phase shift
> measured current (A) -- RMS
> preset voltage (V) -- RMS
> kWh spent since startup (kWh) -- cumulative per second, stored as Watt-seconds
Tom Tobback, Nov 2016
*/
#include "U8glib.h"
U8GLIB_MINI12864 u8g(21, 20, 19, 22); // SPI Com: SCK = 21, MOSI = 20, CS = 19, A0 = 22
#include "EmonLib.h" // Include Emon Library
EnergyMonitor emon; // Create an instance
#include <avr/wdt.h>
#define SSID "xx"
#define PASS "xx"
#define DST_IP "80.243.190.58" // emoncms.org
const byte Vrms = 225; // Vrms for apparent power readings (when no AC-AC voltage sample is present)
// measured by Tom in Mui Wo
const int no_of_samples = 1480;
const float Ical = 66.6; // (2000 turns / 30 Ohm burden) = 66.6
const int esp_rst = 3;
const int esp_pd = 2;
float Irms;
unsigned long lcd_timestamp;
unsigned long wifi_timestamp;
unsigned long sending_interval = 60; // wifi data sending interval in seconds
unsigned long watt_seconds = 0; // to store the energy measured since startup
unsigned long seconds = 0; // counter since start
////////////////////////////////////////////////////////////////////////////////////////////////////////////
void setup() {
pinMode(13, OUTPUT); // LED
digitalWrite(13, LOW); // LOW = backlight ON -- do not use analogWrite
pinMode(esp_rst, OUTPUT); // ESP reset
pinMode(esp_pd, OUTPUT); // ESP power down
digitalWrite(esp_pd, LOW); // ESP off
emon.current(6, Ical); // Current: input pin A6=D4, calibration factor
Serial.begin(9600); // for Serial Monitor
Serial1.begin(115200); // for ESP8266
wdt_enable(WDTO_8S); // options: WDTO_1S, WDTO_2S, WDTO_4S, WDTO_8S
u8g.begin();
u8g.setRot180();
drawIntro(); // LCD intro screen
delay(2000);
digitalWrite(esp_pd, HIGH); // ESP on
checkWifi();
digitalWrite(esp_pd, LOW); // ESP off
delay(2000);
wifi_timestamp = millis(); // reset counters
lcd_timestamp = millis();
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////
void loop() {
wdt_reset(); // confirm to watchdog timer that all is well
Irms = emon.calcIrms(no_of_samples); // Calculate Irms only
int power = Irms * Vrms;
if (millis() - lcd_timestamp > 925L) { // in theory should be 1000 but needs to be calibrated (accuracy 3% over 15min)
lcd_timestamp = millis(); // reset counter to include the time spent doing the display
watt_seconds += power;
seconds++;
int power_average = watt_seconds / seconds;
Serial.print("Measured power (W): ");
Serial.print(power); // Apparent power
Serial.print("\t");
Serial.print(Irms); // Irms
Serial.print("A");
Serial.print("\t");
Serial.print(watt_seconds); // Wattseconds
Serial.print("Ws");
Serial.print("\t");
Serial.print(seconds); // seconds counter
Serial.print("s");
Serial.print("\t");
Serial.print(power_average); // seconds counter
Serial.println("W average");
drawPower(power, power_average);
}
if (millis() - wifi_timestamp > sending_interval * 1000L) {
unsigned long start_sending = millis();
drawSending(power);
digitalWrite(esp_pd, HIGH); // ESP on
sendData(power); // send data to cloud server
digitalWrite(esp_pd, LOW); // ESP off
delay(200); // to avoid interference
float seconds_sending = (millis() - start_sending) / 1000.0;
Serial.print("seconds spent sending = ");
Serial.println(seconds_sending);
watt_seconds += power * seconds_sending;
seconds += seconds_sending;
lcd_timestamp = millis(); // reset timers
wifi_timestamp = millis();
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////
void drawIntro(void) { // INTRO
u8g.firstPage();
do {
u8g.setFont(u8g_font_6x13);
u8g.drawStr(10, 10, "Industruino emonTx");
u8g.drawStr(10, 25, "Energy Monitor");
// u8g.drawStr(10, 60, "Tom Tobback v2");
} while ( u8g.nextPage() );
}
void drawPower(int p, int avg) { // main screen
u8g.firstPage();
do {
u8g.setFont(u8g_font_6x13);
u8g.drawStr(10, 10, "Industruino emonTx");
u8g.setPrintPos(10, 45);
u8g.setFont(u8g_font_fub25n);
u8g.print(p);
u8g.setFont(u8g_font_fub14r);
u8g.print("W");
u8g.setFont(u8g_font_6x13);
u8g.setPrintPos(10, 60);
u8g.print(Irms, 1);
u8g.print("A");
u8g.setPrintPos(45, 60);
u8g.print(Vrms);
u8g.print("V");
u8g.setPrintPos(80, 60);
u8g.print(float (watt_seconds / (3600.0 * 1000.0)), 1);
u8g.print("kWh");
u8g.setPrintPos(95, 25);
u8g.print("avg:");
u8g.setPrintPos(95, 38);
u8g.print(avg);
u8g.print("W");
} while ( u8g.nextPage() );
}
void checkWifi(void) { // check Wifi
u8g.firstPage();
do {
u8g.setFont(u8g_font_6x13);
u8g.drawStr(10, 10, "Industruino emonTx");
u8g.drawStr(10, 25, "WiFi: ");
u8g.drawStr(50, 25, SSID);
u8g.drawStr(10, 35, "testing connection.. ");
} while ( u8g.nextPage() );
if (connectWiFi()) {
u8g.firstPage();
do {
u8g.setFont(u8g_font_6x13);
u8g.drawStr(10, 10, "Industruino emonTx");
u8g.drawStr(10, 25, "WiFi: ");
u8g.drawStr(50, 25, SSID);
u8g.drawStr(10, 45, "connection OK");
} while ( u8g.nextPage() );
}
else {
u8g.firstPage();
do {
u8g.setFont(u8g_font_6x13);
u8g.drawStr(10, 10, "Industruino emonTx");
u8g.drawStr(10, 25, "WiFi: ");
u8g.drawStr(50, 25, SSID);
u8g.drawStr(10, 45, "unable to connect");
} while ( u8g.nextPage() );
}
}
void drawSending(int p) { // sending screen
u8g.firstPage();
do {
u8g.setFont(u8g_font_6x13);
u8g.drawStr(10, 10, "Industruino emonTx");
u8g.setPrintPos(10, 45);
u8g.setFont(u8g_font_fub25n);
u8g.print(p);
u8g.setFont(u8g_font_fub14r);
u8g.print("W");
u8g.setFont(u8g_font_6x13);
u8g.drawStr(10, 60, "sending over WiFi..");
} while ( u8g.nextPage() );
}
///////////////////////////////// WIFI /////////////////////////////////////////
boolean sendData(int p) {
if (!connectWiFi()) {
Serial.println(F("cannot connect to wifi"));
return false;
}
wdt_reset(); // confirm to watchdog timer that all is well
//set the single connection mode
Serial1.println("AT+CIPMUX=0");
delay(500);
String cmd;
cmd = "AT+CIPSTART=\"TCP\",\"";
cmd += DST_IP;
cmd += "\",80";
Serial1.println(cmd);
// Serial.println(cmd);
if (Serial1.find("Error")) return false;
wdt_reset(); // confirm to watchdog timer that all is well
String action;
action = "GET /emoncms/input/post.json?node=29&apikey=xx&csv=";
action += p;
action += " HTTP/1.0\r\n\r\n";
Serial1.print("AT+CIPSEND=");
Serial1.println(action.length());
if (Serial1.find(">"))
{
Serial.print(">");
} else
{
Serial1.println("AT+CIPCLOSE");
Serial.println(F("connect timeout"));
return false;
}
Serial1.println(action);
// Serial.println(action);
// delay(1000);
if (Serial1.find("SEND OK"))
{
Serial.println(F("send OK.."));
}
else {
return false;
}
}
///////////////////////////////////////////////////////////////
boolean connectWiFi() {
Serial.print(F("reset ESP8266.."));
//test if the module is ready
digitalWrite(esp_rst, LOW); // reset the ESP module
delay(100);
digitalWrite(esp_rst, HIGH);
Serial1.println("AT+RST");
if (Serial1.find("ready"))
{
Serial.print(F("module is ready.."));
}
else
{
Serial.println(F("module has no response."));
return false;
}
wdt_reset(); // confirm to watchdog timer that all is well
Serial1.println("AT+CWMODE=1");
delay(1000);
while (Serial1.available()) { // flush Serial1 - seems necessary to clear serial buffer
Serial1.read(); }
wdt_reset(); // confirm to watchdog timer that all is well
String cmd = "AT+CWJAP=\"";
cmd += SSID;
cmd += "\",\"";
cmd += PASS;
cmd += "\"";
// Serial.println(cmd);
Serial1.println(cmd);
delay(5000);
// while (Serial1.available()) { // flush Serial1 - for debugging
// Serial.write(Serial1.read()); }
wdt_reset(); // confirm to watchdog timer that all is well
if (Serial1.find("CONNECTED")) { // response depends on your ESP AT firmware, used to be 'OK'
Serial.print(F("connected to wifi.."));
return true;
} else
{
Serial.println(F("cannot connect to wifi"));
return false;
}
}