スマートメータから電力を取得してHomeAssistantと連携する

モチベーション

OSSと手頃なハードウェアを使ってHEMS(っぽいもの)を作りたい。

実現方法

  • M5StickCにWi-SUNアダプタを載せて、スマートメーターから定期的にデータを取得する
  • 取得したデータをMQTT BrokerにPublishする
  • Home AssistantでMQTT BrokerをSubscribeしてHome Energy Managementに統合する

必要なもの

ハードウェア

M5StickC Plus

M5StickC/Plus用Wi-SUN HATキット

完成品はこんな感じです。 Wi-SUNは障害物に強く通信距離が長いので、設置場所はかなり自由度が高いです。 家のどこに置いてもつながるんじゃないでしょうか。

ソフトウェア

諸々

  • Bルートの開通手続き

開発環境

できたもの

導入方法

Home Assistant / Mosquitto (MQTT Broker)

導入自体は既にたくさん情報があるので割愛します…

configuration.yaml

M5StickCはスマートメーターから取得したデータをMQTT Publishしますので、Home Assistant側にSubscribeの設定をしておきます。 設定はREADMEの通りです。 重要なのはstate_topic、device_class、unit_of_measurement、state_classで、それ以外は多分適当に変えても大丈夫です。

ESP32 B Route to MQTT Smart Meter

  1. esp32-broute2mqtt-smartmeterをチェックアウトする
  2. _SmartMeterConfig.hSmartMeterConfig.hにリネームして自分の環境に合わせて設定する
  3. PlatformIOでビルドして書き込み起動する
  4. 設定があっていればシリアルに取得値を出力する
[ 87618][I][main.cpp:101] setup(): ConvertCumulativeEnergyUnit : 0.100000
[ 87619][I][main.cpp:102] setup(): SyntheticTransformationRatio: 1
[ 91834][I][main.cpp:119] loop(): InstantaneousPower       : 767 W
[ 91834][I][main.cpp:120] loop(): CumulativeEnergyPositive : 4757.399902 kWh
[ 91836][I][main.cpp:121] loop(): InstantaneousCurrentR    : 4.000000 A
[ 91842][I][main.cpp:122] loop(): InstantaneousCurrentT    : 5.000000 A

うまくいっていれば、この時点でHome Assistantから取得値が見れるようになります。

うまくいかない場合はデバッグログを有効にするといいかもしれません。

Home Assistant - Home Energy Management

Home Assistantインスタンス/config/energyからHome Energy Managementを設定します。

グリッド消費の設定にCumulative Energy Positive(積算電力量計測値(正方向))を追加します。 1kWhあたりの使用料金を入れておくと、コストも追跡できるようになります。

基本的な設定はこれで完了です。

できること

リアルタイムなデータ取得

瞬時電力計測値、瞬時電流計測値(T相)、瞬時電流計測値(R相)、積算電力量計測値(正方向)がそこそこリアルタイムで見れます。 スマートメータとの通信周期は5秒にしていますが、データ取得に2,3秒程度かかるため、Home Assistant側の処理時間も合わせると10秒ぐらい遅延してます。

この値をトリガにしてオートメーションすることもできます。 電気の使いすぎをスマホに通知するとか、契約A数付近になったらスイッチオフしてブレーカーが落ちるのを防ぐとか。

データ蓄積

取得したデータを個別のグラフで見れます。

消費電力とコストの追跡

エネルギーのビューから消費電力とコストが追跡できます。 在宅の日と出社の日で消費電力が1.5倍ぐらい違う…なんてことが可視化されてしまいます。

OPNsenseのHAProxyで443ポートを使い回す(https,ssh,OpenVPN)

HAProxyについて

HAProxyはTCPおよびHTTP向けの高機能なロードバランサー/プロキシである。 バックエンドへの振り分け条件はパケットレベルで定義できるため、条件を上手に定義できればプロトコルレベルのマルチプレクサとして振る舞うこともできる。

OPNsenseはこのHAProxyをプラグインとしてサポートしている。 そこで、様々な理由で競合しがちな443ポートを複数のプロコトルで使い回せるようにしてみる。

OPNsense設定

前提条件として、WAN、LAN、DMZの3つのゾーンが存在し、OPNsenseは3つのインタフェースでそれぞれと接続しているという環境。

f:id:nullsnet:20210918221755p:plain

System: Settings: Administration

まずはOPNsense自身が443ポートを待ち受けできるようにする。 デフォルトでは管理画面へのアクセスに使用している可能性があるので見直す。 Listen InterfacesをLANだけにし、WAN側はHAProxyで使えるようにしておく。

f:id:nullsnet:20210918220636p:plain

Services: HAProxy: Settings:

Real Servers

バックエンドとなるサーバを登録する。 今回はOpenVPNSSHHTTPSの3つを登録する。

Virtual Services: Backend Pools

OPNsenseのフロントエンドとなるサービスを定義する。 Real Serversで定義したサーバを登録していけばよい。

Virtual Services: Public Services

OPNsenseのフロントエンドとなるサービスを定義する。 今回はWAN側IPの443をリッスンする設定とする。

Rules & Checks: Conditions / Rules

いよいよプロトコルごとの振り分けを定義していく。 基本的には、TCPの3WAYハンドシェイク後の、各プロトコルの最初のパケットに対する条件(Conditions)を定義してやれば、当該コネクションが丸ごと処理対象となる。 更に、ルール(Rules)にて条件に一致したパケットをどのバックエンドに流すか定義することで、パケットの振り分けが可能となる。

SSH

Conditon

調べてみると、SSHのハンドシェイクのペイロードは「SSH-2.0」から始まるため、これのバイナリ表記である「5353482d322e30」を条件にすればよい、という情報が出てくる。 パケットをキャプチャして調べてみると、確かにそのようだ。 SYN -> SYN/ACK -> ACKの直後のパケットのペイロードが「SSH-2.0」で始まっているのが確認できる。

f:id:nullsnet:20210918231744p:plain

この条件をHAProxyに設定する。 OPNsenseのHAProxyはGUIでのACL設定ができないため、設定値のパススルーで直接設定する。 「ペイロードの0byteから7byte分がバイナリで5353482d322e30であること」としてreq.payload(0,7) -m bin 5353482d322e30を設定する。

f:id:nullsnet:20210918232114p:plain

Rule

あとは、作成したConditionsに一致するパケットをsshのバックエンドに流す、というRuleを定義すればよい。

f:id:nullsnet:20210918233946p:plain

HTTPS(TLS)

Conditon

HTTPS、というかTLSの場合は簡単で、「Client Hello」を送信してきたもの、という条件でよい。

f:id:nullsnet:20210918232547p:plain

こちらもパススルーでreq.ssl_hello_type 1を設定する。

f:id:nullsnet:20210918232617p:plain

他にも、SNIに含まれるドメイン名を指定する方法があるようだ。

Rule

こちらもSSHと同様にRuleを定義すればよい。

加えてもう一つ、先程のConditonとは別に、tcp-request content acceptのルールを作成する。 TLSを扱う場合は必要なルールのようだ。

f:id:nullsnet:20210919000234p:plain

Enhanced SSL Load Balancing with Server Name Indication (SNI) TLS Extension - HAProxy Technologies

OpenVPN

Condition

OpenVPNに関してはあまり情報がなく、default_backendをOpenVPNとすることで「HTTPSでもSSHでもなければOpenVPNへ流す」という設定例しか見つからなかった。 これは実際やってみると、443ポートへのHTTPSでもSSHでもないアクセス(おそらくbotによる攻撃の類と思われる)が全てOpenVPNに集中してしまう。 これは非常に気持ち悪いのでなんとかしたい。

sslhやOpenVPNソースコードを読んだがよくわからなかったので、力技でなんとかしてみることにした。 OpenVPNへのコネクション時のパケットを複数回キャプチャし、共通点を見つけて条件として定義する、という手法でやってみる。 この方法はOpenVPNの設定値にも依存するだろうし、他の環境でうまくいくかはわからない。

1回目

0040         00 2a 38 be 95 8e 13 12 53 5f 66 e1 aa 4a   ...*8.....S_f..J
0050   16 16 a7 eb a6 3b 27 5d 08 cc 81 88 ee cc 2c 63   .....;']......,c
0060   b4 00 00 00 01 61 45 c3 f4 00 00 00 00 00         .....aE.......

2回目

0040         00 2a 38 44 29 20 dc 73 e6 dc 19 8d 0d 2b   .J.*8D) .s.....+
0050   8b cc 75 11 47 cb aa de 00 67 e4 bc 3b 62 73 b9   ..u.G....g..;bs.
0060   dd 00 00 00 01 61 46 01 c1 00 00 00 00 00         .....aF.......

どうやらペイロード[0]~[2][35]~[38]が共通しているようだ。 ここから条件を作成する。 どの条件に従うかはルール側で定義するため、条件は2つに分けて定義する必要がある。 今回はそれぞれopenvpn_1、openvpn_2という名前で定義した。

  • 条件1(openvpn_1)
    • req.payload(0,3) -m bin 002a38
  • 条件2(openvpn_2)
    • req.payload(31,4) -m bin 00000001

この条件で本当に正しいかはわからないので、様子見して調整することにする。

Rule

Rule側ではopenvpn_1とopenvpn_2の両方が成立する場合としたいため、ANDを設定する。 これでOpenVPNのパケットが振り分けられる。

f:id:nullsnet:20210918234744p:plain

Virtual Services: Public Services

最後にOPNsenseがリッスンするサービスを定義する。 Listen AddressesにWAN側のIPアドレス:443を設定し、Select Rulesに定義したルールを設定すればよい。

また、Option pass-throughにはtcp-request inspect-delay 5sを設定する必要があるようだ。 Introduction to HAProxy Stick Tablesにそう書いてあったし、確かに書かないとうまく動かなかった。

You only need to use this in a frontend or backend when you have an ACL on a statement that would be processed in an earlier phase than HAProxy would normally have the information. For example, tcp-request content reject if { path_beg /foo } needs a tcp-request inspect-delay because HAProxy won’t wait in the TCP phase for the HTTP URL path data. In contrast http-request deny if { path_beg /foo } doesn’t need an tcp-request inspect-delay line because HAProxy won’t process http-request rules until it has an HTTP request.

f:id:nullsnet:20210918235359p:plain

動作確認

HAProxyのLog Fileから確認できる。 ちゃんと443で3つのプロトコルが振り分けられているようだ。

2021-09-19T00:08:36 haproxy[16827] XXX.XXX.XXX.XXX:11395 [19/Sep/2021:00:08:36.881] outbound_443 tls/tls 8/0/44 5804 -- 2/2/1/1/0 0/0
2021-09-19T00:09:40 haproxy[16827] XXX.XXX.XXX.XXX:5243 [19/Sep/2021:00:09:01.365] outbound_443 ssh/ssh 1/0/39350 2953 cD 2/2/0/0/0 0/0
2021-09-19T00:17:08 haproxy[56327] XXX.XXX.XXX.XXX:23787 [19/Sep/2021:00:17:01.754] outbound_443 openvpn/openvpn 1/0/6979 14882 -- 2/2/0/0/0 0/0    

コンテナでGPU acceralatedなWebGLを動かしてブラウザゲームを遊ぶ

モチベーション

とあるブラウザゲームをプレイしています。 このゲームは、1日の回数制限があるが報酬がおいしいコンテンツ、いわゆる日課がたくさんあります。 毎日やるのは正直面倒です。

なので、基本的に「フルオート」機能で消化します。 これはスキル発動や攻撃行動を自動で行う機能で、オンにすれば自動で敵を倒してくれるので便利です。 ただし、操作を自動化するだけで戦闘自体はリアルタイムで進行するため、画面はアクティブにしておく必要があります。 スマホで「フルオートだけオンにして放置する、別の作業をする」といったことは出来ないので、少々面倒です。

そこで、簡単にフルオート放置ができる環境を作ってみます。

  • PC/スマホから操作できること
  • ブラウザだけで操作できること
  • 利用規約に則ること

実現方法

  1. DockerとNVIDIA Container ToolkitでX11OpenGL(WebGL)が動くコンテナを作る
  2. GoogleChromeを動作させる
  3. noVNCでブラウザから操作可能にする

少し調べてみると「ホスト側のtmp/.X11-unix$DISPLAYをコンテナへ共有し、描画はホスト側で行う」という手法が多く散見されます。 ホストを汚したくない&ヘッドレスにしたいので、コンテナ側に全て閉じ込める方式にします。 具体的には、X.Org Serverもコンテナ内で実行します。

諸元

とても古い機材ですが、とりあえずNVIDIA Container Toolkitに対応してさえいればなんでもいいはずです。

CPU Core i5-3230M
Host OS Ubuntu 20.04
Container base Ubuntu 20.04
Docker version 20.10.8
GPU NVIDIA GeForce 650M
GPU driver version 470.57.02
CUDA version 11.4

構築

ホスト

NVIDIA関連のドライバやミドルウェアをインストールします。 以前は依存関係が複雑で面倒だったような気がしますが、現在は非常に簡単になったようです。

ドライバ

リポジトリを追加してcuda-driversをインストールすれば最新版が入ります。 ここの通りやるだけです。

ミドルウェア

こちらもリポジトリを追加してnvidia-docker2だけインストールすれば、container-toolkitなども同時にインストールされます。 ここの通りやるだけです。

nvidia-smiコマンドが実行できればOK。 次の作業のために、実際にインストールされたドライバのバージョンを確認しておきます。

$ nvidia-smi
Sun Aug 15 06:02:11 2021
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 470.57.02    Driver Version: 470.57.02    CUDA Version: 11.4     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  NVIDIA GeForce ...  On   | 00000000:01:00.0 N/A |                  N/A |
| N/A   56C    P8    N/A /  N/A |     79MiB /  2000MiB |     N/A      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+

コンテナ

NVIDIAがコンテナをいくつか公開していますが、これらにはX11に必要なファイルを含んでいないようです。 そもそも現状ではX11をサポートしていません。

Display (e.g. X11, Wayland) is not officially supported.

なので自分で環境構築する必要があります。コードはこちら。

ホストとコンテナのドライバのバージョンを合わせる必要があるので、ビルド時の引数で指定します。

docker-compose build --build-arg DRIVER_VERSION=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader)
docker-compose up -d

:8081/vnc.htmlにアクセスすればnoVNCの画面が出ます。 自動的にOpenboxとターミナルが起動するようにしてあります。

f:id:nullsnet:20210815184433p:plain

ここからChromeを起動する場合、rootなのでgoogle-chrome --no-sandboxとする必要があります。

できたもの

コンテナ上で動作するGPU acceralatedなWebGLの環境ができました。 ブラウザを閉じても問題なくフルオートできています。 速度も問題なさそうです。 HTML5に対応してさえいれば、どんな端末からでも操作可能です。 これでフルオート放置が捗ります。

ちゃんとした(?)用途としては

とかがあるでしょうか。あんまりなさそうですが。

ドライバアップデート時

ホストOSのドライバをアップデートをしたらコンテナがうまく起動しなくなった。 Xorgの起動に失敗しているようだ。

/usr/bin/Xorg :0

X.Org X Server 1.20.13
X Protocol Version 11, Revision 0
Build Operating System: linux Ubuntu
Current Operating System: Linux 7b222ef168e6 5.4.0-104-generic #118-Ubuntu SMP Wed Mar 2 19:02:41 UTC 2022 x86_64
Kernel command line: BOOT_IMAGE=/vmlinuz-5.4.0-104-generic root=/dev/mapper/ubuntu--vg-ubuntu--lv ro maybe-ubiquity
Build Date: 14 December 2021  02:14:13PM
xorg-server 2:1.20.13-1ubuntu1~20.04.2 (For technical support please see http://www.ubuntu.com/support) 
Current version of pixman: 0.38.4
        Before reporting problems, check http://wiki.x.org
        to make sure that you have the latest version.
Markers: (--) probed, (**) from config file, (==) default setting,
        (++) from command line, (!!) notice, (II) informational,
        (WW) warning, (EE) error, (NI) not implemented, (??) unknown.
(==) Log file: "/var/log/Xorg.0.log", Time: Sun Mar 13 17:09:39 2022
(==) Using config file: "/etc/X11/xorg.conf"
(==) Using system config directory "/usr/share/X11/xorg.conf.d"
(EE) 
Fatal server error:
(EE) Cannot run in framebuffer mode. Please specify busIDs        for all framebuffer devices
(EE) 
(EE) 
Please consult the The X.Org Foundation support 
         at http://wiki.x.org
 for help. 
(EE) Please also check the log file at "/var/log/Xorg.0.log" for additional information.
(EE) 
(EE) Server terminated with error (1). Closing log file.

dmesgを見るとこんなログが出ている。

NVRM: API mismatch: the client has the version 470.57.02, but
NVRM: this kernel module has the version 470.103.01.  Please
NVRM: make sure that this kernel module

ホスト側とコンテナ側でドライバのバージョンがずれてるだけなので、コンテナをビルドしなおせばOK。