Edited at

自宅状況の見える化

More than 1 year has passed since last update.


はじめに



最終形態


  • 最終的に、4種類のグラフをWeb経由で閲覧できるようになりました。


    1. 瞬時電力計測値

    2. 積算電力計測値

    3. エコキュート監視

    4. 京セラモニタ



  • 下のスクリーンショットは、2017/5/31時点での直近二日間の時系列グラフになります。

FireShot Capture 1 - Grafana - 自宅状況_ - http___katsumin.tk_3000_dashboard_db_zi-zhai-zhuang-kuang.png



システム説明


  • 装置構成


    1. 情報収集用ラズパイ


      • スマートメータからの情報収集

      • エコキュートからの情報収集

      • 京セラモニタからの情報収集

      • 情報保存用サーバへの送信



    2. 情報保存用サーバ(データベース)


      • influxDB」を使っており、情報収集用ラズパイから送られてくるデータ群をデータベースに保存します。



    3. 可視化サーバ(Webサーバ)


      • Grafana」を使っており、情報保存用サーバのデータを参照して、時系列グラフを生成し、Webブラウザに表示します。







スマートメータからの情報収集


config.ini

[smartmeter]

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

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



  • スマートメータ接続プログラム


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




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サーバにデータを送る。




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分ごとに起動。




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)




エコキュートからの情報収集


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分ごとに起動。




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))




京セラモニタからの情報収集


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()

# 30分データの抽出
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)