From 3b2f89324626b6bbff7109869a636358c27518a1 Mon Sep 17 00:00:00 2001 From: trifonovt <87468028+TihomirTrifonov@users.noreply.github.com> Date: Wed, 13 May 2026 13:48:29 +0200 Subject: [PATCH] Add rest candidate Esper projections --- ...eventhub-esper-poc.postman_collection.json | 2 +- .../eventhub/config/EventHubProperties.java | 9 + ...hographEsperDriverProcessingResultDto.java | 5 + ...achographEsperEventsProcessingRequest.java | 6 +- ...EsperDrivingInterruptionIntervalEvent.java | 2 + ...tentialHomeOvernightStayIntervalEvent.java | 21 ++ .../service/DriverTimelineBuilder.java | 210 ++++++++++++++++++ ...achographFileSessionProcessingService.java | 119 +++++++++- ...h-driving-interruption-interval-events.epl | 6 + ...ruption-vehicle-change-interval-events.epl | 18 ++ ...al-home-overnight-stay-interval-events.epl | 65 ++++++ .../TachographFileSessionControllerTest.java | 44 +++- .../service/DriverTimelineBuilderTest.java | 66 ++++++ ...graphFileSessionProcessingServiceTest.java | 88 +++++++- 14 files changed, 650 insertions(+), 11 deletions(-) create mode 100644 src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialHomeOvernightStayIntervalEvent.java create mode 100644 src/main/resources/esper/tachograph-driving-interruption-vehicle-change-interval-events.epl create mode 100644 src/main/resources/esper/tachograph-potential-home-overnight-stay-interval-events.epl diff --git a/postman/eventhub-esper-poc.postman_collection.json b/postman/eventhub-esper-poc.postman_collection.json index 5dfb96c..6489d25 100644 --- a/postman/eventhub-esper-poc.postman_collection.json +++ b/postman/eventhub-esper-poc.postman_collection.json @@ -386,7 +386,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"significantDrivingMinutes\": 3\n}" + "raw": "{\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720\n}" }, "url": { "raw": "{{baseUrl}}/api/eventhub/tachograph-file-sessions/{{sessionId}}/drivers/{{driverKey}}/processing/esper-events", diff --git a/src/main/java/at/procon/eventhub/config/EventHubProperties.java b/src/main/java/at/procon/eventhub/config/EventHubProperties.java index e897a00..73c209f 100644 --- a/src/main/java/at/procon/eventhub/config/EventHubProperties.java +++ b/src/main/java/at/procon/eventhub/config/EventHubProperties.java @@ -358,6 +358,7 @@ public class EventHubProperties { public static class Processing { private int operatingSplitIdleHours = 7; private int significantDrivingMinutes = 3; + private int minimumRestPeriodMinutes = 720; private int mergeGapSeconds = 0; private int gapDetectionToleranceSeconds = 0; @@ -377,6 +378,14 @@ public class EventHubProperties { this.significantDrivingMinutes = Math.max(1, significantDrivingMinutes); } + public int getMinimumRestPeriodMinutes() { + return minimumRestPeriodMinutes; + } + + public void setMinimumRestPeriodMinutes(int minimumRestPeriodMinutes) { + this.minimumRestPeriodMinutes = Math.max(1, minimumRestPeriodMinutes); + } + public int getMergeGapSeconds() { return mergeGapSeconds; } diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographEsperDriverProcessingResultDto.java b/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographEsperDriverProcessingResultDto.java index 098e0b8..c2cde83 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographEsperDriverProcessingResultDto.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographEsperDriverProcessingResultDto.java @@ -2,6 +2,7 @@ package at.procon.eventhub.tachographfilesession.dto; import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; import java.time.OffsetDateTime; @@ -19,11 +20,15 @@ public record TachographEsperDriverProcessingResultDto( int activityIntervalCount, int drivingIntervalCount, int drivingInterruptionIntervalCount, + int drivingInterruptionVehicleChangeIntervalCount, + int potentialHomeOvernightStayIntervalCount, int vehicleUsageIntervalCount, int vuCardAbsentIntervalCount, List activityIntervals, List drivingIntervals, List drivingInterruptionIntervals, + List drivingInterruptionVehicleChangeIntervals, + List potentialHomeOvernightStayIntervals, List vehicleUsageIntervals, List vuCardAbsentIntervals, List notes diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographEsperEventsProcessingRequest.java b/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographEsperEventsProcessingRequest.java index 40f4a70..418c805 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographEsperEventsProcessingRequest.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographEsperEventsProcessingRequest.java @@ -5,9 +5,13 @@ import java.time.OffsetDateTime; public record TachographEsperEventsProcessingRequest( OffsetDateTime occurredFrom, OffsetDateTime occurredTo, - Integer significantDrivingMinutes + Integer significantDrivingMinutes, + Integer minimumRestPeriodMinutes ) { public TachographEsperEventsProcessingRequest { significantDrivingMinutes = significantDrivingMinutes == null ? null : Math.max(1, significantDrivingMinutes); + minimumRestPeriodMinutes = minimumRestPeriodMinutes == null + ? null + : Math.max(1, minimumRestPeriodMinutes); } } diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperDrivingInterruptionIntervalEvent.java b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperDrivingInterruptionIntervalEvent.java index 43c10d8..84df261 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperDrivingInterruptionIntervalEvent.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperDrivingInterruptionIntervalEvent.java @@ -11,6 +11,8 @@ public record TachographEsperDrivingInterruptionIntervalEvent( long durationSeconds, String previousDrivingSourceIntervalId, String nextDrivingSourceIntervalId, + String previousRegistrationKey, + String nextRegistrationKey, String previousVehicleKey, String nextVehicleKey ) { diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialHomeOvernightStayIntervalEvent.java b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialHomeOvernightStayIntervalEvent.java new file mode 100644 index 0000000..191c1e8 --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialHomeOvernightStayIntervalEvent.java @@ -0,0 +1,21 @@ +package at.procon.eventhub.tachographfilesession.model; + +import java.time.OffsetDateTime; +import java.util.UUID; + +public record TachographEsperPotentialHomeOvernightStayIntervalEvent( + UUID sessionId, + String driverKey, + OffsetDateTime startedAt, + OffsetDateTime endedAt, + long durationSeconds, + long unknownDurationSeconds, + double unknownCoveragePercent, + String previousDrivingSourceIntervalId, + String nextDrivingSourceIntervalId, + String previousRegistrationKey, + String nextRegistrationKey, + String previousVehicleKey, + String nextVehicleKey +) { +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineBuilder.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineBuilder.java index 9a0fff1..35fefdb 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineBuilder.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineBuilder.java @@ -20,6 +20,7 @@ import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval; import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographFileSession; @@ -52,6 +53,10 @@ public class DriverTimelineBuilder { 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 DRIVING_INTERRUPTION_VEHICLE_CHANGE_INTERVAL_EVENTS_EPL = + loadResource("esper/tachograph-driving-interruption-vehicle-change-interval-events.epl"); + private static final String POTENTIAL_HOME_OVERNIGHT_STAY_INTERVAL_EVENTS_EPL_TEMPLATE = + loadResource("esper/tachograph-potential-home-overnight-stay-interval-events.epl"); private static final String VEHICLE_USAGE_INTERVAL_EVENTS_EPL = loadResource("esper/tachograph-vehicle-usage-interval-events.epl"); private static final String VU_CARD_ABSENT_INTERVAL_EVENTS_EPL = @@ -176,6 +181,77 @@ public class DriverTimelineBuilder { ); } + public List buildEsperDrivingInterruptionVehicleChangeIntervalEvents( + List drivingInterruptionIntervals + ) { + if (drivingInterruptionIntervals == null || drivingInterruptionIntervals.isEmpty()) { + return List.of(); + } + List result = new ArrayList<>(); + executeWithRuntime( + configuration -> configuration.getCommon().addEventType( + "TachographDrivingInterruptionIntervalInputEvent", + drivingInterruptionIntervalInputDefinition() + ), + DRIVING_INTERRUPTION_VEHICLE_CHANGE_INTERVAL_EVENTS_EPL, + "drivingInterruptionVehicleChangeIntervals", + newData -> collectDrivingInterruptionIntervalEventsFromTimestamps(newData, result), + runtime -> { + for (TachographEsperDrivingInterruptionIntervalEvent interval : drivingInterruptionIntervals) { + runtime.getEventService().sendEventMap( + toDrivingInterruptionIntervalInputMap(interval), + "TachographDrivingInterruptionIntervalInputEvent" + ); + } + } + ); + return result; + } + + public List buildEsperPotentialHomeOvernightStayIntervalEvents( + List drivingInterruptionIntervals, + List vuCardAbsentIntervals, + int minimumRestPeriodMinutes + ) { + if (drivingInterruptionIntervals == null + || drivingInterruptionIntervals.isEmpty() + || vuCardAbsentIntervals == null + || vuCardAbsentIntervals.isEmpty()) { + return List.of(); + } + List result = new ArrayList<>(); + executeWithRuntime( + configuration -> { + configuration.getCommon().addEventType( + "TachographDrivingInterruptionIntervalInputEvent", + drivingInterruptionIntervalInputDefinition() + ); + configuration.getCommon().addEventType( + "TachographVuCardAbsentIntervalInputEvent", + vuCardAbsentIntervalInputDefinition() + ); + }, + renderPotentialHomeOvernightStayIntervalEventsEpl(minimumRestPeriodMinutes), + "potentialHomeOvernightStayIntervals", + newData -> collectPotentialHomeOvernightStayIntervalEvents(newData, result), + runtime -> { + for (TachographEsperVuCardAbsentIntervalEvent interval : vuCardAbsentIntervals) { + runtime.getEventService().sendEventMap( + toVuCardAbsentIntervalInputMap(interval), + "TachographVuCardAbsentIntervalInputEvent" + ); + } + for (TachographEsperDrivingInterruptionIntervalEvent interval : drivingInterruptionIntervals) { + runtime.getEventService().sendEventMap( + toDrivingInterruptionIntervalInputMap(interval), + "TachographDrivingInterruptionIntervalInputEvent" + ); + } + } + ); + return result; + } + public List buildEsperVuCardAbsentIntervalEvents( TachographFileSession session, DriverExtractionSession driverSession @@ -602,6 +678,42 @@ public class DriverTimelineBuilder { return definition; } + private Map drivingInterruptionIntervalInputDefinition() { + Map definition = new LinkedHashMap<>(); + definition.put("sessionId", UUID.class); + definition.put("driverKey", String.class); + definition.put("startedAt", OffsetDateTime.class); + definition.put("endedAt", OffsetDateTime.class); + definition.put("startedAtEpochSecond", long.class); + definition.put("endedAtEpochSecond", long.class); + definition.put("durationSeconds", long.class); + definition.put("previousDrivingSourceIntervalId", String.class); + definition.put("nextDrivingSourceIntervalId", String.class); + definition.put("previousRegistrationKey", String.class); + definition.put("nextRegistrationKey", String.class); + definition.put("previousVehicleKey", String.class); + definition.put("nextVehicleKey", String.class); + return definition; + } + + private Map vuCardAbsentIntervalInputDefinition() { + Map definition = new LinkedHashMap<>(); + definition.put("sessionId", UUID.class); + definition.put("driverKey", String.class); + definition.put("startedAt", OffsetDateTime.class); + definition.put("endedAt", OffsetDateTime.class); + definition.put("startedAtEpochSecond", long.class); + definition.put("endedAtEpochSecond", long.class); + definition.put("durationSeconds", long.class); + definition.put("previousUsageIntervalId", String.class); + definition.put("nextUsageIntervalId", String.class); + definition.put("previousRegistrationKey", String.class); + definition.put("nextRegistrationKey", String.class); + definition.put("previousVehicleKey", String.class); + definition.put("nextVehicleKey", String.class); + return definition; + } + private Map toActivityIntervalInputMap( UUID sessionId, String driverKey, @@ -663,6 +775,44 @@ public class DriverTimelineBuilder { return event; } + private Map toDrivingInterruptionIntervalInputMap( + TachographEsperDrivingInterruptionIntervalEvent interval + ) { + Map event = new LinkedHashMap<>(); + event.put("sessionId", interval.sessionId()); + event.put("driverKey", interval.driverKey()); + event.put("startedAt", interval.startedAt()); + event.put("endedAt", interval.endedAt()); + event.put("startedAtEpochSecond", interval.startedAt().toEpochSecond()); + event.put("endedAtEpochSecond", interval.endedAt().toEpochSecond()); + event.put("durationSeconds", interval.durationSeconds()); + event.put("previousDrivingSourceIntervalId", interval.previousDrivingSourceIntervalId()); + event.put("nextDrivingSourceIntervalId", interval.nextDrivingSourceIntervalId()); + event.put("previousRegistrationKey", interval.previousRegistrationKey()); + event.put("nextRegistrationKey", interval.nextRegistrationKey()); + event.put("previousVehicleKey", interval.previousVehicleKey()); + event.put("nextVehicleKey", interval.nextVehicleKey()); + return event; + } + + private Map toVuCardAbsentIntervalInputMap(TachographEsperVuCardAbsentIntervalEvent interval) { + Map event = new LinkedHashMap<>(); + event.put("sessionId", interval.sessionId()); + event.put("driverKey", interval.driverKey()); + event.put("startedAt", interval.startedAt()); + event.put("endedAt", interval.endedAt()); + event.put("startedAtEpochSecond", interval.startedAt().toEpochSecond()); + event.put("endedAtEpochSecond", interval.endedAt().toEpochSecond()); + event.put("durationSeconds", interval.durationSeconds()); + event.put("previousUsageIntervalId", interval.previousUsageIntervalId()); + event.put("nextUsageIntervalId", interval.nextUsageIntervalId()); + event.put("previousRegistrationKey", interval.previousRegistrationKey()); + event.put("nextRegistrationKey", interval.nextRegistrationKey()); + event.put("previousVehicleKey", interval.previousVehicleKey()); + event.put("nextVehicleKey", interval.nextVehicleKey()); + return event; + } + private String firstSourceIntervalId(ResolvedVehicleUsageInterval interval) { return interval.sourceIntervalIds().isEmpty() ? interval.intervalId() : interval.sourceIntervalIds().get(0); } @@ -746,6 +896,32 @@ public class DriverTimelineBuilder { (Long) event.get("durationSeconds"), (String) event.get("previousDrivingSourceIntervalId"), (String) event.get("nextDrivingSourceIntervalId"), + (String) event.get("previousRegistrationKey"), + (String) event.get("nextRegistrationKey"), + (String) event.get("previousVehicleKey"), + (String) event.get("nextVehicleKey") + )); + } + } + + private void collectDrivingInterruptionIntervalEventsFromTimestamps( + EventBean[] newData, + List target + ) { + if (newData == null) { + return; + } + for (EventBean event : newData) { + target.add(new TachographEsperDrivingInterruptionIntervalEvent( + (UUID) event.get("sessionId"), + (String) event.get("driverKey"), + (OffsetDateTime) event.get("startedAt"), + (OffsetDateTime) event.get("endedAt"), + (Long) event.get("durationSeconds"), + (String) event.get("previousDrivingSourceIntervalId"), + (String) event.get("nextDrivingSourceIntervalId"), + (String) event.get("previousRegistrationKey"), + (String) event.get("nextRegistrationKey"), (String) event.get("previousVehicleKey"), (String) event.get("nextVehicleKey") )); @@ -778,6 +954,32 @@ public class DriverTimelineBuilder { } } + private void collectPotentialHomeOvernightStayIntervalEvents( + EventBean[] newData, + List target + ) { + if (newData == null) { + return; + } + for (EventBean event : newData) { + target.add(new TachographEsperPotentialHomeOvernightStayIntervalEvent( + (UUID) event.get("sessionId"), + (String) event.get("driverKey"), + (OffsetDateTime) event.get("startedAt"), + (OffsetDateTime) event.get("endedAt"), + (Long) event.get("durationSeconds"), + (Long) event.get("unknownDurationSeconds"), + (Double) event.get("unknownCoveragePercent"), + (String) event.get("previousDrivingSourceIntervalId"), + (String) event.get("nextDrivingSourceIntervalId"), + (String) event.get("previousRegistrationKey"), + (String) event.get("nextRegistrationKey"), + (String) event.get("previousVehicleKey"), + (String) event.get("nextVehicleKey") + )); + } + } + @SuppressWarnings("unchecked") private List castSourceIntervalIds(Object value) { return value == null ? List.of() : List.copyOf((List) value); @@ -799,4 +1001,12 @@ public class DriverTimelineBuilder { Long.toString(thresholdSeconds) ); } + + private String renderPotentialHomeOvernightStayIntervalEventsEpl(int minimumRestPeriodMinutes) { + long thresholdSeconds = Math.max(1, minimumRestPeriodMinutes) * 60L; + return POTENTIAL_HOME_OVERNIGHT_STAY_INTERVAL_EVENTS_EPL_TEMPLATE.replace( + "${POTENTIAL_HOME_OVERNIGHT_STAY_THRESHOLD_SECONDS}", + Long.toString(thresholdSeconds) + ); + } } diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingService.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingService.java index 9dc3652..be4c3eb 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingService.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingService.java @@ -15,6 +15,7 @@ import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; 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.TachographEsperPotentialHomeOvernightStayIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; import java.time.Duration; @@ -132,7 +133,7 @@ public class TachographFileSessionProcessingService { TachographEsperEventsProcessingRequest request ) { TachographEsperEventsProcessingRequest effectiveRequest = request == null - ? new TachographEsperEventsProcessingRequest(null, null, null) + ? new TachographEsperEventsProcessingRequest(null, null, null, null) : request; TachographFileSession session = repository.find(sessionId) .orElseThrow(() -> new TachographFileSessionNotFoundException(sessionId)); @@ -148,6 +149,7 @@ public class TachographFileSessionProcessingService { throw new IllegalArgumentException("occurredTo must not be before occurredFrom."); } int significantDrivingMinutes = resolveEsperSignificantDrivingMinutes(effectiveRequest); + int minimumRestPeriodMinutes = resolveMinimumRestPeriodMinutes(effectiveRequest); List activityIntervals = clipEsperActivityIntervalEvents( driverTimelineBuilder.buildEsperActivityIntervalEvents(sessionId, driverKey, timeline), @@ -159,24 +161,47 @@ public class TachographFileSessionProcessingService { requestedFrom, requestedTo ); + List rawDrivingInterruptionIntervals = + driverTimelineBuilder.buildEsperDrivingInterruptionIntervalEvents( + sessionId, + driverKey, + timeline, + significantDrivingMinutes + ); List drivingInterruptionIntervals = clipEsperDrivingInterruptionIntervalEvents( - driverTimelineBuilder.buildEsperDrivingInterruptionIntervalEvents( - sessionId, - driverKey, - timeline, - significantDrivingMinutes + rawDrivingInterruptionIntervals, + requestedFrom, + requestedTo + ); + List drivingInterruptionVehicleChangeIntervals = + clipEsperDrivingInterruptionIntervalEvents( + driverTimelineBuilder.buildEsperDrivingInterruptionVehicleChangeIntervalEvents( + rawDrivingInterruptionIntervals ), requestedFrom, requestedTo ); + List rawVuCardAbsentIntervals = + driverTimelineBuilder.buildEsperVuCardAbsentIntervalEvents(timeline); + List potentialHomeOvernightStayIntervals = + clipEsperPotentialHomeOvernightStayIntervalEvents( + driverTimelineBuilder.buildEsperPotentialHomeOvernightStayIntervalEvents( + rawDrivingInterruptionIntervals, + rawVuCardAbsentIntervals, + minimumRestPeriodMinutes + ), + rawVuCardAbsentIntervals, + requestedFrom, + requestedTo + ); List vehicleUsageIntervals = clipEsperVehicleUsageIntervalEvents( driverTimelineBuilder.buildEsperVehicleUsageIntervalEvents(timeline), requestedFrom, requestedTo ); List vuCardAbsentIntervals = clipEsperVuCardAbsentIntervalEvents( - driverTimelineBuilder.buildEsperVuCardAbsentIntervalEvents(timeline), + rawVuCardAbsentIntervals, requestedFrom, requestedTo ); @@ -192,11 +217,15 @@ public class TachographFileSessionProcessingService { activityIntervals.size(), drivingIntervals.size(), drivingInterruptionIntervals.size(), + drivingInterruptionVehicleChangeIntervals.size(), + potentialHomeOvernightStayIntervals.size(), vehicleUsageIntervals.size(), vuCardAbsentIntervals.size(), activityIntervals, drivingIntervals, drivingInterruptionIntervals, + drivingInterruptionVehicleChangeIntervals, + potentialHomeOvernightStayIntervals, vehicleUsageIntervals, vuCardAbsentIntervals, esperProjectionNotes() @@ -310,6 +339,8 @@ public class TachographFileSessionProcessingService { Duration.between(start, end).getSeconds(), interval.previousDrivingSourceIntervalId(), interval.nextDrivingSourceIntervalId(), + interval.previousRegistrationKey(), + interval.nextRegistrationKey(), interval.previousVehicleKey(), interval.nextVehicleKey() ); @@ -355,6 +386,49 @@ public class TachographFileSessionProcessingService { .toList(); } + private List clipEsperPotentialHomeOvernightStayIntervalEvents( + List intervals, + List rawVuCardAbsentIntervals, + 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; + } + long durationSeconds = Duration.between(start, end).getSeconds(); + long unknownDurationSeconds = overlapSeconds(start, end, rawVuCardAbsentIntervals, interval.driverKey()); + double unknownCoveragePercent = durationSeconds == 0L + ? 0.0d + : (unknownDurationSeconds * 100.0d) / durationSeconds; + return new TachographEsperPotentialHomeOvernightStayIntervalEvent( + interval.sessionId(), + interval.driverKey(), + start, + end, + durationSeconds, + unknownDurationSeconds, + unknownCoveragePercent, + interval.previousDrivingSourceIntervalId(), + interval.nextDrivingSourceIntervalId(), + interval.previousRegistrationKey(), + interval.nextRegistrationKey(), + interval.previousVehicleKey(), + interval.nextVehicleKey() + ); + }) + .filter(Objects::nonNull) + .sorted(Comparator.comparing(TachographEsperPotentialHomeOvernightStayIntervalEvent::startedAt) + .thenComparing(TachographEsperPotentialHomeOvernightStayIntervalEvent::endedAt)) + .toList(); + } + private List synthesizeUnknownGaps( List knownIntervals, Duration gapDetectionTolerance @@ -899,6 +973,12 @@ public class TachographFileSessionProcessingService { : request.significantDrivingMinutes(); } + private int resolveMinimumRestPeriodMinutes(TachographEsperEventsProcessingRequest request) { + return request.minimumRestPeriodMinutes() == null + ? properties.getTachographFileSession().getProcessing().getMinimumRestPeriodMinutes() + : request.minimumRestPeriodMinutes(); + } + private List notes() { return List.of( "This endpoint evaluates operating periods from the in-memory tachograph file-session model.", @@ -913,12 +993,37 @@ public class TachographFileSessionProcessingService { "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 interruption intervals are gaps between consecutive driving intervals longer than the configured significant-driving threshold.", + "Driving interruption vehicle-change intervals are DTI intervals where previousRegistrationKey differs from nextRegistrationKey.", + "Potential home overnight stay intervals are DTI intervals longer than the configured minimum rest-period threshold where VU card-absent overlap covers at least 95% of the DTI.", "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." ); } + private long overlapSeconds( + OffsetDateTime intervalStart, + OffsetDateTime intervalEnd, + List unknownIntervals, + String driverKey + ) { + if (unknownIntervals == null || unknownIntervals.isEmpty()) { + return 0L; + } + long total = 0L; + for (TachographEsperVuCardAbsentIntervalEvent unknown : unknownIntervals) { + if (!Objects.equals(driverKey, unknown.driverKey())) { + continue; + } + OffsetDateTime overlapStart = max(intervalStart, unknown.startedAt()); + OffsetDateTime overlapEnd = min(intervalEnd, unknown.endedAt()); + if (overlapEnd.isAfter(overlapStart)) { + total += Duration.between(overlapStart, overlapEnd).getSeconds(); + } + } + return total; + } + private OffsetDateTime utc(OffsetDateTime value) { return value == null ? null : value.withOffsetSameInstant(java.time.ZoneOffset.UTC); } diff --git a/src/main/resources/esper/tachograph-driving-interruption-interval-events.epl b/src/main/resources/esper/tachograph-driving-interruption-interval-events.epl index 5c659cd..ba93ac1 100644 --- a/src/main/resources/esper/tachograph-driving-interruption-interval-events.epl +++ b/src/main/resources/esper/tachograph-driving-interruption-interval-events.epl @@ -6,6 +6,7 @@ create schema SignificantDrivingInterval( startedAtEpochSecond long, endedAtEpochSecond long, durationSeconds long, + registrationKey string, vehicleKey string ); @@ -17,6 +18,8 @@ create schema DrivingInterruptionInterval( durationSeconds long, previousDrivingSourceIntervalId string, nextDrivingSourceIntervalId string, + previousRegistrationKey string, + nextRegistrationKey string, previousVehicleKey string, nextVehicleKey string ); @@ -30,6 +33,7 @@ select startedAtEpochSecond, endedAtEpochSecond, durationSeconds, + registrationKey, vehicleKey from TachographActivityIntervalInputEvent(activityType = 'DRIVE', durationSeconds > ${SIGNIFICANT_DRIVING_THRESHOLD_SECONDS}); @@ -45,6 +49,8 @@ select next.startedAtEpochSecond - priorInterval.endedAtEpochSecond as durationSeconds, priorInterval.lastSourceIntervalId as previousDrivingSourceIntervalId, next.firstSourceIntervalId as nextDrivingSourceIntervalId, + priorInterval.registrationKey as previousRegistrationKey, + next.registrationKey as nextRegistrationKey, priorInterval.vehicleKey as previousVehicleKey, next.vehicleKey as nextVehicleKey from PreviousSignificantDrivingInterval as priorInterval diff --git a/src/main/resources/esper/tachograph-driving-interruption-vehicle-change-interval-events.epl b/src/main/resources/esper/tachograph-driving-interruption-vehicle-change-interval-events.epl new file mode 100644 index 0000000..66155e6 --- /dev/null +++ b/src/main/resources/esper/tachograph-driving-interruption-vehicle-change-interval-events.epl @@ -0,0 +1,18 @@ +@name('drivingInterruptionVehicleChangeIntervals') +select + sessionId, + driverKey, + startedAt, + endedAt, + durationSeconds, + previousDrivingSourceIntervalId, + nextDrivingSourceIntervalId, + previousRegistrationKey, + nextRegistrationKey, + previousVehicleKey, + nextVehicleKey +from TachographDrivingInterruptionIntervalInputEvent( + previousRegistrationKey is not null, + nextRegistrationKey is not null, + previousRegistrationKey != nextRegistrationKey +); diff --git a/src/main/resources/esper/tachograph-potential-home-overnight-stay-interval-events.epl b/src/main/resources/esper/tachograph-potential-home-overnight-stay-interval-events.epl new file mode 100644 index 0000000..3371baf --- /dev/null +++ b/src/main/resources/esper/tachograph-potential-home-overnight-stay-interval-events.epl @@ -0,0 +1,65 @@ +@name('potentialHomeOvernightStayIntervals') +select + d.sessionId as sessionId, + d.driverKey as driverKey, + d.startedAt as startedAt, + d.endedAt as endedAt, + d.durationSeconds as durationSeconds, + sum( + case + when u.startedAtEpochSecond <= d.startedAtEpochSecond and u.endedAtEpochSecond >= d.endedAtEpochSecond + then d.durationSeconds + when u.startedAtEpochSecond <= d.startedAtEpochSecond + then u.endedAtEpochSecond - d.startedAtEpochSecond + when u.endedAtEpochSecond >= d.endedAtEpochSecond + then d.endedAtEpochSecond - u.startedAtEpochSecond + else u.endedAtEpochSecond - u.startedAtEpochSecond + end + ) as unknownDurationSeconds, + (sum( + case + when u.startedAtEpochSecond <= d.startedAtEpochSecond and u.endedAtEpochSecond >= d.endedAtEpochSecond + then d.durationSeconds + when u.startedAtEpochSecond <= d.startedAtEpochSecond + then u.endedAtEpochSecond - d.startedAtEpochSecond + when u.endedAtEpochSecond >= d.endedAtEpochSecond + then d.endedAtEpochSecond - u.startedAtEpochSecond + else u.endedAtEpochSecond - u.startedAtEpochSecond + end + ) * 100.0d) / d.durationSeconds as unknownCoveragePercent, + d.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId, + d.nextDrivingSourceIntervalId as nextDrivingSourceIntervalId, + d.previousRegistrationKey as previousRegistrationKey, + d.nextRegistrationKey as nextRegistrationKey, + d.previousVehicleKey as previousVehicleKey, + d.nextVehicleKey as nextVehicleKey +from TachographDrivingInterruptionIntervalInputEvent(durationSeconds > ${POTENTIAL_HOME_OVERNIGHT_STAY_THRESHOLD_SECONDS}) as d unidirectional, + TachographVuCardAbsentIntervalInputEvent#keepall as u +where u.driverKey = d.driverKey + and u.startedAtEpochSecond < d.endedAtEpochSecond + and u.endedAtEpochSecond > d.startedAtEpochSecond +group by + d.sessionId, + d.driverKey, + d.startedAt, + d.endedAt, + d.startedAtEpochSecond, + d.endedAtEpochSecond, + d.durationSeconds, + d.previousDrivingSourceIntervalId, + d.nextDrivingSourceIntervalId, + d.previousRegistrationKey, + d.nextRegistrationKey, + d.previousVehicleKey, + d.nextVehicleKey +having sum( + case + when u.startedAtEpochSecond <= d.startedAtEpochSecond and u.endedAtEpochSecond >= d.endedAtEpochSecond + then d.durationSeconds + when u.startedAtEpochSecond <= d.startedAtEpochSecond + then u.endedAtEpochSecond - d.startedAtEpochSecond + when u.endedAtEpochSecond >= d.endedAtEpochSecond + then d.endedAtEpochSecond - u.startedAtEpochSecond + else u.endedAtEpochSecond - u.startedAtEpochSecond + end +) * 100L >= d.durationSeconds * 95L; diff --git a/src/test/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionControllerTest.java b/src/test/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionControllerTest.java index 0385229..9faad0c 100644 --- a/src/test/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionControllerTest.java +++ b/src/test/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionControllerTest.java @@ -21,6 +21,7 @@ import at.procon.eventhub.tachographfilesession.dto.TachographFileSessionSummary import at.procon.eventhub.tachographfilesession.model.ExtractionStats; import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; import at.procon.eventhub.tachographfilesession.service.TachographFileSessionProcessingService; @@ -81,6 +82,8 @@ class TachographFileSessionControllerTest { 2, 1, 1, + 1, + 1, 2, 1, List.of(new TachographEsperActivityIntervalEvent( @@ -129,6 +132,36 @@ class TachographFileSessionControllerTest { 1800L, "ACT-2", "ACT-3", + "12:REG-1", + "12:REG-2", + "VIN-1", + "VIN-2" + )), + 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", + "12:REG-1", + "12:REG-2", + "VIN-1", + "VIN-2" + )), + List.of(new TachographEsperPotentialHomeOvernightStayIntervalEvent( + sessionId, + "12:123", + OffsetDateTime.parse("2026-05-12T10:00:00Z"), + OffsetDateTime.parse("2026-05-12T22:00:00Z"), + 43_200L, + 43_200L, + 100.0d, + "ACT-2", + "ACT-3", + "12:REG-1", + "12:REG-2", "VIN-1", "VIN-2" )), @@ -213,7 +246,8 @@ class TachographFileSessionControllerTest { { "occurredFrom": "2026-05-12T08:30:00Z", "occurredTo": "2026-05-12T11:30:00Z", - "significantDrivingMinutes": 3 + "significantDrivingMinutes": 3, + "minimumRestPeriodMinutes": 720 } """)) .andExpect(status().isOk()) @@ -223,9 +257,17 @@ class TachographFileSessionControllerTest { .andExpect(jsonPath("$.requestedTo").value("2026-05-12T11:30:00Z")) .andExpect(jsonPath("$.activityIntervalCount").value(2)) .andExpect(jsonPath("$.drivingInterruptionIntervalCount").value(1)) + .andExpect(jsonPath("$.drivingInterruptionVehicleChangeIntervalCount").value(1)) + .andExpect(jsonPath("$.potentialHomeOvernightStayIntervalCount").value(1)) .andExpect(jsonPath("$.vuCardAbsentIntervalCount").value(1)) + .andExpect(jsonPath("$.drivingInterruptionIntervals[0].previousRegistrationKey").value("12:REG-1")) + .andExpect(jsonPath("$.drivingInterruptionIntervals[0].nextRegistrationKey").value("12:REG-2")) .andExpect(jsonPath("$.drivingInterruptionIntervals[0].previousVehicleKey").value("VIN-1")) .andExpect(jsonPath("$.drivingInterruptionIntervals[0].nextVehicleKey").value("VIN-2")) + .andExpect(jsonPath("$.drivingInterruptionVehicleChangeIntervals[0].previousRegistrationKey").value("12:REG-1")) + .andExpect(jsonPath("$.drivingInterruptionVehicleChangeIntervals[0].nextRegistrationKey").value("12:REG-2")) + .andExpect(jsonPath("$.drivingInterruptionVehicleChangeIntervals[0].previousVehicleKey").value("VIN-1")) + .andExpect(jsonPath("$.drivingInterruptionVehicleChangeIntervals[0].nextVehicleKey").value("VIN-2")) .andExpect(jsonPath("$.drivingIntervals[0].activityType").value("DRIVE")); mockMvc.perform(post("/api/eventhub/tachograph-file-sessions/{sessionId}/drivers/{driverKey}/processing/operating-periods", sessionId, "12:123") diff --git a/src/test/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineBuilderTest.java b/src/test/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineBuilderTest.java index fa6150e..053d9d4 100644 --- a/src/test/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineBuilderTest.java +++ b/src/test/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineBuilderTest.java @@ -11,6 +11,7 @@ import at.procon.eventhub.tachographfilesession.model.ExtractionStats; import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographFileSession; @@ -448,7 +449,72 @@ class DriverTimelineBuilderTest { 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).previousRegistrationKey()).isEqualTo("12:REG-1"); + assertThat(interruptions.get(0).nextRegistrationKey()).isEqualTo("12:REG-2"); assertThat(interruptions.get(0).previousVehicleKey()).isEqualTo("VIN-1"); assertThat(interruptions.get(0).nextVehicleKey()).isEqualTo("VIN-2"); + + List vehicleChangeInterruptions = + builder.buildEsperDrivingInterruptionVehicleChangeIntervalEvents(interruptions); + + assertThat(vehicleChangeInterruptions).hasSize(1); + assertThat(vehicleChangeInterruptions.get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T08:02:00Z")); + assertThat(vehicleChangeInterruptions.get(0).previousRegistrationKey()).isEqualTo("12:REG-1"); + assertThat(vehicleChangeInterruptions.get(0).nextRegistrationKey()).isEqualTo("12:REG-2"); + assertThat(vehicleChangeInterruptions.get(0).previousVehicleKey()).isEqualTo("VIN-1"); + assertThat(vehicleChangeInterruptions.get(0).nextVehicleKey()).isEqualTo("VIN-2"); + } + + @Test + void buildsPotentialHomeOvernightStayIntervalsFromDtiAndVuCardAbsentOverlap() { + UUID sessionId = UUID.randomUUID(); + List interruptions = List.of( + new TachographEsperDrivingInterruptionIntervalEvent( + sessionId, + "12:123", + OffsetDateTime.parse("2026-05-01T10:00:00Z"), + OffsetDateTime.parse("2026-05-02T00:00:00Z"), + 50_400L, + "ACT-1", + "ACT-2", + "12:REG-1", + "12:REG-1", + "VIN-1", + "VIN-1" + ) + ); + List vuCardAbsentIntervals = List.of( + new TachographEsperVuCardAbsentIntervalEvent( + sessionId, + "12:123", + OffsetDateTime.parse("2026-05-01T10:00:01Z"), + OffsetDateTime.parse("2026-05-02T00:00:00Z"), + 50_399L, + "CVU-1", + "CVU-2", + "12:REG-1", + "12:REG-1", + "VIN-1", + "VIN-1" + ) + ); + + List intervals = + builder.buildEsperPotentialHomeOvernightStayIntervalEvents( + interruptions, + vuCardAbsentIntervals, + 720 + ); + + assertThat(intervals).hasSize(1); + assertThat(intervals.get(0).sessionId()).isEqualTo(sessionId); + assertThat(intervals.get(0).driverKey()).isEqualTo("12:123"); + assertThat(intervals.get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T10:00:00Z")); + assertThat(intervals.get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-02T00:00:00Z")); + assertThat(intervals.get(0).durationSeconds()).isEqualTo(50_400L); + assertThat(intervals.get(0).unknownDurationSeconds()).isEqualTo(50_399L); + assertThat(intervals.get(0).unknownCoveragePercent()).isGreaterThan(99.9d); + assertThat(intervals.get(0).previousDrivingSourceIntervalId()).isEqualTo("ACT-1"); + assertThat(intervals.get(0).nextDrivingSourceIntervalId()).isEqualTo("ACT-2"); } } diff --git a/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingServiceTest.java b/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingServiceTest.java index be6865b..4a5d65d 100644 --- a/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingServiceTest.java +++ b/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingServiceTest.java @@ -86,6 +86,8 @@ class TachographFileSessionProcessingServiceTest { assertThat(result.activityIntervalCount()).isEqualTo(2); assertThat(result.drivingIntervalCount()).isEqualTo(1); assertThat(result.drivingInterruptionIntervalCount()).isEqualTo(0); + assertThat(result.drivingInterruptionVehicleChangeIntervalCount()).isEqualTo(0); + assertThat(result.potentialHomeOvernightStayIntervalCount()).isEqualTo(0); assertThat(result.vehicleUsageIntervalCount()).isEqualTo(2); assertThat(result.vuCardAbsentIntervalCount()).isEqualTo(1); assertThat(result.vuCardAbsentIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T11:00:01Z")); @@ -155,7 +157,8 @@ class TachographFileSessionProcessingServiceTest { new TachographEsperEventsProcessingRequest( OffsetDateTime.parse("2026-05-01T08:45:00Z"), OffsetDateTime.parse("2026-05-01T12:30:00Z"), - 3 + 3, + 720 ) ); @@ -166,10 +169,19 @@ class TachographFileSessionProcessingServiceTest { assertThat(result.activityIntervals().get(0).clippedToRequestedPeriod()).isTrue(); assertThat(result.drivingIntervalCount()).isEqualTo(2); assertThat(result.drivingInterruptionIntervalCount()).isEqualTo(1); + assertThat(result.drivingInterruptionVehicleChangeIntervalCount()).isEqualTo(1); + assertThat(result.potentialHomeOvernightStayIntervalCount()).isEqualTo(0); 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).previousRegistrationKey()).isEqualTo("12:REG-1"); + assertThat(result.drivingInterruptionIntervals().get(0).nextRegistrationKey()).isEqualTo("12:REG-2"); assertThat(result.drivingInterruptionIntervals().get(0).previousVehicleKey()).isEqualTo("VIN-1"); assertThat(result.drivingInterruptionIntervals().get(0).nextVehicleKey()).isEqualTo("VIN-2"); + assertThat(result.drivingInterruptionVehicleChangeIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T09:00:00Z")); + assertThat(result.drivingInterruptionVehicleChangeIntervals().get(0).previousRegistrationKey()).isEqualTo("12:REG-1"); + assertThat(result.drivingInterruptionVehicleChangeIntervals().get(0).nextRegistrationKey()).isEqualTo("12:REG-2"); + assertThat(result.drivingInterruptionVehicleChangeIntervals().get(0).previousVehicleKey()).isEqualTo("VIN-1"); + assertThat(result.drivingInterruptionVehicleChangeIntervals().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(); @@ -178,6 +190,80 @@ class TachographFileSessionProcessingServiceTest { assertThat(result.vuCardAbsentIntervalCount()).isEqualTo(1); } + @Test + void returnsPotentialHomeOvernightStayIntervalsWhenVuCardAbsentCoversLongDti() { + 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-01T10:00:00Z"), + 100L, + 200L, + "12:REG-1", + "VIN-1", + "vu-1" + ), + new ExtractedCardVehicleUsageInterval( + "CVU-2", + OffsetDateTime.parse("2026-05-02T00:00:00Z"), + OffsetDateTime.parse("2026-05-02T02:00:00Z"), + 201L, + 260L, + "12:REG-1", + "VIN-1", + "vu-2" + ) + ), + List.of( + new ExtractedCardActivityInterval("ACT-1", OffsetDateTime.parse("2026-05-01T08:00:00Z"), OffsetDateTime.parse("2026-05-01T10:00:00Z"), "DRIVE", "DRIVER", "INSERTED", "SINGLE", "12:REG-1", "VIN-1", "a"), + new ExtractedCardActivityInterval("ACT-2", OffsetDateTime.parse("2026-05-02T00:00:00Z"), OffsetDateTime.parse("2026-05-02T00:30:00Z"), "DRIVE", "DRIVER", "INSERTED", "SINGLE", "12:REG-1", "VIN-1", "b") + ), + List.of(), + List.of() + ); + TachographFileSession session = new TachographFileSession( + UUID.randomUUID(), + new TachographFileSessionMetadata("default", "legalrequirements-drivercard", "sample", "sample.ddd", "a", 2, "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-01T11:00:00Z"), + OffsetDateTime.parse("2026-05-01T23:00:00Z"), + 3, + 720 + ) + ); + + assertThat(result.potentialHomeOvernightStayIntervalCount()).isEqualTo(1); + assertThat(result.potentialHomeOvernightStayIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T11:00:00Z")); + assertThat(result.potentialHomeOvernightStayIntervals().get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T23:00:00Z")); + assertThat(result.potentialHomeOvernightStayIntervals().get(0).unknownDurationSeconds()).isEqualTo(43_200L); + assertThat(result.potentialHomeOvernightStayIntervals().get(0).unknownCoveragePercent()).isEqualTo(100.0d); + } + @Test void evaluatesOperatingPeriodsFromSessionTimeline() { EventHubProperties properties = new EventHubProperties();