Sync today's driving totals every 30 minutes

This commit is contained in:
TCFDONMEZ
2026-10-08 16:46:47 +03:00
parent c06c37761b
commit 056fc2a5bd
4 changed files with 60 additions and 19 deletions
+9
View File
@@ -1,5 +1,14 @@
# Changelog
## 2.2.39
### Changed
- Today's cumulative driving totals are now upserted to MongoDB every 30 minutes instead
of waiting until the following day. Re-sending totals cannot double-count a retry.
- DC charging sessions remain entirely local while charging and are sent immediately after
completion. Failed completed-session uploads remain queued and retry every five minutes.
## 2.2.38
### Changed
+1
View File
@@ -0,0 +1 @@
52f58d348874fc62a5eefd470686583677769ff55bfe82c234c4f636769a0a90 FatihsMG4-unstable-2.2.39.apk
+2 -2
View File
@@ -34,8 +34,8 @@ android {
applicationId "com.evsuite.abrp"
minSdk 27
targetSdk 36
versionCode 55
versionName "2.2.38"
versionCode 56
versionName "2.2.39"
// MongoDB credentials never ship in the APK. The app only talks to the small
// authenticated API in server/, which owns the database connection.
@@ -15,10 +15,11 @@ import java.time.LocalDate;
import java.util.List;
import java.util.UUID;
/** Best-effort MongoDB sync through the API in server/. Never blocks local persistence. */
/** Best-effort MongoDB sync through the web API. Never blocks local persistence. */
final class ConsumptionCloudClient {
private static final String TAG = "FatihsMG4.Mongo";
private static final long SYNC_INTERVAL_MS = 5 * 60_000L;
private static final long CONSUMPTION_SYNC_INTERVAL_MS = 30 * 60_000L;
private static final long CHARGE_RETRY_INTERVAL_MS = 5 * 60_000L;
private static final int TIMEOUT_MS = 8_000;
private static final String PREFS = "consumption_cloud";
private static final String INSTALLATION_ID = "installation_id";
@@ -30,7 +31,9 @@ final class ConsumptionCloudClient {
private final String token;
private final String installationId;
private final SharedPreferences prefs;
private long lastAttemptMs;
private long lastConsumptionAttemptMs;
private long lastChargeAttemptMs;
private String lastAttemptedChargeId = "";
ConsumptionCloudClient(Context context) {
endpoint = BuildConfig.CONSUMPTION_API_URL.trim();
@@ -51,14 +54,15 @@ final class ConsumptionCloudClient {
*/
void syncIfDue(ConsumptionTracker tracker, ChargeSessionTracker charges, long nowMs) {
long elapsed = android.os.SystemClock.elapsedRealtime();
if (endpoint.isEmpty() || token.isEmpty()
|| (lastAttemptMs != 0L && elapsed - lastAttemptMs < SYNC_INTERVAL_MS)) return;
lastAttemptMs = elapsed;
if (endpoint.isEmpty() || token.isEmpty()) return;
syncPendingChargesIfDue(charges, elapsed);
if (lastConsumptionAttemptMs != 0L
&& elapsed - lastConsumptionAttemptMs < CONSUMPTION_SYNC_INTERVAL_MS) return;
lastConsumptionAttemptMs = elapsed;
try {
syncDailyConsumption(tracker, nowMs);
syncPendingCharges(charges);
} catch (Exception e) {
Log.w(TAG, "Cloud sync unavailable");
Log.w(TAG, "Consumption sync unavailable");
}
}
@@ -70,23 +74,50 @@ final class ConsumptionCloudClient {
catch (RuntimeException ignored) { }
for (int sent = 0; sent < MAX_DAYS_PER_SYNC; sent++) {
LocalDate target = tracker.nextStoredDayAfter(lastUploaded);
if (target == null) return;
if (target == null) break;
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);
JSONObject body = consumptionBody(snapshot, nowMs);
if (!send("/v1/consumption/daily", "PUT", body)) return;
prefs.edit().putString(LAST_CONSUMPTION_DATE, target.toString()).commit();
lastUploaded = target;
}
// Today's cumulative counters overwrite the same installation/date document every
// 30 minutes. Sending totals rather than increments makes retries idempotent.
ConsumptionTracker.CloudSnapshot today = tracker.cloudSnapshot(LocalDate.now());
JSONObject current = consumptionBody(today, nowMs);
send("/v1/consumption/daily", "PUT", current);
}
private void syncPendingCharges(ChargeSessionTracker charges) throws Exception {
private JSONObject consumptionBody(ConsumptionTracker.CloudSnapshot snapshot, long nowMs)
throws Exception {
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);
return body;
}
private void syncPendingChargesIfDue(ChargeSessionTracker charges, long elapsed) {
List<JSONObject> pending = charges.pendingUploads();
if (pending.isEmpty()) return;
String firstId = pending.get(0).optString("sessionId", "");
boolean newlyCompleted = !firstId.equals(lastAttemptedChargeId);
if (!newlyCompleted && lastChargeAttemptMs != 0L
&& elapsed - lastChargeAttemptMs < CHARGE_RETRY_INTERVAL_MS) return;
lastAttemptedChargeId = firstId;
lastChargeAttemptMs = elapsed;
try {
syncPendingCharges(charges, pending);
} catch (Exception e) {
Log.w(TAG, "Charging-session sync unavailable");
}
}
private void syncPendingCharges(ChargeSessionTracker charges, List<JSONObject> pending)
throws Exception {
int sent = 0;
for (JSONObject session : pending) {
JSONObject body = new JSONObject(session.toString()).put("installationId", installationId);