【Python初心者向け】EV充電器の通信プロトコル「OCPP」とmobilityhouse/ocppライブラリを使ってOCPPを実装する方法
この記事で学べること
- OCPPとは何か(EV充電の世界の「共通言語」)
- mobilityhouse/ocppライブラリが何をしてくれるのか
- Pythonの
async/awaitとは何か - 実際のプロジェクトでこれらがどう組み合わさるか
対象読者: Pythonは少し書けるけど、OCPPや非同期処理は初めての方
1. そもそもOCPPって何?
EV充電の世界には「共通言語」が必要だった
電気自動車(EV)を充電するとき、街中にある充電器を使いますよね。
自宅にあるようなコンセントに挿すだけのシンプルな充電器(スタンドアローン型)もありますが、公共施設や商業施設にある 課金やユーザー認証、遠隔監視が必要な充電器(ネットワーク型) は、裏側でクラウド上の管理システムと通信しています。
でも、充電器メーカーはA社、B社、C社…とたくさんあります。
管理システムもX社、Y社、Z社…とさまざまです。
もし各社がバラバラの方法で通信していたら?
- A社の充電器はX社のシステムでしか使えない
- B社の充電器はY社のシステムでしか使えない
- 互換性がなくて大変!
そこで生まれたのが OCPP(Open Charge Point Protocol) です。
OCPPは「注文伝票のフォーマット」
レストランで例えてみましょう。
| 充電の世界 | レストランの例え |
|---|---|
| 充電器 (CS) | ウェイター |
| 管理システム (CSMS) | 厨房のシェフ |
| OCPP | 注文伝票のフォーマット |
| WebSocket | 厨房とホールをつなぐ通路 |
ウェイターが「ハンバーグ1つ!」と叫んでも、シェフが「ステーキ1つ」と聞き間違えたら大変。だから決まったフォーマットの伝票を使います。
OCPPはまさにその「伝票のフォーマット」。世界中の充電器メーカーと管理システムが、この共通フォーマットで会話できるようになりました。
2. OCPPで何を話しているの?
充電器 → 管理システム(報告系)
充電器は色々なことを管理システムに報告します。
| メッセージ名 | 意味 | 日常での例え |
|---|---|---|
| BootNotification | 「起動しました!」 | 出勤報告 |
| Heartbeat | 「まだ生きてますよ〜」 | 定時連絡 |
| StatusNotification | 「今こういう状態です」 | 状況報告 |
| TransactionEvent | 「充電が始まりました/終わりました」 | 作業報告 |
| MeterValues | 「今10kWh充電しました」 | 数値報告 |
| Authorize | 「このカードで充電していい?」 | 許可申請 |
管理システム → 充電器(指示系)
管理システムは充電器に指示を出します。
| メッセージ名 | 意味 | 日常での例え |
|---|---|---|
| RequestStartTransaction | 「充電を開始して」 | 作業指示 |
| RequestStopTransaction | 「充電を止めて」 | 作業中止指示 |
| SetVariables | 「設定を変更して」 | 設定変更指示 |
| Reset | 「再起動して」 | リセット指示 |
3. メッセージの形式を見てみよう
OCPPのメッセージはJSON形式でやり取りされます。
CALL(リクエスト)
充電器が管理システムにリクエストを送るとき:
[2, "abc123", "BootNotification", {
"chargingStation": {
"model": "Model X",
"vendorName": "ABC社"
},
"reason": "PowerUp"
}]
各部分の意味:
| 位置 | 値 | 意味 |
|---|---|---|
| 1番目 | 2 | メッセージタイプ(2=CALL=リクエスト) |
| 2番目 | "abc123" | メッセージID(返事と紐づけるため) |
| 3番目 | "BootNotification" | アクション名(何のリクエストか) |
| 4番目 | {...} | ペイロード(中身のデータ) |
CALLRESULT(レスポンス)
管理システムが返事を返すとき:
[3, "abc123", {
"currentTime": "2025-11-26T12:00:00Z",
"interval": 300,
"status": "Accepted"
}]
| 位置 | 値 | 意味 |
|---|---|---|
| 1番目 | 3 | メッセージタイプ(3=CALLRESULT=返事) |
| 2番目 | "abc123" | メッセージID(元のリクエストと同じ) |
| 3番目 | {...} | ペイロード(返事の内容) |
CALLERROR(エラー)
エラーが発生したとき:
[4, "abc123", "NotImplemented", "この機能は未実装です", {}]
| 位置 | 値 | 意味 |
|---|---|---|
| 1番目 | 4 | メッセージタイプ(4=CALLERROR=エラー) |
| 2番目 | "abc123" | メッセージID |
| 3番目 | "NotImplemented" | エラーコード |
| 4番目 | "この機能は..." | エラーの説明 |
| 5番目 | {} | 詳細情報 |
4. mobilityhouse/ocppライブラリとは
The Mobility House社が公開している、PythonでOCPPを実装するためのデファクトスタンダード的なライブラリです。
このライブラリの特徴は以下の通りです:
- OCPP 1.6 / 2.0.1 両対応: 主要なバージョンをどちらもサポートしています。
- JSONスキーマバリデーション: メッセージの形式が正しいか自動でチェックしてくれます。
- 非同期処理 (asyncio): Pythonの標準的な非同期処理ライブラリ
asyncioをベースにしており、高いパフォーマンスが出せます。 - 直感的な実装:
@onデコレータを使って、「このメッセージが来たらこの処理」という風にシンプルに書けます。
では、このライブラリを使うとどれくらい楽になるのか、比較してみましょう。
ライブラリなしで実装すると…こうなる
もしライブラリを使わずにOCPPを実装すると、こんなコードを書く必要があります:
# ライブラリなしの場合(とても大変!)
import json
async def handle_message(raw_message):
# JSONをパース
data = json.loads(raw_message)
# メッセージタイプを判定
if data[0] == 2: # CALL
message_id = data[1]
action = data[2]
payload = data[3]
# アクションごとに分岐
if action == "BootNotification":
# BootNotificationの処理
vendor = payload.get("chargingStation", {}).get("vendorName")
model = payload.get("chargingStation", {}).get("model")
# 返事を作成
response = {
"currentTime": datetime.now().isoformat(),
"interval": 300,
"status": "Accepted"
}
# JSONに変換して返送
await websocket.send(json.dumps([3, message_id, response]))
elif action == "Heartbeat":
# Heartbeatの処理...
response = {"currentTime": datetime.now().isoformat()}
await websocket.send(json.dumps([3, message_id, response]))
elif action == "StatusNotification":
# StatusNotificationの処理...
pass
elif action == "Authorize":
# Authorizeの処理...
pass
# ... OCPPには50種類以上のアクションがある!
# 全部自分で書く必要がある!
elif data[0] == 3: # CALLRESULT
# レスポンスの処理...
pass
elif data[0] == 4: # CALLERROR
# エラーの処理...
pass
問題点:
- 👎 JSONのパース処理を毎回書く
- 👎 メッセージタイプの判定を毎回書く
- 👎 アクションごとのif文が大量に必要
- 👎 バリデーション(正しい形式かチェック)を自分で実装
- 👎 エラーハンドリングも全部自分で
- 👎 OCPP仕様書を読み込んで正確に実装する必要がある
ライブラリを使うと…こうなる!
# ライブラリありの場合(シンプル!)
from ocpp.routing import on
from ocpp.v201 import ChargePoint, call_result
class MyChargePoint(ChargePoint):
@on('BootNotification')
async def on_boot_notification(self, charging_station, reason):
return call_result.BootNotification(
current_time=datetime.now().isoformat(),
interval=300,
status="Accepted"
)
@on('Heartbeat')
async def on_heartbeat(self):
return call_result.Heartbeat(
current_time=datetime.now().isoformat()
)
@on('StatusNotification')
async def on_status_notification(self, timestamp, connector_status, evse_id, connector_id):
return call_result.StatusNotification()
ビフォーアフター:
| 作業 | ライブラリなし | ライブラリあり |
|---|---|---|
| JSONパース | 自分で書く | 👍 自動 |
| メッセージタイプ判定 | 自分で書く | 👍 自動 |
| アクション振り分け | if文を大量に書く | 👍 @onデコレータで自動 |
| レスポンス生成 | 辞書を手動で作る | 👍 call_resultで型安全 |
| バリデーション | 自分で書く | 👍 自動 |
あなたが書くのはビジネスロジックだけ!
5.ライブラリの主要コンポーネント
1. ChargePoint クラス(基底クラス)
from ocpp.v201 import ChargePoint
OCPPの基本機能が詰まった「設計図」です。 これを継承して自分のクラスを作ることで、OCPPの基本機能を利用できます。
class MyChargePoint(ChargePoint):
# ここに自分のロジックを書く
pass
2. @on デコレータ(ハンドラー登録)
from ocpp.routing import on
「このアクションが来たら、この関数を呼んで」と登録するための仕組みです。
@on('BootNotification') # ← BootNotificationが来たら↓を呼ぶ
async def on_boot_notification(self, charging_station, reason):
# 処理を書く
pass
デコレータとは?
関数の「前後に処理を追加する」仕組みです。@on('BootNotification')は「この関数をBootNotification用として登録してね」という意味。
3. call_result(レスポンス生成)
from ocpp.v201 import call_result
返事を作るための「テンプレート」です。
return call_result.BootNotification(
current_time="2025-11-26T12:00:00Z",
interval=300,
status="Accepted"
)
自分で辞書を作るより安全。型が決まっているので、間違ったキー名を書くとエラーで教えてくれます。
4. route_message()(メッセージ振り分け)
await charge_point.route_message(message)
受け取ったメッセージを適切なハンドラーに振り分けてくれます。
充電器からメッセージが来た
↓
route_message() がJSONをパース
↓
アクション名を見る(例:BootNotification)
↓
@on('BootNotification') がついた関数を探す
↓
見つけた関数を実行
↓
return した内容を自動でJSONに変換
↓
充電器に返送
6. async/await って何?
ここからはPythonの非同期処理の話です。
OCPPライブラリを使う上で必須の知識になります。
一言で言うと
「待ってていいよ」という印です。
時間がかかる処理を待っている間、他の仕事をしていいよ、という意味になります。
日常生活での例え:レストラン編
awaitなし(同期処理)の世界:
あなた:「パスタください」
店員:「はい、作りますね」
(10分間、店員はあなたの前で立ち尽くしている)
(この間、他のお客さんは注文できない)
店員:「パスタできました!」
→ 1人のお客さんに1人の店員がつきっきり。非効率!
awaitあり(非同期処理)の世界:
あなた:「パスタください」
店員:「はい、作りますね。できたら呼びます」← await
(店員は他のお客さんの対応へ)
(あなたはスマホを見て待つ)
店員:「パスタできました!」
→ 待ち時間に他の仕事ができる。効率的!
コードで見てみよう
awaitなし(同期処理):
import time
def make_pasta():
print("パスタ作り始め...")
time.sleep(3) # 3秒待つ(この間、何もできない!)
print("パスタ完成!")
return "🍝"
def make_coffee():
print("コーヒー作り始め...")
time.sleep(2) # 2秒待つ
print("コーヒー完成!")
return "☕"
# 実行
pasta = make_pasta() # 3秒待つ
coffee = make_coffee() # さらに2秒待つ
# 合計: 5秒かかる
awaitあり(非同期処理):
import asyncio
async def make_pasta():
print("パスタ作り始め...")
await asyncio.sleep(3) # 3秒待つ(でも他の処理OK)
print("パスタ完成!")
return "🍝"
async def make_coffee():
print("コーヒー作り始め...")
await asyncio.sleep(2) # 2秒待つ
print("コーヒー完成!")
return "☕"
# 実行(同時に作る!)
async def main():
results = await asyncio.gather(
make_pasta(),
make_coffee()
)
print(results) # ['🍝', '☕']
asyncio.run(main())
# 合計: 3秒で両方完成!(並行処理)
ポイント: await があるところで「他の仕事していいよ」となる。
キーワードの意味
| キーワード | 意味 | 必須の場所 |
|---|---|---|
async | 「この関数は非同期処理を含むよ」という宣言 | 関数定義の前 |
await | 「ここで待つから、その間に他の仕事してもいいよ」 | 非同期関数の呼び出し時 |
async def my_function(): # async = この関数は非同期
result = await some_task() # await = ここで待つ(他の処理可能)
return result
どういうときに使う?
| 処理の種類 | async/await | 理由 |
|---|---|---|
| ネットワーク通信 | ◯ 使う | 相手からの返事を待つから |
| ファイルの読み書き | ◯ 使う | ディスクの処理を待つから |
| データベースアクセス | ◯ 使う | DBの処理を待つから |
| WebSocket通信 | ◯ 使う | メッセージを待つから |
| 計算処理 | ✕ 不要 | 待ち時間がないから |
| 文字列操作 | ✕ 不要 | 待ち時間がないから |
よくあるミス
await を書き忘れる:
# ✕ 間違い
result = some_async_function() # awaitがない!
print(result)
# → <coroutine object some_async_function at 0x...> と表示される
# (実行されていない!)
# ◯ 正しい
result = await some_async_function()
print(result)
# → ちゃんと結果が返る
async をつけ忘れる:
# ✕ 間違い
def my_function():
result = await some_task() # エラー!asyncがないのにawaitは使えない
# ◯ 正しい
async def my_function():
result = await some_task() # OK
7. 実際のプロジェクトでの組み合わせ
ここまでの知識がどう組み合わさるか見てみましょう。
全体の構成
実際のコード
1. ライブラリを継承したクラスを作る:
from ocpp.routing import on
from ocpp.v201 import ChargePoint as OCPPChargePoint
from ocpp.v201 import call_result
class CSMSChargePoint(OCPPChargePoint):
"""CSMSとして動作するChargePointクラス"""
def __init__(self, cs_id, connection):
super().__init__(cs_id, connection)
self.cs_id = cs_id
2. ハンドラーを登録する:
@on('BootNotification')
async def on_boot_notification(self, charging_station, reason):
"""充電器の起動通知を処理"""
print(f"充電器 {self.cs_id} が起動しました")
print(f"メーカー: {charging_station.get('vendorName')}")
print(f"理由: {reason}")
return call_result.BootNotification(
current_time=datetime.now(timezone.utc).isoformat(),
interval=300, # 5分ごとにHeartbeat送ってね
status="Accepted"
)
@on('Heartbeat')
async def on_heartbeat(self):
"""定期的な死活確認を処理"""
print(f"充電器 {self.cs_id} からHeartbeat")
return call_result.Heartbeat(
current_time=datetime.now(timezone.utc).isoformat()
)
3. WebSocketでメッセージを受け取ってルーティング:
@router.websocket("/ocpp/{cs_id}")
async def ocpp_websocket_endpoint(websocket: WebSocket, cs_id: str):
"""充電器からのWebSocket接続を受け付ける"""
# 接続を受け入れる
await websocket.accept(subprotocol="ocpp2.0.1")
# ChargePointインスタンスを作成
charge_point = CSMSChargePoint(cs_id, websocket)
# メッセージを待ち続ける
while True:
# メッセージを受け取る(awaitで待つ)
message = await websocket.receive_text()
# ライブラリにルーティングを任せる
await charge_point.route_message(message)
なぜasync/awaitが必要?
充電器は1台だけでなく、100台、1000台と接続してきます。
時間軸 →
────────────────────────────────────────────────
充電器A: [メッセージ待ち...........][処理][メッセージ待ち...]
充電器B: [メッセージ待ち..][処理][メッセージ待ち........]
充電器C: [メッセージ待ち....][処理][メッセージ待ち..]
await があるからこそ、1つの充電器からのメッセージを待っている間に、他の充電器の処理ができます。
もし await がなかったら、充電器Aのメッセージを待っている間、充電器BもCも処理できなくなってしまいます。
8. よくある疑問Q&A
Q1: OCPPのバージョンって何が違うの?
| バージョン | 特徴 |
|---|---|
| OCPP 1.6 | 古いバージョン。まだ多くの充電器で使われている |
| OCPP 2.0.1 | 新しいバージョン。機能が増えて、セキュリティも強化 |
本プロジェクトでは OCPP 2.0.1 を使用しています。
Q2: WebSocketって何?
HTTPとの違いで説明します。
| プロトコル | 特徴 | 例え |
|---|---|---|
| HTTP | 1回の質問に1回の答え。その後切断。 | 手紙のやり取り |
| WebSocket | 接続しっぱなし。いつでも双方向に送受信可能。 | 電話 |
OCPPでは充電器と常に接続しておきたいので、WebSocketを使います。
Q3: なぜFastAPIを使うの?
FastAPIはPythonのWebフレームワークで、以下の特徴があります:
- WebSocketに対応している
- 非同期処理(async/await)に対応している
- 高速
- 型ヒントでバグが減る
Q4: call_result と call の違いは?
| モジュール | 用途 | 例 |
|---|---|---|
call_result | 受け取ったリクエストへの返事を作る | BootNotificationへの返答 |
call | こちらからリクエストを送る | 充電開始を指示する |
# 返事を返す(受け身)
return call_result.BootNotification(...)
# リクエストを送る(能動的)
await self.call(call.RequestStartTransaction(...))
Q5: @on デコレータのアクション名は自由?
いいえ、OCPP仕様で決まっています。
@on('BootNotification') # ◯ 正しい
@on('boot_notification') # ✕ 間違い(大文字小文字が違う)
@on('MyCustomAction') # ✕ 間違い(存在しないアクション)
まとめ
学んだこと
| 概念 | 一言まとめ |
|---|---|
| OCPP | EV充電器と管理システムの「共通言語」 |
| mobilityhouse/ocpp | OCPPの面倒な処理を自動化してくれるライブラリ |
| @on デコレータ | 「このアクションはこの関数で処理」と登録する仕組み |
| call_result | 返事を型安全に作るテンプレート |
| route_message() | 受け取ったメッセージを適切なハンドラーに振り分ける |
| async | 「この関数は待ち時間がある処理を含むよ」という宣言 |
| await | 「ここで待つけど、その間は他の仕事していいよ」という指示 |
全体像
充電器 ─── WebSocket ───► FastAPI ───► route_message() ───► @on ハンドラ
↑
mobilityhouse/ocpp ライブラリ
(JSONパース、ルーティング、バリデーション)
このライブラリを使うメリット
- OCPPの仕様に準拠 - 仕様書を読み込まなくてもOK
- コードがシンプル - ビジネスロジックに集中できる
- バグが減る - パースやバリデーションは実績あるライブラリにお任せ
- 複数バージョン対応 - OCPP 1.6と2.0.1を同じ書き方で使える
お疲れさまでした!この記事が、OCPPとPythonの非同期処理を理解する助けになれば幸いです。
この記事は役に立ちましたか?
フィードバックはブログの改善に活用させていただきます