簡(jiǎn)電云 | Java + WebSocket 實(shí)現(xiàn) OCPP 2.0.1 充電樁模擬程序-人妻熟女一區(qū)二區(qū)三區(qū)APP下載-國產(chǎn)精品一區(qū)二區(qū)資源
- 報(bào)價(jià)
- 請(qǐng)來電詢價(jià)
- 產(chǎn)品咨詢熱線
- 15920323068
- 微信號(hào)
- 15920323068
- 軟件開發(fā)
- 小程序+公眾號(hào)+APP定制開發(fā)
充電平臺(tái)通常需要同時(shí)處理設(shè)備接入、訂單、計(jì)費(fèi)、告警和遠(yuǎn)程控制。如果所有測(cè)試都依賴真實(shí)充電樁,會(huì)遇到幾個(gè)現(xiàn)實(shí)問題:
設(shè)備數(shù)量有限,難以驗(yàn)證大量設(shè)備同時(shí)在線;
插槍、刷卡、充電和拔槍需要人工操作,回歸測(cè)試效率低;
斷網(wǎng)、重復(fù)報(bào)文、事件亂序和異常停止不容易穩(wěn)定復(fù)現(xiàn);
不同廠商固件行為存在差異,平臺(tái)問題與設(shè)備問題容易混在一起;
開發(fā)人員無法隨時(shí)獲得完整的充電現(xiàn)場(chǎng)環(huán)境。
模擬器并不是為了替代真實(shí)設(shè)備,而是把協(xié)議行為變成可重復(fù)、可編排的測(cè)試輸入。開發(fā)階段先用模擬器驗(yàn)證平臺(tái)狀態(tài)機(jī),再使用真實(shí)充電樁完成硬件、計(jì)量和電氣安全測(cè)試,能夠明顯降低聯(lián)調(diào)成本。
本文實(shí)現(xiàn)以下Zui小閉環(huán):
展開
代碼語言:JavaScript
自動(dòng)換行
AI代碼解釋
Java 充電樁模擬器 Java CSMS
| |
|--- WebSocket / ocpp2.0.1 ----->|
|--- BootNotification ---------->|
|<-- Accepted + interval ---------|
|--- StatusNotification -------->|
|--- Authorize ----------------->|
|<-- Accepted --------------------|
|--- TransactionEvent(Started) -->|
|--- MeterValues --------------->|
|--- TransactionEvent(Updated) -->|
|--- TransactionEvent(Ended) ---->|
|--- Heartbeat ----------------->|
OCPP 2.0.1 JSON 通過 WebSocket 傳輸,握手時(shí)使用子協(xié)議ocpp2.0.1。協(xié)議常用三種消息類型:
類型
編號(hào)
數(shù)組結(jié)構(gòu)
CALL
2
[2, messageId, action, payload]
CALLRESULT
3
[3, messageId, payload]
CALLERROR
4
[4, messageId, errorCode, description, details]
例如充電樁上線時(shí)發(fā)送:
展開
代碼語言:JavaScript
自動(dòng)換行
AI代碼解釋
[2,"msg-001","BootNotification",{
"reason":"PowerUp",
"chargingStation":{
"model":"SIM-AC-22K",
"vendorName":"DemoVendor"
}
}]
CSMS 返回:
代碼語言:JavaScript
自動(dòng)換行
AI代碼解釋
[3,"msg-001",{
"currentTime":"2026-09-30T02:00:00Z",
"interval":30,
"status":"Accepted"}]
messageId 用于關(guān)聯(lián)請(qǐng)求和響應(yīng)。一次連接中的CALL 應(yīng)使用唯一標(biāo)識(shí),客戶端還需要維護(hù)“消息編號(hào)—等待結(jié)果”的映射。
本文為了展示 WebSocket 和 OCPP報(bào)文的底層處理過程,不引入商業(yè) OCPP SDK,而是使用 Jackson 構(gòu)造消息。這樣代碼容易理解,但生產(chǎn)項(xiàng)目仍應(yīng)補(bǔ)充完整的OCPP 2.0.1 JSON Schema 校驗(yàn)和功能塊實(shí)現(xiàn)。
三、創(chuàng)建 Maven 工程示例使用 Java 17。工程結(jié)構(gòu)如下:
代碼語言:JavaScript
自動(dòng)換行
AI代碼解釋
ocpp201-java-demo/
├── pom.xml└── src/main/java/com/example/ocpp/
├── CsmsServer.java
├── CsmsEndpoint.java
└── ChargePointSimulator.java
pom.xml 使用 Tyrus 作為 JakartaWebSocket 實(shí)現(xiàn),Jackson 負(fù)責(zé) JSON 解析:
展開
代碼語言:JavaScript
自動(dòng)換行
AI代碼解釋
<?xml version="1.0" encoding="UTF-8"?><projectxmlns="https://maven.apache.org/POM/4.0.0"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>ocpp201-java-demo</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>jakarta.websocket</groupId>
<artifactId>jakarta.websocket-api</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>org.glassfish.tyrus</groupId>
<artifactId>tyrus-server</artifactId>
<version>2.1.5</version>
</dependency>
<dependency>
<groupId>org.glassfish.tyrus</groupId>
<artifactId>tyrus-container-grizzly-server</artifactId>
<version>2.1.5</version>
</dependency>
<dependency>
<groupId>org.glassfish.tyrus.bundles</groupId>
<artifactId>tyrus-standalone-client</artifactId>
<version>2.1.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.3.0</version>
</plugin>
</plugins>
</build></project>
固定依賴版本有利于復(fù)現(xiàn)。Tyrus 不僅提供WebSocket API,還提供可直接啟動(dòng)的服務(wù)端容器和客戶端實(shí)現(xiàn)。
四、實(shí)現(xiàn) CSMS WebSocket服務(wù)端1. 創(chuàng)建 WebSocket端點(diǎn)創(chuàng)建CsmsEndpoint.java:
展開
代碼語言:JavaScript
自動(dòng)換行
AI代碼解釋
package com.example.ocpp;importcom.fasterxml.jackson.databind.JsonNode;importcom.fasterxml.jackson.databind.ObjectMapper;importcom.fasterxml.jackson.databind.node.ArrayNode;importcom.fasterxml.jackson.databind.node.ObjectNode;importjakarta.websocket.CloseReason;importjakarta.websocket.OnClose;import jakarta.websocket.OnError;importjakarta.websocket.OnMessage;import jakarta.websocket.OnOpen;importjakarta.websocket.Session;importjakarta.websocket.server.PathParam;importjakarta.websocket.server.ServerEndpoint;importjava.io.IOException;import java.time.Instant;
@ServerEndpoint(
value = "/ocpp/{chargePointId}",
subprotocols = {"ocpp2.0.1"}
)
public class CsmsEndpoint {
private static final ObjectMapper JSON = newObjectMapper();
@OnOpen
public void onOpen(
Session session,
@PathParam("chargePointId") String chargePointId) throwsIOException {
if(!"ocpp2.0.1".equals(session.getNegotiatedSubprotocol())) {
session.close(newCloseReason(
CloseReason.CloseCodes.PROTOCOL_ERROR,
"OCPP 2.0.1 subprotocol required"));
return;
}
session.getUserProperties().put("chargePointId",chargePointId);
System.out.printf("[Connected] id=%s,session=%s%n",
chargePointId, session.getId());
}
@OnMessage
public void onMessage(String text, Session session)throws IOException {
JsonNode frame =JSON.readTree(text);
if (!frame.isArray() || frame.size()< 3) {
session.getBasicRemote().sendText(callError(
"unknown", "FormationViolation", "Invalid frame"));
return;
}
int messageType =frame.get(0).asInt();
if (messageType != 2 || frame.size() !=4) {
https:// 本示例服務(wù)端只處理充電樁發(fā)起的CALL。
return;
}
String messageId =frame.get(1).asText();
String action =frame.get(2).asText();
JsonNode payload = frame.get(3);
try {
ObjectNode response =handleCall(session, action, payload);
session.getBasicRemote().sendText(callResult(messageId,response));
} catch (IllegalArgumentException e){
session.getBasicRemote().sendText(callError(
messageId, "NotSupported", e.getMessage()));
} catch (Exception e) {
session.getBasicRemote().sendText(callError(
messageId, "InternalError", "Failed touest"));
}
}
private ObjectNode handleCall(
Session session, Stringaction, JsonNode payload) {
String cpId = (String)session.getUserProperties().get("chargePointId");
ObjectNode response =JSON.createObjectNode();
switch (action) {
case "BootNotification"-> {
System.out.printf("[Boot] id=%s, payload=%s%n", cpId,payload);
response.put("currentTime", Instant.now().toString());
response.put("interval", 30);
response.put("status", "Accepted");
}
case "Heartbeat" ->{
System.out.printf("[Heartbeat] id=%s%n", cpId);
response.put("currentTime", Instant.now().toString());
}
case "StatusNotification"-> System.out.printf(
"[Status] id=%s, evse=%s, connector=%s, status=%s%n",
cpId,
payload.path("evseId").asText(),
payload.path("connectorId").asText(),
payload.path("connectorStatus").asText());
case "Authorize" ->{
String token= payload.path("idToken").path("idToken").asText();
Stringstatus = "TEST-001".equals(token)
? "Accepted" : "Invalid";
response.putObject("idTokenInfo").put("status", status);
System.out.printf("[Authorize] token=%s, status=%s%n",
token, status);
}
case "TransactionEvent"-> System.out.printf(
"[Transaction] id=%s, event=%s, seq=%s, trigger=%s%n",
payload.path("transactionInfo")
.path("transactionId").asText(),
payload.path("eventType").asText(),
payload.path("seqNo").asText(),
payload.path("triggerReason").asText());
case "MeterValues" ->System.out.printf(
"[Meter] id=%s, evse=%s, values=%s%n",
cpId, payload.path("evseId").asText(),
payload.path("meterValue"));
default -> throw newIllegalArgumentException(
"Unsupported action: " + action);
}
return response;
}
private String callResult(String messageId, ObjectNodepayload) {
ArrayNode frame =JSON.createArrayNode();
frame.add(3).add(messageId).add(payload);
return frame.toString();
}
private String callError(
String messageId, Stringcode, String description) {
ArrayNode frame =JSON.createArrayNode();
frame.add(4)
.add(messageId)
.add(code)
.add(description)
.add(JSON.createObjectNode());
return frame.toString();
}
@OnClose
public void onClose(Session session, CloseReasonreason) {
System.out.printf("[Disconnected]session=%s, reason=%s%n",
session.getId(), reason);
}
@OnError
public void onError(Session session, Throwable error){
System.err.printf("[WebSocket error]session=%s, error=%s%n",
session ==null ? "unknown" : session.getId(),
error.getMessage());
}
}
該端點(diǎn)完成四件事:校驗(yàn)協(xié)商后的 WebSocket子協(xié)議、從路徑取得充電樁編號(hào)、按 CALL 格式解析請(qǐng)求、使用相同 messageId 返回 CALLRESULT。
2. 啟動(dòng)服務(wù)端容器創(chuàng)建CsmsServer.java:
展開
代碼語言:JavaScript
自動(dòng)換行
AI代碼解釋
package com.example.ocpp;importorg.glassfish.tyrus.server.Server;importjava.io.BufferedReader;import java.io.InputStreamReader;
public class CsmsServer {
public static void main(String[] args) throwsException {
Server server = new Server(
"0.0.0.0",
9000,
"/",
null,
CsmsEndpoint.class);
try {
server.start();
System.out.println(
"CSMS started:ws:https://127.0.0.1:9000/ocpp/{chargePointId}");
System.out.println("PressEnter to stop.");
new BufferedReader(newInputStreamReader(System.in)).readLine();
} finally {
server.stop();
}
}
}
為了突出協(xié)議鏈路,示例只打印收到的數(shù)據(jù)。正式 CSMS還需要設(shè)備認(rèn)證、Schema 校驗(yàn)、事件持久化、冪等處理、訂單狀態(tài)機(jī)和審計(jì)日志。
五、實(shí)現(xiàn) Java 充電樁模擬器創(chuàng)建ChargePointSimulator.java:
展開
代碼語言:JavaScript
自動(dòng)換行
AI代碼解釋
package com.example.ocpp;importcom.fasterxml.jackson.databind.JsonNode;importcom.fasterxml.jackson.databind.ObjectMapper;importcom.fasterxml.jackson.databind.node.ArrayNode;importcom.fasterxml.jackson.databind.node.ObjectNode;importjakarta.websocket.ClientEndpoint;importjakarta.websocket.CloseReason;importjakarta.websocket.ContainerProvider;importjakarta.websocket.OnClose;import jakarta.websocket.OnError;importjakarta.websocket.OnMessage;import jakarta.websocket.OnOpen;importjakarta.websocket.Session;importjakarta.websocket.WebSocketContainer;import java.net.URI;importjava.time.Instant;import java.util.Map;import java.util.UUID;importjava.util.concurrent.CompletableFuture;importjava.util.concurrent.ConcurrentHashMap;importjava.util.concurrent.TimeUnit;
@ClientEndpoint(subprotocols = {"ocpp2.0.1"})
public class ChargePointSimulator {
private static final ObjectMapper JSON = newObjectMapper();
private static final long RESPONSE_TIMEOUT_SECONDS =10;
private final Map<String,CompletableFuture<JsonNode>> pending =
newConcurrentHashMap<>();
private Session session;
private int heartbeatInterval = 30;
@OnOpen
public void onOpen(Session session) {
this.session = session;
System.out.printf("Connected,subprotocol=%s%n",
session.getNegotiatedSubprotocol());
}
@OnMessage
public void onMessage(String text) {
try {
JsonNode frame =JSON.readTree(text);
int messageType =frame.get(0).asInt();
String messageId =frame.get(1).asText();
if (messageType == 3){
complete(messageId, frame.get(2));
} else if (messageType ==4) {
String error= frame.get(2).asText()
+ ": " + frame.get(3).asText();
fail(messageId, new IllegalStateException(error));
}
} catch (Exception e) {
System.err.println("Invalid response: " +e.getMessage());
}
}
@OnClose
public void onClose(Session session, CloseReasonreason) {
IllegalStateException error = newIllegalStateException(
"WebSocketclosed: " + reason);
pending.values().forEach(future ->future.completeExceptionally(error));
pending.clear();
}
@OnError
public void onError(Session session, Throwable error){
System.err.println("WebSocket error: " +error.getMessage());
}
public JsonNode call(String action, ObjectNodepayload) throws Exception {
String messageId =UUID.randomUUID().toString();
CompletableFuture<JsonNode> future= new CompletableFuture<>();
pending.put(messageId, future);
ArrayNode frame =JSON.createArrayNode();
frame.add(2).add(messageId).add(action).add(payload);
try {
session.getAsyncRemote().sendText(frame.toString(), result-> {
if(!result.isOK()) {
fail(messageId, result.getException());
}
});
returnfuture.get(RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} finally {
pending.remove(messageId);
}
}
private void complete(String messageId, JsonNodepayload) {
CompletableFuture<JsonNode> future= pending.get(messageId);
if (future != null) {
future.complete(payload);
}
}
private void fail(String messageId, Throwable error){
CompletableFuture<JsonNode> future= pending.get(messageId);
if (future != null) {
future.completeExceptionally(error);
}
}
private void boot() throws Exception {
ObjectNode station =JSON.createObjectNode();
station.put("model", "SIM-AC-22K");
station.put("vendorName","DemoVendor");
station.put("serialNumber","SIM-CP-001");
station.put("firmwareVersion","1.0.0");
ObjectNode payload =JSON.createObjectNode();
payload.put("reason", "PowerUp");
payload.set("chargingStation",station);
JsonNode response =call("BootNotification", payload);
if(!"Accepted".equals(response.path("status").asText())) {
throw newIllegalStateException(
"Registration rejected: " + response);
}
heartbeatInterval =response.path("interval").asInt(30);
System.out.printf("Boot accepted,heartbeat=%ds%n", heartbeatInterval);
}
private void heartbeat() throws Exception {
JsonNode response = call("Heartbeat",JSON.createObjectNode());
System.out.println("Heartbeat response:" + response);
}
private void reportStatus(String status) throwsException {
ObjectNode payload =JSON.createObjectNode();
payload.put("timestamp",Instant.now().toString());
payload.put("connectorStatus",status);
payload.put("evseId", 1);
payload.put("connectorId", 1);
call("StatusNotification", payload);
}
private boolean authorize(String token) throwsException {
ObjectNode idToken =JSON.createObjectNode();
idToken.put("idToken", token);
idToken.put("type", "ISO14443");
ObjectNode payload =JSON.createObjectNode();
payload.set("idToken", idToken);
JsonNode response = call("Authorize",payload);
return "Accepted".equals(
response.path("idTokenInfo").path("status").asText());
}
private void transactionEvent(
String eventType,
String triggerReason,
int seqNo,
String transactionId,
String token) throwsException {
ObjectNode payload =JSON.createObjectNode();
payload.put("eventType", eventType);
payload.put("timestamp",Instant.now().toString());
payload.put("triggerReason",triggerReason);
payload.put("seqNo", seqNo);
ObjectNode transactionInfo =payload.putObject("transactionInfo");
transactionInfo.put("transactionId",transactionId);
transactionInfo.put("chargingState",
"Ended".equals(eventType) ? "Idle" : "Charging");
ObjectNode evse =payload.putObject("evse");
evse.put("id", 1);
evse.put("connectorId", 1);
if (token != null) {
ObjectNode idToken =payload.putObject("idToken");
idToken.put("idToken",token);
idToken.put("type","ISO14443");
}
call("TransactionEvent", payload);
}
private void meterValues(double energyKwh) throwsException {
ObjectNode payload =JSON.createObjectNode();
payload.put("evseId", 1);
ObjectNode meterValue =JSON.createObjectNode();
meterValue.put("timestamp",Instant.now().toString());
ObjectNode sampledValue =JSON.createObjectNode();
sampledValue.put("value",energyKwh);
sampledValue.put("context","Sample.Periodic");
sampledValue.put("measurand","Energy.Active.Import.Register");
sampledValue.putObject("unitOfMeasure").put("unit","kWh");
meterValue.putArray("sampledValue").add(sampledValue);
payload.putArray("meterValue").add(meterValue);
call("MeterValues", payload);
}
private void runChargeSession() throws Exception {
String token = "TEST-001";
reportStatus("Available");
Thread.sleep(1_000);
if (!authorize(token)) {
System.out.println("Tokenrejected: " + token);
return;
}
String transactionId =UUID.randomUUID().toString();
reportStatus("Occupied");
transactionEvent(
"Started","Authorized", 0, transactionId, token);
double[] energyValues = {0.2, 0.5,0.9};
for (int i = 0; i <energyValues.length; i++) {
Thread.sleep(2_000);
meterValues(energyValues[i]);
transactionEvent(
"Updated", "MeterValuePeriodic", i + 1,
transactionId, null);
}
transactionEvent(
"Ended","StopAuthorized", 4, transactionId, token);
reportStatus("Available");
System.out.println("Transactionfinished: " + transactionId);
}
public static void main(String[] args) throwsException {
String url ="ws:https://127.0.0.1:9000/ocpp/SIM-CP-001";
ChargePointSimulator simulator = newChargePointSimulator();
WebSocketContainer container =ContainerProvider
.getWebSocketContainer();
Session session =container.connectToServer(
simulator,URI.create(url));
try {
simulator.boot();
simulator.runChargeSession();
https:// 演示一次心跳。常駐模擬器應(yīng)按BootNotification 返回的
https:// interval 使用ScheduledExecutorService 周期發(fā)送。
Thread.sleep(Math.min(simulator.heartbeatInterval, 3) *1_000L);
simulator.heartbeat();
} finally {
session.close();
}
}
}
模擬器中的 pending是關(guān)鍵結(jié)構(gòu)。每發(fā)送一個(gè) CALL,就以messageId 保存一個(gè)CompletableFuture;收到 CALLRESULT或 CALLERROR 后,再完成對(duì)應(yīng)的Future。這樣即使多個(gè)請(qǐng)求同時(shí)在途,也不會(huì)把響應(yīng)關(guān)聯(lián)到錯(cuò)誤的業(yè)務(wù)請(qǐng)求。
六、編譯與運(yùn)行先編譯工程:
代碼語言:JavaScript
自動(dòng)換行
AI代碼解釋
mvn clean compile
打開第一個(gè)終端啟動(dòng) CSMS:
代碼語言:JavaScript
自動(dòng)換行
AI代碼解釋
mvn exec:java-Dexec.mainClass=com.example.ocpp.CsmsServer
打開第二個(gè)終端啟動(dòng)充電樁模擬器:
代碼語言:JavaScript
自動(dòng)換行
AI代碼解釋
mvn exec:java-Dexec.mainClass=com.example.ocpp.ChargePointSimulator
CSMS 會(huì)依次輸出類似日志:
展開
代碼語言:JavaScript
自動(dòng)換行
AI代碼解釋
[Connected] id=SIM-CP-001, session=...
[Boot] id=SIM-CP-001, payload=...
[Status] id=SIM-CP-001, evse=1, connector=1,status=Available[Authorize] token=TEST-001, status=Accepted[Status]id=SIM-CP-001, evse=1, connector=1, status=Occupied[Transaction]id=..., event=Started, seq=0, trigger=Authorized[Meter]id=SIM-CP-001, evse=1, values=...
[Transaction] id=..., event=Updated, seq=1,trigger=MeterValuePeriodic[Transaction] id=..., event=Ended, seq=4,trigger=StopAuthorized[Status] id=SIM-CP-001, evse=1, connector=1,status=Available[Heartbeat] id=SIM-CP-001
OCPP 2.0.1 不再使用 OCPP 1.6的 StartTransaction 和 StopTransaction,而是統(tǒng)一使用 TransactionEvent。充電站生成字符串類型的transactionId,eventType表示開始、更新或結(jié)束,seqNo表示事件在本次交易中的順序。
七、幾個(gè)容易踩坑的實(shí)現(xiàn)細(xì)節(jié)1.WebSocket 路徑不等于可信身份示例從 /ocpp/SIM-CP-001中取得充電樁編號(hào),但客戶端聲明的編號(hào)不能直接作為可信身份。生產(chǎn)環(huán)境至少應(yīng)使用wss:https://,并通過 HTTP BasicAuthentication 或客戶端證書校驗(yàn)設(shè)備身份,同時(shí)檢查憑據(jù)與充電站編號(hào)的綁定關(guān)系。
2. 必須協(xié)商 OCPP 子協(xié)議客戶端和服務(wù)端都聲明ocpp2.0.1,握手后還要檢查Session.getNegotiatedSubprotocol()。如果沒有協(xié)商出雙方支持的版本,應(yīng)關(guān)閉連接,不能繼續(xù)按普通 WebSocket文本處理。
3. BootNotification決定設(shè)備后續(xù)行為CSMS 可以返回:
Accepted:注冊(cè)成功,可以發(fā)送業(yè)務(wù)消息;
Pending:平臺(tái)暫未完成注冊(cè),應(yīng)按interval 再次請(qǐng)求;
Rejected:注冊(cè)被拒絕,不應(yīng)繼續(xù)發(fā)送業(yè)務(wù)消息。
演示代碼只繼續(xù)處理Accepted。完整模擬器還應(yīng)實(shí)現(xiàn) Pending 重試和Rejected 退避策略。
4. WebSocket Ping 不能代替HeartbeatPing/Pong用于判斷網(wǎng)絡(luò)連接是否存活,OCPP Heartbeat 用于協(xié)議層時(shí)鐘同步和在線狀態(tài)維護(hù)。兩者用途不同。常駐模擬器應(yīng)根據(jù)BootNotification 返回的 interval,使用 ScheduledExecutorService周期發(fā)送 Heartbeat。
5. 交易事件必須保證順序與冪等seqNo 應(yīng)在同一交易內(nèi)單調(diào)遞增。設(shè)備斷網(wǎng)恢復(fù)后可能補(bǔ)發(fā)事件,平臺(tái)必須結(jié)合充電樁編號(hào)、transactionId、seqNo和事件內(nèi)容去重,不能因?yàn)?Updated重發(fā)而重復(fù)計(jì)費(fèi)。
本文分別發(fā)送 MeterValues與TransactionEvent(Updated)。實(shí)際項(xiàng)目也可以在 TransactionEvent的 meterValue中攜帶交易計(jì)量值。平臺(tái)應(yīng)保存原始表底,費(fèi)用計(jì)算不應(yīng)依賴浮點(diǎn)差值反復(fù)累加。
6. 狀態(tài)與交易不能互相替代StatusNotification(Occupied) 只表示 EVSE或連接器被占用,不一定已經(jīng)產(chǎn)生有效交易;TransactionEvent(Started) 表示交易建立,也不代表此刻一定有功率輸出。平臺(tái)需要分別維護(hù)設(shè)備、連接器和交易狀態(tài)。
八、擴(kuò)展為自動(dòng)化測(cè)試工具1. 支持遠(yuǎn)程啟動(dòng)和停止當(dāng)前 CSMS 只響應(yīng)充電樁上行請(qǐng)求。下一步可以讓CSMS 主動(dòng)發(fā)送 Retion 和 Reion,模擬器則處理服務(wù)端 CALL 并返回 CALLRESULT。
遠(yuǎn)程啟動(dòng)時(shí)要保存remoteStartId,并在后續(xù)TransactionEvent 中回傳,使平臺(tái)可以關(guān)聯(lián)遠(yuǎn)程命令和實(shí)際交易。由于雙方都可能發(fā)起 CALL,CSMS也需要實(shí)現(xiàn)與模擬器相同的 messageId 和Future 映射。
2. 使用場(chǎng)景文件驅(qū)動(dòng)可以把設(shè)備編號(hào)、EVSE數(shù)量、Token、功率曲線和停止原因放入 YAML:
代碼語言:JavaScript
自動(dòng)換行
AI代碼解釋
chargePointId: SIM-CP-001evseId: 1connectorId: 1token:TEST-001meterValuesKwh: [0.2, 0.5, 0.9]stopReason: Local
測(cè)試人員只需修改場(chǎng)景文件,不必改動(dòng) Java代碼。
3. 增加異常場(chǎng)景場(chǎng)景
模擬方式
平臺(tái)檢查點(diǎn)
Token 無效
使用 TEST-INVALID
不創(chuàng)建交易,不允許啟動(dòng)
充電中斷網(wǎng)
Started 后關(guān)閉WebSocket
訂單保持、超時(shí)與補(bǔ)傳是否正確
請(qǐng)求超時(shí)
CSMS 延遲返回CALLRESULT
Future超時(shí)與重試策略是否正確
消息重復(fù)
重發(fā)同一業(yè)務(wù)事件
是否冪等,費(fèi)用是否重復(fù)累加
事件亂序
延遲發(fā)送較小的 seqNo
是否識(shí)別異常順序并告警
電表回退
后一次表底小于前一次
是否阻止負(fù)電量和負(fù)費(fèi)用
設(shè)備故障
上報(bào) Faulted
是否停止可用性展示并產(chǎn)生告警
批量測(cè)試時(shí),每臺(tái)模擬設(shè)備應(yīng)擁有獨(dú)立的 WebSocketSession、設(shè)備編號(hào)、交易編號(hào)和狀態(tài)。可以使用線程池或虛擬線程并發(fā)創(chuàng)建客戶端,但不要一開始就追求極高連接數(shù)。建議從10、100、500 臺(tái)逐級(jí)增加,同時(shí)觀察 CSMS 的 CPU、內(nèi)存、數(shù)據(jù)庫連接、消息積壓和響應(yīng)時(shí)間。
5. 增加 Schema 校驗(yàn)和測(cè)試斷言當(dāng)前代碼只檢查 WebSocket 數(shù)組結(jié)構(gòu),沒有加載OCPP 2.0.1 JSON Schema。平臺(tái)測(cè)試工具至少應(yīng)增加:
代碼語言:JavaScript
自動(dòng)換行
AI代碼解釋
原始 WebSocket 幀
-> OCPP JSON Schema 校驗(yàn)
-> 消息解析與狀態(tài)更新
-> 場(chǎng)景斷言和測(cè)試報(bào)告
原始幀用于還原事實(shí),Schema用于發(fā)現(xiàn)字段問題,場(chǎng)景斷言用于判斷平臺(tái)行為。日志中的Token、證書信息和用戶標(biāo)識(shí)需要脫敏,并設(shè)置合理的保留期限。
九、從示例走向生產(chǎn)聯(lián)調(diào)本文代碼適合本地開發(fā)和協(xié)議學(xué)習(xí),不應(yīng)直接作為生產(chǎn)充電樁固件或完整CSMS。正式聯(lián)調(diào)前至少需要補(bǔ)齊:
使用WSS、設(shè)備認(rèn)證和證書生命周期管理;
加載 OCPP 2.0.1Schema,嚴(yán)格校驗(yàn)請(qǐng)求和響應(yīng);
建立設(shè)備、EVSE、連接器和交易的持久化狀態(tài)模型;
正確處理 CALLERROR、超時(shí)、重試和WebSocket 重連;
支持離線消息緩存、事件補(bǔ)傳和重復(fù)消息冪等;
處理時(shí)間同步、計(jì)量單位、精度和異常表底;
增加遠(yuǎn)程啟停、復(fù)位、解鎖、固件升級(jí)和智能充電;
使用 OCA一致性測(cè)試工具和真實(shí)設(shè)備完成Zui終驗(yàn)證。
代碼能夠收發(fā) OCPP 報(bào)文,不代表產(chǎn)品已經(jīng)通過OCPP 認(rèn)證。正式項(xiàng)目仍應(yīng)以 Open Charge Alliance 發(fā)布的 OCPP 2.0.1規(guī)范、勘誤、實(shí)施指南和認(rèn)證要求為準(zhǔn)。
十、總結(jié)本文使用 Java 17、JakartaWebSocket、Tyrus 和 Jackson 實(shí)現(xiàn)了一個(gè) OCPP 2.0.1 Zui小通信閉環(huán)。CSMS 接收 WebSocket連接并處理 CALL,充電樁模擬器完成上線、狀態(tài)上報(bào)、鑒權(quán)、交易開始、計(jì)量上報(bào)、交易結(jié)束和心跳。
一個(gè)真正有價(jià)值的模擬器,核心不是生成幾條JSON,而是復(fù)現(xiàn)連接、設(shè)備狀態(tài)、授權(quán)、交易和計(jì)量之間的時(shí)序關(guān)系。在此基礎(chǔ)上加入遠(yuǎn)程命令、異常注入、批量并發(fā)、Schema校驗(yàn)和自動(dòng)斷言,就可以形成供研發(fā)、測(cè)試和交付團(tuán)隊(duì)共同使用的 OCPP 2.0.1 測(cè)試工具。
說明:本文代碼用于技術(shù)學(xué)習(xí)和聯(lián)調(diào)演示,不替代 OCPP2.0.1 正式規(guī)范,也不構(gòu)成生產(chǎn)部署的完整安全方案。
共享充電樁軟件開發(fā),充電樁共享軟件開發(fā),智能充電樁系統(tǒng)開發(fā),充電樁軟件開發(fā)公司,充電樁app的開發(fā),共享充電樁app開發(fā),充電樁平臺(tái)開發(fā),充電樁app開發(fā)
公司介紹廣州優(yōu)匠科技有限公司成立于2017年,于2018年開始深耕充電樁運(yùn)營平臺(tái),是國內(nèi)海外的共享充電運(yùn)營平臺(tái)整體解決方案服務(wù)商,致力于推動(dòng)共享充電領(lǐng)域的標(biāo)準(zhǔn)化、產(chǎn)品化,為用戶提供簡(jiǎn)單便捷的充電體驗(yàn)、充電安全及幫助合作伙伴提供穩(wěn)定、高效的運(yùn)營平臺(tái)。公司為全球運(yùn)營商、充電樁廠家提供電動(dòng)自行車、新能源汽車運(yùn)營平臺(tái)的Saas租賃服務(wù)、私有化部署、個(gè)性定制化開發(fā)服務(wù)。同時(shí)我們將為全球所有國家、地區(qū)等運(yùn)營商提供運(yùn)營平臺(tái)的Saas租賃服務(wù)、幫助中小...