これはなに?
自作した自家発電機で、発電状態を監視するシステムの根幹部分をどうやって作ったか、の記録。
コントローラがブラウザに返すhtml/jsをリバースして作ったのだけど、念のためコントローラメーカーのエンジニアに問い合わせてOK頂いたので、安心して公開できる。
まずはチャージコントローラ選別
「発電状況を外部から取得できるコントローラ」って意外と少ない。少なくとも個人で手が出る価格帯の範囲では。
外部から取得できないなら、センサー類を自前で揃えて自作するしかないのだけど、家電が使える容量のシステムだと流す電流量が多いので、それに耐えるセンサーとなるとまた難しい。
そこで、このTristar社製チャージコントローラ TS-MPPT-60。
10万円程度のお値段で、Ethernetポートを備えている上HTTP経由でバッテリ電圧、太陽光パネル電圧、充電流量、放電流量、ヒートシンク温度に至るまで、様々な情報を得ることができる、お得な製品。

この情報を収集するための手段としてPythonパッケージ化したライブラリを用意し、データを収集して、最終的にはクラウドデータベースに記録して可視化したい、というわけ。
Tristar社製 TS-MPPT-60 API仕様
TS-MPPT-60から得る情報や、その取得方法の仕様はTriStar-MPPT Modbus specification documentを見れば分かるよと、Tristar社のエンジニアの方が教えてくれた。
けど、結構な文量な上、まさに「仕様書」って感じで読みづらい。
ならば、ブラウザ経由でチャージコントローラにアクセスした際に参照するJavascriptソースを解析した方が、目的達成には近道。
のはず。
発電状況取得方法の検証
ということで、まずは普通にブラウザで表示されるフロントエンドの解析をしてみることに。
ChromeのDevelopper Toolsを使う

ブラウザで取得できる情報は以下のとおり。
TS-MPTT-60が何かしらのAPIを提供しているはずなので、ChromeのDevelopper Toolを使ってhtml/jsを抜き出して眺めてみる。
index.html

Battery項目に着目。
battery voltageの値は、fD0というname属性がついたform要素内にある、input要素のlblcurrentValue属性に対して、誰かが属性値を設定していると思われる。
index.htmlヘッダでloadしているliveview.jsを見ると、ページ読み込み完了イベントでLVInit()をコールしていたので、その辺りを深堀りする。
liveview.js
jsをfD0でgrepしてみると、ScaledValueDisplayClassを発見。
ただ、できる限り転送量を減らすためか、全ての変数がABCDE…等のアルファベット1文字で宣言されており、とてもreadableとは呼べないコード…。
以下に引用するjsコードは全て、引数名をそれなりの意味を込めた名前に修正した。
LVInit()
var rowsToUpdate=new Array(); var UPDATE_FREQ_SECS=5; var Vb=new ScaledValueDisplayClass(MBID,38,""V"",""fD0"",""Battery Voltage"",1); var VbT=new ScaledValueDisplayClass(MBID,51,""V"",""fD1"",""Target Voltage"",1); var IbC=new ScaledValueDisplayClass(MBID,39,""A"",""fD2"",""Charge Current"",1); ...
上記のように各要素のクラスインスタンスを生成し、LVInit()でrowsToUpdate配列に格納し、一定時間毎に全ての要素を更新するようになっている。
function LVInit(){ ShowMenu(); Factors.Init(); rowsToUpdate[rowsToUpdate.length]=Vb; rowsToUpdate[rowsToUpdate.length]=VbT; rowsToUpdate[rowsToUpdate.length]=IbC; ... intervalHandle=setInterval(updateAllLVText,100) }
ScaledValueDisplayClass
先のコードと、index.htmlに記述された要素や属性と合わせて読むと、fD0フォームのlblDataName属性が付いたinput要素に対してはそのままlblNameを、lblcurrentValue属性が付いたinput要素に対してはGetScaledValue()が返す値を格納して表示するようになっている。
function ScaledValueDisplayClass(MBID, MBaddress, ScaleFactor, FormName, LabelName, Register){ this.MBID=MBID; this.MBaddress=MBaddress; this.frmName=FormName; this.lblName=LabelName; this.ScaleFactor=ScaleFactor; this.updateLVText=function(){ try{ document.forms[this.frmName].elements.lblDataName.value = this.lblName.toString(); document.forms[this.frmName].elements.lblcurrentValue.value = GetScaledValue(this.MBID, this.MBaddress, this.ScaleFactor, Register).toString() + "" "" + this.ScaleFactor.toString(); return 1 } catch(G){ return 0 } } }
GetScaledValue()
そのGetScaledValue()が以下。[V]、[A]、[W]、[Ah]、[kWh]、それぞれの単位に応じた計算アルゴリズムが見て取れる。
計算元の生データはMBJSReadModbusInts()で取得できるらしい。
function GetScaledValue(MBID, MBaddress, ScaleFactor, Register){ var rawValue = 0; rawValue = MBJSReadModbusInts(MBP, MBID.toString(), MBaddress.toString(), Register); if(Register > 1){ var values = rawValue.split(""#""); rawValue = (parseInt(values[0]) * 65536) + parseInt(values[1]) } else{ rawValue <>= 16 } if(ScaleFactor.toString() == ""V""){ return((rawValue * Factors.VScale) / 32768 / 10).toFixed(2) } else{ if(ScaleFactor.toString() == ""A""){ return((rawValue*Factors.IScale) / 32768 / 10).toFixed(1) } else{ if(ScaleFactor.toString() == ""W""){ return((rawValue * Factors.IScale * Factors.VScale) / 131072 / 100).toFixed(0) } else{ if(ScaleFactor.toString() == ""Ah""){ return(rawValue * 0.1).toFixed(1) } else{ if(ScaleFactor.toString() == ""kWh""){ return(rawValue).toFixed(0) } else{ return(rawValue).toFixed(2) } } } } } }
utilities.js
MBJSReadModbusInts()
そのMBJSReadModbusInts()が以下。ここで ""#"" で区切った値をreturnするので、GetScaledValue()側で ""#"" でsplitした上でulong値に計算し直す処理があるわけですな。
さらにMBJSReadCSV()に降りてみる。
function MBJSReadModbusInts(MBPVAL, MBIDVAL, MBaddress, Register){ var rawValueGotByCgi = MBJSReadCSV(MBPVAL, MBIDVAL, MBaddress, Register); var valuesGotByCgi = rawValueGotByCgi.split("",""); var idxMax = valuesGotByCgi[2]; var idxValue = 3; var retValueString = """"; var retValueShort; while(idxValue < parseInt(idxMax) + 2){ retValueShort = (parseInt(valuesGotByCgi[idxValue++]) * 256); retValueShort += parseInt(valuesGotByCgi[idxValue++]); if(idxValue > 8); var addressLow = encodeURIComponent(parseInt(MBaddress) & 255); var registerHigh = encodeURIComponent(parseInt(Register) >> 8); var registerLow = encodeURIComponent(parseInt(Register) & 255); ajax.open(""GET"", ""MBCSV.cgi?ID="" + id + ""&F="" + field + ""&AHI="" + addressHigh + ""&ALO="" + addressLow + ""&RHI="" + registerHigh + ""&RLO="" + registerLow, IsAsync); ajax.send(null); if(!IsAsync){ response = ajax.responseText; } return response; }
発電状況を取得するPythonパッケージ
そういったPythonモジュールを作成し、githubに置きました。
PyPIにも公開しているので、ご興味ある方はpip install
してみてください。
簡単な使い方紹介
pipコマンドでインストールして、ヘルプ表示すれば使い方もわかるようになってます。
$ pip install tsmppt60_driver
In [1]: import tsmppt60_driver as ts In [2]: ts.SystemStatus? Init signature: ts.SystemStatus(host) Docstring: This is class to get the system status of TS-MPPT-60. Use this like below. print(SystemStatus(""192.168.1.20"").get()) {'Amp Hours': {'group': 'Counter', 'unit': 'Ah', 'value': 18097.9}, 'Array Current': {'group': 'Array', 'unit': 'A', 'value': 1.4}, 'Array Voltage': {'group': 'Array', 'unit': 'V', 'value': 53.41}, 'Battery Voltage': {'group': 'Battery', 'unit': 'V', 'value': 23.93}, 'Charge Current': {'group': 'Battery', 'unit': 'A', 'value': 3.2}, 'Heat Sink Temperature': {'group': 'Temperature', 'unit': 'C', ...}, 'Kilowatt Hours': {'group': 'Counter', 'unit': 'kWh', 'value': 237.0}, 'Target Voltage': {'group': 'Battery', 'unit': 'V', 'value': 28.6}} The above data is limited information. You can disable the limitter like below. print(SystemStatus(""192.168.1.20"", False).get()) {'Amp Hours': {'group': 'Counter', 'unit': 'Ah', 'value': 18097.8}, 'Array Current': {'group': 'Array', 'unit': 'A', 'value': 1.3}, 'Array Voltage': {'group': 'Array', 'unit': 'V', 'value': 53.41}, 'Battery Temperature': {'group': 'Temperature', 'unit': 'C', ...}, 'Battery Voltage': {'group': 'Battery', 'unit': 'V', 'value': 24.01}, 'Charge Current': {'group': 'Battery', 'unit': 'A', 'value': 3.2}, 'Heat Sink Temperature': {'group': 'Temperature', 'unit': 'C', ...}, 'Kilowatt Hours': {'group': 'Counter', 'unit': 'kWh', 'value': 237.0}, 'Output Power': {'group': 'Battery', 'unit': 'W', 'value': 76.0}, 'Sweep Pmax': {'group': 'Array', 'unit': 'W', 'value': 73.0}, 'Sweep Vmp': {'group': 'Array', 'unit': 'V', 'value': 53.41}, 'Sweep Voc': {'group': 'Array', 'unit': 'V', 'value': 60.05}, 'Target Voltage': {'group': 'Battery', 'unit': 'V', 'value': 28.6}} Init docstring: Initialize class object. Keyword arguments: host -- TS-MPPT-60 host address like ""192.168.1.20"" File: ~/.anyenv/envs/pyenv/versions/3.5.1/lib/python3.5/site-packages/tsmppt60_driver/__init__.py Type: type
実際にやってみるとこうなります。
In [1]: import tsmppt60_driver as ts In [3]: d = ts.SystemStatus(""192.168.1.20"") In [4]: d.get() Out[4]: {'Amp Hours': {'group': 'Counter', 'unit': 'Ah', 'value': 32885.7}, 'Array Current': {'group': 'Array', 'unit': 'A', 'value': 0.0}, 'Array Voltage': {'group': 'Array', 'unit': 'V', 'value': 0.3900146484375}, 'Battery Voltage': {'group': 'Battery', 'unit': 'V', 'value': 23.631591796875}, 'Charge Current': {'group': 'Battery', 'unit': 'A', 'value': -0.09521484375}, 'Heat Sink Temperature': {'group': 'Temperature', 'unit': 'C', 'value': 11}, 'Kilowatt Hours': {'group': 'Counter', 'unit': 'kWh', 'value': 604}, 'Target Voltage': {'group': 'Battery', 'unit': 'V', 'value': 0.0}}
まとめ
API仕様を読むより、実装されたjsクライアントを読んだ方が早いっすね。