EV

OCPI 2.2.1 实现指南:如何构建 Locations、Sessions 与 CDR 模块

如果你已经搭建了一套基于 OCPP 的 CSMS,现在需要将其开放给漫游合作伙伴,OCPI 是一类完全不同的对接问题。OCPP 是与你自己控制的充电桩之间建立的单条持久 WebSocket 连接;而 OCPI 是与你并不控制、受合同约束的合作方平台交换的一组带版本号的 REST 模块,其中的数据最终要与真实资金对上账。

本文将逐步讲解真正需要构建的部分:注册与凭证交换,以及生产环境中最关键的三个模块——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 也不会作为接收方去实现来自自家 App 车主发起动作的 commands


第一步: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 Key。在生产环境中,你需要为每个漫游合作方分别存储和轮换一对令牌,并跟踪每个合作方支持的模块版本——这是 CSMS 需要真正数据模型来承载的状态,而不是靠一份配置文件就能解决的。


第二步: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"
}

这里有两个最容易引发 bug 的实现细节:

  • last_updated 决定了一切。 合作方通常只拉取自上次同步以来发生变化的记录(?date_from=)。如果后端没有在每次相关变化(包括嵌套的 EVSE/接口状态)时可靠地更新这个时间戳,合作方就会在不知情的情况下使用过期的可用性数据。
  • Push 还是 Pull 是按合作方协商的,而不是规范里固定死的。有些 eMSP 希望你在每次状态变化时 PUT 更新到他们的 locations 接口;有些则更愿意主动来轮询你这边。你的 CSMS 从一开始就需要同时支持这两个方向,因为你无法决定每个合作方会选哪种模式。

第三步:Sessions —— 真正需要实时的部分

Session 对象追踪一次充电从授权到完成的全过程。最容易踩坑的字段是 status——它需要在几秒钟内(而不是几分钟)反映真实情况,因为 eMSP 车主端 App 正在实时展示这个状态。

{
  "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 session 表之上的周期性批处理任务,而必须订阅 OCPP 层发出的同一批事件(StartTransactionMeterValuesStopTransaction),并在同一个请求周期内 push 或更新 OCPI 一侧的 session 对象。一旦 OCPP 与 OCPI 的会话状态出现偏差,这将是漫游合作方第一个注意到、也是第一个投诉的地方。


第四步: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 一旦发送即不可变更。 如果资费计算出现 bug,不能直接修改已发出的 CDR,而应发出一条新记录,或通过合作方的争议处理流程解决。设计计费流水线时应默认「修正」是例外路径,而不是常规操作。
  • total_cost 必须与你发布的 tariffs 模块所隐含的价格一致。 如果 tariffs 中写的是一个价格,而 CDR 计算出另一个数字,这种不一致正是真正引发工单的原因——通常要等到几周后,在一份双方都不愿意看的对账报告里才会暴露。
  • 幂等性在传输层同样重要。 CDR 的 POST 请求出现网络重试很常见,应将 id 作为去重键,避免重试请求在合作方一侧生成重复的计费记录。

接入真实合作方之前的测试

在对接第一个真实漫游合作方之前,有两个实用步骤:

  1. 先针对 OCA/EVRoaming 的验证工具或沙盒合作方进行测试,而不是直接对接第一个生产环境合作方——模块版本不匹配和字段校验错误,在这个环境里发现的成本要低得多。
  2. 端到端模拟完整生命周期:注册凭证、推送一个 location、开启一个 session、持续推送状态更新、关闭 session、生成 CDR——都跑通之后,再接入第二个真实合作方。大多数对接 bug 出现在模块之间的过渡环节,而不是某个单一模块内部。

OCPI 与 OCPP 的关系

如果你已经构建(或正在运行)一套基于 OCPP 的 CSMS,OCPI 层应该位于其旁边,而不是内部。OCPP 负责充电桩一侧的状态机;OCPI 则把其中一部分状态转换为面向合作方、带版本号、由合同定义的接口。应当把两者视为共享同一套数据模型的两个后端,而不是在一个后端上后补一个接口——这正是「能扛住第三个漫游合作方接入」的 OCPI 实现,和「每接入一个新合作方就要重写一遍」的实现之间的差别。


常见问题

OCPI 需要单独的数据库,还是可以扩展现有的 OCPP session 表?
只要 OCPI 专属字段(party_id、country_code、CDR token、tariff 引用)不是作为事后补加的字段随意塞进去,扩展现有表通常没问题。应将 Locations、Sessions、CDR 建模为引用 OCPP 数据的独立实体,而不是反过来。

双边对接在需要切换到 Hub 之前,现实中大概能支撑多少个合作方?
没有一个硬性数字,但大多数团队在 4-6 个双边合作方左右就会感受到压力——此时令牌管理、各合作方不同的 push/pull 偏好,以及各自独立的模块版本支持,开始需要真正的编排机制,而不再是临时应对。

生产环境中的 OCPI 对接最常在哪里出问题?
按出现频率排序依次是:时间戳处理(last_updated 未能从嵌套对象正确传播)、CDR 与 tariff 不一致,以及 OCPP 层与 OCPI 层之间的会话状态滞后。


无论是在现有 OCPP 后端之上构建 OCPI,还是从零同时搭建两者,这正是我们 EV CSMS — Charging Station Management System (OCPP/OCPI) 服务所覆盖的工作范围。如需技术层面的方案沟通,欢迎联系 hello@simplico.net