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 :

  • analog inputs for measuring voltages,
  • 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

Current transformer 100A/50mA

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

Transformer 220V/6V

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

ESP32 WROOM AZ-DELIVERY

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.

Formulas for calculating powers

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 = "******";
const char* password = "*********";
RemoteDebug Debug;
unsigned long previousMeasureMillis;
unsigned long previousComputeMillis;
unsigned long previousClignoteMillis;
unsigned long previousLightMillis;
bool Clignote = false;
bool LightOn =false;

// Set your Static IP address
IPAddress local_IP(192, 168, 0, 208);
// Set your Gateway IP address
IPAddress gateway(192, 168, 0, 254);

IPAddress subnet(255, 255, 255, 0);
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[100];
int value2[100];

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() {
  Debug.println(F("File Not Found"));
  String message = "File Not Foundnn";
  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()) {
      String line = client.readStringUntil('r');
      // 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;
  value0 = analogRead(AnalogIn0);
  for (int k = 0; k < 11; k++) { //Read values during 1.2ms
    i = (micros() % 20000) / 200;
    value1[i] = analogRead(AnalogIn1) - value0;
    value2[i] = analogRead(AnalogIn2) - value0;
  }
}
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");
  // Configures static IP address
  if (!WiFi.config(local_IP, gateway, subnet, primaryDNS, secondaryDNS)) {
    Serial.println("STA Failed to configure");
  }
  //WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.waitForConnectResult() != WL_CONNECTED) {
    Debug.println("Connection Failed! Rebooting...");
    delay(5000);
    ESP.restart();
  }
  // init remote debug
  Debug.begin("ESP32");

  initOTA();
  Debug.println("Ready");
  Debug.print("IP address: ");
  Debug.println(WiFi.localIP());
  Serial.print("IP address: ");
  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
  // ArduinoOTA.setPassword("admin");
  // Password can be set with it's md5 value as well
  // MD5(admin) = 21232f297a57a5a743894a0e4a801fc3
  // ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3");
  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_RECEIVE_ERROR) Debug.println("Receive 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.

Hardware

Hardware

In an electrician’s box we install :

  • The ESP32 card (AZ-Delivery)
  • A 5V 1A power supply for the ESP32
  • A low voltage transformer to measure the voltage
  • 2 solid state relays connected to 3.3V, controlled by the ESP32
  • On the front panel as an option, a push button and 2 LEDs

Connected to this box, we have the current probe to be placed around the phase wire of the mains to be measured.

Leave a Reply

Your email address will not be published.