OCPP (Open Charge Point Protocol) をPythonとOSSのSteVeで使ってみる

世の中はEV普及に向けて進んでいますが、EVが普及するためには必須事項として充電環境があります。充電器を管理するオープンなプロトコルのOCPP(Open Charge Point Protocol)を触る機会があり記事を書きます。

OCPPとは

OPA(Open Charge Aliance)という団体によりメンテナンスされているプロトコルで、電気自動車の急速充電器を管理する国際標準通信プロトコルです。課金や充電器の保守・運用などを、専用の端末や特別なネットワークを介さず行うことができるようです。(引用元はこちらですが)

イメージ図を公式から引いてきてみました。

f:id:yomon8:20210308084448p:plain

引用元: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を双方向通信で集中管理するためのプロトコルと理解しています。

f:id:yomon8:20210310221738p:plain

OCPPを触りたい

ある接続先サービスがOCPPを話せるため、そのサービスと通信するためのモジュール開発をするために調べ始めたのですが、情報が少ない。

こういうものは触ってみれば理解が進むというところで、Githubを漁って以下を見つけました。

Charge Point側開発に利用するPythonライブラリ

OCPPのPythonライブラリは以下を利用しています。Central Systemも開発できますが、今回はCharge Pointとして利用します。

github.com

SteVe (Central Systemとして使えるOSSパッケージ)

Central Systemは使い方のイメージができずに苦労したのですが、この記事で紹介する SteVe というパッケージを利用することでイメージできました。

github.com

作りたい構成

SteVeで立ち上げたCentral Systemと、Pythonで書いたCharge Pointの疎通をしてみようと思います。

f:id:yomon8:20210310221753p:plain

環境構築(Central System側)

まずはCentral SystemとなるSteveを起動、設定します。

f:id:yomon8:20210310221904p:plain

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で開発します。

f:id:yomon8:20210310223052p:plain

必要パッケージのインストール

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.6BootNotification.reqDataTransfer.req を利用します。

f:id:yomon8:20210310224751p:plain

BootNotification.reqでCharge Pointの起動をCentral Systemに通知する

BootNotification.req は Charge Pointが起動、再起動した際に自身の情報をCentral Systemに通知するものです。

f:id:yomon8:20210310224240p:plain

先程、SteVeに登録したCharge Point my-cp-001 はIDしか設定していませんでした。SteVe上で見てみても何も設定されていません。

f:id:yomon8:20210310225403p:plain

Charge Pointとして開発したPythonプログラムを実行すると以下のような画面で、WebSocketが繋がった待ち状態になります。

メッセージを見てみると、 ModelVendor 情報が送信されているのがわかります。

$ 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 を見てみると情報が設定されているのがわかります。

f:id:yomon8:20210310225704p:plain

DataTransfer.reqでデータを送ってみる

DataTransfer.reqCentral 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 を使ってみようと思います。

f:id:yomon8:20210310230158p:plain

先程、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」を選択します。

f:id:yomon8:20210310224751p:plain

  • 左側メニューにOperationが並んでいるので、 DataTransfer を選択します。
  • Charge Pointに先程設定した my-cp-001 を選択します。
  • 送付するデータの情報を設定します
  • 「Perform」ボタンをクリックします

f:id:yomon8:20210310230843p:plain

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側でも確認できます。

f:id:yomon8:20210310231135p:plain

ちなみにVendor IDに間違った値を設定すると、

f:id:yomon8:20210310231214p:plain

Charge Point(Python)でRejectされます。

f:id:yomon8:20210310231231p:plain

この辺りを応用していけば、色々な連携ができるようになりそうです。

参考資料

まず日本語情報はほぼ無いです。

各種ドキュメントは以下からダウンロードできます。この手順ではV1.6をベースに作業しています。

Downloads - Open Charge Alliance

OPAからWebinerもYoutubeでいくつか公開されています。SteVeで触ってしまった方が理解早いですが、まず学んでみたい人は良いと思います。


OCPP 2.0.1 Tutorial - Open Charge Alliance Webinar