3.RaspberryPiで簡易スマートメーターの作成

2021/2/3~2021/5/4

A/D変換が使えるようになったのでスマートメーターもどきをRaspberryPi作ってみた。

本格的にスマートメーターを作るのであればWattchekerみたいなACコンセントからとってくるような方式の方が良いですが、回路を組んだりするのが超絶面倒だし結構危険です。
電流とるだけなら簡単な方法にACコードにクランプして電流だけを測定する方式があります。簡単だし危なくないのでこちらを採用することにしました。ただ電圧を測定することができないため、ワットとしては精度がばらつくがそこまで精度求めるようなこともしてないのでこれで十分でしょう。

あとなんでESP32とかPICとかマイコンでやらないの?って話もありますが、Raspberrypiはこのような処理があまり得意でないのでマイコンにしたほうが良いですが、スクレイピングするために常時稼働しているRaspberryPiがあるのでそこに追加で作業させるためにRaspberrypiで動作させることにしました。

構成

スマートメーターの構成は下記図のようにしました。①~③の定期的な動作はcronで動作させます。
①RaspberryPiで電流値を測定(1時間の平均をとる)
②電流値をワットに変換し、CSVに保存
③ftpコマンドでホームページにUP
④Webブラウザでグラフを確認





クランプと回路

早速大阪の日本橋にある共立電子でセンサを買いに行きました。セパレートできるクランプ式(ナイロンスプリング・ワンタッチクランプ型)は高いですが、セパレートできないタイプはケーブルを作るのがすごく面倒なのでセパレートタイプを選択したほうが良いと思います。

極小型クランプ式交流電流センサ(AC専用):CTL-6-S32-8F-CL


CTL-6-S32-8F-CL 仕様
適用電流
0.01~15Arms(50/60Hz),RL<10Ω
適用周波数
50~500kHz
2次巻数(n)
800±2ターン




クランプの電流を電圧に変換した値を読むにはA/Dコンバーターが必要になる。RaspberryPi B+にはA/Dコンがついていないため外付けICを選択します。よくADS1115が使われており、Amazonの中華系ストアで2個1000円で売っていたのでそれを買いました。





●回路




クランプには抵抗値をつける必要がありますが、3パターンあります。RL=10Ω、100Ω、1kΩとありますが、10Ωだと0.01Armsが測定できないため、最小値が測定できる100Ωにしました。しかし結合係数特性K値が100Ωだと7A付近から係数がかなり変わるため、K値を変更する計算値をソフトで組む必要が出てきます。(今回はそこまで高い電流を扱わないため、K値は固定にしました。もしK値の計算が面倒であれば変化がほぼない10Ωがベストです。)

E = K x Io x RL /n (Vrms)  →Io=E x n /(K x RL)
K=0.92
Io=結果
E=クランプの取得電圧
RL=100Ω

クランプ式は電流の値しか取れないため、電圧降下分は考慮できないがそこまで精度が不要なので無視する。(電力計算値の電圧は日本のAC100Vという計算でいく。)

あとは自作ACタップ部分に取り付けて完成。


動作確認のプログラム

まず値が取れて、きちんと計算が合っているかのプログラムを組みました。計算結果との差はWatt Checker(Model 2000 MS1 KEISOKU GIKEN製)と比較し、大体合ってそうなのでこれで問題なさそうです。

●プログラム1(※動作確認でとりあえず動いたプログラム。間違えてます。)
#include <stdio.h>
#include <stdint.h>
#include <wiringPi.h>
#include <wiringPiI2C.h>

#define DEV_ADDR 0x48

int main(int argc, char **argv) {
int i;
int fd;
int data;
int volt,rms,amp,wat,total;

wat =0;
total=0;
i=0;

while (i < 50){

volt=0;
rms=0;
amp=0;
data=0;

fd = wiringPiI2CSetup(DEV_ADDR);
wiringPiI2CWriteReg16(fd,0x01,0xe3c1);
data = wiringPiI2CReadReg16(fd,0x00);
printf("data=%x ",data);

volt = ((data & 0x00ff00ff) << 8) | ((data >> 8) & 0x00ff00ff);

if(volt < 32767){
printf("%x ",volt);
rms = volt * 0.1875;
amp = (rms * 800)/(100 * 0.92);

printf("V= %d [mV] , A=%d [mA] \n",rms,amp);
}
else{
printf("%x minus",volt);
volt = 65534 - volt;
rms = volt * 0.1875;
amp = (rms * 800)/(100 * 0.92);
printf("V= %d [mV] , A=%d [mA] MinusValue\n",rms,amp);
}

wat = 100 * amp/1000;
printf("W= %d \n ",wat);
total = total + wat;
i++;
}

total =total /50;
printf("Total Wat Average=%d\n",total);
}


●動作確認
$ time ./i2ctest
data=50ff ff50 minusV= 32 [mV] , A=278 [mA] MinusValue
W= 27
data=b0ff ffb0 minusV= 14 [mV] , A=121 [mA] MinusValue
W= 12

~~~略~~~~

data=2000 20 V= 6 [mV] , A=52 [mA]
W= 5
data=4000 40 V= 12 [mV] , A=104 [mA]
W= 10
data=4000 40 V= 12 [mV] , A=104 [mA]
W= 10
data=3000 30 V= 9 [mV] , A=78 [mA]
W= 7
Total Wat Average=10

real 0m0.069s
user 0m0.014s
sys 0m0.012s

抵抗値:99.6Ω
93.7V → 10.5A → 856W 910~1000W
   → 0.14A →9~11W

Watt Checkerの値と大体合っているのでこれでOK。


プログラムミスについて

あまり速くi2cのアクセスを速く大量にやるとI2Cがはまった。
理由を調べてみるとfd = wiringPiI2CSetup(DEV_ADDR);はループ内に入れてはいけないことがわかった。入れると下記のようにfdファイル開きすぎと怒られる。
fdはループの前に一度開くだけでOKなので注意する。

・・・・・・
16
17
18
19
Unable to open I2C device: Too many open files


プログラムの遅延について

電流値の測定は下記①②のようにする。
①50回の電圧データをとる。
②①を10秒間隔で359回実行する(余裕を持たせて1時間の10秒前に処理を終わらせておく。)

しかし、10秒間隔で実施するが、1時間立つと約20秒遅れていた。

real 60m8.067s
user 0m0.073s
sys 0m1.864s


原因を探るべく1時間ではなく30秒(10秒間隔を3回実施する)で確認すると0.158s遅くなっている。
real 0m30.158s
user 0m0.002s
sys 0m0.024s

つぎに50秒(10秒間隔を5回実施する)と0.256s遅くなっていた。
real 0m50.256s
user 0m0.002s
sys 0m0.036s

上記内容から50回読み取る+αの実行時間で約0.05s遅れている。


delay(10000)は10秒なので、0.05sはdelay(50)なので10000-50して0.05sのズレを補う。delay(9950)を10秒間隔を5回実施すると
real 0m50.005s
user 0m0.000s
sys 0m0.035s
である程度

delay(9950)にして1時間で測定すると大体10秒間隔で359回で良い感じになりました。結構ズレの精度がよかったw
real 59m49.524s
user 0m0.081s
sys 0m1.826s

 


最終FIX プログラム

このプログラムで約1時間測定し、CSVに保存するというプログラムになります。この実行ファイルを1時間起きにcronで実行してあげればOKです。

#include <stdio.h>
#include <stdint.h>
#include <wiringPi.h>
#include <wiringPiI2C.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <limits.h>
#include <string.h>

#define DEV_ADDR 0x48
#define BUF_SIZE 256

int main(int argc, char **argv) {
int i,j;
int total_m;
int fd;
int data;
int volt,rms,amp,wat,total;
FILE *fp;
char buf[BUF_SIZE];
int line = 0;
char *p;
int flag1 = 0;

time_t t;
char fname[32],nowtime[32],year[32],tname[32];

// directry create and chagedir
mkdir("log", 0755);
chdir("log");

// time input
t = time(NULL);
strftime(year, sizeof(year), "%Y", localtime(&t));
strftime(nowtime, sizeof(nowtime), "%Y,%m,%d,%a,%H,", localtime(&t));

snprintf(fname,12,"%swat.csv",year);

//csv file check and write item string
if ((fp = fopen( fname , "r")) == NULL) {
fp = fopen(fname,"a");
fprintf(fp,"ID,year,month,day,week,hour,wat\n");
fclose(fp);
flag1 = 1;
line = 1;
}

 

//final raw count and \n find
if(flag1 == 0){
while (fgets(buf, BUF_SIZE, fp) != NULL) {
line++;
}
//printf("buf=%s\n",buf);

p = strstr(buf,"\n");
if(NULL == p){
fp = fopen ( fname, "a");
fprintf(fp,"-1\n");
fclose(fp);
}
}

 

// write nowtime
snprintf(tname,32,"%05d,%s",line,nowtime);

if( (fp = fopen( fname , "a" )) == NULL) {
printf("can not open write file.");
return -1;
}

fprintf(fp,"%s",tname);
fclose(fp);

wat =0;
total=0;
i=0;
j=0;
total_m=0;

if(( fd = wiringPiI2CSetup(DEV_ADDR))== -1){
printf("not I2C\n");
return -1;
}

// AC100V current measure
do {
do{

volt=0;
rms=0;
amp=0;
data=0;

wiringPiI2CWriteReg16(fd,0x01,0xe3c1);
data = wiringPiI2CReadReg16(fd,0x00);
//printf("data=%x ",data);

volt = ((data & 0x00ff00ff) << 8) | ((data >> 8) & 0x00ff00ff);

if(volt < 32767){
//printf("%x ",volt);
rms = volt * 0.1875;
amp = (rms * 800)/(100 * 0.92);

//printf("V= %d [mV] , A=%d [mA] \n",rms,amp);
}
else{
//printf("%x minus",volt);
volt = 65534 - volt;
rms = volt * 0.1875;
amp = (rms * 800)/(100 * 0.92);
//printf("V= %d [mV] , A=%d [mA] MinusValue\n",rms,amp);
}

wat = 100 * amp/1000;
//printf("W= %d \n ",wat);
total = total + wat;
i++;
total_m = total_m + 1;
}while (i < 49);
//printf("%d\n",j);
j++;
i=0;
delay(9950);
} while(j < 359);

total = total /total_m;
//printf("Total Wat Average=%d %d\n",total,total_m);
if( (fp = fopen( fname , "a" )) == NULL) {
printf("can not open write file.");
return -1;
}
fprintf(fp,"%d\n",total);
fclose(fp);
}



●結果
@raspberrypi:~$ cc -o i2ctest smartmeter.c -lwiringPi
@raspberrypi:~$ /home/pi/temp/i2ctest
yuichi@raspberrypi:~$ ls
log temp
yuichi@raspberrypi:~$ cd log/
yuichi@raspberrypi:~/log$ ls
2021wat.csv
yuichi@raspberrypi:~/log$ cat 2021wat.csv
ID,year,month,day,week,hour,wat
00001,2021,01,31,Sun,19,8

↑ID,年,月,日,曜日,時間(0-23),消費電力(W)

ファイル(FTPの送信マクロも入れてます。)
ワットチェッカープログラム(C_file)


cronでCSVファイルのアップデート

③のcronのCSVファイルのアップデートは下記のように行う。タイミングは動いていないときを狙う。
↓をクローンに追加
「59 23 * * 0 /home/yuichi/script/script.sh」
「file」というファイルがFTPでアクセスするためのファイルになります。IDとかPASSを設定するために中身をいじる必要があります。

スクリプト等は添付の「ワットチェッカープログラム(C_file)」に入っています。→(fileとscript.sh)

・毎週日曜日の23時59分00秒にCSVをWebにUPする。
・59分50秒で消費電力のログが保存されるため、55秒の時点で行うようにしたかったですが無理なのであきらめました。本当は値取得ファイルとアップデートするファイルを分けて、やるべきでしたが今更変えるの面倒なので放置します。


Webでグラフ表示

④のWeb画面でのグラフ表示は下記サイトになります。

Web画面でグラフ表示




■表示について
・CSVファイルはRaspberrypiからFTPでUPされたものが同じフォルダにありそれを読んでいます。
・lengthは最終日から何時間分の表示をするかの値になります。(168は24時間*7日)
・保存されている値より大きい場合はalertを出すようにしており、そこに書かれている数値以下にすればOKです。
・数値は私の部屋の一部の消費電力値になります。更新が止まっているときはRaspberryPiでの取得を止めたときになります

■Javascriptを初めて使って今回わかったこと
・変数がfunctionを跨ぎたいときはfunction外に変数を設ければいける。
(function内で変数を作ると、Functionを出た瞬間になくなる。)

・Javascriptをローカル環境で確認するにはhttpserverを立てる必要があった。Pythonで簡単にできる。
 「python -m http.server 8000」

・CSVはボタンを押すタイミングで読み込ませる方法はonloadが間に合わないのか、ボタンを2度押しする必要があった。
よくよく考えればCSVファイルの更新は1週間に一度だけで毎回変わることがないので、最初のHTML読み込み時にgetCSV
させるようにしたら問題解決しました。
しかしCSVが2021年は2021wat.csv,2022年用は2022wat.csvファイルが作成されるため、ボタンを押したときにCSV読み込みも対応をしたいと思うが、ボタン2度押ししないと反映されないのと、ファイルが存在しない年を読み込んだ後に存在するファイルを読み込もうとしても元に戻ることができないバグをどうしても修正できなかったので諦めました。
(コピペプログラマの限界…javascript難しい…)


[参考]
UM10204 I2C バス仕様およびユーザーマニュアル (nxp.com)
決まった時間に処理する | Make. (bcde.jp)
時間情報の取得 time() - 時間の扱い - 碧色工房 (mm2d.net)
【C言語】time関数|通算秒、UTC、JST時刻を取得するプログラム | なしブロ (nashiza-blog.com)
C言語でテキストファイルの行数を数える方法 – かひわし4v1.memo (myhome.cx)
ラズベリーパイでCronを使って定期処理を行う方法、ハマりポイントやCronが動かない場合の対処方法など | Keep it up! (k99-tech.com)
ディレクトリの基本操作 | LinuxC
Chart.jsでグラフを更新するサンプル(ボタンをクリック) - Qiita
javascriptでcsvファイルを読み込むには|Jin@ICTインストラクター|note
HTML5/フォーム/input要素 数値の入力欄を作る - TAG index
JavaScript 数値入力欄の値を取得/設定する | ITSakura