14
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

自宅状況の見える化

Last updated at Posted at 2017-06-20
1 / 8

はじめに


最終形態

  • 最終的に、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)
14
24
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?