爱一夜夜爽 99久久无色_自拍视频精品一区二区三区_欧美日韩一区观看_欧美-级高清免费一片

簡電云 | Java + WebSocket 實現(xiàn) OCPP 2.0.1 充電樁模擬程序-我吃了教官的J吧男男(H)_16歲自愿跟爸爸關系怎么判定

報價
請來電詢價
產(chǎn)品咨詢熱線
15920323068
微信號
15920323068
軟件開發(fā)
小程序+公眾號+APP定制開發(fā)
一、為什么需要充電樁模擬器

充電平臺通常需要同時處理設備接入、訂單、計費、告警和遠程控制。如果所有測試都依賴真實充電樁,會遇到幾個現(xiàn)實問題:

設備數(shù)量有限,難以驗證大量設備同時在線;

插槍、刷卡、充電和拔槍需要人工操作,回歸測試效率低;

斷網(wǎng)、重復報文、事件亂序和異常停止不容易穩(wěn)定復現(xiàn);

不同廠商固件行為存在差異,平臺問題與設備問題容易混在一起;

開發(fā)人員無法隨時獲得完整的充電現(xiàn)場環(huán)境。

模擬器并不是為了替代真實設備,而是把協(xié)議行為變成可重復、可編排的測試輸入。開發(fā)階段先用模擬器驗證平臺狀態(tài)機,再使用真實充電樁完成硬件、計量和電氣安全測試,能夠明顯降低聯(lián)調(diào)成本。

本文實現(xiàn)以下Zui小閉環(huán):

展開

代碼語言:JavaScript

自動換行

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 的 WebSocket報文

OCPP 2.0.1 JSON 通過 WebSocket 傳輸,握手時使用子協(xié)議ocpp2.0.1。協(xié)議常用三種消息類型:

類型

編號

數(shù)組結(jié)構

CALL

2

[2, messageId, action, payload]

CALLRESULT

3

[3, messageId, payload]

CALLERROR

4

[4, messageId, errorCode, description, details]

例如充電樁上線時發(fā)送:

展開

代碼語言:JavaScript

自動換行

AI代碼解釋

[2,"msg-001","BootNotification",{
 "reason":"PowerUp",
 "chargingStation":{
   "model":"SIM-AC-22K",
   "vendorName":"DemoVendor"
 }
}]

CSMS 返回:

代碼語言:JavaScript

自動換行

AI代碼解釋

[3,"msg-001",{
 "currentTime":"2026-09-30T02:00:00Z",
 "interval":30,
 "status":"Accepted"}]

messageId 用于關聯(lián)請求和響應。一次連接中的CALL 應使用唯一標識,客戶端還需要維護“消息編號—等待結(jié)果”的映射。

本文為了展示 WebSocket 和 OCPP報文的底層處理過程,不引入商業(yè) OCPP SDK,而是使用 Jackson 構造消息。這樣代碼容易理解,但生產(chǎn)項目仍應補充完整的OCPP 2.0.1 JSON Schema 校驗和功能塊實現(xiàn)。

三、創(chuàng)建 Maven 工程

示例使用 Java 17。工程結(jié)構如下:

代碼語言:JavaScript

自動換行

AI代碼解釋

ocpp201-java-demo/
├── pom.xml└── src/main/java/com/example/ocpp/
   ├── CsmsServer.java
   ├── CsmsEndpoint.java
   └── ChargePointSimulator.java

pom.xml 使用 Tyrus 作為 JakartaWebSocket 實現(xiàn),Jackson 負責 JSON 解析:

展開

代碼語言:JavaScript

自動換行

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>

固定依賴版本有利于復現(xiàn)。Tyrus 不僅提供WebSocket API,還提供可直接啟動的服務端容器和客戶端實現(xiàn)。

四、實現(xiàn) CSMS WebSocket服務端1. 創(chuàng)建 WebSocket端點

創(chuàng)建CsmsEndpoint.java:

展開

代碼語言:JavaScript

自動換行

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:// 本示例服務端只處理充電樁發(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());
   }
}

該端點完成四件事:校驗協(xié)商后的 WebSocket子協(xié)議、從路徑取得充電樁編號、按 CALL 格式解析請求、使用相同 messageId 返回 CALLRESULT。

2. 啟動服務端容器

創(chuàng)建CsmsServer.java:

展開

代碼語言:JavaScript

自動換行

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還需要設備認證、Schema 校驗、事件持久化、冪等處理、訂單狀態(tài)機和審計日志。

五、實現(xiàn) Java 充電樁模擬器

創(chuàng)建ChargePointSimulator.java:

展開

代碼語言:JavaScript

自動換行

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:// 演示一次心跳。常駐模擬器應按BootNotification 返回的
           https:// interval 使用ScheduledExecutorService 周期發(fā)送。
          Thread.sleep(Math.min(simulator.heartbeatInterval, 3) *1_000L);
           simulator.heartbeat();
       } finally {
           session.close();
       }
   }
}

模擬器中的 pending是關鍵結(jié)構。每發(fā)送一個 CALL,就以messageId 保存一個CompletableFuture;收到 CALLRESULT或 CALLERROR 后,再完成對應的Future。這樣即使多個請求同時在途,也不會把響應關聯(lián)到錯誤的業(yè)務請求。

六、編譯與運行

先編譯工程:

代碼語言:JavaScript

自動換行

AI代碼解釋

mvn clean compile

打開第一個終端啟動 CSMS:

代碼語言:JavaScript

自動換行

AI代碼解釋

mvn exec:java-Dexec.mainClass=com.example.ocpp.CsmsServer

打開第二個終端啟動充電樁模擬器:

代碼語言:JavaScript

自動換行

AI代碼解釋

mvn exec:java-Dexec.mainClass=com.example.ocpp.ChargePointSimulator

CSMS 會依次輸出類似日志:

展開

代碼語言:JavaScript

自動換行

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表示事件在本次交易中的順序。

七、幾個容易踩坑的實現(xiàn)細節(jié)1.WebSocket 路徑不等于可信身份

示例從 /ocpp/SIM-CP-001中取得充電樁編號,但客戶端聲明的編號不能直接作為可信身份。生產(chǎn)環(huán)境至少應使用wss:https://,并通過 HTTP BasicAuthentication 或客戶端證書校驗設備身份,同時檢查憑據(jù)與充電站編號的綁定關系。

2. 必須協(xié)商 OCPP 子協(xié)議

客戶端和服務端都聲明ocpp2.0.1,握手后還要檢查Session.getNegotiatedSubprotocol()。如果沒有協(xié)商出雙方支持的版本,應關閉連接,不能繼續(xù)按普通 WebSocket文本處理。

3. BootNotification決定設備后續(xù)行為

CSMS 可以返回:

Accepted:注冊成功,可以發(fā)送業(yè)務消息;

Pending:平臺暫未完成注冊,應按interval 再次請求;

Rejected:注冊被拒絕,不應繼續(xù)發(fā)送業(yè)務消息。

演示代碼只繼續(xù)處理Accepted。完整模擬器還應實現(xiàn) Pending 重試和Rejected 退避策略。

4. WebSocket Ping 不能代替Heartbeat

Ping/Pong用于判斷網(wǎng)絡連接是否存活,OCPP Heartbeat 用于協(xié)議層時鐘同步和在線狀態(tài)維護。兩者用途不同。常駐模擬器應根據(jù)BootNotification 返回的 interval,使用 ScheduledExecutorService周期發(fā)送 Heartbeat。

5. 交易事件必須保證順序與冪等

seqNo 應在同一交易內(nèi)單調(diào)遞增。設備斷網(wǎng)恢復后可能補發(fā)事件,平臺必須結(jié)合充電樁編號、transactionId、seqNo和事件內(nèi)容去重,不能因為 Updated重發(fā)而重復計費。

本文分別發(fā)送 MeterValues與TransactionEvent(Updated)。實際項目也可以在 TransactionEvent的 meterValue中攜帶交易計量值。平臺應保存原始表底,費用計算不應依賴浮點差值反復累加。

6. 狀態(tài)與交易不能互相替代

StatusNotification(Occupied) 只表示 EVSE或連接器被占用,不一定已經(jīng)產(chǎn)生有效交易;TransactionEvent(Started) 表示交易建立,也不代表此刻一定有功率輸出。平臺需要分別維護設備、連接器和交易狀態(tài)。

八、擴展為自動化測試工具1. 支持遠程啟動和停止

當前 CSMS 只響應充電樁上行請求。下一步可以讓CSMS 主動發(fā)送 Retion 和 Reion,模擬器則處理服務端 CALL 并返回 CALLRESULT。

遠程啟動時要保存remoteStartId,并在后續(xù)TransactionEvent 中回傳,使平臺可以關聯(lián)遠程命令和實際交易。由于雙方都可能發(fā)起 CALL,CSMS也需要實現(xiàn)與模擬器相同的 messageId 和Future 映射。

2. 使用場景文件驅(qū)動

可以把設備編號、EVSE數(shù)量、Token、功率曲線和停止原因放入 YAML:

代碼語言:JavaScript

自動換行

AI代碼解釋

chargePointId: SIM-CP-001evseId: 1connectorId: 1token:TEST-001meterValuesKwh: [0.2, 0.5, 0.9]stopReason: Local

測試人員只需修改場景文件,不必改動 Java代碼。

3. 增加異常場景

場景

模擬方式

平臺檢查點

Token 無效

使用 TEST-INVALID

不創(chuàng)建交易,不允許啟動

充電中斷網(wǎng)

Started 后關閉WebSocket

訂單保持、超時與補傳是否正確

請求超時

CSMS 延遲返回CALLRESULT

Future超時與重試策略是否正確

消息重復

重發(fā)同一業(yè)務事件

是否冪等,費用是否重復累加

事件亂序

延遲發(fā)送較小的 seqNo

是否識別異常順序并告警

電表回退

后一次表底小于前一次

是否阻止負電量和負費用

設備故障

上報 Faulted

是否停止可用性展示并產(chǎn)生告警

4. 批量并發(fā)設備

批量測試時,每臺模擬設備應擁有獨立的 WebSocketSession、設備編號、交易編號和狀態(tài)??梢允褂镁€程池或虛擬線程并發(fā)創(chuàng)建客戶端,但不要一開始就追求極高連接數(shù)。建議從10、100、500 臺逐級增加,同時觀察 CSMS 的 CPU、內(nèi)存、數(shù)據(jù)庫連接、消息積壓和響應時間。

5. 增加 Schema 校驗和測試斷言

當前代碼只檢查 WebSocket 數(shù)組結(jié)構,沒有加載OCPP 2.0.1 JSON Schema。平臺測試工具至少應增加:

代碼語言:JavaScript

自動換行

AI代碼解釋

原始 WebSocket 幀
 -> OCPP JSON Schema 校驗
 -> 消息解析與狀態(tài)更新
 -> 場景斷言和測試報告

原始幀用于還原事實,Schema用于發(fā)現(xiàn)字段問題,場景斷言用于判斷平臺行為。日志中的Token、證書信息和用戶標識需要脫敏,并設置合理的保留期限。

九、從示例走向生產(chǎn)聯(lián)調(diào)

本文代碼適合本地開發(fā)和協(xié)議學習,不應直接作為生產(chǎn)充電樁固件或完整CSMS。正式聯(lián)調(diào)前至少需要補齊:

使用WSS、設備認證和證書生命周期管理;

加載 OCPP 2.0.1Schema,嚴格校驗請求和響應;

建立設備、EVSE、連接器和交易的持久化狀態(tài)模型;

正確處理 CALLERROR、超時、重試和WebSocket 重連;

支持離線消息緩存、事件補傳和重復消息冪等;

處理時間同步、計量單位、精度和異常表底;

增加遠程啟停、復位、解鎖、固件升級和智能充電;

使用 OCA一致性測試工具和真實設備完成Zui終驗證。

代碼能夠收發(fā) OCPP 報文,不代表產(chǎn)品已經(jīng)通過OCPP 認證。正式項目仍應以 Open Charge Alliance 發(fā)布的 OCPP 2.0.1規(guī)范、勘誤、實施指南和認證要求為準。

十、總結(jié)

本文使用 Java 17、JakartaWebSocket、Tyrus 和 Jackson 實現(xiàn)了一個 OCPP 2.0.1 Zui小通信閉環(huán)。CSMS 接收 WebSocket連接并處理 CALL,充電樁模擬器完成上線、狀態(tài)上報、鑒權、交易開始、計量上報、交易結(jié)束和心跳。

一個真正有價值的模擬器,核心不是生成幾條JSON,而是復現(xiàn)連接、設備狀態(tài)、授權、交易和計量之間的時序關系。在此基礎上加入遠程命令、異常注入、批量并發(fā)、Schema校驗和自動斷言,就可以形成供研發(fā)、測試和交付團隊共同使用的 OCPP 2.0.1 測試工具。

說明:本文代碼用于技術學習和聯(lián)調(diào)演示,不替代 OCPP2.0.1 正式規(guī)范,也不構成生產(chǎn)部署的完整安全方案。



更新時間
皇冠會員
第1年
統(tǒng)一社會信用代碼
91440101MA59N5E7XT
成立日期
2017年05月22日
法定代表人
鄧南輝
注冊資本
100

主營產(chǎn)品

共享充電樁軟件開發(fā),充電樁共享軟件開發(fā),智能充電樁系統(tǒng)開發(fā),充電樁軟件開發(fā)公司,充電樁app的開發(fā),共享充電樁app開發(fā),充電樁平臺開發(fā),充電樁app開發(fā)

公司簡介

公司介紹廣州優(yōu)匠科技有限公司成立于2017年,于2018年開始深耕充電樁運營平臺,是國內(nèi)海外的共享充電運營平臺整體解決方案服務商,致力于推動共享充電領域的標準化、產(chǎn)品化,為用戶提供簡單便捷的充電體驗、充電安全及幫助合作伙伴提供穩(wěn)定、高效的運營平臺。公司為全球運營商、充電樁廠家提供電動自行車、新能源汽車運營平臺的Saas租賃服務、私有化部署、個性定制化開發(fā)服務。同時我們將為全球所有國家、地區(qū)等運營商提供運營平臺的Saas租賃服務、幫助中小...

查看公司詳情
產(chǎn)品咨詢熱線15920323068撥打郵箱[email protected]郵件
產(chǎn)產(chǎn)品經(jīng)理鄧工
地址廣州市番禺區(qū)大龍街東興路317號402
我們其他產(chǎn)品
我們的新聞