akinoのアイコン画像
akino 2024年01月29日作成
製作品 製作品 閲覧数 252
akino 2024年01月29日作成 製作品 製作品 閲覧数 252

スマートメータと気温センサの情報からブレーカー落ちを警告、そして見守りシステムへ

スマートメータと気温センサの情報からブレーカー落ちを警告、そして見守りシステムへ

概要

現在の使用電力と外気温から冬期に多いブレーカー落ちを予測し警告、また使用電力の推移で家人の安否を確認できるシステムの構築を目指しました。

  • SPRESENSEにWi-SUN Add-onボードを載せ、スマートメータに接続し電力値を常時モニターする。
  • この電力値と室内外気温から、(特に冬期の暖房使用時に)電力消費の大きなエアコンによるブレーカー落ちが予想される場合は警告を発しこれを回避させる。
  • また、電力値推移に通常生活レベルの変動がないときは、家人に何らかのトラブルが予想されるためアラーム通知を送信する(見守り、アライブ信号)。

実現させたいこと、その背景

  1. 寒冷地では冬季の電力使用量が多くなり、突発的な電力増加で契約電力をオーバーして「ブレーカー落ち」となることがあります。特にエアコンは、暖房運転での起動時の電力が大きくその可能性が高くなります。これを防止するために、現在の電力値と室内外の気温(エアコン温度設定値との差が大きいほど消費電力は大きくなる)からエアコンの起動時電力を予測し、ブレーカー落ちが予想されるときは警告を発し注意喚起を行います。この警告があれば、他の機器の使用を停止させてからエアコンを起動するなどの対処ができます。
    • ブレーカー落ちには、契約アンペア数を上げる手もありますが、当家の場合その発生は子供たちが帰省する年末年始休み中がほとんどです。そのために普段の料金が高くなるのは避けたいです。落ちるのは電気ファンヒータ使用中にエアコンを起動した場合が多く、この場合も気温が高めのときは落ちません。またエアコンの連続運転中に電気ファンヒータを入れても落ちません。いかにエアコンの暖房時起動電力が大きいかが実感できます。
    • 当地は盆地で標高800m超、雪よりが寒さが厳しいところです。暖冬傾向とは言え朝晩、特に朝の冷え込みがきつく、マイナス10度近いことも珍しくはありません。このような地域では水道の屋外配管部には 凍結防止帯 というヒーターが巻かれます。当家の場合、これらがすべて通電されると実測で330Wになり、これも消費電力を底上げしています。サーモスタットで温度検知しオンオフはされますが、閾値は当然高めで、冬場の電気料金の高さにびっくりすることもあります。そのため このような製品 も当地では必需品です。しかし、こちらもまだ閾値は高めです。
      (実際には、水道管内は水圧がかかっているので、マイナス3度くらいまで凍結しないようです。)
    • 現在、電力使用量は電力会社サイトからの他に、設置した Cube-J1 でも確認できてはいます。但し、電力会社のものは1時間単位で遅れたものですし、またCube-J1はスマホアプリからリアルタイムで確認はできますが、そのデータを活用する手段は提供されていません。
  2. 通常の生活を続けていれば、電力消費量には必ず定型的なパターンやピーク的な電力消費があるはずです。電力値の推移をモニターし、このようなピーク消費などが設定時間内(例えば8時間)に発生しない場合は、(不在時を除き)家人に何かあったものと判断できると考えました。そのような場合に、家人の縁者にアラート通知(メール、LINE等)を発信すれば、独居や老人世帯でのいわゆる「見守りシステム」として利用することができると思います。

構成

全体の構成は下図の通りです。

  • メインボードへはW5500-Etherボードをアドオンしています。
  • Wi-SUNボードは、W5500-Etherのピンソケットへジャンパー線で接続しています。
  • 両ボードで競合するGPIOは、Wi-SUN側を変更し別端子に接続しています。
  • 確認用にLCDパネルを設置、他に室内用として温湿度センサ、警告灯を外付けしています。
  • 外気温は、ESP8266+温度センサ からWi-Fi接続でラズパイに送信しています。
  • スマートメータから電力値をWi-SUNで取得し、温度値とともにラズパイにMQTTで送信します。
  • データの管理、「見える化」(ブラウザでグラフ表示)、アラームの発報などはラズパイで行います。

全体構成図

接続情報

メインボード
拡張ボード

使用部品

製品名 品番 メーカ
SPRESENSE SPRESENSE SONY
Wi-SUN Add-onボード SPRESENSE-WiSUN-EVK-701 Rohm
有線ネットワーク拡張ボード for Spresense W5500-Ether クレイン電子
ILI19341搭載2.8"SPI制御タッチパネル付TFT液晶 MSP2807 秋月電子
※以上、モニターとして提供して頂いた製品
温湿度センサー(室内用) BME280 秋月電子
温度センサ(室外用) ADT7140ESP8266 秋月電子

アドオンボードについて

  • ethernet(W5500-Ether) と Wi-SUN の2枚のアドオンを使用するので共存させるのに一工夫が必要でした。
  • 今回は W5500-Ether をメインボードにアドオンし、Wi-SUN はブレッドボードに挿して W5500-Ether から配線しました。
  • W5500-Etherボードにはメイン基板からの全端子にピンソケットが搭載されています。
    これによりメイン基板へのアドオンを、他にもう1枚載せることができます。
    しかしながらその一端にRJ-45コネクタが実装されており、Wi-SUNボードはこれと干渉してしまい載せることができませんでした。
  • また W5500-Ether は SPI、Wi-SUN はシリアルを使用しますが、回路図を見るといくつかの GPIO が競合しており、ピンソケットに下駄をはかせてRJ-45をよけても、さらに一工夫が必要と分かりました。
  • なお、アドオンボードのピン形状はブレッドボードへの挿抜は考慮していないようで、径も長さも適していません(薄く短い)。さらに使用したブレッドボードのピンピッチが2.5mmだったようで、まっすぐ深く挿せず気を付けていないと浮いた状態になり、度々悩まされました。

機能

動作の詳細について説明します。

全体の流れ

  1. SPRESENSE+拡張ボード+W5500-Ether+Wi-SUN Add-onボード+LCDパネルを一体として主機器としています。(以下、Aと記す)
  2. AからWi-SUNでスマートメータに接続、瞬時電力値を取得します。
  3. Aには別途、温室度センサー(BME280)をI2C接続し室温を取得します。
  4. 常時、AのLCDパネルに電力値、室内外気温などを表示、設定の切り替え等も可能にします。
  5. AのW5500-Etherで、別途設置したラズパイと通信します。
  6. 測定データの管理、ブレーカー落ちの予測、ブラウザによる「見える化」などはラズパイで行います。
  7. MQTTのBrokerはラズパイに置き、Aとラズパイとの測定値の送受はMQTTを使います。
  8. 外気温は、別途ESP8266+温度センサー(ADT7410、電池駆動)で取得し、MQTTでラズパイに送信します。
  9. 各測定値からの予測でブレーカー落ちの危険があるときは、ラズパイからAにアラート信号を送信します。
  10. Aはこれを受けて警告灯を点灯します。
  11. 見守り機能の「安否確認」は、ラズパイで行い、異常と判断した場合はアラート通知を送信します。

見える化

  • スマートメータからの瞬時電力値と気温をグラフ化し、ブラウザで見ることができるようにしました。
  • 地球温暖化の中、節電意識を高める効果もあると思いました。
  • 下図がその表示例です。
    (現在試験中のため、温度センサはエアコンがある部屋とは別室にあります。また、Wi-SUNもしくはMQTTの通信でリカバリー不足があるようで、時々停止していることがあり一部dataが欠落しています。検討中です!)

リアルタイムでブラウザから確認、その画面例

Ambientでも見られるようにした、その画面例

プログラム開発

  • SPRESENSEを使うのは初めてでした。Arduino IDEで開発しました。

Wi-SUN によるスマートメータからの電力値取得

  • WI-SUNデバイスも使うのは初めてでした。
    elchika内にある 84mot さんの投稿を参考にさせて頂きました。というかほぼそのまま使わせてもらいました(感謝します)。
    温度測定、MQTT によるラズパイへの送受信を追加しました。
     Spresenseで家庭の消費電力値をスマートメータから取得してEVの充電を制御するデバイス
  • 84mot さんも書かれていますが、Rohm さんの下記資料は大変参考になりました。これと84mot さんのコードとを突き合わせながら理解していきました。84motさんがDEBUG用コードまで載せて頂いていたので、動かしながら理解することができ非常に助かりました。
     BP35C0-J11 B ルート通信について Rohm
  • Wi-SUNについては、ネットにもチャレンジされた方の記事が多いですが、多くは BP35A1 や BP35C0 を使用したものでした。これらはテキストベースでコマンドを送るようでしたので、開発は比較的安易ではないかと考えていました。が、いざ始めてみるとこのアドオンボードの仕組みは別物のようだと気づき焦りました。1週間くらいドタバタする中で 84mot さんの記事にたどり着き、やっと明かりが見えました。

ethernet (W5500-Ether)

  • クレイン電子さんから提供されているW5500-Ether用のサンプルコード(下記)一式を同一フォルダに置き、サンプルコードを参照しながらMQTT部を入れ込んで作成しました。
     Arduino IDE開発用 Ethernet-spi5.ino

温(湿)度センサ (BME280)

  • SWITCHSCIENCEさんの解説にあるものを参考にしました。
     SWITCHSCIENSE:
      BME280搭載 温湿度・気圧センサモジュールの使い方
  • 今回、気圧は使っていませんが、標高800m超の当地は 920hPA 程度(ネットの天気情報では 1000 くらいのとき)と測定されしばらく悩みました。現地気圧と海面気圧の違いでした。

MQTT

見える化

  • Google グラフのツールの google chart と、php を使ってhtmlを書きました。
     google chartphp
  • 現在は、1分に一回のdata(24h x 60m =1440回)の表示です。
  • 電力値の取得自体は、3秒間隔くらいで可能でした。

ソースコード

Wi-SUN部

  • 本体のコードはmota2さんのものです。
  • 瞬時電力値を取得後に BME280で温度値を測定、電力値と温度値を MQTT で publish しています。

温(湿)度センサ (BME280)

  • 以下のコードを単独で動作確認し、
  • setup() を setup_bme280、loop() を loop_bme280() に変更、本体のソースコードと同一のフォルダに置きます。
  • loop_bme280() は 温度値を持って return するように変更します。
  • 本体コード側で、setup()時に setup_mqtt()を呼び、loop()内で loop_bme280() を呼び温度値を測定します。

室内気温の取得

#include <Wire.h> #define BME280_ADDRESS 0x76 unsigned long int temp_raw; // 実測data signed long int temp_cal; // 補正data double temp_act; // 変換data uint8_t osrs_t = 1; // Temperature oversampling x 1 uint8_t osrs_p = 1; // Pressure oversampling x 1 uint8_t osrs_h = 1; // Humidity oversampling x 1 uint8_t mode = 0; // Normal mode (0:Sleep、1 or 2:Forced) uint8_t t_sb = 0; // Tstandby 1000ms (Normal mode以外は0でよい) uint8_t filter = 0; // Filter off uint8_t spi3w_en = 0; // 3-wire SPI Disable uint8_t ctrl_meas_reg = (osrs_t << 5) | (osrs_p << 2) | mode; // 設定値生成 uint8_t config_reg = (t_sb << 5) | (filter << 2) | spi3w_en; uint8_t ctrl_hum_reg = osrs_h; struct trim_data { // ======= 補正data ====== uint32_t crc32; // 補正データのcrc値 uint16_t dig_T1; // 16bitの非負整数 int16_t dig_T2; // 16bit整数 int16_t dig_T3; uint16_t dig_P1; int16_t dig_P2; int16_t dig_P3; int16_t dig_P4; int16_t dig_P5; int16_t dig_P6; int16_t dig_P7; int16_t dig_P8; int16_t dig_P9; int8_t dig_H1; // 8bit整数 int16_t dig_H2; int8_t dig_H3; int16_t dig_H4; int16_t dig_H5; int8_t dig_H6; } tdata; void setup() { Wire.begin(); writeReg(0xF2,ctrl_hum_reg); // 各レジスタの設定 writeReg(0xF4,ctrl_meas_reg); // Sleep mode writeReg(0xF5,config_reg); Serial.begin(115200); delay(100); Serial.println("\nbme280-9 start\n"); readTrim(); //補正データを読込 } void loop() { writeReg(0xF4, ctrl_meas_reg + 1); // Foced mode、deepsleep使用時は省エネ少 while (readReg(0xF3) & 0b00001000) { // bit3 1:測定中、0:終了 (待ち時間≒10msだった) delay(1); } readData(); temp_cal = calibration_T(temp_raw); // 実測値を補正 temp_act = (double)temp_cal / 100.0; // 変換 Serial.print("TEMP : "); Serial.println(temp_act); } // ================================================= void readTrim() { // 補正値の読み込み uint8_t data[32],i=0; Wire.beginTransmission(BME280_ADDRESS); Wire.write(0x88); // tdata.dig_T1 をセット Wire.endTransmission(); Wire.requestFrom(BME280_ADDRESS,24); // 連続24byteを要求、T1-3 と P1-9 while(Wire.available()){ // read()で読取可能なbyte数を返す data[i] = Wire.read(); i++; } Wire.beginTransmission(BME280_ADDRESS); Wire.write(0xA1); // tdata.dig_H1 をセット Wire.endTransmission(); Wire.requestFrom(BME280_ADDRESS,1); // H1 data[i] = Wire.read(); i++; Wire.beginTransmission(BME280_ADDRESS); Wire.write(0xE1); // tdata.dig_H2 をセット Wire.endTransmission(); Wire.requestFrom(BME280_ADDRESS,7); // 連続7byteを要求、H2-6 while(Wire.available()){ data[i] = Wire.read(); i++; } tdata.dig_T1 = (data[1] << 8) | data[0]; // 各補正値を生成(合成) tdata.dig_T2 = (data[3] << 8) | data[2]; tdata.dig_T3 = (data[5] << 8) | data[4]; tdata.dig_P1 = (data[7] << 8) | data[6]; tdata.dig_P2 = (data[9] << 8) | data[8]; tdata.dig_P3 = (data[11]<< 8) | data[10]; tdata.dig_P4 = (data[13]<< 8) | data[12]; tdata.dig_P5 = (data[15]<< 8) | data[14]; tdata.dig_P6 = (data[17]<< 8) | data[16]; tdata.dig_P7 = (data[19]<< 8) | data[18]; tdata.dig_P8 = (data[21]<< 8) | data[20]; tdata.dig_P9 = (data[23]<< 8) | data[22]; tdata.dig_H1 = data[24]; tdata.dig_H2 = (data[26]<< 8) | data[25]; tdata.dig_H3 = data[27]; tdata.dig_H4 = (data[28]<< 4) | (0x0F & data[29]); tdata.dig_H5 = (data[30] << 4) | ((data[29] >> 4) & 0x0F); tdata.dig_H6 = data[31]; } void readData() { int i = 0; uint32_t data[8]; Wire.beginTransmission(BME280_ADDRESS); // 送信処理開始 Wire.write(0xF7); // dataをキューへ送り(0xF7は測定dataの先頭) Wire.endTransmission(); // 送信実行 Wire.requestFrom(BME280_ADDRESS,8); // data 8byte を読み込む while(Wire.available()){ data[i] = Wire.read(); i++; } temp_raw = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4); // 温度 20bit } void writeReg(uint8_t reg_address, uint8_t data) { Wire.beginTransmission(BME280_ADDRESS); Wire.write(reg_address); Wire.write(data); Wire.endTransmission(); } uint8_t readReg(uint8_t reg_address) { uint8_t reg_data; Wire.beginTransmission(BME280_ADDRESS); Wire.write(reg_address); Wire.endTransmission(); Wire.requestFrom(BME280_ADDRESS, 1); reg_data = Wire.read(); return reg_data; } signed long int calibration_T(signed long int adc_T) { // キャリブレーション後のdata signed long int var1, var2, t_fine, T; var1 = ((((adc_T >> 3) - ((signed long int)tdata.dig_T1<<1))) * ((signed long int)tdata.dig_T2)) >> 11; var2 = (((((adc_T >> 4) - ((signed long int)tdata.dig_T1)) * ((adc_T>>4) - ((signed long int)tdata.dig_T1))) >> 12) * ((signed long int)tdata.dig_T3)) >> 14; t_fine = var1 + var2; T = (t_fine * 5 + 128) >> 8; return T; }

MQTT送受信

  • 以下のコードを単独で動作確認し、
  • setup() を setup_mqtt、loop() を loop_mqtt() に変更、本体のソースコードと同一のフォルダに置きます。
  • 本体コード側で、setup()時に setup_mqtt()を呼び、loop()内で電力値と温度値を loop_mqtt() で publish します。

MQTTで送受信

#include <SPI.h> #include "src/Ethernet.h" //Add-onボード用(クレイン電子提供) #include "src/M24C64.h" #include <PubSubClient.h> #define BAUDRATE (115200) #define USE_SPRESENSE_SPI5 //Add-on 用はSPI5を定義(socket.cpp内にて使用) #define TFT_BL 7 //BLピンに D07 を指定、LCD(back light)のon/off #define ALARM 4 //ALARM用のLEDに D04 を指定(L active) M24C64 eep; //Add-onボード搭載EEPROMへのアクセス用 byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0x98, 0x01 }; IPAddress ip(192, 168, ***, ***); //network IPAddress mqtt_server(192, 168, ***, ***); //MQTT Broker EthernetClient ethClient; PubSubClient mqttClient(ethClient); String payload; //pubのmessage部分 void setup() { pinMode(TFT_BL, OUTPUT); //TFT_BL でLCDパネルの表示on/off digitalWrite(TFT_BL, HIGH); //'H'で点灯 pinMode(ALARM, OUTPUT); //ALARM で警告灯をon/off('L'で点灯) Serial.begin(BAUDRATE); while (!Serial) { }; //接続待ち w5500_init(); //ethernet(w5500)の初期化と接続 mqttClient.setServer(mqtt_server, 1883); // MQTT server を設定 mqttClient.setCallback(subscribeReceive); // ラズパイからの alarm を subscribe reconnect(); // broker に接続(リトライあり) Serial.println(""); } void loop() { if(!mqttClient.connected()) { reconnect(); } mqttClient.loop(); // loop の先頭に置く payload = String(pw) + "," + String(temp, 1); if(mqttClient.publish("e7t", (char*)payload.c_str())) { Serial.println("Publish message success [" + payload + "]"); } else { Serial.println("Could not send message"); } } void reconnect() { while (!mqttClient.connected()) { Serial.println("Attempting MQTT connection >>> "); if (mqttClient.connect("spresense")) { Serial.println("MQTT connected"); mqttClient.subscribe("alrt1"); //subscribe する topic を設定 } else { Serial.print("failed, rc="); Serial.print(mqttClient.state()); Serial.println(". try again in 1 seconds"); delay(1000); } } } void subscribeReceive(char* topic, byte* payload, unsigned int length) { //subscribe受信時の処理 Serial.print("Topic: "); Serial.print(topic); Serial.print(" / Message: "); for(int i = 0; i < length; i ++) { Serial.print(char(payload[i])); } Serial.println(""); } void w5500_init(){ digitalWrite(21, LOW); //W5500-Ether用の初期化(RESET) delay(500); digitalWrite(21, HIGH); Serial.println("W5500 reset done."); eep.init(0x57); Serial.println("MAC read from on board eeprom."); //mac[] をEEPROMの内容に書換 for(int i=0;i<6;i++){ mac[i] = eep.read(i); Serial.print(mac[i],HEX); Serial.print(":"); } Serial.println(""); Ethernet.begin(mac, ip); //mac,ip で初期化 if (Ethernet.hardwareStatus() == EthernetNoHardware) { //w5500 の検出確認 Serial.println("Ethernet shield was not found."); while (true) { delay(1); } } if (Ethernet.linkStatus() == LinkOFF) { Serial.println("Ethernet cable is not connected."); } Serial.print("my spresense is at "); Serial.println(Ethernet.localIP()); }

ラズパイでのMQTT送受信、data保存 (python)

  • ラズパイで受信したdataは 60秒に1回、csvファイル に保存します。
  • 1日分を1ファイルにしています。

MQTT送受信、csvファイルに記録

import os import datetime import csv import codecs import paho.mqtt.client as mqtt import time #240121 書込みdata数減、60秒に1回にする host = '127.0.0.1' port = 1883 topic = 'e7t' topic_pub = 'alrt1' def on_connect(client, userdata, flags, respons_code): print('status {0}'.format(respons_code)) client.subscribe(topic) def publish(client): payload = 'overload' ret = client.publish(topic_pub, payload) if ret[0] == 0: print(f"publish `{payload}` for topic `{topic_pub}`") else: print(f"failed to publish for topic '{topic_pub}'") def on_message(client, userdata, msg): global start t = time.time() - start print(t) print(msg.topic + ' ' + str(msg.payload, "utf-8")) # pytho3 への対応 if t >= 60: mqtt_data = str(msg.payload, "utf-8") # pytho3 への対応 d = "{0:%y%m%d%H:%M:%S}".format(datetime.datetime.now()) # 24011810:48:11 p = d[:4] # 2401 <= dir name f = d[:6] # 240118 <= file name t = d[-8:] # 10:48:11 <= time if os.path.exists(p): pass else: os.mkdir(p) filename = p + '/' + f + '_' + msg.topic + '.csv' # 2401/240118_e7t.csv class CustomFormat(csv.excel): quoting = csv.QUOTE_ALL csv_file = codecs.open(filename, 'a', 'shift_jis') writer = csv.writer(csv_file, CustomFormat()) row = mqtt_data.split(',') # カンマ区切り、一旦分割(複数dataへの対応) row.insert(0, t) # 時間を先頭に挿入 row = tuple(row) # リストをタプルに変換 writer.writerow(row) csv_file.close() print(row) print(f"write to csv '{row}'") start = time.time() pw = int(row[1]) if pw >= 3000: # 閾値を超えたら発報、SPRESENSEでsubscribe publish(client) # --------------------------------------------------- if __name__ == '__main__': start = time.time() # csvへの書込みを60秒毎にする client = mqtt.Client(protocol=mqtt.MQTTv311) # Publisherと同様に v3.1.1を利用 client.on_connect = on_connect client.on_message = on_message client.connect(host, port=port, keepalive=60) client.loop_forever() # 待ち受け状態にする

※以下は Ambient への送信部 (python)
 上記とは取り合えず分けて確認しました。

Ambientへの送信

# -*- coding: utf-8 -*- import time import paho.mqtt.client as mqtt import ambient host = '127.0.0.1' port = 1883 topic = 'e7t' channelID = ***** writeKey = '****************' ambi = ambient.Ambient(channelID, writeKey) def on_connect(client, userdata, flags, respons_code): print('status {0}'.format(respons_code)) client.subscribe(topic) def on_message(client, userdata, msg): global start print(msg.topic + ' ' + str(msg.payload, "utf-8")) # pytho3 への対応 mqtt_data = str(msg.payload, "utf-8") # topicは'str'、payloadは'byte' row = mqtt_data.split(',') print(row) t = time.time() - start print(t) if t >= 60: # 60秒に1回送信 dt = list(map(float, row)) # 文字 > 数値変換 r = ambi.send({"d1": dt[0], "d2": dt[1]}) start = time.time() print('send data to ambinet') # Ambientの制限事項(最短送信間隔は5秒、最大登録数は3000件/日) # --------------------------------------------------- if __name__ == '__main__': start = time.time() client = mqtt.Client(protocol=mqtt.MQTTv311) # Publisherと同様に v3.1.1を利用 client.on_connect = on_connect client.on_message = on_message client.connect(host, port=port, keepalive=60) client.loop_forever() # 待ち受け状態にする

今後

  • リストした機能に未達が残っていますので、これを仕上げたいと思います。
  • Wi-SUN もしくは MQTT の通信が停止することがあるので、この改善をしたいと思います。
  • Wi-SUNボードを含め一体化し、ケースに入れて設置しようと思います。
  • SPRESENSEの特徴を活かした改良(例えばマルチコアを使う)をしたいと思います。
  • エアコンなどを ECHONET-LITE を使い制御し、能動的な連携を検討したいと思います。

感想

SPRESENSEを使うのは初めてでした。多くのモニター品を提供頂き大変感謝しております。
ハードも持っていなかったので、事前に情報を調べていたとはいえ短期間での開発は大変でした。
しかし、SPRESENSEもWi-SUNも使ってみたいと思っていたものでしたので、よい経験ができました。ありがとうございました。
未達部分を残していますが、モニター品を提供頂いておりますので投稿いたしました。

  • akino さんが 2024/01/29 に 編集 をしました。 (メッセージ: 初版)
ログインしてコメントを投稿する