EV

OCPI 2.2.1 実装ガイド:Locations、Sessions、CDR をどう構築するか

すでにOCPPベースのCSMSを構築していて、これをローミングパートナーに公開する必要がある場合、OCPIはまったく異なる種類のインテグレーション課題です。OCPPは自社が制御する充電器への単一の永続的WebSocket接続ですが、OCPIは自社が制御しないパートナープラットフォームと、契約の下で交換する、バージョン管理されたRESTモジュール群であり、そのデータは最終的に金銭と突き合わせる必要があります。

本記事では、実際に構築すべきもの——登録とcredentialsの交換、そして本番環境で最も重要となる3つのモジュール、Locations・Sessions・CDR——を実践的に解説します。


OCPIインテグレーションの全体像

すべてのOCPI当事者(CPOまたはeMSP)はversionsエンドポイントを公開し、それぞれ独自にバージョン管理されるモジュールの一部を実装します。充電インフラをeMSPに公開するCPOにとって中心となるモジュールは次の通りです。

  • credentials — 登録とトークン交換
  • locations — 充電器・コネクタ情報(CPO → eMSP、pushまたはpull)
  • sessions — 進行中の充電セッション状態(CPO → eMSP)
  • cdrs — 最終的な請求記録(CPO → eMSP)
  • tariffs — 料金情報(CPO → eMSP)
  • commands — リモート開始/停止要求(eMSP → CPO)

すべての当事者がすべてのモジュールを実装するわけではありません。純粋なeMSPは送信者としてlocationsを実装しませんし、純粋なCPOは自社アプリからのドライバー起点アクションを受け取る受信者としてcommandsを実装することはありません。


ステップ1:VersionsとCredentials

実データのやり取りが始まる前に、双方はcredentialsモジュールを通じて互いに登録し合います。

GET https://your-csms.example.com/ocpi/versions
{
  "status_code": 1000,
  "data": [
    { "version": "2.2.1", "url": "https://your-csms.example.com/ocpi/2.2.1/details" }
  ]
}

パートナーは/2.2.1/detailsを取得して対応モジュールとエンドポイントの一覧を確認したうえで、トークンを登録します。

POST /ocpi/2.2.1/credentials
Authorization: Token CREDENTIALS_TOKEN_A

{
  "token": "CREDENTIALS_TOKEN_B",
  "url": "https://your-csms.example.com/ocpi/versions",
  "roles": [
    { "role": "CPO", "party_id": "SIM", "country_code": "TH",
      "business_details": { "name": "Simplico EV Network" } }
  ]
}

多くのチームが過小評価しがちな点はここです。OCPIの認証は単一のAPIキーではなく、関係ごとに成立します。本番環境では、ローミングパートナーごとに異なるトークンペアを保管・ローテーションし、各パートナーがどのモジュールバージョンに対応しているかを追跡する必要があります。これはコンフィグファイルではなく、CSMSがきちんとしたデータモデルを持つべき状態です。


ステップ2:Locations — 内容だけでなく構造が重要

Locationは単一のフラットなレコードではなく、階層構造になっています。

Location
 └─ EVSE(物理的な充電器)
     └─ Connector(プラグ種別、出力、状態)
{
  "id": "LOC001",
  "party_id": "SIM",
  "country_code": "TH",
  "name": "Simplico Demo Site",
  "address": "123 Sukhumvit Rd",
  "city": "Bangkok",
  "coordinates": { "latitude": "13.736717", "longitude": "100.523186" },
  "evses": [
    {
      "uid": "EVSE001",
      "evse_id": "TH*SIM*E001",
      "status": "AVAILABLE",
      "connectors": [
        { "id": "1", "standard": "IEC_62196_T2", "format": "SOCKET",
          "power_type": "AC_3_PHASE", "max_voltage": 400, "max_amperage": 32 }
      ]
    }
  ],
  "last_updated": "2026-08-07T09:00:00Z"
}

ここで最もバグを生みやすい実装上の2点があります。

  • last_updatedがすべてを左右します。 パートナーは前回同期以降に変更されたレコードのみを取得することが多い(?date_from=)ため、ネストされたEVSE/コネクタのステータスを含め、関連する変更のたびにこのタイムスタンプを確実に更新しないと、パートナーは古い空き状況データを知らないまま使い続けてしまいます。
  • PushかPullかはパートナーごとに合意される事項であり、仕様で固定されているわけではありません。ステータス変更のたびに自社のlocationsエンドポイントへPUT更新してほしいというeMSPもあれば、こちらから取得したいというeMSPもあります。どちらのモデルを選ぶかはパートナー次第なので、CSMSは初日から両方向をサポートできる設計にしておく必要があります。

ステップ3:Sessions — 本当にリアルタイムでなければならない部分

Sessionオブジェクトは認証から完了まで充電の状態を追跡します。特に見落とされがちなのがstatusフィールドで、eMSP側のドライバーアプリがこの情報をリアルタイムで表示するため、分単位ではなく秒単位で実態を反映する必要があります。

{
  "id": "SESS001",
  "location_id": "LOC001",
  "evse_uid": "EVSE001",
  "connector_id": "1",
  "start_date_time": "2026-08-07T14:02:00Z",
  "kwh": 12.4,
  "auth_method": "WHITELIST",
  "status": "ACTIVE",
  "last_updated": "2026-08-07T14:15:00Z"
}

実務的には、OCPI側のsessionsモジュールをOCPPのセッションテーブルの上に乗せた定期バッチジョブにしてはいけません。OCPP層が発行するのと同じイベント(StartTransactionMeterValuesStopTransaction)を購読し、同じリクエストサイクルでOCPI側のセッションオブジェクトをpushまたは更新する必要があります。OCPPとOCPIのセッション状態が乖離すれば、それはローミングパートナーが最初に気づき、そして最初にクレームを入れる箇所になります。


ステップ4:CDR — 請求が実際に成立する場所

CDR(Charge Detail Record)は、それ以前のすべてが正しく生成するために存在するオブジェクトです。セッション終了後に一度だけ送信され、精算のsource of truthとなります。

{
  "id": "CDR001",
  "session_id": "SESS001",
  "start_date_time": "2026-08-07T14:02:00Z",
  "end_date_time": "2026-08-07T14:48:00Z",
  "cdr_token": { "uid": "TOKEN123", "type": "RFID", "contract_id": "TH-SIM-0001" },
  "total_energy": 12.4,
  "total_time": 0.77,
  "total_cost": { "excl_vat": 62.00, "incl_vat": 66.34 },
  "charging_periods": [
    { "start_date_time": "2026-08-07T14:02:00Z",
      "dimensions": [ { "type": "ENERGY", "volume": 12.4 } ] }
  ]
}

本番環境で発見するのではなく、あらかじめ意図的に設計しておくべき点がいくつかあります。

  • CDRは送信後は不変です。 料金計算にバグがあっても、CDRを編集するのではなく、新しいCDRを発行するか、パートナーの紛争処理プロセスで対応します。修正は例外パスであり、通常の運用ではないという前提で請求パイプラインを設計してください。
  • total_costは公開しているtariffsモジュールの内容と一致していなければなりません。 tariffsである価格を示しているのにCDRが別の金額を計算していれば、その不整合こそが実際にサポートチケットを生む原因です——たいてい数週間後、双方とも読みたくない照合レポートの中で発覚します。
  • トランスポート層でのべき等性が重要です。 CDRのPOSTに対するネットワーク再送は珍しくありません。idを重複排除キーとして使い、再送されたリクエストがパートナー側で重複した請求レコードを作らないようにしてください。

実際のパートナーと接続する前のテスト

最初の本番ローミングパートナーと接続する前に、実践的な2つのステップがあります。

  1. 最初の本番相手ではなく、OCA/EVRoamingのバリデーターまたはサンドボックスパートナーに対してテストする。 モジュールバージョンの不一致やフィールドバリデーションのエラーは、こちらの環境で見つける方がはるかにコストが低くて済みます。
  2. 2社目の実パートナーを接続する前に、ライフサイクル全体をエンドツーエンドでシミュレートする。 credentialsの登録、locationのpush、sessionのオープン、ステータス更新のストリーミング、sessionのクローズ、CDRの生成まで一通り確認します。ほとんどのインテグレーションバグは、単一のモジュールの内部ではなく、モジュール間の遷移部分に潜んでいます。

OCPPとの位置関係

すでにOCPPベースのCSMSを構築済み(または運用中)であれば、OCPI層はその内側ではなくに位置づけるべきです。OCPPは充電器側のステートマシンを所有し、OCPIはその状態の一部を、バージョン管理され契約で定義されたパートナー向けインターフェースに変換します。両者を「追加のエンドポイントが後付けされた1つのバックエンド」としてではなく、「データモデルを共有する2つのバックエンド」として扱うことが、3社目のローミングパートナーを迎えても持ちこたえられるOCPIインテグレーションと、そのたびに作り直しが必要になるインテグレーションとの分かれ目です。


よくある質問

OCPI用に別のデータベースが必要ですか、それとも既存のOCPPセッションテーブルを拡張できますか?
OCPI固有のフィールド(party_id、country_code、CDRトークン、tariff参照)が後付けの雑なカラムとして追加されない限り、拡張で問題ありません。Locations、Sessions、CDRはOCPPデータを参照する独立したエンティティとしてモデル化してください。逆方向にはしないことが重要です。

Bilateral接続は、Hubが必要になるまでに現実的に何社のパートナーに対応できますか?
明確な数字はありませんが、多くのチームは4〜6社のBilateralパートナーあたりで負荷を感じ始めます。トークン管理、パートナーごとのpush/pull方針、独立したモジュールバージョン対応が、その場しのぎではなく本格的なオーケストレーションを必要とし始めるタイミングです。

本番のOCPIインテグレーションで最もよく壊れる箇所はどこですか?
多い順に、タイムスタンプの扱い(ネストされたオブジェクトからlast_updatedが正しく伝播しない)、CDRとtariffの不整合、そしてOCPP層とOCPI層の間のセッションステータスの遅延です。


既存のOCPPバックエンドの上にOCPIを構築する場合も、両方をゼロから立ち上げる場合も、それはまさに当社のEV CSMS — Charging Station Management System (OCPP/OCPI)サービスが対応する領域です。技術的なスコープ検討についてのご相談はhello@simplico.netまでご連絡ください。