こんにちは。前回の記事 では、実家の太陽光発電 システムを活用した、電力使用状況の可視化について、全体の概要とデータの考察をご紹介しました。
後編となる今回の記事では、実装を中心に解説します。
実装環境・バージョン
実装環境は次の通りです。
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