Routeur Solaire. Mesure de Puissance avec un ESP32

Mesurer en temps réel la puissance électrique consommée, voire produite par une installation solaire permet une meilleure gestion. Par exemple, en cas d’excédant, dans une installation en autoconsommation, on peut enclencher la fabrication d’eau chaude.
Le montage ci-après:
– mesure la puissance
– fourni la courbe instantanée de la tension et du courant sur une page web
– actionne un relais en cas de production au-dessus de la consommation
– fourni un rapport au système central de domotique (Domoticz) pour l’enregistrement.

L’ESP32 est un microcontrôleur adapté à notre besoin. Il comprend :
– des entrées analogiques pour mesurer des tensions,
– des entrées/sorties numériques pour actionner un relais si besoin,
– une liaison WIFI pour faire du reporting à distance sur une page web ou un système de domotique.

Routeur Solaire

Capteur du Courant

Transformateur de courant 100A/50mA

Pour mesurer le courant, on utilise un capteur de courant dans lequel on fait passer le fil de phase du secteur. En sortie, agissant comme un transformateur, il fourni un courant identique, mais 2000 fois plus faible. Ce courant est envoyé aux bornes d’une résistance et nous allons mesurer la tension générée.

Il existe différents modèles suivant le courant Max que l’on souhaite mesurer. La version 100A est adaptée à un domicile ayant une puissance max délivrée de 12kVA. On la trouve en Chine chez Aliexpress.

Capteur de la tension

Transformateur 220V/6V

Pour mesurer la tension, on utilise un transformateur bobiné classique abaisseur de tension qui nous isole du secteur. Par exemple un 230v/6v. Il faut un modèle le plus petit possible, on ne prélève aucune puissance. Cela n’est plus très facile à trouver. Un transformateur, dit de sonnette, peut faire l’affaire.

Mesure Courant et Tension

La mesure des 2 tensions représentantes du courant et le la tension secteur se fait par les entrées analogiques de l’ESP32. Ces entrées acceptent une tension entre 0 et 3.3V et numérisent la valeur sur 12 bits, valeurs entre 0 et 4095. Pour s’adapter à la dynamique d’entrée, on crée une référence de tension au milieu de la plage à 1.65V =3.3V/2.

On prélève le 3.3V de l’ESP32 qui en passant par un pont de 2 résistances (R6 et R7) de 4700 ohm connecté à la masse nous fourni au milieu une référence de 1.65V. Pour éviter du bruit de mesure, un condensateur de 470uF (C2) filtre le 3.3V et un autre de 10uF (C1) filtre le point milieu à 1.65V.

Afin de ne pas dépasser les 3.3V crête à crête des signaux à mesurer, ou 1.65V crête, on se fixe une limite de +-1V efficace maximum.

Pour la sonde de courant avec 80A et une résistance de 24 ohm , on arrive à peu près au 1V.

24*80A/2000=0.96V

Chez moi, avec un abonnement de 12KVA, je ne devrai pas dépasser les 60A.

Pour la mesure de tension, il faut mettre un pont de résistances (R4 et R5) pour abaisser le 6V autour de 1V efficace.

Raccordement à l’ESP32

ESP32 Development Board 2*19 pins

Le jeu de piste avec ces cartes qui intègrent un ESP32, est de trouver les GPIO disponibles et non utilisés pour la programmation Flash etc.
Dans notre cas, on mesure les tensions suivantes:
– GPIO 35 : la tension de référence à 1.65V en théorie.
– GPIO 33 : la tension représentant le courant à mesurer
– GPIO 32 : la tension en sortie du transformateur

2 LED sur les GPIO 18 et 19 clignotent toutes les 2s. La jaune si on consomme du courant, la verte si on fournit du courant, car nous sommes en surproduction.

2 relais solides sur les GPIO 22 et 23 permettent:
– d’allumer une lumière dans le local commandé par l’interrupteur sur le GPIO 05,
– exciter le relais de puissance de mise en route du chauffe-eau en cas de production importante d’électricité.

Mesure

La mesure des 2 valeurs représentant la tension et le courant prend environ 120uS. En pratique, on prévoit sur une période de 20ms (1/50Hz) de prélever 100 couples de valeurs, ce qui donnera une bonne description de la tension à priori sinusoidale et du courant souvent chahuté par les alimentations à découpage.

Formules de calcul des puissances

Pour bien caler dans le temps chaque mesure, on utilise l’horloge en micro seconde de l’ESP32.

Toutes les 2 s, on effectue :
– la mesure des tensions et courants durant 20ms
– le calcul du courant efficace
– le calcul de la tension efficace
– le calcul de la puissance en kVA
– le calcul de la puissance en kW

Un calibrage préalable a été fait pour définir la constante multiplicative pour convertir la tension mesurée en binaire vers la tension réelle ou le courant.

Code

L’ensemble du code est écrit en utilisant l’IDE Arduino. Il est injecté dans un premier temps par la liaison série, puis une fois en place, on peut le modifier si besoin par le WIFI comme décrit ici.

#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 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];

int WIFIbug = 0;

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 = 1012;
int Nloop = 0;
int day = -1;

//  SERVER
//***********
void handleRoot() {
  String S;
  Debug.println(F("Client Web"));
  S = "<body style='background: linear-gradient(#118,#55b,#118);font-size:150%;'  onload='setTimeout(\"location.reload();\",2000);'><h2 style='text-align:center;color:white'>Total Power Consumption</h2>";
  S += "<div style='text-align:center;width:96%;margin:auto;''>";
    S += "<div style='width:400px;margin:auto;'><table align='center' style='border:3px inset grey;padding:4px;background-color:white;font-size:120%;width:100%;'>";
    S += "<tr><td>U :</td><td style='text-align: right;'>" + String(Uef) + "</td></tr>";
    S += "<tr><td>I :</td><td style='text-align: right;'>" + String(Ief) + "</td></tr>";
    S += "<tr><td>P Watt :</td><td style='text-align: right;'>" + String(PW) + "</td></tr>";
    S += "<tr><td>P VA :</td><td style='text-align: right;'>" + String(PVA) + "</td></tr>";
    S += "<tr><td>Power Factor : </td><td style='text-align: right;'>" + String(PowerFactor) + "</td></tr>";
    S += "<tr><td>kWh :</td><td style='text-align: right;'>" + String(Wh / 1000) + "</td></tr>";
    S += "<tr><td>Heure :</td><td style='text-align: right;'>" + String(floor(millis()/ 36000)/100) + "</td></tr>";
    S += "</table></div>";

    S +="<div style='text-align:center;width:1000px;margin:auto;'>";
      S += "<div style='text-align:center;'><h3><span style='color:red;'>U_</span><span style='color:lightgreen;'> I_</span></h3>";
      S += "<p><svg height='400' width='1000' style='border:3px inset grey;background-color:white;'>";
      S += "<line x1='0' y1='400' x2='0' y2='0' style='stroke:rgb(0,0,0);stroke-width:2' />";
      S += "<line x1='0' y1='200' x2='1000' y2='200' 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 = 200 - 200 * value1[i] / Vmax;
        int X = 10 * i;
        S += String(X) + "," + String(Y) + " ";
      }
      S += "' style='fill:none;stroke:red;stroke-width:6' />";
      S += "<polyline points='";
      for (int i = 0; i < 100; i++) {
        int Y = 200 - 200 * value2[i] / Imax;
        int X = 10 * i;
        S += String(X) + "," + String(Y) + " ";
      }
      S += "' style='fill:none;stroke:green;stroke-width:6' />";
      S += "</svg></p></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;
      String C="yellow";
      if (PW < 0) {
        X = 198 - Y;
        Y = 0;
        C="green";
      }
        S += "<div style='position:absolute;top:3px;height:14px;background-color:" +C +";left:" + String(X) + "%;right:" + String(Y) + "%;'></div>";
      S += "</div>";
    S +="</div>";
  S +="<div></body>";
  server.send(200, "text/html", S );
}

void handleNotFound() {
  Debug.println(F("File Not Found"));
  String message = "File Not Found\n\n";
  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) % 10;

  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.1\r\n" +
                 "Host: " + host + "\r\n" +
                 "Connection: close\r\n\r\n");
    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);
  unsigned long MeasureMillis = millis();
  unsigned long T= micros();
  while (millis() - MeasureMillis < 21) { //Read values during 20ms
    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 <-700){ //switch On water heater    
    digitalWrite(RelayWaterHeater,HIGH);
  }
  if (PW>1000){ //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");
  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() - previousComputeMillis >= 2000) {
    previousComputeMillis = millis();
    MeasurePower();
    ComputePower();
    Overproduction();

    if (WiFi.waitForConnectResult() != WL_CONNECTED) {
      Debug.println("Connection Failed! #" +String(WIFIbug));
       WIFIbug ++;
       if ( WIFIbug >20) {
        ESP.restart();
       }
    } else {
     Debug.println("Connection OK! ");
     WIFIbug = 0;
    }
    
    SendToDomoticz();
    Debug.println("Power:"+String(PW));
  }
  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-Power");
  // 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();
}

Page Web

Le code comprend un petit serveur Web qui permet d’afficher sur une page, les différentes mesures ainsi que la tension et le courant durant une période de 20 ms.

Montage

Montage

Dans une boite d’électricien on installe :

  • La carte ESP32 (AZ-Delivery)
  • Une alimentation 5V 1A pour l’ESP32
  • Un transformateur basse tension pour mesurer la tension
  • 2 Relais solide branché en 3.3V, commandé par l’ESP32
  • En face avant en option, un bouton poussoir et 2 LEDs

Raccordé à cette boite, on a la sonde de courant à placer autour du fil de phase du secteur à mesurer.

Mesure de puissance en entrée maison

Articles sur le photovoltaïque

19 réflexions sur « Routeur Solaire. Mesure de Puissance avec un ESP32 »

  1. bonjour
    ça fait quelques temps que j’essaie de programmer ce type de produit non sans mal, merci de l’avoir publié ça va me faciliter grandement la tâche.
    petite variante que j’aurais à proposer et de piloter non pas un relais mais un variateur de puissance (dimmer) pour injecter dans le chauffe-eau que le surplus ni plus ni moins, je n’hésiterai pas à le partager si j’arrive à l’intégrer.
    encore merci

  2. Un grand merci pour le temps passé et pour tout le travail exposé.
    Pour pinailler un peu mais en aucun cas une critique de ma part, j’ai été déçu de l’esp32 sur la mesure analogique car la plage de mesure n’est pas linéaire sur la plage 0 / 3.3V . Pour des mesures de puissances, les résultats sont plus fiables pour ma part avec un bon Atmega328 ( Arduino UNO) malgré une précisions plus faible que l ESP
    Mais le gros point fort de l’ESP32 c’est de pouvoir tout faire avec sans ajout de trop de composants externes comme un convertisseur NA.
    Merci encore, heureux d’avoir découvert ton site

    1. La non linéarité de l’ESP32 autour de OV et 3.3V est connue. Avec le schéma proposé, on est cadré au milieu et on ne va pas chercher le 0V ou le 3.3V suivant la résistance de charge choisie.
      Merci pour votre remarque pertinente.

  3. Dans la procédure overproduction le relais n’est activé qu’à partir d’un excédant de 700W. Y-a-t-il une contre indication pour abaisser ce seuil à 300 ou 400W?

    1. On peut l’abaisser. Attention à ne pas transformer le système en oscillateur suivant la consommation. La valeur d’enclenchement doit être supérieure à la valeur de déconnexion du montant de la consommation.
      Pour s’adapter aux grosses consommations comme un chauffe-eau et les petites surproductions, je prépare une version 2 avec un triac à la place du relais pour pouvoir délivrer des puissances proportionnelles à la surproduction. J’attend les pièces de chine.

  4. salut , super vidéos et super site aussi , mais je ne comprends pas bien l’utilité du transfo 220/6 v , seul l’intensité sous environ 230 v peux suffire a déterminer la puissance absorbée ou produite non ??
    cela dit je suis toujours étonné de voir les quantités de choses que l’on peut faire avec les ESP 8266 , les 12 et les 32
    encore bravo et merci pour vos travaux

    1. La tension est importante pour comparer sa phase avec celle du courant. C’est le seul moyen pour savoir dans quel sens se fait le transfert de puissance.

  5. Bonjour,
    Je recherchais justement à réaliser un wattmetre qui pouvait voir si on était en production ou consommation.
    J’avais fait un premier montage avec un pzem-004t mais celui-ci ne donnait pas le sens du courant.
    Merci donc d’avoir publier ce projet avec des explications claires.

  6. vraiment merci pour ces explications et pour ce tutoriel

    j’ai un soucis quand je televerse le code je ne sais plus cmt continuer
    j’essaye de me connecter au serveur mais sans succès

    svp je souhaiterais avoir une marche à suivre une fois que le code est telversé

    1. Il faut definir dans le code l’adresse IP qui se trouve dans le groupe d’adresse gérée par votre box Internet et tapez l’adresse dans un navigateur.

    1. Vos prenez un voltmètre et corrigé kV en faisant une règle de 3 par rapport à la valeur actuelle.
      Pour kI, faite la même chose avec une pince ampèremétrique ou un petit ampèremètre en serie en chargeant le systeme avec un courant acceptable (ex lampe à filament, radiateur). Evitez les alimentations à decoupage de LED.

  7. Bonjour
    Merci pour ce très bon document et la qualité des explications
    J’ai juste une petite question , après avoir décortiqué le code ; d’où viennent les valeurs KV et KI (0.2063 et 0.642)
    Je suppose qu’il s’agit de valeur de calibration ….ou bien ai-je raté quelque chose.
    Je mets en oeuvre cet am …
    Merci encore pour cet article et bon dimanche

    1. Oui kV et kI sont 2 coefficients de calibration à ajuster suivant votre systeme. La sortie du transfo etc en comparant avec les données d’un ampèremètre/voltmètre.

  8. Bonjour, d’abord superbe travail pour le montage du esp32 jusqu’à présents j’ai bien suivi tout le montage et je pense que ça fonctionne bien, la seule chose qui m’a échappé c’est l’intégration dans Domoticz je n’ai pas encore trouvé comment vous avez pu faire.
    pouvez-vous m’expliquer comment faire merci d’avance pour tout?
    Daniel

  9. Bonjour
    je termine mon installation solaire pour laquelle j’avais prévu un routeur Solaire Fronius Ohmpilot
    au final celui-ci n’est pas compatible àvec mon chauffe-eau stéatite.
    Je pratique modestement l’Arduino
    votre solution m’intéresse vivement mais j’ai un problème lors de la compilation
    j’ai cherché et pas trouvé le fichier ESPmDNS.h sur internet !!
    ——————————————————–
    Erreur de compilation:
    Routeur_solaire_ESP8266:2:10: fatal error: ESPmDNS.h: No such file or directory
    #include
    ^~~~~~~~~~~
    compilation terminated.
    Plusieurs bibliothèque trouvées pour « WiFi.h »
    Utilisé : C:\Program Files (x86)\Arduino\libraries\WiFi
    Non utilisé : C:\Users\admin\Documents\Arduino\libraries\WiFiEspAT
    exit status 1
    ESPmDNS.h: No such file or directory
    ————————————————-
    Remarque personnelle:
    Avec l’Arduino quand on galère rien que pour gérer les bibliothèques on se demande comment on peut produire un programme fonctionnel aussi complexe
    FELICITATIONS
    Cordialement

    1. Utilisez vous le version 2 d’Arduino? Elle contient plein d’exemples autour du web/DNS pour ESP32 etc. Essayez les. Je n’ai pas eu de souci de bibliothèque.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *