$name

日常の記録から技術メモまで

太陽光発電システムを活用しておうちの電力をGrafanaで可視化(後編)

こんにちは。前回の記事では、実家の太陽光発電システムを活用した、電力使用状況の可視化について、全体の概要とデータの考察をご紹介しました。

後編となる今回の記事では、実装を中心に解説します。

実装環境・バージョン

実装環境は次の通りです。

  • Debian 12.8
  • Docker 27.3.1
  • Docker Compose 2.29.7
  • node:lts-bookworm コンテナイメージ(Node.js 22.14.0)
  • influxdb:2 コンテナイメージ(InfluxDB 2.7.11)
  • grafana コンテナイメージ(Grafana 11.6.0)

データ収集ツール「heliostat」

太陽光発電の計測システムにGETリクエストを送り、JSONデータを取得し、InfluxDBにデータを書き込む自作ツールです。

開発環境はnvm, npmを用いて構築し、ある程度形ができあがってからDockerコンテナとして動作するようにしました。環境構築の解説はこの記事のメインテーマではないので割愛します。

予備実験

実験として、太陽光発電の計測システムにGETリクエストを送り、レスポンスが得られること、さらにJSONとして値を取得できることを確認します。

Node.js公式の学習ページにあるコーディングサンプルを参考に、Undiciを用いてGET処理を行います。

以下に、コンソールに電圧・電流・電力を出力するプログラムを示します。

電圧などの定数名は前編の「設計」の項を参照してください。また、${solarUrl}は計測ユニットのホスト名またはIPアドレスに置き換えてください。

async function main() {

    setInterval(fetchData, 1000);

    // 1秒ごとに fetchData() を実行

}
 

async function fetchData() {

    // GETリクエストを行い、レスポンスをJSONとして読み込み

    let response = await fetch(`${solarUrl}/asyncquery.cgi?type=SysInfo`);

    const sysInfo = await response.json();

    response = await fetch(`${solarUrl}/asyncquery.cgi?type=CurCtDir`);

    const curCtDir = await response.json();

    response = await fetch(`${solarUrl}/asyncquery.cgi?type=PcsOne&pcsNumber=0`);

    const pcsOne = await response.json();

 

    // JSONから値を取得

    const pu = sysInfo.mainSensorData.costValueU;

    const vu = sysInfo.mainSensorData.voltageU / 10;

    const iu = sysInfo.mainSensorData.currentU / 10;

    const pw = sysInfo.mainSensorData.costValueW;

    const vw = sysInfo.mainSensorData.voltageW / 10;

    const iw = sysInfo.mainSensorData.currentW / 10;

    const ptrade = curCtDir.instantPower.tradedPower;

    const pconsume = curCtDir.instantPower.consumedPower;

    const pgen = pcsOne.onePcsInfoData.power;

    const vdc = pcsOne.onePcsInfoData.directVoltageArray[0] / 10;

    const idc = pcsOne.onePcsInfoData.directCurrentArray[0] / 10;
 

    // 取得した値をコンソールに出力

    console.log("Phase U: " + vu + "V " + iu + "A " + pu + "W");

    console.log("Phase W: " + vw + "V " + iw + "A " + pw + "W");

    console.log("Traded: " + ptrade + "W");

    console.log("Power Consumption: " + pconsume + "W");

    console.log("Generated Power: " + pgen + "W");

    console.log("DC Bus: " + vdc + "V " + idc + "A\n");

}

実行例は次のようになります*1

Phase U: 103.5V 23.5A 2189W

Phase W: 99.8V 4.2A 335W

Traded: 2524W

Power Consumption: 2524W

Generated Power: 0W

DC Bus: 0V 0A

InfluxDBの利用

予備実験を通して、太陽光発電の計測システムから電圧・電流・電力の各値を取得できることがわかりました。先ほどのコードを元に、取得したデータをInfluxDBに格納するコードを作成します。

まず、InfluxDB OSS v2 Documentationを参考に、@influxdata/influxdb-clientと@influxdata/influxdb-client-apisをnpmコマンドでインストールします。公式ドキュメンテーションにはTypeScriptをインストールするよう指示がありますが、今回はJavaScriptで開発するのでTypeScriptをインストールする必要はありません。

続いて、コードを書いていきます。

Import文

configをインポートしているのは、計測ユニットのIPアドレスやデータの取得間隔、InfluxDB関連の情報など、環境依存の情報をconfig.jsonファイルにすべてまとめるためです。これらを別のファイルに分離すると、環境が変わってもコードを書き換える必要はなくなります。

import { InfluxDB, Point } from "@influxdata/influxdb-client";
import config from '../config.json' with { type: 'json' };

main関数

configファイルのfetchIntervalSecondsキーで指定した秒数おきに、計測ユニットにGETリクエストを投げます。

async function main() {
    setInterval(fetchData, config.fetchIntervalSeconds * 1000);
}

fetchData関数

計測ユニットにGETリクエストを投げ、データの取得ができたら、write関数を呼びます。
データの取得に失敗してもプログラムが落ちないように、try-catch文によるエラーハンドリングを入れています。データの取得に失敗したら、格納をスキップし、エラーが起きた旨をコンソールに吐き出します。

async function fetchData() {
    try {
        let response = await fetch(`${config.solarUrl}/asyncquery.cgi?type=SysInfo`);
        const sysInfo = await response.json();
        response = await fetch(`${config.solarUrl}/asyncquery.cgi?type=CurCtDir`);
        const curCtDir = await response.json();
        response = await fetch(`${config.solarUrl}/asyncquery.cgi?type=PcsOne&pcsNumber=0`);
        const pcsOne = await response.json();

        await write(sysInfo, curCtDir, pcsOne);
    } catch (error) {
        console.error(new Date() + ": " + error);
    }    
}

write関数

InfluxDBクラス、Pointクラスの基本的な使い方は Write data with the InfluxDB JavaScript client library | InfluxDB OSS v2 Documentation を見ると良いと思います。

今回は、realtimedataというMeasurement名を使用し、タグを付けずに、取得したデータをフィールドに投入しています。

Pointの挿入が完了したらコンソールにログを出します。ただ、本番運用ではエラーログが流れると不便なので、configのsuppressLogがtrueであれば、成功時のログは出しません。

async function write(sysInfo, curCtDir, pcsOne) {
    const influxdb = new InfluxDB({ url: config.influxdb.url, token: config.influxdb.token });
    const writeApi = influxdb.getWriteApi(config.influxdb.org, config.influxdb.bucket);
    const point_realtimedata = new Point('realtimedata')
        .floatField('Vu', sysInfo.mainSensorData.voltageU / 10)
        .floatField('Iu', sysInfo.mainSensorData.currentU / 10)
        .intField('Pu', sysInfo.mainSensorData.costValueU)
        .floatField('Vw', sysInfo.mainSensorData.voltageW / 10)
        .floatField('Iw', sysInfo.mainSensorData.currentW / 10)
        .intField('Pw', sysInfo.mainSensorData.costValueW)
        .intField('Ptrade', curCtDir.instantPower.tradedPower)
        .intField('Pconsume', curCtDir.instantPower.consumedPower)
        .intField('Pgen', pcsOne.onePcsInfoData.power)
        .floatField('Vdc', pcsOne.onePcsInfoData.directVoltageArray[0] / 10)
        .floatField('Idc', pcsOne.onePcsInfoData.directCurrentArray[0] / 10);

    writeApi.writePoint(point_realtimedata);
    if(!config.suppressLog){
        console.log(new Date() + ": Write OK");
    }
}

コードの全体は、helioview/src/index.js at main · HKShuttle/helioview · GitHubを見てください。

うっかり経験したトラブルとしては、データ収集プログラムからInfluxDBにアクセスできない、というものがありました。データ収集プログラムから「localhost」や「127.0.0.1」を指定してInfluxDBに接続しようとしていましたが、これではアクセスできません。「influxdb2」を指定すると接続できました。
Docker Composeはコンテナのサービス名を用いて名前解決ができるので、別コンテナにアクセスしたいときはcompose.ymlで定義したサービス名をホスト名として使うのが良いようです。別のコンテナにアクセスするわけですから、localhostじゃ繋がらないのは当たり前ですね...。

Docker

先ほどのプログラムをコンテナとして動作させるために、まずDockerfileを書きます。このように書きました。
コンテナをビルドすると、npm installまでが実行されます。コンテナを実行すると、ENTRYPOINTに書いたコマンド「npm run test」が実行されます。

FROM node:lts-bookworm

COPY . /helioview

WORKDIR /helioview

RUN npm install

ENTRYPOINT [ "npm", "run", "test"]

上記のコンテナを、InfluxDBやGrafanaと連携して動作させるためのcompose.ymlを書きます。長いので、全体はhelioview/compose.yml at main · HKShuttle/helioview · GitHubを見てください。

データ収集プログラムはInfluxDBに依存して動くので、compose.ymlに以下のようなhealthcheckを入れています。

healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8086/health"]
      interval: 5s
      timeout: 10s
      retries: 10

InfluxDB

InfluxDBでは、RDBと違う用語がいくつか出てきます。

InfluxDBにおいて、PointはRDBにおけるレコードの行に相当するといわれています。同様に、Measurementはテーブル、Bucketはデータベースに相当するといわれています。

また、InfluxDBにおいて、Pointを挿入する前にBucketを作成する必要はありますが、Measurementを作成する必要はありません。Pointを挿入すれば、存在しないMeasurementは自動で作成されますし、同じ名前のMeasurementが存在すればそちらにPointが追加されていきます。

準備としては、まず、ターミナルで $ sudo docker compose up -d influxdb2 を実行し、InfluxDBを立ち上げます。正常に起動すれば、ブラウザでInfluxDBにアクセスできます。アクセスできない場合は、ターミナルで $ sudo docker compose logs -f influxdb2 を実行してログを確認します。

Organizationの作成

Organizationは、Bucketにアクセスするための資格情報になります。WebUIからOrganizationを作成すると、Bucketも自動で作成されます。

左側の青い「O」ボタンをクリックし、Create Organizationをクリックします。すると下の画像のような画面が出るので、Organization Nameに任意の名前、Bucket NameにそのOrganizationで使いたいBucketの名前を入力して、CREATEを押します。

これで、Organizationの作成は完了です。

Organizationの作成

ここまでの準備がうまくいけば、計測ユニットから取得したデータがInfluxDBに格納されるはずです。

InfluxDBにデータが格納されている様子

Grafana

Data sourceの設定

ブラウザでGrafanaにアクセスし、メニューバーの「Connections」から「Data soueces」を選びます。Data sourcesの画面で「Add new data source」をクリックします。Add data sourcesの画面で「InfluxDB」を選択します。ここまでは簡単ですよね。

InfluxDBの追加画面では、「Query language」に「Flux」を選択します。「HTTP>URL」には「http://influxdb2:8086*2と入力します。「InfluxDB Details」の「Organization」「Token」「Default Bucket」にも適切なものを入力します。他は初期値のままで構いません。

「Save & test」をクリックして、設定がうまくいっていることを確認します。

Data sourceの設定

ダッシュボードの作成

メニューバーから「Dashboards」を選択してダッシュボード一覧画面を表示します。右側の「New」ボタンをクリックし、「New dashboard」をクリックします。

空のダッシュボードが表示されるので、画面中央の「+Add visualization」をクリックします。

空のダッシュボード。ここにVisualizationを追加していく

Add visualizationをクリックすると、「Select data source」のモーダルが出ます。先ほど追加したinfluxdbがハイライトされているので、それをクリックします。すると、データベースへのクエリ文を入力する画面が出るはずです。クエリ結果として時系列データが返ってくれば、カラム*3ごとの時系列の値の変化をGrafanaが自動的にグラフ化してくれます。

クエリ文の入力画面

クエリ文のビルド

Data sourceを追加するときに、クエリ言語には「Flux」を指定しました。Fluxによるクエリ文は、InfluxDBのWebUIで簡単にビルドできるので、それを試してみます。

InfluxDBのData Explorer画面で、可視化したいフィールドキーにチェックを付け*4、「SUBMIT」をクリックします。例として、電流値3種類を可視化してみます。左上のプルダウンメニューで「Graph」を選ぶと、値の時系列変化がグラフとして描画されることを確認できます。

可視化したいフィールドキーにチェックを付けてグラフを確認

そのまま、右側の「SCRIPT EDITOR」をクリックすると、Flux言語によるクエリ文が得られます。このクエリ文を、GrafanaのVisualization作成画面に貼り付けて、「Reflesh」ボタンを押せば、Grafanaでグラフが描画されます。

自動でビルドされたクエリ文を取得できる

グラフに単位を付ける

グラフに単位があるとグラフを読みやすいです。Grafanaの活用法の一例として、「グラフに単位を付ける」機能を紹介します。

先ほどの例では、電流値3種類をグラフ化しました。電流の単位は「アンペア(A)」ですので、縦軸をアンペアにしてみます。

右側のメニューの「Standard options」から「Unit」の欄内をクリックします。電気の単位は「Energy」に分類されているので、Energyから「Ampare」をクリックします。これで、グラフの縦軸に単位が付きます。

グラフに単位を付ける

ダッシュボードの保存

作成したパネルには名前を付けておくとよいでしょう。右側メニューの一番上「Panel options」から「Title」を選び、パネルに名前を付けます。

また、グラフの更新間隔や時間軸も保存されるので、最も見やすいと思うものを選択しておきます。

最後に、右上の「Save dashboard」を押し、ダッシュボードに好きな名前を付けて保存します。ここまでくればようやく、一通りの作業が完了です!

1枚のダッシュボードには複数のパネルを表示できますし、Visualizationの種類も折れ線グラフ以外にさまざまなものがあります。ここで紹介しきれない機能も多く、私自身がまだ使いこなせていない機能、見つけきれていない機能も数多くありますが、まさに「習うより慣れよ」で、色んな操作を試してみるのが良さそうです。

感想

太陽光発電の計測システムを色々いじっていて、電圧や電流といった情報を取ってきて可視化できそうだということには1年以上前から気づいていました。作りたいとはずっと思っていたのですが、なかなか時間ができず寝かせていました。実際に作ってみると拍子抜けするほど簡単で、早く作ればよかったなと思いました。

そして今回、Node.jsやInfluxDBなどの技術を実践的に学ぶ機会ができました。InfluxDBのスキーマ設計など、まだよくわかっていない点も多く、今後も触れることがあればさらに勉強を重ねたいです。

技術記事を書いたのもかなり久しぶりで、アウトプットの難しさを痛感しました。単にものを作るだけではなく、誰かに読んでもらえるような文章を残す事も今後続けたいです。

リポジトリの紹介

今回開発したプロジェクトはGitHubで公開しています。同じ太陽光発電システムをお持ちの方はぜひぜひ自由に使ってください。

オムロン製の計測ユニット「KP-MU1P」系列以外には対応していません。

私の実装を流用して、他の太陽光発電システムやHEMSに対応するように改良してもらってもかまいませんし、コードを参考に別のプロジェクトを作ってもらってもかまいません。

なにかあればPRを投げてもらえるとうれしいです。

github.com

*1:力率の関係で、有効電力は電圧と電流の積より小さくなります

*2:compose.ymlで定義したサービス名

*3:InfluxDBの場合はField keyが相当

*4:チェックを付けなければすべてのフィールドキーを選択できます