## Power measurement with an ESP32

Measuring in real time the electrical power consumed or even produced by a solar installation allows for better management. For example, in the event of a surplus, in a self-consumption installation, hot water production can be started.
The assembly below:

• measures power
• provided the instantaneous voltage and current curve on a web page
• activates a relay in case of production above consumption
• provided a report to the central home automation system (Domoticz) for registration.

The ESP32 is a microcontroller adapted to our needs. It includes :

• digital inputs/outputs to activate a relay if necessary,
• a WIFI connection for remote reporting on a web page or a home automation system.

## Current Sensor

To measure the current, a current sensor is used through which the mains phase wire is passed. At the output, acting like a transformer, it provides an identical current, but 2000 times lower. This current is sent across a resistor and we will measure the voltage generated.

There are different models depending on the Max current that you want to measure. The 100A version is suitable for a home with a maximum power delivered of 12kVA. It is found in China at Aliexpress.

## Voltage sensor

To measure the voltage, we use a classic wire-wound step-down transformer that isolates us from the mains. For example a 220v/6v. We need a model as small as possible, we do not take any power. It’s not very easy to find anymore. A so-called bell transformer can do the trick.

## Current and Voltage Measurement

The measurement of the 2 voltages representing the current and the mains voltage is done by the analog inputs of the ESP32. These inputs accept a voltage between 0 and 3.3V and digitize the value on 12 bits, values between 0 and 4095. To adapt to the input dynamics, a voltage reference is created in the middle of the range at 1.65V = 3.3V/2.

We take the 3.3V from the ESP32 which, passing through a bridge of 2 resistors (R6 and R7) of 4700 ohm connected to ground, provides us with a reference of 1.65V in the middle. To avoid measurement noise, a 470uF capacitor (C2) filters the 3.3V and another 10uF (C1) filters the midpoint at 1.65V.

In order not to exceed 3.3V peak to peak of the signals to be measured, or 1.65V peak, we set a limit of +-1V effective maximum.

For the current probe with 80A and a resistance of 24 ohm, we arrive at approximately 1V.

`24*80A/2000=0.96V`

At home, with a 12KVA subscription, I should not exceed 60A..

For the voltage measurement, you have to put a bridge of resistors (R4 and R5) to lower the 6V around 1V effective.

## Connection to ESP32

The treasure hunt with these cards that integrate an ESP32, is to find the GPIOs available and not used for Flash programming etc.
In our case, we measure the following voltages:

• GPIO 35: the reference voltage at 1.65V in theory.
• GPIO 33: the voltage representing the current to be measured
• GPIO 32: the output voltage of the transformer

2 LEDs on GPIOs 18 and 19 flash every 2s. The yellow if we consume current, the green if we supply current, because we are in overproduction.

2 solid relays on GPIO 22 and 23 allow:

• to turn on a light in the room controlled by the switch on the GPIO 05,
• energize the water heater start-up power relay in the event of significant electricity production.

## Measure

Measuring the 2 values representing voltage and current takes about 120uS. In practice, it is planned over a period of 20ms (1/50Hz) to sample 100 pairs of values, which will give a good description of the a priori sinusoidal voltage and of the current often disrupted by switching power supplies.

In order not to block the ESP32 for 20ms, it may need to transmit messages, we will take about ten measurements of the current/voltage pair for a little over 1 ms. A series of 10 measurements will be restarted 21 ms later on the next period of the sector. Thus the ESP32 can devote itself to other tasks. To properly time each measurement, we use the microsecond clock of the ESP32.

Every 10 s, we perform:

• the calculation of the effective current
• the calculation of the effective voltage
• the calculation of the power in kVA
• the calculation of the power in kW

A prior calibration was made to define the multiplicative constant to convert the voltage measured in binary to the real voltage or current.

## Code

All code is written using the Arduino IDE. It is initially injected by the serial link, then once in place, it can be modified if necessary by WIFI as described here.

``````#include <WiFi.h>
#include <ESPmDNS.h>
#include <WiFiUdp.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <ArduinoOTA.h>
#include <RemoteDebug.h>

const char* ssid = "******";
RemoteDebug Debug;
unsigned long previousMeasureMillis;
unsigned long previousComputeMillis;
unsigned long previousClignoteMillis;
unsigned long previousLightMillis;
bool Clignote = false;
bool LightOn =false;

IPAddress primaryDNS(8, 8, 8, 8);   //optional
IPAddress secondaryDNS(8, 8, 4, 4); //optional

WebServer server(80);

//PINS
const int AnalogIn0 = 35;  // GPIO 35
const int AnalogIn1 = 32;  // GPIO 32
const int AnalogIn2 = 33;  // GPIO 33
const int RelayLight = 22;
const int RelayWaterHeater = 23;
const int LedYellow = 18;
const int LedGreen = 19;
const int Inter = 5;

int value0;
int value1;
int value2;

float Uef;
float Ief;
float PVA;
float PW;
float PowerFactor;
float kV = 0.2083;
float kI = 0.0642;
float Wh = 0;

//Client of Domoticz
const char* host = "192.168.0.99";
const int httpPort = 8080;
const int idxPower = 1011;
int Nloop = 0;
int day = -1;

//  SERVER
//***********
void handleRoot() {
String S;
Debug.println(F("Client Web"));
S = "<h3 style='text-align:center'>Total Power Consumption</h3>";
S += "<div style='display:flex;justify-content:space-around'>";
S += "<div><table align='center' style='border:3px inset grey;padding:4px;background-color:#ddd;'>";
S += "<tr><td>U :</td><td>" + String(Uef) + "</td></tr>";
S += "<tr><td>I :</td><td>" + String(Ief) + "</td></tr>";
S += "<tr><td>P Watt :</td><td>" + String(PW) + "</td></tr>";
S += "<tr><td>P VA :</td><td>" + String(PVA) + "</td></tr>";
S += "<tr><td>Power Factor :</td><td>" + String(PowerFactor) + "</td></tr>";
S += "<tr><td>kWh :</td><td>" + String(Wh / 1000) + "</td></tr>";
S += "</table></div>";
S += "<div style='text-align:center;'><strong><span style='color:red;'>U_</span><span style='color:lightgreen;'> I_</span></strong>";
S += "<p><svg height='200' width='500' style='border:3px inset grey;background-color:#ddd;'>";
S += "<line x1='0' y1='200' x2='0' y2='0' style='stroke:rgb(0,0,0);stroke-width:2' />";
S += "<line x1='0' y1='100' x2='500' y2='100' style='stroke:rgb(0,0,0);stroke-width:2' />";
int Vmax = 1;
int Imax = 1;
for (int i = 0; i < 100; i++) {
Vmax = max(abs(value1[i]), Vmax);
Imax = max(abs(value2[i]), Imax);
Debug.println(value1[i]);
}
S += "<polyline points='";
for (int i = 0; i < 100; i++) {
int Y = 100 - 100 * value1[i] / Vmax;
int X = 5 * i;
S += String(X) + "," + String(Y) + " ";
}
S += "' style='fill:none;stroke:red;stroke-width:2' />";
S += "<polyline points='";
for (int i = 0; i < 100; i++) {
int Y = 100 - 100 * value2[i] / Imax;
int X = 5 * i;
S += String(X) + "," + String(Y) + " ";
}
S += "' style='fill:none;stroke:green;stroke-width:2' />";
S += "</svg></p></div></div>";
S += "<div style='width:96%;margin:auto;border:3px inset grey;background-color:#666;height:20px;position:relative;'>";
int X = 0;
int Y = 99 - 99 * PW / 12000;
if (PW < 0) {
X = 198 - Y;
Y = 0;
}
S += "<div style='position:absolute;top:3px;height:14px;background-color:yellow;left:" + String(X) + "%;right:" + String(Y) + "%;'>";
S += "</div></div>";
server.send(200, "text/html", S );
}

void handleNotFound() {
message += "URI: ";
message += server.uri();
message += "nMethod: ";
message += (server.method() == HTTP_GET) ? "GET" : "POST";
message += "nArguments: ";
message += server.args();
message += "n";
for (uint8_t i = 0; i < server.args(); i++) {
message += " " + server.argName(i) + ": " + server.arg(i) + "n";
}
server.send(404, "text/plain", message);

}
// DOMOTICZ client
//****************
void SendToDomoticz() {
String url;
Nloop = (Nloop + 1) % 4;

if (Nloop == 0 || Nloop == 2) {
// Use WiFiClient class to create TCP connections
WiFiClient client;
if (!client.connect(host, httpPort)) {
Serial.println("connection failed");
return;
}
if (Nloop == 2) {
// We now create a URI for the request
url = "/json.htm?type=command&param=getServerTime";  //Obtain time from Domoticz server

} else {
// We now create a URI for the request
url = "/json.htm?type=command&param=udevice&idx=";
url += String(idxPower);
url += "&nvalue=0&svalue=";
url += String(PW);
url += ";";
url += String(Wh);

}

// This will send the request to the server
client.print(String("GET ") + url + " HTTP/1.1rn" +
"Host: " + host + "rn" +
"Connection: closernrn");
unsigned long timeout = millis();
while (client.available() == 0) {
if (millis() - timeout > 5000) {
Serial.println(">>> Client Timeout !");
client.stop();
return;
}
}

// Read all the lines of the reply from server and print them to Serial
while (client.available()) {
// Serial.print(line);
int p = line.indexOf("ServerTime");
if (p > 0) {
String subline = line.substring(p + 14, p + 32);
p = subline.lastIndexOf("-");
subline = subline.substring(p + 1, p + 3); //day after month
// Serial.print(subline);
int theday = subline.toInt();
if (day != theday) {
day = theday;
Wh = 0; // Rest Watt Heure of the day
//   Serial.println("Reset Watt Hour");
}
}

}

//  Serial.println();
//  Serial.println("closing connection");
}

}

// POWER
//********
void MeasurePower() {
int i;
for (int k = 0; k < 11; k++) { //Read values during 1.2ms
i = (micros() % 20000) / 200;
}
}
void ComputePower() {
float V;
float I;
Uef = 0;
Ief = 0;
PW = 0;
for (int i = 0; i < 100; i++) {
V = kV * float(value1[i]);
Uef += sq(V);
I = kI * float(value2[i]);
Ief += sq(I );
PW += V * I;
}
Uef = sqrt(Uef / 100) ;
Ief = sqrt(Ief / 100) ;
PW = floor(PW / 100);
PVA = floor(Uef * Ief);
PowerFactor = floor(100 * PW / PVA) / 100;
Wh += PW / 360; //Every 10s
}

void Overproduction(){
if (PW <-500){ //switch On water heater
digitalWrite(RelayWaterHeater,HIGH);
}
if (PW>2000){ //Switch Off with hyteresis
digitalWrite(RelayWaterHeater,LOW) ;
}
}

// SETUP
//*******
void setup() {
Serial.begin(115200);
Serial.println("Booting");
if (!WiFi.config(local_IP, gateway, subnet, primaryDNS, secondaryDNS)) {
Serial.println("STA Failed to configure");
}
//WiFi.mode(WIFI_STA);
while (WiFi.waitForConnectResult() != WL_CONNECTED) {
Debug.println("Connection Failed! Rebooting...");
delay(5000);
ESP.restart();
}
// init remote debug
Debug.begin("ESP32");

initOTA();
Debug.println(WiFi.localIP());
server.on("/", handleRoot);

server.on("/inline", []() {
server.send(200, "text/plain", "this works as well");
});

server.onNotFound(handleNotFound);

server.begin();
Debug.println("HTTP server started");
previousMeasureMillis = millis();
previousComputeMillis = millis();
previousClignoteMillis = millis();

pinMode( RelayLight , OUTPUT);
pinMode( RelayWaterHeater , OUTPUT);
pinMode( LedYellow , OUTPUT);
pinMode( LedGreen , OUTPUT);
pinMode( Inter , INPUT_PULLUP);
digitalWrite(RelayLight, LOW);
digitalWrite(RelayWaterHeater, LOW);
digitalWrite(LedYellow, LOW);
digitalWrite(LedGreen, LOW);
}

// LOOP
//******
void loop() {
ArduinoOTA.handle();
Debug.handle();
server.handleClient();
if (millis() - previousMeasureMillis >= 21) {
previousMeasureMillis = millis();
MeasurePower();
}
if (millis() - previousComputeMillis >= 10000) {
previousComputeMillis = millis();
ComputePower();
Overproduction();
SendToDomoticz();
}
if (millis() - previousClignoteMillis >= 2000) {
Clignote = !Clignote;
if (Clignote) {
previousClignoteMillis = millis() - 1950;

} else {
previousClignoteMillis = millis();
}
if (PW >= 0) {
digitalWrite(LedYellow, Clignote);
digitalWrite(LedGreen, LOW);
} else {
digitalWrite(LedYellow, LOW);
digitalWrite(LedGreen, Clignote);
}
}
//Light
if (digitalRead(Inter)==LOW  &&  (millis()-previousLightMillis) >500) {
LightOn=!LightOn;
digitalWrite(RelayLight,LightOn);
previousLightMillis=millis();
}
if (millis() - previousLightMillis >= 120000) {
LightOn=false;
digitalWrite(RelayLight,LightOn);
previousLightMillis=millis();
}

}

// OTA
//******
void initOTA() {
// Port defaults to 3232
// ArduinoOTA.setPort(3232);
// Hostname defaults to esp3232-[MAC]
ArduinoOTA.setHostname("ESP32");
// No authentication by default
// Password can be set with it's md5 value as well
ArduinoOTA
.onStart([]() {
String type;
if (ArduinoOTA.getCommand() == U_FLASH)
type = "sketch";
else // U_SPIFFS
type = "filesystem";
// NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end()
Debug.println("Start updating " + type);
})
.onEnd([]() {
Debug.println("nEnd");
})
.onProgress([](unsigned int progress, unsigned int total) {
Debug.printf("Progress: %u%%r", (progress / (total / 100)));
})
.onError([](ota_error_t error) {
Debug.printf("Error[%u]: ", error);
if (error == OTA_AUTH_ERROR) Debug.println("Auth Failed");
else if (error == OTA_BEGIN_ERROR) Debug.println("Begin Failed");
else if (error == OTA_CONNECT_ERROR) Debug.println("Connect Failed");
else if (error == OTA_END_ERROR) Debug.println("End Failed");
});
ArduinoOTA.begin();
}``````

## Web Page

The code includes a small web server that displays on one page the various measurements as well as the voltage and current over a period of 20 ms.