Rename app and persist all-time consumption to MongoDB
This commit is contained in:
@@ -130,6 +130,7 @@ public class AbrpUploadService extends Service {
|
||||
|
||||
private EnergyTelemetryReader energyReader;
|
||||
private ConsumptionTracker consumptionTracker;
|
||||
private ConsumptionCloudClient consumptionCloudClient;
|
||||
/**
|
||||
* Keeps the unit on the phone hotspot without a trip into Settings. Attempted on the
|
||||
* upload tick because that is exactly when being offline costs something, and it is
|
||||
@@ -196,6 +197,7 @@ public class AbrpUploadService extends Service {
|
||||
// One shared, firmware-aware EVHardware snapshot owns every vehicle signal.
|
||||
energyReader = new EnergyTelemetryReader(getApplicationContext());
|
||||
consumptionTracker = ConsumptionTracker.get(this);
|
||||
consumptionCloudClient = new ConsumptionCloudClient(getApplicationContext());
|
||||
wifi = new WifiAutoConnect(getApplicationContext());
|
||||
// The head unit's own weather service, bound for one reason: ENV_OUTSIDE_TEMPERATURE
|
||||
// is not implemented on this vehicle, so the car cannot say how warm it is outside
|
||||
@@ -324,6 +326,7 @@ public class AbrpUploadService extends Service {
|
||||
long sampleMs = System.currentTimeMillis();
|
||||
EnergySnapshot vehicle = energyReader.read(sampleMs);
|
||||
consumptionTracker.sample(vehicle);
|
||||
consumptionCloudClient.syncIfDue(consumptionTracker.cloudSnapshot(), sampleMs);
|
||||
|
||||
// Keep local consumption history alive while another app owns the ABRP feed.
|
||||
if (!settings.abrpUploadEnabled) {
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Best-effort MongoDB sync through the API in server/. 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 int TIMEOUT_MS = 8_000;
|
||||
private static final String PREFS = "consumption_cloud";
|
||||
private static final String INSTALLATION_ID = "installation_id";
|
||||
|
||||
private final String endpoint;
|
||||
private final String token;
|
||||
private final String installationId;
|
||||
private long lastAttemptMs;
|
||||
|
||||
ConsumptionCloudClient(Context context) {
|
||||
endpoint = BuildConfig.CONSUMPTION_API_URL.trim();
|
||||
token = BuildConfig.CONSUMPTION_API_TOKEN.trim();
|
||||
SharedPreferences prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
|
||||
String existing = prefs.getString(INSTALLATION_ID, "");
|
||||
if (existing == null || existing.isEmpty()) {
|
||||
existing = UUID.randomUUID().toString();
|
||||
prefs.edit().putString(INSTALLATION_ID, existing).commit();
|
||||
}
|
||||
installationId = existing;
|
||||
}
|
||||
|
||||
void syncIfDue(ConsumptionTracker.CloudSnapshot snapshot, long nowMs) {
|
||||
if (endpoint.isEmpty() || token.isEmpty() || nowMs - lastAttemptMs < SYNC_INTERVAL_MS) return;
|
||||
lastAttemptMs = nowMs;
|
||||
HttpURLConnection connection = null;
|
||||
try {
|
||||
JSONObject body = new JSONObject()
|
||||
.put("installationId", installationId)
|
||||
.put("recordedAt", nowMs)
|
||||
.put("lifetime", totals(snapshot.lifetime));
|
||||
if (snapshot.sohPercent != null) body.put("sohPercent", snapshot.sohPercent);
|
||||
byte[] bytes = body.toString().getBytes(StandardCharsets.UTF_8);
|
||||
connection = (HttpURLConnection) new URL(trimSlash(endpoint) + "/v1/consumption").openConnection();
|
||||
connection.setRequestMethod("PUT");
|
||||
connection.setConnectTimeout(TIMEOUT_MS);
|
||||
connection.setReadTimeout(TIMEOUT_MS);
|
||||
connection.setDoOutput(true);
|
||||
connection.setFixedLengthStreamingMode(bytes.length);
|
||||
connection.setRequestProperty("Content-Type", "application/json");
|
||||
connection.setRequestProperty("Authorization", "Bearer " + token);
|
||||
try (OutputStream out = connection.getOutputStream()) { out.write(bytes); }
|
||||
int status = connection.getResponseCode();
|
||||
try (InputStream ignored = status < 400
|
||||
? connection.getInputStream() : connection.getErrorStream()) { }
|
||||
if (status < 200 || status >= 300) Log.w(TAG, "Sync rejected with HTTP " + status);
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Consumption sync unavailable");
|
||||
} finally {
|
||||
if (connection != null) connection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private static JSONObject totals(ConsumptionTracker.Totals totals) throws Exception {
|
||||
return new JSONObject().put("km", totals.km).put("kwh", totals.kwh)
|
||||
.put("hours", totals.hours).put("socDrop", totals.soc);
|
||||
}
|
||||
|
||||
private static String trimSlash(String value) {
|
||||
int end = value.length();
|
||||
while (end > 0 && value.charAt(end - 1) == '/') end--;
|
||||
return value.substring(0, end);
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ final class ConsumptionTracker {
|
||||
*/
|
||||
private static final String PREFS = "consumption_counters_v2";
|
||||
private static final String LEGACY_PREFS = "consumption_counters_v1";
|
||||
private static final String SOH_PERCENT = "manual_soh_percent";
|
||||
|
||||
/** How often the in-memory accumulators reach disk. Matches DriveHub_Dort. */
|
||||
private static final long PERSIST_INTERVAL_MS = 30_000L;
|
||||
@@ -138,6 +139,29 @@ final class ConsumptionTracker {
|
||||
return loadTotals(dayKey(date));
|
||||
}
|
||||
|
||||
synchronized CloudSnapshot cloudSnapshot() {
|
||||
return new CloudSnapshot(lifetime.snapshot(), sohPercent());
|
||||
}
|
||||
|
||||
static final class CloudSnapshot {
|
||||
final Totals lifetime;
|
||||
final Float sohPercent;
|
||||
|
||||
CloudSnapshot(Totals lifetime, Float sohPercent) {
|
||||
this.lifetime = lifetime;
|
||||
this.sohPercent = sohPercent;
|
||||
}
|
||||
}
|
||||
|
||||
synchronized Float sohPercent() {
|
||||
return prefs.contains(SOH_PERCENT) ? prefs.getFloat(SOH_PERCENT, 0f) : null;
|
||||
}
|
||||
|
||||
synchronized boolean setSohPercent(float value) {
|
||||
if (!Float.isFinite(value) || value <= 0f || value > 100f) return false;
|
||||
return prefs.edit().putFloat(SOH_PERCENT, value).commit();
|
||||
}
|
||||
|
||||
/** Flushes the day that just ended and opens the new one. */
|
||||
private void rollDayIfNeeded() {
|
||||
LocalDate date = LocalDate.now();
|
||||
|
||||
@@ -12,6 +12,7 @@ import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.Toast;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
@@ -42,7 +43,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
private static final int LOCATION_PERMISSION_REQUEST = 100;
|
||||
/** Shared with {@link AbrpUploadService}, which reads it on every tick. */
|
||||
static final String WIFI_AUTO_KEY = "wifi_auto_connect";
|
||||
private static final String REPOSITORY_URL = "https://github.com/fatihdonmezdev/MG4ABRP";
|
||||
private static final String REPOSITORY_URL = "https://github.com/fatihdonmezdev/FatihsMG4";
|
||||
|
||||
/** Navigation destinations in page order. Parallel to {@link #panes}. */
|
||||
private static final int[] TAB_IDS =
|
||||
@@ -114,6 +115,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
consumptionSpeed, consumptionTime, consumptionSoc, consumptionDailyHistory;
|
||||
private Button consumptionReset;
|
||||
private MaterialButton consumptionDateButton;
|
||||
private TextInputLayout consumptionSohLayout;
|
||||
private TextInputEditText consumptionSohInput;
|
||||
private java.time.LocalDate selectedConsumptionDate = java.time.LocalDate.now();
|
||||
private TextView chargeStatus, chargeSoc, chargeVoltage, chargeCurrent, chargePower,
|
||||
chargeEnergy, chargeDuration, chargeCurveTable;
|
||||
@@ -861,6 +864,12 @@ public class MainActivity extends AppCompatActivity {
|
||||
consumptionDateButton.setText(selectedConsumptionDate.toString());
|
||||
consumptionDateButton.setOnClickListener(v -> showConsumptionDatePicker());
|
||||
consumptionReset = consumptionPane.findViewById(R.id.consumption_reset);
|
||||
consumptionSohLayout = consumptionPane.findViewById(R.id.consumption_soh_layout);
|
||||
consumptionSohInput = consumptionPane.findViewById(R.id.consumption_soh_input);
|
||||
Float savedSoh = consumptionTracker.sohPercent();
|
||||
if (savedSoh != null) consumptionSohInput.setText(String.format(
|
||||
java.util.Locale.US, "%.1f", savedSoh));
|
||||
consumptionPane.findViewById(R.id.consumption_soh_save).setOnClickListener(v -> saveSoh());
|
||||
bindPeriodButton(R.id.consumption_lifetime, ConsumptionTracker.Period.LIFETIME);
|
||||
bindPeriodButton(R.id.consumption_week, ConsumptionTracker.Period.WEEK);
|
||||
bindPeriodButton(R.id.consumption_month, ConsumptionTracker.Period.MONTH);
|
||||
@@ -880,6 +889,19 @@ public class MainActivity extends AppCompatActivity {
|
||||
});
|
||||
}
|
||||
|
||||
private void saveSoh() {
|
||||
String raw = textOf(consumptionSohInput).replace(',', '.');
|
||||
try {
|
||||
float value = Float.parseFloat(raw);
|
||||
if (!consumptionTracker.setSohPercent(value)) throw new NumberFormatException();
|
||||
consumptionSohLayout.setError(null);
|
||||
consumptionSohInput.setText(String.format(java.util.Locale.US, "%.1f", value));
|
||||
Toast.makeText(this, "SoH kaydedildi", Toast.LENGTH_SHORT).show();
|
||||
} catch (NumberFormatException e) {
|
||||
consumptionSohLayout.setError("1 ile 100 arasında bir değer girin");
|
||||
}
|
||||
}
|
||||
|
||||
private void bindPeriodButton(int id, ConsumptionTracker.Period period) {
|
||||
View button = consumptionPane.findViewById(id);
|
||||
button.setOnClickListener(v -> {
|
||||
|
||||
Reference in New Issue
Block a user