コモノポリタン

コモノ、デジモノが好きなKomonopolitan住民 (はてなダイアリーからの引っ越しです)

【Home AssistantでDIY Smart Home】電力見える化!HEMS?Part 2

【Home Assistant(Hass.io)】
スマートメーターから電力情報を取得する!

「Home Assistant(Hass.io)でホームオートメーション 再起動!」シリーズです。

 ホームエネルギー管理の全体像は前回まとめたので、今回はその中身第1弾として「スマートメーターから情報を得る!」です。まだまだ改良の余地は沢山ありそうですが、とりあえずパネル表示まで出来ました。

 既に触れましたが、我が家がスマートメーター化したのは5年ほど前。2024年以降にはスマートメータはWifiで、なんて政府が言い出したりしているので、本末転倒風味ではありますが今の仕組みのうちに電力消費量の見える化が出来るように頑張りますか!

おうちでエネルギー管理については、こんな感じで進める予定です。

  1. 電力把握の全体像をまとめる(前回
  2. スマートメーターから情報を得る(今回)
  3. 分電盤から情報を得る

[2022/07/12追記] 本記事の修正があります。下記、補足記事を参照ください。Pythonスクリプトの修正とか…。実力不足…お恥ずかしい。
maky-ba.hatenablog.com

この記事の前提条件
Home Assistant 2022.5.1
HassOS 7.6
Server Raspberry Pi 4(2GB) /32bit

 上記バージョンを前提とした手順です。 (最新版では動かないこともあるかもしれませんが、私が使っている限り、備忘録を兼ねて最新化してゆきたいとは思っています)

1. 準備

Wi-SUN対応無線モジュール(BP35A1)
スマートメーターにWi-LAN接続できるROHMの無線モジュールです。
価格:以前はこれとアダプタとアクセサリの3点で1万円ちょっとで一番安かったのですが…。

Wi-SUN対応無線モジュールアダプタボード(BP35A7A)
BP35A1のコネクタはとても細かいので、2.54 mmピッチ間隔への変換ボードは必須。
価格:RSコンポーネンツで以前は千円ぐらいだったのですが、今は2倍以上に。

アダプタボード(BP35A7A)用アクセサリ
ピンははんだ付けしてしまいましたが、ピンとネジ・ネットとスペーサーのセットです。今はどこのサイトでも手に入らないのが残念です。汎用品なので他で手に入りはしますが。
価格:以前は300円弱でした。

Raspberry Pi Zero WH / Adafruit Raspberry Pi Zero用ケース、SDカード、ピンソケット
Raspberry Pi Zero Wのピンヘッダー付き版。40ピンソケットも忘れずに(秋月なら[C-00085]かな)。
価格:Pi Zero WHとケースのセットは3,000円ちょっとでした。

Raspberry Pi用基板
Raspberry Pi B用基板[秋月P-11073]ですので、少し大きめなのでカットしてつかいます。どうせアダプタの全ピンは必要ないのでRaspberry Pi Zero用基板[秋月P-14031]の方がよいかもしれませんね。
価格:150円

 2017年12月も残りわずかという頃に発注して、ちょっとだけ動作確認をしてみて今日に至る、です。当時はRSコンポーネンツ(メインは会社利用(BtoB)だが個人の登録・利用も可)で上記3点そろって購入できたのですが、今は本体(BP35A1)と変換アダプターボード(BP35A7A)としか手に入りません。しかもアダプタボードは値上がり…。chip1stopの変換アダプタは安いままなのですが、chip1stopは会社利用のみですので残念。まあ、今はもっと安い下記のようなUSBドングルがあるので、そっちを使うのが良いかと。

www.tessera.co.jp

また一つSmart Homeの要素がはんだ付け不要に…(ま、いいことなんですけどね)。

2. スマートメーター関連の申請

2.1. スマートメーター設置申込

 例えば、東電では2014年から開始した電力メーターのスマートメーター化ですが、2021年3月時点で可能な範囲はすべて完了とのこと。以前は「スマートメーターにして、お願い」などど電力会社に言う必要がある家もあったかと聞いておりますが、いまや、スマメがデフォルトな時代…。

2.2. Bルートサービス申込

 設置してあるスマートメーターから情報を取得するには、「Bルート」なるものを利用可能にする必要あります。Aルートがスマートメーターと電力会社との間の情報のルートで、Bの方がスマートメーターと家の間の情報ルートを表します。
 東京電力では下記のページからオンラインで申し込めます。
www.tepco.co.jp  ただしその後は、上記ページにあるとおり、本人確認書類を送ったり、IDが紙で送られてきたりと、総デジタル化にはちょっとまだ距離感あります。日本のDXの夜明けはいつ?

3. Wi-SUN対応無線モジュールの組み立て

 スマートメーターとの無線のやりとりはWi-SUN規格に基づいて行われます。このWi-SUNに対応無線モジュール(BP35A1)とRaspberry Pi Zero WHとはシリアル通信でつなぎます。下記のピンを接続する必要があります。

Raspberry Pi
Zero WH
ピン ピン アダプタボード
(BP35A7A)
GPIO14 - TxD #8 CN2 #4 RXD
GPIO15 - RxD #10 CN2 #5 TXD
3.3V #1, #17
どれか
CN1 #4, #5
どれか
VCC
GND #6, #9, #14, #20, #25,
#30, #34,#39
どれか
CN1 #1, #9
CN2 #1, #9
どれか
GND

 Raspberry Pi用の基板を7穴程短く詰めて、BP35A1アダプタボード用のピンソケット(表側)とRaspberry Pi用のピンソケット(裏側)をはんだ付けします。表はこんな感じです。

 まずWi-SUN対応無線モジュール(BP35A1)をアダプタボード(BP35A7A)のコネクタにはめ込み、うまい具合にスペーサーを差し入れてねじ止めします。アダプターボードを先ほど作った基板に嵌めて、それをRaspberry Pi Zero WHに差し込みます。完成!

 完成…ですが、さすがに剥き身はアレかな。3Dプリントでガワを作ろうかな。

4. Raspberry Pi Zero WHの設定

4.1. Raspberry Pi Zero WHのOSセットアップ

 もはや定型のセットアップですが、メモとして記載します。Raspberry Piのセットアップ中はWi-SUN対応無線モジュールは接続しません。

  1. Raspberry Pi Imager」を起動して「Choose OS」では、[Raspberry Pi OS (other)]-->[Raspberry Pi OS Lite (32bir)]を選択。これがBullseyeのGUI無し版です。
  2. そのmicroSDをもう一度PCに刺し直して、そのルートディレクトリ(ドライブ名:boot)にsshという空ファイルを作る。これで再起動時にsshで接続できるようになります
    Windowsならエクスプローラで右ボタンでtextファイルを作り、拡張子txtを削除。Linuxならtouchコマンドかな)
  3. 無線LANを使えるように、microSDのルートディレクトリ(ドライブ名:boot)にwpa_supplicant.confを作成する
    Windowsならエクスプローラで右ボタンでtextファイルをwpa_supplicant.conf.txtという名前で作成しメモ帳で下記を記入し保存。ファイル名から拡張子.txtを削除、かな)
country=JP
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
network={
  ssid="<接続する無線LANのSSID>"
  psk="<暗号化キー>"
}

 MicroSDRaspberry Pi Zero Wに挿して起動しておいて、どこからか(Home AssistantのTerminalからでも)sshでこのRaspberry Pi Zero Wに接続。その後、$ sudo raspi-configで設定モードに入り、piユーザのパスワード変更やLocale, Timezone, SSH onなどの設定をしておきましょう。
 それから諸々の最新化も。

$ sudo su
# apt-get update -y
# apt-get upgrade -y
# apt-get autoremove
# apt-get clean

4.2. Python3モジュールのインストール

 今回必要なモジュールは、pyserialinfluxdb(のクライアントの方)とmqttぐらいですが、pyserialはデフォルトで入っているようなので、influxdbとmqttをインストールしておきます。

$ sudo pip install influxdb
$ sudo pip install paho-mqtt

5. スマートメーターからの情報取得

[2022/07/12追記] 本章のPythonスクリプトおよびサービス定義ファイルには誤りがあります。下記記事を参照ください。
maky-ba.hatenablog.com

5.1. スマートメーター情報取得スクリプト

 この辺りを参考にしながら、試行錯誤でPythonスクリプトを作成しました。
qiita.com www.ishikawa-lab.com

 はまったのがPython3の文字列のデータ型かな。参考にしたスクリプトはpython2が多く、最近ではRaspberry PiPythonが3系列になっておりいろいろ変化あり、特に文字列strの仕様変更にはハマりました。
 それとタイムアウト処理かな。こっちはBP35A1の仕様の理解不足か、時々返答が無くなったり、UDP送信失敗したり。エラーハンドリングを使う正規のタイムアウト処理も考えましたが、とりあえずシリアル通信のタイムアウト時間を設定すれば応答の有無にかかわらずシリアル通信が改行されるので、それで何とかすることに。(瞬間電力問い合わせへの返答が特定回数で得られなかったら、それはつまり「応答なし状況」と判断して再度問い合わせ(SKSENDTO)からやり直す事でなんとか回避)

 以下がスマートメーターから瞬時電力計測値を取得するPythonスクリプトです。
 けっこうやっつけで作ったので汚いかも…。

#! /usr/bin/env python3
#
# Getting Data from Smart Meter
#  by makyBa, 2022/05/05
#
import serial
import sys
import datetime
import time
from influxdb import InfluxDBClient
import paho.mqtt.client as mqtt

# MQTTの設定  #★1:Home Assistantとの通信はMQTTを使用
broker = '192.168.XX.XX'
port = 1883
topic = "home/power/insta-power"
username = 'mqtt'
password = 'xxxxxxxx'

def  connect_mqtt(): #★2
    client = mqtt.Client()
    client.username_pw_set(username, password)
    client.connect(broker, port, 60)
    return(client)

# InfluxDBの設定 #★3:データ格納用にInfluxDBサーバへも書き込み
client = InfluxDBClient(host='192.168.XX.XX',
                        port=8086,
                        username='root',
                        password='xxxxxxxxx',
                        database='power')
measurement = 'power'
tags = {'place': 'my_home', 'host': 'BP35A1'}

def influx_json(fields, nowtime): #★4
    json_body = [
        {
            'measurement': measurement,
            'tags': tags,
            'fields': fields,
            'time': nowtime
        }
    ]
    client.write_points(json_body)

# スマートメーターの設定 #★5:スマートメーターへはBP35A1へのシリアル通信経由
# BルートのID
routeBId = "000000XXXXXXXXXXXXXXXXXXXXXXXX"
# Bルートのパスワード
routeBPwd ="XXXXXXXXXXXX"
# シリアルデバイス
serialPortDev = '/dev/ttyAMA0'

def waitOK(serial_port): #★6
    while True:
        line = serial_port.readline()
        if line.startswith(b"OK"):
            break

def initialize(): #★7
    # シリアルの初期化
    serial_port = serial.Serial(port=serialPortDev, baudrate=115200)

    # Bルートのパスワードを設定
    serial_port.write(("SKSETPWD C " + routeBPwd + "\r\n").encode())
    waitOK(serial_port)

    # BルートのIDを設定
    serial_port.write(("SKSETRBID " + routeBId + "\r\n").encode())
    waitOK(serial_port)

    # ネットワークのスキャン
    scanDuration = 5
    scanRes = {}
    while 'Channel' not in scanRes.keys():
        serial_port.write(("SKSCAN 2 FFFFFFFF " + str(scanDuration) + "\r\n").encode())
        scanEnd = False
        while not scanEnd:
            line = serial_port.readline()
            if line.startswith(b"EVENT 22"):
                scanEnd = True
            elif line.startswith(b"  "):
                cols_str = line.decode()
                cols = cols_str.strip().split(':')
                scanRes[cols[0]] = cols[1]
        scanDuration+=1
        if 14 < scanDuration and 'Channel' not in scanRes.keys(): #★8
            sys.exit()

    # チャネルを設定
    serial_port.write(("SKSREG S2 " + scanRes["Channel"] + "\r\n").encode())
    waitOK(serial_port)

    # PAN IDを設定
    serial_port.write(("SKSREG S3 " + scanRes["Pan ID"] + "\r\n").encode())
    waitOK(serial_port)

    # IPv6アドレスを取得
    serial_port.write(("SKLL64 " + scanRes["Addr"] + "\r\n").encode())
    serial_port.readline()
    ipv6Addr = serial_port.readline().decode().strip()

    # PANA認証
    serial_port.write(("SKJOIN " + ipv6Addr + "\r\n").encode())
    waitOK(serial_port)

    bConnected = False
    while not bConnected:
        line = serial_port.readline()
        if line.startswith(b"EVENT 24"):
            sys.exit(1)
        elif line.startswith(b"EVENT 25"):
            bConnected = True

    return (serial_port, ipv6Addr)

# Main 
if __name__ == '__main__':

    mqtt_client = connect_mqtt()
    mqtt_client.loop_start()
    (serial_port, ipv6Addr) = initialize()
    serial_port.timeout = 10 #★9
    echonetLiteFrame = b'\x10\x81\x00\x01\x05\xFF\x01\x02\x88\x01\x62\x01\xE7\x00'
    command = ("SKSENDTO 1 {0} 0E1A 1 {1:04X} ".format(ipv6Addr, len(echonetLiteFrame))).encode()
    command += echonetLiteFrame
    command += ("\r\n").encode()

    sleep_sec = 9
    while True:
        res_count = 0
        serial_port.write(command)
        while True:
            line = serial_port.readline().decode()
            res_count += 1
            if res_count > 5: #★10
                break;
            if line.startswith("ERXUDP"):
                cols = line.split(' ')
                res = cols[8]
                seoj = res[8:8+6]
                ESV = res[20:20+2]
                if seoj == "028801" and ESV == "72": # 028801:Smart Meter
                    EPC = res[24:24+2]
                    if EPC == "E7": # E7:瞬時電力計測値
                        signedhexPower = line.rstrip()[-8:]
                        intPower = int(signedhexPower, 16)
                        if (intPower >> 31) == 1:  #★11
                            intPower = (intPower ^ 0xFFFFFFFF) * (-1) -1
                        u = datetime.datetime.utcnow().isoformat("T") + "Z"
                        fields = {'power': intPower}
                        influx_json(fields, u)
                        mqtt_client.publish(topic, str(intPower))
                break;
        time.sleep(sleep_sec)

    serial_port.close()

★1:MQTTとの接続準備。最初InfluxDB経由でHome Assistantに情報を送ろうと思ったのですが、タイミングがずれるのでMQTTを使用。Raspberry Pi Zero WH with BP35A1 ---(Publish)---> mosquitto message broker ---(Subscribe)---> Home Assistant
★2:MQTTブローカーへの接続関数。通常はon_connectとかの関数も登録するのですが、今回は無視して最小限に。
★3:InfluxDBとの接続準備。データ格納用にInfluxDBサーバにデータを投げ込む。こうしておけばInfluxDB+Grafanaでグラフ化できたりしますね。
★4:InfluxDBに書き込む関数。
★5:シリアル通信の準備。スマートメーターからの情報はBP35A1へシリアル通信することで取得します。
★6:BP35A1からの返答はすぐにはこないので、OKをじっくり待つ。 ★7:BP35A1との接続初期化の手続き。
★8:Scan Durationは、BP35A1のネットワークスキャンの待ち時間(指数関数)を指定します。どんどん長くなるので、14まで設定可能とは言いながらも実用的ではない様子。だいたいScanDuration=8ぐらいまでにはスキャン終わります。
★9:シリアル通信の返答待ち時間。10秒経つと空欄の返答が返ります。
★10:SKSENDTOコマンドの返答は通常なら5行(1行目:エコーバック、2行目:EVENT 21、3行:OK、4行目:空白、5行目:ERXUDP(=返答))で大丈夫。それで返ってこなかったら、再度コマンド送信へ。
★11:買電・売電の両方をしているので返答がマイナスになることもあるので、32bitの符号付2進数として処理。

5.2. スマートメーターからの情報取得をサービス化する

 OMRONの環境センサーから情報取得でやったのと同じように、この情報取得スクリプト自動起動できるようにします。まずは先ほどのPythonスクリプトchmodして実行可能にしつつ、次にサービス定義ファイルを編集します。

$ chmod 775 smart_meter.py
$ sudo nano /etc/systemd/system/smart_meter.service
[Unit]
Desctiption = Smart Meter

[Service]
ExecStart = /home/pi/smart_meter.py
Type = simple

[Install]
WantedBy = muti-user.target

 編集し終えたら、サービス定義を再読込(daemon-reload)して、サービスを立ち上げる(start)。OS起動時の自動起動リストにも追加しておくこと(enable)。

$ sudo systemctl daemon-reload
$ sudo systemctl start smart_meter.service
$ sudo systemctl enable smart_meter.service

 InfluxDBサーバに接続すれば、刻々と記録される瞬時電力計測値を読み取ることができます。

6. Home Assistant側の設定

 下記、Home AssistantのドキュメントのMy ENERGY CONFIGURATIONボタンから設定画面に飛べます(「設定」→「ダッシュボード」の「エネルギー(ENERGY)」からでも行けます)。
www.home-assistant.io

 グリッド(系統電力網)関連のエネルギー管理について説明は下記ですが、あまり大したことは書かれていません。時間帯で変わる電力利用料(Tariffs)についても対応してますよー、とかありますが、残念ながら私の利用料は時間固定で、電力量に応じた従量課金…。それから、電気メーターを数値を読み取るハードウェアの紹介もありますが、それらは欧米のモノ…。
www.home-assistant.io

 実は一番役に立ったのがFAQ!
 「Energy vs Power(電力量 vs 電力)」おお?!
 エネルギー管理パネル(ENERGY PANEL)に表示されるのは「電力量(ENERGY)」であって、「電力(POWER)」ではない、と言っています。まあそうかも。使うのは電力(単位:W)ですが、課金されるのは電力量(単位:kWh)ですし。
www.home-assistant.io

 むむ?ということは頑張って書いた瞬時電力計測値(=電力[W])の取得スクリプトでは、エネルギー管理パネルのお役に立てない?スクリプトの書き直し?(ちょっと疲れちゃったので、それはのちほどにしたい…)

 おっ、助かった、同じくFAQ内の「Creating an Energy Sensor out of a Power Sensor(電力センサーから電力量センサーを作る)」が、その悩みを解決してくれました。作れるんですね電力「量」センサー。

6.1. MQTTを受け止める電力センサーを作成

 まずは普通に送られてきたMQTTメッセージをもとに「瞬時電力計測値」を受け取るセンサーを作ります。電力(Power)は常に、その瞬間瞬間の値なので「瞬時」だの「Instantaneous」だのは変かなとも思いますが、気にしないでいきましょう。

(前略)
sensor:
  # Smart Meter - Grid Power(W)
  - platform: mqtt
    name: "Instantaneous Power"
    state_topic: "home/power/insta-power"
    unit_of_measurement: "W"
(後略)

 これでHome Assistantがhome/power/insta-powerトピックのメッセージを受信(Subscribe)することが出来るようになります。

6.2. 売電(電力)センサーと買電(電力)センサーに分離する

 エネルギー管理パネルの設定画面を見てもらえばわかるのですが、グリッド(Grid、系統電力網)の所には「グリッド消費(Grid Consumption)」と「グリッド戻り(Grid Return)」の2つのセンサーを登録できるようになっています。
 まあ見てお分かりの通り、系統電力網からの買電が「グリッド消費」で、太陽光などの売電が「グリッド戻り」です。先ほど作った電力センサーは、+の値が買電で、-の値が売電となっています。まずはこれを2つに分離しましょう。
 Template Sensorを使って、正負をif文で判別して「瞬時電力(消費)」(instantaneous power consumed)と「瞬時電力(戻り)」(instantaneous power returned)を作ります。

(前略)
template:
  - sensor:
    # Smart Meter
    #
      # Intantaneous Power From Grid to Home (Consumption) 
      - name: instantaneous power consumed 
        unit_of_measurement: W
        device_class: power
        state: >-
          {% set current = states('sensor.instantaneous_power') | int(0) %}
          {% if current >= 0 %}
            {% set result = current %}
          {% else %}
            {% set result = 0.0 %}
          {% endif %}
          {{ result }}
          
      # Instantaneous Power From Home to Grid (Returns) 
      - name: instantaneous power returned
        unit_of_measurement: W
        device_class: power
        state: >-
          {% set current = states('sensor.instantaneous_power') | int(0) %}
          {% if current < 0 %}
            {% set result = current * -1 %}
          {% else %}
            {% set result = 0.0 %}
          {% endif %}
          {{ result }}
(後略)

 ここで肝は「unit_of_measurement: W」と「device_class: power」らしいです。これを設定していると次のIntegration後に値がclass: energyになっていい感じになる、らしいですよ。

 天気が良いので売電しまくっていて、ちょっと分かりにくいですが、下記の様に元々の電力センサーの正の部分と負の部分が、それぞれ別のセンサーに分離できています。どちらも値は正なので、売電の方は上下反転して見えますね。

6.3. 電力センサーから電力量センサーをつくる

 次に「Creating an Energy Sensor out of a Power Sensor(電力センサーから電力量センサーを作る)」にあるように「瞬時」であるところの電力値から、「量」であるところの電力量値に変換するセンサーを作ります。要は積分ですね。ということで「積分操作」を表す「Integration」プラットフォームのセンサーを使います。*1

(前略)
sensor:
  # Smart Meter - Grid Energy(kWh)
  - platform: integration
    source: sensor.instantaneous_power_consumed
    name: grid_import
    unit_prefix: k
    round: 2
  - platform: integration
    source: sensor.instantaneous_power_returned
    name: grid_export
    unit_prefix: k
    round: 2
(後略)

 ちなみに「Integration - Riemann sum integral」(積分 - リーマン和積分)のドキュメントページからはMy ADD INTEGRATIONで設定画面を呼べと誘導されますが、私はそれではうまくいきませんでした。地道にconfiguration.yamlファイルを編集したらうまくいきましたよ。
 しかし、まあ、リーマン和(Riemann sum)による区分積分かぁ。高校の勉強が役に立つ(?)日が来るのですね。

6.4. エネルギー設定

 さて、やっと準備が整いました。これでエネルギー設定画面の電力網のパネルの「消費を追加」や「戻りを追加」を選んだ時に下記のような候補が表示されるようになるはずです。

 こんな風(下記)に設定します。他にもソーラーパネル、蓄電システム、ガス消費量、個々のデバイスなどをエネルギー管理パネルで設定することが出来ますね。

 しばらく待つと(2時間ぐらい待て、と言われます)下記の様なグラフが表示され始めます。

 ちょっと残念なのが「エネルギー管理パネル」のタイトルです。なぜ縦書きなのか?はみ出てるし。

7. おわりに

 とりあえずスマートメーター周りの取っ掛かりはできました。ただ今回はかなり無理矢理エネルギー管理パネルを使いましたが、スマートメーターからはもっと沢山の情報を得ることができます。そもそもダイレクトに「電力量」情報だって、30分単位で取得することが出来ます。
 そっちの方がより正確な情報*2になるでしょう。つぎはそのあたりか?
 それとも、太陽光とか燃料電池*3の情報を取得するために予定通りCTクランプに行くか?

maky-ba.hatenablog.com

*1:「インテグレーション」(システムの「統合」)もIntegrationsなので、紛らわしいこと…。

*2:電力会社はこの情報を基に電気代を請求しているので

*3:実はHome Assistantのエネルギー管理パネルに燃料電池の項目はない!さてどうする?!