资讯动态

OCPP1.6充电桩JSON消息解析与实战避坑指南

发布时间:2026/10/16 11:55:31 来源:尧图企业网站定制
简介这份资源面向欧标充电桩开发、测试及协议对接人员整理了OCPP1.6标准下全部消息事件的JSON格式定义帮助解决充电桩与后台通信时字段含义不清、报文构造困难的问题。压缩包共78个文件全部为json格式整体约28KB涵盖BootNotification、Authorize、StartTransaction、MeterValues、RemoteStartTransaction等核心消息的request与response结构并标注必要字段与可选字段便于对照协议原文理解一问一答的通信模式。已有5462人学习下载说明其在充电桩协议入门与联调场景中具有较高参考价值。读者可借助这些JSON样例模拟发送报文复现充电桩业务流程快速验证后台接口与桩端交互逻辑也可作为开发自测、协议培训或问题排查的字段速查依据适合需要深入掌握OCPP1.6通信细节的工程师与测试人员使用。1. 欧标充电桩 OCPP1.6 消息事件 JSON 格式从抓包到解析的落地笔记如果你正在做欧标交流或直流充电桩的对接大概率绕不开 OCPP1.6 这套协议。它规定了桩和后台之间怎么握手、怎么启动充电、怎么上报状态而所有这些交互最终都落在一条条 JSON 消息上。我手上这份资源就是围绕 OCPP1.6 的消息事件 JSON 格式整理出来的实战参考覆盖了从 BootNotification 到 MeterValues 的常见消息类型每条都给出了字段结构和示例。它解决的不是“协议是什么”这种教科书问题而是“抓到的报文里这个字段到底填什么、后台为什么拒收”这类现场问题。适合刚接触充电桩协议的后端开发、嵌入式工程师以及需要做协议一致性测试的测试人员。Linux 环境下用 Python 或 Node.js 做解析验证是最顺手的路径。2. OCPP1.6 消息骨架JSON 结构拆解与字段语义2.1 消息数组的四段式结构OCPP1.6 的每条消息都是一个 JSON 数组固定四个元素顺序不能乱。这是最容易翻车的地方——很多人第一次看抓包以为是个对象结果解析时直接报错。结构长这样[ 2, 19223201, BootNotification, { chargePointVendor: VendorX, chargePointModel: ModelY } ]第一个元素是消息类型 ID2 表示 CALL请求3 表示 CALLRESULT响应4 表示 CALLERROR错误。第二个是唯一消息 ID通常用递增数字或 UUID用来把请求和响应配对。第三个是动作名称比如 BootNotification、Heartbeat、StartTransaction。第四个是 payload具体字段随动作不同而变化。注意消息 ID 在同一个连接会话里必须唯一但不同会话之间可以重复。我见过有人用时间戳做 ID结果同一毫秒内发两条就撞了后台直接返回错误。2.2 常见动作的 payload 字段对照不同动作的 payload 差异很大但有几个字段反复出现。下面这张表是我从资源里整理的高频动作字段对照方便你快速定位动作名称方向关键字段易错点BootNotification桩→后台chargePointVendor, chargePointModel厂商名和型号不能为空否则后台可能拒绝注册Heartbeat桩→后台无间隔由后台在 BootNotification 响应里指定别自己写死StatusNotification桩→后台connectorId, status, errorCodeconnectorId 从 0 开始0 是桩本身1 才是第一个枪StartTransaction桩→后台connectorId, idTag, meterStart, timestampmeterStart 单位是 Wh不是 kWh差一千倍MeterValues桩→后台connectorId, transactionId, meterValuesampledValue 里 value 是字符串不是数字StopTransaction桩→后台transactionId, meterStop, timestamptransactionId 必须和 StartTransaction 响应里的一致这张表建议打印出来贴在工位上。尤其是 connectorId 和 meterStart 的单位问题我见过不止一个项目在这两个地方栽跟头。2.3 用 Python 做最小化解析验证拿到一份 JSON 消息样本后别急着往业务代码里塞。先用 Python 做一次结构校验确认字段类型和必填项都对得上。下面这段脚本是我常用的最小验证逻辑import json # 模拟一条从桩端抓到的 StartTransaction 消息 raw [2, 1001, StartTransaction, {connectorId: 1, idTag: ABC123, meterStart: 0, timestamp: 2024-01-15T10:00:00Z}] msg json.loads(raw) msg_type msg[0] # 消息类型2CALL msg_id msg[1] # 消息 ID action msg[2] # 动作名 payload msg[3] # 业务字段 # 校验必填字段是否存在 required [connectorId, idTag, meterStart, timestamp] missing [f for f in required if f not in payload] if missing: print(f缺少字段: {missing}) else: print(f动作 {action} 字段完整connectorId{payload[connectorId]}) # 校验 meterStart 类型必须是整数 if not isinstance(payload[meterStart], int): print(meterStart 不是整数检查是否误传了字符串)这段代码的逻辑很直白先按四段式拆开再检查 payload 里必填字段有没有缺最后确认 meterStart 是整数类型。参数方面required 列表按动作不同要替换比如 BootNotification 就换成 chargePointVendor 和 chargePointModel。实际项目中我会把每个动作的必填字段写成一个字典循环校验避免漏掉。3. 从抓包到复现在 Linux 上搭一套 OCPP1.6 消息验证环境3.1 用 tcpdump 和 Wireshark 分离 WebSocket 帧OCPP1.6 走的是 WebSocket默认路径是 /ocpp/端口常见 80 或 443。在 Linux 上抓包第一步是确认 WebSocket 握手成功。用 tcpdump 抓 80 端口的流量sudo tcpdump -i eth0 -w ocpp.pcap port 80抓完后用 Wireshark 打开过滤表达式用websocket就能看到帧。但 Wireshark 默认把 WebSocket 帧显示为二进制需要右键选择 “Decode As” → WebSocket才能看到文本 payload。如果 payload 是 JSON 数组直接就能读。注意如果桩端用的是 wssTLStcpdump 抓到的是加密流量需要在 Wireshark 里配置服务器私钥才能解密。测试环境建议先用 ws省去证书配置的麻烦。3.2 用 Python websockets 库模拟桩端发送验证消息格式最直接的办法是自己写一个模拟桩端往后台发消息看后台怎么回。Python 的 websockets 库足够轻量import asyncio import json import websockets async def send_boot(): uri ws://backend.example.com/ocpp/CP001 async with websockets.connect(uri, subprotocols[ocpp1.6]) as ws: # 构造 BootNotification 消息 msg [ 2, 2001, BootNotification, { chargePointVendor: TestVendor, chargePointModel: TestModel } ] await ws.send(json.dumps(msg)) resp await ws.recv() print(后台响应:, resp) asyncio.run(send_boot())关键参数说明subprotocols 必须指定为 ocpp1.6否则后台可能拒绝握手。URI 里的 CP001 是充电桩标识后台用它来区分不同桩。消息 ID 用 2001实际项目中建议用递增计数器。发送后等待响应响应格式是 [3, 2001, {…}]其中第三个元素是后台返回的配置比如 heartbeatInterval。3.3 用 jsonschema 做消息格式的自动化校验手工比对字段容易漏尤其是 MeterValues 这种嵌套多层的数据。用 jsonschema 定义每个动作的 schema每次收到消息自动校验from jsonschema import validate, ValidationError # 定义 StartTransaction 的 payload schema start_tx_schema { type: object, required: [connectorId, idTag, meterStart, timestamp], properties: { connectorId: {type: integer, minimum: 1}, idTag: {type: string, maxLength: 20}, meterStart: {type: integer, minimum: 0}, timestamp: {type: string, format: date-time} } } payload { connectorId: 1, idTag: ABC123, meterStart: 0, timestamp: 2024-01-15T10:00:00Z } try: validate(instancepayload, schemastart_tx_schema) print(校验通过) except ValidationError as e: print(f校验失败: {e.message})这个 schema 里connectorId 最小值设为 1因为 0 是桩本身StartTransaction 不应该用 0。idTag 最大长度 20是 OCPP1.6 的规定。timestamp 用 date-time 格式确保带时区。实际使用时把每个动作的 schema 存成单独文件收到消息后根据 action 名加载对应 schema就能批量校验。4. 避坑排查OCPP1.6 JSON 消息的五个高频翻车点4.1 消息 ID 不匹配导致响应被丢弃现象桩端发了 StartTransaction后台也回了 CALLRESULT但桩端日志显示“未找到对应请求”交易无法启动。原因桩端用消息 ID 做键来匹配请求和响应但发送时用了字符串 1001接收时却按整数 1001 去查类型不一致导致匹配失败。解决统一消息 ID 的类型。建议全部用字符串发送前str(msg_id)接收后也按字符串比较。在代码里加一层断言确保发送和接收的 ID 类型一致。4.2 meterStart 单位混淆导致计费异常现象后台显示的起始电量是 0.05 kWh但实际电表读数是 50 Wh差了 1000 倍。原因OCPP1.6 规定 meterStart 和 meterStop 的单位是 Wh但有些电表直接输出 kWh开发人员没换算就塞进去了。解决在桩端做一次单位转换meterStart_wh int(meter_kwh * 1000)。同时在后台加校验如果 meterStart 小于 1 且交易时长超过 1 分钟触发告警。4.3 StatusNotification 的 connectorId 从 0 还是 1 开始现象后台显示“枪 0 故障”但现场只有一把枪运维人员找不到对应的物理接口。原因OCPP1.6 里 connectorId0 表示充电桩本身connectorId1 才是第一个充电接口。桩端把桩体状态和枪状态混在一起上报了。解决桩体状态用 connectorId0枪状态从 1 开始。在 StatusNotification 的 payload 里connectorId 和 status 必须对应。如果桩有两把枪connectorId 就是 1 和 2。4.4 timestamp 格式不带时区被后台拒绝现象后台返回 CALLERROR错误码 FormationViolation提示 timestamp 格式错误。原因桩端用了2024-01-15 10:00:00这种不带 T 和时区的格式而 OCPP1.6 要求 ISO 8601 格式必须带 T 和 Z 或时区偏移。解决统一用datetime.utcnow().isoformat() Z生成时间戳。如果桩端有本地时间先转成 UTC 再加 Z。不要用strftime手动拼容易漏掉 T。4.5 MeterValues 的 sampledValue 数组为空现象后台收到 MeterValues 消息但解析后没有电量数据计费模块拿不到读数。原因sampledValue 是数组里面每个元素有 value、measurand、unit 等字段。桩端只填了 value没填 measurand后台按 measurand 过滤时全部丢弃。解决每个 sampledValue 至少填 value、measurand、unit 三个字段。measurand 常用值有 Energy.Active.Import.Register有功电能、Power.Active.Import有功功率。unit 用 Wh 或 W。如果一次上报多个值数组里放多个对象。5. 进阶技巧用消息模板和回归测试守住格式一致性5.1 把高频消息做成模板减少手写错误项目跑起来之后最怕的是不同开发人员手写 JSON 时字段名拼错、类型写错。我的做法是把每个动作的 payload 做成模板函数调用时只传业务参数def build_start_transaction(connector_id, id_tag, meter_start, timestamp): return [ 2, str(next_msg_id()), StartTransaction, { connectorId: connector_id, idTag: id_tag, meterStart: meter_start, timestamp: timestamp } ] def build_meter_values(connector_id, transaction_id, energy_wh): return [ 2, str(next_msg_id()), MeterValues, { connectorId: connector_id, transactionId: transaction_id, meterValue: [ { timestamp: datetime.utcnow().isoformat() Z, sampledValue: [ { value: str(energy_wh), measurand: Energy.Active.Import.Register, unit: Wh } ] } ] } ]模板函数的好处是字段名和类型在函数内部固定调用方只关心业务值。next_msg_id 用一个全局计数器保证唯一性。MeterValues 里的 value 必须是字符串这是 OCPP1.6 的硬性规定模板里直接 str() 转换避免调用方传数字。5.2 用回归测试覆盖消息格式变更每次协议版本升级或后台字段调整都要跑一遍回归测试。我一般用 pytest 写一组测试用例把每个动作的模板输出和预期 JSON 做比对import pytest from ocpp_templates import build_start_transaction def test_start_transaction_format(): msg build_start_transaction(1, ABC123, 0, 2024-01-15T10:00:00Z) assert msg[0] 2 assert msg[2] StartTransaction assert isinstance(msg[3][connectorId], int) assert isinstance(msg[3][meterStart], int) assert msg[3][timestamp].endswith(Z) def test_meter_values_value_is_string(): msg build_meter_values(1, 100, 500) sampled msg[3][meterValue][0][sampledValue][0] assert isinstance(sampled[value], str) assert sampled[unit] Wh这些测试跑一遍不到一秒但能挡住大部分低级错误。尤其是 value 必须是字符串这条手写时很容易写成数字测试一跑就暴露。5.3 用日志回放做现场问题复现现场出问题时桩端日志里通常存了原始 JSON。把这些日志导出来用脚本回放到测试后台就能复现问题。我习惯在日志里给每条消息加一个方向标记SEND/RECV和时间戳回放时按时间顺序重发# 从日志提取所有 SEND 消息按行回放 grep SEND ocpp.log | sed s/.*SEND // | while read line; do echo $line | websocat ws://test-backend/ocpp/CP001 sleep 0.5 donewebsocat 是一个命令行 WebSocket 客户端适合做这种快速回放。sleep 0.5 是给后台留处理时间避免消息堆积。回放过程中观察后台响应如果某条消息触发了 CALLERROR就能定位到具体字段。从那以后我每次拿到新的 OCPP 消息样本都强制走一遍“模板生成 → schema 校验 → 回归测试 → 日志回放”这四步再往业务代码里合。这套流程帮我挡掉了至少三次字段类型错误和两次单位换算问题。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑