Energy Management System : real & factor power, délesteur, ESP32

Le compteur Linky n’affiche pas la puissance active, ni la puissance réinjectée tant que la demande spécifique n’est pas faite à ENEDIS.

Le compteur Linky affiche donc la puissance apparente positive qui est saturé à 0.

Le compteur Linky n’a pas de wi fi donc non communicant pour gérer de l’autoconsommation avec du photovoltaïque, ni la recharge d’une batterie domestique, ni la recharge d’un véhicule.

Par conséquent, le compteur linky ne permet pas de communiquer pour faire un délestage. Les applications cloud des energy meter vendu dans le commerce n’ont pas de période d’échantillonnage correcte pour faire varier le chargement d’un véhicule via l’autoconsommation photovoltaïque lors de couverture nuageuse variable, ni de faire la commande de délestage.

Donc, on propose de faire EMS (Energy Management System) pour mesurer la puissance monophasée de 8KVA (30A, 230V) avec un wifi d’où l’utilisation d’un ESP32 avec monitoring.

Il y a déjà pleins de choses sur internet avec des méthodes de programmations et des capteurs différents mais sans donner la précision des mesures. Certains utilisent des Library de capteur qui doivent être calibré, d’autres des circuits spécialisés tel que ATM90E26, PZEM-004T,….)

Notre choix a été d’utiliser des capteurs basiques pour tester plusieurs méthodes tout en isolant la tension secteur du micro tel que zmpt101b sensor voltage (0.5V d’amplitude de sortie) pour 311V et le sensor current SCT-013-050 qui passe de 50A d’amplitude à 1V. les 2 permettent d’être lu directement par l’ESP32.

Mais pour que le micro puisse mesurer le facteur de puissance positif et négatif, il faut une période d’échantillonnage convenable et avoir l’ondulation globale donc avec une tension d’offset de 1.65V

Le rapport de transformation zmpt101b 2mA de la tension secondaire dépend du rapport des résistances d’entrée et de sortie

https://www.alldatasheet.fr/datasheet-pdf/view/1821973/MICRO-TRANSFORMER/ZMPT101B.html

Avec le schéma électrique suivant la tension théorique sur la résistance R1 sera égale à la tension suivante avec une tension secteur de 240V

U2=240V*100/820k=0.029V

Puis, il y a 2 filtres inverseurs avec LM358 en cascades avec un gain de 10 chacun en théorie.

Mais, pour une amplitude secteur 340V (240V RMS), l’amplitude est de 0.55V avec un offset de 2.25V étant alimenté en 4.85V donc avec l’ESP 32 la précision de la mesure sera de 2V.

Pour une alimentation en 3.3V, l’amplitude de sortie est de 0.25V avec 1.65V d’offset.

Une fréquence d’échantillonnage de 0.2ms va être utilisé pour avoir 100 échantillons de la période secteur 20ms ce qui permettra d’avoir une mesure de précision du déphasage de 3.6° entre la tension et le courant. Mais, la méthode de la mesure du facteur de puissance par la puissance moyenne divisé par la puissance apparente mais il faudra calibrer le déphasage du capteur

Mais dans un premier temps, le sensor voltage va être testé sans routine d’interruption sur 1000 échantillon donc 10 périodes

Les méthodes de mesures sont bien expliquées sur ce lien

Voici le programme pour lire la tension secteur

// ZMPT101B sensor voltage
// Mesure de tension AC RMS

const int pinZMPT = 12;  //240V 50Hz
const int nombreEchantillons = 1000;   //10 periodes
float facteurCalibration = 1.00;
  float somme = 0;
  float sommeCarres = 0;

  int valeurADC;
  float tensionADC;
  float offset=3103;
  float signal1;
  float rmsADC;
  float tensionRMS;

  float valeurMax;  //
  float valeurMin;
  float amplitude;

void setup() {
  Serial.begin(115200);

  analogReadResolution(12);       // ADC 12 bits : 0 à 4095
  analogSetAttenuation(ADC_11db); // Plage ADC environ 0 à 3,3 V
}

void loop() {

  valeurMax = 0;
  valeurMin = 4095;

sommeCarres = 0;
  //  calcul des sommes   verif offset   2.5V=>3103
  for (int i = 0; i < nombreEchantillons; i++) {
    valeurADC = analogRead(pinZMPT);
    somme += valeurADC;            //
       
    if (valeurADC > valeurMax)   valeurMax = valeurADC;
//    if (valeurADC < valeurMin)   valeurMin = valeurADC;
        
    signal1 = valeurADC - offset;      // Suppression de l'offset pour RMS
    sommeCarres += signal1 * signal1;   // Carré du signal
    delayMicroseconds(200);        //pour 20ms secteur  mesure sur 100echantillon par periode
  }
// calcul de l'offset
  offset = somme / nombreEchantillons;
  somme=0;
// determination amplitude
  amplitude=((valeurMax-offset)*3.3*facteurCalibration)/4095;


  // Calcul RMS du signal alternatif
   rmsADC = sqrt(sommeCarres / nombreEchantillons);

   tensionADC = (rmsADC / 4095.0) * 3.3;
   tensionRMS = tensionADC * facteurCalibration;

  // Affichage monitor serie

  Serial.print("Offset=");
  Serial.print(offset, 1);

  Serial.print(" | RMS=");
  Serial.print(tensionRMS, 3);

  Serial.print(" | ampl=");    //peu precis
  Serial.println(amplitude, 3);

}

Dans la copie d’écran, le facteur de calibration est de 1 donc il faudra le passer à 582 en réel.
On remarquera que la mesure de l’amplitude n’est pas précise. De plus, le capteur déforme un peu le signal sinusoïdal, donc le rapport de racine de 2 entre l’amplitude de la valeur efficace ne fonctionne pas.

La suite des explications pour le teste du capteur de courant avec son offset avec la méthode de mesure de puissance et facteur de puissance ce sera pour bientôt.

Si l’on désire vérifier le déphasage entre la tension et le courant secteur avec une période d’échantillonnage rapide, le serial plotter ne peut pas être utilisé car l’envoie d’un float met 200us avec 115200 bauds.

De plus, le serial.print bloque la routine d’interruption.

Evidemment, un oscilloscope peut faire le job, mais utiliser le micro permet de vérifier l’acquisition.

Donc la solution est d’enregistrer les données dans une table tous les 4 secondes ici et envoie les données en bloquant les acquisitions.

Le programme utilise une routine d’interruption pour acquérir les 50 échantillons de la tension secteur avec si la période d’échantillonnage est de 400us, une période est visualisable.

Si l’on choisit une routine d’interruption de 800us alors 2 périodes seront visualisables

Voici le programme

//TTGO dispaly   ZMPT101B sensor voltage
// Mesure de tension et intensity  AC RMS 

#include <TFT_eSPI.h>
#include <SPI.h>

hw_timer_t * timer = NULL;

TFT_eSPI tft = TFT_eSPI();  //library   1,4" color 135, 240


const int nbrEchantillons = 2500;   // periodes
const float facteurCalibrationV = 582.00;
const float facteurCalibrationI = 1.00;
 int sampler=0;
 bool flag;
 bool flagenvoie;
  float sommeV = 0;
  float sommeI=0;
  float sommeCarres = 0;
  float sommeCarresI = 0;

  int valeurADC;  // tension,
  int valeurI;    //current

const int nbrsampler=50;
  int16_t tableV[nbrsampler];
  int16_t tableI[nbrsampler];
  byte indexTable=0;



  int offsetV=3103;
  int offsetI=2048;
  int signalV;
  int signalI;

  float rmsV;
  float tensionRMS;

  float rmsI;
  float currentRMS;

  float MaxV;  //
  float minV;
  float amplitudeV;

  float MaxI;  //
  float amplitudeI;

void IRAM_ATTR timer_isr() {    //routine interruption 1 seconde
 if (flagenvoie==1) {flag=0;} else  {flag=1;}    //bloque l'acquisition lors d'envoie des data
  sampler++;     //10 periode de 20ms
     
}//fin isr




void setup() {

  Serial.begin(115200);

  pinMode(22, OUTPUT);
  tft.init();                 //135 × 240.
  tft.setRotation(0); //0 format portrait a l'endroit  2 180°

  tft.fillScreen(TFT_BLACK);   
   
  tft.setCursor(0, 0, 2);  //// (x,y,taille)
  tft.setTextColor(TFT_GREEN,TFT_BLACK); //(couleur police,    background police)
  tft.println("IUT GEII SOISSONS");

  analogReadResolution(12);       // ADC 12 bits : 0 à 4095
  analogSetAttenuation(ADC_11db); // Plage ADC environ 0 à 3,3 V

uint8_t timer_id = 1;     
  uint16_t prescaler = 80; // Between 0 and 65 535
  int threshold = 800; // 800us

  timer = timerBegin(timer_id, prescaler, true);
  timerAttachInterrupt(timer, &timer_isr, true);
  timerAlarmWrite(timer, threshold, true);
  timerAlarmEnable(timer);

}

void loop() {

if (sampler>nbrEchantillons)  {   //400us*10000=4s   800us*2500=2
  flagenvoie=1;
  
  offsetV = sommeV / nbrEchantillons;
  offsetI = sommeI / nbrEchantillons;

  for (uint16_t i = 0; i < nbrsampler; i++)
    {
      Serial.print(tableV[i]);
      Serial.print(",");
      Serial.println(tableI[i]);
    }


// determination amplitude  
  amplitudeV=((MaxV-offsetV)*0.47);
  amplitudeI=((MaxI-offsetI)*0.0035);

  // Calcul RMS tension alternatif
   rmsV = sqrt(sommeCarres / nbrEchantillons);

   tensionRMS = (rmsV *0.47);           //3.3*582/4095=0.47
   

   rmsI = sqrt(sommeCarresI / nbrEchantillons);

  currentRMS= (rmsI*0.0035);            //3.3* /4095=0.004


tft.setCursor(0, 20, 4);// (x,y,taille)
tft.setTextColor(TFT_GREEN,TFT_BLACK);
tft.print("V:  ");
tft.print(tensionRMS,0);
tft.print("   ");

tft.setCursor(0, 50, 4);// (x,y,taille)
tft.setTextColor(TFT_GREEN,TFT_BLACK);
tft.print("aV:  ");
tft.print(amplitudeV,0);
tft.print("   ");

tft.setCursor(0, 80, 4);// (x,y,taille)
tft.setTextColor(TFT_RED,TFT_BLACK);
tft.print("I: ");
tft.print(currentRMS,3);
tft.print("   ");    

tft.setCursor(0, 110, 4);// (x,y,taille)
tft.setTextColor(TFT_RED,TFT_BLACK);
tft.print("aI:  ");
tft.print(amplitudeI,3);
tft.print("   ");

sommeCarres=0;
sommeCarresI=0;

  sommeV=0;
  sommeI=0;

    MaxV = 0;
  minV = 4095;
    MaxI = 0;

      sampler=0;
  flagenvoie=0;
  }  //end sampler



 if (flag==1)   {
   flag=0;
    if (digitalRead(22)==0)   {digitalWrite(22,1); } else {digitalWrite(22,0); }
    valeurADC = analogRead(27);      //pinZMPT sensor voltage
    sommeV += valeurADC;            //
       
    if (valeurADC > MaxV)   {MaxV = valeurADC;}
        
    signalV = valeurADC - offsetV;      // Suppression de l'offset pour RMS
    
    valeurI = analogRead(26);      //pin intensity
    sommeI += valeurI;
    signalI = valeurI - offsetI;   
    if (valeurI > MaxI)   {MaxI = valeurI;}

    sommeCarres += signalV * signalV;    // Carré du signal
    sommeCarresI += signalI * signalI;   // Carré du signal

    tableV[indexTable] = signalV;
    tableI[indexTable] = signalI;
    indexTable++;
    if  (indexTable>=nbrsampler)    {indexTable=0; }
                                                }  //flag==1


}   //end loop


On peut remarquer que le calcul de l’offset est légèrement décalé (amplitude positive n’est pas identique à l’amplitude négative), le ZMT provoque quelques distorsions du signal secteur ainsi que l’ADC.
Le courant a des distorsions souvent à cause des systèmes électroniques qui utilisent des redresseurs filtrés en tension mais aussi si le courant est faible.

Maintenant que la phase tension est bien synchro avec le courant et que les facteurs d’échelles sont bien calibrés avec une précision de la tension à 0.47V et celui du courant 0.0035A.

Alors la puissance apparente et active peut être programmer.

Sur des petits courants, de 0.3A AC, avec un capteur de 1V pour 50A SCT-013-050 , la mesure du courant est assez bruitée comme on peut l’observer sur la figure suivante.
https://www.alldatasheet.com/html-pdf/1160244/YHDC/SCT013-050/114/1/SCT013-050.html

D’ailleurs, ce bruit est mesurable car lorsqu’il n’y a pas de courant qui est de 0.034A.
Ce courant n’est pas significatif par rapport à un système qui peut mesurer 50A.
Il faudrait un amplificateur avec différents calibres pour améliorer cette précision pour les petits courants

Par contre, la précision de la mesure de la puissance active et apparente est correcte même pour les petits courants en utilisant la puissance instante divisée par 2500 échantillons toutes les 2 secondes

La puissance est bien nulle lorsqu’il n’y en a pas de courant

Voici le programme de base utilisant la somme de puissance instantanée divisée par 2500 échantillons pour avoir la puissance moyenne donc le calcul de l’énergie se fait sur l’intégration de la puissance tous les 2s.

/TTGO dispaly   ZMPT101B sensor voltage
// Mesure de tension et intensity  AC RMS   power energy

#include <TFT_eSPI.h>
#include <SPI.h>

hw_timer_t * timer = NULL;

TFT_eSPI tft = TFT_eSPI();  //library   1,4" color 135, 240


const int nbrEchantillons = 2500;   // periodes

 int sampler=0;
 bool flag;
 bool flagenvoie;
  float sommeV = 0;
  float sommeI=0;
  float sommeCarres = 0;
  float sommeCarresI = 0;

  float sommePower;

  int valeurADC;  // tension,
  int valeurI;    //current

const int nbrsampler=50;
const float calibreI=0.019;
const float calibreV=0.5;

  int16_t tableV[nbrsampler];
  int16_t tableI[nbrsampler];
  byte indexTable=0;

  int offsetV=3103;
  int offsetI=2048;
  int signalV;
  int signalI;

  float rmsV;
  float tensionRMS;

  float rmsI;
  float currentRMS;

  float MaxV;  //
  float minV;
  float amplitudeV;

  float MaxI;  //
  float amplitudeI;

  float Power;
  float S;      //power appararente
  float FP;     //factor power

  float joule2;  
  float Energy;

void IRAM_ATTR timer_isr() {    //routine interruption 800u seconde
 if (flagenvoie==1) {flag=0;} else  {flag=1;}    //bloque l'acquisition lors d'envoie des data
  sampler++;     //100 periode de 20ms
     
}//fin isr




void setup() {

  Serial.begin(115200);

  pinMode(22, OUTPUT);
  tft.init();                 //135 × 240.
  tft.setRotation(0); //0 format portrait a l'endroit  2 180°

  tft.fillScreen(TFT_BLACK);   
   
  tft.setCursor(0, 0, 2);  //// (x,y,taille)
  tft.setTextColor(TFT_GREEN,TFT_BLACK); //(couleur police,    background police)
  tft.println("IUT GEII SOISSONS");

  analogReadResolution(12);       // ADC 12 bits : 0 à 4095
  analogSetAttenuation(ADC_11db); // Plage ADC environ 0 à 3,3 V

uint8_t timer_id = 1;     
  uint16_t prescaler = 80; // Between 0 and 65 535
  int threshold = 800; // 800us

  timer = timerBegin(timer_id, prescaler, true);
  timerAttachInterrupt(timer, &timer_isr, true);
  timerAlarmWrite(timer, threshold, true);
  timerAlarmEnable(timer);

}

void loop() {

if (sampler>nbrEchantillons)  {   //400us*10000=4s   800us*2500=2
  flagenvoie=1;
  
  offsetV = sommeV / nbrEchantillons;
  offsetI = sommeI / nbrEchantillons;

  for (uint16_t i = 0; i < nbrsampler; i++)
    {
      Serial.print(tableV[i]);
      Serial.print(",");
      Serial.println(tableI[i]*4);
    }


// determination amplitude  
  amplitudeV=((MaxV-offsetV)*calibreV);
  amplitudeI=((MaxI-offsetI)*calibreI);

  // Calcul RMS tension alternatif
   rmsV = sqrt(sommeCarres / nbrEchantillons);
   tensionRMS = (rmsV *calibreV);           //3.3*582/4095=0.47
   
   rmsI = sqrt(sommeCarresI / nbrEchantillons);
   currentRMS= (rmsI*calibreI);            //3.3* /4095=0.004

   
tft.setCursor(0, 20, 4);// (x,y,taille)
tft.setTextColor(TFT_GREEN,TFT_BLACK);
tft.print("V:  ");
tft.print(tensionRMS,0);
tft.print("   ");

tft.setCursor(0, 50, 4);// (x,y,taille)
tft.setTextColor(TFT_GREEN,TFT_BLACK);
tft.print("aV:  ");
tft.print(amplitudeV,0);
tft.print("   ");

tft.setCursor(0, 80, 4);// (x,y,taille)
tft.setTextColor(TFT_RED,TFT_BLACK);
tft.print("I: ");
tft.print(currentRMS,3);
tft.print("   ");    


/*tft.setCursor(0, 110, 4);// (x,y,taille)
tft.setTextColor(TFT_RED,TFT_BLACK);
tft.print("aI:  ");
tft.print(amplitudeI,3);
tft.print("   ");
*/

Power=(sommePower*calibreV*calibreI)/ nbrEchantillons;    //power real
S=currentRMS*tensionRMS  ;         //power apparent
if (currentRMS>0.05) {FP=Power/S;  }   else {FP=1;  }   //factor power

joule2=joule2+Power;     //W*2s
Energy=(joule2*2)/3600;   //wh

tft.setCursor(0, 110, 4);// (x,y,taille)
tft.setTextColor(TFT_RED,TFT_BLACK);
tft.print("E:  ");
tft.print(Energy,0);
tft.print("   ");
                          
tft.setCursor(0, 140, 4);// (x,y,taille)
tft.setTextColor(TFT_BLUE,TFT_BLACK);
tft.print("P:  ");
tft.print(Power,0);
tft.print("    ");

tft.setCursor(0, 170, 4);// (x,y,taille)
tft.setTextColor(TFT_BLUE,TFT_BLACK);
tft.print("S:  ");
tft.print(S,0);
tft.print("   ");

tft.setCursor(0, 200, 4);// (x,y,taille)
tft.setTextColor(TFT_BLUE,TFT_BLACK);
tft.print("FP:  ");
tft.print(FP,2);
tft.print("   ");

sommePower=0;

sommeCarres=0;
sommeCarresI=0;

  sommeV=0;
  sommeI=0;

    MaxV = 0;
  minV = 4095;
    MaxI = 0;

      sampler=0;
  flagenvoie=0;
  }  //end sampler



 if (flag==1)   {
   flag=0;
    if (digitalRead(22)==0)   {digitalWrite(22,1); } else {digitalWrite(22,0); }
    valeurADC = analogRead(27);      //pinZMPT sensor voltage
    sommeV += valeurADC;            //
       
    if (valeurADC > MaxV)   {MaxV = valeurADC;}
        
    signalV = valeurADC - offsetV;      // Suppression de l'offset pour RMS
    
    valeurI = analogRead(26);      //pin intensity
    sommeI += valeurI;
    signalI = valeurI - offsetI;   
    if (valeurI > MaxI)   {MaxI = valeurI;}

    sommeCarres += signalV * signalV;    // Carré du signal
    sommeCarresI += signalI * signalI;   // Carré du signal
    
    sommePower  += signalI * signalV;

    tableV[indexTable] = signalV;
    tableI[indexTable] = signalI;
    indexTable++;
    if  (indexTable>=nbrsampler)    {indexTable=0; }
                                                }  //flag==1


}   //end loop

Sachant que la puissance instantanée contient une ondulation du double de la puissance secteur donc à 100 Hz. Donc pour avoir la puissance moyenne, il est possible d’utiliser une filtre IIR passe pas de fréquence de coupure de 1s qui donnera une puissance plus lissée.

Dans un premier temps, un filtre du premier ordre sera utilisé qui donne une atténuation de 0.016 donc 1.6% pour une fréquence de coupure de 0.16Hz et une constante de temps de 1s.
Donc l’énergie peut être déterminée toutes les 800us.

Evidemment, un filtre du second ordre aurait pu être utilisée. Mais avec une fréquence de coupure de 1Hz avec 800us d’échantillonnage, l’atténuation sera de 0.04 mais la précision des coefficients du filtre demandé ne pourra être réaliser avec 7 chiffres significatifs
a1 = -1.99289;
a2 = 0.99291;

Par contre, il serait possible de mettre 2 filtres du premier ordre en cascade si l’on veut améliorer la précision.

Voici le programme avec la puissance moyenne par le filtre

/TTGO dispaly   ZMPT101B sensor voltage
// Mesure de tension et intensity  AC RMS  filtre IIR power energy

#include <TFT_eSPI.h>
#include <SPI.h>

hw_timer_t * timer = NULL;

TFT_eSPI tft = TFT_eSPI();  //library   1,4" color 135, 240


const int nbrEchantillons = 2500;   // periodes

 int sampler=0;
 bool flag;
 bool flagenvoie;
  float sommeV = 0;
  float sommeI=0;
  float sommeCarres = 0;
  float sommeCarresI = 0;

  int valeurADC;  // tension,
  int valeurI;    //current

const int nbrsampler=50;
const float calibreI=0.019;
const float calibreV=0.5;

  int16_t tableV[nbrsampler];
  int16_t tableI[nbrsampler];
  byte indexTable=0;

  int offsetV=3103;
  int offsetI=2048;
  int signalV;
  int signalI;

  float rmsV;
  float tensionRMS;

  float rmsI;
  float currentRMS;

  float MaxV;  //
  float minV;
  float amplitudeV;

  float MaxI;  //
  float amplitudeI;

  float Powerinstant;
  float Power;
  float Powern;
  float Powerf;
  float S;      //power appararente
  float FP;     //factor power

  float joule2;  
  float Energy;

//  const float alpha = 1.0f - expf(-0.0008 / cstemps);    //0.0016    cstemps=1
const float alpha=0.0016;

void IRAM_ATTR timer_isr() {    //routine interruption 800u seconde
 if (flagenvoie==1) {flag=0;} else  {flag=1;}    //bloque l'acquisition lors d'envoie des data
  sampler++;     //100 periode de 20ms
     
}//fin isr




void setup() {

  Serial.begin(115200);

  pinMode(22, OUTPUT);
  tft.init();                 //135 × 240.
  tft.setRotation(0); //0 format portrait a l'endroit  2 180°

  tft.fillScreen(TFT_BLACK);   
   
  tft.setCursor(0, 0, 2);  //// (x,y,taille)
  tft.setTextColor(TFT_GREEN,TFT_BLACK); //(couleur police,    background police)
  tft.println("IUT GEII SOISSONS");

  analogReadResolution(12);       // ADC 12 bits : 0 à 4095
  analogSetAttenuation(ADC_11db); // Plage ADC environ 0 à 3,3 V

uint8_t timer_id = 1;     
  uint16_t prescaler = 80; // Between 0 and 65 535
  int threshold = 800; // 800us

  timer = timerBegin(timer_id, prescaler, true);
  timerAttachInterrupt(timer, &timer_isr, true);
  timerAlarmWrite(timer, threshold, true);
  timerAlarmEnable(timer);

}

void loop() {

if (sampler>nbrEchantillons)  {   //400us*10000=4s   800us*2500=2secondes affichage et bilan
  flagenvoie=1;
  
  offsetV = sommeV / nbrEchantillons;
  offsetI = sommeI / nbrEchantillons;

  for (uint16_t i = 0; i < nbrsampler; i++)
    {
      Serial.print(tableV[i]);
      Serial.print(",");
      Serial.println(tableI[i]*4);
    }


// determination amplitude  
  amplitudeV=((MaxV-offsetV)*calibreV);
  amplitudeI=((MaxI-offsetI)*calibreI);

  // Calcul RMS tension alternatif
   rmsV = sqrt(sommeCarres / nbrEchantillons);
   tensionRMS = (rmsV *calibreV);           //3.3*582/4095=0.47
   
   rmsI = sqrt(sommeCarresI / nbrEchantillons);
   currentRMS= (rmsI*calibreI);            //3.3* /4095=0.004

   
tft.setCursor(0, 20, 4);// (x,y,taille)
tft.setTextColor(TFT_GREEN,TFT_BLACK);
tft.print("V:  ");
tft.print(tensionRMS,0);
tft.print("   ");

tft.setCursor(0, 50, 4);// (x,y,taille)
tft.setTextColor(TFT_GREEN,TFT_BLACK);
tft.print("aV:  ");
tft.print(amplitudeV,0);
tft.print("   ");

tft.setCursor(0, 80, 4);// (x,y,taille)
tft.setTextColor(TFT_RED,TFT_BLACK);
tft.print("I: ");
tft.print(currentRMS,3);
tft.print("   ");    


/*tft.setCursor(0, 110, 4);// (x,y,taille)
tft.setTextColor(TFT_RED,TFT_BLACK);
tft.print("aI:  ");
tft.print(amplitudeI,3);
tft.print("   ");
*/

Power=Powerf*calibreV*calibreI;

S=currentRMS*tensionRMS  ;         //power apparent
if (currentRMS>0.05) {FP=Power/S;  }   else {FP=1;  }   //factor power


tft.setCursor(0, 110, 4);// (x,y,taille)
tft.setTextColor(TFT_RED,TFT_BLACK);
tft.print("E:  ");
tft.print(Energy,0);
tft.print("   ");
                          
tft.setCursor(0, 140, 4);// (x,y,taille)
tft.setTextColor(TFT_BLUE,TFT_BLACK);
tft.print("P:  ");
tft.print(Power,0);
tft.print("    ");

tft.setCursor(0, 170, 4);// (x,y,taille)
tft.setTextColor(TFT_BLUE,TFT_BLACK);
tft.print("S:  ");
tft.print(S,0);
tft.print("   ");

tft.setCursor(0, 200, 4);// (x,y,taille)
tft.setTextColor(TFT_BLUE,TFT_BLACK);
tft.print("FP:  ");
tft.print(FP,2);
tft.print("   ");


sommeCarres=0;
sommeCarresI=0;

  sommeV=0;
  sommeI=0;

    MaxV = 0;
  minV = 4095;
    MaxI = 0;

      sampler=0;
  flagenvoie=0;
  }  //end sampler



 if (flag==1)   {
   flag=0;
    if (digitalRead(22)==0)   {digitalWrite(22,1); } else {digitalWrite(22,0); }
    valeurADC = analogRead(27);      //pinZMPT sensor voltage
    sommeV += valeurADC;            //
       
    if (valeurADC > MaxV)   {MaxV = valeurADC;}
        
    signalV = valeurADC - offsetV;      // Suppression de l'offset pour RMS
    
    valeurI = analogRead(26);      //pin intensity
    sommeI += valeurI;
    signalI = valeurI - offsetI;   
    if (valeurI > MaxI)   {MaxI = valeurI;}

    sommeCarres += signalV * signalV;    // Carré du signal
    sommeCarresI += signalI * signalI;   // Carré du signal
    
    Powerinstant = signalI * signalV;      
    Powerf = Powern + alpha * (Powerinstant - Powern);  //puissance flitrée
    Powern = Powerf;           //n-1

Power=Powerf*calibreV*calibreI;
joule2=joule2+Power;     //
Energy=(joule2*0.0008)/3600;   //wh

    tableV[indexTable] = signalV;    //enregiste les 50 derniers mesures
    tableI[indexTable] = signalI;
    indexTable++;
    if  (indexTable>=nbrsampler)    {indexTable=0; }
                                                }  //flag==1


}   //end loop

La méthode du filtre IIR passe bas peut être utilisée aussi sur la mesure du courant RMS et pour enlever la composante continue un filtre passe haut donc plus besoin de déterminer l’offset

ibrut[n]→filtre passe-haut →i[n]→i^2[n]→IIR RMS

mais ce sera pour plus tard

Donc pour mesurer la tension et le courant un filtre passe haut du premier ordre a été utilisé avec une fréquence de coupure de 2Hz pour retirer la composante continue 0Hz mais gardant la composante alternative de 50Hz.
Puis un filtre passe bas du signal pour avoir la valeur efficace RMS du signal

Le temps de calcul pour avoir le courant, la tension et la puissance est de 250us.

Il n’est pas possible de mettre ce temps de calcul dans la routine d’interruption qui est pourtant de 800us car cela bug.

Par contre avec 1000us de la routine cela passe.

mais, on a refait un flag dans la fonction loop pour garder 800us

L’avantage du filtre passe bas est d’avoir un affichage stable qui évolue progressivement donc de façon moins abrupte surtout au niveau du courant qui est assez variable.

Voici le programme ou il n’y a pas besoin de mettre les données dans une table, c’est juste une vérification.

/TTGO dispaly   ZMPT101B sensor voltage
// Mesure de tension et intensity  AC RMS  filtre IIR passe haut et passe bas 

#include <TFT_eSPI.h>
#include <SPI.h>

hw_timer_t * timer = NULL;

TFT_eSPI tft = TFT_eSPI();  //library   1,4" color 135, 240


const int nbrEchantillons = 2500;   // pour affichage
volatile int sampler=0;
volatile  bool flag;
volatile  bool flagenvoie;


  float valeurADC,valeurADCn ;  // tension,
  float valeurI,valeurIn ;    //current

const int nbrsampler=50;       
const float calibreI=0.019;
const float calibreV=0.5;

  int16_t tableV[nbrsampler];
  int16_t tableI[nbrsampler];
  byte indexTable=0;

  float signalV,signalVn;
  float signalI,signalIn;

  float rmsV, rmsVn;
  float tensionRMS;

  float rmsI, rmsIn;
  float currentRMS;

  float MaxV;  //
  float minV;
  float amplitudeV;

  float MaxI;  //
  float amplitudeI;

  float Powerinstant;
  float Power;
  float Powern;
  float Powerf;
  float S;      //power appararente
  float FP;     //factor power

  float joule2;  
  float Energy;

//  const float alpha = 1.0f - expf(-0.0008 / cstemps);    //0.0016    cstemps=1
const float alphaPB=0.0016;
const float alphaHP =0.9901;

void IRAM_ATTR timer_isr() {    //routine interruption 800u seconde
if (flagenvoie==1) {flag=0;} else  {flag=1;} 
 sampler++;     //affichage tous les 2s


}//fin isr




void setup() {

  Serial.begin(115200);

  pinMode(25, OUTPUT);
  tft.init();                 //135 × 240.
  tft.setRotation(0); //0 format portrait a l'endroit  2 180°

  tft.fillScreen(TFT_BLACK);   
   
  tft.setCursor(0, 0, 2);  //// (x,y,taille)
  tft.setTextColor(TFT_GREEN,TFT_BLACK); //(couleur police,    background police)
  tft.println("IUT GEII SOISSONS");

  analogReadResolution(12);       // ADC 12 bits : 0 à 4095
  analogSetAttenuation(ADC_11db); // Plage ADC environ 0 à 3,3 V

uint8_t timer_id = 1;     
  uint16_t prescaler = 80; // Between 0 and 65 535
  int threshold = 800; // 800us

  timer = timerBegin(timer_id, prescaler, true);
  timerAttachInterrupt(timer, &timer_isr, true);
  timerAlarmWrite(timer, threshold, true);
  timerAlarmEnable(timer);
}




void loop() {

if (flag==1)  {flag=0;
// if (digitalRead(25)==0)   {digitalWrite(25,1); } else {digitalWrite(25,0); }
digitalWrite(25,1);    //temps de calcul du flag 250us
valeurADC = analogRead(27);      //pinZMPT sensor voltage     
    signalV = alphaHP * (signalVn + valeurADC - valeurADCn);  //filtre passe haut
    signalVn=signalV;       //valeur precedente
    valeurADCn=valeurADC;    
    rmsV = rmsVn + alphaPB * (signalV*signalV - rmsVn);  //filtre passe bas
    rmsVn=rmsV;             //valeur precedente
tensionRMS=(sqrt(rmsV))*calibreV;

    valeurI = analogRead(26);      //pin intensity
    signalI = alphaHP * (signalIn + valeurI - valeurIn);  //filtre passe haut
    signalIn=signalI;       //valeur precedente
    valeurIn=valeurI;    
    rmsI = rmsIn + alphaPB * (signalI*signalI - rmsIn);  //filtre passe bas
    rmsIn=rmsI;             //valeur precedente
currentRMS= (sqrt(rmsI))*calibreI; 
    
    Powerinstant = signalI * signalV;      
    Powerf = Powern + alphaPB * (Powerinstant - Powern);  //puissance flitrée
    Powern = Powerf;           //n-1

Power=Powerf*calibreV*calibreI;
joule2=joule2+Power;     //
Energy=(joule2*0.0008)/3600;   //wh


    tableV[indexTable] = signalV;    //enregiste les 50 derniers mesures
    tableI[indexTable] = signalI;
    indexTable++;
    if  (indexTable>=nbrsampler)    {indexTable=0; }

digitalWrite(25,0);

}// fin flag




if (sampler>2500)  {   //  800us*2500=2secondes affichage et bilan
 flagenvoie=1;

  for (uint16_t i = 0; i < nbrsampler; i++)
    {
      Serial.print(tableV[i]);
      Serial.print(",");
      Serial.println(tableI[i]*5);    //facteur d'echelle
    } 

tft.setCursor(0, 20, 4);// (x,y,taille)
tft.setTextColor(TFT_GREEN,TFT_BLACK);
tft.print("V:  ");
tft.print(tensionRMS,0);
tft.print("   ");

tft.setCursor(0, 50, 4);// (x,y,taille)
tft.setTextColor(TFT_GREEN,TFT_BLACK);
tft.print("aV:  ");
tft.print(amplitudeV,0);
tft.print("   ");

tft.setCursor(0, 80, 4);// (x,y,taille)
tft.setTextColor(TFT_RED,TFT_BLACK);
tft.print("I: ");
tft.print(currentRMS,3);
tft.print("   ");    

S=currentRMS*tensionRMS  ;         //power apparent
if (currentRMS>0.05) {FP=Power/S;  }   else {FP=1;  }   //factor power

tft.setCursor(0, 110, 4);// (x,y,taille)
tft.setTextColor(TFT_RED,TFT_BLACK);
tft.print("E:  ");
tft.print(Energy,0);
tft.print("   ");
                          
tft.setCursor(0, 140, 4);// (x,y,taille)
tft.setTextColor(TFT_BLUE,TFT_BLACK);
tft.print("P:  ");
tft.print(Power,0);
tft.print("    ");

tft.setCursor(0, 170, 4);// (x,y,taille)
tft.setTextColor(TFT_BLUE,TFT_BLACK);
tft.print("S:  ");
tft.print(S,0);
tft.print("   ");

tft.setCursor(0, 200, 4);// (x,y,taille)
tft.setTextColor(TFT_BLUE,TFT_BLACK);
tft.print("FP:  ");
tft.print(FP,2);
tft.print("   ");
sampler=0;
flagenvoie=0;
}  //fin flagaencoie et affichage

}   //end loop

Pourrait-on utiliser le double core de l’ESP32 entre l’affichage et la mesure ?

Exemple : Core 0 → mesure, Core 1 → TFT avec une synchronisation entre les 2 core

Evidement on peut utiliser les 2 core…

Pour simplifier le code de l’utilisation des 2 core avec la routine d’interruption, seule la tension est affichée.
Core 0 → mesure et calculs, Core 1 → TFT avec une synchronisation entre les 2

Voici le code

/TTGO display   double core
//Core 0 → mesure, Core 1 → TFT avec une synchronisation entre les 2
//isr de 500us et affichage tous les 1.25 s

#include <Arduino.h>
#include <TFT_eSPI.h>

TFT_eSPI tft;

#define PIN_ZMPT        27
#define N_ECHANTILLONS  2500
#define PERIODE_US      500
#define calibreV        0.5f

hw_timer_t *timer = NULL;

TaskHandle_t mesureTaskHandle = NULL;  // HANDLE DE LA TACHE


struct Mesure   // DONNEES PARTAGEES CORE 0 / CORE 1
{
  float tensionRMS;
  float moyenne;
  float moyenCarre;  
};

volatile Mesure resultat;
volatile bool nouvelleMesure ;

portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;  // PROTECTION DES DONNEES PARTAGEES


void IRAM_ATTR timerISR()   // INTERRUPTION TIMER
{
  BaseType_t xHigherPriorityTaskWoken = pdFALSE;

  // Réveille la tâche de mesure sur Core 0
  vTaskNotifyGiveFromISR(  mesureTaskHandle,  &xHigherPriorityTaskWoken  );

  if (xHigherPriorityTaskWoken)    {  portYIELD_FROM_ISR();  }
}



// CORE 0 : MESURE
// =====================================================

void mesureCore0(void *parameter)
{
const float alphaPB=0.0016;
const float alphaHP =0.9901;
float valeurADC,valeurADCn;
float signalV, signalVn;
float rmsV,rmsVn;
uint16_t echantillon;
float tensionRMS;


  while (true)   //loopcore0
  {
 if (digitalRead(25)==0)   {digitalWrite(25,1); } else {digitalWrite(25,0); }
// Attente de l'interruption 500 us
    ulTaskNotifyTake( pdTRUE,portMAX_DELAY   );

 valeurADC = analogRead(27);      //pinZMPT sensor voltage     
    signalV = alphaHP * (signalVn + valeurADC - valeurADCn);  //filtre passe haut
    signalVn=signalV;       //valeur precedente
    valeurADCn=valeurADC;    
    rmsV = rmsVn + alphaPB * (signalV*signalV - rmsVn);  //filtre passe bas
    rmsVn=rmsV;             //valeur precedente
   tensionRMS=(sqrt(rmsV))*calibreV;
   echantillon++;
      // =================================================
    if (echantillon >= N_ECHANTILLONS)   // Attente de l'interruption 800 us
    {   // ENVOI CORE 0 -> CORE 1
      portENTER_CRITICAL(&mux);
        resultat.tensionRMS = tensionRMS;
        nouvelleMesure =1;
      portEXIT_CRITICAL(&mux);

      echantillon = 0;  // Remise à zéro
    }
  }
}



void setup()
{
  Serial.begin(115200);

  analogReadResolution(12);
  analogSetAttenuation(ADC_11db);
  pinMode(PIN_ZMPT, INPUT);
  pinMode(25, OUTPUT);

  tft.init();
  tft.setRotation(0);

  tft.fillScreen(TFT_BLACK);
  tft.setTextColor(TFT_GREEN, TFT_BLACK);
  tft.setCursor(0, 0, 2);
  tft.println("IUT GEII SOISSONS");

  xTaskCreatePinnedToCore(      // TACHE MESURE -> CORE 0
    mesureCore0,          // fonction
    "MESURE",             // nom
    4096,                 // stack
    NULL,                 // paramètre
    2,                    // priorité
    &mesureTaskHandle,    // handle
    0                     // Core 0
  );

  uint8_t timer_id = 1;     // TIMER
  uint16_t prescaler = 80;

  timer = timerBegin( timer_id,   prescaler,   true   );
  timerAttachInterrupt( timer, &timerISR, true );
  timerAlarmWrite( timer, PERIODE_US, true   );
  timerAlarmEnable(timer);
}


// =====================================================
float V;

void loop()    // CORE 1 : TFT
{
  if (nouvelleMesure==1)   {
//      if (digitalRead(25)==0)   {digitalWrite(25,1); } else {digitalWrite(25,0); }
      
    portENTER_CRITICAL(&mux);      // Lecture protégée
      V = resultat.tensionRMS;
      nouvelleMesure = 0;
    portEXIT_CRITICAL(&mux);

    tft.setTextColor(TFT_GREEN, TFT_BLACK);
    tft.setCursor(0, 50, 4);
    tft.print("V = ");
    tft.print(V, 1);

    Serial.print("Vrms = ");      // Moniteur série
    Serial.println(V, 2);

  }//end new mesure


  delay(10);
}//en loop

Avec l’utilisation des 2 cores, il est possible de diminuer le temps de la routine d’interruption.

Attention, si on diminue la routine d’interruption, on modifie les fréquences de coupures du filtre passe haut et passe bas.

Un autre avantage d’utilisation du filtre c’est que la racine de la tension avec l’offset retiré ne peut etre négative par rapport au calcul de la somme

Maintenant, il faut faire la communication

Au niveau de la communication de ESP 32 de l’energy metre et commander d’autres ESP32 pour faire du délestage via le wifi ou bluetooth, il y a plusieurs possibilités mais je n’ai pas de recul pour savoir ce qui est le mieux et je ne connais pas les limites des possibilités de chacun tout en restant gratuit.

Le net donne 3 possibilités : ESP32 → Wi-Fi → box → Vlan → INTERNET → PC/téléphone

Qu’est ce qui serait le mieux ?? il va falloir tester

Dans un premier temps, une communication locale sera effectuée et permetra detester la valeur du signal le wifi autour de la box

ESP32 → Wi-Fi → box → INTERNET locale → PC/téléphone

En locale, il faut que le PC ou le telephone ne soit pas loin de la box

Il est possible de se balader avec son ESP32 pour connaitre la valeur du signal dans une maison est de savoir si cela va passer ou pas.

· 30 à -50 dBm : Signal excellent. L'ESP32 est à côté de la box (routeur)

· -60 à -67 dBm : Signal très bon.

· -70 à -75 dBm : Signal correct. La connexion est stable, mais les obstacles commencent à l'atténuer.

· -80 à -85 dBm : Signal faible. Risque de déconnexions fréquentes et de pertes de paquets de données.

· -90 dBm et au-delà : Signal critique. La connexion est instable ou impossible à maintenir (au dela de 10m)

Sinon, il faut un wifi extender pour améliorer la communication

Pour ne pas compliquer le code, voici le programme qui envoie une valeur en temps réel comme la température en page HTML directement sur l’adresse IP que l’ESP32 a envoyé dans la liaison série.

//TTGO wifi server
#include <WiFi.h>
#include <WebServer.h>
#include <TFT_eSPI.h>

const char* ssid = "          ";
const char* password = "            ";

TFT_eSPI tft = TFT_eSPI();

WebServer server(80);  //crée le serveur Web.

float temperature = 23.5;   // à remplacer par votre capteur

void handleRoot()  //Quand la page web PC demande /, exécute la fonction handleRoot(). »
{
String page = "<!DOCTYPE html>";
page += "<html>";
page += "<head>";

page += "<meta charset='UTF-8'>";
page += "<meta name='viewport' content='width=device-width, initial-scale=1.0'>";
page += "<meta http-equiv='refresh' content='4'>";   //refraissement 4s

page += "<title>ESP32 Temperature</title>";  //affiché dans les résultats des moteurs de recherche.
page += "</head>";

page += "<body>";      //ouvre l'ecriture
page += "<h1>ESP32</h1>";
page += "<p>Temperature : " + String(temperature) + " &deg;C</p>";   //<p>  paragraphe
page += "<p>Signal Wi-Fi : " + String(WiFi.RSSI()) + " dBm</p>";

page += "</body>";
page += "</html>";
  server.send(200, "text/html", page);  //requete  ok
}

void setup()
{
  Serial.begin(115200);

  // Ecran
  tft.init();
  tft.setRotation(0);
  tft.fillScreen(TFT_BLACK);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.setTextSize(2);

  tft.setCursor(10, 20);
  tft.println("Connexion WiFi...");

  // Wi-Fi
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }

  Serial.println();
  Serial.println("WiFi connecte !");
  Serial.print("Adresse IP : ");     //indique l'adresse IP  locale de l'ESP32
  Serial.println(WiFi.localIP());    //sinon regarder dans la box


  // Serveur Web
  server.on("/", handleRoot);
  server.begin();

  Serial.println("Serveur Web demarre");
}

void loop()
{
  server.handleClient();  //traiter les demandes reçues.

temperature=temperature+0.1;  //simuulation

  if (WiFi.status() == WL_CONNECTED)
  {
    tft.fillScreen(TFT_BLACK);

    tft.setCursor(0, 20);
    tft.print("dBm: ");
    tft.print(WiFi.RSSI());  //indique la puissance du signal

    tft.setCursor(0, 130);
    tft.print("T: ");
    tft.print(temperature, 1);
    tft.print(" C");
  }

delay (2000);  
}

Voilà ce que donne le script du code précèdent en temps réel

Si l’on désire envoyer une table de valeur glissante des 10 dernières minutes et voir jusque 1 heure en fichier .csv via le wifi.
L’objectif de ce fichier est d’avoir la connaissance de dynamique des données

Voici le code

//TTGO dispaly  wifi server table buffer circulaire   .CSV

#include <WiFi.h>
#include <WebServer.h>
#include <TFT_eSPI.h>

const char* ssid = "      ";    // name box WIFI
const char* password = "            ";  //password

TFT_eSPI tft = TFT_eSPI();
WebServer server(80);

float temperature = 0;   // A remplacer par le vrai capteur

const int TAILLE_TAMPON = 300;   // 300 mesures x 2 secondes = 600 secondes =>  10 minutes

float bufferTemperature[TAILLE_TAMPON];
unsigned long bufferTemps[TAILLE_TAMPON];

int indexActuel = 0;
int nombreValeurs = 0;

unsigned long derniereMesure = 0;
unsigned long tempsEnregistrement = 0;

// Intervalle entre deux mesures
const unsigned long INTERVALLE = 2000;   // 2 secondes

void handleRoot()   // PAGE WEB PRINCIPALE
{
  String page = "";
  page += "<!DOCTYPE html>";
  page += "<html>";
  page += "<head>";

  page += "<meta charset='UTF-8'>";
  page += "<meta name='viewport' content='width=device-width, initial-scale=1.0'>";

  page += "<meta http-equiv='refresh' content='2'>";    // Actualisation de la page toutes les 2 secondes
  page += "<title>ESP32 Temperature</title>";
  page += "<style>";

  page += "body {";
  page += "font-family: Arial;";
  page += "margin: 20px;";
  page += "background-color: #eeeeee;";
  page += "}";

  page += "h1 { color: #333333; }";

  page += ".temperature {";
  page += "font-size: 30px;";
  page += "color: red;";
  page += "font-weight: bold;";
  page += "}";

  page += ".csv {";
  page += "background-color: white;";
  page += "border: 1px solid #999;";
  page += "padding: 10px;";
  page += "font-family: monospace;";
  page += "white-space: pre;";
  page += "overflow-x: auto;";
  page += "height: 400px;";
  page += "overflow-y: scroll;";
  page += "}";

  page += "button {";
  page += "font-size: 18px;";
  page += "padding: 10px 20px;";
  page += "}";

  page += "</style>";
  page += "</head>";
  page += "<body>";
  // ----------------------------------------------------------
  page += "<h1>ESP32</h1>";
  page += "<p>Temperature en temps reel :";
  page += "<div class='temperature'>";
  page += String(temperature, 1);
  page += " &deg;C";
  page += "</div>";

//  page += "<h2>Buffer</h2>";    // INFORMATIONS BUFFER
//  page += "<p>"+= "buffer : " += String(nombreValeurs);+= " / "+= String(TAILLE_TAMPON) "</p>";   // INFORMATIONS BUFFER
/*
  page += "<h2>Donnees CSV</h2>";     // AFFICHAGE CSV

  page += "<div class='csv'>";

  // En-tête CSV
  page += "Temps(s);Temperature(°C)\n";

  if (nombreValeurs > 0)    {int debut;
    if (nombreValeurs < TAILLE_TAMPON)      {  debut = 0;    }
    else     { debut = indexActuel;    }

    // Affichage dans l'ordre chronologique
    for (int i = 0; i < nombreValeurs; i++)
    {
      int position = (debut + i) % TAILLE_TAMPON;

      page += String(bufferTemps[position]);
      page += ";";
      page += String(bufferTemperature[position], 1);

      page += "\n";
    }
  }
  page += "</div>";
*/
  // ----------------------------------------------------------
  page += "<br>";      // BOUTON TELECHARGEMENT

  page += "<a href='/download'>";
  page += "<button>Télécharger le fichier CSV</button>";
  page += "</a>";

  page += "</body>";
  page += "</html>";

  server.send(200, "text/html", page);
}

// ============================================================
void handleDownload()     // CREATION ET TELECHARGEMENT DU CSV
{
  String csv = "";
  csv += "Temps(s);Temperature(°C)\r\n";

  // ORDRE CHRONOLOGIQUE

  if (nombreValeurs > 0)    { int debut;

    if (nombreValeurs < TAILLE_TAMPON)   { debut = 0; }   else    { debut = indexActuel;   }

    for (int i = 0; i < nombreValeurs; i++)
    {
      int position = (debut + i) % TAILLE_TAMPON;

      csv += String(bufferTemps[position]);
      csv += ";";
      csv += String(bufferTemperature[position], 1);
      csv += "\r\n";
    }
  }

  // ----------------------------------------------------------
  server.sendHeader(         // ENVOI DU FICHIER
    "Content-Disposition",
    "attachment; filename=temperature.csv");

  server.send( 200, "text/csv", csv );
}


// ============================================================
void setup()   {
  Serial.begin(115200);

  tft.init();
  tft.setRotation(0);
  tft.fillScreen(TFT_BLACK);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.setTextSize(2);
  tft.setCursor(10, 20);

  tft.println("Connexion WiFi...");

  WiFi.mode(WIFI_STA);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED)    { delay(500);    Serial.print(".");  }
  Serial.println();
  Serial.println("WiFi connecte !");
  Serial.print("Adresse IP : ");

  Serial.println(WiFi.localIP());

  server.on("/", handleRoot);     // ENVOI DU FICHIER
  server.on("/download", handleDownload);
  server.begin();
  Serial.println("Serveur Web demarre");

  derniereMesure = millis();      // INITIALISATION TEMPS
  tempsEnregistrement = 0;
}  //fin set up

// ============================================================
void loop()   {

  server.handleClient();     // SERVEUR WEB

  unsigned long maintenant = millis();      // NOUVELLE MESURE TOUTES LES 2 SECONDES
  if (maintenant - derniereMesure >= INTERVALLE)     { derniereMesure = maintenant;

    temperature = temperature + 0.1;       // SIMULATION de data TEMPERATURE
    bufferTemps[indexActuel] = tempsEnregistrement;       // ENREGISTREMENT DU TEMPS
    bufferTemperature[indexActuel] = temperature;
    tempsEnregistrement += 2;        // INCREMENT DU TEMPS

    indexActuel++;  
    if (indexActuel >= TAILLE_TAMPON)    {indexActuel = 0;    }
    if (nombreValeurs < TAILLE_TAMPON)   {nombreValeurs++;    }

      tft.fillScreen(TFT_BLACK);
      tft.setCursor(0, 20);
      tft.setTextSize(2);
      tft.print("T:");
      tft.print(temperature, 1);
      tft.print(" C");
    
  }  //fin intervalle
}  //End loop

Le fichier s’enregistre directement dans le dossier download

Si l’on desire passer via la 4G, il faudra utiliser MQTT :
│ESP32 IP : 192.168.1.34│ → publish → │Wi-Fi TCP / réseau IP box│ → Broker MQTT mosquito → subscribe (ecoute) → │4G téléphone Home Assistant│

Mais d’abord, on va rajouter dans le code du wifi local, la mesure de la tension et du courant pour avoir la puissance active, et le facteur de puissance

J’ai demandé à une IA de fusionner le programme du wifi local et le programme 2 core avec toutes les mesures (tension et courant, puissance et facteur de puissance)
L’IA donne meme l’algo


Par contre, l’IA rajoute des trucs qu’on ne lui demande pas.

Par exemple des lignes de debug, des decalages dans l’ecran, mais des explications.
Je n’aime pas sa façon d’ecrire.
Mais elle a fait le taf

Voici le programme fusionné

//TTGO dispaly  wifi server table buffer circulaire   .CSV

// ================================================================
// ESP32 TTGO - DOUBLE CORE
//
// CORE 0 :
//   - Acquisition tension/courant
//   - Echantillonnage toutes les 800 us
//   - Filtre passe-haut
//   - Calcul RMS filtré
//   - Calcul puissance active filtrée
//
// CORE 1 :
//   - Ecran TFT
//   - Serveur Web WiFi
//   - Enregistrement d'une mesure toutes les 2 secondes
//   - Buffer circulaire de 10 minutes
//   - Téléchargement CSV
//
// 300 mesures x 2 secondes = 600 secondes = 10 minutes
//
// CSV :
// Temps(s);Tension_RMS(V);Courant_RMS(A);Puissance_Active(W);FP
// ================================================================

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <TFT_eSPI.h>


const char* ssid     = "Livebox-6D02_EXT";       // NOM WIFI
const char* password = "115C1FFE35545AD1593EFE15C9"; // MOT DE PASSE

WebServer server(80);

TFT_eSPI tft = TFT_eSPI();


#define PIN_ZMPT       32       // Capteur tension
#define PIN_intensity  33       // SCT-013-050
#define PIN_LED        25       // Test acquisition


#define N_ECHANTILLONS 2500   // PARAMETRES ACQUISITION  
#define PERIODE_US     800    // 2500 x 800 us = 2 secondes

// 2500 x 800 us = 2 secondes
//
// Une mesure complète est donc calculée toutes les 2 secondes.

#define calibreV  0.5f    // CALIBRATION
#define calibreI  0.019f


const int TAILLE_TAMPON = 3600;   //1heure/1800

// 300 mesures x 2 secondes = 600 secondes = 10 minutes
float bufferTension[TAILLE_TAMPON];
float bufferCourant[TAILLE_TAMPON];
float bufferPuissance[TAILLE_TAMPON];
float bufferFP[TAILLE_TAMPON];

unsigned long bufferTemps[TAILLE_TAMPON];

int indexActuel = 0;
int nombreValeurs = 0;

unsigned long tempsEnregistrement = 0;

hw_timer_t *timer = NULL;
TaskHandle_t mesureTaskHandle = NULL;


struct Mesure   // STRUCTURE DES DONNEES PARTAGEES CORE 0 -> CORE 1
{
  float tensionRMS;
  float currentRMS;
  float Power;
  float FP;
};

volatile Mesure resultat;
volatile bool nouvelleMesure = false;

// Protection des données partagées
portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;

// ================================================================
// INTERRUPTION TIMER
// ================================================================

void IRAM_ATTR timerISR()
{
  BaseType_t xHigherPriorityTaskWoken = pdFALSE;

  // Réveille la tâche de mesure sur Core 0
  vTaskNotifyGiveFromISR(
    mesureTaskHandle,
    &xHigherPriorityTaskWoken
  );

  if (xHigherPriorityTaskWoken)
  {
    portYIELD_FROM_ISR();
  }
}

// ================================================================
// CORE 0 : TACHE DE MESURE
// ================================================================

void mesureCore0(void *parameter)
{

  const float alphaPB = 0.0016f;     // FILTRES
  const float alphaHP = 0.9901f;

  // --------------------------------------------------------------
  // TENSION
  // --------------------------------------------------------------

  float valeurADC  = 0.0f;
  float valeurADCn = 0.0f;

  float signalV  = 0.0f;
  float signalVn = 0.0f;

  float rmsV  = 0.0f;
  float rmsVn = 0.0f;

  float tensionRMS = 0.0f;

  // --------------------------------------------------------------
  // COURANT
  // --------------------------------------------------------------

  float valeurI  = 0.0f;
  float valeurIn = 0.0f;

  float signalI  = 0.0f;
  float signalIn = 0.0f;

  float rmsI  = 0.0f;
  float rmsIn = 0.0f;

  float currentRMS = 0.0f;

  // --------------------------------------------------------------
  // PUISSANCE
  // --------------------------------------------------------------

  float Powerinstant = 0.0f;

  float Powerf = 0.0f;
  float Powern = 0.0f;

  float Power = 0.0f;

  float S  = 0.0f;
  float FP = 0.0f;

  uint16_t echantillon = 0;      // COMPTEUR ECHANTILLONS

  // ==============================================================
  // BOUCLE DE MESURE
  // ==============================================================

  while (true)     {      // Attente de l'interruption timer

    ulTaskNotifyTake( pdTRUE,  portMAX_DELAY  );


 //   digitalWrite(PIN_LED, !digitalRead(PIN_LED));   // LED de test

    // ============================================================
    // MESURE TENSION
    // ============================================================
    valeurADC = analogRead(PIN_ZMPT);

    // Filtre passe-haut
    signalV =
      alphaHP *
      (signalVn + valeurADC - valeurADCn);

    signalVn   = signalV;
    valeurADCn = valeurADC;

    // RMS filtré
    rmsV =
      rmsVn +
      alphaPB *
      (signalV * signalV - rmsVn);

    rmsVn = rmsV;

    tensionRMS = sqrt(rmsV) * calibreV;

    // MESURE COURANT
    // ============================================================

    valeurI = analogRead(PIN_intensity);

    // Filtre passe-haut
    signalI =
      alphaHP *
      (signalIn + valeurI - valeurIn);

    signalIn = signalI;
    valeurIn = valeurI;

    // RMS filtré
    rmsI =
      rmsIn +
      alphaPB *
      (signalI * signalI - rmsIn);

    rmsIn = rmsI;

    currentRMS = sqrt(rmsI) * calibreI;

    Powerinstant = signalV * signalI;          // PUISSANCE ACTIVE

    // Filtrage passe-bas de la puissance instantanée
    Powerf =
      Powern +
      alphaPB *
      (Powerinstant - Powern);

    Powern = Powerf;
    Power = Powerf * calibreV * calibreI;

    echantillon++;       // COMPTEUR

    // ============================================================
    // FIN DES 2500 ECHANTILLONS    2500 x 800 us = 2 secondes
    // ============================================================

    if (echantillon >= N_ECHANTILLONS)     {

      S = tensionRMS * currentRMS;

      if (S > 0.05f)   { FP = Power / S;         // Facteur de puissance
        if (FP > 1.0f)   FP = 1.0f;
        if (FP < -1.0f)  FP = -1.0f;
                      }   else   { FP = 0.0f;   }



      portENTER_CRITICAL(&mux);    // ENVOI CORE 0 -> CORE 1

      resultat.tensionRMS  = tensionRMS;
      resultat.currentRMS  = currentRMS;
      resultat.Power       = Power;
      resultat.FP          = FP;

      nouvelleMesure = true;

      portEXIT_CRITICAL(&mux);

      echantillon = 0;   // Remise à zéro du compteur
    }
  }
}

// ================================================================

void handleRoot() {  // PAGE WEB PRINCIPALE

 float V;
  float I;
  float P;
  float FP;
  float tempsbuffer;

  portENTER_CRITICAL(&mux);

  V  = resultat.tensionRMS;
  I  = resultat.currentRMS;
  P  = resultat.Power;
  FP = resultat.FP;

  portEXIT_CRITICAL(&mux);


  // PAGE HTML
  // --------------------------------------------------------------
unsigned long debut = millis();  
  String page = "";

  page += "<!DOCTYPE html>";
  page += "<html>";
  page += "<head>";

  page += "<meta charset='UTF-8'>";
  page += "<meta name='viewport' content='width=device-width, initial-scale=1.0'>";

  // Actualisation toutes les 2 secondes
  page += "<meta http-equiv='refresh' content='2'>";

  page += "<title>ESP32 - Mesure électrique</title>";

  page += "<style>";

  page += "body {";
  page += "font-family: Arial;";
  page += "margin: 20px;";
  page += "background-color: #eeeeee;";
  page += "}";

  page += "h1 {";
  page += "color: #333333;";
  page += "}";

  page += ".mesure {";
  page += "background-color: white;";
  page += "border-radius: 10px;";
  page += "padding: 15px;";
  page += "margin-bottom: 10px;";
  page += "font-size: 25px;";
  page += "font-weight: bold;";
  page += "}";

  page += ".tension { color: green; }";
  page += ".courant { color: red; }";
  page += ".puissance { color: blue; }";
  page += ".fp { color: purple; }";

  page += "button {";
  page += "font-size: 18px;";
  page += "padding: 10px 20px;";
  page += "}";

  page += "</style>";

  page += "</head>";

  page += "<body>";

  // --------------------------------------------------------------
  // TITRE
  // --------------------------------------------------------------

  page += "<h1>ESP32 - Mesure électrique</h1>";

  // --------------------------------------------------------------
  // TENSION
  // --------------------------------------------------------------

  page += "<div class='mesure tension'>";
  page += "Tension RMS : ";
  page += String(V, 1);
  page += " V";
  page += "</div>";

  // --------------------------------------------------------------
  // COURANT
  // --------------------------------------------------------------

  page += "<div class='mesure courant'>";
  page += "Courant RMS : ";
  page += String(I, 3);
  page += " A";
  page += "</div>";

  // --------------------------------------------------------------
  // PUISSANCE
  // --------------------------------------------------------------

  page += "<div class='mesure puissance'>";
  page += "Puissance active : ";
  page += String(P, 1);
  page += " W";
  page += "</div>";

  // --------------------------------------------------------------
  // FACTEUR DE PUISSANCE
  // --------------------------------------------------------------

  page += "<div class='mesure fp'>";
  page += "Facteur de puissance : ";
  page += String(FP, 2);
  page += "</div>";

  // --------------------------------------------------------------
  // INFORMATIONS BUFFER
  // --------------------------------------------------------------
  page += "<p>";
  page += "Mesures enregistrees : ";
  page += String(nombreValeurs);
  page += " / ";
  page += String(TAILLE_TAMPON);
  page += "</p>";

  page += "<p>";
  page += "Duree maximale du fichier : 2 heures";   //   TAILLE_TAMPON*2s/3600
  page += "</p>";

  // --------------------------------------------------------------
  // BOUTON CSV
  // --------------------------------------------------------------

  page += "<br>";

  page += "<a href='/download'>";
  page += "<button>";
  page += "Telecharger le fichier CSV";
  page += "</button>";
  page += "</a>";

  page += "</body>";
  page += "</html>";

  server.send( 200, "text/html",  page  );

unsigned long duree = millis() - debut;

  Serial.print("Duree requete (ms) : ");
  Serial.println(duree);                    //5ms

}

// ================================================================
// TELECHARGEMENT CSV
// ================================================================

void handleDownload()  {
  String csv = "";

  csv += "Temps(s);Tension_RMS(V);Courant_RMS(A);Puissance_Active(W);FP\r\n";     // EN-TETE CSV


  if (nombreValeurs > 0)   {      // ORDRE CHRONOLOGIQUE
    int debut;

    if (nombreValeurs < TAILLE_TAMPON)   { debut = 0;  }
    else   { debut = indexActuel;   }

    // Parcours des données
    // ------------------------------------------------------------
    for (int i = 0; i < nombreValeurs; i++)   {
      int position =
        (debut + i) % TAILLE_TAMPON;

      csv += String(bufferTemps[position]); csv += ";";

      csv += String(bufferTension[position], 2);  csv += ";";

      csv += String(bufferCourant[position], 3);  csv += ";";

      csv += String(bufferPuissance[position], 2);  csv += ";";

      csv += String(bufferFP[position], 3);

      csv += "\r\n";
    }
  }   

  // NOM DU FICHIER
  // --------------------------------------------------------------
  server.sendHeader(  "Content-Disposition",  "attachment; filename=mesures_2H.csv"  );

  server.send(  200,  "text/csv", csv  );
}  //end handleDownload()


// ================================================================
void setup()
{
  Serial.begin(115200);

  analogReadResolution(12);
  analogSetAttenuation(ADC_11db);

  pinMode(PIN_ZMPT, INPUT);
  pinMode(PIN_intensity, INPUT);

  pinMode(PIN_LED, OUTPUT);
  digitalWrite(PIN_LED, LOW);

  tft.init();
  tft.setRotation(0);

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid,  password  );
  while (WiFi.status() != WL_CONNECTED)   { delay(500);  Serial.print(".");  }

  server.on( "/", handleRoot  );     // SERVEUR WEB
  server.on("/download",  handleDownload );
  server.begin();

  Serial.println("Serveur Web demarre");

  resultat.tensionRMS = 0;
  resultat.currentRMS = 0;
  resultat.Power      = 0;
  resultat.FP         = 0;

  nouvelleMesure = false;

  indexActuel = 0;
  nombreValeurs = 0;

  tempsEnregistrement = 0;

  // CREATION TACHE MESURE SUR CORE 0
  // ==============================================================

  xTaskCreatePinnedToCore(
    mesureCore0,          // fonction
    "MESURE",             // nom
    4096,                 // stack
    NULL,                 // parametre
    2,                    // priorité
    &mesureTaskHandle,    // handle
    0                     // CORE 0
  );

  uint8_t timer_id = 1;      // // 1 tick = 1 us
  uint16_t prescaler = 80;   //80 MHz / 80 = 1 MHz

  timer = timerBegin( timer_id, prescaler,  true  );
  timerAttachInterrupt( timer, &timerISR, true );
  timerAlarmWrite(  timer, PERIODE_US, true  );
  timerAlarmEnable(timer);


  tft.fillScreen(TFT_BLACK);
  tft.setCursor(0, 0, 2);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.println("MESURE ELECTRIQUE");

  Serial.println("Systeme demarre");
}


// ================================================================
float V  = 0;   // VARIABLES CORE 1
float I  = 0;
float P  = 0;
float FP = 0;
float E, joule;


void loop()   {    // LOOP CORE 1


  server.handleClient();     // SERVEUR WEB   1ms   

  if (nouvelleMesure == true)    {
 //   digitalWrite(25,1);      //durée 5ms
    portENTER_CRITICAL(&mux);      // Lecture protégée des données
    V  = resultat.tensionRMS;
    I  = resultat.currentRMS;
    P  = resultat.Power;
    FP = resultat.FP;
    nouvelleMesure = false;
    portEXIT_CRITICAL(&mux);

      // ENREGISTREMENT DANS LE BUFFER CIRCULAIRE
    // ============================================================

    bufferTemps[indexActuel] =tempsEnregistrement;
    bufferTension[indexActuel] = V;
    bufferCourant[indexActuel] = I;
    bufferPuissance[indexActuel] = P;
    bufferFP[indexActuel] =   FP;

    tempsEnregistrement += 2;
    indexActuel++;

    if (indexActuel >= TAILLE_TAMPON)   { indexActuel = 0;    }       // Avancement buffer circulaire
    if (nombreValeurs < TAILLE_TAMPON)    {nombreValeurs++;   }      // Nombre de valeurs enregistrées

    tft.setCursor(0, 25, 4);

    tft.setTextColor( TFT_GREEN,  TFT_BLACK );
    tft.print("V: ");        // TENSION
    tft.print(V, 1);
    tft.print(" V");

    tft.setCursor(0, 55, 4);
    tft.setTextColor( TFT_RED,  TFT_BLACK );
    tft.print("I: ");
    tft.print(I, 2);       // COURANT
    tft.print(" A ");

    tft.setCursor(0, 85, 4);
    tft.setTextColor( TFT_BLUE, TFT_BLACK );
    tft.print("P:");
    tft.print(P, 0);        // PUISSANCE ACTIVE
    tft.print("   ");

    tft.setCursor(0, 115, 4);
    tft.setTextColor( TFT_WHITE,  TFT_BLACK   );
    tft.print("FP:");       // FACTEUR DE PUISSANCE
    tft.print(FP, 2);
    tft.print("  ");

    joule=joule+P;   //
E=(joule*2)/3600;   //wh

    tft.setCursor(0, 145, 4);
    tft.setTextColor( TFT_WHITE,  TFT_BLACK   );
    tft.print("E:");       // FACTEUR DE PUISSANCE
    tft.print(E,0);
   tft.print("  ");

//  digitalWrite(25,0); 
  }

  delay(5);     // Petite pause pour laisser le serveur WiFi respirer
}

Avec les cinq tableaux de 300 valeurs de float 4 octet (10minutes), le compilateur indique que 15% de la mémoire SRAM de 520k de l’ESP32 est utilisé et que Le programme prend 62%.

Avec 1800 valeurs de buffer (1 heure) , (1800*5*4oxter=36koctet), le compilateur indique que la SRAM utilise 24%

Avec 3600 valeurs de buffer (2 heures) , la compilation indique que la SRAM utilise 35%

Avec 5400 valeurs de buffer, le compilateur ne veut plus transmettre le programme à cause du string+csv qui doit prendre pas mal de RAM


Pour faire une étude énergétique poussée et de sauvegarder les données du fichier .CSV automatiquement toutes les 2 heures.
Il est possible de changer le script HTML de l’ESP32 (quand le buffer est plein) vers Google Drive/cloud, par MQTT, par e-mail sans avoir à appuyer sur « Télécharger »

J’ai fait des tests pour faire une comparaison avec du matériel (relais, compteur, disjoncteur numérique) existant que l’on m’a prêté….

Le KWS-303WF ne donne pas la puissance injectée car elle n’est pas bidirectionnelle sur cet appareil !
Il y a une application sur smartphone du KWS-303WF qui trace la puissance et le facteur de puissance mais toutes les heures donc ne permet pas d’étudier un réseau électrique (consommation) ni faire du délestage.
Ce n’est pas pertinent d’avoir un graphique avec une mesure de puissance toutes les heures !
Le taux de rafraîchissement des données de l’application par le wifi est de 1 minute minimum.
Mais, avec l’application, on peut couper ce qu’il alimente manuellement.
J’ai un doute sur la mesure du facteur de puissance qui est trop volatile.

Je n’ai pas essayé la version KWS-303 RS485


Dans le même genre, j’ai testé le TOMPD-63LW qui lui est bidirectionnel et indique la puissance négative sur l’écran, mais pas sur l’application ????
il mesure le courant différentiel en permanence et on peut savoir quel système a un défaut d’isolement.
Le rafraichissement des données par le wifi de l’application est toutes les 2s.

Impossible de récupérer les données via l’adresse IP en wifi local via la plateforme Tuya car le Vlan est bloqué et il faut obligatoirement passer par le cloud tuya payant.

Sur la plateform tuya avec TOMPD-63LW, la data de la puissance n’est pas négative non plus ????

Sur le net, on peut observer que les anciennes versions TOMPD-63LW était plus libre mais s’il faut changer le firmware, c’est pénible. Et certains ont essayé sans succès ou pour avoir une écriture en mandarin ou un ecran tout noir, ce n’est pas gagné.

Etant donné que la puissance négative n’est pas prise en compte, l’energie consommée totale ne prend pas en compte la réinjection dans l’application.
Lorsqu’il y a réinjection, la tension réseau augmente légèrement de 1V par rapport à la chute de la tension dans les câbles.

Des systèmes domotiques existent tel que Shelly Pro 3EM 120A – Smart Meter (130€) via des contacteurs modulaires (50€), il va commander des appareils en tout ou rien. Sachant que la priorité du délestage se ferait avec l’application android (Shelly Smart Control) mais cela n’est pas bien expliqué ????

Il existe plusieurs architectures de l’utilisation des panneaux photovoltaïque avec batterie de stockage, mais là plus utilisé est la suivante à cause que si la batterie de stockage est pleine le surplus ira dans le réseau

Panneaux PV → onduleur hybride ↔ batterie stockage et maison ↔ réseau

Mais cela peut se compliquer à donner des priorités en fonction de metéo futur…

Panneaux PV → mesure du surplus (energy meter) → contacteur (pompe arrosage) → contacteurs (energy meter) → (chargeur vélo elec, batterie de stockage, contacteur ballon d’eau chaude,….)

Bref, pas facile de gérer l’énergie et faire du délestage autoconsommation d’énergie photovoltaïque ou éolien, les systèmes deviennent très vite propriétaire, les manuels de ces appareils n’explique pas tout donc ce n’est pas la « smartlife ».