世の中はEV普及に向けて進んでいますが、EVが普及するためには必須事項として充電環境があります。充電器を管理するオープンなプロトコルのOCPP(Open Charge Point Protocol)を触る機会があり記事を書きます。
OCPPとは
OPA(Open Charge Aliance)という団体によりメンテナンスされているプロトコルで、電気自動車の急速充電器を管理する国際標準通信プロトコルです。課金や充電器の保守・運用などを、専用の端末や特別なネットワークを介さず行うことができるようです。(引用元はこちらですが)
イメージ図を公式から引いてきてみました。
引用元:https://www.openchargealliance.org/uploads/files/OCA-Open-Standards-White-Paper-compressed.pdf
大きくはCentral System(図のNetwork Management System)とCharge Point(図のCharging Stations)という概念があり、Central Systemから複数のCharge Pointを双方向通信で集中管理するためのプロトコルと理解しています。
OCPPを触りたい
ある接続先サービスがOCPPを話せるため、そのサービスと通信するためのモジュール開発をするために調べ始めたのですが、情報が少ない。
こういうものは触ってみれば理解が進むというところで、Githubを漁って以下を見つけました。
Charge Point側開発に利用するPythonライブラリ
OCPPのPythonライブラリは以下を利用しています。Central Systemも開発できますが、今回はCharge Pointとして利用します。
SteVe (Central Systemとして使えるOSSパッケージ)
Central Systemは使い方のイメージができずに苦労したのですが、この記事で紹介する SteVe
というパッケージを利用することでイメージできました。
作りたい構成
SteVeで立ち上げたCentral Systemと、Pythonで書いたCharge Pointの疎通をしてみようと思います。
環境構築(Central System側)
まずはCentral SystemとなるSteveを起動、設定します。
SteVeの起動
SteVeはDockerで提供されているので、docker-composerを使うことで簡単に起動することができます。(WSLのUbuntu環境で作業しています)
まず、GithubからリポジトリをCloneして、 docker-compose up
します。
git clone --depth 1 https://github.com/RWTH-i5-IDSG/steve cd steve docker-compose up -d
内部でMavenのビルド処理が入るのでログ見ながら暫く待ちます。(マシンスペックに依存するとおもいますが、Build処理にかなり時間かかると思いますので気長に待ちます)
docker-compose logs -f
起動完了すると以下のようなログが表示されます。
app_1 | Hint: You can stop the application by pressing CTRL+C app_1 | app_1 | Access the web interface using app_1 | - http://172.21.0.3:8180/steve/manager/home app_1 | SOAP endpoint for OCPP app_1 | - http://172.21.0.3:8180/steve/services/CentralSystemService app_1 | WebSocket/JSON endpoint for OCPP app_1 | - ws://172.21.0.3:8180/steve/websocket/CentralSystemService/(chargeBoxId)
ブラウザからSteVeにログイン
WSLで上げたので、localhost
でログに表示されたのと同様のアドレスに接続してみます。
http://localhost:8081/steve/manager/home
ログオン画面が表示されます。
デフォルトのユーザとパスワードは以下の通りです。
User | Password |
---|---|
admin | 1234 |
ちなみにパスワードは以下で定義されています。
steve/main.properties at steve-3.4.4 · RWTH-i5-IDSG/steve · GitHub
ログインできると以下のようにHome画面が表示されます。
Central System(SteVe)にCharge Pointの登録
Central System側に接続するCharge Pointの情報を登録しておきます。
Charge Pointの情報として今回は以下を定義しようと思います。
Charge Point設定項目 | 値 |
---|---|
ID | my-cp-001 |
VENDOR | MY-VENDOR |
MODEL | Model 001 |
メニューの「DATA MANAGEMENT」>「CHARGE POINTS」よりChargePointを登録します。
定義したIDだけ入力してAddをクリックします。
定義したID my-cp-001
というCharge Pointが登録されました。
環境構築(Charge Point側)
次に簡易なCharge PointをPythonで開発します。
必要パッケージのインストール
Python3の環境で以下の2モジュールをインストールします。
pip3 install ocpp websockets
PythonによるCharge Pointの実装
以下のような DataTransfer.req
というリクエストをCentral Systemより受け取るための簡易な実装を行いました。
※ DataTransfer.req
については後述
こちらを steve_ocpp_cp.py
という名前で保存しておきます。
#! /usr/bin/env python3 import asyncio import logging import websockets from ocpp.v16 import call, call_result from ocpp.routing import on from ocpp.v16 import ChargePoint as cp from ocpp.v16.call_result import ( BootNotificationPayload, ) from ocpp.v16.enums import ( DataTransferStatus, Action, ) logging.basicConfig(level=logging.INFO) ########################### # Parameters ########################### CP_ID = "my-cp-001" CP_VENDOR = "MY-VENDOR" CP_SERIAL = "MY-SERIAL-001" CP_MODEL = "MY-MODEL" WS_ENDPOINT = f"ws://localhost:8180/steve/websocket/CentralSystemService/{CP_ID}" class ChargePoint(cp): @on(Action.DataTransfer) async def respond_datatransfer(self, vendor_id, message_id, data): print(f"DataTransfer Vendor ID -> {vendor_id}") print(f"DataTransfer Message ID -> {message_id}") print(f"Datatransfer Data -> {data}") if vendor_id != CP_VENDOR: message = f"{CP_ID}:NG Vendor ID ({vendor_id}) not valid , please set correct vendor id" return call_result.DataTransferPayload(DataTransferStatus.rejected, message) message = f"{CP_ID}:OK" return call_result.DataTransferPayload(DataTransferStatus.accepted, message) async def send_boot_notification(self, model, serial, vendor): req = call.BootNotificationPayload( charge_point_model=model, charge_point_serial_number=serial, charge_point_vendor=vendor, ) response: BootNotificationPayload = await self.call(req) print(f"Res -> {response}") return response async def main(): async with websockets.connect(WS_ENDPOINT, subprotocols=["ocpp1.6"]) as ws: cp = ChargePoint(CP_ID, ws, response_timeout=5) await asyncio.gather( cp.start(), cp.send_boot_notification(CP_MODEL, CP_SERIAL, CP_VENDOR), ) if __name__ == "__main__": try: # asyncio.run() is used when running this example with Python 3.7 and # higher. asyncio.run(main()) except AttributeError: # For Python 3.6 a bit more code is required to run the main() task on # an event loop. loop = asyncio.get_event_loop() loop.run_until_complete(main()) loop.close()
動かしてみる
動かしてみる前に
先程、DataTransfer.req
という用語を出しましたが、OCPPでは Operationという決まり事でCentral SystemとCharge Point間のMessageをやりとりします。
やりとりできるOperationの種類や、Messageの項目はOCPPのバージョンによって差異があります。SteVeの画面からもCentral SystemからCharge PointへOperationを発行できるのですが、以下のようにバージョン毎の画面が用意されています。
今回は OCPP V1.6
の BootNotification.req
と DataTransfer.req
を利用します。
BootNotification.reqでCharge Pointの起動をCentral Systemに通知する
BootNotification.req
は Charge Pointが起動、再起動した際に自身の情報をCentral Systemに通知するものです。
先程、SteVeに登録したCharge Point my-cp-001
はIDしか設定していませんでした。SteVe上で見てみても何も設定されていません。
Charge Pointとして開発したPythonプログラムを実行すると以下のような画面で、WebSocketが繋がった待ち状態になります。
メッセージを見てみると、 Model
や Vendor
情報が送信されているのがわかります。
$ python3 steve_ocpp_cp.py INFO:ocpp:my-cp-001: send [2,"949ebe83-22c8-48e9-9e9d-4b71779d2952","BootNotification",{"chargePointModel":"MY-MODEL","chargePointVendor":"MY-VENDOR","chargePointSerialNumber":"MY-SERIAL-001"}] INFO:ocpp:my-cp-001: receive message [3,"949ebe83-22c8-48e9-9e9d-4b71779d2952",{"status":"Accepted","currentTime":"2021-03-10T13:54:48.423Z","interval":14400}] Res -> BootNotificationPayload(current_time='2021-03-10T13:54:48.423Z', interval=14400, status='Accepted')
SteVeでもう一度 my-cp-001
を見てみると情報が設定されているのがわかります。
DataTransfer.reqでデータを送ってみる
DataTransfer.req
は Central System
-> Charge Point
(Initiated by Central System) でも Charge Point
-> Central System
(Initiated by Charge Point)でも、双方向に使えるデータ送信のOperationです。
先程のBootNotificatin.reqはCharge Point側からCentral Systemへのデータ送信でしたが、今度はCentral SystemからCharge Pointにデータを送るのに DataTransfer.req
を使ってみようと思います。
先程、BootNotification.req
を実行したWebSocketが接続されたままになっていると思います。この接続を利用してCentral System側からメッセージを送ります。
$ python3 steve_ocpp_cp.py INFO:ocpp:my-cp-001: send [2,"949ebe83-22c8-48e9-9e9d-4b71779d2952","BootNotification",{"chargePointModel":"MY-MODEL","chargePointVendor":"MY-VENDOR","chargePointSerialNumber":"MY-SERIAL-001"}] INFO:ocpp:my-cp-001: receive message [3,"949ebe83-22c8-48e9-9e9d-4b71779d2952",{"status":"Accepted","currentTime":"2021-03-10T13:54:48.423Z","interval":14400}] Res -> BootNotificationPayload(current_time='2021-03-10T13:54:48.423Z', interval=14400, status='Accepted')
SteVeの画面から 「Operation」>「OCPP V1.6」を選択します。
- 左側メニューにOperationが並んでいるので、
DataTransfer
を選択します。 - Charge Pointに先程設定した
my-cp-001
を選択します。 - 送付するデータの情報を設定します
- 「Perform」ボタンをクリックします
WebSocketのセッションを見ると以下のようにCentral Systemからデータが送られてきたことが確認できると思います。
$ python3 steve_ocpp_cp.py INFO:ocpp:my-cp-001: send [2,"cc90323e-39af-4adf-9a0a-bda00e55b4b5","BootNotification",{"chargePointModel":"MY-MODEL","chargePointVendor":"MY-VENDOR","chargePointSerialNumber":"MY-SERIAL-001"}] INFO:ocpp:my-cp-001: receive message [3,"cc90323e-39af-4adf-9a0a-bda00e55b4b5",{"status":"Accepted","currentTime":"2021-03-10T14:07:30.037Z","interval":14400}] Res -> BootNotificationPayload(current_time='2021-03-10T14:07:30.037Z', interval=14400, status='Accepted') INFO:ocpp:my-cp-001: receive message [2,"f0f22885-ba58-4cc2-868a-87436bd67c13","DataTransfer",{"vendorId":"MY-VENDOR","messageId":"ID12345","data":"DataTransfer送る"}] DataTransfer Vendor ID -> MY-VENDOR DataTransfer Message ID -> ID12345 Datatransfer Data -> DataTransfer送る INFO:ocpp:my-cp-001: send [3,"f0f22885-ba58-4cc2-868a-87436bd67c13",{"status":"Accepted","data":"my-cp-001:OK"}]
Python側の最後でSteVe側に OK
メッセージを返しているので、SteVe側でも確認できます。
ちなみにVendor IDに間違った値を設定すると、
Charge Point(Python)でRejectされます。
この辺りを応用していけば、色々な連携ができるようになりそうです。
参考資料
まず日本語情報はほぼ無いです。
各種ドキュメントは以下からダウンロードできます。この手順ではV1.6をベースに作業しています。
Downloads - Open Charge Alliance
OPAからWebinerもYoutubeでいくつか公開されています。SteVeで触ってしまった方が理解早いですが、まず学んでみたい人は良いと思います。