Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions Sources/BBCLI/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,36 @@ func fetchEVTripDetails(state: CLIState) async throws {
}
}

@MainActor
func fetchEVTripInfo(state: CLIState) async throws {
guard let vehicle = selectVehicle(state: state) else { return }
guard let token = state.authToken else {
throw APIError(message: "Not logged in")
}

guard let client = state.client else {
throw APIError(message: "No API client initialized")
}

let dateString = prompt("Enter date (yyyyMMdd): ")
guard !dateString.isEmpty else { return }

printSubheader("Fetching EV Trip Info for \(vehicle.model) on \(dateString)")

let trips = try await client.fetchEVTripInfo(for: vehicle, authToken: token, dateString: dateString) ?? []

printSuccess("Found \(trips.count) individual trip(s) for the day")

for (index, info) in trips.enumerated() {
print("\n[\(index + 1)] Time: \(info.hhmmss)")
print(" Distance: \(info.distance)")
print(" Drive Time: \(info.driveTimeMinutes) min")
print(" Idle Time: \(info.idleTimeMinutes) min")
print(" Avg Speed: \(info.avgSpeed)")
print(" Max Speed: \(info.maxSpeed)")
}
}

// MARK: - Interactive Menu

func showMenu() {
Expand All @@ -474,6 +504,7 @@ func showMenu() {
8. Stop Charge
9. Set Charge Limits
10. Fetch EV Trip Details
11. Fetch EV Trip Info
0. Exit

""")
Expand Down Expand Up @@ -520,6 +551,8 @@ func runInteractiveLoop(state: CLIState) async {
)
case "10":
try await fetchEVTripDetails(state: state)
case "11":
try await fetchEVTripInfo(state: state)
case "0", "q", "quit", "exit":
print("\nGoodbye!")
return
Expand Down
7 changes: 7 additions & 0 deletions Sources/BetterBlueKit/API/APIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ public protocol APIClientProtocol {
/// Optional: Fetch EV trip details for a vehicle (not all brands/APIs support this)
func fetchEVTripDetails(for vehicle: Vehicle, authToken: AuthToken) async throws -> [EVTripDetail]?

/// Optional: Fetch specific EV trip info summary for a given date (not all brands/APIs support this)
func fetchEVTripInfo(for vehicle: Vehicle, authToken: AuthToken, dateString: String) async throws -> [EVTripInfo]?

/// Returns true if this API client implements `fetchEVTripDetails`
func supportsEVTripDetails() -> Bool

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can clear up all the naming here. Instead of having EVTripDetails and EVTripInfo, which is super unclear, we could have EVTripsSummary and EVTripInfo.

This supportsEVTripDetails() would then return a list of EVTripType enums. US would return [EVTripSummary], EU would return [EVTripSummary, EVTripInfo]


Expand Down Expand Up @@ -170,6 +173,10 @@ extension APIClientProtocol {
nil
}

public func fetchEVTripInfo(for vehicle: Vehicle, authToken: AuthToken, dateString: String) async throws -> [EVTripInfo]? {
nil
}

public func supportsEVTripDetails() -> Bool {
false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -424,10 +424,51 @@ extension HyundaiEuropeAPIClient {
drivetrainEnergy: motorPwrCsp,
batteryCareEnergy: batteryMgPwrCsp,
startDate: startDate,
durationSeconds: 0, // Not provided
avgSpeed: 0, // Not provided
maxSpeed: 0 // Not provided
durationSeconds: 0, // Populated later from /tripinfo
avgSpeed: 0, // Populated later from /tripinfo
maxSpeed: 0 // Populated later from /tripinfo
)
}
}

package func parseIndividualTripsResponse(_ data: Data) throws -> [EVTripInfo] {
guard
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let resMsg = json["resMsg"] as? [String: Any],
let dayTripList = resMsg["dayTripList"] as? [[String: Any]]
else {
throw APIError(message: "Failed to parse EU individual trips", apiName: apiName)
}

var allTrips: [EVTripInfo] = []

for dayTrip in dayTripList {
let date = dayTrip["tripDay"] as? String ?? dayTrip["date"] as? String ?? ""

guard let tripList = dayTrip["tripList"] as? [[String: Any]] else {
continue
}

for tripData in tripList {
let hhmmss = tripData["tripTime"] as? String ?? tripData["hhmmss"] as? String ?? ""
let driveTime = tripData["tripDrvTime"] as? Int ?? tripData["drive_time"] as? Int ?? 0
let idleTime = tripData["tripIdleTime"] as? Int ?? tripData["idle_time"] as? Int ?? 0
let distance = getDoubleFromJson(from: tripData, key: "tripDist")
let avgSpeed = getDoubleFromJson(from: tripData, key: "tripAvgSpeed")
let maxSpeed = getDoubleFromJson(from: tripData, key: "tripMaxSpeed")

allTrips.append(EVTripInfo(
date: date,
hhmmss: hhmmss,
driveTimeMinutes: driveTime,
idleTimeMinutes: idleTime,
distance: distance,
avgSpeed: avgSpeed,
maxSpeed: maxSpeed
))
}
}

return allTrips
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -328,4 +328,23 @@ public final class HyundaiEuropeAPIClient: APIClientBase, APIClientProtocol {

return try parseEVTripDetailsResponse(data, vehicle: vehicle)
}

public func fetchEVTripInfo(for vehicle: Vehicle, authToken: AuthToken, dateString: String) async throws -> [EVTripInfo]? {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make the dateString an actual Date object, which then gets turned into a string inside the fetchEVTripInfo. It will make the interface a lot cleaner, and minimizes funny business with European/American dates if this endpoint is supported in the US in the future

let ccs2 = vehicle.marketOptions?.ccs2Supported ?? false
let url = "\(baseURL)/api/v1/spa/vehicles/\(vehicle.regId)/tripinfo"

let (data, _, _) = try await performJSONRequest(
url: url,
method: .POST,
headers: authorizedHeaders(authToken: authToken, ccs2: ccs2),
body: [
"tripPeriodType": 1,
"setTripDay": dateString
],
requestType: .fetchEVTripDetails,
vin: vehicle.vin
)

return try parseIndividualTripsResponse(data)
}
}
23 changes: 23 additions & 0 deletions Sources/BetterBlueKit/Models/EVTripDetails.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,26 @@ public struct EVTripDetailsResponse: Codable, Sendable {
self.trips = trips
}
}

// MARK: - EV Trip Info

/// Represents summary of a specific trip from the tripinfo endpoint
public struct EVTripInfo: Codable, Hashable, Sendable {
public let date: String

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use stronger types here -- Date, Distance, and Duration

public let hhmmss: String
public let driveTimeMinutes: Int
public let idleTimeMinutes: Int
public let distance: Double
public let avgSpeed: Double
public let maxSpeed: Double

public init(date: String, hhmmss: String, driveTimeMinutes: Int, idleTimeMinutes: Int, distance: Double, avgSpeed: Double, maxSpeed: Double) {
self.date = date
self.hhmmss = hhmmss
self.driveTimeMinutes = driveTimeMinutes
self.idleTimeMinutes = idleTimeMinutes
self.distance = distance
self.avgSpeed = avgSpeed
self.maxSpeed = maxSpeed
}
}