智能充电(Smart Charging)功能档是 OCPP 1.6 不再是一个简单请求/响应协议的地方。SetChargingProfile 有嵌套结构、三种不同用途、一套堆叠模型,以及一组很容易在细处弄错的配置合并规则。
它也是大多数实现可以一直不管、蒙混过去的那部分规范——直到某个电网运营商要求他们削减负荷,这件事才突然变得紧急。
一份充电配置长什么样
一份充电配置回答的是一个问题:*这台充电桩被允许输出多大电流或功率,以及在什么时候?* 结构里的每一样东西都在为这件事服务。
[2, "msg-01", "SetChargingProfile", {
"connectorId": 1,
"csChargingProfiles": {
"chargingProfileId": 100,
"stackLevel": 0,
"chargingProfilePurpose": "TxDefaultProfile",
"chargingProfileKind": "Absolute",
"chargingSchedule": {
"chargingRateUnit": "A",
"chargingSchedulePeriod": [
{ "startPeriod": 0, "limit": 32.0 },
{ "startPeriod": 21600, "limit": 16.0 },
{ "startPeriod": 64800, "limit": 32.0 }
]
}
}
}]这份配置说的是:一开始 32 A,六小时后降到 16 A,十八小时后回到 32 A。startPeriod 是相对于计划开始时刻的秒数,既不是钟表时间,也不是时长。
三种用途互不可换
chargingProfilePurpose 决定这份配置作用在什么东西上,大部分混乱都从这里开始。
ChargePointMaxProfile 是整台充电桩的物理上限。它设在 connectorId: 0 上,因为它描述的是站点而不是某个插口。任何东西都不能超过它——那是硬件以及它背后供电能力的极限。
TxDefaultProfile 是交易的默认配置。可以设在某个具体接口上,也可以设在 connectorId: 0 上,让所有接口共用同一个默认值。它作用于那些自身没有配置的交易,包括将来的交易。
TxProfile 作用于一笔具体的、正在进行的交易。它需要 transactionId,并且在那笔交易结束时被丢弃。要削减一次正在进行的会话,用的就是它。
实际后果是:给一笔已经停止的交易发 TxProfile,不会产生你会注意到的错误——充电桩收下它,然后扔掉。
堆叠不是相加
stackLevel 是一个整数,数值大的胜出。这是人们最常读错的一条规则。
处于不同堆叠层级的配置不会相加、取平均或混合。在同一种用途之内,层级最高的那个有效配置会完整替换它下面的所有配置。stackLevel 2 的配置不是在 stackLevel 1 之上再加一道约束,而是取代它。
跨用途之间的叠加是另一回事,那一层确实表现为上限:实际生效的限值,是适用的 ChargePointMaxProfile、胜出的 TxDefaultProfile 和胜出的 TxProfile 三者中最低的那个。所以用途之间互相设上限,而同一用途内的堆叠层级之间互相覆盖。
一个常见的部署套路:
- stackLevel 0 —— TxDefaultProfile,日常运行限值
- stackLevel 1 —— TxDefaultProfile,由电价驱动的夜间计划
- stackLevel 2 —— TxProfile,运营方针对某一次会话的削减
把层级 2 的配置移除,层级 1 的计划就恢复。没有任何东西需要把它替换掉的内容再重述一遍。
Absolute、Recurring、Relative
chargingProfileKind 决定 startPeriod: 0 是什么意思。
Absolute 把计划锚定在 startSchedule 这个显式时间戳上。用于一次性的窗口:今晚 18:00 到 20:00 之间削减。
Recurring 按天或按周重复,由 recurrencyKind 控制。startSchedule 设定相位——也就是周期从每天的哪个时刻开始。用于电价场景。
Relative 把 startPeriod: 0 锚定在交易开始的那一刻,并且完全忽略 startSchedule。用于取决于会话已进行多久、而不是取决于一天中时刻的行为:第一个小时满电流,之后降下来。
一个常犯的错误是发 Absolute 却不带 startSchedule。规范要求 Absolute 和 Recurring 都必须带它,而各家充电桩的做法不一:有的拒掉这份配置,有的悄悄把它当成 Relative。
安培还是瓦特,以及为什么这要紧
chargingRateUnit 取 A 或 W,而充电桩并不被要求同时支持两者。对 ChargingScheduleAllowedChargingRateUnit 做一次 GetConfiguration,就能知道哪些可用。
交流充电下,换算取决于相数,而配置本身并不携带相数。单相 230 V 上的 32 A 大约是 7.4 kW;同样的 32 A 在三相上大约是 22 kW。如果你的后端按瓦特思考而充电桩按安培工作,那么关于相数的假设就藏在你代码的某个地方,而它必须是显式的。
计划时段里的 numberPhases 在省略时默认为 3。在单相安装上这个默认值是错的,由它算出来的限值会高出三倍。
核实充电桩实际做了什么
SetChargingProfile 会回 Accepted、Rejected 或 NotSupported。Accepted 的意思是充电桩把这份配置存下来了——不代表它当前生效,也不代表它产生了你想要的限值。
要看所有配置合并之后的结果,用 GetCompositeSchedule:
[2, "msg-02", "GetCompositeSchedule", {
"connectorId": 1,
"duration": 86400,
"chargingRateUnit": "A"
}]充电桩会返回它实际会遵循的那份扁平化计划,所有配置都已经解算完毕。这是检验你堆叠逻辑唯一可靠的办法,也是当一次削减看起来没生效时第一个该伸手去拿的工具。
如果复合计划和你预期的不一样,常见原因有三个:堆叠层级低于某个已经装上去的配置;TxProfile 的 transactionId 和正在进行的交易对不上;或者有一个 ChargePointMaxProfile 在上面压着你。
清除配置
ClearChargingProfile 是按过滤条件删除,不是只按 ID 删除。每个字段都是可选的,而全部省略就会清掉所有东西:
[2, "msg-03", "ClearChargingProfile", {
"connectorId": 1,
"chargingProfilePurpose": "TxProfile"
}]这会清掉 1 号接口上的 TxProfile,而默认配置保持不动。发一个 {} 会清掉这台充电桩上的每一份配置——偶尔这正是你想要的,更多时候它是一次故障。
怎么测
智能充电很难拿真实硬件来测,因为有意思的情况都跟时间有关——一个在跨天边界上出问题的周期性配置、一次在会话中途到达的削减、一份只有在三个配置叠起来之后才算错的复合计划。
这正是值得放到仿真里去跑的那类逻辑。SimPilot 可以把一个会话一直挂着,让你对着它安装、堆叠和清除配置;而 TestPilot 则按规范检查一台充电桩自身对智能充电功能档的处理。完整的消息集记录在我们的 OCPP 1.6 参考文档里。