As I Please

MTのいんすとーるの練習と、その他びぼうろく・・・

タグ「python3」が付けられているもの


switchbot 温湿度計2つ目 交換品

がamazonから送られてきたのでさっそくつ買ってみた。冷蔵庫の中に新しいものと古いものをほぼ同じ場所に置いて計測。(cactiから influxdb + grafana に移行中)

古いswitchbot温湿度計
switchbot_old_b.png

新しいswitchbot温湿度計
switchbot_new_b.png


データは5分に1回取得するように設定。一目瞭然で、新しい方はほぼデータを拾えているのに、古い方は1時間1回程度。
製品の性能にばらつきありすぎのような気もするけど、中華製ならこんなもんですか?
とりあえずこの新しいものを冷蔵庫に。
また、gatt でエラーが出る件についても、新しい方では解消しなかった。
charastaristicsは同じようだが。。。
ただ、公式githubには、同じことを言っている人がいる。2022/02/10 の投稿のようなのでやはり最近の話っぽい。公式のレスポンスは無いが、そろそろ何か書き込みあるかも。

sshへの不正アクセスが多いので、denyhosts をいまさらながらに導入

しつこいのは手動で /etc/hosts.allow をメンテしていたり、/etc/ssh/sshd_config で MaxAuthTries 2 とかですぐにたたき落とすようにしていたけど、やはり自動化しておいたほうが無難かと。

http://tokcs.com/2012/12/freebsd-denyhosts%E3%82%92%E5%B0%8E%E5%85%A5/を見つけてdenyhostsが簡単そうなので、インストールしてみる。
portsから入れたが、python3 が入っていればすんなり入る。
/sbin/iptables を起動しようとしたりするけどとりあえず放っておく。
早速起動したら、/var/log/auth.log を見て、3つのIPアドレスを denyした。
台湾、フランス、ロシア(シベリア)
今後はこの手は必須で入れておく必要ありかなぁ。。。時にアドレスリストのメンテが必要かもしれない(膨大に増えていく?)けど。


webコンテンツの localキャッシュ時の改行コード問題

jsonの内容を定期的にチェックするのに、一度取得してきた textデータをローカルに保存して、毎回比較(本当は headや not modifiedを利用すべきだろうけど、perlのlibwww,LWP::Simple の mirrorのようなものが見当たらなかった)することに。
単純に、requests.get, write したファイル内容と、次回の取得したデータを比較しても毎回違う結果になって、なぜ?と思ったら、元のテキストファイルが DOS形式(LF+CR)で、writeしたときに LFに置き換えてるということだった。
そのため、毎回取得して比較しても同じということに。
ということで回避策は、open()のときに newline='' のオプションをつけて改行を変更しないということで。
https://www.curict.com/item/1b/1b608b2.html
これでまた3時間。。。

imapsync の再インストール (Big Sur)他

Big Sur がそろそろ落ち着いているかと思ってアップデートしたら、imapsyncが、Segmentation faultで動かなくなってしまった。
/usr/local/bin/imapsync (imapsync を起動する bash script wrapper へのシンボリックリンク)がおかしくて、単体の /usr/local/Cellar/imapsync/1.977/libexec/bin/imapsyncはちゃんと動く。。。
bashがおかしくなったのかとも思ったが、https://brewinstall.org/install-imapsync-on-mac-with-brew/ を見て、単に "brew reinstall imapsync" で動き始めた。ここらへんの依存関係、わかりにくい。

ビエラ panasonic のIPコントロール(DT5)とめざましじゃんけん

古いパナソニックビエラ(うちのはDT5)でリモコン操作(赤外線)を、IP経由で行う。 nature remo で赤外線飛ばすのは時に安定しないしプログラマブルにはほど遠い感じがするので、 IPでやれないかと。 手法は、
  1. upnp でIPアドレス、送信先を取得
  2. soapで投げつける。
という方向で。 参照、利用したのは、pyvieraというライブラリ。 古いので認証なしで使える。ただし、python2 用のようで、viera.py を改修。urlilb2を使っていたり、海外のリモコンエミューレトなのでいくつかのボタンが無い(NRC_DATA-ONOFF:dボタン、NRC_PROG-ONOFF:番組表、NRC_SPLIT-ONOFF:2画面?)ので、これらにちょっとだけ対処。ドイツのPanasonic TV mit IPS Steuernというものを参考に。dボタンのコードがなかなか見つからなかった。改修した viera.pyはこちら
import urllib.request,urllib.error, urllib.parse
class Viera(object):
    def __init__(self, hostname, control_url, service_type):
        self.hostname = hostname
        self.control_url = control_url
        self.service_type = service_type
        self.sendkey_action = Action('X_SendKey', ('X_KeyEvent',))
    def _sendkey(self, slug):
        req = self.sendkey_action.to_soap_request(
            self.control_url,
            self.hostname,
            self.service_type,
            (slug,),
        )
        urllib.request.urlopen(req).read()
    def __unicode__(self):
        return '' % (
            self.hostname,
            self.control_url,
            self.service_type,
        )
    def vol_up(self):
        self._sendkey('NRC_VOLUP-ONOFF')
    def vol_down(self):
        self._sendkey('NRC_VOLDOWN-ONOFF')
    def mute(self):
        self._sendkey('NRC_MUTE-ONOFF')
    def num(self, number):
        for digit in str(number):
            self._sendkey('NRC_D%s-ONOFF' % digit)
    def power(self):
        self._sendkey('NRC_TV-ONOFF')
    def toggle_3D(self):
        self._sendkey('NRC_3D-ONOFF')
    def toggle_SDCard(self):
        self._sendkey('NRC_SD_CARD-ONOFF')
    def red(self):
        self._sendkey('NRC_RED-ONOFF')
    def green(self):
        self._sendkey('NRC_GREEN-ONOFF')
    def yellow(self):
        self._sendkey('NRC_YELLOW-ONOFF')
    def blue(self):
        self._sendkey('NRC_BLUE-ONOFF')
    def vtools(self):
        self._sendkey('NRC_VTOOLS-ONOFF')
    def cancel(self):
        self._sendkey('NRC_CANCEL-ONOFF')
    def option(self):
        self._sendkey('NRC_SUBMENU-ONOFF')
    def Return(self):
        self.sendkey('NRC_RETURN-ONOFF')
    def enter(self):
        self._sendkey('NRC_ENTER-ONOFF')
    def right(self):
        self._sendkey('NRC_RIGHT-ONOFF')
    def left(self):
        self._sendkey('NRC_LEFT-ONOFF')
    def up(self):
        self._sendkey('NRC_UP-ONOFF')
    def down(self):
        self._sendkey('NRC_DOWN-ONOFF')
    def display(self):
        self._sendkey('NRC_DISP_MODE-ONOFF')
    def menu(self):
        self._sendkey('NRC_MENU-ONOFF')
    def connect(self):
        self._sendkey('NRC_INTERNET-ONOFF')
    def link(self):
        self._sendkey('NRC_VIERA_LINK-ONOFF')
    def guide(self):
        self._sendkey('NRC_EPG-ONOFF')
    def text(self):
        self._sendkey('NRC_TEXT-ONOFF')
    def subtitles(self):
        self._sendkey('NRC_STTL-ONOFF')
    def info(self):
        self._sendkey('NRC_INFO-ONOFF')
    def index(self):
        self._sendkey('NRC_INDEX-ONOFF')
    def hold(self):
        self._sendkey('NRC_HOLD-ONOFF')
    def d(self):
        self._sendkey('NRC_DATA-ONOFF')
    def prog(self):
        self._sendkey('NRC_PROG-ONOFF')
    def split(self):
        self._sendkey('NRC_SPLIT-ONOFF')
class Action(object):
    def __init__(self, name, arguments):
        self.name = name
        self.arguments = arguments
    def to_soap_request(self, url, hostname, service_type, values):
        assert len(values) == len(self.arguments)
        params = ''.join(['<%s>%s' % (arg, value, arg) for arg, value in zip(self.arguments, values)])
        soap_body = (
            ''
            ''
            ''
            ''
            '%(params)s'
            ''
            ''
        ''
        ) % {
            'method_name': self.name,
            'service_type': service_type,
            'params': params,
        }
        headers = {
            'Host': hostname,
            'Content-Length': len(soap_body),
            'Content-Type': 'text/xml',
            'SOAPAction': '"%s#%s"' % (service_type, self.name),
        }
        soap_body = soap_body.encode()
#        headers = urllib.parse.urlencode(headers).encode()
        req = urllib.request.Request(url, soap_body, headers)
        return req
テスト稼働で、フジテレビめざましじゃんけんを自動で毎日やらせてみた。
import socket
import urllib.request,urllib.error,urllib.parse
from urllib.request import urlopen
from viera import Viera
from pyviera import VieraFinder
import time
import random
tv = Viera('(IP_address)','http://(IP_address):55000/nrc/control_0','urn:panasonic-com:service:p00NetworkControl:1')
if __name__ == '__main__':
    tv.num(8)
    time.sleep(5)
    tv.info()
    for i in range(20):
        button = random.choice(('tv.blue()','tv.red()','tv.green()'))
        eval(button)
        time.sleep(3)
これを、月~金は 05:57,06:57,07:34,07:57に、土は 07:37,08:21 にcronで起動。 テレビがついていれば、IP経由でch8 に変更して、ランダムに青赤緑を投げつける。 半年以上動かしていて、一週間で最大300点オーバーの週もあった。 ちなみに、最近のvieraはアプリ最初のアクセス時に認証を求めるようで、そうするとこちらのライブラリが良さそう。 https://github.com/florianholzapfel/panasonic-viera/issues/9
https://github.com/florianholzapfel/panasonic-viera/blob/master/panasonic_viera/__init__.py PINコードを入力して、credential情報を取得し使い回す必要があるようだ。

certbot on FreeBSD9

なかなかやめられない古いOSで、次は証明書(let's encrypt) を使い続けたくて。。。 運用には certbot を使うのがデフォルトのようで、ports が使えないことを前提にすれば、git clone で持ってくるのがベスト。 ということで、

git clone https://github.com/certbot/certbot

を行ったら、

SSL23_GET_SERVER_HELLO:tlsv1 alert protocol version

ということで、gitが tls1.2をしゃべらない!のが問題(古い git clientすぎた)ので、まずはこれをバージョンアップしてみた。ところが、、、直らない。。。見ると gitは裏では curl を使っているようなので、これが tls1.2を使うようにしないといけなかった。ということで、curlも最新版を持ってきて openssl も tls1.2 を使えるバージョンを参照してinstall. でも、今度は git-remote-https.core を吐いて何もすすまない。shared ライブラリ参照が違うとかそこらへんだろうから、ということで、libzも入れ直してみたがやはりおかしい。。。 コンパイラ環境の問題かもと疑ってみると、そこらへんだったみたい。 curl -> /usr/bin/gcc 利用 git -> /usr/bin/gcc 利用は coreを吐く。 ということで環境変数 CC=/usr/local/bin/gcc を設定しコンパイルしなした gitだとちゃんと cloneできた。 libz,libiconv,libintl,libchasetあたりも/usr/local/lib 側をちゃんとリンクしてくれた。 いまどきllvm,clangなんでしょうが、、、、、もうちょい生きながらえさせて。 git cloneでファイルをダウンロードできたが、次にhttps://certbot.eff.org/docs/contributing.htmlを参照して、

python3 tools/venv3.py

なのだが、ここでどうも yaml(pyyaml)のところでエラー。これも gccを利用しているが/usr/bin/gcc を見ているケースと/usr/local/bin/gcc を見るケースがあるのが問題のよう。 .cshrc で /usr/local/bin を優先させることで /usr/local/bin/gcc を先に見るようにしたところ、まずはここはクリア。 bash で
source venv3/bin/activate
run_acme_server &
certbot_test certonly --standalone -d test.example.com
と実行してまずはローカルで動くかどうかを試すのだが、run_acme_server のところで落ちる。うまく webserver が起き上がってくれない。
[venv3] root@host:/usr/local/src/certbot # run_acme_server
=> Starting pebble instance deployment...
ELF binary type "0" not known.
=> Tear down the test infrastructure...
=> Test infrastructure stopped and cleaned up.
Traceback (most recent call last):
  File "/usr/local/src/certbot/venv3/bin/run_acme_server", line 11, in 
    load_entry_point('certbot-ci', 'console_scripts', 'run_acme_server')()
  File "/usr/local/src/certbot/certbot-ci/certbot_integration_tests/utils/acme_server.py", line 221, in main
    with acme_server as acme_xdist:
  File "/usr/local/src/certbot/certbot-ci/certbot_integration_tests/utils/acme_server.py", line 96, in __enter__
    self.start()
  File "/usr/local/src/certbot/certbot-ci/certbot_integration_tests/utils/acme_server.py", line 62, in start
    raise e
  File "/usr/local/src/certbot/certbot-ci/certbot_integration_tests/utils/acme_server.py", line 57, in start
    self._prepare_pebble_server()
  File "/usr/local/src/certbot/certbot-ci/certbot_integration_tests/utils/acme_server.py", line 135, in _prepare_pebble_server
    self._launch_process(
  File "/usr/local/src/certbot/certbot-ci/certbot_integration_tests/utils/acme_server.py", line 205, in _launch_process
    process = subprocess.Popen(command, stdout=stdout, stderr=subprocess.STDOUT, cwd=cwd, env=env)
  File "/usr/local/lib/python3.8/subprocess.py", line 854, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
  File "/usr/local/lib/python3.8/subprocess.py", line 1702, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
OSError: [Errno 8] Exec format error: '/usr/local/src/certbot/certbot-ci/certbot_integration_tests/assets/pebble_v2.3.0_linux-amd64'
なんか、certbot が linuxの実行ファイル(linux binary for amd64)を抱えていてこれが freebsdでは動かない、ということのようだ。linux-compat とか入れればいいのかもしれないがそこまでやるかな。。。ということでこの線はあきらめて、、、 certbot は普通に python(2or3)のコードだったことを思い出して、$src/certbot/の中を見ると、setup.pyがあるので、
python3 setup.py build
python3 setup.py install
で、/usr/local/bin/certbot にインストールされた。
certbot certonly --standalone -d test.example.com
でちゃんと証明書が降りてきた。

certbot on FreeBSD9

なかなかやめられない古いOSで、次は証明書(let's encrypt) を使い続けたくて。。。 運用には certbot を使うのがデフォルトのようで、ports が使えないことを前提にすれば、git clone で持ってくるのがベスト。 ということで、

git clone https://github.com/certbot/certbot

を行ったら、

SSL23_GET_SERVER_HELLO:tlsv1 alert protocol version

ということで、gitが tls1.2をしゃべらない!のが問題(古い git clientすぎた)ので、まずはこれをバージョンアップしてみた。ところが、、、直らない。。。見ると gitは裏では curl を使っているようなので、これが tls1.2を使うようにしないといけなかった。ということで、curlも最新版を持ってきて openssl も tls1.2 を使えるバージョンを参照してinstall. でも、今度は git-remote-https.core を吐いて何もすすまない。shared ライブラリ参照が違うとかそこらへんだろうから、ということで、libzも入れ直してみたがやはりおかしい。。。 コンパイラ環境の問題かもと疑ってみると、そこらへんだったみたい。 curl -> /usr/bin/gcc 利用 git -> /usr/bin/gcc 利用は coreを吐く。 ということで環境変数 CC=/usr/local/bin/gcc を設定しコンパイルしなした gitだとちゃんと cloneできた。 libz,libiconv,libintl,libchasetあたりも/usr/local/lib 側をちゃんとリンクしてくれた。 いまどきllvm,clangなんでしょうが、、、、、もうちょい生きながらえさせて。 git cloneでファイルをダウンロードできたが、次にhttps://certbot.eff.org/docs/contributing.htmlを参照して、

python3 tools/venv3.py

なのだが、ここでどうも yaml(pyyaml)のところでエラー。これも gccを利用しているが/usr/bin/gcc を見ているケースと/usr/local/bin/gcc を見るケースがあるのが問題のよう。 .cshrc で /usr/local/bin を優先させることで /usr/local/bin/gcc を先に見るようにしたところ、まずはここはクリア。 bash で
source venv3/bin/activate
run_acme_server &
certbot_test certonly --standalone -d test.example.com
と実行してまずはローカルで動くかどうかを試すのだが、run_acme_server のところで落ちる。うまく webserver が起き上がってくれない。
[venv3] root@host:/usr/local/src/certbot # run_acme_server
=> Starting pebble instance deployment...
ELF binary type "0" not known.
=> Tear down the test infrastructure...
=> Test infrastructure stopped and cleaned up.
Traceback (most recent call last):
  File "/usr/local/src/certbot/venv3/bin/run_acme_server", line 11, in 
    load_entry_point('certbot-ci', 'console_scripts', 'run_acme_server')()
  File "/usr/local/src/certbot/certbot-ci/certbot_integration_tests/utils/acme_server.py", line 221, in main
    with acme_server as acme_xdist:
  File "/usr/local/src/certbot/certbot-ci/certbot_integration_tests/utils/acme_server.py", line 96, in __enter__
    self.start()
  File "/usr/local/src/certbot/certbot-ci/certbot_integration_tests/utils/acme_server.py", line 62, in start
    raise e
  File "/usr/local/src/certbot/certbot-ci/certbot_integration_tests/utils/acme_server.py", line 57, in start
    self._prepare_pebble_server()
  File "/usr/local/src/certbot/certbot-ci/certbot_integration_tests/utils/acme_server.py", line 135, in _prepare_pebble_server
    self._launch_process(
  File "/usr/local/src/certbot/certbot-ci/certbot_integration_tests/utils/acme_server.py", line 205, in _launch_process
    process = subprocess.Popen(command, stdout=stdout, stderr=subprocess.STDOUT, cwd=cwd, env=env)
  File "/usr/local/lib/python3.8/subprocess.py", line 854, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
  File "/usr/local/lib/python3.8/subprocess.py", line 1702, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
OSError: [Errno 8] Exec format error: '/usr/local/src/certbot/certbot-ci/certbot_integration_tests/assets/pebble_v2.3.0_linux-amd64'
なんか、certbot が linuxの実行ファイル(linux binary for amd64)を抱えていてこれが freebsdでは動かない、ということのようだ。linux-compat とか入れればいいのかもしれないがそこまでやるかな。。。ということでこの線はあきらめて、、、 certbot は普通に python(2or3)のコードだったことを思い出して、$src/certbot/の中を見ると、setup.pyがあるので、
python3 setup.py build
python3 setup.py install
で、/usr/local/bin/certbot にインストールされた。
certbot certonly --standalone -d test.example.com
でちゃんと証明書が降りてきた。

python3 で、web上の csvを取得してメールにて添付して毎日飛ばす。

web上の csv ファイルを取得して、メールに添付しながら本文にはテキストでどこぞのキーでsortして毎日・週一でレポートを飛ばす。そのときの備忘録。

python3 で、web上の csvを取得してメールにて添付して毎日飛ばす。

web上の csv ファイルを取得して、メールに添付しながら本文にはテキストでどこぞのキーでsortして毎日・週一でレポートを飛ばす。そのときの備忘録。

スマートメーター HEMS データのグラフ化 for python3

HEMSでデータが取得出来るようになったので、時系列でグラフ化してみる。 自宅では特に発電してるわけでもないので、「定時積算電力量計測値(逆方向計測値)」とか、履歴はいらないか。
ここでは「瞬時電流計測値」「瞬時電力計測値」「積算電力量計測値(正方向計測値)」「積算電力量計測値(逆方向計測値)」を測り、cacti の scriptsで動かして5分毎のデータをグラフ化。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
#  smartmeter.py3
#
from __future__ import print_function
import sys
import serial
import time
# Bルート認証ID(東京電力パワーグリッドから郵送で送られてくる)
rbid  = "AAAABBBBCCCCDDDD"
# Bルート認証パスワード(東京電力パワーグリッドからメールで送られてくる)
rbpwd = "PASSWORDDDDDDD"
# シリアルポートデバイス名
#serialPortDev = 'COM3'  # Windows の場合
serialPortDev = '/dev/ttyUSB0'  # Linux(ラズパイなど)の場合
#serialPortDev = '/dev/cu.usbserial-A103BTPR'    # Mac の場合
#
ser = serial.Serial(serialPortDev, 115200)
#
# Bルート認証パスワード設定
wcom="SKSETPWD C " + rbpwd + "\r\n"
ser.write(str.encode(wcom))
# Bルート認証ID設定
wcom="SKSETRBID " + rbid + "\r\n"
ser.write(str.encode(wcom))
scanRes = {} # スキャン結果の入れ物
scanRes["Channel"]="33"
scanRes["Channel Page"]="09"
scanRes["Pan ID"]="24EF"
scanRes["Addr"]="00FFEEDDCCBBAA99"
scanRes["LQI"]="3C"
scanRes["PairID"]="12345678"
# スキャン結果からChannelを設定。
wcom="SKSREG S3 " + scanRes["Pan ID"] + "\r\n"
ser.write(str.encode(wcom))
#print(ser.readline(), end="") # エコーバック
ser.readline()
#print(ser.readline(), end="") # OKが来るはず(チェック無し)
ser.readline()
# スキャン結果からPan IDを設定
wcom="SKSREG S3 " + scanRes["Pan ID"] + "\r\n"
ser.write(str.encode(wcom))
#print(ser.readline(), end="") # エコーバック
ser.readline()
#print(ser.readline(), end="") # OKが来るはず(チェック無し)
ser.readline()
# 直接指定しちゃう
ipv6Addr = "FE80:0000:0000:0000:02FF:EEDD:CCBB:AA99"
#
# いきなり SKJOINをしてもつながらないことがあるため、SKPINGしてみます。
#
while True:
    wcom="SKPING " + ipv6Addr + "\r\n"
    ser.write(str.encode(wcom))
    time.sleep(1)
#    print(ser.readline(), end="") # echo
#    print(ser.readline(), end="") # res
    pingres = ser.readline() #
    if pingres.startswith(b"OK"):
       break
#
#
#
# PANA 接続シーケンスを開始します。
#time.sleep(1)
wcom="SKJOIN " + ipv6Addr + "\r\n"
ser.write(str.encode(wcom))
# PANA 接続完了待ち(10行ぐらいなんか返してくる)
bConnected = False
while not bConnected :
    line = ser.readline()
#    print(line, end="")
    if line.startswith(b"EVENT 24") :
#        print("PANA 接続失敗")
        sys.exit()  #### 糸冬了 ####
    elif line.startswith(b"EVENT 25") :
        # 接続完了!
        bConnected = True
# これ以降、シリアル通信のタイムアウトを設定
ser.timeout = 2
# スマートメーターがインスタンスリスト通知を投げてくる
# (ECHONET-Lite_Ver.1.12_02.pdf p.4-16)
#print(ser.readline(), end="") #無視
while True:
    # ECHONET Lite フレーム作成
    #  参考資料
    #  ・ECHONET-Lite_Ver.1.12_02.pdf (以下 EL) : 最新版は ver 1.13_02
    #  ・Appendix_H.pdf (以下 AppH)            : 最新版は J (2018/07/30現在)
    #  for EPC = 0xE7  (瞬時電力計測値)
    echonetLiteFrame = b"" # echonetLiteFrame type is byte
    echonetLiteFrame += b'\x10\x81'      # EHD (参考:EL p.3-2) : 2byte目は \x81(電文形式1) or \x82(電文形式2)
    echonetLiteFrame += b'\x00\x01'      # TID (参考:EL p.3-3) : TID= Transaction ID
    # ここから EDATA
    echonetLiteFrame += b'\x05\xFF\x01'  # SEOJ (参考:EL p.3-3 AppH p.3-408~) : コントローラクラス規定
    echonetLiteFrame += b'\x02\x88\x01'  # DEOJ (参考:EL p.3-3 AppH p.3-274~) : 低圧スマート電力量メータクラス規定
    echonetLiteFrame += b'\x62'          # ESV(62:プロパティ値読み出し要求) (参考:EL p.3-5)  : Get
    echonetLiteFrame += b'\x01'          # OPC(1個)(参考:EL p.3-7)
#    echonetLiteFrame += "\x02"          # OPC(2個)(参考:EL p.3-7)   : 2つのデータを取得する。
    echonetLiteFrame += b'\xE7'          # EPC(参考:EL p.3-7 AppH p.3-275) : 瞬時電力計測値
    echonetLiteFrame += b'\x00'          # PDC(参考:EL p.3-9)
#    echonetLiteFrame += "\xE8"          # EPC(参考:EL p.3-7 AppH p.3-275) : 瞬時電流計測値 R相T相
#    echonetLiteFrame += "\x00"          # PDC(参考:EL p.3-9)
    # コマンド送信
    com1 = "SKSENDTO 1 {0} 0E1A 1 {1:04X} ".format(ipv6Addr, len(echonetLiteFrame))
    command = com1.encode() + echonetLiteFrame
    ser.write(command)
    ser.readline()
#    print(ser.readline(), end="") # EVENT 21 が来るはず(チェック無し)
    ser.readline()
#    print(ser.readline(), end="") # OKが来るはず(チェック無し)
    ser.readline()
    line = ser.readline()         # ERXUDPが来るはず
#    print(line, end="")
    # 取りこぼしたりして変なデータを拾うことがあるので
    # チェックを厳しめにしてます。
    if line.startswith(b"ERXUDP") :
        line = line.decode()
        cols = line.strip().split(' ')
        res = cols[8]   # UDP受信データ部分
        #tid = res[4:4+4];
        seoj = res[8:8+6]
        #deoj = res[14,14+6]
        ESV = res[20:20+2]
        #OPC = res[22,22+2]
        if seoj == "028801" and ESV == "72" :
            # スマートメーター(028801)から来た応答(72)なら
            EPC = res[24:24+2]
            if EPC == "E7" :
                # 内容が瞬時電力計測値(E7)だったら
                hexPower = line[-8:]    # 最後の4バイト(16進数で8文字)が瞬時電力計測値
                intPower = int(hexPower, 16)
                outputE7=intPower
                break
#####################################################################################################
#
#   for EPC = 0xE8 (瞬時電流計測値)
#
while True:
    # ECHONET Lite フレーム作成
    #  参考資料
    #  ・ECHONET-Lite_Ver.1.12_02.pdf (以下 EL)
    #  ・Appendix_H.pdf (以下 AppH)
    #  for EPC = 0xE7  (瞬時電力計測値)
    echonetLiteFrame = b""
    echonetLiteFrame += b'\x10\x81'      # EHD (参考:EL p.3-2) : 2byte目は \x81(電文形式1) or \x82(電文形式2)
    echonetLiteFrame += b'\x00\x01'      # TID (参考:EL p.3-3) : TID= Transaction ID
    # ここから EDATA
    echonetLiteFrame += b'\x05\xFF\x01'  # SEOJ (参考:EL p.3-3 AppH p.3-408~) : コントローラクラス規定
    echonetLiteFrame += b'\x02\x88\x01'  # DEOJ (参考:EL p.3-3 AppH p.3-274~) : 低圧スマート電力量メータクラス規定
    echonetLiteFrame += b'\x62'          # ESV(62:プロパティ値読み出し要求) (参考:EL p.3-5)  : Get
    echonetLiteFrame += b'\x01'          # OPC(1個)(参考:EL p.3-7)
#    echonetLiteFrame += "\x02"          # OPC(2個)(参考:EL p.3-7)   : 2つのデータを取得する。
#    echonetLiteFrame += "\xE7"          # EPC(参考:EL p.3-7 AppH p.3-275) : 瞬時電力計測値
#    echonetLiteFrame += "\x00"          # PDC(参考:EL p.3-9)
    echonetLiteFrame += b'\xE8'          # EPC(参考:EL p.3-7 AppH p.3-275) : 瞬時電流計測値 R相T相
    echonetLiteFrame += b'\x00'          # PDC(参考:EL p.3-9)
    # コマンド送信
    com1 = "SKSENDTO 1 {0} 0E1A 1 {1:04X} ".format(ipv6Addr, len(echonetLiteFrame))
    command = com1.encode() + echonetLiteFrame
    ser.write(command)
#    print(ser.readline(), end="") # エコーバック
    ser.readline()
#    print(ser.readline(), end="") # EVENT 21 が来るはず(チェック無し)
    ser.readline()
#    print(ser.readline(), end="") # OKが来るはず(チェック無し)
    ser.readline()
    line = ser.readline()         # ERXUDPが来るはず
#    print(line, end="")
    # 取りこぼしたりして変なデータを拾うことがあるので
    # チェックを厳しめにしてます。
    if line.startswith(b"ERXUDP") :
        line = line.decode()
        cols = line.strip().split(' ')
        res = cols[8]   # UDP受信データ部分
        #tid = res[4:4+4];
        seoj = res[8:8+6]
        #deoj = res[14,14+6]
        ESV = res[20:20+2]
        #OPC = res[22,22+2]
        if seoj == "028801" and ESV == "72" :
            # スマートメーター(028801)から来た応答(72)なら
            EPC = res[24:24+2]
            if EPC == "E8" :
                # 内容が瞬時電流力計測値(E8)だったら
#                hexPower = line[-8:]    # 最後の4バイト(16進数で8文字)が瞬時電力計測値
#                intPower = int(hexPower, 16)
                rCur = res[-8:-4]
                tCur = res[-4:]
                intrCur = round( float( int(rCur, 16) * 0.1 ), 1)
                inttCur = round( float( int(tCur, 16) * 0.1 ), 1)
                break
##################################################################################################
#
#   for EPC = 0xE0 (積算電力量計測値(正方向計測値))
#
while True:
    # ECHONET Lite フレーム作成
    #  参考資料
    #  ・ECHONET-Lite_Ver.1.12_02.pdf (以下 EL)
    #  ・Appendix_H.pdf (以下 AppH)
    #  for EPC = 0xE7  (瞬時電力計測値)
    echonetLiteFrame = b""
    echonetLiteFrame += b'\x10\x81'      # EHD (参考:EL p.3-2) : 2byte目は \x81(電文形式1) or \x82(電文形式2)
    echonetLiteFrame += b'\x00\x01'      # TID (参考:EL p.3-3) : TID= Transaction ID
    # ここから EDATA
    echonetLiteFrame += b'\x05\xFF\x01'  # SEOJ (参考:EL p.3-3 AppH p.3-408~) : コントローラクラス規定
    echonetLiteFrame += b'\x02\x88\x01'  # DEOJ (参考:EL p.3-3 AppH p.3-274~) : 低圧スマート電力量メータクラス規定
    echonetLiteFrame += b'\x62'          # ESV(62:プロパティ値読み出し要求) (参考:EL p.3-5)  : Get
    echonetLiteFrame += b'\x01'          # OPC(1個)(参考:EL p.3-7)
#    echonetLiteFrame += "\x02"          # OPC(2個)(参考:EL p.3-7)   : 2つのデータを取得する。
#    echonetLiteFrame += "\xE7"          # EPC(参考:EL p.3-7 AppH p.3-275) : 瞬時電力計測値
#    echonetLiteFrame += "\x00"          # PDC(参考:EL p.3-9)
    echonetLiteFrame += b'\xE0'          # EPC(参考:EL p.3-7 AppH p.3-275) : 積算電力量計測値
    echonetLiteFrame += b'\x00'          # PDC(参考:EL p.3-9)
    # コマンド送信
    com1 = "SKSENDTO 1 {0} 0E1A 1 {1:04X} ".format(ipv6Addr, len(echonetLiteFrame))
    command = com1.encode() + echonetLiteFrame
    ser.write(command)
#    print(ser.readline(), end="") # エコーバック
    ser.readline()
#    print(ser.readline(), end="") # EVENT 21 が来るはず(チェック無し)
    ser.readline()
#    print(ser.readline(), end="") # OKが来るはず(チェック無し)
    ser.readline()
    line = ser.readline()         # ERXUDPが来るはず
#    print(line, end="")
    # 取りこぼしたりして変なデータを拾うことがあるので
    # チェックを厳しめにしてます。
    if line.startswith(b"ERXUDP") :
        line = line.decode()
        cols = line.strip().split(' ')
        res = cols[8]   # UDP受信データ部分
        #tid = res[4:4+4];
        seoj = res[8:8+6]
        #deoj = res[14,14+6]
        ESV = res[20:20+2]
        #OPC = res[22,22+2]
        if seoj == "028801" and ESV == "72" :
            # スマートメーター(028801)から来た応答(72)なら
            EPC = res[24:24+2]
            if EPC == "E0" :
                # 内容が瞬時電流力計測値(E8)だったら
                hexPower = line[-8:]    # 最後の4バイト(16進数で8文字)が瞬時電力計測値
#                intPower = int(hexPower, 16)
#                floatPower = round ( float( int( hexPower, 16) * 0.1 ),1)
#               cacti の Data Source で COUNTER を使いたい、となるとデータは intでなくてはいけなくて、floatはだめ。
#               なので、係数を使わずにそのままにする。
                intPower =  int( hexPower, 16)
                break
##################################################################################################
#
#   for EPC = 0xE3 (積算電力量計測値(逆方向計測値))
#
while True:
    #  for EPC = 0xE3
    echonetLiteFrame = b""
    echonetLiteFrame += b'\x10\x81'      # EHD (参考:EL p.3-2) : 2byte目は \x81(電文形式1) or \x82(電文形式2)
    echonetLiteFrame += b'\x00\x01'      # TID (参考:EL p.3-3) : TID= Transaction ID
    # ここから EDATA
    echonetLiteFrame += b'\x05\xFF\x01'  # SEOJ (参考:EL p.3-3 AppH p.3-408~) : コントローラクラス規定
    echonetLiteFrame += b'\x02\x88\x01'  # DEOJ (参考:EL p.3-3 AppH p.3-274~) : 低圧スマート電力量メータクラス規定
    echonetLiteFrame += b'\x62'          # ESV(62:プロパティ値読み出し要求) (参考:EL p.3-5)  : Get
    echonetLiteFrame += b'\x01'          # OPC(1個)(参考:EL p.3-7)
    echonetLiteFrame += b'\xE3'          # EPC(参考:EL p.3-7 AppH p.3-275) : 積算電力量計測値
    echonetLiteFrame += b'\x00'          # PDC(参考:EL p.3-9)
    # コマンド送信
    com1 = "SKSENDTO 1 {0} 0E1A 1 {1:04X} ".format(ipv6Addr, len(echonetLiteFrame))
    command = com1.encode() + echonetLiteFrame
    ser.write(command)
    ser.readline()
    ser.readline()
    ser.readline()
    line = ser.readline()         # ERXUDPが来るはず
    # 取りこぼしたりして変なデータを拾うことがあるので
    # チェックを厳しめにしてます。
    if line.startswith(b"ERXUDP") :
        line = line.decode()
        cols = line.strip().split(' ')
        res = cols[8]   # UDP受信データ部分
        #tid = res[4:4+4];
        seoj = res[8:8+6]
        #deoj = res[14,14+6]
        ESV = res[20:20+2]
        #OPC = res[22,22+2]
        if seoj == "028801" and ESV == "72" :
            # スマートメーター(028801)から来た応答(72)なら
            EPC = res[24:24+2]
            if EPC == "E3" :
                # 内容が瞬時電流力計測値(E8)だったら
                hexPower = line[-8:]    # 最後の4バイト(16進数で8文字)が瞬時電力計測値
#                intPower = int(hexPower, 16)
                intPowerM = int(hexPower, 16)
                break
#######################################################################################################
#
print ("E7:{0} E8r:{1} E8t:{2} E0:{3} E3:{4}".format(outputE7,intrCur,inttCur,intPower,intPowerM),end="")
#
#
ser.close()

出力はcactiに食わせる形式にしていて、E7:瞬時電力計測値,E8r:瞬時電流計測値(R相),E8t:瞬時電流計測値(T相),E0:積算電力量計測値(正方向計測値),E3:積算電力量計測値(逆方向計測値)。

raspberrypi:~/smartmeter3# python3 /usr/share/cacti/scripts/smartmeter.py3
E7:686 E8r:7.0 E8t:1.0 E0:86704 E3:10
瞬間電力値、瞬間電流値は5分に1回のピンポイントの計測なので、瞬発的な消費は拾えてないこともある。

瞬間電流値の5分グラフ(T相、R相) cacti_graph_143.png 電子レンジ、電気ポットなどでの瞬間ピークが出ている。

瞬間電力値 cacti_graph_142.png

積算電力量を cactiの counterでグラフ化したもの。 cacti_graph_144.png 5分データだと、最小単位(0.1 KWH)を超えないときがある。もっと細かな数値が取れると良いけど HEMS機器の出力積算電力量単位(EPC 0xE1)が 0.1 kWhなので難しい。

瞬間値については5分に1回ではなくもっと詳細に取るのもありだと思うが、一応HEMSのBルートは送信制限時間の制限(たしか1時間に360秒)がある。たとえばここ。この制限、どういう基準で計測しての360秒なのか詳しくわからない。まさか TCPでの接続セッションの時間とは思えない。UDPだとさらにわかりにくい?。電波系に詳しい人にはご承知おきのことかもしれない。にしても、連続して毎秒で計測値を取るような利用の仕方だとあっさり超えそうな気もする。
今後は、この資料のP.32にもあるように、「通信負担を軽減する目的から、1つのパケットで複数のコマンド処理が可能なマルチゲットコマンド(エコーネットコンソーシアムが定義)などの実装が推奨される。」を行ってみることか? 最近はいろいろな Bルート製品が出ているのも気になる。

スマートメーター HEMS データのグラフ化 for python3

HEMSでデータが取得出来るようになったので、時系列でグラフ化してみる。 自宅では特に発電してるわけでもないので、「定時積算電力量計測値(逆方向計測値)」とか、履歴はいらないか。
ここでは「瞬時電流計測値」「瞬時電力計測値」「積算電力量計測値(正方向計測値)」「積算電力量計測値(逆方向計測値)」を測り、cacti の scriptsで動かして5分毎のデータをグラフ化。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
#  smartmeter.py3
#
from __future__ import print_function
import sys
import serial
import time
# Bルート認証ID(東京電力パワーグリッドから郵送で送られてくる)
rbid  = "AAAABBBBCCCCDDDD"
# Bルート認証パスワード(東京電力パワーグリッドからメールで送られてくる)
rbpwd = "PASSWORDDDDDDD"
# シリアルポートデバイス名
#serialPortDev = 'COM3'  # Windows の場合
serialPortDev = '/dev/ttyUSB0'  # Linux(ラズパイなど)の場合
#serialPortDev = '/dev/cu.usbserial-A103BTPR'    # Mac の場合
#
ser = serial.Serial(serialPortDev, 115200)
#
# Bルート認証パスワード設定
wcom="SKSETPWD C " + rbpwd + "\r\n"
ser.write(str.encode(wcom))
# Bルート認証ID設定
wcom="SKSETRBID " + rbid + "\r\n"
ser.write(str.encode(wcom))
scanRes = {} # スキャン結果の入れ物
scanRes["Channel"]="33"
scanRes["Channel Page"]="09"
scanRes["Pan ID"]="24EF"
scanRes["Addr"]="00FFEEDDCCBBAA99"
scanRes["LQI"]="3C"
scanRes["PairID"]="12345678"
# スキャン結果からChannelを設定。
wcom="SKSREG S3 " + scanRes["Pan ID"] + "\r\n"
ser.write(str.encode(wcom))
#print(ser.readline(), end="") # エコーバック
ser.readline()
#print(ser.readline(), end="") # OKが来るはず(チェック無し)
ser.readline()
# スキャン結果からPan IDを設定
wcom="SKSREG S3 " + scanRes["Pan ID"] + "\r\n"
ser.write(str.encode(wcom))
#print(ser.readline(), end="") # エコーバック
ser.readline()
#print(ser.readline(), end="") # OKが来るはず(チェック無し)
ser.readline()
# 直接指定しちゃう
ipv6Addr = "FE80:0000:0000:0000:02FF:EEDD:CCBB:AA99"
#
# いきなり SKJOINをしてもつながらないことがあるため、SKPINGしてみます。
#
while True:
    wcom="SKPING " + ipv6Addr + "\r\n"
    ser.write(str.encode(wcom))
    time.sleep(1)
#    print(ser.readline(), end="") # echo
#    print(ser.readline(), end="") # res
    pingres = ser.readline() #
    if pingres.startswith(b"OK"):
       break
#
#
#
# PANA 接続シーケンスを開始します。
#time.sleep(1)
wcom="SKJOIN " + ipv6Addr + "\r\n"
ser.write(str.encode(wcom))
# PANA 接続完了待ち(10行ぐらいなんか返してくる)
bConnected = False
while not bConnected :
    line = ser.readline()
#    print(line, end="")
    if line.startswith(b"EVENT 24") :
#        print("PANA 接続失敗")
        sys.exit()  #### 糸冬了 ####
    elif line.startswith(b"EVENT 25") :
        # 接続完了!
        bConnected = True
# これ以降、シリアル通信のタイムアウトを設定
ser.timeout = 2
# スマートメーターがインスタンスリスト通知を投げてくる
# (ECHONET-Lite_Ver.1.12_02.pdf p.4-16)
#print(ser.readline(), end="") #無視
while True:
    # ECHONET Lite フレーム作成
    #  参考資料
    #  ・ECHONET-Lite_Ver.1.12_02.pdf (以下 EL) : 最新版は ver 1.13_02
    #  ・Appendix_H.pdf (以下 AppH)            : 最新版は J (2018/07/30現在)
    #  for EPC = 0xE7  (瞬時電力計測値)
    echonetLiteFrame = b"" # echonetLiteFrame type is byte
    echonetLiteFrame += b'\x10\x81'      # EHD (参考:EL p.3-2) : 2byte目は \x81(電文形式1) or \x82(電文形式2)
    echonetLiteFrame += b'\x00\x01'      # TID (参考:EL p.3-3) : TID= Transaction ID
    # ここから EDATA
    echonetLiteFrame += b'\x05\xFF\x01'  # SEOJ (参考:EL p.3-3 AppH p.3-408~) : コントローラクラス規定
    echonetLiteFrame += b'\x02\x88\x01'  # DEOJ (参考:EL p.3-3 AppH p.3-274~) : 低圧スマート電力量メータクラス規定
    echonetLiteFrame += b'\x62'          # ESV(62:プロパティ値読み出し要求) (参考:EL p.3-5)  : Get
    echonetLiteFrame += b'\x01'          # OPC(1個)(参考:EL p.3-7)
#    echonetLiteFrame += "\x02"          # OPC(2個)(参考:EL p.3-7)   : 2つのデータを取得する。
    echonetLiteFrame += b'\xE7'          # EPC(参考:EL p.3-7 AppH p.3-275) : 瞬時電力計測値
    echonetLiteFrame += b'\x00'          # PDC(参考:EL p.3-9)
#    echonetLiteFrame += "\xE8"          # EPC(参考:EL p.3-7 AppH p.3-275) : 瞬時電流計測値 R相T相
#    echonetLiteFrame += "\x00"          # PDC(参考:EL p.3-9)
    # コマンド送信
    com1 = "SKSENDTO 1 {0} 0E1A 1 {1:04X} ".format(ipv6Addr, len(echonetLiteFrame))
    command = com1.encode() + echonetLiteFrame
    ser.write(command)
    ser.readline()
#    print(ser.readline(), end="") # EVENT 21 が来るはず(チェック無し)
    ser.readline()
#    print(ser.readline(), end="") # OKが来るはず(チェック無し)
    ser.readline()
    line = ser.readline()         # ERXUDPが来るはず
#    print(line, end="")
    # 取りこぼしたりして変なデータを拾うことがあるので
    # チェックを厳しめにしてます。
    if line.startswith(b"ERXUDP") :
        line = line.decode()
        cols = line.strip().split(' ')
        res = cols[8]   # UDP受信データ部分
        #tid = res[4:4+4];
        seoj = res[8:8+6]
        #deoj = res[14,14+6]
        ESV = res[20:20+2]
        #OPC = res[22,22+2]
        if seoj == "028801" and ESV == "72" :
            # スマートメーター(028801)から来た応答(72)なら
            EPC = res[24:24+2]
            if EPC == "E7" :
                # 内容が瞬時電力計測値(E7)だったら
                hexPower = line[-8:]    # 最後の4バイト(16進数で8文字)が瞬時電力計測値
                intPower = int(hexPower, 16)
                outputE7=intPower
                break
#####################################################################################################
#
#   for EPC = 0xE8 (瞬時電流計測値)
#
while True:
    # ECHONET Lite フレーム作成
    #  参考資料
    #  ・ECHONET-Lite_Ver.1.12_02.pdf (以下 EL)
    #  ・Appendix_H.pdf (以下 AppH)
    #  for EPC = 0xE7  (瞬時電力計測値)
    echonetLiteFrame = b""
    echonetLiteFrame += b'\x10\x81'      # EHD (参考:EL p.3-2) : 2byte目は \x81(電文形式1) or \x82(電文形式2)
    echonetLiteFrame += b'\x00\x01'      # TID (参考:EL p.3-3) : TID= Transaction ID
    # ここから EDATA
    echonetLiteFrame += b'\x05\xFF\x01'  # SEOJ (参考:EL p.3-3 AppH p.3-408~) : コントローラクラス規定
    echonetLiteFrame += b'\x02\x88\x01'  # DEOJ (参考:EL p.3-3 AppH p.3-274~) : 低圧スマート電力量メータクラス規定
    echonetLiteFrame += b'\x62'          # ESV(62:プロパティ値読み出し要求) (参考:EL p.3-5)  : Get
    echonetLiteFrame += b'\x01'          # OPC(1個)(参考:EL p.3-7)
#    echonetLiteFrame += "\x02"          # OPC(2個)(参考:EL p.3-7)   : 2つのデータを取得する。
#    echonetLiteFrame += "\xE7"          # EPC(参考:EL p.3-7 AppH p.3-275) : 瞬時電力計測値
#    echonetLiteFrame += "\x00"          # PDC(参考:EL p.3-9)
    echonetLiteFrame += b'\xE8'          # EPC(参考:EL p.3-7 AppH p.3-275) : 瞬時電流計測値 R相T相
    echonetLiteFrame += b'\x00'          # PDC(参考:EL p.3-9)
    # コマンド送信
    com1 = "SKSENDTO 1 {0} 0E1A 1 {1:04X} ".format(ipv6Addr, len(echonetLiteFrame))
    command = com1.encode() + echonetLiteFrame
    ser.write(command)
#    print(ser.readline(), end="") # エコーバック
    ser.readline()
#    print(ser.readline(), end="") # EVENT 21 が来るはず(チェック無し)
    ser.readline()
#    print(ser.readline(), end="") # OKが来るはず(チェック無し)
    ser.readline()
    line = ser.readline()         # ERXUDPが来るはず
#    print(line, end="")
    # 取りこぼしたりして変なデータを拾うことがあるので
    # チェックを厳しめにしてます。
    if line.startswith(b"ERXUDP") :
        line = line.decode()
        cols = line.strip().split(' ')
        res = cols[8]   # UDP受信データ部分
        #tid = res[4:4+4];
        seoj = res[8:8+6]
        #deoj = res[14,14+6]
        ESV = res[20:20+2]
        #OPC = res[22,22+2]
        if seoj == "028801" and ESV == "72" :
            # スマートメーター(028801)から来た応答(72)なら
            EPC = res[24:24+2]
            if EPC == "E8" :
                # 内容が瞬時電流力計測値(E8)だったら
#                hexPower = line[-8:]    # 最後の4バイト(16進数で8文字)が瞬時電力計測値
#                intPower = int(hexPower, 16)
                rCur = res[-8:-4]
                tCur = res[-4:]
                intrCur = round( float( int(rCur, 16) * 0.1 ), 1)
                inttCur = round( float( int(tCur, 16) * 0.1 ), 1)
                break
##################################################################################################
#
#   for EPC = 0xE0 (積算電力量計測値(正方向計測値))
#
while True:
    # ECHONET Lite フレーム作成
    #  参考資料
    #  ・ECHONET-Lite_Ver.1.12_02.pdf (以下 EL)
    #  ・Appendix_H.pdf (以下 AppH)
    #  for EPC = 0xE7  (瞬時電力計測値)
    echonetLiteFrame = b""
    echonetLiteFrame += b'\x10\x81'      # EHD (参考:EL p.3-2) : 2byte目は \x81(電文形式1) or \x82(電文形式2)
    echonetLiteFrame += b'\x00\x01'      # TID (参考:EL p.3-3) : TID= Transaction ID
    # ここから EDATA
    echonetLiteFrame += b'\x05\xFF\x01'  # SEOJ (参考:EL p.3-3 AppH p.3-408~) : コントローラクラス規定
    echonetLiteFrame += b'\x02\x88\x01'  # DEOJ (参考:EL p.3-3 AppH p.3-274~) : 低圧スマート電力量メータクラス規定
    echonetLiteFrame += b'\x62'          # ESV(62:プロパティ値読み出し要求) (参考:EL p.3-5)  : Get
    echonetLiteFrame += b'\x01'          # OPC(1個)(参考:EL p.3-7)
#    echonetLiteFrame += "\x02"          # OPC(2個)(参考:EL p.3-7)   : 2つのデータを取得する。
#    echonetLiteFrame += "\xE7"          # EPC(参考:EL p.3-7 AppH p.3-275) : 瞬時電力計測値
#    echonetLiteFrame += "\x00"          # PDC(参考:EL p.3-9)
    echonetLiteFrame += b'\xE0'          # EPC(参考:EL p.3-7 AppH p.3-275) : 積算電力量計測値
    echonetLiteFrame += b'\x00'          # PDC(参考:EL p.3-9)
    # コマンド送信
    com1 = "SKSENDTO 1 {0} 0E1A 1 {1:04X} ".format(ipv6Addr, len(echonetLiteFrame))
    command = com1.encode() + echonetLiteFrame
    ser.write(command)
#    print(ser.readline(), end="") # エコーバック
    ser.readline()
#    print(ser.readline(), end="") # EVENT 21 が来るはず(チェック無し)
    ser.readline()
#    print(ser.readline(), end="") # OKが来るはず(チェック無し)
    ser.readline()
    line = ser.readline()         # ERXUDPが来るはず
#    print(line, end="")
    # 取りこぼしたりして変なデータを拾うことがあるので
    # チェックを厳しめにしてます。
    if line.startswith(b"ERXUDP") :
        line = line.decode()
        cols = line.strip().split(' ')
        res = cols[8]   # UDP受信データ部分
        #tid = res[4:4+4];
        seoj = res[8:8+6]
        #deoj = res[14,14+6]
        ESV = res[20:20+2]
        #OPC = res[22,22+2]
        if seoj == "028801" and ESV == "72" :
            # スマートメーター(028801)から来た応答(72)なら
            EPC = res[24:24+2]
            if EPC == "E0" :
                # 内容が瞬時電流力計測値(E8)だったら
                hexPower = line[-8:]    # 最後の4バイト(16進数で8文字)が瞬時電力計測値
#                intPower = int(hexPower, 16)
#                floatPower = round ( float( int( hexPower, 16) * 0.1 ),1)
#               cacti の Data Source で COUNTER を使いたい、となるとデータは intでなくてはいけなくて、floatはだめ。
#               なので、係数を使わずにそのままにする。
                intPower =  int( hexPower, 16)
                break
##################################################################################################
#
#   for EPC = 0xE3 (積算電力量計測値(逆方向計測値))
#
while True:
    #  for EPC = 0xE3
    echonetLiteFrame = b""
    echonetLiteFrame += b'\x10\x81'      # EHD (参考:EL p.3-2) : 2byte目は \x81(電文形式1) or \x82(電文形式2)
    echonetLiteFrame += b'\x00\x01'      # TID (参考:EL p.3-3) : TID= Transaction ID
    # ここから EDATA
    echonetLiteFrame += b'\x05\xFF\x01'  # SEOJ (参考:EL p.3-3 AppH p.3-408~) : コントローラクラス規定
    echonetLiteFrame += b'\x02\x88\x01'  # DEOJ (参考:EL p.3-3 AppH p.3-274~) : 低圧スマート電力量メータクラス規定
    echonetLiteFrame += b'\x62'          # ESV(62:プロパティ値読み出し要求) (参考:EL p.3-5)  : Get
    echonetLiteFrame += b'\x01'          # OPC(1個)(参考:EL p.3-7)
    echonetLiteFrame += b'\xE3'          # EPC(参考:EL p.3-7 AppH p.3-275) : 積算電力量計測値
    echonetLiteFrame += b'\x00'          # PDC(参考:EL p.3-9)
    # コマンド送信
    com1 = "SKSENDTO 1 {0} 0E1A 1 {1:04X} ".format(ipv6Addr, len(echonetLiteFrame))
    command = com1.encode() + echonetLiteFrame
    ser.write(command)
    ser.readline()
    ser.readline()
    ser.readline()
    line = ser.readline()         # ERXUDPが来るはず
    # 取りこぼしたりして変なデータを拾うことがあるので
    # チェックを厳しめにしてます。
    if line.startswith(b"ERXUDP") :
        line = line.decode()
        cols = line.strip().split(' ')
        res = cols[8]   # UDP受信データ部分
        #tid = res[4:4+4];
        seoj = res[8:8+6]
        #deoj = res[14,14+6]
        ESV = res[20:20+2]
        #OPC = res[22,22+2]
        if seoj == "028801" and ESV == "72" :
            # スマートメーター(028801)から来た応答(72)なら
            EPC = res[24:24+2]
            if EPC == "E3" :
                # 内容が瞬時電流力計測値(E8)だったら
                hexPower = line[-8:]    # 最後の4バイト(16進数で8文字)が瞬時電力計測値
#                intPower = int(hexPower, 16)
                intPowerM = int(hexPower, 16)
                break
#######################################################################################################
#
print ("E7:{0} E8r:{1} E8t:{2} E0:{3} E3:{4}".format(outputE7,intrCur,inttCur,intPower,intPowerM),end="")
#
#
ser.close()

出力はcactiに食わせる形式にしていて、E7:瞬時電力計測値,E8r:瞬時電流計測値(R相),E8t:瞬時電流計測値(T相),E0:積算電力量計測値(正方向計測値),E3:積算電力量計測値(逆方向計測値)。

raspberrypi:~/smartmeter3# python3 /usr/share/cacti/scripts/smartmeter.py3
E7:686 E8r:7.0 E8t:1.0 E0:86704 E3:10
瞬間電力値、瞬間電流値は5分に1回のピンポイントの計測なので、瞬発的な消費は拾えてないこともある。

瞬間電流値の5分グラフ(T相、R相) cacti_graph_143.png 電子レンジ、電気ポットなどでの瞬間ピークが出ている。

瞬間電力値 cacti_graph_142.png

積算電力量を cactiの counterでグラフ化したもの。 cacti_graph_144.png 5分データだと、最小単位(0.1 KWH)を超えないときがある。もっと細かな数値が取れると良いけど HEMS機器の出力積算電力量単位(EPC 0xE1)が 0.1 kWhなので難しい。

瞬間値については5分に1回ではなくもっと詳細に取るのもありだと思うが、一応HEMSのBルートは送信制限時間の制限(たしか1時間に360秒)がある。たとえばここ。この制限、どういう基準で計測しての360秒なのか詳しくわからない。まさか TCPでの接続セッションの時間とは思えない。UDPだとさらにわかりにくい?。電波系に詳しい人にはご承知おきのことかもしれない。にしても、連続して毎秒で計測値を取るような利用の仕方だとあっさり超えそうな気もする。
今後は、この資料のP.32にもあるように、「通信負担を軽減する目的から、1つのパケットで複数のコマンド処理が可能なマルチゲットコマンド(エコーネットコンソーシアムが定義)などの実装が推奨される。」を行ってみることか? 最近はいろいろな Bルート製品が出ているのも気になる。

スマートメーター HEMS デバイススキャン for python3

以前書いた、HEMS 機器からのデータの取り出し関連で、とても参考にさせてもらった スマートメーターの情報を最安ハードウェアで引っこ抜くをベースにプログラムを書いた。
このサンプルプログラムはpython2 でのプログラムだったので、今後のためにもpython3 用に書き直しておいたほうが良かったので、作成してみた。例示してあるプログラムはデバイスを探索してデータを取得するのだが、定期的にデータを取得して時系列データを作成するのであれば、デバイス探索は毎回繰り返さなくても良い(スマートメーターのサイトローカルのIPv6アドレスで特定できる)ので、(1)デバイスを探索し、アクセスするための諸元データを取得する。(2)データを定期的に取得する。の2つにわけてしまうほうが良さそうで、それを実施してみた。 まずは (1)の python3 に自分なりに書き換えたものを。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import print_function
import sys
import serial
import time
# Bルート認証ID(東京電力パワーグリッドから郵送で送られてくる)
rbid  = "AAAABBBBCCCCDDDD"
# Bルート認証パスワード(東京電力パワーグリッドからメールで送られてくる)
rbpwd = "PASSWORDDDDDDD"
# シリアルポートデバイス名
#serialPortDev = 'COM3'  # Windows の場合
serialPortDev = '/dev/ttyUSB0'  # Linux(ラズパイなど)の場合
#serialPortDev = '/dev/cu.usbserial-A103BTPR'    # Mac の場合
#
ser = serial.Serial(serialPortDev, 115200)
#
wcom = "SKVER\r\n"
ser.write(str.encode(wcom))
print(ser.readline().decode('utf-8'), end="",flush=True) # エコーバック
print(ser.readline().decode('utf-8'), end="",flush=True) # バージョン
#
# Bルート認証パスワード設定
wcom = "SKSETPWD C " + rbpwd + "\r\n"
ser.write(str.encode(wcom))
ser.readline() # エコーバック
# Bルート認証ID設定
wcom = "SKSETRBID " + rbid + "\r\n"
ser.write(str.encode(wcom))
print(ser.readline().decode('utf-8'), end="",flush=True) # エコーバック
ser.readline()
print(ser.readline().decode('utf-8'), end="",flush=True) # OKが来るはず(チェック無し)
ser.readline()
scanDuration = 4;   # スキャン時間。サンプルでは6なんだけど、4でも行けるので。(ダメなら増やして再試行)
scanRes = {} # スキャン結果の入れ物
# スキャンのリトライループ(何か見つかるまで)
#while not scanRes.has_key("Channel") :  python2
while not 'Channel' in scanRes :
        # アクティブスキャン(IE あり)を行う
        # 時間かかります。10秒ぐらい?
        #ser.write(str.encode("SKSCAN 2 FFFFFFFF " + str(scanDuration) + "\r\n"))
        wcom = "SKSCAN 2 FFFFFFFF " + str(scanDuration) + "\r\n"
        ser.write(str.encode(wcom))
        # スキャン1回について、スキャン終了までのループ
        scanEnd = False
        while not scanEnd :
            line = ser.readline()
            print(line.decode(), end="",flush=True)
#
            line = line.decode()
            if line.startswith("EVENT 22") :
#                # スキャン終わったよ(見つかったかどうかは関係なく)
                scanEnd = True
            elif line.startswith("  ") :
                # スキャンして見つかったらスペース2個あけてデータがやってくる
                # 例
                #  Channel:39
                #  Channel Page:09
                #  Pan ID:FFFF
                #  Addr:FFFFFFFFFFFFFFFF
                #  LQI:A7
                #  PairID:FFFFFFFF
                cols = line.strip().split(':')
                scanRes[cols[0]] = cols[1]
        scanDuration+=1
        if 7 < scanDuration and not scanRes.has_key("Channel"):
            # 引数としては14まで指定できるが、7で失敗したらそれ以上は無駄っぽい
            print("スキャンリトライオーバー")
            sys.exit()  #### 糸冬了 ####
これを実行すると次のような結果が出てくる。
raspberrypi:~/smartmeter3# ./test-bs_scan.py
SKVER
EVER 1.2.8
SKSETPWD C PASSWORDDDDDDD
SKSETRBID AAAABBBBCCCCDDDD
SKSCAN 2 FFFFFFFF 4
OK
EVENT 20 FE80:0000:0000:0000:1234:5678:9ABC:DEF0
EPANDESC
  Channel:33
  Channel Page:09
  Pan ID:24EF
  Addr:00FFEEDDCCBBAA99
  LQI:32
  PairID:12345678
EVENT 22 FE80:0000:0000:0000:1234:5678:9ABC:DEF0
Addr は64bit、これを IPv6のリンクローカルアドレスに変換するには SKLL64 コマンドを使う、ようだが 単純に、MACアドレス 48bit を、EUI-64で変換する場合の、'FF:FE'の挿入をやめただけのよう。先頭の7bit目を反転させて、後半64bit の IPv6 アドレスにしてしまえば良いみたい。例の 'Addr:00FFEEDDCCBBAA99' なら、IPv6アドレスは 'FE80:0000:0000:0000:02FF:EEDD:CCBB:AA99' になる、と思われる。
これで取得できた Channel, Channel Page, Pan ID,PairID, 生成した IPv6アドレスを利用して定期的なデータ取得 scriptを動かす。

スマートメーター HEMS デバイススキャン for python3

以前書いた、HEMS 機器からのデータの取り出し関連で、とても参考にさせてもらった スマートメーターの情報を最安ハードウェアで引っこ抜くをベースにプログラムを書いた。
このサンプルプログラムはpython2 でのプログラムだったので、今後のためにもpython3 用に書き直しておいたほうが良かったので、作成してみた。例示してあるプログラムはデバイスを探索してデータを取得するのだが、定期的にデータを取得して時系列データを作成するのであれば、デバイス探索は毎回繰り返さなくても良い(スマートメーターのサイトローカルのIPv6アドレスで特定できる)ので、(1)デバイスを探索し、アクセスするための諸元データを取得する。(2)データを定期的に取得する。の2つにわけてしまうほうが良さそうで、それを実施してみた。 まずは (1)の python3 に自分なりに書き換えたものを。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import print_function
import sys
import serial
import time
# Bルート認証ID(東京電力パワーグリッドから郵送で送られてくる)
rbid  = "AAAABBBBCCCCDDDD"
# Bルート認証パスワード(東京電力パワーグリッドからメールで送られてくる)
rbpwd = "PASSWORDDDDDDD"
# シリアルポートデバイス名
#serialPortDev = 'COM3'  # Windows の場合
serialPortDev = '/dev/ttyUSB0'  # Linux(ラズパイなど)の場合
#serialPortDev = '/dev/cu.usbserial-A103BTPR'    # Mac の場合
#
ser = serial.Serial(serialPortDev, 115200)
#
wcom = "SKVER\r\n"
ser.write(str.encode(wcom))
print(ser.readline().decode('utf-8'), end="",flush=True) # エコーバック
print(ser.readline().decode('utf-8'), end="",flush=True) # バージョン
#
# Bルート認証パスワード設定
wcom = "SKSETPWD C " + rbpwd + "\r\n"
ser.write(str.encode(wcom))
ser.readline() # エコーバック
# Bルート認証ID設定
wcom = "SKSETRBID " + rbid + "\r\n"
ser.write(str.encode(wcom))
print(ser.readline().decode('utf-8'), end="",flush=True) # エコーバック
ser.readline()
print(ser.readline().decode('utf-8'), end="",flush=True) # OKが来るはず(チェック無し)
ser.readline()
scanDuration = 4;   # スキャン時間。サンプルでは6なんだけど、4でも行けるので。(ダメなら増やして再試行)
scanRes = {} # スキャン結果の入れ物
# スキャンのリトライループ(何か見つかるまで)
#while not scanRes.has_key("Channel") :  python2
while not 'Channel' in scanRes :
        # アクティブスキャン(IE あり)を行う
        # 時間かかります。10秒ぐらい?
        #ser.write(str.encode("SKSCAN 2 FFFFFFFF " + str(scanDuration) + "\r\n"))
        wcom = "SKSCAN 2 FFFFFFFF " + str(scanDuration) + "\r\n"
        ser.write(str.encode(wcom))
        # スキャン1回について、スキャン終了までのループ
        scanEnd = False
        while not scanEnd :
            line = ser.readline()
            print(line.decode(), end="",flush=True)
#
            line = line.decode()
            if line.startswith("EVENT 22") :
#                # スキャン終わったよ(見つかったかどうかは関係なく)
                scanEnd = True
            elif line.startswith("  ") :
                # スキャンして見つかったらスペース2個あけてデータがやってくる
                # 例
                #  Channel:39
                #  Channel Page:09
                #  Pan ID:FFFF
                #  Addr:FFFFFFFFFFFFFFFF
                #  LQI:A7
                #  PairID:FFFFFFFF
                cols = line.strip().split(':')
                scanRes[cols[0]] = cols[1]
        scanDuration+=1
        if 7 < scanDuration and not scanRes.has_key("Channel"):
            # 引数としては14まで指定できるが、7で失敗したらそれ以上は無駄っぽい
            print("スキャンリトライオーバー")
            sys.exit()  #### 糸冬了 ####
これを実行すると次のような結果が出てくる。
raspberrypi:~/smartmeter3# ./test-bs_scan.py
SKVER
EVER 1.2.8
SKSETPWD C PASSWORDDDDDDD
SKSETRBID AAAABBBBCCCCDDDD
SKSCAN 2 FFFFFFFF 4
OK
EVENT 20 FE80:0000:0000:0000:1234:5678:9ABC:DEF0
EPANDESC
  Channel:33
  Channel Page:09
  Pan ID:24EF
  Addr:00FFEEDDCCBBAA99
  LQI:32
  PairID:12345678
EVENT 22 FE80:0000:0000:0000:1234:5678:9ABC:DEF0
Addr は64bit、これを IPv6のリンクローカルアドレスに変換するには SKLL64 コマンドを使う、ようだが 単純に、MACアドレス 48bit を、EUI-64で変換する場合の、'FF:FE'の挿入をやめただけのよう。先頭の7bit目を反転させて、後半64bit の IPv6 アドレスにしてしまえば良いみたい。例の 'Addr:00FFEEDDCCBBAA99' なら、IPv6アドレスは 'FE80:0000:0000:0000:02FF:EEDD:CCBB:AA99' になる、と思われる。
これで取得できた Channel, Channel Page, Pan ID,PairID, 生成した IPv6アドレスを利用して定期的なデータ取得 scriptを動かす。