Pythonでスマートメーターの情報を引っこ抜く

#はじめに
Qiita漁っていたらこんな記事が
[スマートメーターの情報を最安ハードウェアで引っこ抜く](https://qiita.com/rukihena/items/82266ed3a43e4b652adb)

なるほど、Wi-SUN通信デバイスとやらがあると瞬時電力値を取得できるのか！
面白そうなのでやってみた


#Wi-SUN通信デバイスを買う
2019年3月時点で一番安いデバイスは、テセラ・テクノロジーさんの[RL7023 Stick-D/IPS](https://www.tessera.co.jp/rl7023stick-d_ips.html)というものらしい、それを買いました。
[chip 1 stop](https://www.chip1stop.com/view/dispDetail/DispDetail?partId=TSRA-0000042)さんでも売っていましたが、テセラさんから直で買いました。(特に理由はない)


#Bルート利用の申し込み
私の生活圏は中部電力の管轄になるので、[電力メーター情報発信サービス接続利用申込書 兼 委任状](https://www.chuden.co.jp/resource/home/smuse_02_3.pdf)を書いて[営業所・サービスステーション](https://www.chuden.co.jp/corporate/company/officelist/eigyosho/index.html)から最寄りの営業所を探して郵送しましょう。
~~このご時世に手書きで、しかも郵送とか・・・~~

1週間から10日ぐらいでIDとPWが郵送されてきます。
時間がかかるので、Wi-SUNデバイスより先に申し込んだ方が良いです。
<img src="https://qiita-image-store.s3.amazonaws.com/0/133206/6e055bb0-2fe7-3ca9-7c76-a7ec90874584.jpeg" width=50%>

#環境
windows10 + Python 3.6.5

参考にした@rukihenaさんの記事では2系のPythonを使われていましたが、せっかくなので3系で動かすことにしました。

#設定データの取得
スマートメータとの通信するため用のiniファイルを作ります。

- broute_id：電力会社から送られてきたBルートの【認証ID】を書く
- broute_pw：電力会社から送られてきたBルートの【パスワード】を書く

```ini:SmartMeter.ini
[settings]
broute_id = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
broute_pw = XXXXXXXXXXXX
```
#Channel,Pan ID,Addressの取得
スマートメータとやり取りするための各種データを取得します。
取得後はSmartMeter.iniに設定値を保存します。

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

import sys
import serial
import configparser

# シリアルポートデバイス名
serialPortDev = 'COM5'  # Windows の場合
#serialPortDev = '/dev/ttyUSB0'  # Linuxの場合

# 設定情報読み出し
inifile       = configparser.ConfigParser()
inifile.read('./SmartMeter.ini', 'utf-8')
Broute_id     = inifile.get('settings', 'Broute_id')
Broute_pw     = inifile.get('settings', 'Broute_pw')

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

# Bルート認証パスワード設定
print('Bルートパスワード設定')
ser.write(str.encode("SKSETPWD C " + Broute_pw + "\r\n"))
ser.readline()
print(ser.readline().decode(encoding='utf-8'), end="")  # 成功ならOKを返す

# Bルート認証ID設定
print('Bルート認証ID設定')
ser.write(str.encode("SKSETRBID " + Broute_id + "\r\n"))
ser.readline()
print(ser.readline().decode(encoding='utf-8'), end="")  # 成功ならOKを返す

scanDuration = 6 # スキャン時間
scanRes = {} # スキャン結果の入れ物

# アクティブスキャン
while ('Channel' not in scanRes):
    ser.write(str.encode("SKSCAN 2 FFFFFFFF " + str(scanDuration) + "\r\n"))
    scanEnd = False
    while not scanEnd :
        line = ser.readline().decode(encoding='utf-8')
        if line.startswith("EVENT 22") :
            # スキャン終了
            scanEnd = True
        elif line.startswith("  ") :
            cols = line.strip().split(':')
            scanRes[cols[0]] = cols[1]
    scanDuration+=1

    if 7 < scanDuration and ('Channel' not in scanRes):
        print("スキャンリトライオーバー")
        sys.exit()

# Channel設定
print("Channel:" + scanRes["Channel"])
inifile.set('settings', 'Channel', scanRes["Channel"])

# Pan ID設定
print("PanID:" + scanRes["Pan ID"])
inifile.set('settings', 'PanId', scanRes["Pan ID"])

# MACアドレスをIPV6リンクローカルアドレスに変換
ser.write(str.encode("SKLL64 " + scanRes["Addr"] + "\r\n"))
ser.readline().decode(encoding='utf-8')
ipv6Addr = ser.readline().decode(encoding='utf-8').strip()
print("Address:" + ipv6Addr)
inifile.set('settings', 'Address', ipv6Addr)

with open('./SmartMeter.ini', 'w') as configfile:
    inifile.write(configfile)
```


#瞬時消費電力の取得
そんなこんなで瞬時電力を取得するコードは以下。
参考記事ではChannelとかの情報の取得もメインの処理の中でしていましたが、スキャン時間が長いので各種データは取得済みでiniファイル書かれているものとして処理を作りました。

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

from time import sleep

import sys
import serial
import configparser

# シリアルポートデバイス名
serialPortDev = 'COM5'  # Windows の場合
#serialPortDev = '/dev/ttyUSB0'  # Linuxの場合

# 瞬時電力計測値取得コマンドフレーム
echonetLiteFrame = b'\x10\x81\x00\x01\x05\xFF\x01\x02\x88\x01\x62\x01\xE7\x00'

# 設定情報読み出し
inifile       = configparser.ConfigParser()
inifile.read('./SmartMeter.ini', 'utf-8')
Broute_id     = inifile.get('settings', 'broute_id')
Broute_pw     = inifile.get('settings', 'broute_pw')
Channel       = inifile.get('settings', 'channel')
PanId         = inifile.get('settings', 'panid')
Address       = inifile.get('settings', 'address')

# シリアルポート初期化
ser = serial.Serial(serialPortDev, 115200) # シリアルポートオープン
ser.timeout = 2 # シリアル通信のタイムアウトを設定

# Bルート認証パスワード設定
print('Bルートパスワード設定')
ser.write(str.encode("SKSETPWD C " + Broute_pw + "\r\n"))
ser.readline() # エコーバック
print(ser.readline().decode(encoding='utf-8'), end="")  # 成功ならOKを返す

# Bルート認証ID設定
print('Bルート認証ID設定')
ser.write(str.encode("SKSETRBID " + Broute_id + "\r\n"))
ser.readline() # エコーバック
print(ser.readline().decode(encoding='utf-8'), end="") # 成功ならOKを返す

# Channel設定
print('Channel設定')
ser.write(str.encode("SKSREG S2 " + Channel + "\r\n"))
ser.readline()  # エコーバック
print(ser.readline().decode(encoding='utf-8'), end="")  # 成功ならOKを返す

# PanID設定
print('PanID設定')
ser.write(str.encode("SKSREG S3 " + PanId + "\r\n"))
ser.readline() # エコーバック
print(ser.readline().decode(encoding='utf-8'), end="") # 成功ならOKを返す

# PANA 接続シーケンス
print('PANA接続シーケンス')
ser.write(str.encode("SKJOIN " + Address + "\r\n"))
ser.readline()  # エコーバック
print(ser.readline().decode(encoding='utf-8'), end="") # 成功ならOKを返す

# PANA 接続完了待ち
bConnected = False
while not bConnected :
    line = ser.readline().decode(encoding='utf-8')
    if line.startswith("EVENT 24") :
        print("PANA 接続失敗")
        sys.exit() #接続失敗した時は終了
    elif line.startswith("EVENT 25") :
        print('PANA 接続成功')
        bConnected = True

ser.readline() #インスタンスリストダミーリード

while True:
    # コマンド送信
    command = "SKSENDTO 1 {0} 0E1A 1 {1:04X} ".format(Address, len(echonetLiteFrame))
    ser.write(str.encode(command) + echonetLiteFrame)

    #コマンド受信
    ser.readline() # エコーバック
    ser.readline() # EVENT 21
    ser.readline() # 成功ならOKを返す

    # 返信データ取得
    Data = ser.readline().decode(encoding='utf-8')

    # データチェック
    if Data.startswith("ERXUDP"):
        cols = Data.strip().split(' ')
        res = cols[8]   # UDP受信データ部分
        seoj = res[8:8+6]
        ESV = res[20:20+2]
        # スマートメーター(028801)から来た応答(72)なら
        if seoj == "028801" and ESV == "72" :
            EPC = res[24:24 + 2]
            # 瞬時電力計測値(E7)なら
            if EPC == "E7" :
                hexPower = Data[-8:] # 最後の4バイトが瞬時電力計測値
                intPower = int(hexPower, 16)
                print(u"瞬時電力計測値:{0}[W]".format(intPower))

    # 1分毎に取得する
    sleep(60)

# ガード処理
ser.close()

```

#実行結果
ということでこんな感じに瞬時電力の取得ができるようになりました！
次はデータベースに登録して、グラフィカルに表示できるようにしたいな
![image.png](https://qiita-image-store.s3.amazonaws.com/0/133206/e3c220d8-2cdc-fe45-48b0-0f565fbfe84e.png)

# おまけ
## 悩んだこと1
参考記事のコードをそのまま動かそうとするとシリアル通信でコマンドを送る時にエラーが出るんですよね。
詳しいことは [Python2とPython3のstr型/unicode型/bytes型/encode/decodeの違い解説](https://qiita.com/miyagaw61/items/0e8cc40d95bf6236729e) を見ていだければわかりますが、python2ではエンコード不明のバイナリっぽいデータだった文字列がpython3ではunicode型の文字列になったそうです。

つまり、送信するコードは.encodeでbytes型に変換する必要があります。

```python
str.encode("SKVER\r\n")
````

##悩んだこと2
スマートメータに投げるフレームデータはバイナリデータで送信するのですが、
参考記事のように定義すると、str型になってコマンドを認識してくれない。

悩んだこと1と同じで、python3ではunicode形式になってしまうので、バイナリデータはb"\x00"の形で記載しないとダメです。

```python
echonetLiteFrame  = b"\x10\x81"
echonetLiteFrame += b"\x00\x01"
```