Add esper driving interruption projections

This commit is contained in:
trifonovt 2026-05-13 12:35:39 +02:00
parent 0e2b83270c
commit eb4e04f144
12 changed files with 717 additions and 32 deletions

View File

@ -374,6 +374,38 @@
} }
} }
}, },
{
"name": "Process tachograph file session Esper events",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"significantDrivingMinutes\": 3\n}"
},
"url": {
"raw": "{{baseUrl}}/api/eventhub/tachograph-file-sessions/{{sessionId}}/drivers/{{driverKey}}/processing/esper-events",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"eventhub",
"tachograph-file-sessions",
"{{sessionId}}",
"drivers",
"{{driverKey}}",
"processing",
"esper-events"
]
}
}
},
{ {
"name": "Process tachograph file session operating periods", "name": "Process tachograph file session operating periods",
"request": { "request": {

View File

@ -1,6 +1,7 @@
package at.procon.eventhub.tachographfilesession.api; package at.procon.eventhub.tachographfilesession.api;
import at.procon.eventhub.tachographfilesession.dto.CreateTachographFileSessionResponse; import at.procon.eventhub.tachographfilesession.dto.CreateTachographFileSessionResponse;
import at.procon.eventhub.tachographfilesession.dto.TachographEsperEventsProcessingRequest;
import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto; import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto;
import at.procon.eventhub.tachographfilesession.dto.TachographFileDriverDetailDto; import at.procon.eventhub.tachographfilesession.dto.TachographFileDriverDetailDto;
import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingRequest; import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingRequest;
@ -75,6 +76,15 @@ public class TachographFileSessionController {
return ResponseEntity.ok(processingService.getEsperDriverProcessingResults(sessionId, driverKey)); return ResponseEntity.ok(processingService.getEsperDriverProcessingResults(sessionId, driverKey));
} }
@PostMapping("/{sessionId}/drivers/{driverKey}/processing/esper-events")
public ResponseEntity<TachographEsperDriverProcessingResultDto> evaluateEsperDriverProcessingResults(
@PathVariable UUID sessionId,
@PathVariable String driverKey,
@RequestBody(required = false) TachographEsperEventsProcessingRequest request
) {
return ResponseEntity.ok(processingService.getEsperDriverProcessingResults(sessionId, driverKey, request));
}
@PostMapping("/{sessionId}/drivers/{driverKey}/processing/operating-periods") @PostMapping("/{sessionId}/drivers/{driverKey}/processing/operating-periods")
public ResponseEntity<TachographOperatingPeriodsProcessingResultDto> evaluateOperatingPeriods( public ResponseEntity<TachographOperatingPeriodsProcessingResultDto> evaluateOperatingPeriods(
@PathVariable UUID sessionId, @PathVariable UUID sessionId,

View File

@ -1,6 +1,7 @@
package at.procon.eventhub.tachographfilesession.dto; package at.procon.eventhub.tachographfilesession.dto;
import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
@ -13,12 +14,16 @@ public record TachographEsperDriverProcessingResultDto(
String sourceKind, String sourceKind,
OffsetDateTime loadedFrom, OffsetDateTime loadedFrom,
OffsetDateTime loadedTo, OffsetDateTime loadedTo,
OffsetDateTime requestedFrom,
OffsetDateTime requestedTo,
int activityIntervalCount, int activityIntervalCount,
int drivingIntervalCount, int drivingIntervalCount,
int drivingInterruptionIntervalCount,
int vehicleUsageIntervalCount, int vehicleUsageIntervalCount,
int vuCardAbsentIntervalCount, int vuCardAbsentIntervalCount,
List<TachographEsperActivityIntervalEvent> activityIntervals, List<TachographEsperActivityIntervalEvent> activityIntervals,
List<TachographEsperActivityIntervalEvent> drivingIntervals, List<TachographEsperActivityIntervalEvent> drivingIntervals,
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals,
List<TachographEsperVehicleUsageIntervalEvent> vehicleUsageIntervals, List<TachographEsperVehicleUsageIntervalEvent> vehicleUsageIntervals,
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals, List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals,
List<String> notes List<String> notes

View File

@ -0,0 +1,13 @@
package at.procon.eventhub.tachographfilesession.dto;
import java.time.OffsetDateTime;
public record TachographEsperEventsProcessingRequest(
OffsetDateTime occurredFrom,
OffsetDateTime occurredTo,
Integer significantDrivingMinutes
) {
public TachographEsperEventsProcessingRequest {
significantDrivingMinutes = significantDrivingMinutes == null ? null : Math.max(1, significantDrivingMinutes);
}
}

View File

@ -0,0 +1,17 @@
package at.procon.eventhub.tachographfilesession.model;
import java.time.OffsetDateTime;
import java.util.UUID;
public record TachographEsperDrivingInterruptionIntervalEvent(
UUID sessionId,
String driverKey,
OffsetDateTime startedAt,
OffsetDateTime endedAt,
long durationSeconds,
String previousDrivingSourceIntervalId,
String nextDrivingSourceIntervalId,
String previousVehicleKey,
String nextVehicleKey
) {
}

View File

@ -19,12 +19,15 @@ import at.procon.eventhub.tachographfilesession.model.ResolvedActivityInterval;
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval; import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval;
import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographFileSession; import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@ -47,6 +50,8 @@ public class DriverTimelineBuilder {
loadResource("esper/tachograph-activity-interval-events.epl"); loadResource("esper/tachograph-activity-interval-events.epl");
private static final String DRIVING_INTERVAL_EVENTS_EPL = private static final String DRIVING_INTERVAL_EVENTS_EPL =
loadResource("esper/tachograph-driving-interval-events.epl"); loadResource("esper/tachograph-driving-interval-events.epl");
private static final String DRIVING_INTERRUPTION_INTERVAL_EVENTS_EPL_TEMPLATE =
loadResource("esper/tachograph-driving-interruption-interval-events.epl");
private static final String VEHICLE_USAGE_INTERVAL_EVENTS_EPL = private static final String VEHICLE_USAGE_INTERVAL_EVENTS_EPL =
loadResource("esper/tachograph-vehicle-usage-interval-events.epl"); loadResource("esper/tachograph-vehicle-usage-interval-events.epl");
private static final String VU_CARD_ABSENT_INTERVAL_EVENTS_EPL = private static final String VU_CARD_ABSENT_INTERVAL_EVENTS_EPL =
@ -139,6 +144,38 @@ public class DriverTimelineBuilder {
return timeline == null ? List.of() : buildEsperVehicleUsageIntervalEvents(timeline.vehicleUsageIntervals()); return timeline == null ? List.of() : buildEsperVehicleUsageIntervalEvents(timeline.vehicleUsageIntervals());
} }
public List<TachographEsperDrivingInterruptionIntervalEvent> buildEsperDrivingInterruptionIntervalEvents(
TachographFileSession session,
DriverExtractionSession driverSession,
int significantDrivingMinutes
) {
String sourceKind = session.metadata().driverCardFile() ? "DRIVER_CARD" : "VEHICLE_UNIT";
List<ResolvedActivityInterval> activityIntervals = resolveActivities(driverSession.cardActivityIntervals(), sourceKind);
return buildEsperDrivingInterruptionIntervalEvents(
session.sessionId(),
driverSession.driverKey(),
activityIntervals,
significantDrivingMinutes
);
}
public List<TachographEsperDrivingInterruptionIntervalEvent> buildEsperDrivingInterruptionIntervalEvents(
UUID sessionId,
String driverKey,
ResolvedDriverTimeline timeline,
int significantDrivingMinutes
) {
if (timeline == null) {
return List.of();
}
return buildEsperDrivingInterruptionIntervalEvents(
sessionId,
driverKey,
timeline.activityIntervals(),
significantDrivingMinutes
);
}
public List<TachographEsperVuCardAbsentIntervalEvent> buildEsperVuCardAbsentIntervalEvents( public List<TachographEsperVuCardAbsentIntervalEvent> buildEsperVuCardAbsentIntervalEvents(
TachographFileSession session, TachographFileSession session,
DriverExtractionSession driverSession DriverExtractionSession driverSession
@ -244,6 +281,36 @@ public class DriverTimelineBuilder {
return result; return result;
} }
private List<TachographEsperDrivingInterruptionIntervalEvent> buildEsperDrivingInterruptionIntervalEvents(
UUID sessionId,
String driverKey,
List<ResolvedActivityInterval> activityIntervals,
int significantDrivingMinutes
) {
if (activityIntervals == null || activityIntervals.size() < 2) {
return List.of();
}
List<TachographEsperDrivingInterruptionIntervalEvent> result = new ArrayList<>();
executeWithRuntime(
configuration -> configuration.getCommon().addEventType(
"TachographActivityIntervalInputEvent",
activityIntervalInputDefinition()
),
renderDrivingInterruptionIntervalEventsEpl(significantDrivingMinutes),
"drivingInterruptionIntervals",
newData -> collectDrivingInterruptionIntervalEvents(newData, result),
runtime -> {
for (ResolvedActivityInterval interval : activityIntervals) {
runtime.getEventService().sendEventMap(
toActivityIntervalInputMap(sessionId, driverKey, interval),
"TachographActivityIntervalInputEvent"
);
}
}
);
return result;
}
private List<TachographEsperVuCardAbsentIntervalEvent> buildEsperVuCardAbsentIntervalEvents( private List<TachographEsperVuCardAbsentIntervalEvent> buildEsperVuCardAbsentIntervalEvents(
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals List<ResolvedVehicleUsageInterval> vehicleUsageIntervals
) { ) {
@ -477,7 +544,10 @@ public class DriverTimelineBuilder {
sender.accept(runtime); sender.accept(runtime);
} catch (EPCompileException | EPDeployException e) { } catch (EPCompileException | EPDeployException e) {
throw new IllegalStateException("Cannot compile/deploy tachograph projection EPL", e); throw new IllegalStateException(
"Cannot compile/deploy tachograph projection EPL for statement '" + statementName + "'",
e
);
} finally { } finally {
if (runtime != null) { if (runtime != null) {
runtime.destroy(); runtime.destroy();
@ -497,8 +567,12 @@ public class DriverTimelineBuilder {
definition.put("registrationKey", String.class); definition.put("registrationKey", String.class);
definition.put("vehicleKey", String.class); definition.put("vehicleKey", String.class);
definition.put("sourceKind", String.class); definition.put("sourceKind", String.class);
definition.put("firstSourceIntervalId", String.class);
definition.put("lastSourceIntervalId", String.class);
definition.put("startedAt", OffsetDateTime.class); definition.put("startedAt", OffsetDateTime.class);
definition.put("endedAt", OffsetDateTime.class); definition.put("endedAt", OffsetDateTime.class);
definition.put("startedAtEpochSecond", long.class);
definition.put("endedAtEpochSecond", long.class);
definition.put("durationSeconds", long.class); definition.put("durationSeconds", long.class);
definition.put("sourceIntervalIds", java.util.List.class); definition.put("sourceIntervalIds", java.util.List.class);
definition.put("synthetic", boolean.class); definition.put("synthetic", boolean.class);
@ -512,8 +586,12 @@ public class DriverTimelineBuilder {
definition.put("sessionId", UUID.class); definition.put("sessionId", UUID.class);
definition.put("driverKey", String.class); definition.put("driverKey", String.class);
definition.put("intervalId", String.class); definition.put("intervalId", String.class);
definition.put("firstSourceIntervalId", String.class);
definition.put("lastSourceIntervalId", String.class);
definition.put("startedAt", OffsetDateTime.class); definition.put("startedAt", OffsetDateTime.class);
definition.put("endedAt", OffsetDateTime.class); definition.put("endedAt", OffsetDateTime.class);
definition.put("startedAtEpochSecond", long.class);
definition.put("endedAtEpochSecond", Long.class);
definition.put("durationSeconds", long.class); definition.put("durationSeconds", long.class);
definition.put("odometerBeginKm", Long.class); definition.put("odometerBeginKm", Long.class);
definition.put("odometerEndKm", Long.class); definition.put("odometerEndKm", Long.class);
@ -540,8 +618,12 @@ public class DriverTimelineBuilder {
event.put("registrationKey", interval.registrationKey()); event.put("registrationKey", interval.registrationKey());
event.put("vehicleKey", interval.vehicleKey()); event.put("vehicleKey", interval.vehicleKey());
event.put("sourceKind", interval.sourceKind()); event.put("sourceKind", interval.sourceKind());
event.put("firstSourceIntervalId", firstSourceIntervalId(interval));
event.put("lastSourceIntervalId", lastSourceIntervalId(interval));
event.put("startedAt", interval.from()); event.put("startedAt", interval.from());
event.put("endedAt", interval.to()); event.put("endedAt", interval.to());
event.put("startedAtEpochSecond", interval.from().toEpochSecond());
event.put("endedAtEpochSecond", interval.to().toEpochSecond());
event.put("durationSeconds", interval.durationSeconds()); event.put("durationSeconds", interval.durationSeconds());
event.put("sourceIntervalIds", interval.sourceIntervalIds()); event.put("sourceIntervalIds", interval.sourceIntervalIds());
event.put("synthetic", interval.synthetic()); event.put("synthetic", interval.synthetic());
@ -550,13 +632,27 @@ public class DriverTimelineBuilder {
return event; return event;
} }
private String firstSourceIntervalId(ResolvedActivityInterval interval) {
return interval.sourceIntervalIds().isEmpty() ? interval.intervalId() : interval.sourceIntervalIds().get(0);
}
private String lastSourceIntervalId(ResolvedActivityInterval interval) {
return interval.sourceIntervalIds().isEmpty()
? interval.intervalId()
: interval.sourceIntervalIds().get(interval.sourceIntervalIds().size() - 1);
}
private Map<String, Object> toVehicleUsageIntervalInputMap(ResolvedVehicleUsageInterval interval) { private Map<String, Object> toVehicleUsageIntervalInputMap(ResolvedVehicleUsageInterval interval) {
Map<String, Object> event = new LinkedHashMap<>(); Map<String, Object> event = new LinkedHashMap<>();
event.put("sessionId", interval.sessionId()); event.put("sessionId", interval.sessionId());
event.put("driverKey", interval.driverKey()); event.put("driverKey", interval.driverKey());
event.put("intervalId", interval.intervalId()); event.put("intervalId", interval.intervalId());
event.put("firstSourceIntervalId", firstSourceIntervalId(interval));
event.put("lastSourceIntervalId", lastSourceIntervalId(interval));
event.put("startedAt", interval.from()); event.put("startedAt", interval.from());
event.put("endedAt", interval.to()); event.put("endedAt", interval.to());
event.put("startedAtEpochSecond", interval.from().toEpochSecond());
event.put("endedAtEpochSecond", interval.to() == null ? null : interval.to().toEpochSecond());
event.put("durationSeconds", interval.durationSeconds()); event.put("durationSeconds", interval.durationSeconds());
event.put("odometerBeginKm", interval.odometerBeginKm()); event.put("odometerBeginKm", interval.odometerBeginKm());
event.put("odometerEndKm", interval.odometerEndKm()); event.put("odometerEndKm", interval.odometerEndKm());
@ -567,6 +663,16 @@ public class DriverTimelineBuilder {
return event; return event;
} }
private String firstSourceIntervalId(ResolvedVehicleUsageInterval interval) {
return interval.sourceIntervalIds().isEmpty() ? interval.intervalId() : interval.sourceIntervalIds().get(0);
}
private String lastSourceIntervalId(ResolvedVehicleUsageInterval interval) {
return interval.sourceIntervalIds().isEmpty()
? interval.intervalId()
: interval.sourceIntervalIds().get(interval.sourceIntervalIds().size() - 1);
}
private void collectActivityIntervalEvents( private void collectActivityIntervalEvents(
EventBean[] newData, EventBean[] newData,
List<TachographEsperActivityIntervalEvent> target List<TachographEsperActivityIntervalEvent> target
@ -622,6 +728,30 @@ public class DriverTimelineBuilder {
} }
} }
private void collectDrivingInterruptionIntervalEvents(
EventBean[] newData,
List<TachographEsperDrivingInterruptionIntervalEvent> target
) {
if (newData == null) {
return;
}
for (EventBean event : newData) {
long startedAtEpochSecond = (Long) event.get("startedAtEpochSecond");
long endedAtEpochSecond = (Long) event.get("endedAtEpochSecond");
target.add(new TachographEsperDrivingInterruptionIntervalEvent(
(UUID) event.get("sessionId"),
(String) event.get("driverKey"),
OffsetDateTime.ofInstant(Instant.ofEpochSecond(startedAtEpochSecond), ZoneOffset.UTC),
OffsetDateTime.ofInstant(Instant.ofEpochSecond(endedAtEpochSecond), ZoneOffset.UTC),
(Long) event.get("durationSeconds"),
(String) event.get("previousDrivingSourceIntervalId"),
(String) event.get("nextDrivingSourceIntervalId"),
(String) event.get("previousVehicleKey"),
(String) event.get("nextVehicleKey")
));
}
}
private void collectVuCardAbsentIntervalEvents( private void collectVuCardAbsentIntervalEvents(
EventBean[] newData, EventBean[] newData,
List<TachographEsperVuCardAbsentIntervalEvent> target List<TachographEsperVuCardAbsentIntervalEvent> target
@ -630,11 +760,13 @@ public class DriverTimelineBuilder {
return; return;
} }
for (EventBean event : newData) { for (EventBean event : newData) {
long startedAtEpochSecond = (Long) event.get("startedAtEpochSecond");
long endedAtEpochSecond = (Long) event.get("endedAtEpochSecond");
target.add(new TachographEsperVuCardAbsentIntervalEvent( target.add(new TachographEsperVuCardAbsentIntervalEvent(
(UUID) event.get("sessionId"), (UUID) event.get("sessionId"),
(String) event.get("driverKey"), (String) event.get("driverKey"),
(OffsetDateTime) event.get("startedAt"), OffsetDateTime.ofInstant(Instant.ofEpochSecond(startedAtEpochSecond), ZoneOffset.UTC),
(OffsetDateTime) event.get("endedAt"), OffsetDateTime.ofInstant(Instant.ofEpochSecond(endedAtEpochSecond), ZoneOffset.UTC),
(Long) event.get("durationSeconds"), (Long) event.get("durationSeconds"),
(String) event.get("previousUsageIntervalId"), (String) event.get("previousUsageIntervalId"),
(String) event.get("nextUsageIntervalId"), (String) event.get("nextUsageIntervalId"),
@ -659,4 +791,12 @@ public class DriverTimelineBuilder {
throw new IllegalStateException("Cannot load EPL resource: " + path, e); throw new IllegalStateException("Cannot load EPL resource: " + path, e);
} }
} }
private String renderDrivingInterruptionIntervalEventsEpl(int significantDrivingMinutes) {
long thresholdSeconds = Math.max(1, significantDrivingMinutes) * 60L;
return DRIVING_INTERRUPTION_INTERVAL_EVENTS_EPL_TEMPLATE.replace(
"${SIGNIFICANT_DRIVING_THRESHOLD_SECONDS}",
Long.toString(thresholdSeconds)
);
}
} }

View File

@ -1,6 +1,7 @@
package at.procon.eventhub.tachographfilesession.service; package at.procon.eventhub.tachographfilesession.service;
import at.procon.eventhub.config.EventHubProperties; import at.procon.eventhub.config.EventHubProperties;
import at.procon.eventhub.tachographfilesession.dto.TachographEsperEventsProcessingRequest;
import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto; import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto;
import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingRequest; import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingRequest;
import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingResultDto; import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingResultDto;
@ -12,6 +13,7 @@ import at.procon.eventhub.tachographfilesession.model.ProcessedShiftDrivingEvalu
import at.procon.eventhub.tachographfilesession.model.ResolvedActivityInterval; import at.procon.eventhub.tachographfilesession.model.ResolvedActivityInterval;
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographFileSession; import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent;
@ -121,6 +123,17 @@ public class TachographFileSessionProcessingService {
UUID sessionId, UUID sessionId,
String driverKey String driverKey
) { ) {
return getEsperDriverProcessingResults(sessionId, driverKey, null);
}
public TachographEsperDriverProcessingResultDto getEsperDriverProcessingResults(
UUID sessionId,
String driverKey,
TachographEsperEventsProcessingRequest request
) {
TachographEsperEventsProcessingRequest effectiveRequest = request == null
? new TachographEsperEventsProcessingRequest(null, null, null)
: request;
TachographFileSession session = repository.find(sessionId) TachographFileSession session = repository.find(sessionId)
.orElseThrow(() -> new TachographFileSessionNotFoundException(sessionId)); .orElseThrow(() -> new TachographFileSessionNotFoundException(sessionId));
DriverExtractionSession driver = session.driversByKey().get(driverKey); DriverExtractionSession driver = session.driversByKey().get(driverKey);
@ -129,14 +142,44 @@ public class TachographFileSessionProcessingService {
} }
ResolvedDriverTimeline timeline = driverTimelineBuilder.build(session, driver); ResolvedDriverTimeline timeline = driverTimelineBuilder.build(session, driver);
List<TachographEsperActivityIntervalEvent> activityIntervals = OffsetDateTime requestedFrom = effectiveRequest.occurredFrom() == null ? timeline.loadedFrom() : utc(effectiveRequest.occurredFrom());
driverTimelineBuilder.buildEsperActivityIntervalEvents(sessionId, driverKey, timeline); OffsetDateTime requestedTo = effectiveRequest.occurredTo() == null ? timeline.loadedTo() : utc(effectiveRequest.occurredTo());
List<TachographEsperActivityIntervalEvent> drivingIntervals = if (requestedFrom != null && requestedTo != null && requestedTo.isBefore(requestedFrom)) {
driverTimelineBuilder.buildEsperDrivingIntervalEvents(sessionId, driverKey, timeline); throw new IllegalArgumentException("occurredTo must not be before occurredFrom.");
List<TachographEsperVehicleUsageIntervalEvent> vehicleUsageIntervals = }
driverTimelineBuilder.buildEsperVehicleUsageIntervalEvents(timeline); int significantDrivingMinutes = resolveEsperSignificantDrivingMinutes(effectiveRequest);
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals =
driverTimelineBuilder.buildEsperVuCardAbsentIntervalEvents(timeline); List<TachographEsperActivityIntervalEvent> activityIntervals = clipEsperActivityIntervalEvents(
driverTimelineBuilder.buildEsperActivityIntervalEvents(sessionId, driverKey, timeline),
requestedFrom,
requestedTo
);
List<TachographEsperActivityIntervalEvent> drivingIntervals = clipEsperActivityIntervalEvents(
driverTimelineBuilder.buildEsperDrivingIntervalEvents(sessionId, driverKey, timeline),
requestedFrom,
requestedTo
);
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals =
clipEsperDrivingInterruptionIntervalEvents(
driverTimelineBuilder.buildEsperDrivingInterruptionIntervalEvents(
sessionId,
driverKey,
timeline,
significantDrivingMinutes
),
requestedFrom,
requestedTo
);
List<TachographEsperVehicleUsageIntervalEvent> vehicleUsageIntervals = clipEsperVehicleUsageIntervalEvents(
driverTimelineBuilder.buildEsperVehicleUsageIntervalEvents(timeline),
requestedFrom,
requestedTo
);
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals = clipEsperVuCardAbsentIntervalEvents(
driverTimelineBuilder.buildEsperVuCardAbsentIntervalEvents(timeline),
requestedFrom,
requestedTo
);
return new TachographEsperDriverProcessingResultDto( return new TachographEsperDriverProcessingResultDto(
sessionId, sessionId,
@ -144,18 +187,174 @@ public class TachographFileSessionProcessingService {
timeline.sourceKind(), timeline.sourceKind(),
timeline.loadedFrom(), timeline.loadedFrom(),
timeline.loadedTo(), timeline.loadedTo(),
requestedFrom,
requestedTo,
activityIntervals.size(), activityIntervals.size(),
drivingIntervals.size(), drivingIntervals.size(),
drivingInterruptionIntervals.size(),
vehicleUsageIntervals.size(), vehicleUsageIntervals.size(),
vuCardAbsentIntervals.size(), vuCardAbsentIntervals.size(),
activityIntervals, activityIntervals,
drivingIntervals, drivingIntervals,
drivingInterruptionIntervals,
vehicleUsageIntervals, vehicleUsageIntervals,
vuCardAbsentIntervals, vuCardAbsentIntervals,
esperProjectionNotes() esperProjectionNotes()
); );
} }
private List<TachographEsperActivityIntervalEvent> clipEsperActivityIntervalEvents(
List<TachographEsperActivityIntervalEvent> intervals,
OffsetDateTime requestedFrom,
OffsetDateTime requestedTo
) {
if (requestedFrom == null || requestedTo == null) {
return List.of();
}
return intervals.stream()
.map(interval -> {
OffsetDateTime start = max(interval.startedAt(), requestedFrom);
OffsetDateTime end = min(interval.endedAt(), requestedTo);
if (!end.isAfter(start)) {
return null;
}
boolean clipped = interval.clippedToRequestedPeriod()
|| !start.equals(interval.startedAt())
|| !end.equals(interval.endedAt());
return new TachographEsperActivityIntervalEvent(
interval.sessionId(),
interval.driverKey(),
interval.intervalId(),
interval.activityType(),
interval.cardSlot(),
interval.cardStatus(),
interval.drivingStatus(),
interval.registrationKey(),
interval.vehicleKey(),
interval.sourceKind(),
start,
end,
Duration.between(start, end).getSeconds(),
interval.sourceIntervalIds(),
interval.synthetic(),
clipped,
interval.level()
);
})
.filter(Objects::nonNull)
.sorted(Comparator.comparing(TachographEsperActivityIntervalEvent::startedAt)
.thenComparing(TachographEsperActivityIntervalEvent::endedAt)
.thenComparing(TachographEsperActivityIntervalEvent::activityType, Comparator.nullsLast(String::compareTo)))
.toList();
}
private List<TachographEsperVehicleUsageIntervalEvent> clipEsperVehicleUsageIntervalEvents(
List<TachographEsperVehicleUsageIntervalEvent> intervals,
OffsetDateTime requestedFrom,
OffsetDateTime requestedTo
) {
if (requestedFrom == null || requestedTo == null) {
return List.of();
}
return intervals.stream()
.map(interval -> {
OffsetDateTime start = max(interval.startedAt(), requestedFrom);
OffsetDateTime end = min(interval.endedAt(), requestedTo);
if (!end.isAfter(start)) {
return null;
}
boolean startClipped = !start.equals(interval.startedAt());
boolean endClipped = !end.equals(interval.endedAt());
return new TachographEsperVehicleUsageIntervalEvent(
interval.sessionId(),
interval.driverKey(),
interval.intervalId(),
start,
end,
Duration.between(start, end).getSeconds(),
startClipped ? null : interval.odometerBeginKm(),
endClipped ? null : interval.odometerEndKm(),
interval.registrationKey(),
interval.vehicleKey(),
interval.sourceKind(),
interval.sourceIntervalIds()
);
})
.filter(Objects::nonNull)
.sorted(Comparator.comparing(TachographEsperVehicleUsageIntervalEvent::startedAt)
.thenComparing(TachographEsperVehicleUsageIntervalEvent::endedAt)
.thenComparing(TachographEsperVehicleUsageIntervalEvent::intervalId, Comparator.nullsLast(String::compareTo)))
.toList();
}
private List<TachographEsperDrivingInterruptionIntervalEvent> clipEsperDrivingInterruptionIntervalEvents(
List<TachographEsperDrivingInterruptionIntervalEvent> intervals,
OffsetDateTime requestedFrom,
OffsetDateTime requestedTo
) {
if (requestedFrom == null || requestedTo == null) {
return List.of();
}
return intervals.stream()
.map(interval -> {
OffsetDateTime start = max(interval.startedAt(), requestedFrom);
OffsetDateTime end = min(interval.endedAt(), requestedTo);
if (!end.isAfter(start)) {
return null;
}
return new TachographEsperDrivingInterruptionIntervalEvent(
interval.sessionId(),
interval.driverKey(),
start,
end,
Duration.between(start, end).getSeconds(),
interval.previousDrivingSourceIntervalId(),
interval.nextDrivingSourceIntervalId(),
interval.previousVehicleKey(),
interval.nextVehicleKey()
);
})
.filter(Objects::nonNull)
.sorted(Comparator.comparing(TachographEsperDrivingInterruptionIntervalEvent::startedAt)
.thenComparing(TachographEsperDrivingInterruptionIntervalEvent::endedAt))
.toList();
}
private List<TachographEsperVuCardAbsentIntervalEvent> clipEsperVuCardAbsentIntervalEvents(
List<TachographEsperVuCardAbsentIntervalEvent> intervals,
OffsetDateTime requestedFrom,
OffsetDateTime requestedTo
) {
if (requestedFrom == null || requestedTo == null) {
return List.of();
}
return intervals.stream()
.map(interval -> {
OffsetDateTime start = max(interval.startedAt(), requestedFrom);
OffsetDateTime end = min(interval.endedAt(), requestedTo);
if (!end.isAfter(start)) {
return null;
}
return new TachographEsperVuCardAbsentIntervalEvent(
interval.sessionId(),
interval.driverKey(),
start,
end,
Duration.between(start, end).getSeconds(),
interval.previousUsageIntervalId(),
interval.nextUsageIntervalId(),
interval.previousRegistrationKey(),
interval.nextRegistrationKey(),
interval.previousVehicleKey(),
interval.nextVehicleKey()
);
})
.filter(Objects::nonNull)
.sorted(Comparator.comparing(TachographEsperVuCardAbsentIntervalEvent::startedAt)
.thenComparing(TachographEsperVuCardAbsentIntervalEvent::endedAt))
.toList();
}
private List<ResolvedActivityInterval> synthesizeUnknownGaps( private List<ResolvedActivityInterval> synthesizeUnknownGaps(
List<ResolvedActivityInterval> knownIntervals, List<ResolvedActivityInterval> knownIntervals,
Duration gapDetectionTolerance Duration gapDetectionTolerance
@ -694,6 +893,12 @@ public class TachographFileSessionProcessingService {
: request.gapDetectionToleranceSeconds(); : request.gapDetectionToleranceSeconds();
} }
private int resolveEsperSignificantDrivingMinutes(TachographEsperEventsProcessingRequest request) {
return request.significantDrivingMinutes() == null
? properties.getTachographFileSession().getProcessing().getSignificantDrivingMinutes()
: request.significantDrivingMinutes();
}
private List<String> notes() { private List<String> notes() {
return List.of( return List.of(
"This endpoint evaluates operating periods from the in-memory tachograph file-session model.", "This endpoint evaluates operating periods from the in-memory tachograph file-session model.",
@ -707,7 +912,10 @@ public class TachographFileSessionProcessingService {
return List.of( return List.of(
"This endpoint returns Esper-backed per-driver interval projections from the in-memory tachograph file-session model.", "This endpoint returns Esper-backed per-driver interval projections from the in-memory tachograph file-session model.",
"Driving intervals are a filtered projection of activity intervals where activityType = DRIVE.", "Driving intervals are a filtered projection of activity intervals where activityType = DRIVE.",
"VU card-absent intervals are gaps between consecutive normalized vehicle-usage intervals for the same driver." "Driving interruption intervals are gaps between consecutive driving intervals longer than the configured significant-driving threshold.",
"VU card-absent intervals are gaps between consecutive normalized vehicle-usage intervals for the same driver.",
"occurredFrom and occurredTo clip the returned interval projections to the requested UTC time window.",
"Vehicle-usage intervals clear clipped odometer endpoints because boundary odometer values cannot be recomputed safely from the source interval."
); );
} }

View File

@ -0,0 +1,64 @@
create schema SignificantDrivingInterval(
sessionId java.util.UUID,
driverKey string,
firstSourceIntervalId string,
lastSourceIntervalId string,
startedAtEpochSecond long,
endedAtEpochSecond long,
durationSeconds long,
vehicleKey string
);
create schema DrivingInterruptionInterval(
sessionId java.util.UUID,
driverKey string,
startedAtEpochSecond long,
endedAtEpochSecond long,
durationSeconds long,
previousDrivingSourceIntervalId string,
nextDrivingSourceIntervalId string,
previousVehicleKey string,
nextVehicleKey string
);
insert into SignificantDrivingInterval
select
sessionId,
driverKey,
firstSourceIntervalId,
lastSourceIntervalId,
startedAtEpochSecond,
endedAtEpochSecond,
durationSeconds,
vehicleKey
from TachographActivityIntervalInputEvent(activityType = 'DRIVE', durationSeconds > ${SIGNIFICANT_DRIVING_THRESHOLD_SECONDS});
create window PreviousSignificantDrivingInterval#unique(driverKey) as SignificantDrivingInterval;
on SignificantDrivingInterval as next
insert into DrivingInterruptionInterval
select
priorInterval.sessionId as sessionId,
priorInterval.driverKey as driverKey,
priorInterval.endedAtEpochSecond as startedAtEpochSecond,
next.startedAtEpochSecond as endedAtEpochSecond,
next.startedAtEpochSecond - priorInterval.endedAtEpochSecond as durationSeconds,
priorInterval.lastSourceIntervalId as previousDrivingSourceIntervalId,
next.firstSourceIntervalId as nextDrivingSourceIntervalId,
priorInterval.vehicleKey as previousVehicleKey,
next.vehicleKey as nextVehicleKey
from PreviousSignificantDrivingInterval as priorInterval
where priorInterval.driverKey = next.driverKey
and next.startedAtEpochSecond > priorInterval.endedAtEpochSecond;
@Priority(20)
on SignificantDrivingInterval
delete from PreviousSignificantDrivingInterval;
@Priority(10)
on SignificantDrivingInterval as current
insert into PreviousSignificantDrivingInterval
select *;
@name('drivingInterruptionIntervals')
select * from DrivingInterruptionInterval;

View File

@ -3,8 +3,8 @@ create context PerDriver partition by driverKey from TachographVehicleUsageInter
create schema VuCardAbsentInterval( create schema VuCardAbsentInterval(
sessionId java.util.UUID, sessionId java.util.UUID,
driverKey string, driverKey string,
startedAt java.time.OffsetDateTime, startedAtEpochSecond long,
endedAt java.time.OffsetDateTime, endedAtEpochSecond long,
durationSeconds long, durationSeconds long,
previousUsageIntervalId string, previousUsageIntervalId string,
nextUsageIntervalId string, nextUsageIntervalId string,
@ -17,38 +17,37 @@ create schema VuCardAbsentInterval(
context PerDriver context PerDriver
create window PreviousVehicleUsageInterval#lastevent as TachographVehicleUsageIntervalInputEvent; create window PreviousVehicleUsageInterval#lastevent as TachographVehicleUsageIntervalInputEvent;
context PerDriver
@Priority(30) @Priority(30)
context PerDriver
on TachographVehicleUsageIntervalInputEvent as next on TachographVehicleUsageIntervalInputEvent as next
insert into VuCardAbsentInterval insert into VuCardAbsentInterval
select select
prev.sessionId as sessionId, priorInterval.sessionId as sessionId,
prev.driverKey as driverKey, priorInterval.driverKey as driverKey,
prev.endedAt.plusSeconds(1) as startedAt, priorInterval.endedAtEpochSecond + 1L as startedAtEpochSecond,
next.startedAt as endedAt, next.startedAtEpochSecond as endedAtEpochSecond,
java.time.Duration.between(prev.endedAt.plusSeconds(1), next.startedAt).getSeconds() as durationSeconds, next.startedAtEpochSecond - (priorInterval.endedAtEpochSecond + 1L) as durationSeconds,
prev.intervalId as previousUsageIntervalId, priorInterval.lastSourceIntervalId as previousUsageIntervalId,
next.intervalId as nextUsageIntervalId, next.firstSourceIntervalId as nextUsageIntervalId,
prev.registrationKey as previousRegistrationKey, priorInterval.registrationKey as previousRegistrationKey,
next.registrationKey as nextRegistrationKey, next.registrationKey as nextRegistrationKey,
prev.vehicleKey as previousVehicleKey, priorInterval.vehicleKey as previousVehicleKey,
next.vehicleKey as nextVehicleKey next.vehicleKey as nextVehicleKey
from PreviousVehicleUsageInterval as prev from PreviousVehicleUsageInterval as priorInterval
where prev.endedAt is not null where priorInterval.endedAt is not null
and next.startedAt is not null and next.startedAt is not null
and next.startedAt.isAfter(prev.endedAt.plusSeconds(1)); and next.startedAtEpochSecond > priorInterval.endedAtEpochSecond + 1L;
context PerDriver
@Priority(20) @Priority(20)
context PerDriver
on TachographVehicleUsageIntervalInputEvent on TachographVehicleUsageIntervalInputEvent
delete from PreviousVehicleUsageInterval; delete from PreviousVehicleUsageInterval;
context PerDriver
@Priority(10) @Priority(10)
context PerDriver
on TachographVehicleUsageIntervalInputEvent as current on TachographVehicleUsageIntervalInputEvent as current
insert into PreviousVehicleUsageInterval insert into PreviousVehicleUsageInterval
select *; select *;
@name('vuCardAbsentIntervals') @name('vuCardAbsentIntervals')
context PerDriver
select * from VuCardAbsentInterval; select * from VuCardAbsentInterval;

View File

@ -9,6 +9,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import at.procon.eventhub.tachographfilesession.dto.CreateTachographFileSessionResponse; import at.procon.eventhub.tachographfilesession.dto.CreateTachographFileSessionResponse;
import at.procon.eventhub.tachographfilesession.dto.TachographEsperEventsProcessingRequest;
import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto; import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto;
import at.procon.eventhub.tachographfilesession.dto.TachographFileDriverDetailDto; import at.procon.eventhub.tachographfilesession.dto.TachographFileDriverDetailDto;
import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingResultDto; import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingResultDto;
@ -19,6 +20,7 @@ import at.procon.eventhub.tachographfilesession.dto.TachographFileSessionListDri
import at.procon.eventhub.tachographfilesession.dto.TachographFileSessionSummaryDto; import at.procon.eventhub.tachographfilesession.dto.TachographFileSessionSummaryDto;
import at.procon.eventhub.tachographfilesession.model.ExtractionStats; import at.procon.eventhub.tachographfilesession.model.ExtractionStats;
import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent;
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionProcessingService; import at.procon.eventhub.tachographfilesession.service.TachographFileSessionProcessingService;
@ -63,15 +65,22 @@ class TachographFileSessionControllerTest {
when(service.getSession(sessionId)).thenReturn(summary); when(service.getSession(sessionId)).thenReturn(summary);
when(service.listDrivers(sessionId)).thenReturn(new TachographFileSessionListDriversResponse(sessionId, List.of(driver))); when(service.listDrivers(sessionId)).thenReturn(new TachographFileSessionListDriversResponse(sessionId, List.of(driver)));
when(service.getDriver(sessionId, "12:123")).thenReturn(new TachographFileDriverDetailDto(sessionId, "12:123", null, null, List.of(), List.of(), List.of(), List.of(), List.of(), List.of())); when(service.getDriver(sessionId, "12:123")).thenReturn(new TachographFileDriverDetailDto(sessionId, "12:123", null, null, List.of(), List.of(), List.of(), List.of(), List.of(), List.of()));
when(processingService.getEsperDriverProcessingResults(sessionId, "12:123")) when(processingService.getEsperDriverProcessingResults(
eq(sessionId),
eq("12:123"),
org.mockito.ArgumentMatchers.any(TachographEsperEventsProcessingRequest.class)
))
.thenReturn(new TachographEsperDriverProcessingResultDto( .thenReturn(new TachographEsperDriverProcessingResultDto(
sessionId, sessionId,
"12:123", "12:123",
"DRIVER_CARD", "DRIVER_CARD",
OffsetDateTime.parse("2026-05-12T08:00:00Z"), OffsetDateTime.parse("2026-05-12T08:00:00Z"),
OffsetDateTime.parse("2026-05-12T12:00:00Z"), OffsetDateTime.parse("2026-05-12T12:00:00Z"),
OffsetDateTime.parse("2026-05-12T08:30:00Z"),
OffsetDateTime.parse("2026-05-12T11:30:00Z"),
2, 2,
1, 1,
1,
2, 2,
1, 1,
List.of(new TachographEsperActivityIntervalEvent( List.of(new TachographEsperActivityIntervalEvent(
@ -112,6 +121,17 @@ class TachographFileSessionControllerTest {
false, false,
"RAW_INTERVAL" "RAW_INTERVAL"
)), )),
List.of(new TachographEsperDrivingInterruptionIntervalEvent(
sessionId,
"12:123",
OffsetDateTime.parse("2026-05-12T10:00:00Z"),
OffsetDateTime.parse("2026-05-12T10:30:00Z"),
1800L,
"ACT-2",
"ACT-3",
"VIN-1",
"VIN-2"
)),
List.of(new TachographEsperVehicleUsageIntervalEvent( List.of(new TachographEsperVehicleUsageIntervalEvent(
sessionId, sessionId,
"12:123", "12:123",
@ -187,12 +207,25 @@ class TachographFileSessionControllerTest {
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.driverKey").value("12:123")); .andExpect(jsonPath("$.driverKey").value("12:123"));
mockMvc.perform(get("/api/eventhub/tachograph-file-sessions/{sessionId}/drivers/{driverKey}/processing/esper-events", sessionId, "12:123")) mockMvc.perform(post("/api/eventhub/tachograph-file-sessions/{sessionId}/drivers/{driverKey}/processing/esper-events", sessionId, "12:123")
.contentType("application/json")
.content("""
{
"occurredFrom": "2026-05-12T08:30:00Z",
"occurredTo": "2026-05-12T11:30:00Z",
"significantDrivingMinutes": 3
}
"""))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.driverKey").value("12:123")) .andExpect(jsonPath("$.driverKey").value("12:123"))
.andExpect(jsonPath("$.sourceKind").value("DRIVER_CARD")) .andExpect(jsonPath("$.sourceKind").value("DRIVER_CARD"))
.andExpect(jsonPath("$.requestedFrom").value("2026-05-12T08:30:00Z"))
.andExpect(jsonPath("$.requestedTo").value("2026-05-12T11:30:00Z"))
.andExpect(jsonPath("$.activityIntervalCount").value(2)) .andExpect(jsonPath("$.activityIntervalCount").value(2))
.andExpect(jsonPath("$.drivingInterruptionIntervalCount").value(1))
.andExpect(jsonPath("$.vuCardAbsentIntervalCount").value(1)) .andExpect(jsonPath("$.vuCardAbsentIntervalCount").value(1))
.andExpect(jsonPath("$.drivingInterruptionIntervals[0].previousVehicleKey").value("VIN-1"))
.andExpect(jsonPath("$.drivingInterruptionIntervals[0].nextVehicleKey").value("VIN-2"))
.andExpect(jsonPath("$.drivingIntervals[0].activityType").value("DRIVE")); .andExpect(jsonPath("$.drivingIntervals[0].activityType").value("DRIVE"));
mockMvc.perform(post("/api/eventhub/tachograph-file-sessions/{sessionId}/drivers/{driverKey}/processing/operating-periods", sessionId, "12:123") mockMvc.perform(post("/api/eventhub/tachograph-file-sessions/{sessionId}/drivers/{driverKey}/processing/operating-periods", sessionId, "12:123")

View File

@ -10,6 +10,7 @@ import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent;
import at.procon.eventhub.tachographfilesession.model.ExtractionStats; import at.procon.eventhub.tachographfilesession.model.ExtractionStats;
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographFileSession; import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
@ -375,4 +376,79 @@ class DriverTimelineBuilderTest {
assertThat(absentIntervals.get(0).previousVehicleKey()).isEqualTo("VIN-1"); assertThat(absentIntervals.get(0).previousVehicleKey()).isEqualTo("VIN-1");
assertThat(absentIntervals.get(0).nextVehicleKey()).isEqualTo("VIN-2"); assertThat(absentIntervals.get(0).nextVehicleKey()).isEqualTo("VIN-2");
} }
@Test
void buildsEsperDrivingInterruptionIntervalEventsFromSignificantDrivingGaps() {
DriverExtractionSession driver = new DriverExtractionSession(
"12:123",
null,
null,
List.of(),
List.of(),
List.of(),
List.of(
new ExtractedCardActivityInterval(
"ACT-1",
OffsetDateTime.parse("2026-05-01T08:00:00Z"),
OffsetDateTime.parse("2026-05-01T08:02:00Z"),
"DRIVE",
"DRIVER",
"INSERTED",
"SINGLE",
"12:REG-1",
"VIN-1",
"a"
),
new ExtractedCardActivityInterval(
"ACT-2",
OffsetDateTime.parse("2026-05-01T08:02:00Z"),
OffsetDateTime.parse("2026-05-01T08:10:00Z"),
"WORK",
"DRIVER",
"INSERTED",
"SINGLE",
"12:REG-1",
"VIN-1",
"b"
),
new ExtractedCardActivityInterval(
"ACT-3",
OffsetDateTime.parse("2026-05-01T08:10:00Z"),
OffsetDateTime.parse("2026-05-01T08:15:00Z"),
"DRIVE",
"DRIVER",
"INSERTED",
"SINGLE",
"12:REG-2",
"VIN-2",
"c"
)
),
List.of(),
List.of()
);
TachographFileSession session = new TachographFileSession(
UUID.randomUUID(),
new TachographFileSessionMetadata("default", "legalrequirements-drivercard", "sample", "sample.ddd", "a", 3, "42", "b", true, null),
Map.of(driver.driverKey(), driver),
new ExtractionStats(1, 3, 0, 0, 0, 0),
List.of(),
Instant.now(),
Instant.now().plus(4, ChronoUnit.HOURS)
);
List<TachographEsperDrivingInterruptionIntervalEvent> interruptions =
builder.buildEsperDrivingInterruptionIntervalEvents(session, driver, 1);
assertThat(interruptions).hasSize(1);
assertThat(interruptions.get(0).sessionId()).isEqualTo(session.sessionId());
assertThat(interruptions.get(0).driverKey()).isEqualTo("12:123");
assertThat(interruptions.get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T08:02:00Z"));
assertThat(interruptions.get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T08:10:00Z"));
assertThat(interruptions.get(0).durationSeconds()).isEqualTo(480L);
assertThat(interruptions.get(0).previousDrivingSourceIntervalId()).isEqualTo("ACT-1");
assertThat(interruptions.get(0).nextDrivingSourceIntervalId()).isEqualTo("ACT-3");
assertThat(interruptions.get(0).previousVehicleKey()).isEqualTo("VIN-1");
assertThat(interruptions.get(0).nextVehicleKey()).isEqualTo("VIN-2");
}
} }

View File

@ -3,6 +3,7 @@ package at.procon.eventhub.tachographfilesession.service;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import at.procon.eventhub.config.EventHubProperties; import at.procon.eventhub.config.EventHubProperties;
import at.procon.eventhub.tachographfilesession.dto.TachographEsperEventsProcessingRequest;
import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto; import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto;
import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingRequest; import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingRequest;
import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingResultDto; import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingResultDto;
@ -84,12 +85,99 @@ class TachographFileSessionProcessingServiceTest {
assertThat(result.sourceKind()).isEqualTo("DRIVER_CARD"); assertThat(result.sourceKind()).isEqualTo("DRIVER_CARD");
assertThat(result.activityIntervalCount()).isEqualTo(2); assertThat(result.activityIntervalCount()).isEqualTo(2);
assertThat(result.drivingIntervalCount()).isEqualTo(1); assertThat(result.drivingIntervalCount()).isEqualTo(1);
assertThat(result.drivingInterruptionIntervalCount()).isEqualTo(0);
assertThat(result.vehicleUsageIntervalCount()).isEqualTo(2); assertThat(result.vehicleUsageIntervalCount()).isEqualTo(2);
assertThat(result.vuCardAbsentIntervalCount()).isEqualTo(1); assertThat(result.vuCardAbsentIntervalCount()).isEqualTo(1);
assertThat(result.vuCardAbsentIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T11:00:01Z")); assertThat(result.vuCardAbsentIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T11:00:01Z"));
assertThat(result.vuCardAbsentIntervals().get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T12:00:00Z")); assertThat(result.vuCardAbsentIntervals().get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T12:00:00Z"));
} }
@Test
void appliesOccurredWindowToEsperDriverProcessingResults() {
EventHubProperties properties = new EventHubProperties();
TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties);
TachographFileSessionProcessingService service = new TachographFileSessionProcessingService(
repository,
new DriverTimelineBuilder(),
properties
);
DriverExtractionSession driver = new DriverExtractionSession(
"12:123",
null,
null,
List.of(),
List.of(),
List.of(
new ExtractedCardVehicleUsageInterval(
"CVU-1",
OffsetDateTime.parse("2026-05-01T08:00:00Z"),
OffsetDateTime.parse("2026-05-01T11:00:00Z"),
100L,
200L,
"12:REG-1",
"VIN-1",
"vu-1"
),
new ExtractedCardVehicleUsageInterval(
"CVU-2",
OffsetDateTime.parse("2026-05-01T12:00:00Z"),
OffsetDateTime.parse("2026-05-01T13:00:00Z"),
201L,
260L,
"12:REG-2",
"VIN-2",
"vu-2"
)
),
List.of(
new ExtractedCardActivityInterval("ACT-1", OffsetDateTime.parse("2026-05-01T08:30:00Z"), OffsetDateTime.parse("2026-05-01T09:00:00Z"), "DRIVE", "DRIVER", "INSERTED", "SINGLE", "12:REG-1", "VIN-1", "a"),
new ExtractedCardActivityInterval("ACT-2", OffsetDateTime.parse("2026-05-01T09:00:00Z"), OffsetDateTime.parse("2026-05-01T10:00:00Z"), "WORK", "DRIVER", "INSERTED", "SINGLE", "12:REG-1", "VIN-1", "b"),
new ExtractedCardActivityInterval("ACT-3", OffsetDateTime.parse("2026-05-01T10:00:00Z"), OffsetDateTime.parse("2026-05-01T10:05:00Z"), "DRIVE", "DRIVER", "INSERTED", "SINGLE", "12:REG-2", "VIN-2", "c")
),
List.of(),
List.of()
);
TachographFileSession session = new TachographFileSession(
UUID.randomUUID(),
new TachographFileSessionMetadata("default", "legalrequirements-drivercard", "sample", "sample.ddd", "a", 3, "42", "b", true, null),
Map.of(driver.driverKey(), driver),
new ExtractionStats(1, 2, 2, 1, 1, 0),
List.of(),
Instant.now(),
Instant.now().plus(4, ChronoUnit.HOURS)
);
repository.save(session);
TachographEsperDriverProcessingResultDto result = service.getEsperDriverProcessingResults(
session.sessionId(),
driver.driverKey(),
new TachographEsperEventsProcessingRequest(
OffsetDateTime.parse("2026-05-01T08:45:00Z"),
OffsetDateTime.parse("2026-05-01T12:30:00Z"),
3
)
);
assertThat(result.requestedFrom()).isEqualTo(OffsetDateTime.parse("2026-05-01T08:45:00Z"));
assertThat(result.requestedTo()).isEqualTo(OffsetDateTime.parse("2026-05-01T12:30:00Z"));
assertThat(result.activityIntervalCount()).isEqualTo(3);
assertThat(result.activityIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T08:45:00Z"));
assertThat(result.activityIntervals().get(0).clippedToRequestedPeriod()).isTrue();
assertThat(result.drivingIntervalCount()).isEqualTo(2);
assertThat(result.drivingInterruptionIntervalCount()).isEqualTo(1);
assertThat(result.drivingInterruptionIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T09:00:00Z"));
assertThat(result.drivingInterruptionIntervals().get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T10:00:00Z"));
assertThat(result.drivingInterruptionIntervals().get(0).previousVehicleKey()).isEqualTo("VIN-1");
assertThat(result.drivingInterruptionIntervals().get(0).nextVehicleKey()).isEqualTo("VIN-2");
assertThat(result.vehicleUsageIntervalCount()).isEqualTo(2);
assertThat(result.vehicleUsageIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T08:45:00Z"));
assertThat(result.vehicleUsageIntervals().get(0).odometerBeginKm()).isNull();
assertThat(result.vehicleUsageIntervals().get(1).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T12:30:00Z"));
assertThat(result.vehicleUsageIntervals().get(1).odometerEndKm()).isNull();
assertThat(result.vuCardAbsentIntervalCount()).isEqualTo(1);
}
@Test @Test
void evaluatesOperatingPeriodsFromSessionTimeline() { void evaluatesOperatingPeriodsFromSessionTimeline() {
EventHubProperties properties = new EventHubProperties(); EventHubProperties properties = new EventHubProperties();