OCPP 1.6J通信实战:WebSocket消息帧、事务链路与联调避坑

发布时间:2026/11/3 4:00:30
OCPP 1.6J通信实战:WebSocket消息帧、事务链路与联调避坑 简介面向充电桩、中央管理系统开发及充电网络运维人员的OCPP 1.6J协议学习资料围绕开放充电点协议1.6第2版最终版2017-09-28展开适合需要理解充电设备与后台通信规范的初中级开发者及测试人员。压缩包仅含1个PDF文件大小约3.84MB内容为协议文档整理便于集中查阅版本说明、术语定义、缩略语、参考文献和章节结构。已有879人学习说明其在充电桩协议入门与资料检索中具有一定参考价值。文档覆盖功能配置文件、操作一般视图、本地授权与离线行为、事务相关消息、连接器编号和ID Tokens等目录要点并延伸到远程控制、状态报告、计费支付、安全通信与软件更新等应用场景读者可据此建立协议消息交换和实施的总体框架并对照硬件兼容性、网络稳定性、数据安全及扩展性等实施挑战查漏补缺。1. 刚把充电桩连上后台第一帧该发什么WebSocket 握手成功、日志里刷出一行connected然后就没动静了——这是不少刚接触 OCPP 1.6J 的工程师遇到的第一个卡点。连接建立只是把通道打通真正要说什么、按什么顺序说、对方等的是哪个字段全靠协议文档。《OCPP1.6J学习质料》收的就是 OCPP 1.6 第 2 版最终版2017-09-28 定稿的完整文档目录从术语约定、功能配置文件一路排到第 6 章逐条消息定义充电点侧 10 类上报、中央系统侧 19 条下发全在里面。它适合三类人写桩端固件的、搭 CSMS 后台的、给充电运营平台做中间层对接的。抓包抓到一串[2,...,Authorize,{...}]却不知道下一步查哪一页的时候这份资料就是那本可以随时翻的字典。2. WebSocket 之下跑 JSONCALL、CALLRESULT、CALLERROR 三种帧怎么对应2.1 1.6J 里的 J 指什么新项目为什么都选它OCPP 1.6 有两个平行的传输绑定一个是 SOAP over HTTPXML 报文请求-响应模式另一个是 JSON over WebSocket也就是常说的 1.6JJ 就是 JSON。同样的 action 名、同样的字段语义换了一套序列化和传输层行为差别却不小。SOAP 那一侧是短连接中央系统想把一条 RemoteStartTransaction 送到桩上只能等桩下一次轮询或者下一次 HTTP 请求顺手带过去WebSocket 是全双工长连接后台随时能推。做过充电运营的人都知道从用户扫码到插枪启动只有几秒后台能不能立刻下发指令直接决定这次充电能不能成。所以新项目基本不会选 SOAP 分支。但反过来如果现场对接的是一批老桩、只支持 SOAP那这份文档第 6 章的.req/.conf元素结构表正好是给你查 XML 节点用的。同一份资料两种用法取决于你对面的设备是哪一代。选型定下来之后剩下要弄清楚的其实就三件事连接怎么建、帧怎么排、messageId 怎么对上号。这三件事弄明白后面所有业务动作都只是往 payload 里填字段。2.2 三种帧的数组结构和 messageId 的匹配规则OCPP-J 的所有消息都是一个 JSON 数组第一个元素是消息类型编号这个设计非常省事解析的时候先看frame[0]就能分流。// 充电点 - 中央系统CALL发起一个操作 [2, a1b2c3, BootNotification, {chargePointVendor: VendorX, chargePointModel: ModelY}] // 中央系统 - 充电点CALLRESULT对上面那条的应答 [3, a1b2c3, {status: Accepted, currentTime: 2024-05-01T08:00:00Z, interval: 300}] // 中央系统 - 充电点CALLERROR处理失败 [4, a1b2c3, NotImplemented, action not supported, {}]messageId是个字符串规范建议不超过 36 个字符实际用 UUID 或自增序号都行关键是唯一。它唯一的用途是把应答和请求对应起来本身不携带任何业务含义。真正容易出错的是超时重发中央系统等不到 CALLRESULT 会换一个新的 messageId 重发同一条指令桩端如果只看 action 名不看 messageId就会把同一条 RemoteStartTransaction 执行两次继电器啪嗒两下。CALLERROR 的errorCode是闭集常见的有NotImplemented、NotSupported、FormationViolation、ProtocolError、InternalError、SecurityError、GenericError。多数情况填NotImplemented就够了FormationViolation留给报文结构都解析不了的情况比如某个必填字段缺失。注意errorDetails虽然是可选但规范要求这个位置必须占住填个空对象{}比省略更稳妥。2.3 握手阶段的三个参数子协议、URL 末段、超时连接建不起来或者一连上就被踢八成是这三个参数之一没对上。子协议必须是ocpp1.6写在 WebSocket 握手的Sec-WebSocket-Protocol头里。有些服务端框架默认不校验子协议桩端发什么它都收本地联调一路顺风一上生产换了个网关就被拒。我的习惯是服务端也强制校验一次对不上直接 400。ChargePointId 放在 URL 最后一段形如ws://host:port/ocpp/CP001。这个 ID 就是后台识别桩的身份必须和 BootNotification 里上报的充电点信息属于同一台设备。见过有人在 URL 里写 MAC 地址、BootNotification 里写序列号的后台建了两条设备档案计费数据全裂开。约定好一个唯一标识两处统一。超时参数ConnectionTimeOut是配置项默认一般取 30 秒指的是发出 CALL 之后等 CALLRESULT 的最长时间。这个值不要设得太小桩端在做固件校验、写本地存储的时候确实会慢也不要设太大超过一分钟用户在桩前已经走开了。2.4 一个能接收报文的最小服务端联调初期服务端能把报文原样打出来比什么都重要。下面这段用 Python 的websockets库搭一个最小接收端只处理 BootNotification 和 Heartbeat其余动作一律回 CALLERROR用来确认桩端能不能正常连上、帧格式对不对。import asyncio import json import logging import websockets logging.basicConfig(levellogging.INFO, format%(asctime)s %(message)s) async def handle(ws, path): # path 形如 /ocpp/CP001末段即 ChargePointId cp_id path.rsplit(/, 1)[-1] logging.info(connected: %s, subprotocol%s, cp_id, ws.subprotocol) try: async for raw in ws: frame json.loads(raw) msg_type frame[0] if msg_type 2: # CALL桩端发起 _, msg_id, action, payload frame logging.info( %s %s, action, payload) if action BootNotification: # 先回 conf业务异步推进别在做完事之后再回 await ws.send(json.dumps([3, msg_id, { status: Accepted, currentTime: 2024-05-01T08:00:00Z, interval: 300 # 心跳间隔秒 }])) elif action Heartbeat: await ws.send(json.dumps([3, msg_id, {currentTime: 2024-05-01T08:00:00Z}])) else: await ws.send(json.dumps([4, msg_id, NotImplemented, action, {}])) elif msg_type 3: # CALLRESULT我们对下发指令的响应 logging.info( result %s %s, frame[1], frame[2]) elif msg_type 4: # CALLERROR logging.warning( error %s %s %s, frame[1], frame[2], frame[3]) except websockets.ConnectionClosed: logging.info(closed: %s, cp_id) async def main(): async with websockets.serve(handle, 0.0.0.0, 9000, subprotocols[ocpp1.6]): await asyncio.Future() asyncio.run(main())这段代码有三个地方值得说一下。websockets.serve的subprotocols参数必须显式声明不声明的话握手阶段服务端不会回Sec-WebSocket-Protocol头严格的桩端会判定协商失败并主动断开handle的第二个参数path在 websockets 12 之前是必填的12 之后改成了ws.request.path升级库版本的时候这里最容易翻车BootNotification返回的interval决定桩端后续心跳周期联调阶段可以先设成 300等确定连接稳定了再调到 900 甚至更大。3. 充电点侧 10 类上报从上电到拔枪的完整消息链3.1 上电到就绪BootNotification 与 Heartbeat 的取值依据桩端上电后的第一条 CALL 一定是 BootNotification这不是约定俗成是规范强制。payload 里chargePointVendor和chargePointModel是两个必填字段后台通常拿它们做设备型号匹配从而决定下发哪套配置。chargePointSerialNumber、firmwareVersion、iccid、imsi、meterType、meterSerialNumber都是可选的但建议能填都填运维排查的时候这几个字段价值极高。中央系统回的status有三种Accepted、Pending、Rejected。Accepted表示正常Pending表示后台还在做设备注册或人工审核桩端不能进入可充电状态要隔一段时间重发 BootNotificationRejected表示这台设备不允许接入桩端应该停止重试并且本地告警。很多项目实现里只处理Accepted另外两个当成异常吞掉了结果就是审核中的桩一直在那儿闪灯现场没人知道发生了什么。Heartbeat是维持连接和上报存活的消息payload 里只有一个可选的currentTime。它的周期由 BootNotification 应答里的interval决定也可以用配置项HeartbeatInterval覆盖。这个值太小会给后台制造无谓压力几百台桩同时 10 秒一次心跳消息队列很快就吃满太大又会导致掉线检测迟钝。经验值是 300 秒起步桩数量过千可以放到 900 秒同时靠 WebSocket 层的 Ping 保活。3.2 插枪到拔枪一串消息的先后顺序这条链是整个协议里最核心的部分顺序错了后台的实时状态就全乱。一条典型的交流桩充电流程是这样的// 1. 桩空闲连接器状态上报 Available [2, m1, StatusNotification, {connectorId: 1, errorCode: NoError, status: Available}] // 2. 用户插枪状态转 Preparing [2, m2, StatusNotification, {connectorId: 1, errorCode: NoError, status: Preparing}] // 3. 刷卡发起授权 [2, m3, Authorize, {idTag: 04A1B2C3D4}] [3, m3, {idTagInfo: {status: Accepted}}] // 4. 启动交易meterStart 单位是 Wh [2, m4, StartTransaction, {connectorId: 1, idTag: 04A1B2C3D4, meterStart: 0, timestamp: 2024-05-01T08:01:00Z}] [3, m4, {transactionId: 10086, idTagInfo: {status: Accepted}}] // 5. 状态转 Charging之后按 MeterValueSampleInterval 周期上报 [2, m5, StatusNotification, {connectorId: 1, errorCode: NoError, status: Charging}] // 6. 拔枪停止交易meterStop 是结束时的累计读数 [2, m6, StopTransaction, {transactionId: 10086, meterStop: 3420, timestamp: 2024-05-01T09:00:00Z, reason: EVDisconnected}] [3, m6, {idTagInfo: {status: Accepted}}]顺序上有两个容易被忽略的细节。一是StatusNotification的Preparing必须在Authorize之前因为有些后台是拿这个状态判断枪已经插到位如果先授权再报状态用户在 App 上看到的是已授权但未连接。二是StartTransaction的应答里transactionId由后台分配桩端拿到之后必须存下来后面所有的MeterValues和最终的StopTransaction都要带上它。还有一个高频误判StartTransaction.conf里如果idTagInfo.status返回ConcurrentTx意思是这张卡已经在另一个连接器上有一笔进行中的交易。规范建议桩端在收到这个值时仍然建立本地交易并开始充电只是这笔交易后续可能被后台拒绝结算。不少实现直接当成失败处理用户插着枪充不了电投诉电话就来了。3.3 事务边界meterStart、meterStop 和离线补发电表读数的单位是 Wh不是 kWh。这个单位搞错账单差三个数量级而且后台很难发现——数字看起来就是对的只是小数点位置不对。meterStart是交易开始那一刻的累计电能读数meterStop是结束时的累计读数meterStop - meterStart就是本次充电量后台会拿它和周期上报的MeterValues累加值做交叉校验对不上会标记异常。StopTransaction的reason是枚举值规范列了十来个EmergencyStop、EVDisconnected、HardReset、Local、Other、PowerLoss、Reboot、Remote、SoftReset、UnlockCommand、DeAuthorized。全填Other是省事但运营侧做充电失败归因的时候就没有任何信息了只能一条条翻日志。硬拔枪就是EVDisconnected后台远程停就是Remote掉电重启就是PowerLoss或Reboot按实际填并不费事。离线交易是另一个必须提前设计的场景。网络断了桩端不能让用户干等常见做法是本地缓存授权名单LocalAuthorizeOffline配成 trueAllowOfflineTxForUnknownId按业务决定离线启动交易时先在本地编号记账把StartTransaction压进本地队列。网络恢复后按时间顺序补发拿到后台真实transactionId之后再把后续积压的MeterValues挂上去。这里最怕的是补发乱序StopTransaction先于StartTransaction到达后台交易记录就永远关闭不了。3.4 MeterValues 的字段怎么填MeterValues是流量最大的消息字段组合也比较多。一条报文里meterValue是数组每个元素带一个timestamp和一组sampledValuesampledValue里value是字符串形式的值其余全是描述这个值的元数据。字段常见取值说明measurandEnergy.Active.Import.Register累计正向有功电能计费主依据measurandPower.Active.Import瞬时功率画曲线用measurandCurrent.Import分相电流需配 phasemeasurandVoltage电压判过压欠压measurandTemperature枪温或模块温过热保护measurandSoC车辆电量百分比需要车端支持unitWh、W、A、V、Celsius、Percentmeasurand 为 Energy 时用 WhcontextSample.Periodic、Sample.Clock周期采样、整点对齐contextTransaction.Begin、Transaction.End交易开始/结束那一个点contextInterruption.Begin、Interruption.End充电中断前后phaseL1、L2、L3、L1-N、L2-N、L3-N三相桩必须带否则后台分不清哪一相locationBody、Cable、EV、Inlet、Outlet采样点位置配置项MeterValuesSampledData决定周期上报里包含哪些 measurandMeterValuesAlignedData决定整点对齐上报包含哪些StopTxnSampledData决定交易结束时补发哪些。三个配置分开是为了让后台能按不同频率拿到不同粒度数据。默认值往往只包含Energy.Active.Import.Register想画功率曲线的话必须用ChangeConfiguration改这一点在第 4 章还会提到。connectorId在MeterValues里填 0 表示整个充电点的汇总数据填 1 到 N 表示具体某把枪。汇总和分枪数据同时上报是允许的但后台要能区分不要让汇总数据覆盖了分枪的电量。4. 中央系统侧 19 条下发指令哪些关键操作只能远程完成哪些要本地保底4.1 控制类远程启停和解锁RemoteStartTransaction的 payload 只有idTag、可选的connectorId和可选的chargingProfile。它是一条请求不是命令桩端回Accepted只代表收到了实际能不能启动还得看本地条件枪插好了没、有没有故障、是不是在预约占用中。如果connectorId不填桩端自己挑一个空闲的连接器。一个常见的错误理解是后台发了RemoteStartTransaction用户就能充上电。实际上用户还得把枪插上桩端要本地校验通过才会真的合闸。设计界面的时候App 上应该显示指令已下发请插枪而不是正在充电。RemoteStopTransaction只需要transactionId没有连接器编号。这意味着桩端必须维护一张 transactionId 到 connectorId 的映射表而且这张表要能跨重启存活。表丢了后台的远程停止就永远落不到具体某把枪上。UnlockConnector的语义是电子锁解锁用来应对用户拔不出枪的情况。规范说得很清楚如果连接器正在充电桩端应该先停止交易再解锁。实现的时候建议加一道本地判断充电电流没降到安全阈值之前不要给解锁信号机械锁带着大电流弹开是要出事的。4.2 配置类ChangeConfiguration、GetConfiguration 与本地授权列表ChangeConfiguration是运维阶段用得最多的一条。payload 是key和value两个字符串应答有四种状态Accepted、Rejected、RebootRequired、NotSupported。RebootRequired表示配置改了但需要重启才生效桩端应该记住这个标记并在合适的时机提示。NotSupported表示这个 key 桩端压根不实现后台不要反复重试。下面这张表是实际项目里最常改的几个 key值都是字符串形式布尔值写true而不是true。key典型值作用HeartbeatInterval300心跳间隔秒ConnectionTimeOut30等待 CALLRESULT 的超时秒MeterValueSampleInterval60周期采样上报间隔秒ClockAlignedDataInterval900整点对齐上报间隔秒MeterValuesSampledDataEnergy.Active.Import.Register,Power.Active.Import周期上报包含的 measurand 列表逗号分隔StopTxnSampledDataEnergy.Active.Import.Register交易结束补发的 measurandWebSocketPingInterval30WS 层 Ping 间隔0 表示关闭LocalAuthorizeOfflinetrue离线时是否允许本地授权LocalPreAuthorizefalse连接建立时是否先用缓存授权AllowOfflineTxForUnknownIdfalse未知 idTag 是否允许离线交易AuthorizationCacheEnabledtrue是否启用授权缓存SupportedFeatureProfilesCore,FirmwareManagement,LocalAuthListManagement,Reservation,SmartCharging,RemoteTrigger支持的配置文件只读ChargeProfileMaxStackLevel3支持的充电曲线最大堆叠层级NumberOfConnectors2连接器数量只读GetConfiguration是它的反向操作key数组可选不填就返回全部。应答里除了configurationKey数组还有一个unknownKey数组用来告诉后台哪些 key 桩端不认识。这个字段经常被两边都忽略导致后台以为配置下发成功其实桩端根本没这个能力。SendLocalList用于下发本地授权名单核心字段是listVersion、updateType和localAuthorizationList。updateType为Full是全量替换Differential是增量更新。版本号机制要理解清楚后台发的listVersion必须大于桩端当前的版本才会被接受桩端通过GetLocalListVersion上报当前版本。网络不稳定的时候增量更新可能丢包全量替换反而是更安全的选择。ClearCache清的是授权缓存不是本地授权名单这两个别搞混。前者是运行时缓存后者是持久化的名单文件。4.3 智能充电ChargingProfile 的三层堆叠SetChargingProfile是整个 1.6J 里最复杂的一条也是真正拉开产品差距的地方。一条ChargingProfile包含这些关键字段chargingProfileId曲线编号同一个 ID 再次下发是覆盖transactionId可选绑定到具体交易stackLevel堆叠层级数字越大优先级越高chargingProfilePurposeChargePointMaxProfile、TxDefaultProfile、TxProfile三选一chargingProfileKindAbsolute、Recurring、RelativechargingSchedule包含startSchedule、duration、chargingRateUnitA 或 W、minChargingRate和chargingSchedulePeriod数组chargingSchedulePeriod里每个元素是startPeriod相对startSchedule的秒数和limit该时段的功率或电流上限三相桩还要带numberPhases。举一个最简单的例子从 0 秒到 3600 秒限 16A3600 秒之后限 8AchargingSchedulePeriod就是[{startPeriod: 0, limit: 16}, {startPeriod: 3600, limit: 8}]。三个 purpose 的关系是叠加不是替换。ChargePointMaxProfile管的是整台桩的总功率上限TxDefaultProfile是新交易的默认曲线TxProfile是绑定到某一笔交易的专用曲线。同一个 purpose 下stackLevel高的生效不同 purpose 之间取交集。最终实际执行的限值可以通过GetCompositeSchedule让桩端算出来返回。ClearChargingProfile用来清除可以按id、connectorId、chargingProfilePurpose、stackLevel组合过滤。过滤条件不填就是全清这个操作要谨慎把ChargePointMaxProfile清掉可能导致整站超容。4.4 运维类复位、固件、诊断和触发Reset的type只有Soft和Hard两个值。Soft是优雅重启等当前交易结束Hard是立刻重启正在充电的交易会中断。后台下发Hard之前应该先确认没有活跃交易否则用户那边就是充到一半突然断电。UpdateFirmware给的是location固件下载地址、retrieveDate最早开始下载的时间、retries和retryInterval。整个过程是异步的桩端下载、校验、安装每个阶段通过FirmwareStatusNotification上报状态取值包括Downloading、Downloaded、Installing、Installed、DownloadFailed、InstallationFailed。后台如果只看Installed中间失败了完全不知道固件版本永远升不上去。GetDiagnostics让桩端把日志上传到指定地址字段和固件更新类似。桩端回Accepted之后开始上传通过DiagnosticsStatusNotification上报Idle、Uploaded、UploadFailed。TriggerMessage是联调阶段最有价值的一条。payload 是requestedMessage和可选的connectorId桩端收到后主动发出指定的那条 CALL。可以触发的消息有BootNotification、Heartbeat、MeterValues、StatusNotification、DiagnosticsStatusNotification、FirmwareStatusNotification。想让桩立刻上报一次状态不用等下一个周期直接触发就行。DataTransfer是留给厂商自定义扩展的通道vendorId加上任意data字符串后台原样回。做私有功能的时候走这条不要往标准消息里塞非标字段否则换一家后台就不认了。这些下发指令有一个共同的处理原则先在ConnectionTimeOut之内回 CALLRESULT再异步去做真正的事情。回Accepted的含义是这个请求我接受并会处理不是事情已经做完了。把这两件事混在一起后台侧看到的就是大面积超时重发。5. 联调避坑五条具体故障记录每条都能帮你省下半天5.1 报文层字段名、超时和心跳坑一拿 1.6 主文档当 JSON 字段表用。现象按文档第 6 章Authorize.req的字段名写 JSON桩端发出去后台回 CALLERRORFormationViolation或者字段被当成未知字段静默丢弃。 原因这份 2017-09-28 定稿的 1.6 文档第 6 章描述的是 SOAP 绑定的.req/.conf元素结构元素名首字母大写类型是 XML Schema 那一套。JSON 绑定的字段定义在配套的 OCPP-J 文档里两者语义一致但类型表述和枚举大小写有差异。 解决把主文档第 6 章当成这个 action 有哪些字段的索引来查具体类型、必填性、枚举大小写去 JSON 绑定文档核对。枚举值全部抄成常量表Accepted和accepted在 JSON 里是两个东西。坑二先处理业务再回 CALLRESULT。现象后台日志显示RemoteStartTransaction超时连发三次桩端实际执行了一次但用户看到 App 上转了半分钟圈。 原因桩端收到 CALL 之后先去做开继电器、等鉴权、写数据库这一串事几十秒后才回 CALLRESULT。后台的ConnectionTimeOut一般配 30 秒超时就换新 messageId 重发。 解决收到 CALL 立刻回 CALLRESULT业务处理放到后台任务里。需要告诉后台最终结果的话用StatusNotification或者对应的通知类消息补上不要憋在 CALLRESULT 里。坑三Heartbeat 代替不了 WebSocket Ping。现象连接空闲几分钟后莫名其妙断开桩端日志里没有任何异常后台显示离线过一会儿又自动连上。 原因Heartbeat是 OCPP 应用层消息和业务报文走同一根连接WebSocket Ping 是协议层控制帧。中间有网关、负载均衡、NAT 设备的时候有些设备只认 Pong 帧不认应用层心跳空闲超时就把连接掐了。 解决WebSocketPingInterval配 30 秒左右HeartbeatInterval配 300 秒两个都开。桩端还要处理 1006 异常关闭后的重连退避别用固定 1 秒死循环重连。5.2 业务层事务标识和枚举语义坑四transactionId 只放在内存里。现象桩重启或者断网重连之后后台的实时交易列表里多出一条永远不结束的交易月底对账时电量差一截。 原因transactionId由后台在StartTransaction.conf里分配桩端如果只存在内存变量里重启后就丢了后续StopTransaction要么填了个默认值要么根本发不出去。 解决交易开始和结束都写本地持久化存储键就是 connectorId。离线启动的交易用本地编号占位上线后按顺序补发StartTransaction拿到后台真实 ID 再回填到积压的MeterValues和StopTransaction上。补发队列一定要按时间排序乱序的后果比丢失更难排查。坑五connectorId 填 0 和枚举值随手写。现象后台界面上某台桩一直显示充电中现场看枪早就拔了或者充电失败原因统计里九成都是其他。 原因两个问题叠在一起。一是connectorId填 0 在StatusNotification和MeterValues里表示整个充电点不是某把枪桩端把枪级状态用 0 发出去后台就理解为整桩状态分枪状态永远停在旧值。二是StopTransaction的reason十来个枚举值全填Other后台做归因分析时没有任何可用信息。 解决connectorId用 1 到 N 表示具体连接器0 只留给充电点级别的汇总消息。reason按实际情况填拔枪是EVDisconnected远程停是Remote掉电是PowerLoss多打几个字符的事换来的是可分析的运营数据。6. 不接真桩也能自证用 TriggerMessage 和 GetCompositeSchedule 把后台跑通后台写完不代表能用得有人从桩那一侧发消息过来验证。手上没有真桩的时候我的做法是写一个几十行的模拟客户端把关键消息都打一遍。TriggerMessage是这里最省事的入口。它让桩端主动发一条指定消息正好可以用来逐个验证后台的解析逻辑不用干等心跳周期。import asyncio import json import uuid import websockets async def probe(cp_id, action, connector_id1): 向本地后台发一条 TriggerMessage观察桩端被触发后上报的消息 url fws://127.0.0.1:9000/ocpp/{cp_id} async with websockets.connect(url, subprotocols[ocpp1.6]) as ws: msg_id str(uuid.uuid4()) await ws.send(json.dumps([2, msg_id, TriggerMessage, {requestedMessage: action, connectorId: connector_id}])) while True: frame json.loads(await ws.recv()) if frame[0] in (3, 4) and frame[1] msg_id: print(trigger conf:, frame[2:]) # Accepted / Rejected break if frame[0] 2: # 桩端被触发后主动上报的 CALL await ws.send(json.dumps([3, frame[1], {}])) print(reported:, frame[2], frame[3]) asyncio.run(probe(CP001, MeterValues))requestedMessage可以填BootNotification、Heartbeat、MeterValues、StatusNotification、DiagnosticsStatusNotification、FirmwareStatusNotification。注意TriggerMessage.conf回的status是Accepted、Rejected、NotImplemented三选一Rejected通常意味着当前状态不允许触发这条消息比如没有活跃交易的时候触发MeterValues。智能充电那块光看报文验证不了因为SetChargingProfile下发之后桩端算出来的有效曲线才是真正执行的东西。这时候用GetCompositeSchedule传connectorId、duration秒和chargingRateUnit桩端会把这段时间内的合成曲线返回包含每个区间的startPeriod和limit。我的验证顺序是先下一层ChargePointMaxProfile限 32A再下一层TxDefaultProfile限 16A然后调GetCompositeSchedule看返回的limit是不是 16A再把TxProfile提到 24A 叠上去看返回是不是变成 24A 但被 32A 的总上限卡住。这套组合跑通堆叠逻辑基本就没问题了。顺便提一个联调清单每次换后台或者升固件我都会从头过一遍BootNotification 三种 status 都能正确响应、Heartbeat 间隔和配置项一致、Authorize 的idTagInfo.status五种取值都处理、StartTransaction 的ConcurrentTx不会被当失败、MeterValues 的 unit 和 measurand 配套、StopTransaction 的 reason 有区分、SendLocalList 全量和增量都试、SetChargingProfile 三层堆叠结果正确、UpdateFirmware 六个状态都上报。这套跑完八成的问题在实验室就暴露了。说实话我最早做 OCPP 对接的时候因为meterStart单位填成 kWh整批测试桩的账单全部差了 1000 倍最后还是财务那边做对账发现数字太整齐才回头查的。从那以后我每次接新后台都会先用一条MeterValues手工造一个 1000 Wh 的读数确认后台存进去的确实是 1 kWh 而不是 1000 kWh才敢往下走。希望这些内容能帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询