自宅状況の見える化

# はじめに
- これまで、以下のような調査・実験をやってきました。そのまとめ記事となります。
    - [Wi-SUN対応無線モジュール（BP35C0-T01）とラズパイを繋ぐ。（制作・実験編）](http://qiita.com/katsumin/items/acd567e9b8e81c691ed4)
    - [エコキュートのECHONETパケットを覗いてみた。](http://qiita.com/katsumin/items/14970c340c6dc65dbd93)
    - [京セラモニタ（PMD47A）とのシリアル通信データ解析](http://qiita.com/katsumin/items/27fbae6b61edd1fa8167)
    - [Upverterで回路・PCB設計して、基板を起こしてみた。](http://qiita.com/katsumin/items/5e7dcb7ccb4d04efffee)

---
# 最終形態
- 最終的に、４種類のグラフをWeb経由で閲覧できるようになりました。
    1. 瞬時電力計測値
    2. 積算電力計測値
    3. エコキュート監視
    4. 京セラモニタ
- 下のスクリーンショットは、2017/5/31時点での直近二日間の時系列グラフになります。

![FireShot Capture 1 - Grafana - 自宅状況_ - http___katsumin.tk_3000_dashboard_db_zi-zhai-zhuang-kuang.png](https://qiita-image-store.s3.amazonaws.com/0/166435/acce78db-5cba-47d4-2356-848fde3acca6.png)

---

# システム説明
- 装置構成
    1. 情報収集用ラズパイ
        - スマートメータからの情報収集
        - エコキュートからの情報収集
        - 京セラモニタからの情報収集
        - 情報保存用サーバへの送信
    1. 情報保存用サーバ（データベース）
        - 「[influxDB](https://portal.influxdata.com/downloads)」を使っており、情報収集用ラズパイから送られてくるデータ群をデータベースに保存します。
    1. 可視化サーバ（Webサーバ）
        - 「[Grafana](https://grafana.com/grafana/download)」を使っており、情報保存用サーバのデータを参照して、時系列グラフを生成し、Webブラウザに表示します。

---
# スマートメータからの情報収集
- [Wi-SUN対応無線モジュール（BP35C0-T01）とラズパイを繋ぐ。（制作・実験編）](http://qiita.com/katsumin/items/acd567e9b8e81c691ed4)で作成した基板を使っています。
- 設定ファイル
    - 人が設定するものは、下記の３点。（addressは、接続プログラムが設定する）
        1. bid（Bルートサービス開通の際に受け取った認証ID）
        1. pwd（Bルートサービス開通の際に受け取ったパスワード）
        1. url（influxDBサーバのURL）

```config.ini
[smartmeter]
address = [スマートメータのアドレス]
pwd = [Bルートパスワード]
bid = [Bルート認証ID]

[server]
url = [influxDBサーバのURL]
```

- スマートメータ接続プログラム
    - 一度だけ実行するプログラムで、スマートメータを検索し、接続する。検索したスマートメータのアドレスを、設定ファイル（config.ini）に書き込む。

```python:bp35c0_join2.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import print_function

import sys
import serial
import time
import ConfigParser

iniFile = ConfigParser.SafeConfigParser()
iniFile.read('./config.ini')

args = sys.argv

# シリアルポート初期化
serialPortDev = '/dev/ttyAMA0'
ser = serial.Serial(serialPortDev, 115200)

# 関数
def waitOk() :
    while True :
        line = ser.readline()
        print(line, end="")
        if line.startswith("OK") :
            break

# コマンド送信
while True :
    ser.write("SKVER\r\n")
    line = ser.readline()
    if line.startswith("OK") :
        break

pwd = iniFile.get('smartmeter', 'pwd')
ser.write("SKSETPWD C {0}\r\n".format(pwd))
waitOk()

bid = iniFile.get('smartmeter', 'bid')
ser.write("SKSETRBID {0}\r\n".format(bid))
waitOk()

scanRes = {}
ser.write("SKSCAN 2 FFFFFFFF 6 0\r\n")
while True :
    line = ser.readline()
    print(line, end="")
    if line.startswith("EVENT 22") :
        break
    elif line.startswith("  ") :
        cols = line.strip().split(':')
        scanRes[cols[0]] = cols[1]

ser.write("SKSREG S2 " + scanRes["Channel"] + "\r\n")
waitOk()

ser.write("SKSREG S3 " + scanRes["Pan ID"] + "\r\n")
waitOk()

ser.write("SKLL64 " + scanRes["Addr"] + "\r\n")
while True :
    line = ser.readline()
    print(line, end="")
    if not line.startswith("SKLL64") :
        ipv6Addr = line.strip()
        break
print(ipv6Addr)

iniFile.set('smartmeter','address',ipv6Addr)
fp=open('./config.ini','w')
iniFile.write(fp)
fp.close()

command = "SKJOIN {0}\r\n".format(ipv6Addr)
ser.write(command)

while True:
    line = ser.readline()
    print(line, end="")
    if line.startswith("EVENT 24") :
        break
    elif line.startswith("EVENT 25") :
        break
```

- 受信プログラム
    - 常駐して、瞬時電力値応答パケットと定時積算電力値パケットを受け取り、influxDBサーバにデータを送る。

```python:bp35c0_rcv2.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import print_function

import sys
import serial
import datetime
import locale
import time
import commands
import ConfigParser

iniFile = ConfigParser.SafeConfigParser()
iniFile.read('./config.ini')

# シリアルポート初期化
serialPortDev = '/dev/ttyAMA0'
ser = serial.Serial(serialPortDev, 115200)

k = 0.1

url = iniFile.get('server', 'url')
def influxUp(text) :
    d = datetime.datetime.today()
    f = open('power_tmp.txt', 'w')
    f.write(text)
    f.close()
    commands.getoutput("curl -i XPOST '{0}:8086/write?db=smartmeter' --data-binary @power_tmp.txt".format(url))
    commands.getoutput("cat power_tmp.txt >> power{0:04d}{1:02d}{2:02d}.txt".format(d.year,d.month,d.day))

def rcv_e1(res) :
    if len(res) < 2+1*2 :
        return 0
    PDC = res[0:0+2] 
    EDT = res[2:2+2]    # 最後の1バイト（16進数で2文字）が積算電力量単位
    if EDT == "00" :
        k = 1
        print("1kWh")
    elif EDT == "01" :
        k = 0.1
        print("0.1kWh")
    elif EDT == "02" :
        k = 0.01
        print("0.01kWh")
    elif EDT == "03" :
        k = 0.001
        print("0.001kWh")
    elif EDT == "04" :
        k = 0.0001
        print("0.0001kWh")
    elif EDT == "0A" :
        k = 10
        print("10kWh")
    elif EDT == "0B" :
        k = 100
        print("100kWh")
    elif EDT == "0C" :
        k = 1000
        print("1000kWh")
    elif EDT == "0D" :
        k = 10000
        print("10000kWh")
    else :
        print(u"unknown: {0}".format(hex))
    return 4

def rcv_e7(res) :
    # 内容が瞬時電力計測値(E7)だったら
    if len(res) < 2+4*2 :
        return 0
    PDC = res[0:0+2] 
    hexPower = res[2:2+8] # 最後の4バイト（16進数で8文字）が瞬時電力計測値
    intPower = int(hexPower, 16)
    if intPower > 0x80000000 :
        intPower = intPower - 0x100000000
    print("瞬時電力計測値:{0}[W]".format(intPower))
    timestamp = int(time.mktime(d.timetuple())) * 1000000000
    influxUp("power value={0} {1}\n".format(intPower,timestamp))
    return 10

def rcv_eaeb(res,prefix) :
    if len(res) < 2+11*2 :
        return 0
    PDC = res[0:0+2]
    EDT = res[2:]
    intYear = int(EDT[0:0+4],16)
    intMonth = int(EDT[4:4+2],16)
    intDay = int(EDT[6:6+2],16)
    intHour = int(EDT[8:8+2],16)
    intMin = int(EDT[10:10+2],16)
    intSec = int(EDT[12:12+2],16)
    intValue = int(EDT[14:14+8],16)
    d = time.strptime("{0:04d}/{1:02d}/{2:02d} {3:02d}:{4:02d}:{5:02d}".format(intYear,intMonth,intDay,intHour,intMin,intSec), "%Y/%m/%d %H:%M:%S")
    timestamp = int(time.mktime(d)) * 1000000000
    print("正方向定時積算電力量: {0:04d}/{1:02d}/{2:02d} {3:02d}:{4:02d}:{5:02d} {6:.1f}[kWh]".format(intYear,intMonth,intDay,intHour,intMin,intSec,float(intValue)*k))
    influxUp("{0}power value={1} {2}\n".format(prefix,float(intValue)*k,timestamp))
    return 24

while True:
    line = ser.readline()         # ERXUDPが来るはず
    print(line, end="")
    d = datetime.datetime.today()

    if line.startswith("ERXUDP") :
        cols = line.strip().split(' ')
        res = cols[9]   # UDP受信データ部分
        ehd1 = res[0:0+2]
        ehd2 = res[2:2+2]
        tid = res[4:4+4]
        EDATA = res[8:]
        seoj = EDATA[0:0+6]
        deoj = EDATA[6:6+6]
        offset = 24
        if seoj == "028801" :
            ESV = EDATA[12:12+2]
            if ESV == "72" or ESV == "73" :
                # スマートメーター(028801)から来た応答(72)なら
                # スマートメーター(028801)から来たプロパティ通知(73)なら
                OPC = EDATA[14:14+2]
                iOPC = int(OPC, 16)
                offset = 16
                step = 0
                for index in range(iOPC) :
                    EPC = EDATA[offset:offset+2]
                    offset = offset + 2
                    if EPC == "E7" :
                        # 内容が瞬時電力計測値(E7)だったら
                        step = rcv_e7(EDATA[offset:])
                    elif EPC == "E1" :
                        step = rcv_e1(EDATA[offset:])
                    elif EPC == "EA" :
                        step = rcv_eaeb(EDATA[offset:],"+")
                    elif EPC == "EB" :
                        step = rcv_eaeb(EDATA[offset:],"-")
                    else :
                        print(u"Other EPC: {0}".format(EPC))
                    if step == 0 :
                        break
                    offset = offset + step
            else :
                print(u"Other ESV: {0}".format(ESV))
        else :
            print(u"Other SEOJ: {0}".format(seoj))
    else :
        print(u"Not ERXUDP")
```

- 送信プログラム
    - 瞬時電力値要求パケットを送る。
    - cronで、1分ごとに起動。

```python:bp35c0_e7.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import print_function

import sys
sys.path.append("/home/pi")
import serial
import time
import echonet

# シリアルポート初期化
serialPortDev = '/dev/ttyAMA0'
ser = serial.Serial(serialPortDev, 115200)

# コマンド送信
command = echonet.generateSmartMeterCommand("\xE7")
ser.write(command)
```

---
# エコキュートからの情報収集
- [エコキュートのECHONETパケットを覗いてみた。](http://qiita.com/katsumin/items/14970c340c6dc65dbd93)で解析した内容に基づいています。

- 受信プログラム
    - 常駐して、エコキュートからのECHONETパケットを受け取り、influxDBサーバにデータを送る。

```python:ec_rcv.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import struct
import sys
import socket
import datetime
import time
import commands
from contextlib import closing
import ConfigParser

iniFile = ConfigParser.SafeConfigParser()
iniFile.read('./config.ini')

url = iniFile.get('server', 'url')
def influxUp(text) :
    d = datetime.datetime.today()
    f = open('ec_tmp.txt', 'w')
    f.write(text)
    f.close()
    commands.getoutput("curl -i XPOST '{0}:8086/write?db=smartmeter' --data-binary @ec_tmp.txt".format(url))
    commands.getoutput("cat ec_tmp.txt >> ec{0:04d}{1:02d}{2:02d}.txt".format(d.year,d.month,d.day))


host = '127.0.0.1'
port = 3610
bufsize = 4096

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
with closing(sock) :
  sock.bind(('', port))
  while True :
    p = sock.recv(bufsize)
    print("{0}: {1}".format(len(p),[p]))
    tuple = struct.unpack('!BHBHBB',p[4:12])
    seoj = tuple[0] * 0x10000 + tuple[1]
    deoj = tuple[2] * 0x10000 + tuple[3]
    esv = tuple[4]
    opc = tuple[5]
    if seoj == 0x026b01 and deoj == 0x05ff01 and opc == 3 :
      tuple = struct.unpack('!BBHBBIBBH',p[12:])
      epc1 = tuple[0]
      pdc1 = tuple[1]
      edt1 = tuple[2]
      epc2 = tuple[3]
      pdc2 = tuple[4]
      edt2 = tuple[5]
      epc3 = tuple[6]
      pdc4 = tuple[7]
      edt3 = tuple[8]
      d = datetime.datetime.today()
      timestamp = int(time.mktime(d.timetuple())) * 1000000000
      text = "ecocute power={0},powerSum={1},tank={2} {3}\n".format(edt1,edt2,edt3,timestamp)
      print(text)
      influxUp(text)
```

- 送信プログラム
    - 積算電力値と残湯量の要求パケットを送る。
    - cronで10分ごとに起動。

```python:ec_snd.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
sys.path.append("/home/pi")
import socket
from contextlib import closing

# 送信コマンド生成
def generateEcocuteCommand(EPC) :
    echonetLiteFrame = ""
    echonetLiteFrame += "\x10\x81"      # EHD
    echonetLiteFrame += "\x00\x01"      # TID
    # EDATA
    echonetLiteFrame += "\x05\xFF\x01"  # SEOJ
    echonetLiteFrame += "\x02\x6b\x01"  # DEOJ
    echonetLiteFrame += "\x62"          # ESV(62:プロパティ値読み出し要求) 
    echonetLiteFrame += "\x03"          # OPC(3個)
    echonetLiteFrame += "\x84"
    echonetLiteFrame += "\x00"          # PDC
    echonetLiteFrame += "\x85"
    echonetLiteFrame += "\x00"          # PDC
    echonetLiteFrame += "\xE1"
    echonetLiteFrame += "\x00"          # PDC
    return echonetLiteFrame


host = [エコキュートのIPアドレス]
port = 3610
bufsize = 4096

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
with closing(sock) :
  sock.sendto(generateEcocuteCommand("\x84"), (host, port))
```

---
# 京セラモニタからの情報収集
- LPC810に書き込んだ、[I2C-UARTコンバータ(GitHub)](https://github.com/katsumin/LPC_i2c_uart)を経由して、京セラモニタとやり取りします。
- プロトコルは、[京セラモニタ（PMD47A）とのシリアル通信データ解析](http://qiita.com/katsumin/items/27fbae6b61edd1fa8167)で解析した内容に基づいています。
- 収集プログラム
    - 30分ごとの発電データ、30分毎の買電―売電データを取得して、influxDBサーバにデータを送る。
    - cronで、毎時10分と40分に起動。

```python:kyocera_dump.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import smbus
import threading
import time
import sys
import datetime
import commands


# 初期化（38400bps）
# 0:300, 1:600, 2:1200, 3:2400, 4:4800, 5:9600, 6:19200, 7:38400, 8:57600, 9:115200
event = threading.Event()
lock = threading.Lock()
i2c = smbus.SMBus(1)
i2c.write_byte(0x40, 0x07)

ack = ""
ack += "\x02\x06\x03"
nack = ""
nack += "\x02\x15\x03"


# 受信データ数を返す
def receiveCount() :
  lock.acquire()
  try :
    count = i2c.read_byte(0x40)
  finally :
    lock.release()
  return count


# データ受信
def receiveData() :
  lock.acquire()
  try :
    data = i2c.read_byte(0x41)
  finally :
    lock.release()
  return data


# データ送信
def sendData(data) :
  lock.acquire()
  try :
    i2c.write_byte(0x41, data)
  finally :
    lock.release()


# 受信データ列を返す
def receiveMultiData(len) :
  multiData = bytearray()
  count = 0
  timeout = 10
  while event.is_set() != True :
    pre = count
    count = receiveCount()
    # 要求するデータ数を超えていたら、データ引き取り
    if count >= len :
      multiData = bytearray(len)
      for i in range(len) :
        multiData[i] = receiveData()
      return multiData
#    else :
#      print "count: {0}".format(count)

    if pre == count :
      time.sleep(0.1) # 0.1s wait
      timeout = timeout - 1
      if timeout < 1 :
        print 'receive timeout'
        return multiData
    else :
      # 受信があれば、タイムアウトカウンタをリセット
      timeout = 10
  # 終了イベントで抜ける
  print 'receive exit'
  return multiData


# データ列を送信する
def sendMultiData(multiData) :
  for i in range(multiData.__len__()) :
    sendData(ord(multiData[i]))


# コマンド受信
def rcv_cmd() :
  data = ""

  # STX待ち
  while event.is_set() != True :
    stx = receiveMultiData(1)
    if stx.__len__() > 0 :
      if stx == "\x02" :
        data += stx
        break
    else :
      sys.exit(1)

  # ETX待ち
  while event.is_set() != True :
    etx = receiveMultiData(1)
    if etx.__len__() > 0 :
      data += etx
      if etx == "\x03" :
        break
    else :
      sys.exit(1)
  return data


# データフレーム受信
def rcv_data() :
  data = bytearray(0)
  while event.is_set() != True :
    len = receiveMultiData(1)
    if len.__len__() > 0 :
      i_len = ord(len)
      sum = i_len
      rcv_data = receiveMultiData(i_len)
      if rcv_data.__len__() > 0 :
        data = bytearray(i_len-1)
        for i in range(i_len-1) :
          data[i] = rcv_data[i]
          sum += data[i]
        sum += rcv_data[i_len-1] # bcc
        sum &= 255
        if sum == 255 :
          sendMultiData(ack)
          break
        else :
          print sum
          sendMultiData(nack)
      else :
        sys.exit(1)
    else :
      sys.exit(1)
  return data


# influxDBサーバへ送信
def influxUp(text) :
  f = open('power_tmp.txt', 'w')
  f.write(text)
  f.close()
  commands.getoutput("curl -i -XPOST '[influxDBサーバのURL]:8086/write?db=smartmeter' --data-binary @power_tmp.txt")


# データ・ダンプ
def dump() :
  d = datetime.datetime.today()
  f = open("kyocera_{0:04d}{1:02d}{2:02d}.txt".format(d.year,d.month,d.day), 'w')
  idx = 0
  count = total_data.__len__()
  count /=  32
  for i in range(count) :
    text = "{0:04X}: ".format(idx)
    for j in range(32) :
      text += "{0:02X}".format(total_data[idx])
      idx += 1
    text += "\n"
    f.write(text)

  count = total_data.__len__()
  count -= idx
  print count
  print "{0:04X}".format(idx)
  text = "{0:04X}: ".format(idx)
  for i in range(count) :
    print i
    text += "{0:02X}".format(total_data[idx])
    idx += 1
  text += "\n"
  f.write(text)
  f.close()


# ３０分データの抽出
def generate_txt(day) :
  day_pos = int("007C",16)
  hatuden_pos = int("00E5",16) # 発電
  baiden_pos = int("0E05",16) # 買電ー売電
  txt = ""
  date = "20{0:02X}/{1:02X}/{2:02X}".format(total_data[day * 3 + day_pos], total_data[day * 3 + day_pos+1], total_data[day * 3 + day_pos+2])

  # 発電
  pos = hatuden_pos
  for idx in range(48) :
    d1 = total_data[pos + day * 96 + idx * 2 + 0]
    d2 = total_data[pos + day * 96 + idx * 2 + 1]
    if d1 != 0x75 or d2 != 0x30 :
      upper = d1 * 256
      upper += d2
      if upper > 32767 :
        upper -= 65536
      upper /= 10.0
      dt = "{0} {1:02d}:{2:02d}:00".format(date,idx/2,idx%2*30)
      print("{0} -> {1}".format(dt,upper))
      d = time.strptime(dt, "%Y/%m/%d %H:%M:%S")
      timestamp = int(time.mktime(d)) * 1000000000
      txt = txt + "hatuden value={0} {1}\n".format(upper,timestamp)
  # 買電ー売電
  pos = baiden_pos
  for idx in range(48) :
    d1 = total_data[pos + day * 96 + idx * 2 + 0]
    d2 = total_data[pos + day * 96 + idx * 2 + 1]
    if d1 != 0x75 or d2 != 0x30 :
      upper = d1 * 256
      upper += d2
      if upper > 32767 :
        upper -= 65536
      upper /= 10.0
      dt = "{0} {1:02d}:{2:02d}:00".format(date,idx/2,idx%2*30)
      print("{0} -> {1}".format(dt,upper))
      d = time.strptime(dt, "%Y/%m/%d %H:%M:%S")
      timestamp = int(time.mktime(d)) * 1000000000
      txt = txt + "baiden_diff value={0} {1}\n".format(upper,timestamp)
  return txt


# 受信バッファ・クリア
for i in range(receiveCount()) :
  receiveData()

# 要求コマンド
time.sleep(1)
cmd = ""
cmd += "\x02CMD1000FFFF50\x03"
sendMultiData(cmd)

# ACK
data = rcv_cmd()

# 受信開始コマンド
data = rcv_cmd()
print data
# +0+1+2+3+4+5+6+7+8+9+A+B+C+D+E+F+0+1+2
# 02434D44303130303030303032313446434203
# st C M D 0 1 0 0 0 0 0 0 2 1 4 F C B etx
total = int(data[12:12+4].decode(),16)
total_data = bytearray(total)

# ACK
sendMultiData(ack)

# データ受信
count = 0
len = 0
while event.is_set() != True and len < total :
  data = rcv_data()
  total_data[len:len+data.__len__()] = data
  count = count + 1
  len += data.__len__()
  print("{0}, {1}, {2}".format(count, data.__len__(), len))

# 受信終了コマンド
data = rcv_cmd()

# ACK
sendMultiData(ack)

dump()

pow_txt = generate_txt(0)
pow_txt += generate_txt(1)
print pow_txt

influxUp(pow_txt)
```

---
# 情報保存用サーバと可視化サーバについて
- Google Cloud Platform上のCompute Engineで動作させています。
    - InfluxDBとGrafanaしかインストールしてません。
    - Googleからは、ひとつ上のマシンタイプに上げることを推奨されていますが、（一応使えているので）そのままにしています。
        - メモリ（0.6GB）がキツイようです。
- （現在のところ）無料で使える範囲内でやってます。
    - 課金されてないので、クレジット（アカウント登録するともらえる$300）は約33000円残っています。
    - 当初、ゾーンをasiaにして試したりしたので、150円ほどクレジットが減っています。
- 下記の設定で動かしています。
    - ゾーン
        - us-west1-a
    - マシンタイプ
        - f1-micro（vCPU x 1、メモリ 0.6 GB）
    - CPUプラットフォーム
        - Intel Broadwell
    - 追加ディスクなし（10GB）