Add rest candidate Esper projections

This commit is contained in:
trifonovt 2026-05-13 13:48:29 +02:00
parent eb4e04f144
commit 3b2f893246
14 changed files with 650 additions and 11 deletions

View File

@ -386,7 +386,7 @@
], ],
"body": { "body": {
"mode": "raw", "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": { "url": {
"raw": "{{baseUrl}}/api/eventhub/tachograph-file-sessions/{{sessionId}}/drivers/{{driverKey}}/processing/esper-events", "raw": "{{baseUrl}}/api/eventhub/tachograph-file-sessions/{{sessionId}}/drivers/{{driverKey}}/processing/esper-events",

View File

@ -358,6 +358,7 @@ public class EventHubProperties {
public static class Processing { public static class Processing {
private int operatingSplitIdleHours = 7; private int operatingSplitIdleHours = 7;
private int significantDrivingMinutes = 3; private int significantDrivingMinutes = 3;
private int minimumRestPeriodMinutes = 720;
private int mergeGapSeconds = 0; private int mergeGapSeconds = 0;
private int gapDetectionToleranceSeconds = 0; private int gapDetectionToleranceSeconds = 0;
@ -377,6 +378,14 @@ public class EventHubProperties {
this.significantDrivingMinutes = Math.max(1, significantDrivingMinutes); 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() { public int getMergeGapSeconds() {
return mergeGapSeconds; return mergeGapSeconds;
} }

View File

@ -2,6 +2,7 @@ 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.TachographEsperDrivingInterruptionIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent;
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;
@ -19,11 +20,15 @@ public record TachographEsperDriverProcessingResultDto(
int activityIntervalCount, int activityIntervalCount,
int drivingIntervalCount, int drivingIntervalCount,
int drivingInterruptionIntervalCount, int drivingInterruptionIntervalCount,
int drivingInterruptionVehicleChangeIntervalCount,
int potentialHomeOvernightStayIntervalCount,
int vehicleUsageIntervalCount, int vehicleUsageIntervalCount,
int vuCardAbsentIntervalCount, int vuCardAbsentIntervalCount,
List<TachographEsperActivityIntervalEvent> activityIntervals, List<TachographEsperActivityIntervalEvent> activityIntervals,
List<TachographEsperActivityIntervalEvent> drivingIntervals, List<TachographEsperActivityIntervalEvent> drivingIntervals,
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals, List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals,
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals,
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> potentialHomeOvernightStayIntervals,
List<TachographEsperVehicleUsageIntervalEvent> vehicleUsageIntervals, List<TachographEsperVehicleUsageIntervalEvent> vehicleUsageIntervals,
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals, List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals,
List<String> notes List<String> notes

View File

@ -5,9 +5,13 @@ import java.time.OffsetDateTime;
public record TachographEsperEventsProcessingRequest( public record TachographEsperEventsProcessingRequest(
OffsetDateTime occurredFrom, OffsetDateTime occurredFrom,
OffsetDateTime occurredTo, OffsetDateTime occurredTo,
Integer significantDrivingMinutes Integer significantDrivingMinutes,
Integer minimumRestPeriodMinutes
) { ) {
public TachographEsperEventsProcessingRequest { public TachographEsperEventsProcessingRequest {
significantDrivingMinutes = significantDrivingMinutes == null ? null : Math.max(1, significantDrivingMinutes); significantDrivingMinutes = significantDrivingMinutes == null ? null : Math.max(1, significantDrivingMinutes);
minimumRestPeriodMinutes = minimumRestPeriodMinutes == null
? null
: Math.max(1, minimumRestPeriodMinutes);
} }
} }

View File

@ -11,6 +11,8 @@ public record TachographEsperDrivingInterruptionIntervalEvent(
long durationSeconds, long durationSeconds,
String previousDrivingSourceIntervalId, String previousDrivingSourceIntervalId,
String nextDrivingSourceIntervalId, String nextDrivingSourceIntervalId,
String previousRegistrationKey,
String nextRegistrationKey,
String previousVehicleKey, String previousVehicleKey,
String nextVehicleKey String nextVehicleKey
) { ) {

View File

@ -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
) {
}

View File

@ -20,6 +20,7 @@ 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.TachographEsperDrivingInterruptionIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent;
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;
@ -52,6 +53,10 @@ public class DriverTimelineBuilder {
loadResource("esper/tachograph-driving-interval-events.epl"); loadResource("esper/tachograph-driving-interval-events.epl");
private static final String DRIVING_INTERRUPTION_INTERVAL_EVENTS_EPL_TEMPLATE = private static final String DRIVING_INTERRUPTION_INTERVAL_EVENTS_EPL_TEMPLATE =
loadResource("esper/tachograph-driving-interruption-interval-events.epl"); 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 = 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 =
@ -176,6 +181,77 @@ public class DriverTimelineBuilder {
); );
} }
public List<TachographEsperDrivingInterruptionIntervalEvent> buildEsperDrivingInterruptionVehicleChangeIntervalEvents(
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals
) {
if (drivingInterruptionIntervals == null || drivingInterruptionIntervals.isEmpty()) {
return List.of();
}
List<TachographEsperDrivingInterruptionIntervalEvent> 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<TachographEsperPotentialHomeOvernightStayIntervalEvent> buildEsperPotentialHomeOvernightStayIntervalEvents(
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals,
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals,
int minimumRestPeriodMinutes
) {
if (drivingInterruptionIntervals == null
|| drivingInterruptionIntervals.isEmpty()
|| vuCardAbsentIntervals == null
|| vuCardAbsentIntervals.isEmpty()) {
return List.of();
}
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> 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<TachographEsperVuCardAbsentIntervalEvent> buildEsperVuCardAbsentIntervalEvents( public List<TachographEsperVuCardAbsentIntervalEvent> buildEsperVuCardAbsentIntervalEvents(
TachographFileSession session, TachographFileSession session,
DriverExtractionSession driverSession DriverExtractionSession driverSession
@ -602,6 +678,42 @@ public class DriverTimelineBuilder {
return definition; return definition;
} }
private Map<String, Object> drivingInterruptionIntervalInputDefinition() {
Map<String, Object> 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<String, Object> vuCardAbsentIntervalInputDefinition() {
Map<String, Object> 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<String, Object> toActivityIntervalInputMap( private Map<String, Object> toActivityIntervalInputMap(
UUID sessionId, UUID sessionId,
String driverKey, String driverKey,
@ -663,6 +775,44 @@ public class DriverTimelineBuilder {
return event; return event;
} }
private Map<String, Object> toDrivingInterruptionIntervalInputMap(
TachographEsperDrivingInterruptionIntervalEvent interval
) {
Map<String, Object> 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<String, Object> toVuCardAbsentIntervalInputMap(TachographEsperVuCardAbsentIntervalEvent interval) {
Map<String, Object> 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) { private String firstSourceIntervalId(ResolvedVehicleUsageInterval interval) {
return interval.sourceIntervalIds().isEmpty() ? interval.intervalId() : interval.sourceIntervalIds().get(0); return interval.sourceIntervalIds().isEmpty() ? interval.intervalId() : interval.sourceIntervalIds().get(0);
} }
@ -746,6 +896,32 @@ public class DriverTimelineBuilder {
(Long) event.get("durationSeconds"), (Long) event.get("durationSeconds"),
(String) event.get("previousDrivingSourceIntervalId"), (String) event.get("previousDrivingSourceIntervalId"),
(String) event.get("nextDrivingSourceIntervalId"), (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<TachographEsperDrivingInterruptionIntervalEvent> 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("previousVehicleKey"),
(String) event.get("nextVehicleKey") (String) event.get("nextVehicleKey")
)); ));
@ -778,6 +954,32 @@ public class DriverTimelineBuilder {
} }
} }
private void collectPotentialHomeOvernightStayIntervalEvents(
EventBean[] newData,
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> 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") @SuppressWarnings("unchecked")
private List<String> castSourceIntervalIds(Object value) { private List<String> castSourceIntervalIds(Object value) {
return value == null ? List.of() : List.copyOf((List<String>) value); return value == null ? List.of() : List.copyOf((List<String>) value);
@ -799,4 +1001,12 @@ public class DriverTimelineBuilder {
Long.toString(thresholdSeconds) 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)
);
}
} }

View File

@ -15,6 +15,7 @@ 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.TachographEsperDrivingInterruptionIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographFileSession; 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.TachographEsperVehicleUsageIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent;
import java.time.Duration; import java.time.Duration;
@ -132,7 +133,7 @@ public class TachographFileSessionProcessingService {
TachographEsperEventsProcessingRequest request TachographEsperEventsProcessingRequest request
) { ) {
TachographEsperEventsProcessingRequest effectiveRequest = request == null TachographEsperEventsProcessingRequest effectiveRequest = request == null
? new TachographEsperEventsProcessingRequest(null, null, null) ? new TachographEsperEventsProcessingRequest(null, null, null, null)
: request; : request;
TachographFileSession session = repository.find(sessionId) TachographFileSession session = repository.find(sessionId)
.orElseThrow(() -> new TachographFileSessionNotFoundException(sessionId)); .orElseThrow(() -> new TachographFileSessionNotFoundException(sessionId));
@ -148,6 +149,7 @@ public class TachographFileSessionProcessingService {
throw new IllegalArgumentException("occurredTo must not be before occurredFrom."); throw new IllegalArgumentException("occurredTo must not be before occurredFrom.");
} }
int significantDrivingMinutes = resolveEsperSignificantDrivingMinutes(effectiveRequest); int significantDrivingMinutes = resolveEsperSignificantDrivingMinutes(effectiveRequest);
int minimumRestPeriodMinutes = resolveMinimumRestPeriodMinutes(effectiveRequest);
List<TachographEsperActivityIntervalEvent> activityIntervals = clipEsperActivityIntervalEvents( List<TachographEsperActivityIntervalEvent> activityIntervals = clipEsperActivityIntervalEvents(
driverTimelineBuilder.buildEsperActivityIntervalEvents(sessionId, driverKey, timeline), driverTimelineBuilder.buildEsperActivityIntervalEvents(sessionId, driverKey, timeline),
@ -159,24 +161,47 @@ public class TachographFileSessionProcessingService {
requestedFrom, requestedFrom,
requestedTo requestedTo
); );
List<TachographEsperDrivingInterruptionIntervalEvent> rawDrivingInterruptionIntervals =
driverTimelineBuilder.buildEsperDrivingInterruptionIntervalEvents(
sessionId,
driverKey,
timeline,
significantDrivingMinutes
);
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals = List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals =
clipEsperDrivingInterruptionIntervalEvents( clipEsperDrivingInterruptionIntervalEvents(
driverTimelineBuilder.buildEsperDrivingInterruptionIntervalEvents( rawDrivingInterruptionIntervals,
sessionId, requestedFrom,
driverKey, requestedTo
timeline, );
significantDrivingMinutes List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals =
clipEsperDrivingInterruptionIntervalEvents(
driverTimelineBuilder.buildEsperDrivingInterruptionVehicleChangeIntervalEvents(
rawDrivingInterruptionIntervals
), ),
requestedFrom, requestedFrom,
requestedTo requestedTo
); );
List<TachographEsperVuCardAbsentIntervalEvent> rawVuCardAbsentIntervals =
driverTimelineBuilder.buildEsperVuCardAbsentIntervalEvents(timeline);
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> potentialHomeOvernightStayIntervals =
clipEsperPotentialHomeOvernightStayIntervalEvents(
driverTimelineBuilder.buildEsperPotentialHomeOvernightStayIntervalEvents(
rawDrivingInterruptionIntervals,
rawVuCardAbsentIntervals,
minimumRestPeriodMinutes
),
rawVuCardAbsentIntervals,
requestedFrom,
requestedTo
);
List<TachographEsperVehicleUsageIntervalEvent> vehicleUsageIntervals = clipEsperVehicleUsageIntervalEvents( List<TachographEsperVehicleUsageIntervalEvent> vehicleUsageIntervals = clipEsperVehicleUsageIntervalEvents(
driverTimelineBuilder.buildEsperVehicleUsageIntervalEvents(timeline), driverTimelineBuilder.buildEsperVehicleUsageIntervalEvents(timeline),
requestedFrom, requestedFrom,
requestedTo requestedTo
); );
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals = clipEsperVuCardAbsentIntervalEvents( List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals = clipEsperVuCardAbsentIntervalEvents(
driverTimelineBuilder.buildEsperVuCardAbsentIntervalEvents(timeline), rawVuCardAbsentIntervals,
requestedFrom, requestedFrom,
requestedTo requestedTo
); );
@ -192,11 +217,15 @@ public class TachographFileSessionProcessingService {
activityIntervals.size(), activityIntervals.size(),
drivingIntervals.size(), drivingIntervals.size(),
drivingInterruptionIntervals.size(), drivingInterruptionIntervals.size(),
drivingInterruptionVehicleChangeIntervals.size(),
potentialHomeOvernightStayIntervals.size(),
vehicleUsageIntervals.size(), vehicleUsageIntervals.size(),
vuCardAbsentIntervals.size(), vuCardAbsentIntervals.size(),
activityIntervals, activityIntervals,
drivingIntervals, drivingIntervals,
drivingInterruptionIntervals, drivingInterruptionIntervals,
drivingInterruptionVehicleChangeIntervals,
potentialHomeOvernightStayIntervals,
vehicleUsageIntervals, vehicleUsageIntervals,
vuCardAbsentIntervals, vuCardAbsentIntervals,
esperProjectionNotes() esperProjectionNotes()
@ -310,6 +339,8 @@ public class TachographFileSessionProcessingService {
Duration.between(start, end).getSeconds(), Duration.between(start, end).getSeconds(),
interval.previousDrivingSourceIntervalId(), interval.previousDrivingSourceIntervalId(),
interval.nextDrivingSourceIntervalId(), interval.nextDrivingSourceIntervalId(),
interval.previousRegistrationKey(),
interval.nextRegistrationKey(),
interval.previousVehicleKey(), interval.previousVehicleKey(),
interval.nextVehicleKey() interval.nextVehicleKey()
); );
@ -355,6 +386,49 @@ public class TachographFileSessionProcessingService {
.toList(); .toList();
} }
private List<TachographEsperPotentialHomeOvernightStayIntervalEvent> clipEsperPotentialHomeOvernightStayIntervalEvents(
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> intervals,
List<TachographEsperVuCardAbsentIntervalEvent> 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<ResolvedActivityInterval> synthesizeUnknownGaps( private List<ResolvedActivityInterval> synthesizeUnknownGaps(
List<ResolvedActivityInterval> knownIntervals, List<ResolvedActivityInterval> knownIntervals,
Duration gapDetectionTolerance Duration gapDetectionTolerance
@ -899,6 +973,12 @@ public class TachographFileSessionProcessingService {
: request.significantDrivingMinutes(); : request.significantDrivingMinutes();
} }
private int resolveMinimumRestPeriodMinutes(TachographEsperEventsProcessingRequest request) {
return request.minimumRestPeriodMinutes() == null
? properties.getTachographFileSession().getProcessing().getMinimumRestPeriodMinutes()
: request.minimumRestPeriodMinutes();
}
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.",
@ -913,12 +993,37 @@ public class TachographFileSessionProcessingService {
"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.",
"Driving interruption intervals are gaps between consecutive driving intervals longer than the configured significant-driving threshold.", "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.", "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.", "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." "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<TachographEsperVuCardAbsentIntervalEvent> 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) { private OffsetDateTime utc(OffsetDateTime value) {
return value == null ? null : value.withOffsetSameInstant(java.time.ZoneOffset.UTC); return value == null ? null : value.withOffsetSameInstant(java.time.ZoneOffset.UTC);
} }

View File

@ -6,6 +6,7 @@ create schema SignificantDrivingInterval(
startedAtEpochSecond long, startedAtEpochSecond long,
endedAtEpochSecond long, endedAtEpochSecond long,
durationSeconds long, durationSeconds long,
registrationKey string,
vehicleKey string vehicleKey string
); );
@ -17,6 +18,8 @@ create schema DrivingInterruptionInterval(
durationSeconds long, durationSeconds long,
previousDrivingSourceIntervalId string, previousDrivingSourceIntervalId string,
nextDrivingSourceIntervalId string, nextDrivingSourceIntervalId string,
previousRegistrationKey string,
nextRegistrationKey string,
previousVehicleKey string, previousVehicleKey string,
nextVehicleKey string nextVehicleKey string
); );
@ -30,6 +33,7 @@ select
startedAtEpochSecond, startedAtEpochSecond,
endedAtEpochSecond, endedAtEpochSecond,
durationSeconds, durationSeconds,
registrationKey,
vehicleKey vehicleKey
from TachographActivityIntervalInputEvent(activityType = 'DRIVE', durationSeconds > ${SIGNIFICANT_DRIVING_THRESHOLD_SECONDS}); from TachographActivityIntervalInputEvent(activityType = 'DRIVE', durationSeconds > ${SIGNIFICANT_DRIVING_THRESHOLD_SECONDS});
@ -45,6 +49,8 @@ select
next.startedAtEpochSecond - priorInterval.endedAtEpochSecond as durationSeconds, next.startedAtEpochSecond - priorInterval.endedAtEpochSecond as durationSeconds,
priorInterval.lastSourceIntervalId as previousDrivingSourceIntervalId, priorInterval.lastSourceIntervalId as previousDrivingSourceIntervalId,
next.firstSourceIntervalId as nextDrivingSourceIntervalId, next.firstSourceIntervalId as nextDrivingSourceIntervalId,
priorInterval.registrationKey as previousRegistrationKey,
next.registrationKey as nextRegistrationKey,
priorInterval.vehicleKey as previousVehicleKey, priorInterval.vehicleKey as previousVehicleKey,
next.vehicleKey as nextVehicleKey next.vehicleKey as nextVehicleKey
from PreviousSignificantDrivingInterval as priorInterval from PreviousSignificantDrivingInterval as priorInterval

View File

@ -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
);

View File

@ -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;

View File

@ -21,6 +21,7 @@ import at.procon.eventhub.tachographfilesession.dto.TachographFileSessionSummary
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.TachographEsperDrivingInterruptionIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent;
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;
@ -81,6 +82,8 @@ class TachographFileSessionControllerTest {
2, 2,
1, 1,
1, 1,
1,
1,
2, 2,
1, 1,
List.of(new TachographEsperActivityIntervalEvent( List.of(new TachographEsperActivityIntervalEvent(
@ -129,6 +132,36 @@ class TachographFileSessionControllerTest {
1800L, 1800L,
"ACT-2", "ACT-2",
"ACT-3", "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-1",
"VIN-2" "VIN-2"
)), )),
@ -213,7 +246,8 @@ class TachographFileSessionControllerTest {
{ {
"occurredFrom": "2026-05-12T08:30:00Z", "occurredFrom": "2026-05-12T08:30:00Z",
"occurredTo": "2026-05-12T11:30:00Z", "occurredTo": "2026-05-12T11:30:00Z",
"significantDrivingMinutes": 3 "significantDrivingMinutes": 3,
"minimumRestPeriodMinutes": 720
} }
""")) """))
.andExpect(status().isOk()) .andExpect(status().isOk())
@ -223,9 +257,17 @@ class TachographFileSessionControllerTest {
.andExpect(jsonPath("$.requestedTo").value("2026-05-12T11: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("$.drivingInterruptionIntervalCount").value(1))
.andExpect(jsonPath("$.drivingInterruptionVehicleChangeIntervalCount").value(1))
.andExpect(jsonPath("$.potentialHomeOvernightStayIntervalCount").value(1))
.andExpect(jsonPath("$.vuCardAbsentIntervalCount").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].previousVehicleKey").value("VIN-1"))
.andExpect(jsonPath("$.drivingInterruptionIntervals[0].nextVehicleKey").value("VIN-2")) .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")); .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

@ -11,6 +11,7 @@ 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.TachographEsperDrivingInterruptionIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent;
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;
@ -448,7 +449,72 @@ class DriverTimelineBuilderTest {
assertThat(interruptions.get(0).durationSeconds()).isEqualTo(480L); assertThat(interruptions.get(0).durationSeconds()).isEqualTo(480L);
assertThat(interruptions.get(0).previousDrivingSourceIntervalId()).isEqualTo("ACT-1"); assertThat(interruptions.get(0).previousDrivingSourceIntervalId()).isEqualTo("ACT-1");
assertThat(interruptions.get(0).nextDrivingSourceIntervalId()).isEqualTo("ACT-3"); 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).previousVehicleKey()).isEqualTo("VIN-1");
assertThat(interruptions.get(0).nextVehicleKey()).isEqualTo("VIN-2"); assertThat(interruptions.get(0).nextVehicleKey()).isEqualTo("VIN-2");
List<TachographEsperDrivingInterruptionIntervalEvent> 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<TachographEsperDrivingInterruptionIntervalEvent> 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<TachographEsperVuCardAbsentIntervalEvent> 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<TachographEsperPotentialHomeOvernightStayIntervalEvent> 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");
} }
} }

View File

@ -86,6 +86,8 @@ class TachographFileSessionProcessingServiceTest {
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.drivingInterruptionIntervalCount()).isEqualTo(0);
assertThat(result.drivingInterruptionVehicleChangeIntervalCount()).isEqualTo(0);
assertThat(result.potentialHomeOvernightStayIntervalCount()).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"));
@ -155,7 +157,8 @@ class TachographFileSessionProcessingServiceTest {
new TachographEsperEventsProcessingRequest( new TachographEsperEventsProcessingRequest(
OffsetDateTime.parse("2026-05-01T08:45:00Z"), OffsetDateTime.parse("2026-05-01T08:45:00Z"),
OffsetDateTime.parse("2026-05-01T12:30: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.activityIntervals().get(0).clippedToRequestedPeriod()).isTrue();
assertThat(result.drivingIntervalCount()).isEqualTo(2); assertThat(result.drivingIntervalCount()).isEqualTo(2);
assertThat(result.drivingInterruptionIntervalCount()).isEqualTo(1); 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).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).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).previousVehicleKey()).isEqualTo("VIN-1");
assertThat(result.drivingInterruptionIntervals().get(0).nextVehicleKey()).isEqualTo("VIN-2"); 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.vehicleUsageIntervalCount()).isEqualTo(2);
assertThat(result.vehicleUsageIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T08:45:00Z")); 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(0).odometerBeginKm()).isNull();
@ -178,6 +190,80 @@ class TachographFileSessionProcessingServiceTest {
assertThat(result.vuCardAbsentIntervalCount()).isEqualTo(1); 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 @Test
void evaluatesOperatingPeriodsFromSessionTimeline() { void evaluatesOperatingPeriodsFromSessionTimeline() {
EventHubProperties properties = new EventHubProperties(); EventHubProperties properties = new EventHubProperties();