おうち電力の Observability: parser combinator をガリガリ書いてスマートメーターとおしゃべりする

この記事は、 NTT Communications Advent Calendar 2023 6日目の記事です。

こんにちは。 SDPF クラウド・仮想サーバーチームの杉浦 (@Kumassy_) です。 普段は OpenStack の開発・運用をしており、最近は Observability まわりを取り組んでいます。

この記事では、以前私が Tech-Night という社内 LT 会で発表した以下のプロジェクトのご紹介します。

Tech-Night については以下の記事をご覧ください。

きっかけ

今年は不安定な世界情勢と円安、猛暑により電気代を気にする機会が多かったのではないでしょうか。 私もあるとき 7-9 月の電気代を確認したところ、電力使用量が 330 kWh、電気代が 10,000 円を超えていました。これは私のチームの 4 人家族のご家庭と比べても多い値でした。

なぜ私の家では電気代がかかってしまうのか? 私のチームはリモートワークが中心なため、働きやすいように冷房をつけっぱなしにしていました。エアコンが原因でしょうか。 それとも 24 時間ゲーミング PC を起動して Cookie Clicker を動かしているからでしょうか?今使っているゲーミング PC は Aura Sync 対応パーツで揃えて自作したものです。 ライティングが美しいので、消費電力は実質ゼロであり、電気代とは無関係なはずです。

自宅の消費電力を測定する

電気代をケチる前に、一体何が電力を消費しているのか測定したいところです。

はじめに検討した方法はワットモニターを使うことです。 瞬間的な消費電力を測定するのには向いていそうですが、 1 日の消費電力を時系列で確認するのはつらそうです。他にもっと安い製品もありそうですが、少し価格も高めです。

次にスマートプラグも検討しました。 スマートプラグは本来コンセントそのものを IoT 化するためのデバイスだと思いますが、消費電力を測定できる機能をもつ製品もあります。 これなら消費電力をグラフとして確認できるのでよさそうです。 ただ、エアコンや冷蔵庫等 1 つずつスマートプラグをつけようとすると高くなってしまいそうです。 また、風呂場の換気扇等、スマートプラグをつけられなさそうな電化製品の電力は測れなさそうです。

SwitchBot プラグミニ(JP)

電力会社はどのように電力使用量を測定しているか

昔は検針員1が各住宅を巡回し、電力使用量を確認していました。

今では、住宅にはスマートメーターという通信機能つきの電力計が設置されており、電力使用量が自動的に収集されています。

都市部ではスマートメーター同士が P2P で通信し、電力会社の端末までデータを転送しているそうです。面白いですね。 電力会社が使う通信路を A ルートと呼ぶそうです。

スマートメーターには通信機能が備わっていることがわかりました。 実は B ルートという方式を使えば、一般人でもスマートメーターから情報を取り出すことができます。

Wi-SUN モジュールを使ってスマートメーターとおしゃべりする

スマートメーターとおしゃべりするには、 Wi-SUN という無線規格を使います。 Wi-SUN には低電力で長距離伝送できることとメッシュネットワークを構成できることが特徴とのこと。 Wi−SUN に対応した専用のモジュールはいくつかありますが、家に転がっていた Raspberry Pi を有効活用したかったので BP35A1 というモジュールを購入しました。 BP35A1 の他にも USB タイプの Wi-SUN モジュールもあるようなので、そちらのほうがお手軽かもしれません。

B ルートは暗号化されているため、 ID とパスワードを入手する必要があります。 東京電力管内であれば以下のサイトから ID とパスワードを確認できます。 ちなみに、 ID は郵便で送られてきます。

シリアル通信周りの設定をして、 Wi−SUN モジュールと Raspberry Pi を接続します。 メス-メスのジャンパ線が家になかったのでブレッドボードを介して繋げておきました。

スマートメーターにパケットを送るには、

  • B ルート ID とパスワードを設定
  • ネットワークをスキャン
  • PANA 認証
  • ECHONET Lite 規格のパケットを送信

という手順を踏みます。

まずは B ルート ID を設定するため、

SKSETRBID <B ルート ID><CRLF>

といったコマンドを Wi-SUN モジュールに送信します。 すると送信したコマンドのエコーバックと

OK<CRLF>

が返ってきます。 Wi−SUN モジュールからの応答が想定通りかどうかをチェックしたいところです。

瞬間消費電力のリクエストを投げるときはどうでしょうか。このときは SKSENDTO コマンドを使います。 レスポンスとしては

ERXUDP <DATA><CRLF>

が返ってくるので、これもバリデーションしたいです。 <DATA> は ECHONET Lite というプロトコルのバイナリ形式のデータです。 ECHONET Lite は仕様書が公開されており、以下のページから確認できます。

パーサーを書く

さて、 Wi-SUN モジュールからの応答には OK<CRLF> のような ASCII 文字列とバイナリ形式のデータが混じっていることがわかりました。 これをうまくパースしてバリデーションをしたいのですが、どのようなコードを書けばよいでしょうか。 出力が ASCII 文字列であれば <CRLF> で文字列を区切ってしまえば簡単にパースできそうです。 そのような実装もあります2が、<DATA> には <CRLF> に相当する \x13\x10 が含まれることがあり、私の環境ではうまく動きませんでした。 また、実験の結果レスポンスは 10 bytes ずつ返ってきたので、一時的に出力をバッファしておく必要があります。 パース処理はバッファに対して複数回試行されるので、パース処理が失敗したとしてもバッファの中身が変更されないようにする必要があります。

以上のような要件にあうパーサーのフレームワークを探したところ、 nom がよさそうでした。

説明書きには byte 列を食べて (bite) くれるといったことが書かれており、遊び心があってよいですね。 nom はパーサーコンビネータと呼ばれる種類のパーサーのようですが、パーサーコンビネータとはなんでしょうか。

パーサーコンビネータは小さいシンプルなパーサーを組み合わせて所望のパーサーを実現する方式です。 私は大学でコンパイラを作る授業を受けたのですが、そのときは lex と yacc を使って、

  • 正規表現を書いて字句解析する
  • BNF 記法で文法を定義する
  • parser generator を使ってパーサーを生成する

というトップダウン的なアプローチでパーサーを作っていました。 lex, yacc に渡すファイルの書式が独特なことと、文法を定義することが大変でやや苦労しました。 パーサーコンビネータはこれとは対象的に、小さなパーサーを組み合わせるボトムアップ的なアプローチでパーサーを作ります。

試しに

OK<CRLF>

をパースするパーサーを作ってみましょう。 OK という文字列をパースするには、 tag を、 <CRLF> をパースするには crlf を使います。 これらの間には他の文字は入らないので、 tuple を使ってこれらが連続して出てきたときにのみパースが成功するようにします。 簡単なパーサーなのに早くも tagcrlf という 2 つのパーサーを組み合わせてしまいました!

同様に IPv6 アドレスのパーサーを作ってみましょう。 Wi−SUN モジュールでの IPv6 アドレスは 0 を省略せず、次のようなフォーマットで表します。 take_while_m_n は条件式が成立する限り m 以上 n 以下の長さのバイト列を切り取ります。 OK パーサーと同様に tuple を使ってパーサーを組み合わせればよいです。

次に EVENT のパーサーを作ってみましょう。 EVENT のフォーマットは次のようになります。

EVENT <イベント番号> <IPv6アドレス> <パラメータ><CRLF>

<パラメータ> の部分はイベント番号によって存在したりしなかったりします。 このようなときは opt コンビネータを使うことで、パースできなかったときに None を返すようにできます。 map_res はパースした結果を加工するコンビネータです。 21 というバイト列は ASCII コードから \x32\x31 と解釈されてしまうので、代わりに \x21 を得るために from_hex_u8 という自作の関数を適用します。 IPv6 アドレスのパースには先程作成した IPv6 パーサーがそのまま使えますね!

さらに複雑なバイト列も、これまで書いてきたパーサーを組み合わせることでパースできます。 このように自作のパーサーを組み上げることで、パースできる対象が広がっていくのが面白いところです。

さて、ここまでは ASCII 文字列のバイト列を扱ってきましたが、それ以外のバイト列はどのように扱えばよいのでしょうか? tag の代わりに be_u8 などのパーサーが利用できます。 図 3-6 は ECHONET Lite プロトコルのパケットです。 OPC が要求数で、後ろに何個要求が含まれるかを表します。

まずは OPC の値を読み取るために be_u8 を使います。次に要求をパースするのですが、 count というコンビネータが便利です。これは引数に与えたパーサーを指定の回数適用し、結果を Vec にまとめて返してくれるコンビネータです。 各要求のパーサーを parse_edata_property として作成しておいたので、あとは count と組み合わせるだけですね。

同じ要領で Wi-SUN モジュールの応答をパースできるパーサーを用意しました。 あとはこれらを alt コンビネータに渡せば完成です。 alt は複数のパーサーを受け取り、最初に成功したパーサーを適用した結果を返すコンビネータです。

ということで、できました。 ソースコードは以下のページに置いてあります。 Raspberry Pi の OS 設定や配線、 Grafana Agent の設定方法も書いてあるので、よければ参考にしてみてください。 Grafana Agent は Exporter を Scrape して外部にメトリクスを送信してくれるエージェントです。 私の環境では、 Granafa Cloud に向けてメトリクスを送信するように Grafana Agent を設定してみました。

一日の消費電力を時系列で測定してみた結果

Grafana Cloud で作成したダッシュボードはこのような見た目になります。 この日は 10:30 頃にゲーミング PC の電源を入れたようです。他の家電製品は触っていないので、おそらくゲーミング PC のアイドル時の消費電力は 250 W 程度なのでしょう。 13:30 ごろに電子レンジを使って昼食を温めていたようです。 電子レンジの出力は 700 W のはずですが、ダッシュボードをみるに 1400 W 近く消費しているようです。 本当に 1400 W も消費しているのか疑わしいのでワットメーターを購入して検証してみたいところです。

消費電力がスパイクしている 21:00 ごろは、おそらく夕食を温めていたのでしょう。 夜間は重めの 3D ゲームで遊んでおり、 450 W ほど消費電力が増えています。 アイドル時の消費電力は 250W ほどだったので、このときのゲーミング PC は 700 W 消費している計算になるのです。

さて、ゲーミング PC のアイドル時の消費電力を 200 W として 24 時間稼働させたときの月当たりの消費電力は以下のようになります。 ここに、クッキー工場の経営者として不都合な真実が浮かび上がります。 ゲーミング PC を 24 時間くらいつけっぱなしにすると、月 144 kWh くらい消費しており、月間の消費電力の半分近くを占める計算です。

対策として、 Intel N100 チップを搭載したミニ PC を購入し、お財布及び環境に配慮した形でクッキーを焼くようにしました。

まとめ

今回の発表のまとめです。 今では自宅に設置されている電力計はスマートメーターという通信機能がついたものに置き換わっています。 B ルートという仕組みを使うことで、一般人でもスマートメーターから瞬間消費電力などの情報を取り出すことができます。 Wi−SUN モジュールとおしゃべりしたいときなど、なにかをパースしなければならないときはパーサーコンビネータのフレームワークを使ってみるものいいでしょう。 パーサーコンビネータは、小さいパーサーを組み合わせることで目的のテキストやバイナリ列をパースできるようにするボトムアップ的なアプローチをとるものでした。 Rust では nom が有名なので、検討してみるとよいでしょう。 最後に、クッキーを焼くときは電気代に注意し、 CPS (Cookie per Second) だけではなく CPW (Cookie per Watt) にも気を配るようにしましょう。

参考資料

© NTT Communications Corporation All Rights Reserved.