Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,22 @@
*/
package de.rwth.idsg.steve.ocpp.task;

import de.rwth.idsg.steve.SteveException;
import de.rwth.idsg.steve.ocpp.CommunicationTask;
import de.rwth.idsg.steve.ocpp.OcppVersion;
import de.rwth.idsg.steve.ocpp.task.impl.OcppVersionHandler;
import de.rwth.idsg.steve.ocpp.task.impl.TaskDefinition;
import de.rwth.idsg.steve.repository.ChargingProfileRepository;
import de.rwth.idsg.steve.web.dto.ocpp.RemoteStartTransactionParams;
import ocpp.cp._2015._10.ChargingProfile;
import ocpp.cp._2015._10.ChargingProfilePurposeType;
import org.jspecify.annotations.Nullable;

public class RemoteStartTransactionTask extends CommunicationTask<RemoteStartTransactionParams, String> {
public class RemoteStartTransactionTask
extends CommunicationTask<RemoteStartTransactionTask.RemoteStartTransactionWithProfileParams, String> {

private static final TaskDefinition<RemoteStartTransactionParams, String> TASK_DEFINITION =
TaskDefinition.<RemoteStartTransactionParams, String>builder()
private static final TaskDefinition<RemoteStartTransactionWithProfileParams, String> TASK_DEFINITION =
TaskDefinition.<RemoteStartTransactionWithProfileParams, String>builder()
.versionHandler(
OcppVersion.V_12,
new OcppVersionHandler<>(
Expand All @@ -49,16 +55,62 @@ public class RemoteStartTransactionTask extends CommunicationTask<RemoteStartTra
new OcppVersionHandler<>(
task -> new ocpp.cp._2015._10.RemoteStartTransactionRequest()
.withIdTag(task.getParams().getIdTag())
.withConnectorId(task.getParams().getConnectorId()),
.withConnectorId(task.getParams().getConnectorId())
.withChargingProfile(task.getParams().chargingProfile),
(ocpp.cp._2015._10.RemoteStartTransactionResponse r) ->
r.getStatus().value()))
.build();

public RemoteStartTransactionTask(RemoteStartTransactionParams params) {
super(TASK_DEFINITION, params);
public RemoteStartTransactionTask(
RemoteStartTransactionParams params, ChargingProfileRepository chargingProfileRepository) {
super(
TASK_DEFINITION,
new RemoteStartTransactionWithProfileParams(
params, createChargingProfile(params.getChargingProfilePk(), chargingProfileRepository)));
}

public RemoteStartTransactionTask(
RemoteStartTransactionParams params, String caller, ChargingProfileRepository chargingProfileRepository) {
super(
TASK_DEFINITION,
new RemoteStartTransactionWithProfileParams(
params, createChargingProfile(params.getChargingProfilePk(), chargingProfileRepository)),
caller);
}

public static class RemoteStartTransactionWithProfileParams extends RemoteStartTransactionParams {
private final ocpp.cp._2015._10.@Nullable ChargingProfile chargingProfile;

public RemoteStartTransactionWithProfileParams(
RemoteStartTransactionParams params, ocpp.cp._2015._10.@Nullable ChargingProfile chargingProfile) {
super();
this.setIdTag(params.getIdTag());
this.setConnectorId(params.getConnectorId());
this.chargingProfile = chargingProfile;
}

@Override
public @Nullable Integer getChargingProfilePk() {
if (chargingProfile == null) {
return null;
}
return chargingProfile.getChargingProfileId();
}
}

public RemoteStartTransactionTask(RemoteStartTransactionParams params, String caller) {
super(TASK_DEFINITION, params, caller);
private static @Nullable ChargingProfile createChargingProfile(
@Nullable Integer chargingProfilePk, ChargingProfileRepository chargingProfileRepository) {
if (chargingProfilePk == null) {
return null;
}
de.rwth.idsg.steve.repository.dto.ChargingProfile.Details details = chargingProfileRepository
.getDetails(chargingProfilePk)
.orElseThrow(() ->
new SteveException.BadRequest("ChargingProfile with PK " + chargingProfilePk + " not found"));
ocpp.cp._2015._10.ChargingProfile chargingProfile = SetChargingProfileTask.mapToOcpp(details, null);
if (chargingProfile.getChargingProfilePurpose() != ChargingProfilePurposeType.TX_PROFILE) {
throw new SteveException.BadRequest("ChargingProfilePurposeType is not TX_PROFILE");
}
return chargingProfile;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ public OcppCallback<String> createDefaultCallback() {
@Override
public void success(String chargeBoxId, String statusValue) {
addNewResponse(chargeBoxId, statusValue);
if ("Accepted".equalsIgnoreCase(statusValue)) {
var purpose = ChargingProfilePurposeType.fromValue(details.getChargingProfilePurpose());
if ("Accepted".equalsIgnoreCase(statusValue) && ChargingProfilePurposeType.TX_PROFILE != purpose) {
repo.setProfile(details.getChargingProfilePk(), chargeBoxId, params.getConnectorId());
}
}
Expand All @@ -110,6 +111,14 @@ public void failed(String chargeBoxId, Exception e) {

private static SetChargingProfileRequest buildRequestFromDb(
SetChargingProfileParams params, ChargingProfile.Details details) {
var ocppProfile = mapToOcpp(details, params.getTransactionId());

return new SetChargingProfileRequest()
.withConnectorId(params.getConnectorId())
.withCsChargingProfiles(ocppProfile);
}

public static ocpp.cp._2015._10.ChargingProfile mapToOcpp(ChargingProfile.Details details, Integer transactionId) {
var schedulePeriods = details.getPeriods().stream()
.map(k -> {
ChargingSchedulePeriod p = new ChargingSchedulePeriod();
Expand All @@ -127,8 +136,9 @@ private static SetChargingProfileRequest buildRequestFromDb(
.withMinChargingRate(details.getMinChargingRate())
.withChargingSchedulePeriod(schedulePeriods);

var ocppProfile = new ocpp.cp._2015._10.ChargingProfile()
return new ocpp.cp._2015._10.ChargingProfile()
.withChargingProfileId(details.getChargingProfilePk())
.withTransactionId(transactionId)
.withStackLevel(details.getStackLevel())
.withChargingProfilePurpose(ChargingProfilePurposeType.fromValue(details.getChargingProfilePurpose()))
.withChargingProfileKind(ChargingProfileKindType.fromValue(details.getChargingProfileKind()))
Expand All @@ -138,10 +148,6 @@ private static SetChargingProfileRequest buildRequestFromDb(
.withValidFrom(toOffsetDateTime(details.getValidFrom()))
.withValidTo(toOffsetDateTime(details.getValidTo()))
.withChargingSchedule(schedule);

return new SetChargingProfileRequest()
.withConnectorId(params.getConnectorId())
.withCsChargingProfiles(ocppProfile);
}

private static void checkAdditionalConstraints(SetChargingProfileRequest request) {
Expand All @@ -158,6 +164,16 @@ private static void checkAdditionalConstraints(SetChargingProfileRequest request
throw new SteveException.InternalError(
"TxProfile should only be set at Charge Point ConnectorId > 0");
}

if (ChargingProfilePurposeType.TX_PROFILE == purpose
&& request.getCsChargingProfiles().getTransactionId() == null) {
throw new SteveException.InternalError("transaction id is required for TxProfile");
}

if (ChargingProfilePurposeType.TX_PROFILE != purpose
&& request.getCsChargingProfiles().getTransactionId() != null) {
throw new SteveException.InternalError("transaction id should only be set for TxProfile");
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -189,14 +189,14 @@ public final int updateFirmware(UpdateFirmwareParams params, OcppCallback<String
// -------------------------------------------------------------------------
@SafeVarargs
public final int remoteStartTransaction(RemoteStartTransactionParams params, OcppCallback<String>... callbacks) {
RemoteStartTransactionTask task = new RemoteStartTransactionTask(params);
RemoteStartTransactionTask task = new RemoteStartTransactionTask(params, chargingProfileRepository);
return addRemoteStartTask(task, callbacks);
}

@SafeVarargs
public final int remoteStartTransaction(
RemoteStartTransactionParams params, String caller, OcppCallback<String>... callbacks) {
RemoteStartTransactionTask task = new RemoteStartTransactionTask(params, caller);
RemoteStartTransactionTask task = new RemoteStartTransactionTask(params, caller, chargingProfileRepository);
return addRemoteStartTask(task, callbacks);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,23 @@

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;

/**
* @author Sevket Goekay <sevketgokay@gmail.com>
* @since 01.01.2015
*/
@Getter
@Setter
public class RemoteStartTransactionParams extends SingleChargePointSelect {

@Min(value = 0, message = "Connector ID must be at least {value}") @Nullable private Integer connectorId;

@NotBlank(message = "User ID Tag is required") @IdTag
@Setter
private String idTag;

@Positive private @Nullable Integer chargingProfilePk;

/**
* Not for a specific connector, when frontend sends the value 0.
* This corresponds to not to include the connector id parameter in OCPP request.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,6 @@ public class SetChargingProfileParams extends MultipleChargePointSelect {
@NotNull @Min(value = 0, message = "Connector ID must be at least {value}") private Integer connectorId;

@NotNull @Positive private Integer chargingProfilePk;

@Positive private Integer transactionId;
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,14 @@ public class Ocpp12Controller {
// Helpers
// -------------------------------------------------------------------------

/**
* https://github.com/steve-community/steve/issues/1759
* used to create form in order to send charging profile within remote start tx for ocpp 1.6.
*/
protected void setCommonAttributesForRemoteStartTx(Model model) {
// nothing to do for versions below 1.6
}

protected void setCommonAttributesForTx(Model model) {
setCommonAttributes(model);
}
Expand Down Expand Up @@ -150,6 +158,7 @@ public String getGetDiag(Model model) {
public String getRemoteStartTx(Model model) {
setCommonAttributesForTx(model);
setActiveUserIdTagList(model);
setCommonAttributesForRemoteStartTx(model);
model.addAttribute(PARAMS, new RemoteStartTransactionParams());
return getPrefix() + REMOTE_START_TX_PATH;
}
Expand Down Expand Up @@ -233,6 +242,7 @@ public String postRemoteStartTx(
if (result.hasErrors()) {
setCommonAttributesForTx(model);
setActiveUserIdTagList(model);
setCommonAttributesForRemoteStartTx(model);
return getPrefix() + REMOTE_START_TX_PATH;
}
return REDIRECT_TASKS_PATH + chargePointServiceClient.remoteStartTransaction(params);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ public Ocpp16Controller(
// Helpers
// -------------------------------------------------------------------------

protected void setCommonAttributesForRemoteStartTx(Model model) {
model.addAttribute("profileForRemoteStartTx", Boolean.TRUE);
model.addAttribute("profileList", chargingProfileRepository.getBasicInfo());
}

@Override
protected void setCommonAttributesForTx(Model model) {
model.addAttribute("cpList", chargePointHelperService.getChargePoints(OcppVersion.V_16));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@
</form:select>
</td>
</tr>
<c:if test="${profileForRemoteStartTx}">
<tr>
<td>Charging Profile ID:</td>
<td>
<form:select path="chargingProfilePk">
<option value="" selected>-- Empty --</option>
<form:options items="${profileList}" itemLabel="itemDescription" itemValue="chargingProfilePk"/>
</form:select>
</td>
</tr>
</c:if>
Comment on lines +36 to +46
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Avoid forcing the empty option selected; it breaks re-selection after validation errors.

Let Spring bind the selected option; add error display.

-        <c:if test="${profileForRemoteStartTx}">
+        <c:if test="${profileForRemoteStartTx}">
             <tr>
                 <td>Charging Profile ID:</td>
                 <td>
-                    <form:select path="chargingProfilePk">
-                        <option value="" selected>-- Empty --</option>
-                        <form:options items="${profileList}" itemLabel="itemDescription" itemValue="chargingProfilePk"/>
-                    </form:select>
+                    <form:select path="chargingProfilePk">
+                        <form:option value="">-- Empty --</form:option>
+                        <form:options items="${profileList}" itemLabel="itemDescription" itemValue="chargingProfilePk"/>
+                    </form:select>
+                    <form:errors path="chargingProfilePk" cssClass="error"/>
                 </td>
             </tr>
         </c:if>

Ensure chargingProfilePk is a nullable wrapper (Integer/Long) so empty option binds to null:


🏁 Script executed:

#!/bin/bash
rg -n 'class RemoteStartTransactionParams' -n steve-core/src/main/java
rg -nP '(\bInteger\b|\bLong\b)\s+chargingProfilePk' steve-core/src/main/java/de/rwth/idsg/steve/web/dto/ocpp/RemoteStartTransactionParams.java -C2

Length of output: 279


Avoid forcing the empty option selected; let Spring bind and show validation errors.

Use <form:option value=""> (no selected attribute) and add form:errors so the previously selected value is restored after validation. chargingProfilePk is @nullable Integer — verified in steve-core/src/main/java/de/rwth/idsg/steve/web/dto/ocpp/RemoteStartTransactionParams.java:43.

-        <c:if test="${profileForRemoteStartTx}">
+        <c:if test="${profileForRemoteStartTx}">
             <tr>
                 <td>Charging Profile ID:</td>
                 <td>
-                    <form:select path="chargingProfilePk">
-                        <option value="" selected>-- Empty --</option>
-                        <form:options items="${profileList}" itemLabel="itemDescription" itemValue="chargingProfilePk"/>
-                    </form:select>
+                    <form:select path="chargingProfilePk">
+                        <form:option value="">-- Empty --</form:option>
+                        <form:options items="${profileList}" itemLabel="itemDescription" itemValue="chargingProfilePk"/>
+                    </form:select>
+                    <form:errors path="chargingProfilePk" cssClass="error"/>
                 </td>
             </tr>
         </c:if>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<c:if test="${profileForRemoteStartTx}">
<tr>
<td>Charging Profile ID:</td>
<td>
<form:select path="chargingProfilePk">
<option value="" selected>-- Empty --</option>
<form:options items="${profileList}" itemLabel="itemDescription" itemValue="chargingProfilePk"/>
</form:select>
</td>
</tr>
</c:if>
<c:if test="${profileForRemoteStartTx}">
<tr>
<td>Charging Profile ID:</td>
<td>
<form:select path="chargingProfilePk">
<form:option value="">-- Empty --</form:option>
<form:options items="${profileList}" itemLabel="itemDescription" itemValue="chargingProfilePk"/>
</form:select>
<form:errors path="chargingProfilePk" cssClass="error"/>
</td>
</tr>
</c:if>
🤖 Prompt for AI Agents
In
steve-ui-jsp/src/main/resources/webapp/WEB-INF/views/op-forms/RemoteStartTransactionForm.jsp
around lines 36 to 46, the empty option is hard-coded with selected which
prevents Spring form binding from restoring a previously chosen value and
suppresses validation feedback; replace the hard-coded HTML option with a Spring
form:option value="" (no selected attribute) so the form taglib can bind the
value, ensure the select uses form:options as already present, and add a
<form:errors path="chargingProfilePk" /> next to the select to display
validation messages; chargingProfilePk is a nullable Integer so the empty value
must be allowed.

<tr><td></td><td><div class="submit-button"><input type="submit" value="Perform"></div></td></tr>
</table>
</form:form>
</form:form>
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
<td>Connector ID (integer):</td>
<td><form:input path="connectorId" placeholder="0 = charge point as a whole"/></td>
</tr>
<tr>
<td>Transaction ID (integer):</td>
<td><form:input path="transactionId" placeholder="only necessary for TxProfile"/></td>
</tr>
<tr><td></td><td><div class="submit-button"><input type="submit" value="Perform"></div></td></tr>
</table>
</form:form>
</form:form>
Loading