Techに戻る

【Python初心者向け】EV充電器の通信プロトコル「OCPP」とmobilityhouse/ocppライブラリを使ってOCPPを実装する方法

2025-11-29
PythonでOCPPを実装する基礎とmobilityhouse/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(リクエスト)

充電器が管理システムにリクエストを送るとき:

json
[2, "abc123", "BootNotification", {
  "chargingStation": {
    "model": "Model X",
    "vendorName": "ABC社"
  },
  "reason": "PowerUp"
}]

各部分の意味:

位置意味
1番目2メッセージタイプ(2=CALL=リクエスト)
2番目"abc123"メッセージID(返事と紐づけるため)
3番目"BootNotification"アクション名(何のリクエストか)
4番目{...}ペイロード(中身のデータ)

CALLRESULT(レスポンス)

管理システムが返事を返すとき:

json
[3, "abc123", {
  "currentTime": "2025-11-26T12:00:00Z",
  "interval": 300,
  "status": "Accepted"
}]
位置意味
1番目3メッセージタイプ(3=CALLRESULT=返事)
2番目"abc123"メッセージID(元のリクエストと同じ)
3番目{...}ペイロード(返事の内容)

CALLERROR(エラー)

エラーが発生したとき:

json
[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を実装すると、こんなコードを書く必要があります:

python
# ライブラリなしの場合(とても大変!)
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仕様書を読み込んで正確に実装する必要がある

ライブラリを使うと…こうなる!

python
# ライブラリありの場合(シンプル!)
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 クラス(基底クラス)

python
from ocpp.v201 import ChargePoint

OCPPの基本機能が詰まった「設計図」です。 これを継承して自分のクラスを作ることで、OCPPの基本機能を利用できます。

python
class MyChargePoint(ChargePoint):
    # ここに自分のロジックを書く
    pass

2. @on デコレータ(ハンドラー登録)

python
from ocpp.routing import on

「このアクションが来たら、この関数を呼んで」と登録するための仕組みです。

python
@on('BootNotification')  # ← BootNotificationが来たら↓を呼ぶ
async def on_boot_notification(self, charging_station, reason):
    # 処理を書く
    pass

デコレータとは?

関数の「前後に処理を追加する」仕組みです。@on('BootNotification')は「この関数をBootNotification用として登録してね」という意味。

3. call_result(レスポンス生成)

python
from ocpp.v201 import call_result

返事を作るための「テンプレート」です。

python
return call_result.BootNotification(
    current_time="2025-11-26T12:00:00Z",
    interval=300,
    status="Accepted"
)

自分で辞書を作るより安全。型が決まっているので、間違ったキー名を書くとエラーで教えてくれます。

4. route_message()(メッセージ振り分け)

python
await charge_point.route_message(message)

受け取ったメッセージを適切なハンドラーに振り分けてくれます。

text
充電器からメッセージが来た
         ↓
route_message() がJSONをパース
         ↓
アクション名を見る(例:BootNotification)
         ↓
@on('BootNotification') がついた関数を探す
         ↓
見つけた関数を実行
         ↓
return した内容を自動でJSONに変換
         ↓
充電器に返送

6. async/await って何?

ここからはPythonの非同期処理の話です。
OCPPライブラリを使う上で必須の知識になります。

一言で言うと

「待ってていいよ」という印です。

時間がかかる処理を待っている間、他の仕事をしていいよ、という意味になります。

日常生活での例え:レストラン編

awaitなし(同期処理)の世界:

text
あなた:「パスタください」
店員:「はい、作りますね」

(10分間、店員はあなたの前で立ち尽くしている)
(この間、他のお客さんは注文できない)

店員:「パスタできました!」

→ 1人のお客さんに1人の店員がつきっきり。非効率!

awaitあり(非同期処理)の世界:

text
あなた:「パスタください」
店員:「はい、作りますね。できたら呼びます」← await

(店員は他のお客さんの対応へ)
(あなたはスマホを見て待つ)

店員:「パスタできました!」

→ 待ち時間に他の仕事ができる。効率的!

コードで見てみよう

awaitなし(同期処理):

python
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あり(非同期処理):

python
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「ここで待つから、その間に他の仕事してもいいよ」非同期関数の呼び出し時
python
async def my_function():      # async = この関数は非同期
    result = await some_task() # await = ここで待つ(他の処理可能)
    return result

どういうときに使う?

処理の種類async/await理由
ネットワーク通信◯ 使う相手からの返事を待つから
ファイルの読み書き◯ 使うディスクの処理を待つから
データベースアクセス◯ 使うDBの処理を待つから
WebSocket通信◯ 使うメッセージを待つから
計算処理✕ 不要待ち時間がないから
文字列操作✕ 不要待ち時間がないから

よくあるミス

await を書き忘れる:

python
# ✕ 間違い
result = some_async_function()  # awaitがない!
print(result)  
# → <coroutine object some_async_function at 0x...> と表示される
# (実行されていない!)

# ◯ 正しい
result = await some_async_function()
print(result)  
# → ちゃんと結果が返る

async をつけ忘れる:

python
# ✕ 間違い
def my_function():
    result = await some_task()  # エラー!asyncがないのにawaitは使えない

# ◯ 正しい
async def my_function():
    result = await some_task()  # OK

7. 実際のプロジェクトでの組み合わせ

ここまでの知識がどう組み合わさるか見てみましょう。

全体の構成

実際のコード

1. ライブラリを継承したクラスを作る:

python
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. ハンドラーを登録する:

python
    @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でメッセージを受け取ってルーティング:

python
@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台と接続してきます。

text
時間軸 →
────────────────────────────────────────────────
充電器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との違いで説明します。

プロトコル特徴例え
HTTP1回の質問に1回の答え。その後切断。手紙のやり取り
WebSocket接続しっぱなし。いつでも双方向に送受信可能。電話

OCPPでは充電器と常に接続しておきたいので、WebSocketを使います。

Q3: なぜFastAPIを使うの?

FastAPIはPythonのWebフレームワークで、以下の特徴があります:

  • WebSocketに対応している
  • 非同期処理(async/await)に対応している
  • 高速
  • 型ヒントでバグが減る

Q4: call_resultcall の違いは?

モジュール用途
call_result受け取ったリクエストへの返事を作るBootNotificationへの返答
callこちらからリクエストを送る充電開始を指示する
python
# 返事を返す(受け身)
return call_result.BootNotification(...)

# リクエストを送る(能動的)
await self.call(call.RequestStartTransaction(...))

Q5: @on デコレータのアクション名は自由?

いいえ、OCPP仕様で決まっています。

python
@on('BootNotification')      # ◯ 正しい
@on('boot_notification')     # ✕ 間違い(大文字小文字が違う)
@on('MyCustomAction')        # ✕ 間違い(存在しないアクション)

まとめ

学んだこと

概念一言まとめ
OCPPEV充電器と管理システムの「共通言語」
mobilityhouse/ocppOCPPの面倒な処理を自動化してくれるライブラリ
@on デコレータ「このアクションはこの関数で処理」と登録する仕組み
call_result返事を型安全に作るテンプレート
route_message()受け取ったメッセージを適切なハンドラーに振り分ける
async「この関数は待ち時間がある処理を含むよ」という宣言
await「ここで待つけど、その間は他の仕事していいよ」という指示

全体像

充電器 ─── WebSocket ───► FastAPI ───► route_message() ───► @on ハンドラ
                                              ↑
                                    mobilityhouse/ocpp ライブラリ
                                    (JSONパース、ルーティング、バリデーション)

このライブラリを使うメリット

  1. OCPPの仕様に準拠 - 仕様書を読み込まなくてもOK
  2. コードがシンプル - ビジネスロジックに集中できる
  3. バグが減る - パースやバリデーションは実績あるライブラリにお任せ
  4. 複数バージョン対応 - OCPP 1.6と2.0.1を同じ書き方で使える

お疲れさまでした!この記事が、OCPPとPythonの非同期処理を理解する助けになれば幸いです。

この記事は役に立ちましたか?

フィードバックはブログの改善に活用させていただきます