はじめに
自宅のエアコンを最近交換したのをきっかけに、どのくらい電力を消費するのかを測定してみたくなりました。
電流をカレントトランスとADコンバータを用いて測定し、1秒間ごとにRMS値(1秒間の平均)をESP-Nowの通信で、Raspberry PiにUSB接続されたESP32に送り、Node-REDで少し加工した後、influxDBに格納し、Grafanaで可視化してみましたので投稿します。
今回の用途では、わざわざESP-Nowを使って送る必要性は特になく、WiFiで直接Raspberry Piに送っても良いのですが、WiFiの環境が変わっても 送り側のソフトを変更せずに済む効果もあるかと思い、ESP-Nowで通信してみました。
また、わざわざ、node-REDを間に介さなくても、適当なPython等のプログラムで、シリアル通信を受信して、influxDBに書き込むことも問題なく出来ますが、私の自宅IoTでは、基本的に全ての入り口と出口を node-REDで統合して管理するようにしているので、今回もnode-REDを介する事にした次第です。node-REDは各種の信号の流れを描くには、とても適したツールだと思います。
ESP-Nowは、ESP8266とESP32しか対応していないので、一旦ESP32で受けてからRaspberry Piへ送る形になっています。1秒ごとに、InfluxDBのテーブル(Measurement)に書き込み、1分毎の平均値をGrafanaでグラフ化しました。
送信側 (ESP8266: ESP-WROOM02)
AD変換
エアコンで使われる交流電流の測定のために、秋月電子で購入したカレントトランスを用いました。最大80Aまでの電流を通した時に、3000:1の比率で電流を取り出す事が出来、それを抵抗終端によって電圧に変換したものを、ADS1015というADコンバータでデジタル値に変換しました。(使用したのは、秋月電子のモジュール)。ADS1015は、差動入力が出来るため、今回のように交流電圧でプラス側・マイナス側に振れるモノを、波形のまま測定する事ができます。
トランスが3000:1という変換比率で、今回は220 Ωの終端抵抗を使いましたので、抵抗両端の電圧Vと、2次側電流 i と、1次側の電流Iとの関係は、
I(mA) = i(mA) x 3000 = V(mV)/220 * 3000
になります。(I,i,VはRMS値)
抵抗値の誤差が、そのまま測定結果の誤差になりますので、手持ちの220 Ωの金属皮膜抵抗の中で、とりあえず最も220.0 Ωに近いものを選びました。
送信側のコード
コードは、AD関係とESP-Now関係を別ファイルに分け、三つのファイルで構成しました。
AD変換関係
ADS1015のI2C制御には、ライブラリは使用せず、直接I2C通信のコードをべた書きで作りました。特殊な事は何もしていません。I2Cのクロックは、出来るだけ高速に読み書きを行うため、400kHzに設定しています。(ただ、この時、クロック周波数を実測すると、400kHzは出ておらず、なぜか 259kHzでした。理由は分かりません・・・)
ADS1015の設定としては、ゲインの設定で 1bitが1mVとしてあります(±2.048V)。ADS1015のデータシート参照
今回の測定は、ACの波形そのものを読みますが、Wire.readで2バイトを読んで16bitに結合した後、符号も必要なため、bitシフト処理では無く、「16で割る」ことにより、符号付のまま、16bit → 12bitにしています。(16bitの状態で 2's complementになっているため)
#include <Wire.h>
#define ADS1015_ADDR 0x48
#define CONVERSION_REG 0x00
#define CONFIG_REG 0x01
#define CONFIG_0H 0b01000100
#define CONFIG_1H 0b01010100
#define CONFIG_2H 0b01100100
#define CONFIG_3H 0b01110100
#define CONFIG_01H 0b00000100
#define CONFIG_23H 0b00110100
#define CONFIG_L 0b10000011
void init_ADS1015() {
Wire.begin(); // as default SDA=4, SCL=5
Wire.setClock(400000L);
Wire.beginTransmission(ADS1015_ADDR);
Wire.write(CONFIG_REG);
Wire.write(CONFIG_01H);
Wire.write(CONFIG_L);
Wire.endTransmission();
}
int16_t read_ADS1015_Diff01() {
int16_t val; //符号付き
Wire.beginTransmission(ADS1015_ADDR);
Wire.write(CONVERSION_REG);
Wire.endTransmission();
Wire.requestFrom(ADS1015_ADDR, 2);
val = Wire.read() << 8 | Wire.read() ;
val /= 16; //シフトでは無く、除算して符号を保持
return (val);
}
ESP-Now関連
ESP-Now通信に必要な初期設定と、データを送信する関数を定義しています。
指定するMACアドレスは、Slave側のAPモードのMACアドレスになります。(Stationモードの時のMACアドレスとは異なりますので注意が必要です。後述するように、ESP32に受信側のコードを書き込んで走らせると、シリアルターミナルに、APモードのMACアドレスが表示されますので、その値を使います。 もし、複数のESP-Nowスレーブへ送信したい場合には、slaveMACの二次元配列に、必要な数のMACアドレスを追加し、それぞれ、Peerとして追加すれば送れます。
送信側のESP8266を、CONTROLLERと定義し、受信側の相手(peer)をSLAVEあるいはCOMBOとして設定しています。
送信時の call back関数を定義していますが、正常に通信できた場合は、OK、失敗した時は、FailedというStatusが、対応するMACアドレスとともに表示されますので、デバッグ時に便利です。
あとは、実際に送るデータ(浮動小数値)に、送り元のデバイス名を付与して、文字列を作ったうえで、sendします。送り先に NULLを指定すれば、事前に登録したPEER全てに送る事が出来ますし、ここで送り先のMACアドレスを特定すれば、送り先毎に送信内容を分けることも可能です。
#include <ESP8266WiFi.h>
#include <espnow.h>
#define WIFI_CHANNEL 1
uint8_t slaveMAC[][6] = {
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00} //ESP32 COM16 AP
};
void printMacAddress(uint8_t* macaddr) {
Serial.print("{");
for (int i = 0; i < 6; i++) {
Serial.print("0x");
Serial.print(macaddr[i], HEX);
if (i < 5) Serial.print(',');
}
Serial.print("}");
}
void send_cb(uint8_t* mac, uint8_t sendStatus) {
printMacAddress(mac);
Serial.print(" --> ");
Serial.println(sendStatus == 0 ? "OK" : "Failed");
}
void init_ESP_Now() { // Initialize ESP_Now as Master (Controller)
WiFi.mode(WIFI_STA);
if (esp_now_init() != 0) {
Serial.println("*** ESP-Now init failed");
}
esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER);
Serial.println("add peer");
Serial.println(esp_now_add_peer(slaveMAC[0], ESP_NOW_ROLE_COMBO, WIFI_CHANNEL, NULL, 0));
esp_now_register_send_cb(send_cb);
}
void send_ESP_Now(double x) {
uint8_t bs[250];
char buf[250];
char s_rms[8];
char* s_device = "8266-ADS1015";
dtostrf(x, 0, 2, s_rms);
sprintf(buf, "%s,%s", s_device, s_rms);
memcpy(bs, buf, strlen(buf));
Serial.println(buf);
esp_now_send(NULL, bs, strlen(buf)); // NULL means send to all peers
}
メイン
前述の二つのファイルをincludeして、メイン処理をこのファイルに纏めました。
出来る限り高速で(delay()を入れる事無く)AD変換を繰り返し、RMSを求めるために二乗して加算をし続け、100msec毎に平均を取ってルートを取る事でRMSを計算し、さらにそれを、10回平均して1秒に一回送信する形にしました。
digitalWriteで 16ピンに出力しているのは、周期を確認するためのデバッグ用です。正確に100msec毎に反転している事は確認済みです。
RMSで得られた値は、3000:1、220 Ω終端時の電流値になりますので、換算した後、1次側電流のRMS値として出力します。(注:終端抵抗が 220 Ωでない場合や、カレントトランスの変換比率が 3000:1以外の場合には、修正が必要です)
/*
ADS1015 I2C control basics
just using Wire.h (without any other library)
send the result with ESP-Now to the Slave 2020/6/7 Y.Yasuda
*/
#include "ads1015.h"
#include "espnow_mst.h"
uint64_t t_now = 0;
uint64_t t_prev = 0;
double v_sum = 0;
int v_cnt = 0;
double rms_sum = 0;
int rms_ctr = 0;
boolean dbg_pin = false;
void setup() {
Serial.begin(115200);
pinMode(16, OUTPUT); //for debug purpose
init_ESP_Now();
init_ADS1015();
t_now = t_prev = millis();
}
void loop() {
int16_t val; //符号付き
double v_now, v_rms;
double rms_avg;
val = read_ADS1015_Diff01(); //(int16) read ADS1015, differential mode, 0-1
t_now = millis();
if (t_now - t_prev >= 100) { //every 100msec
digitalWrite(16, dbg_pin);
dbg_pin = !dbg_pin;
v_rms = sqrt(v_sum / v_cnt); //calc RMS value
t_prev = t_now;
v_cnt = 0; //reset for next RMS calculation
v_sum = 0;
if (rms_ctr == 10) { //every 1 sec.
rms_avg = rms_sum / rms_ctr;
rms_avg = rms_avg * 3000 / 220; //i=V/R=rms_avg*3000/220 ohm -> mA
send_ESP_Now(rms_avg);
rms_sum = 0;
rms_ctr = 0;
}
rms_sum += v_rms;
rms_ctr ++;
} else {
v_now = val * val; //Square
v_sum += v_now; // sum up
v_cnt ++; // counter
}
}
以上が送信側のコードと、概略の説明です。
##読み取った波形の例
別のコードで実験した例ですが、出来る限り速くAD変換をして、読み取った値をそのままグラフにすると、以下のようにAC波形がそのまま出力できます。この例では、白熱電球を負荷にして測定したため、きれいな波形が出ています。
参考までに、他の波形も載せておきます。
LEDのフラットランプ ・・ もの凄くスイッチングのノイズが大きいです。
受信側
ESP32: ESP-WROOM32 での ESP-Now Slaveモード
ESP-Nowの受信側には、ESP32を使いました。ESP-Nowに対応しているデバイスは、ESP8266かESP32のみですが、ESP8266での実験は以前試したことがあるので、今回は秋月電子の ESP32のDevelopmentボードを使って、ESP8266からのESP-Now通信を受信してみました。
受信側 ESP32のコード (Slave、APモード)
ESP8266で includeするファイルは、espnow.hでしたが、ESP32では、esp_now.hになります。関数の種類や引数のタイプ等が、色々違っていますので注意が必要です。
マスター側(コントローラー側)からは、スレーブのMACアドレスに対してESP-Now通信をしますので、スレーブ側のMACアドレスを最初に表示するようにしています。
MACアドレスには、STATIONモード時の値と、APモード時の値がそれぞれありますので、ここでは、APモードの値を表示して使います。
一旦、WiFi.mode(WIFI_AP)で、APモードを立ち上げる準備をしていますが、APモードでのMACアドレスを表示した後は、直ぐ、Disconnectしています。すなわち、WiFiのAPモードそのものを利用するわけでは無いので、本来のAPを立ち上げる必要はないという事です。
ESP-Nowの初期化をした後、受信バッファにデータが届いた際のcallback 関数を定義します。データが届いた際は、このcallback関数が呼ばれるので、必要な処理はこの中に書きます。 やっている事は単純で、受信バッファに入った文字列を、そのままシリアルに出力するだけです。 文字列の処理で、最後に NULLを入れて文字列を作ったうえで、Serial.printlnしています。(送信側のデータには、最後にNULLは付与されていないので) 後は、待つだけなので、loop()の中味は何もありません。
どのデバイスからどんなデータが来るのかは、このSlaveでは関与しない事にしたため、このSlaveのコードは汎用的に使えるはずです。Raspberry Piに比べ、ESP系のソフトウエア変更は、OTAを使わない限り、シリアル接続が必要になりますので、出来るだけ汎用的なコードにしておいて、書き換える頻度を減らして、メンテナンスを楽にしたいという意図があります。
#include <esp_now.h>
#include <WiFi.h>
#define WIFI_CHANNEL 1
char buf[200];
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_AP);
Serial.println( WiFi.softAPmacAddress() );
WiFi.disconnect();
if (esp_now_init() != ESP_OK) {
Serial.println("ESP-Now Init Failed....");
return;
}
esp_now_register_recv_cb(recv_cb);
}
// call back for receiving data
void recv_cb(const uint8_t * mac, const uint8_t *recvData, int len) {
memcpy(&buf[0], recvData, len);
buf[len]='\0'; //lenには文字列末のNULLが入らないので暫定的に。
Serial.println(buf);
}
void loop() {
}
このコードを、ESP32のDevボードに書き込んで終わりです。シリアル通信のボーレートは、115200にしました。
Raspberry Piの USBポートを固定
Raspberry PiのUSBポートに、Arduinoや、ESP32、ESP8266等をシリアル接続すると、/dev/ttyUSB* というドライバーで通信する事になりますが、/dev/ttyUSB0とか /dev/ttyUSB1とかいう最後の数字が、Raspberry Piのリブート時などに、立上りの順番で入れ替わってしまう事が良くあります。入れ替わってしまうと、受信側のソフトウエアでの設定が食い違うため、正常な通信が出来なくなります。
それを回避するために、「どのデバイスには、どの番号」というルールを固定しておく必要があります。この方法は、ネットのあちこちにありますので、ここでは詳細を書きませんが、lsusbでデバイスの情報(Vendor Id, Product Id)を取得して、/etc/udev/rules.d/99-local.rulesのファイルを編集して、それぞれのシンボリックリンクを張っておくことで実現できます。
ここでは、このESP32に対しては、/dev/ttyUSB_ESP32-1 という名前を付けました。
$ ls -l /dev/ttyUSB*
crw-rw---- 1 root dialout 188, 0 6月 2 08:47 /dev/ttyUSB0
crw-rw---- 1 root dialout 188, 1 6月 7 11:17 /dev/ttyUSB1
lrwxrwxrwx 1 root root 7 6月 7 11:17 /dev/ttyUSB_ESP32-1 -> ttyUSB1
lrwxrwxrwx 1 root root 7 6月 2 08:47 /dev/ttyUSB_ESP8266-1 -> ttyUSB0
$
node-RED設定
node-REDでの読込設定で、先ほどのシンボリックリンク/dev/ttyUSB_ESP32-1を使います。レートは、115200が設定されています。下記のフローを作成し、デプロイすれば、USBに繋がった ESP32が「接続済」となり、データが入る毎に、データベースに書き込まれます。
influxdb用のノードは、標準的にインストールされたnode-REDには、入っていませんので、「パレットの管理」から "node-red-contrib-influxdb"をインストールする必要があります。バージョンは、0.4.0 と少し心配になる番号ですが、問題なく使用できています。
インストールすると、ストレージの部分に、"influxdb out", "influxdb in", "influx batch"の三つが追加されます。データ書き込み(insert)には、influxdb outを使います。
フロー図 (既にESP32が接続された状態)
対象部分のnode-REDのフロー図は以下です。 USB経由で Serial in入力を行い、functionノードで、influxdb用に書式変換を行い、influxdb outノードでデータベースへの書き込みを行うという流れです。
serial inノードで受信
functionノードで変換
functionノードで、payloadの中から、必要なデータを取出し、influxDB用に書式変換します。
// influxDBへ書き込むためのの書式へ変更
var x = msg.payload.split(',');
if (x[0] === "8266-ADS1015"){
msg.payload = {
"power": parseFloat(x[1]),
}
return msg;
}
influxdb out ノード
influxdb outノードで、データベースに書き込みます。同じRaspberry Pi上に作ってある influx_iot01というデータベースの、table1というテーブル(influxdbでは、measurementと呼ぶ)に、入力が入る毎に、timestampと一緒に書きこまれます。
###フロー
[{"id":"a803abf3.140428","type":"serial in","z":"c851379d.611068","name":"ESP32-COM16","serial":"51c49bfa.f987f4","x":180,"y":220,"wires":[["967f0572.6257f8"]]},{"id":"8680d261.609c9","type":"comment","z":"c851379d.611068","name":"ESP-Now Slave ESP32-COM16 (電力 from ESP8266 書斎エアコン)","info":"","x":350,"y":180,"wires":[]},{"id":"967f0572.6257f8","type":"function","z":"c851379d.611068","name":"電力CT3000-220","func":"// influxDBへ書き込むためのの書式へ変更\nvar x = msg.payload.split(',');\n\nif (x[0] === \"8266-ADS1015\"){\n msg.payload = {\n \"power\": parseFloat(x[1]),\n }\n return msg;\n}\n","outputs":1,"noerr":0,"x":370,"y":220,"wires":[["fb09fc73.e27a2","5259d61e.a86118"]]},{"id":"fb09fc73.e27a2","type":"debug","z":"c851379d.611068","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":590,"y":280,"wires":[]},{"id":"5259d61e.a86118","type":"influxdb out","z":"c851379d.611068","influxdb":"e2d98b52.c73258","name":"","measurement":"table1","precision":"","retentionPolicy":"","x":660,"y":220,"wires":[]},{"id":"51c49bfa.f987f4","type":"serial-port","z":"","serialport":"/dev/ttyUSB_ESP32-1","serialbaud":"115200","databits":"8","parity":"none","stopbits":"1","waitfor":"","newline":"\\n","bin":"false","out":"char","addchar":"","responsetimeout":"10000"},{"id":"e2d98b52.c73258","type":"influxdb","z":"","hostname":"127.0.0.1","port":"8086","protocol":"http","database":"influx_iot01","name":"","usetls":false,"tls":""}]
influxdbは、時系列データベースの一つであり、IoTのような「時間」によるデータの変化を記録したり、Queryしたりする際にとても便利で、使いやすいデータベースです。可視化のGrafanaツールとも非常に相性が良く、簡単に時系列をベースとした可視化が実現できるのでお勧めです。
influxDB設定
influxdbが起動している状態であれば、特別な設定は不要です。予めテーブルを作っておく必要も無ければ、あらかじめフィールドの定義をしておく必要もありません。書き込めば、自動的に対応したフィールドが作成されます。主キーは常に挿入した時刻(タイムスタンプ、UTC時刻)になります。
どのような形で、データベースに記録されているのかを、念のために確認します。influxコマンドを使って、対象のデータベース(ここでは、influx_iot01)を開き、書き込んだテーブル(ここでは、table1)から、電力のフィールド(ここでは、power・・・実際には、電流値:mAで記録されている)を表示してみました。 ちなみに、timeフィールドは、挿入時に自動的に、挿入時刻の値が付与されます(UTC時刻)
$ influx
Connected to http://localhost:8086 version 1.8.0
InfluxDB shell version: 1.8.0
> use influx_iot01
Using database influx_iot01
> select power from table1 limit 10
name: table1
time power
---- -----
1591496377829846016 237.35
1591496378770157606 237.04
1591496379623660077 237.1
1591496380629092720 236.49
1591496381623668463 237.2
1591496382623850159 237.07
1591496383627910477 237.66
1591496384622593468 237.78
1591496385624646721 237.85
1591496386639334993 237.81
>
データベースには、正しく書き込まれているようなので、Grafanaを使って可視化してみます。
Grafana設定
ダッシュボードに、パネルを一つ作成し、"Graph"パネルから、必要なフィールドを設定してグラフ化します。特に計算等が必要で無ければ、GUIから選択していけばグラフは簡単にできます。
今回は、データベース内の電流値(mA)から、100V時の電力 P=IV (Watt)を計算してからグラフ化したかったので、得られた値 x 100V / 1000 という計算をするため、Queryを変更しました。
SELECT mean("power") /10 FROM "table1" WHERE $timeFilter GROUP BY time(1m) fill(null)
おわりに・・・・今後の分析のために
とりあえず、エアコンの消費電力をグラフ化出来ました。これで、エアコンの各モード(例えば、省エネモードとか)を切り替えた時に、どのように消費電力が変化するのかを調べてみるために、面白そうです。ただ、これだけでは、「見るだけ」なので、もう少し深い解析をしたり、電力料金との比較をしたり・・などの分析を進めていきたいと思っています。 もう少し、分析できたら、投稿したいと思います。