ESP32で電力計を作りBlynkとAmbientで表示する

published_with_changes 更新日: event_note 公開日:

labelBlynk labelESP32 labelIoT label電力計

前報前々報からの続きで、電力計として仕上げます。電流センサは分電盤内の単相三線式100V配線のL1相、L2相を測ります。電圧センサのトランスはACアダプタなので100Vコンセントに差し込みます。

表示はBlynkを使います。将来は常時表示を思っています。データはAmbientへ送り蓄積します。


構成

ハード

  • マイコン:ESP32 Devkit V1
  • CTセンサ:YHDC SCT-013-050 ; 2個 割れたコアは瞬間接着剤、セロテープ、ホットボンドで固めました。
  • 電圧センサ:ACアダプター改造;Sony DC adapter output 4,5V 500mA
  • AC/DCアダプター:5V 1A出力;iphone用
  • 回路図;周波数検出の信号は20kΩから、4.7kΩに変更しました。電流高負荷時に矩形波整形がうまくいかない問題がでて、その対策です。

手前の32,33と書いたのがCTセンサ入力のステレオピンジャックで、その左のDCジャックが電圧の入力です。


ソフト(スケッチ)

先人の方々がブログに載せられているスケッチを活用させていただきました。

電流、電圧は1サイクルを100分割して、1サイクルあたりの2乗平均を求めています。Blynkへは2秒ごとに平均値を渡し、Ambientへは送信間隔30秒間の最大値を渡しています。

AC電源周波数は、attachInterrupt()で波形がHighからLowへ切り換わるタイミングで割り込みを入れて指定個数(50か60サイクル)の波形をカウントし、それに要する時間をmicros()関数で求め、電源周波数に同期する形で周波数に変換しています。Blynkへは2秒ごとに平均値を渡し、Ambientへは送信間隔30秒間の最大値と最小値を渡しています。

各々の測定に値は合わせ込みのため、実験的に求めた補正係数をかけています。周波数は基準はオシロの読み値にしました。補正係数を掛けないと2%程度高めになります。途中の値やサイクル数などを細かくチェックしたのですが、まちがいはなく原因はわかりませんでした。micros()関数の精度なのでしょうか?

しばらく使っていたのですが、27日くらいでwifiが切れて繋がらない現象が起きました。正確に経過時間(日数)を測定した訳ではないのですが、ESP32の起動後27日くらいで何度かwifiが繋がらなくなりました。その対策として、wifiが繋がらないとESP32をリセットするようにスケッチを変更しました。(上記のスケッチをそのまま入れ込みました。)

つなげて作ったスクリプトをしまします。約500行あるので非表示にしています。クリックすると見えます。

/*
"なんでも作っちゃう、かも。Arduino電力計 - 回路図とスケッチ"から引用
http://arms22.blog91.fc2.com/

簡単に処理の流れを解説します。まずsetupでいくつかの変数を初期化。
loopで1秒間に数十回calcWattをコール、1秒間の電力の平均を求め、シリアルで出力します。

電圧・電流のサンプリング、電力の計算はcalcWattで行います。
calcWattは交流電源の1サイクル分(関西だと60Hz、関東だと50Hz)の電圧・電流をサンプリングし、
サンプリングした値から1サイクル分の電圧・電流、電力を計算しています。
計算した値はVrms変数、Arms変数、Watt変数に格納します。

電圧はanalogReadで読み取った値をVに変換し、実験で求めた係数(87.5734677)を掛けて求めます。
電流は次の式で求められます。Io = Eo / (K * RL / n)


 ESP32で商用電源周波数をAmbientに書き込み File:20210303AcFreqLogToAmbient.ino
 2021/03/03 ラジオペンチ http://radiopench.blog96.fc2.com/
 (セマフォやミューテックスを使った排他制御を行っていないので問題が出るかも?)
*/

// <<完成型>>
// 電圧1ch, 電流 2ch
// Blynk 2秒間隔で送信
// Ambient 30秒間の最大値を30秒間隔で送信

//for Blynk
/* Comment this out to disable prints and save space */
#define BLYNK_PRINT Serial

#include <WiFi.h>
#include <WiFiClient.h>
#include <BlynkSimpleEsp32.h>

// You should get Auth Token in the Blynk App.
// Go to the Project Settings (nut icon).
char auth[] = "**********BLYNK auth************_";

// Your WiFi credentials.
// Set password to "" for open networks.
char ssid[] = "***your id***";
char password[] = "***your pass***";
char server[] = "192.168.**.**";  // IP for your Local Server
int port = 8080;

BlynkTimer timer;// Create a Timer object called "timer"

// for ambient
#include "Ambient.h"

unsigned int channelId = *****;
const char* writeKey = "****writeKey****";

WiFiClient client;
Ambient ambient;

// 端子定義
// ESP32のピン番号は、基板に書かれているIOxxのxx 他のArduino互換機と違うので注意!!
const int arefPin = 35;       // 仮想GND
const int voltagePin = 34;    // AC Voltage
const int freqPin = 36;       // AC Freqency
const int currentPin_A = 32;  // CT_A
const int currentPin_B = 33;  // CT_B

// 商用電源周波数
#define AC_FREQ        (60)

// 1サイクルあたりのサンプル数
#define NUMBER_OF_SAMPLES (100)

// サンプリング間隔(マイクロ秒)
#define SAMPLING_INTERVAL   (1000000/(AC_FREQ * NUMBER_OF_SAMPLES))

//シリアルとBlynkに出力する間隔(秒)
#define OUTPUT_INTERVAL (2)

//Ambientに出力する間隔(秒)
#define AMBIENT_INTERVAL (30)

// デバッグ用
#define DEBUG 0

// 実効電圧、実効電流、有効電力
float Vrms;
float Irms_A;
float Irms_B;
float Watt;

// サンプリング用バッファ数
int VASamples[NUMBER_OF_SAMPLES*6];


#define KV    (1.414*102.2/1.032/2.2447/1000*0.971) // 電圧換算係数数*実験的補正係数
#define KC_A  (1.414*2.2447*13.823 / 0.986) // 電流換算係数/実験的補正係数
#define KC_B  (1.414*2.2447*13.823 /1.14) // 電流換算係数/実験的補正係数
#define Def_A  0.285 // 実測にもとづくY軸切片 (A)
#define Def_B  0.303 // 実測にもとづくY軸切片 (A)
#define K_Freq       0.977    // 周波数補正係数 (デフォルトは1.0)


unsigned long t1,t2;
int i,r,v1,a1,a2,b1,b2,v2;

float watt_hour;
float vrms_sum;
float Irms_A_sum;
float Irms_B_sum;
float app_Power;
float watt_sum;
float Power_factor;

int num_samples;
unsigned long last_update;
boolean Bflag; // Ambient送信後、最大、最小、現在のAC周波数が揃ってしまうので一回目読み飛ばす

int logCount = 0;              // 1秒間に50(60)サイクルをひとかたまりではlogCount = 1
volatile uint32_t t_freq;      // 50(60)サイクル分の周期(us単位、標準1000000us)
volatile boolean flag;
 
float fCul;                // 周波数の1秒平均値
float fMax = 0.0;          // 最大周波数
float fMin = 1000.0;       // 最小周波数
float fAve;                // 平均周波数
float fSum = 0.0;

//ambientに送る30秒区間での最大値
float vrms_max;
float Irms_A_max;
float Irms_B_max;
float Irms_AB_max;
float app_Power_max;
float watt_max;
float fSum_max = 0.0;
float fSum_min = 1000.0;


void IRAM_ATTR acIrq() {   // ピン割込み:電源ピンがHIGHからLOWになる(1サイクル)毎に割込み
  static uint16_t n;
  static uint32_t lastMicros = 0;
  uint32_t x;
  n++;                          // サイクルカウンタの値を1増やす
  if (n >= AC_FREQ) {           // 指定回数(50 or 60)なら
    x = micros();               // 現在の時刻を記録
    t_freq = x - lastMicros;    // 経過時間を計算(この値はほぼ100000になる);指定回数(50or60cycles)にかかった時間

    lastMicros = x;
    n = 0;
    flag = true;                // 指定回数(1秒)毎にvoid mesureAC_FREQ()に吐き出すためにフラグをセット
  }
}

void mesureAC_FREQ(void) {
  if (flag) {                                   // flag = trueで指定回数(50 or 60サイクル)経過でデーターが準備できているならば
    fCul = K_Freq * 1.0E6 * AC_FREQ / t_freq;   // 1秒間の平均周波数を計算
    logCount++;                                 // サイクルカウンタの値を1増やす
    if (fCul < fMin) fMin = fCul;
    if (fCul > fMax) fMax = fCul;
    fSum += fCul;
  }
  flag = false;
}

void calcWatt(void) //NUMBER_OF_SAMPLES(100)個のデータの2乗平均(rms)
{
  t1 = micros();

  // 1サイクル分のAD値をサンプリング
  for(i=0; i<NUMBER_OF_SAMPLES; i++){

    r = analogRead(arefPin);
    v1 = analogRead(voltagePin);
    a1 = analogRead(currentPin_A);
    a2 = analogRead(currentPin_A);
    b1 = analogRead(currentPin_B);
    b2 = analogRead(currentPin_B);
    v2 = analogRead(voltagePin);

    VASamples[(i*6)+0] = v1 - r;
    VASamples[(i*6)+1] = a1 - r;
    VASamples[(i*6)+2] = a2 - r;
    VASamples[(i*6)+3] = v2 - r;
    VASamples[(i*6)+4] = b1 - r;
    VASamples[(i*6)+5] = b2 - r;

    do {
      t2 = micros();
    } 
    while((t2 - t1) < SAMPLING_INTERVAL);//サンプリング間隔(167マイクロ秒)の間は待つ
    t1 += SAMPLING_INTERVAL;

  }


    // 1サイクル分の電圧と電流、電力を加算
    Vrms    = 0;
    Irms_A  = 0;
    Irms_B  = 0;
    Watt    = 0;

  for(i=0; i<NUMBER_OF_SAMPLES; i++){
    v1 = VASamples[(i*6)+0];
    a1 = VASamples[(i*6)+1];
    a2 = VASamples[(i*6)+2];
    v2 = VASamples[(i*6)+3];
    b1 = VASamples[(i*6)+4];
    b2 = VASamples[(i*6)+5];

    float vv = (v1+v2)/2 * KV;
    float aa = (a1+a2)/2 / KC_A;
    float bb = (b1+b2)/2 / KC_B;

    Vrms    += vv * vv; // Vrms = Vrms + vv * vv;
    Irms_A  += aa * aa;
    Irms_B  += bb * bb;
    Watt    += vv * ( aa + bb );
  }

  // 2乗平均平方根(rms)を求める
  Vrms    = sqrt(Vrms / NUMBER_OF_SAMPLES);
  Irms_A  = sqrt(Irms_A / NUMBER_OF_SAMPLES) ;if (Irms_A < Def_A) Irms_A = 0;
  Irms_B  = sqrt(Irms_B / NUMBER_OF_SAMPLES) ;if (Irms_B < Def_B) Irms_B = 0;

  // 平均電力を求める
  Watt = Watt / NUMBER_OF_SAMPLES;
}

// for Blynk
// This function sends Arduino's up time every second to Virtual Pin().
// In the app, Widget's reading frequency should be set to PUSH. This means
// that you define how often to send data to Blynk App.

void myTimerEvent()
{
  unsigned long curr_time;

  // You can send any value at any time.
  // Please don't send more that 10 values per second.
  // 1秒あたり10を超える値を送信しないでください

  // timer.setIntervalで設定した秒数(OUTPUT_INTERVAL)経過したらシリアルとBlynkに出力

    //設定秒数(OUTPUT_INTERVAL)の間に積算したデータをサンプル数で割り平均を出す
    vrms_sum    /= num_samples; // vrms_sum = vrms_sum/num_samples
    Irms_A_sum  /= num_samples;
    Irms_B_sum  /= num_samples;
    watt_sum    /= num_samples;
    fSum        /= logCount;

    app_Power     = vrms_sum * ( Irms_A_sum + Irms_B_sum );//皮相電力 VA
    Power_factor  = watt_sum / app_Power *100;// 力率 % = 有効電力 / 皮相電力

#if DEBUG
    for(int i=0; i<NUMBER_OF_SAMPLES; i++){
      Serial.print(num_samples);
      Serial.print('\t');
      Serial.print(VASamples[(i*6)+0]); //v1
      Serial.print('\t');
      Serial.print(VASamples[(i*6)+1]); //a1
      Serial.print('\t');
      Serial.println(VASamples[(i*6)+4]); //b1
      Serial.print(num_samples);
      Serial.print('\t');
      Serial.print(VASamples[(i*6)+3]); //v2
      Serial.print('\t');
      Serial.print(VASamples[(i*6)+2]); //a2
      Serial.print('\t');
      Serial.println(VASamples[(i*6)+5]); //b2
    }
#endif

      Blynk.virtualWrite(V0, vrms_sum);//電圧 V
      Blynk.virtualWrite(V1, Irms_A_sum + Irms_B_sum);//CT_A + CT_B電流 A
      Blynk.virtualWrite(V2, Irms_A_sum);//CT_A電流 A
      Blynk.virtualWrite(V3, Irms_B_sum);//CT_B電流 A
      Blynk.virtualWrite(V4, app_Power);//皮相電力 VA
      Blynk.virtualWrite(V5, watt_sum);//電力 W
      Blynk.virtualWrite(V6, Power_factor);// 力率 % = 有効電力 / 皮相電力
      Blynk.virtualWrite(V7, watt_hour);// 積算 Wh
      Blynk.virtualWrite(V8, fSum);// AC周波数 Hz

      //ambientに送る30秒区間での最大値を記録する
      if(vrms_max < vrms_sum){vrms_max = vrms_sum;}
      if(Irms_A_max < Irms_A_sum){Irms_A_max = Irms_A_sum;}
      if(Irms_B_max < Irms_B_sum){Irms_B_max = Irms_B_sum;}
      if(Irms_AB_max < Irms_A_sum + Irms_B_sum){Irms_AB_max = Irms_A_sum + Irms_B_sum;}
      if(app_Power_max < app_Power){app_Power_max = app_Power;}
      if(watt_max < watt_sum){watt_max = watt_sum;}
      if(fSum_max < fSum){fSum_max = fSum;}
      if(fSum_min > fSum){fSum_min = fSum;}
      
    if (Bflag) {
      Serial.print(vrms_sum);
      Serial.print("V, ");

      Serial.print(Irms_A_sum + Irms_B_sum);
      Serial.print("A, ");

      Serial.print(Irms_A_sum);
      Serial.print("A, ");

      Serial.print(Irms_B_sum);
      Serial.print("A, ");

      Serial.print(app_Power);
      Serial.print("VA, ");

      Serial.print(watt_sum);
      Serial.print("W, ");

      // AC周波数
      Serial.print(fSum);
      Serial.print("Hz, ");

      // min AC周波数 30sec間
      Serial.print(fSum_min);
      Serial.print("Hz, ");

      // max AC周波数 30sec間
      Serial.print(fSum_max);
      Serial.print("Hz ");

      // 力率 = 有効電力 / 皮相電力
      Serial.print(Power_factor);
      Serial.print("%, ");

      // 積算Whを求める
      watt_hour += watt_sum / 3600.0;
      Serial.print(watt_hour);
      Serial.println("Wh, ");
    }
    Bflag = true;

    // AMBIENT_INTERVAL(30)秒経過したらAmbientに出力
    curr_time = millis();
    if( (curr_time - last_update) > AMBIENT_INTERVAL *1000 ){

    //#if DEBUG
      Serial.print(curr_time/1000);Serial.print(" usec");
      Serial.print('\t');
      Serial.print(last_update/1000);Serial.print(" usec");
      Serial.println('\t');

      Serial.println("Now, sending the Maximum value during the period to Ambient .....");
      Serial.print(vrms_max);
      Serial.print("V, ");

      Serial.print(Irms_AB_max);
      Serial.print("A, ");

      Serial.print(Irms_A_max);
      Serial.print("A, ");

      Serial.print(Irms_B_max);
      Serial.print("A, ");

      Serial.print(app_Power_max);
      Serial.print("VA, ");

      Serial.print(watt_max);
      Serial.print("W, ");

      // AC周波数
      Serial.print(fSum);
      Serial.print("Hz, ");

      // min AC周波数 30sec間
      Serial.print(fSum_min);
      Serial.print("Hz, ");

      // max AC周波数 30sec間
      Serial.print(fSum_max);
      Serial.println("Hz");
    
    //#endif

      ambient.set(1, vrms_max);//電圧 V
      ambient.set(2, Irms_AB_max);//CT_A + CT_B電流 A
      ambient.set(3, Irms_A_max);//CT_A電流 A
      ambient.set(4, Irms_B_max);//CT_B電流 A
      ambient.set(5, app_Power);//皮相電力 VA
      ambient.set(6, watt_max);//電力 W
      ambient.set(7, fSum_max);// 区間最大周波数
      ambient.set(8, fSum_min);// 区間最小周波数
      ambient.send();    

      vrms_max = Irms_A_max = Irms_B_max = Irms_AB_max = watt_max = app_Power_max = 0;
      fSum_max = 0.0;
      fSum_min = 1000.0;
      last_update = curr_time;
      Bflag  = false;
    }  

    num_samples = 0;
    vrms_sum = Irms_A_sum = Irms_B_sum = watt_sum = 0;
    
    fMin = 1000.0;                          // 次回の測定用に変数を初期化
    fMax = 0.0;
    fSum = 0.0;
    logCount = 0;

}

void setup()
{
  Serial.begin(115200);
  analogSetAttenuation(ADC_6db);        // アッテネーターを設定
  pinMode(arefPin, INPUT);
  pinMode(freqPin, INPUT);
  pinMode(voltagePin, INPUT);
  pinMode(currentPin_A, INPUT);
  pinMode(currentPin_B, INPUT);

  watt_hour     = 0;
  vrms_sum      = vrms_max    = 0;
  Irms_A_sum    = Irms_A_max  = 0;
  Irms_B_sum    = Irms_B_max  = 0;
  Irms_AB_max   = 0;
  watt_sum      = watt_max    = 0;
  app_Power     = app_Power_max  =0;
  num_samples   = 0;
  last_update   = millis();

  attachInterrupt(freqPin, acIrq, FALLING);    // ピンがHIGHからLOWになった時に割り込みacIrq関数の割り込み発生

  Blynk.begin(auth, ssid, password, server, port);  // Blocking until server connected
  // You can also specify server:
  //Blynk.begin(auth, ssid, password, "blynk-cloud.com", 80);
  //Blynk.begin(auth, ssid, password, IPAddress(192,168,1,100), 8080);

  // Setup a function to be called every interval second
  timer.setInterval(OUTPUT_INTERVAL * 1000L, myTimerEvent);

  // for Ambient 
  ambient.begin(channelId, writeKey, &client);
}

/*
 * void wifi_lan_check()
 *   WiFi Lan の監視をします。
 *   WiFi.status() で判断するよりも、ping で応答をチェックした方が、早いかも。
 */
void wifi_lan_check(){
  int i;
  if(WiFi.status() !=3){ // 3はWL_CONNECTED
    //Serial.print("wifi_lan_check() :#1 st=");
    //Serial.println(WiFi.status());
  }
  if(WiFi.status() != WL_CONNECTED){
    //Serial.println("WiFi disconnected");
    WiFi.disconnect();
    delay(50);
    WiFi.begin(ssid, password);
    i=50;
    // 結局は、回復しないで、wdt reset になるので、直ぐ自分で restart させた方が良いかも。
    while (WiFi.status() != WL_CONNECTED) {
      delay(250);
      //Serial.print(".");
      i--;
      if(i<0){
        //Serial.println("wifi_lan_check() :#2 ESP.restart()");
        delay(250);
        // 自分で restart しないで、放っておくと、 wdt reset になるので、
        // 敢えて ESP.restart() しなくてもよいのだが、気が短い人は、どうぞ。
        ESP.restart();
      }
    }
  }
}

void loop()
{
/*
  // check for incomming client connections frequently in the main loop:
  server.handleClient();  // この処理でクライアントからのアクセスに対応 server.handleClient();
*/

  // WiFi Lan の監視をします。接続していないとリセット
  wifi_lan_check();


  // 電力を計算(100個のデータのrms)
  calcWatt();
  // AC周波数測定
  mesureAC_FREQ();
  
  // 指定秒数(OUTPUT_INTERVAL)分加算する
  vrms_sum    += Vrms; // vrms_sum = vrms_sum + Vrms
  Irms_A_sum  += Irms_A;
  Irms_B_sum  += Irms_B;
  watt_sum    += Watt;
  num_samples++; // num_samples = num_samples + 1

  // 加算後、Blynk Object の myTimerEvent() へデータを渡す
  // そこで、指定秒数間の平均を計算する
  Blynk.run(); // Blynk起動
  timer.run(); // BlynkTimer object 開始
}


測定結果

Blynk live表示

ドライヤを負荷と問たときの送風、cool、hotでの結果です。電流はL1相(白色)、L2相(青色)、L1+L2(オレンジ色)です。同じ電流を測っていますが、わずかの違いは出ています。電流は時系列でも見えるようにしました。


Ambient

上から、電流(L1, L2, L1+L2)、電圧と最大/最小周波数、電力の約4時間の時系列波形です。

電流波形は使用している家電の状況が見えて、とても興味深いです。それよりも興味深かったのはAC100Vの電圧やその周波数が結構変動することとか、大きな電流が流れるタイミングで電圧が低下することです。






Powered by Blogger | Designed by QooQ

keyboard_double_arrow_down

keyboard_double_arrow_down