Meter DC charging only, on a monotonic clock
The charging session meter used the head unit's wall clock, which jumps when it picks up GPS or NTP time. One forward jump mid-charge invented both kWh and hours. The consumption counters already integrate on elapsedRealtime for exactly this reason; the session meter now does too, and keeps wall-clock values only as the start and end stamps a reader needs to place a session on a timeline. Intervals longer than five minutes are no longer bridged. A head unit that slept mid-charge came back and billed the whole gap at the last power reading, and a session interrupted overnight reported the night as charging time. Reported duration is metered charging time now, not the span from start to end. Sessions are metered on DC alone, at or above 10 kW. AC charging on this car is an overnight affair the head unit sleeps through, so its curve, duration and energy were guesses either way. The charging screen says so rather than letting the previous DC session's figures be read as the current AC charge. Also: - A daily consumption record uploaded after an offline spell carries the lifetime total as it stood at the close of that day. Every backlogged day used to be stamped with the current lifetime, which plots as a flat line and one cliff. - The offline session queue is capped at 50. It is a single SharedPreferences string read into memory in full, and nothing drained it while the cloud endpoint was unconfigured. - A long charge keeps the start of its curve: at 720 points the sample interval doubles and the curve is thinned, rather than dropping the oldest point. - The cloud sync throttle runs on the monotonic clock, and catch-up sends up to 10 days and 10 sessions per cycle rather than one and three. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1a37e52eda
commit
e9bfa1217b
@@ -23,6 +23,8 @@ final class ConsumptionCloudClient {
|
||||
private static final String PREFS = "consumption_cloud";
|
||||
private static final String INSTALLATION_ID = "installation_id";
|
||||
private static final String LAST_CONSUMPTION_DATE = "last_consumption_date";
|
||||
private static final int MAX_DAYS_PER_SYNC = 10;
|
||||
private static final int MAX_CHARGES_PER_SYNC = 10;
|
||||
|
||||
private final String endpoint;
|
||||
private final String token;
|
||||
@@ -42,9 +44,16 @@ final class ConsumptionCloudClient {
|
||||
installationId = existing;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param nowMs wall clock, recorded with the payload — the throttle below runs on a
|
||||
* monotonic clock instead, so a backwards NTP correction cannot stall
|
||||
* syncing for however long the jump was.
|
||||
*/
|
||||
void syncIfDue(ConsumptionTracker tracker, ChargeSessionTracker charges, long nowMs) {
|
||||
if (endpoint.isEmpty() || token.isEmpty() || nowMs - lastAttemptMs < SYNC_INTERVAL_MS) return;
|
||||
lastAttemptMs = nowMs;
|
||||
long elapsed = android.os.SystemClock.elapsedRealtime();
|
||||
if (endpoint.isEmpty() || token.isEmpty()
|
||||
|| (lastAttemptMs != 0L && elapsed - lastAttemptMs < SYNC_INTERVAL_MS)) return;
|
||||
lastAttemptMs = elapsed;
|
||||
try {
|
||||
syncDailyConsumption(tracker, nowMs);
|
||||
syncPendingCharges(charges);
|
||||
@@ -53,23 +62,26 @@ final class ConsumptionCloudClient {
|
||||
}
|
||||
}
|
||||
|
||||
/** Drains the backlog a few days per cycle; one a cycle took an hour per fortnight. */
|
||||
private void syncDailyConsumption(ConsumptionTracker tracker, long nowMs) throws Exception {
|
||||
String saved = prefs.getString(LAST_CONSUMPTION_DATE, "");
|
||||
LocalDate lastUploaded = null;
|
||||
try { if (saved != null && !saved.isEmpty()) lastUploaded = LocalDate.parse(saved); }
|
||||
catch (RuntimeException ignored) { }
|
||||
LocalDate target = tracker.nextStoredDayAfter(lastUploaded);
|
||||
if (target == null) return;
|
||||
ConsumptionTracker.CloudSnapshot snapshot = tracker.cloudSnapshot(target);
|
||||
JSONObject body = new JSONObject()
|
||||
.put("installationId", installationId)
|
||||
.put("recordedAt", nowMs)
|
||||
.put("date", snapshot.date)
|
||||
.put("day", totals(snapshot.day))
|
||||
.put("lifetime", totals(snapshot.lifetime));
|
||||
if (snapshot.sohPercent != null) body.put("sohPercent", snapshot.sohPercent);
|
||||
if (send("/v1/consumption/daily", "PUT", body)) {
|
||||
for (int sent = 0; sent < MAX_DAYS_PER_SYNC; sent++) {
|
||||
LocalDate target = tracker.nextStoredDayAfter(lastUploaded);
|
||||
if (target == null) return;
|
||||
ConsumptionTracker.CloudSnapshot snapshot = tracker.cloudSnapshot(target);
|
||||
JSONObject body = new JSONObject()
|
||||
.put("installationId", installationId)
|
||||
.put("recordedAt", nowMs)
|
||||
.put("date", snapshot.date)
|
||||
.put("day", totals(snapshot.day))
|
||||
.put("lifetime", totals(snapshot.lifetime));
|
||||
if (snapshot.sohPercent != null) body.put("sohPercent", snapshot.sohPercent);
|
||||
if (!send("/v1/consumption/daily", "PUT", body)) return;
|
||||
prefs.edit().putString(LAST_CONSUMPTION_DATE, target.toString()).commit();
|
||||
lastUploaded = target;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +93,7 @@ final class ConsumptionCloudClient {
|
||||
String id = session.getString("sessionId");
|
||||
if (!send("/v1/charging-sessions/" + id, "PUT", body)) return;
|
||||
charges.markUploaded(id);
|
||||
if (++sent >= 3) return;
|
||||
if (++sent >= MAX_CHARGES_PER_SYNC) return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user