Added GPS, Odometer History, and Safety Event

This commit is contained in:
Hyojin Ahn 2026-01-12 12:24:46 -05:00
parent 1a6ab4669d
commit ce146cd41a
7 changed files with 235 additions and 20 deletions

View File

@ -44,7 +44,7 @@ public class SamsaraClient {
* Vehicle live stats (engine + gps) * Vehicle live stats (engine + gps)
* Raw JSON 반환 * Raw JSON 반환
*/ */
public String getVehicleStatsFeed(List<String> vehicleExternalIds) { public String getVehicleGpsFeed(List<String> vehicleExternalIds) {
String vehicleIdsParam = String.join(",", vehicleExternalIds); String vehicleIdsParam = String.join(",", vehicleExternalIds);
@ -58,4 +58,51 @@ public class SamsaraClient {
.bodyToMono(String.class) .bodyToMono(String.class)
.block(); .block();
} }
/**
* Vehicle odometer history (OBD odometer meters)
* Raw JSON 반환
*/
public String getVehicleOdometerHistory(
List<String> vehicleExternalIds,
Instant startTime,
Instant endTime
) {
String vehicleIdsParam = String.join(",", vehicleExternalIds);
return webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/fleet/vehicles/stats/history")
.queryParam("vehicleIds", vehicleIdsParam)
.queryParam("startTime", startTime.toString())
.queryParam("endTime", endTime.toString())
.queryParam("types", "obdOdometerMeters")
.build())
.retrieve()
.bodyToMono(String.class)
.block();
}
/**
* Vehicle Safety Event
* Raw JSON 반환
*
*/
public String getVehicleSafetyEvents(Instant startTime, Instant endTime, List<String> vehicleExternalIds) {
String vehicleIdsParam = String.join(",", vehicleExternalIds);
return webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/fleet/safety-events")
.queryParam("startTime", startTime.toString())
.queryParam("endTime", endTime.toString())
.queryParam("vehicleIds", vehicleIdsParam)
.build())
.retrieve()
.bodyToMono(String.class)
.block();
}
} }

View File

@ -1,10 +1,13 @@
package com.goi.integration.samsara.controller; package com.goi.integration.samsara.controller;
import com.goi.integration.samsara.dto.VehicleStatResponseDto; import com.goi.integration.samsara.dto.VehicleOdometerHistoryResponseDto;
import com.goi.integration.samsara.service.VehicleStatService; import com.goi.integration.samsara.dto.VehicleGpsResponseDto;
import com.goi.integration.samsara.service.VehicleOdometerHistoryService;
import com.goi.integration.samsara.service.VehicleGpsService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import java.time.Instant;
import java.util.List; import java.util.List;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -14,12 +17,36 @@ import org.springframework.web.bind.annotation.*;
@RequiredArgsConstructor @RequiredArgsConstructor
public class VehicleController { public class VehicleController {
private final VehicleStatService vehicleStatService; private final VehicleGpsService vehicleStatService;
private final VehicleOdometerHistoryService vehicleOdometerHistoryService;
@GetMapping("/stat") @GetMapping("/stat/gps")
public List<VehicleStatResponseDto> getStats( public List<VehicleGpsResponseDto> getGps(
@RequestParam List<String> vehicleIds @RequestParam List<String> vehicleIds
) { ) {
return vehicleStatService.getVehicleStats(vehicleIds); return vehicleStatService.getVehicleGps(vehicleIds);
} }
/**
* Vehicle odometer history summary (window-based)
*/
@GetMapping("/stat/odometer/history")
public List<VehicleOdometerHistoryResponseDto> getOdometerHistory(
@RequestParam List<String> vehicleIds,
@RequestParam Instant startTime,
@RequestParam Instant endTime
) {
return vehicleOdometerHistoryService.getOdometerHistory(
vehicleIds, startTime, endTime
);
}
// @GetMapping("/safety/events")
// public List<VehicleSafetyEventResponseDto> getSafetyEvents(
// @RequestParam Instant startTime,
// @RequestParam Instant endTime,
// @RequestParam List<String> vehicleIds
// ) {
// return vehicleSafetyEventService.getSafetyEvents(vehicleIds, startTime, endTime);
// }
} }

View File

@ -12,10 +12,10 @@ import java.time.Instant;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@Builder @Builder
public class VehicleStatResponseDto { public class VehicleGpsResponseDto {
String vesVehicleExternalId; private String vesVehicleExternalId;
Boolean vesEngineOn; private Boolean vesEngineOn;
BigDecimal vesLatitude; private BigDecimal vesLatitude;
BigDecimal vesLongitude; private BigDecimal vesLongitude;
Instant vesGpsTime; // UTC 기준 절대 시점 private Instant vesGpsTime; // UTC 기준 절대 시점
} }

View File

@ -0,0 +1,21 @@
package com.goi.integration.samsara.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.Instant;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class VehicleOdometerHistoryResponseDto {
private String vohVehicleExternalId;
private Instant vohFirstSampleTime;
private Instant vohLastSampleTime;
private Long vohFirstOdometerMeters;
private Long vohLastOdometerMeters;
private Integer vohSampleCount;
}

View File

@ -0,0 +1,21 @@
package com.goi.integration.samsara.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.Instant;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class VehicleSafetyEventResponseDto {
private String vesVehicleExternalId;
private Boolean vesEngineOn;
private BigDecimal vesLatitude;
private BigDecimal vesLongitude;
private Instant vesGpsTime; // UTC 기준 절대 시점
}

View File

@ -3,7 +3,7 @@ package com.goi.integration.samsara.service;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.goi.integration.samsara.client.SamsaraClient; import com.goi.integration.samsara.client.SamsaraClient;
import com.goi.integration.samsara.dto.VehicleStatResponseDto; import com.goi.integration.samsara.dto.VehicleGpsResponseDto;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -20,7 +20,7 @@ import java.util.Optional;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j @Slf4j
public class VehicleStatService { public class VehicleGpsService {
private final SamsaraClient samsaraClient; private final SamsaraClient samsaraClient;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
@ -28,14 +28,14 @@ public class VehicleStatService {
/** /**
* 차량들의 실시간 상태 (engine + gps) * 차량들의 실시간 상태 (engine + gps)
*/ */
public List<VehicleStatResponseDto> getVehicleStats(List<String> vehicleExternalIds) { public List<VehicleGpsResponseDto> getVehicleGps(List<String> vehicleExternalIds) {
if (vehicleExternalIds == null || vehicleExternalIds.isEmpty()) { if (vehicleExternalIds == null || vehicleExternalIds.isEmpty()) {
return List.of(); return List.of();
} }
// call Samsara API // call Samsara API
String rawJson = samsaraClient.getVehicleStatsFeed(vehicleExternalIds); String rawJson = samsaraClient.getVehicleGpsFeed(vehicleExternalIds);
if (rawJson == null || rawJson.isBlank()) { if (rawJson == null || rawJson.isBlank()) {
log.warn("Samsara stats feed returned empty response"); log.warn("Samsara stats feed returned empty response");
return List.of(); return List.of();
@ -52,7 +52,7 @@ public class VehicleStatService {
} }
// set response // set response
List<VehicleStatResponseDto> result = new ArrayList<>(); List<VehicleGpsResponseDto> result = new ArrayList<>();
for (JsonNode vehicleNode : dataArray) { for (JsonNode vehicleNode : dataArray) {
parseVehicleNode(vehicleNode).ifPresent(result::add); parseVehicleNode(vehicleNode).ifPresent(result::add);
} }
@ -69,7 +69,7 @@ public class VehicleStatService {
Parsing 필요한 정보만 Parsing 필요한 정보만
=============================== */ =============================== */
private Optional<VehicleStatResponseDto> parseVehicleNode(JsonNode vehicleNode) { private Optional<VehicleGpsResponseDto> parseVehicleNode(JsonNode vehicleNode) {
// Samsara vehicle ID (external) // Samsara vehicle ID (external)
String vehicleId = vehicleNode.path("id").asText(null); String vehicleId = vehicleNode.path("id").asText(null);
@ -81,7 +81,7 @@ public class VehicleStatService {
GpsInfo gpsInfo = extractLatestGps(vehicleNode.path("gps")); GpsInfo gpsInfo = extractLatestGps(vehicleNode.path("gps"));
return Optional.of( return Optional.of(
VehicleStatResponseDto.builder() VehicleGpsResponseDto.builder()
.vesVehicleExternalId(vehicleId) .vesVehicleExternalId(vehicleId)
.vesEngineOn(engineOn) .vesEngineOn(engineOn)
.vesLatitude(gpsInfo.latitude) .vesLatitude(gpsInfo.latitude)

View File

@ -0,0 +1,99 @@
package com.goi.integration.samsara.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.goi.integration.samsara.client.SamsaraClient;
import com.goi.integration.samsara.dto.VehicleOdometerHistoryResponseDto;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class VehicleOdometerHistoryService {
private final SamsaraClient samsaraClient;
private final ObjectMapper objectMapper = new ObjectMapper();
public List<VehicleOdometerHistoryResponseDto> getOdometerHistory(
List<String> vehicleExternalIds,
Instant startTime,
Instant endTime
) {
// api 호출
String rawJson = samsaraClient.getVehicleOdometerHistory(vehicleExternalIds, startTime, endTime);
return parseOdometerHistory(rawJson);
}
private List<VehicleOdometerHistoryResponseDto> parseOdometerHistory(String rawJson) {
List<VehicleOdometerHistoryResponseDto> results = new ArrayList<>();
try {
JsonNode root = objectMapper.readTree(rawJson);
JsonNode dataArray = root.path("data");
for (JsonNode vehicleNode : dataArray) {
String vehicleId = vehicleNode.path("id").asText();
JsonNode odoArray = vehicleNode.path("obdOdometerMeters");
if (!odoArray.isArray() || odoArray.isEmpty()) {
// 데이터 없음
results.add(
VehicleOdometerHistoryResponseDto.builder()
.vohVehicleExternalId(vehicleId)
.vohSampleCount(0)
.build()
);
continue;
}
Iterator<JsonNode> it = odoArray.elements();
JsonNode first = it.next();
JsonNode last = first;
int count = 1;
while (it.hasNext()) {
last = it.next();
count++;
}
// 거리 계산은 opr 에서. 여기선 이전 누적치를 모름.
long firstValue = first.path("value").asLong();
long lastValue = last.path("value").asLong();
Instant firstTime = Instant.parse(first.path("time").asText());
Instant lastTime = Instant.parse(last.path("time").asText());
results.add(
VehicleOdometerHistoryResponseDto.builder()
.vohVehicleExternalId(vehicleId)
.vohFirstSampleTime(firstTime)
.vohLastSampleTime(lastTime)
.vohFirstOdometerMeters(firstValue)
.vohLastOdometerMeters(lastValue)
.vohSampleCount(count)
.build()
);
}
} catch (Exception e) {
log.error("[ODOMETER][PARSE_FAIL]", e);
throw new RuntimeException("Failed to parse vehicle odometer history", e);
}
return results;
}
}