App 2.3.0: Server tab, live telemetry to the car server
- New Server tab: server URL, ingest token and installation id, with
Save, Test connection (health, then a read-only token check) and the
config-file import that upstream removed (app folder / USB stick first,
document picker as fallback). Settings are kept encrypted and read on
every use, so the service picks up a change without a restart. Each
installation generates its own id; the inherited upstream UUID and
the baked-in CONSUMPTION_API_* build fields are gone.
- Live telemetry restored from before ea8032a (TelemetryPayload,
UploadCadence, UploadSettings, GPS) and sent in batches to
POST /v1/telemetry instead of ABRP; the server forwards to ABRP. Adds
ext_temp, voltage and current. Unsent samples queue in memory (about
8 h) while offline.
- Upload tab: the live-telemetry switch and cadence controls work again;
the dead service and autostart switches are removed.
- Manifest: location permissions and foreground service types.
- Unit tests compile again: 114 (stable) / 136 (unstable) pass. Tests
for the deleted AbrpApi and HeadUnitWeather removed; new tests for
ServerConfig and TelemetryQueue. Lint passes.
Not yet tried on the car.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,12 +20,13 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Best-effort MongoDB sync through the web API. Never blocks local persistence.
|
||||
* Best-effort sync to the car server. Never blocks local persistence.
|
||||
*
|
||||
* The installationId is a fixed value baked into BuildConfig — never a random UUID — so an
|
||||
* APK wipe cannot orphan the cloud history under a new identity. The consumption UI reads
|
||||
* back its own uploaded totals via {@link #fetchHistoryIfDue}, which means an APK reinstall
|
||||
* picks up right where the old one left off instead of showing zeroes.
|
||||
* Server URL, token and installation id come from {@link ServerConfig}, read on every use so
|
||||
* a change in the Server tab applies at once. The installation id is kept across updates; a
|
||||
* reinstall gets it back through the config file, so the history is not orphaned under a new
|
||||
* identity. The consumption UI reads back its own uploaded totals via
|
||||
* {@link #fetchHistoryIfDue}, so a reinstall picks up where the old one left off.
|
||||
*/
|
||||
final class ConsumptionCloudClient {
|
||||
private static final String TAG = "NilsMG4.Mongo";
|
||||
@@ -37,9 +38,7 @@ final class ConsumptionCloudClient {
|
||||
private static final String CHUNK_CACHE = "chunk_cache"; // JSON array of pending chunks
|
||||
private static final int MAX_CHARGES_PER_SYNC = 10;
|
||||
|
||||
private final String endpoint;
|
||||
private final String token;
|
||||
private final String installationId;
|
||||
private final Context context;
|
||||
private final SharedPreferences prefs;
|
||||
private long lastConsumptionAttemptMs;
|
||||
private long lastChargeAttemptMs;
|
||||
@@ -65,14 +64,12 @@ final class ConsumptionCloudClient {
|
||||
}
|
||||
|
||||
ConsumptionCloudClient(Context context) {
|
||||
endpoint = BuildConfig.CONSUMPTION_API_URL.trim();
|
||||
token = BuildConfig.CONSUMPTION_API_TOKEN.trim();
|
||||
this.context = context.getApplicationContext();
|
||||
prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
|
||||
installationId = BuildConfig.CONSUMPTION_INSTALLATION_ID.trim();
|
||||
if (endpoint.isEmpty() || token.isEmpty()) {
|
||||
Log.w(TAG, "Consumption API not configured: endpoint=" + (endpoint.isEmpty() ? "empty" : "ok")
|
||||
+ ", token=" + (token.isEmpty() ? "empty" : "ok"));
|
||||
}
|
||||
}
|
||||
|
||||
private ServerConfig config() {
|
||||
return ServerConfig.get(context);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,7 +84,7 @@ final class ConsumptionCloudClient {
|
||||
*/
|
||||
void syncIfDue(ConsumptionTracker tracker, ChargeSessionTracker charges, long nowMs) {
|
||||
long elapsed = android.os.SystemClock.elapsedRealtime();
|
||||
if (endpoint.isEmpty() || token.isEmpty()) {
|
||||
if (!config().isConfigured()) {
|
||||
Log.i(TAG, "Sync skipped: API not configured");
|
||||
return;
|
||||
}
|
||||
@@ -200,7 +197,7 @@ final class ConsumptionCloudClient {
|
||||
private JSONObject consumptionBody(ConsumptionTracker.CloudSnapshot snapshot, long nowMs)
|
||||
throws Exception {
|
||||
JSONObject body = new JSONObject()
|
||||
.put("installationId", installationId)
|
||||
.put("installationId", config().installationId)
|
||||
.put("recordedAt", nowMs)
|
||||
.put("date", snapshot.date)
|
||||
.put("day", totals(snapshot.day));
|
||||
@@ -230,7 +227,7 @@ final class ConsumptionCloudClient {
|
||||
Log.i(TAG, "Found " + pending.size() + " pending charge sessions");
|
||||
int sent = 0;
|
||||
for (JSONObject session : pending) {
|
||||
JSONObject body = new JSONObject(session.toString()).put("installationId", installationId);
|
||||
JSONObject body = new JSONObject(session.toString()).put("installationId", config().installationId);
|
||||
String id = session.getString("sessionId");
|
||||
Log.i(TAG, "Uploading charge session: " + id);
|
||||
if (send("/v1/charging-sessions/" + id, "PUT", body)) {
|
||||
@@ -250,7 +247,7 @@ final class ConsumptionCloudClient {
|
||||
HttpURLConnection connection = null;
|
||||
try {
|
||||
byte[] bytes = body.toString().getBytes(StandardCharsets.UTF_8);
|
||||
String url = trimSlash(endpoint) + path;
|
||||
String url = trimSlash(config().url) + path;
|
||||
Log.i(TAG, "Sending " + method + " request to " + url + " (" + bytes.length + " bytes)");
|
||||
Log.i(TAG, "Body: " + body.toString());
|
||||
connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
@@ -260,7 +257,7 @@ final class ConsumptionCloudClient {
|
||||
connection.setDoOutput(true);
|
||||
connection.setFixedLengthStreamingMode(bytes.length);
|
||||
connection.setRequestProperty("Content-Type", "application/json");
|
||||
connection.setRequestProperty("Authorization", "Bearer " + token);
|
||||
connection.setRequestProperty("Authorization", "Bearer " + config().token);
|
||||
try (OutputStream out = connection.getOutputStream()) { out.write(bytes); }
|
||||
int status = connection.getResponseCode();
|
||||
Log.i(TAG, "Response: HTTP " + status);
|
||||
@@ -295,7 +292,7 @@ final class ConsumptionCloudClient {
|
||||
* Returns the current cache immediately; the fresh copy lands on the next poll.
|
||||
*/
|
||||
synchronized List<DayRecord> fetchHistoryIfDue() {
|
||||
if (endpoint.isEmpty() || token.isEmpty()) return cachedHistory;
|
||||
if (!config().isConfigured()) return cachedHistory;
|
||||
long elapsed = android.os.SystemClock.elapsedRealtime();
|
||||
if (lastHistoryFetchMs != 0L && elapsed - lastHistoryFetchMs < HISTORY_FETCH_INTERVAL_MS) {
|
||||
return cachedHistory;
|
||||
@@ -329,7 +326,7 @@ final class ConsumptionCloudClient {
|
||||
* status for the log.
|
||||
*/
|
||||
String flushNow(ConsumptionTracker tracker, long nowMs) {
|
||||
if (endpoint.isEmpty() || token.isEmpty()) return "API not configured";
|
||||
if (!config().isConfigured()) return "API not configured";
|
||||
int cached = drainChunkCache();
|
||||
ConsumptionTracker.CloudSnapshot chunk = tracker.takeChunkDelta();
|
||||
int total = cached;
|
||||
@@ -357,12 +354,12 @@ final class ConsumptionCloudClient {
|
||||
private List<DayRecord> fetchHistory() throws Exception {
|
||||
HttpURLConnection connection = null;
|
||||
try {
|
||||
String url = trimSlash(endpoint) + "/v1/consumption/history/" + installationId;
|
||||
String url = trimSlash(config().url) + "/v1/consumption/history/" + config().installationId;
|
||||
connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
connection.setRequestMethod("GET");
|
||||
connection.setConnectTimeout(TIMEOUT_MS);
|
||||
connection.setReadTimeout(TIMEOUT_MS);
|
||||
connection.setRequestProperty("Authorization", "Bearer " + token);
|
||||
connection.setRequestProperty("Authorization", "Bearer " + config().token);
|
||||
int status = connection.getResponseCode();
|
||||
Log.i(TAG, "History GET response: HTTP " + status);
|
||||
if (status < 200 || status >= 300) {
|
||||
|
||||
Reference in New Issue
Block a user