Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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 @@ -48,7 +48,7 @@ public class CentralSystemService16_ServiceValidator {
private static final DateTime MAX = new DateTime(Long.MAX_VALUE);

private final Clock clock;
private final Duration operationalDeltaForNow;
private final Duration operationalDelta;

@Autowired
public CentralSystemService16_ServiceValidator(Clock clock) {
Expand All @@ -64,7 +64,7 @@ public SteveException validateStart(StartTransactionRequest params) {
return new SteveException("StartTransaction.meterStart must not be negative");
}

if (params.getTimestamp().getMillis() > clock.instant().plus(operationalDeltaForNow).toEpochMilli()) {
if (params.getTimestamp().getMillis() > clock.instant().plus(operationalDelta).toEpochMilli()) {
return new SteveException("StartTransaction.timestamp is in the future");
}

Expand All @@ -88,7 +88,7 @@ public SteveException validateStop(TransactionRecord thisTx, StopTransactionRequ
return new SteveException("start.timestamp is after stop.timestamp");
}

if (stopParams.getTimestamp().getMillis() > clock.instant().plus(operationalDeltaForNow).toEpochMilli()) {
if (stopParams.getTimestamp().getMillis() > clock.instant().plus(operationalDelta).toEpochMilli()) {
return new SteveException("stop.timestamp is in the future");
}

Expand Down Expand Up @@ -145,19 +145,22 @@ private SteveException validateMeterValuesInternal(List<MeterValue> meterValues,
return new SteveException("MeterValue.timestamp is empty");
}

if (latest.getMillis() > clock.instant().plus(operationalDeltaForNow).toEpochMilli()) {
// allow operational delta tolerance for the following timestamp checks, since charge points
// may have slight clock drift and meter values can be sampled a little bit later or before
// our reference point.
long deltaMillis = operationalDelta.toMillis();

if (latest.getMillis() > clock.instant().toEpochMilli() + deltaMillis) {
return new SteveException("at least one MeterValue.timestamp is in the future");
}

if (stopTimestamp != null && latest.isAfter(stopTimestamp)) {
return new SteveException("at least one MeterValue.timestamp is after stop.timestamp");
if (stopTimestamp != null) {
if (latest.getMillis() > stopTimestamp.getMillis() + deltaMillis) {
return new SteveException("at least one MeterValue.timestamp is after stop.timestamp");
}
}

// allow the same operational delta tolerance for start timestamp check, since charge points
// may have slight clock drift and meter values can be sampled before the StartTransaction
// message is processed on the server side
if (startTimestamp != null) {
long deltaMillis = operationalDeltaForNow.toMillis();
if (earliest.getMillis() < startTimestamp.getMillis() - deltaMillis) {
return new SteveException("at least one MeterValue.timestamp is before start.timestamp");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,10 @@ public void validateStop_transactionDataFutureTimestamp_returnsError() {

@Test
public void validateStop_transactionDataAfterStopTimestamp_returnsError() {
// more than 5 minutes (operational delta) after stop
var tx = tx("100", DateTime.parse("2026-02-17T09:00:00Z"), null, null, null);
var params = stopParams(DateTime.parse("2026-02-17T10:00:00Z"), "200")
.withTransactionData(List.of(meterValue("2026-02-17T10:00:01Z")));
.withTransactionData(List.of(meterValue("2026-02-17T10:10:00Z")));
var result = validator.validateStop(tx, params);

Assertions.assertNotNull(result);
Expand Down Expand Up @@ -251,6 +252,22 @@ public void validateStop_transactionDataAtStartTimestamp_isAllowed() {
Assertions.assertNull(result);
}

/**
* this behavior is coming from a real station in the field: the latest of meterValues is 1 second
* after stopTimestamp, because the station sampled stop timestamp first, and then later the timestamp
* for this meterValue entry.
*/
@Test
public void validateStop_transactionDataOneSecondAfterStop_isAllowed() {
var tx = tx("100", DateTime.parse("2026-02-17T09:00:00Z"), null, null, null);

var params = stopParams(DateTime.parse("2026-02-17T10:00:00Z"), "200")
.withTransactionData(List.of(meterValue("2026-02-17T10:00:01Z")));
var result = validator.validateStop(tx, params);

Assertions.assertNull(result);
}

@Test
public void validateMeterValues_timestampsOutOfOrder_returnsError() {
var result = validator.validateMeterValues(meterValuesParams(1, List.of(
Expand Down