Skip to content

Add OCPP 2.1 support#405

Open
robert-s-ubi wants to merge 7 commits intomasterfrom
add_ocpp_2.1_support
Open

Add OCPP 2.1 support#405
robert-s-ubi wants to merge 7 commits intomasterfrom
add_ocpp_2.1_support

Conversation

@robert-s-ubi
Copy link
Contributor

@robert-s-ubi robert-s-ubi commented Dec 12, 2025

Add Java classes for all OCPP 2.1 messages and types as well as client and server event handler interfaces and function classes corresponding to the OCPP 2.1 "functional blocks."

Add OCPP 2.1 to the ProtocolVersion enum.

Specify UTF-8 encoding for the Java source files in build.gradle and pom.xml to avoid compiler warnings about the UTF-8 sequences in the source files.

@robert-s-ubi robert-s-ubi mentioned this pull request Dec 12, 2025
@robert-s-ubi robert-s-ubi marked this pull request as draft December 12, 2025 21:17
@robert-s-ubi robert-s-ubi marked this pull request as ready for review December 13, 2025 00:34
Add Java classes for all OCPP 2.1 messages and types as well as client
and server event handler interfaces and function classes corresponding
to the OCPP 2.1 "functional blocks."

Add OCPP 2.1 to the ProtocolVersion enum.

Specify UTF-8 encoding for the Java source files in build.gradle and
pom.xml to avoid compiler warnings about the UTF-8 sequences in the
source files.
Implement the CALLRESULTERROR RPC as a response to a CALLRESULT, in case
that failed internal validation, or if the completion action last added
to the CompletionStage returned by send() using .whenComplete[Async]()
throws any exception.

Add an optional confirmationError() callback method to the ClientEvents
and ServerEvents interfaces to allow the application to get notified
about an incoming CALLRESULTERROR. The application may use the uniqueId
parameter to match it to the Request#getOcppMessageId() of one of the
requests it last responded to, provided it keeps track of them.

Implement the SEND RPC as requests which do not have a confirmation
type. These are sent through the existing send() method, returning a
CompletableFuture which is already completed when the method returns,
to which a completion action can be added to check whether a local
exception prevented the request from being sent.

Fix the only OCPP message using the SEND RPC, NotifyPeriodicEventStream,
and add its missing Feature and Function handlers. The handler for this
message is a void method, as it has no response.

Remove sending CALLERROR in response to anything other than a CALL,
except for the RpcFrameworkError when the message could not be parsed.

Simplify the pendingPromises synchronization in Session by using the
ConcurrentHashMap class rather than HashMap.

Fix a few typos encountered along the way.
Add multi-protocol integration tests for OCPP 1.6/2.0.1/2.1 protocol
selection and exchanging BootNotification messages.

Add integration tests for the OCPP 2.1 CALLRESULTERROR and SEND RPCs.
@neinhart
Copy link

Bug: ReportDERControl message direction is reversed

While integrating this PR, I found that ReportDERControl has its message direction (ownership) reversed compared to all other Report* messages in the codebase.

Issue

Per OCPP 2.1 spec, ReportDERControl follows the same Get→Report pattern as GetChargingProfiles → ReportChargingProfiles: the Charging Station (Client) sends ReportDERControlRequest to the CSMS (Server).

However, the current implementation has it backwards:

File Current (Wrong) Expected
ServerDERControlFunction new ReportDERControlFeature(null) new ReportDERControlFeature(this)
ClientDERControlFunction new ReportDERControlFeature(this) new ReportDERControlFeature(null)
ServerDERControlEventHandler missing handleReportDERControlRequest should have it
ClientDERControlEventHandler has handleReportDERControlRequest should not have it
ServerDERControlFunction has createReportDERControlRequest should not have it
ClientDERControlFunction missing createReportDERControlRequest should have it

Cross-reference with correct pattern

ReportChargingProfiles in SmartCharging follows the correct pattern:

  • ServerSmartChargingFunction: new ReportChargingProfilesFeature(this) — server receives
  • ClientSmartChargingFunction: new ReportChargingProfilesFeature(null) — client sends

Affected files

  • ClientDERControlEventHandler.java
  • ClientDERControlFunction.java
  • ServerDERControlEventHandler.java
  • ServerDERControlFunction.java

Happy to submit a PR with the fix if helpful.

@neinhart
Copy link

Spec reference for the ReportDERControl direction fix

Confirmed against OCPP 2.1 Edition 2 (2025-12-03), Part 2 - Specification:

R04 Requirements (p.561) — GetDERControl section:

  • R04.FR.31: "The Charging Station SHALL set the requestId in every ReportDERControlRequest to the value of requestId in GetDERControlRequest."
  • R04.FR.32: "Charging Station SHALL set the tbc flag to true for all ReportDERControlRequest messages except the last."
  • R04.FR.33: "Charging Station … SHALL return a status = Accepted and send one or more ReportDERControlRequest messages for all controls."

All three requirements explicitly state that the Charging Station (CS) sends ReportDERControlRequest.

Message definitions (p.607-608):

  • §1.64.1 ReportDERControlRequest: "Reports DER controls requested by a GetDERControlRequest message." (CS → CSMS)
  • §1.64.2 ReportDERControlResponse: "This is an empty message sent by CSMS in response to a ReportDERControlRequest message." (CSMS → CS)

GetDERControl definition (p.592):

  • §1.29.1 GetDERControlRequest.requestId: "Required. RequestId to be used in ReportDERControlRequest." — confirming CSMS sends GetDERControl, CS responds with ReportDERControl.

This matches the ReportChargingProfiles pattern (GetChargingProfiles → ReportChargingProfiles, CS → CSMS) and is consistent with all other Report/Notify messages in the codebase.

@neinhart
Copy link

Patch for the ReportDERControl direction fix

Here is the patch to fix the reversed message ownership. Apply with git apply:

Click to expand patch
diff --git a/ocpp-v2/src/main/java/eu/chargetime/ocpp/v21/feature/function/ClientDERControlEventHandler.java b/ocpp-v2/src/main/java/eu/chargetime/ocpp/v21/feature/function/ClientDERControlEventHandler.java
index fbf62591..6cb24543 100644
--- a/ocpp-v2/src/main/java/eu/chargetime/ocpp/v21/feature/function/ClientDERControlEventHandler.java
+++ b/ocpp-v2/src/main/java/eu/chargetime/ocpp/v21/feature/function/ClientDERControlEventHandler.java
@@ -44,14 +44,6 @@ public interface ClientDERControlEventHandler {
    */
   GetDERControlResponse handleGetDERControlRequest(GetDERControlRequest request);
 
-  /**
-   * Handle a {@link ReportDERControlRequest} and return a {@link ReportDERControlResponse}.
-   *
-   * @param request incoming {@link ReportDERControlRequest} to handle.
-   * @return outgoing {@link ReportDERControlResponse} to reply with.
-   */
-  ReportDERControlResponse handleReportDERControlRequest(ReportDERControlRequest request);
-
   /**
    * Handle a {@link SetDERControlRequest} and return a {@link SetDERControlResponse}.
    *
diff --git a/ocpp-v2/src/main/java/eu/chargetime/ocpp/v21/feature/function/ClientDERControlFunction.java b/ocpp-v2/src/main/java/eu/chargetime/ocpp/v21/feature/function/ClientDERControlFunction.java
index c22bf8ec..5bc842de 100644
--- a/ocpp-v2/src/main/java/eu/chargetime/ocpp/v21/feature/function/ClientDERControlFunction.java
+++ b/ocpp-v2/src/main/java/eu/chargetime/ocpp/v21/feature/function/ClientDERControlFunction.java
@@ -48,7 +48,7 @@ public class ClientDERControlFunction implements Function {
     features.add(new GetDERControlFeature(this));
     features.add(new NotifyDERAlarmFeature(null));
     features.add(new NotifyDERStartStopFeature(null));
-    features.add(new ReportDERControlFeature(this));
+    features.add(new ReportDERControlFeature(null));
     features.add(new SetDERControlFeature(this));
   }
 
@@ -63,8 +63,6 @@ public class ClientDERControlFunction implements Function {
       return eventHandler.handleClearDERControlRequest((ClearDERControlRequest) request);
     } else if (request instanceof GetDERControlRequest) {
       return eventHandler.handleGetDERControlRequest((GetDERControlRequest) request);
-    } else if (request instanceof ReportDERControlRequest) {
-      return eventHandler.handleReportDERControlRequest((ReportDERControlRequest) request);
     } else if (request instanceof SetDERControlRequest) {
       return eventHandler.handleSetDERControlRequest((SetDERControlRequest) request);
     }
@@ -96,4 +94,14 @@ public class ClientDERControlFunction implements Function {
       String controlId, Boolean started, ZonedDateTime timestamp) {
     return new NotifyDERStartStopRequest(controlId, started, timestamp);
   }
+
+  /**
+   * Create a client {@link ReportDERControlRequest} with all required fields.
+   *
+   * @param requestId RequestId from GetDERControlRequest.
+   * @return an instance of {@link ReportDERControlRequest}
+   */
+  public ReportDERControlRequest createReportDERControlRequest(Integer requestId) {
+    return new ReportDERControlRequest(requestId);
+  }
 }
diff --git a/ocpp-v2/src/main/java/eu/chargetime/ocpp/v21/feature/function/ServerDERControlEventHandler.java b/ocpp-v2/src/main/java/eu/chargetime/ocpp/v21/feature/function/ServerDERControlEventHandler.java
index 875b053b..d4dd0ad6 100644
--- a/ocpp-v2/src/main/java/eu/chargetime/ocpp/v21/feature/function/ServerDERControlEventHandler.java
+++ b/ocpp-v2/src/main/java/eu/chargetime/ocpp/v21/feature/function/ServerDERControlEventHandler.java
@@ -48,4 +48,14 @@ public interface ServerDERControlEventHandler {
    */
   NotifyDERStartStopResponse handleNotifyDERStartStopRequest(
       UUID sessionIndex, NotifyDERStartStopRequest request);
+
+  /**
+   * Handle a {@link ReportDERControlRequest} and return a {@link ReportDERControlResponse}.
+   *
+   * @param sessionIndex identifier of the session on which the request was received.
+   * @param request incoming {@link ReportDERControlRequest} to handle.
+   * @return outgoing {@link ReportDERControlResponse} to reply with.
+   */
+  ReportDERControlResponse handleReportDERControlRequest(
+      UUID sessionIndex, ReportDERControlRequest request);
 }
diff --git a/ocpp-v2/src/main/java/eu/chargetime/ocpp/v21/feature/function/ServerDERControlFunction.java b/ocpp-v2/src/main/java/eu/chargetime/ocpp/v21/feature/function/ServerDERControlFunction.java
index 5614d63e..10d543d3 100644
--- a/ocpp-v2/src/main/java/eu/chargetime/ocpp/v21/feature/function/ServerDERControlFunction.java
+++ b/ocpp-v2/src/main/java/eu/chargetime/ocpp/v21/feature/function/ServerDERControlFunction.java
@@ -47,7 +47,7 @@ public class ServerDERControlFunction implements Function {
     features.add(new GetDERControlFeature(null));
     features.add(new NotifyDERAlarmFeature(this));
     features.add(new NotifyDERStartStopFeature(this));
-    features.add(new ReportDERControlFeature(null));
+    features.add(new ReportDERControlFeature(this));
     features.add(new SetDERControlFeature(null));
   }
 
@@ -64,6 +64,9 @@ public class ServerDERControlFunction implements Function {
     } else if (request instanceof NotifyDERStartStopRequest) {
       return eventHandler.handleNotifyDERStartStopRequest(
           sessionIndex, (NotifyDERStartStopRequest) request);
+    } else if (request instanceof ReportDERControlRequest) {
+      return eventHandler.handleReportDERControlRequest(
+          sessionIndex, (ReportDERControlRequest) request);
     }
     return null;
   }
@@ -88,16 +91,6 @@ public class ServerDERControlFunction implements Function {
     return new GetDERControlRequest(requestId);
   }
 
-  /**
-   * Create a server {@link ReportDERControlRequest} with all required fields.
-   *
-   * @param requestId RequestId from GetDERControlRequest.
-   * @return an instance of {@link ReportDERControlRequest}
-   */
-  public ReportDERControlRequest createReportDERControlRequest(Integer requestId) {
-    return new ReportDERControlRequest(requestId);
-  }
-
   /**
    * Create a server {@link SetDERControlRequest} with all required fields.
    *

Summary of changes:

  • ReportDERControlFeature ownership swapped: Server=this (receives), Client=null (sends)
  • handleReportDERControlRequest moved from ClientDERControlEventHandlerServerDERControlEventHandler (with UUID sessionIndex parameter)
  • createReportDERControlRequest moved from ServerDERControlFunctionClientDERControlFunction
  • Request dispatch in handleRequest() updated accordingly

@robert-s-ubi
Copy link
Contributor Author

Hi @neinhart, thank you very much for pointing this out! I took another look at all the new messages added in OCPP 2.1 and found that I got the direction wrong on three messages:

  1. ClosePeriodicEventStreamRequest
  2. ReportDERControlRequest (as you pointed out)
  3. RequestBatterySwapRequest

I'll prepare a commit to fix all these.

Fix the direction of these OCPP 2.1 messages which were reversed:

1. ClosePeriodicEventStreamRequest is CS to CSMS
2. ReportDERControlRequest is CS to CSMS
3. RequestBatterySwapRequest is CSMS to CS
Move the handleClosePeriodicEventStreamRequest() implementation from the
client side to the server side in the test code.
@robert-s-ubi
Copy link
Contributor Author

@neinhart these two commits incorporate your fix (thank you very much for so diligently preparing this!) and fix the other two messages as well. Please let me know if you run into any further issues.

Move the Battery Swapping messages to a dedicated function, in line with
the OCPP 2.1 specification which has a dedicated chapter for these.
@robert-s-ubi
Copy link
Contributor Author

I noticed that the Battery Swapping messages have a dedicated chapter in OCPP 2.1, so they belong in a dedicated function (functions mirror OCPP chapters) as well.

Added a commit which adds the "BatterySwapping" function and moves these messages there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants