ESP8266＋ADS1015でエアコンの消費電流を測定し、ESP-Now → ESP32 → Raspberry Pi → Node-RED → InfluxDB → Grafanaで可視化。

# はじめに
自宅のエアコンを最近交換したのをきっかけに、どのくらい電力を消費するのかを測定してみたくなりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266665/7535cf51-c29a-5e21-c7e7-d45c53572b92.png)

電流をカレントトランスと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）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266665/46562d4c-84b3-d097-c16a-a9099693bbca.png)


## AD変換
エアコンで使われる交流電流の測定のために、[秋月電子で購入したカレントトランス](http://akizukidenshi.com/catalog/g/gP-08960/)を用いました。最大80Aまでの電流を通した時に、3000:1の比率で電流を取り出す事が出来、それを抵抗終端によって電圧に変換したものを、ADS1015というADコンバータでデジタル値に変換しました。[(使用したのは、秋月電子のモジュール)](http://akizukidenshi.com/catalog/g/gK-13838/)。ADS1015は、差動入力が出来るため、今回のように交流電圧でプラス側・マイナス側に振れるモノを、波形のまま測定する事ができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266665/a6918e3e-bb49-de80-1083-2d74d3af3230.png)

トランスが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のデータシート参照](https://www.tij.co.jp/jp/lit/ds/symlink/ads1015.pdf?ts=1591509918361&ref_url=https://www.tij.co.jp/product/jp/ADS1015)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266665/82d165b5-6888-2acb-8121-3dfb2fcaef9b.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266665/347f0f72-fe2c-9796-409b-a4ce7a986a32.png)


今回の測定は、ACの波形そのものを読みますが、Wire.readで2バイトを読んで16bitに結合した後、符号も必要なため、bitシフト処理では無く、「16で割る」ことにより、符号付のまま、16bit → 12bitにしています。(16bitの状態で 2's complementになっているため）


```:ads1015.h
#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波形がそのまま出力できます。この例では、白熱電球を負荷にして測定したため、きれいな波形が出ています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266665/90225c40-87c7-baac-2fc1-5edd6c0c0af8.png)

参考までに、他の波形も載せておきます。

LEDのフラットランプ　・・　もの凄くスイッチングのノイズが大きいです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266665/b541f15e-2f74-e606-9fdc-7838d8421b33.png)

LEDバックライトのコンピュータモニタ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266665/27c6edf5-9407-99ee-9ecd-524afb1cb3be.png)

蛍光灯のデスクライト
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266665/9abb6abe-ec1f-21ba-7379-58dce0d3062c.png)


# 受信側
## ESP32: ESP-WROOM32 での ESP-Now Slaveモード
ESP-Nowの受信側には、ESP32を使いました。ESP-Nowに対応しているデバイスは、ESP8266かESP32のみですが、ESP8266での実験は以前試したことがあるので、今回は秋月電子の [ESP32のDevelopmentボード](http://akizukidenshi.com/catalog/g/gM-11819/)を使って、ESP8266からのESP-Now通信を受信してみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266665/c3f3cfcb-2e1b-fdaf-604d-07f0f25f5d8f.png)

## 受信側 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 と少し心配になる番号ですが、問題なく使用できています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266665/8ea3f148-50a7-1425-83e6-4933ef780a73.png)
インストールすると、ストレージの部分に、"influxdb out", "influxdb in", "influx batch"の三つが追加されます。データ書き込み(insert）には、influxdb outを使います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266665/066de0b0-2185-abdc-c8c0-8d68c5b4b816.png)


### フロー図　（既にESP32が接続された状態）
対象部分のnode-REDのフロー図は以下です。 USB経由で Serial in入力を行い、functionノードで、influxdb用に書式変換を行い、influxdb outノードでデータベースへの書き込みを行うという流れです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266665/042a86c0-10f5-fc84-26ab-b0dff8b63461.png)

### serial inノードで受信
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266665/b2a7d509-fde1-08ea-7316-e5b0ca54d5fa.png)

### functionノードで変換
functionノードで、payloadの中から、必要なデータを取出し、influxDB用に書式変換します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266665/01e24314-15dc-8d96-f0d5-bde6bf012a57.png)

```
// 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と一緒に書きこまれます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266665/11d88cec-74e3-c023-9c82-61d483e46f76.png)

###フロー

```
[{"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から選択していけばグラフは簡単にできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266665/c284908d-4522-d56d-2140-393af05b4ab7.png)

今回は、データベース内の電流値(mA）から、100V時の電力 P=IV (Watt)を計算してからグラフ化したかったので、得られた値 x 100V / 1000 という計算をするため、Queryを変更しました。

```
SELECT mean("power") /10 FROM "table1" WHERE $timeFilter GROUP BY time(1m) fill(null)
```

##得られたグラフ
結果として、以下のグラフが出来ました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266665/2d54df4f-cc15-42a3-0273-a87c0f98b641.png)

他の情報と併せたダッシュボードとして、可視化できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266665/4ec50345-2229-92b3-0f2f-e15e44e71e44.png)

# おわりに・・・・今後の分析のために
とりあえず、エアコンの消費電力をグラフ化出来ました。これで、エアコンの各モード（例えば、省エネモードとか）を切り替えた時に、どのように消費電力が変化するのかを調べてみるために、面白そうです。ただ、これだけでは、「見るだけ」なので、もう少し深い解析をしたり、電力料金との比較をしたり・・などの分析を進めていきたいと思っています。　もう少し、分析できたら、投稿したいと思います。