Add persistent consumption and charging dashboards
This commit is contained in:
@@ -129,6 +129,7 @@ public class AbrpUploadService extends Service {
|
||||
private volatile Location lastLocation;
|
||||
|
||||
private EnergyTelemetryReader energyReader;
|
||||
private ConsumptionTracker consumptionTracker;
|
||||
/**
|
||||
* 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
|
||||
@@ -194,13 +195,12 @@ public class AbrpUploadService extends Service {
|
||||
|
||||
// One shared, firmware-aware EVHardware snapshot owns every vehicle signal.
|
||||
energyReader = new EnergyTelemetryReader(getApplicationContext());
|
||||
consumptionTracker = ConsumptionTracker.get(this);
|
||||
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
|
||||
// and something else has to. Idempotent, asynchronous, and absent on a car that does
|
||||
// not have the map stack — in which case ext_temp simply stays unreported.
|
||||
SaicWeather.INSTANCE.connect(getApplicationContext());
|
||||
registerWeatherReceiver();
|
||||
|
||||
requestLocationUpdates();
|
||||
|
||||
@@ -267,6 +267,7 @@ public class AbrpUploadService extends Service {
|
||||
public void onDestroy() {
|
||||
Log.i(TAG, "Service stopping");
|
||||
if (scheduler != null) scheduler.shutdownNow();
|
||||
if (consumptionTracker != null) consumptionTracker.persist();
|
||||
if (locationManager != null) {
|
||||
try { locationManager.removeUpdates(locationListener); } catch (Exception ignored) {}
|
||||
}
|
||||
@@ -318,6 +319,10 @@ public class AbrpUploadService extends Service {
|
||||
}
|
||||
|
||||
private void doUpload() {
|
||||
long sampleMs = System.currentTimeMillis();
|
||||
EnergySnapshot vehicle = energyReader.read(sampleMs);
|
||||
consumptionTracker.sample(vehicle);
|
||||
|
||||
String token = securePrefs.getString(SecurePrefs.KEY_TOKEN, "").trim();
|
||||
String apiKey = securePrefs.getString(SecurePrefs.KEY_API_KEY, "").trim();
|
||||
|
||||
@@ -332,15 +337,12 @@ public class AbrpUploadService extends Service {
|
||||
|
||||
// Read whatever the car will give us. Every getter returns null when the read
|
||||
// fails, and a null field is omitted from the payload — never sent as 0.
|
||||
long sampleMs = System.currentTimeMillis();
|
||||
EnergySnapshot vehicle = energyReader.read(sampleMs);
|
||||
boolean carUp = vehicle.getHasVehicleData();
|
||||
Float speedKmh = vehicle.getSpeedKmh();
|
||||
Integer soc = vehicle.getSocPercent() == null
|
||||
? null : Math.round(vehicle.getSocPercent());
|
||||
Integer rangeKm = vehicle.getRangeKm() == null
|
||||
? null : Math.round(vehicle.getRangeKm());
|
||||
Float extTemp = vehicle.getOutsideTempCelsius();
|
||||
Float powerKw = vehicle.getBatteryPowerKw();
|
||||
Float chargeRateKw = powerKw == null ? null : -powerKw;
|
||||
Boolean portConnected = vehicle.getChargePortConnected();
|
||||
@@ -393,40 +395,10 @@ public class AbrpUploadService extends Service {
|
||||
// ENV_OUTSIDE_TEMPERATURE, so there is no vehicle reading to correct. The head unit's
|
||||
// weather service answers for a position, and ambient air at the car is what ext_temp
|
||||
// means — a better figure than a bumper sensor in the sun would give anyway.
|
||||
String tempSource = "car";
|
||||
SaicWeather.Reading sky = null;
|
||||
if (!TelemetryPayload.isPlausibleTemp(extTemp)) {
|
||||
// The broadcast first. It is what fills the head unit's own status bar, so on this
|
||||
// car it is the source known to be producing a number, where the map service's
|
||||
// query never even bound.
|
||||
// The broadcast arrives when the weather app feels like it, so a reading that
|
||||
// never came is retried here rather than only at start-up.
|
||||
if (headUnitWeather.temperatureC() == null) probeStickyWeather();
|
||||
Float broadcast = weatherIsFresh() ? headUnitWeather.temperatureC() : null;
|
||||
if (TelemetryPayload.isPlausibleTemp(broadcast)) {
|
||||
extTemp = broadcast;
|
||||
tempSource = "bcast:" + headUnitWeather.diagnostic();
|
||||
} else {
|
||||
sky = weatherAt(loc);
|
||||
if (sky != null && sky.getTemperatureCelsius() != null) {
|
||||
extTemp = sky.getTemperatureCelsius().floatValue();
|
||||
tempSource = "map:" + sky.getCity();
|
||||
} else {
|
||||
// Neither answered. Which one failed, and how, is the difference between
|
||||
// "the broadcast never arrives" and "it arrives in a shape this cannot
|
||||
// read" — and nothing on the ABRP side tells them apart.
|
||||
tempSource = "none bcast=" + headUnitWeather.diagnostic()
|
||||
+ " map=" + (SaicWeather.INSTANCE.isAvailable() ? "bound" : "unbound")
|
||||
+ (lastQueryMs < 0 ? "" : "/" + lastQueryMs + "ms");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TelemetryPayload tlm = new TelemetryPayload(System.currentTimeMillis() / 1000);
|
||||
tlm.soc = soc;
|
||||
tlm.speedKmh = speedKmh;
|
||||
tlm.rangeKm = rangeKm;
|
||||
tlm.extTemp = extTemp;
|
||||
tlm.powerKw = powerKw;
|
||||
tlm.charging = charging;
|
||||
tlm.dcfc = dcfc;
|
||||
@@ -450,7 +422,7 @@ public class AbrpUploadService extends Service {
|
||||
}
|
||||
|
||||
String tlmJson = tlm.build();
|
||||
String summary = TelemetryPayload.summarize(tlmJson) + " [temp " + tempSource + "]";
|
||||
String summary = TelemetryPayload.summarize(tlmJson);
|
||||
if (loc != null) {
|
||||
summary += " (fix " + locationAgeMs(loc) / 1000 + "s old"
|
||||
+ " +/-" + (loc.hasAccuracy() ? Math.round(loc.getAccuracy()) + "m" : "?")
|
||||
@@ -458,7 +430,6 @@ public class AbrpUploadService extends Service {
|
||||
// The weather service names the place it answered for. ABRP shows an address it
|
||||
// reverse-geocodes from the same coordinates, so the two disagreeing says the
|
||||
// coordinates are wrong, and the two agreeing says ABRP is showing something old.
|
||||
if (sky != null && !sky.getCity().isEmpty()) summary += ", near " + sky.getCity();
|
||||
summary += ")";
|
||||
}
|
||||
// What actually went on the wire. Kept on the log entry as well as in logcat: the
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Lightweight SOC/power charging curve, avoiding a heavy chart dependency on AAOS 9. */
|
||||
public final class ChargingGraphView extends View {
|
||||
private final Paint grid = new Paint(1), line = new Paint(1), text = new Paint(1);
|
||||
private final List<float[]> points = new ArrayList<>();
|
||||
public ChargingGraphView(Context c, AttributeSet a) { super(c,a); grid.setColor(0x3348C7D9); line.setColor(0xFFFFC857); line.setStrokeWidth(4); text.setColor(0xFFBDC4C6); text.setTextSize(24); }
|
||||
synchronized void add(Float soc, Float power) {
|
||||
if (soc == null || power == null || power >= 0) return;
|
||||
float p = Math.abs(power);
|
||||
if (!points.isEmpty() && Math.abs(points.get(points.size()-1)[0] - soc) < .1f) points.remove(points.size()-1);
|
||||
points.add(new float[]{soc,p}); if (points.size() > 500) points.remove(0); invalidate();
|
||||
}
|
||||
@Override protected synchronized void onDraw(Canvas c) {
|
||||
super.onDraw(c); float w=getWidth(), h=getHeight()-34;
|
||||
for(int i=0;i<=4;i++) c.drawLine(0,h*i/4,w,h*i/4,grid);
|
||||
c.drawText("SOC % → Şarj gücü (kW)",12,getHeight()-5,text);
|
||||
float max=20; for(float[] p:points) max=Math.max(max,p[1]);
|
||||
for(int i=1;i<points.size();i++){float[] a=points.get(i-1),b=points.get(i); c.drawLine(a[0]*w/100,h-a[1]*h/max,b[0]*w/100,h-b[1]*h/max,line);}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import com.evsuite.hardware.telemetry.EnergySnapshot;
|
||||
|
||||
/** Persistent, read-only trip/lifetime integration patterned after DriveHub_Dort. */
|
||||
final class ConsumptionTracker {
|
||||
enum Period { START, MOTOR, LIFETIME, TRIP_A, TRIP_B }
|
||||
|
||||
static final class Totals {
|
||||
final double km, kwh, hours, soc;
|
||||
Totals(double km, double kwh, double hours, double soc) {
|
||||
this.km = km; this.kwh = kwh; this.hours = hours; this.soc = soc;
|
||||
}
|
||||
}
|
||||
|
||||
private static final String PREFS = "consumption_counters_v1";
|
||||
private static ConsumptionTracker instance;
|
||||
static synchronized ConsumptionTracker get(Context context) {
|
||||
if (instance == null) instance = new ConsumptionTracker(context.getApplicationContext());
|
||||
return instance;
|
||||
}
|
||||
|
||||
private final SharedPreferences prefs;
|
||||
private Totals start = zero(), motor = zero(), lifetime, tripA, tripB;
|
||||
private long lastMs;
|
||||
private Float lastSpeed, lastPower, lastSoc;
|
||||
private Boolean lastParked;
|
||||
|
||||
private ConsumptionTracker(Context context) {
|
||||
prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
|
||||
lifetime = load("life"); tripA = load("a"); tripB = load("b");
|
||||
}
|
||||
|
||||
synchronized void sample(EnergySnapshot s) {
|
||||
long now = s.getTimestampMs();
|
||||
Boolean parked = s.getParked();
|
||||
if (Boolean.FALSE.equals(parked) && !Boolean.FALSE.equals(lastParked)) motor = zero();
|
||||
if (lastMs > 0 && now > lastMs) {
|
||||
double hours = (now - lastMs) / 3_600_000d;
|
||||
// Never bridge a process pause/sleep into invented driving data.
|
||||
if (hours <= 0.1d) {
|
||||
Float speed = s.getSpeedKmh();
|
||||
Float power = s.getBatteryPowerKw();
|
||||
double km = speed == null ? 0d : Math.max(0d,
|
||||
((lastSpeed == null ? speed : lastSpeed) + speed) * 0.5d) * hours;
|
||||
double kwh = power == null ? 0d :
|
||||
((lastPower == null ? power : lastPower) + power) * 0.5d * hours;
|
||||
double dsoc = (lastSoc == null || s.getSocPercent() == null ||
|
||||
Boolean.TRUE.equals(s.getChargePortConnected())) ? 0d : lastSoc - s.getSocPercent();
|
||||
start = add(start, km, kwh, hours, dsoc);
|
||||
lifetime = add(lifetime, km, kwh, hours, dsoc);
|
||||
tripA = add(tripA, km, kwh, hours, dsoc);
|
||||
tripB = add(tripB, km, kwh, hours, dsoc);
|
||||
if (Boolean.FALSE.equals(parked)) motor = add(motor, km, kwh, hours, dsoc);
|
||||
persist();
|
||||
}
|
||||
}
|
||||
lastMs = now; lastSpeed = s.getSpeedKmh(); lastPower = s.getBatteryPowerKw();
|
||||
lastSoc = s.getSocPercent(); lastParked = parked;
|
||||
}
|
||||
|
||||
synchronized Totals totals(Period period) {
|
||||
switch (period) {
|
||||
case START: return start; case MOTOR: return motor; case TRIP_A: return tripA;
|
||||
case TRIP_B: return tripB; default: return lifetime;
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void reset(Period period) {
|
||||
if (period == Period.TRIP_A) tripA = zero();
|
||||
else if (period == Period.TRIP_B) tripB = zero();
|
||||
else return;
|
||||
persist();
|
||||
}
|
||||
|
||||
synchronized void persist() {
|
||||
SharedPreferences.Editor e = prefs.edit();
|
||||
save(e, "life", lifetime); save(e, "a", tripA); save(e, "b", tripB);
|
||||
e.commit();
|
||||
}
|
||||
|
||||
private Totals load(String key) { return new Totals(bits(key,"km"), bits(key,"kwh"), bits(key,"h"), bits(key,"soc")); }
|
||||
private double bits(String key, String field) { return Double.longBitsToDouble(prefs.getLong(key + "_" + field, 0L)); }
|
||||
private static void save(SharedPreferences.Editor e, String key, Totals t) {
|
||||
e.putLong(key+"_km", Double.doubleToRawLongBits(t.km));
|
||||
e.putLong(key+"_kwh", Double.doubleToRawLongBits(t.kwh));
|
||||
e.putLong(key+"_h", Double.doubleToRawLongBits(t.hours));
|
||||
e.putLong(key+"_soc", Double.doubleToRawLongBits(t.soc));
|
||||
}
|
||||
private static Totals add(Totals t, double km, double kwh, double h, double soc) {
|
||||
return new Totals(t.km + km, t.kwh + kwh, t.hours + h, t.soc + soc);
|
||||
}
|
||||
private static Totals zero() { return new Totals(0,0,0,0); }
|
||||
}
|
||||
@@ -46,15 +46,15 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
/** Navigation destinations in page order. Parallel to {@link #panes}. */
|
||||
private static final int[] TAB_IDS =
|
||||
{ R.id.tabVehicle, R.id.tabAbrp, R.id.tabService, R.id.tabWifi,
|
||||
{ R.id.tabVehicle, R.id.tabConsumption, R.id.tabCharging, R.id.tabAbrp, R.id.tabService, R.id.tabWifi,
|
||||
R.id.tabLog };
|
||||
|
||||
private static final int[] PAGE_TITLES = {
|
||||
R.string.design_overview_title, R.string.design_abrp_title,
|
||||
R.string.design_overview_title, R.string.consumption_title, R.string.charging_title, R.string.design_abrp_title,
|
||||
R.string.design_service_title, R.string.design_wifi_title, R.string.design_log_title
|
||||
};
|
||||
private static final int[] PAGE_SUBTITLES = {
|
||||
R.string.design_overview_subtitle, R.string.design_abrp_subtitle,
|
||||
R.string.design_overview_subtitle, R.string.consumption_subtitle, R.string.charging_subtitle, R.string.design_abrp_subtitle,
|
||||
R.string.design_service_subtitle, R.string.design_wifi_subtitle, R.string.design_log_subtitle
|
||||
};
|
||||
|
||||
@@ -85,6 +85,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
private View servicePane;
|
||||
private View logPane;
|
||||
private View vehiclePane;
|
||||
private View consumptionPane;
|
||||
private View chargingPane;
|
||||
private View wifiPane;
|
||||
|
||||
/** The five pages in navigation order. Parallel to {@link #TAB_IDS}. */
|
||||
@@ -103,8 +105,18 @@ public class MainActivity extends AppCompatActivity {
|
||||
*/
|
||||
private EnergyTelemetryReader vehicleReader;
|
||||
private TextView vehSoc, vehRange, vehPower, vehSpeed, vehOdometer,
|
||||
vehCharging, vehExtTemp, vehHvac, vehSoe,
|
||||
vehCharging, vehHvac, vehSoe,
|
||||
vehTireFl, vehTireFr, vehTireRl, vehTireRr;
|
||||
private ConsumptionTracker consumptionTracker;
|
||||
private Spinner consumptionPeriod;
|
||||
private TextView consumptionDistance, consumptionEnergy, consumptionAverage,
|
||||
consumptionSpeed, consumptionTime, consumptionSoc;
|
||||
private Button consumptionReset;
|
||||
private TextView chargeStatus, chargeSoc, chargeVoltage, chargeCurrent, chargePower,
|
||||
chargeEnergy, chargeDuration;
|
||||
private ChargingGraphView chargeGraph;
|
||||
private long chargeStartMs, lastChargeMs;
|
||||
private double chargedKwh;
|
||||
|
||||
/**
|
||||
* Inflates a page with no parent, then gives it the MATCH_PARENT/MATCH_PARENT layout
|
||||
@@ -181,6 +193,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
refreshStatus();
|
||||
refreshCallLog();
|
||||
refreshVehicle();
|
||||
refreshConsumption();
|
||||
refreshWifi();
|
||||
uiHandler.postDelayed(this, 2_000L);
|
||||
}
|
||||
@@ -219,11 +232,15 @@ public class MainActivity extends AppCompatActivity {
|
||||
abrpPane = inflatePane(R.layout.pane_abrp);
|
||||
servicePane = inflatePane(R.layout.pane_service);
|
||||
vehiclePane = inflatePane(R.layout.pane_vehicle);
|
||||
consumptionPane = inflatePane(R.layout.pane_consumption);
|
||||
chargingPane = inflatePane(R.layout.pane_charging);
|
||||
wifiPane = inflatePane(R.layout.pane_wifi);
|
||||
logPane = inflatePane(R.layout.pane_log);
|
||||
panes = new View[] { vehiclePane, abrpPane, servicePane, wifiPane, logPane };
|
||||
panes = new View[] { vehiclePane, consumptionPane, chargingPane, abrpPane, servicePane, wifiPane, logPane };
|
||||
|
||||
bindVehiclePane();
|
||||
bindConsumptionPane();
|
||||
bindChargingPane();
|
||||
bindWifiPane();
|
||||
|
||||
apiKeyLayout = abrpPane.findViewById(R.id.api_key_layout);
|
||||
@@ -761,7 +778,6 @@ public class MainActivity extends AppCompatActivity {
|
||||
vehSpeed = vehiclePane.findViewById(R.id.vehicle_speed);
|
||||
vehOdometer = vehiclePane.findViewById(R.id.vehicle_odometer);
|
||||
vehCharging = vehiclePane.findViewById(R.id.vehicle_charging);
|
||||
vehExtTemp = vehiclePane.findViewById(R.id.vehicle_ext_temp);
|
||||
vehHvac = vehiclePane.findViewById(R.id.vehicle_hvac);
|
||||
vehSoe = vehiclePane.findViewById(R.id.vehicle_soe);
|
||||
vehTireFl = vehiclePane.findViewById(R.id.vehicle_tire_fl);
|
||||
@@ -784,13 +800,14 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
// The timestamp is passed explicitly: its Kotlin default is not visible from Java.
|
||||
EnergySnapshot s = vehicleReader.read(System.currentTimeMillis());
|
||||
ConsumptionTracker.get(this).sample(s);
|
||||
refreshCharging(s);
|
||||
|
||||
vehSoc.setText(fmt(s.getSocPercent(), "%.0f %%"));
|
||||
vehRange.setText(fmt(s.getRangeKm(), "%.0f km"));
|
||||
vehPower.setText(fmt(s.getBatteryPowerKw(), "%.1f kW"));
|
||||
vehSpeed.setText(fmt(s.getSpeedKmh(), "%.0f km/h"));
|
||||
vehOdometer.setText(fmt(s.getOdometerKm(), "%.0f km"));
|
||||
vehExtTemp.setText(fmt(s.getOutsideTempCelsius(), "%.0f °C"));
|
||||
vehHvac.setText(fmt(s.getClimate().getDriverTargetCelsius(), "%.0f °C"));
|
||||
vehSoe.setText(fmt(s.getBatteryEnergyKwh(), "%.1f kWh"));
|
||||
|
||||
@@ -805,6 +822,94 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private void bindConsumptionPane() {
|
||||
consumptionTracker = ConsumptionTracker.get(this);
|
||||
consumptionPeriod = consumptionPane.findViewById(R.id.consumption_period);
|
||||
String[] periods = {"Başlangıçtan itibaren", "Motor çalıştıktan itibaren",
|
||||
"Tüm zamanlar", "Trip A", "Trip B"};
|
||||
ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
|
||||
android.R.layout.simple_spinner_dropdown_item, periods);
|
||||
consumptionPeriod.setAdapter(adapter);
|
||||
consumptionDistance = consumptionPane.findViewById(R.id.consumption_distance);
|
||||
consumptionEnergy = consumptionPane.findViewById(R.id.consumption_energy);
|
||||
consumptionAverage = consumptionPane.findViewById(R.id.consumption_average);
|
||||
consumptionSpeed = consumptionPane.findViewById(R.id.consumption_speed);
|
||||
consumptionTime = consumptionPane.findViewById(R.id.consumption_time);
|
||||
consumptionSoc = consumptionPane.findViewById(R.id.consumption_soc);
|
||||
consumptionReset = consumptionPane.findViewById(R.id.consumption_reset);
|
||||
consumptionPeriod.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override public void onItemSelected(AdapterView<?> p, View v, int position, long id) {
|
||||
consumptionReset.setVisibility(position >= 3 ? View.VISIBLE : View.GONE);
|
||||
refreshConsumption();
|
||||
}
|
||||
@Override public void onNothingSelected(AdapterView<?> p) {}
|
||||
});
|
||||
consumptionReset.setOnClickListener(v -> {
|
||||
int selected = consumptionPeriod.getSelectedItemPosition();
|
||||
if (selected < 3) return;
|
||||
String name = selected == 3 ? "Trip A" : "Trip B";
|
||||
new MaterialAlertDialogBuilder(this).setTitle(name + " sıfırlansın mı?")
|
||||
.setMessage("Bu sayacın kayıtlı tüketim geçmişi kalıcı olarak silinir.")
|
||||
.setNegativeButton("Vazgeç", null)
|
||||
.setPositiveButton("Sıfırla", (d, w) -> {
|
||||
consumptionTracker.reset(selected == 3
|
||||
? ConsumptionTracker.Period.TRIP_A : ConsumptionTracker.Period.TRIP_B);
|
||||
refreshConsumption();
|
||||
}).show();
|
||||
});
|
||||
}
|
||||
|
||||
private void bindChargingPane() {
|
||||
chargeStatus = chargingPane.findViewById(R.id.charge_status);
|
||||
chargeSoc = chargingPane.findViewById(R.id.charge_soc);
|
||||
chargeVoltage = chargingPane.findViewById(R.id.charge_voltage);
|
||||
chargeCurrent = chargingPane.findViewById(R.id.charge_current);
|
||||
chargePower = chargingPane.findViewById(R.id.charge_power);
|
||||
chargeEnergy = chargingPane.findViewById(R.id.charge_energy);
|
||||
chargeDuration = chargingPane.findViewById(R.id.charge_duration);
|
||||
chargeGraph = chargingPane.findViewById(R.id.charge_graph);
|
||||
}
|
||||
|
||||
private void refreshCharging(EnergySnapshot s) {
|
||||
if (chargeStatus == null) return;
|
||||
Integer code = s.getChargingStatus();
|
||||
boolean charging = (code != null && (code == 1 || code == 10)) ||
|
||||
(Boolean.TRUE.equals(s.getChargePortConnected()) && s.getBatteryPowerKw() != null
|
||||
&& s.getBatteryPowerKw() < -0.2f);
|
||||
long now = s.getTimestampMs();
|
||||
if (charging && chargeStartMs == 0) { chargeStartMs = now; chargedKwh = 0; }
|
||||
if (!charging) { chargeStartMs = 0; lastChargeMs = 0; chargedKwh = 0; }
|
||||
if (charging && lastChargeMs > 0 && s.getBatteryPowerKw() != null)
|
||||
chargedKwh += Math.max(0, -s.getBatteryPowerKw()) * (now-lastChargeMs) / 3_600_000d;
|
||||
if (charging) lastChargeMs = now;
|
||||
chargeStatus.setText(charging ? (code != null && code == 10 ? "DC hızlı şarj" : "AC şarj") : "Şarj yok");
|
||||
chargeSoc.setText(fmt(s.getSocPercent(), "%.1f %%"));
|
||||
chargeVoltage.setText(fmt(s.getBatteryVoltageV(), "%.1f V"));
|
||||
chargeCurrent.setText(fmt(s.getBatteryCurrentA(), "%.1f A"));
|
||||
chargePower.setText(s.getBatteryPowerKw() == null ? "—" : String.format(java.util.Locale.getDefault(), "%.1f kW", Math.abs(s.getBatteryPowerKw())));
|
||||
chargeEnergy.setText(charging ? String.format(java.util.Locale.getDefault(), "%.2f kWh", chargedKwh) : "—");
|
||||
long sec = chargeStartMs == 0 ? 0 : (now-chargeStartMs)/1000;
|
||||
chargeDuration.setText(charging ? String.format(java.util.Locale.getDefault(), "%02d:%02d:%02d", sec/3600,(sec%3600)/60,sec%60) : "—");
|
||||
if (charging) chargeGraph.add(s.getSocPercent(), s.getBatteryPowerKw());
|
||||
}
|
||||
|
||||
private void refreshConsumption() {
|
||||
if (consumptionTracker == null || consumptionPeriod == null) return;
|
||||
ConsumptionTracker.Period period = ConsumptionTracker.Period.values()[
|
||||
Math.max(0, consumptionPeriod.getSelectedItemPosition())];
|
||||
ConsumptionTracker.Totals t = consumptionTracker.totals(period);
|
||||
consumptionDistance.setText(String.format(java.util.Locale.getDefault(), "%.1f km", t.km));
|
||||
consumptionEnergy.setText(String.format(java.util.Locale.getDefault(), "%.2f kWh", t.kwh));
|
||||
consumptionAverage.setText(t.km < 0.1 ? "—" : String.format(java.util.Locale.getDefault(),
|
||||
"%.1f kWh/100 km", t.kwh * 100d / t.km));
|
||||
consumptionSpeed.setText(t.hours <= 0 ? "—" : String.format(java.util.Locale.getDefault(),
|
||||
"%.1f km/h", t.km / t.hours));
|
||||
long minutes = Math.round(t.hours * 60d);
|
||||
consumptionTime.setText(String.format(java.util.Locale.getDefault(), "%d sa %02d dk",
|
||||
minutes / 60, minutes % 60));
|
||||
consumptionSoc.setText(String.format(java.util.Locale.getDefault(), "%.1f %%", t.soc));
|
||||
}
|
||||
|
||||
/**
|
||||
* The charging row: the session state when the car reports one, the port otherwise.
|
||||
*
|
||||
|
||||
@@ -35,7 +35,6 @@ final class TelemetryPayload {
|
||||
|
||||
// --- Lower priority ---
|
||||
Integer rangeKm; // km, vehicle's own estimate
|
||||
Float extTemp; // °C, outside
|
||||
Float cabinTemp; // °C
|
||||
Float battTempC; // °C, traction battery
|
||||
Float capacityKwh; // kWh, usable pack capacity
|
||||
@@ -63,13 +62,6 @@ final class TelemetryPayload {
|
||||
putIfPresent(tlm, "soc", soc);
|
||||
putIfPresent(tlm, "speed", speedKmh == null ? null : Math.round(speedKmh));
|
||||
putIfPresent(tlm, "est_battery_range", rangeKm);
|
||||
// Outside temp gets the same plausibility guard as the other temperatures:
|
||||
// a VHAL that does not implement ENV_OUTSIDE_TEMPERATURE answers 0.0, and
|
||||
// ABRP treats "0 °C outside" as a real reading that costs range in its plan.
|
||||
// A genuine 0 °C is lost with it, which is the cheaper of the two errors.
|
||||
if (isPlausibleTemp(extTemp)) {
|
||||
tlm.put("ext_temp", Math.round(extTemp));
|
||||
}
|
||||
if (powerKw != null) tlm.put("power", round2(powerKw));
|
||||
putIfPresent(tlm, "is_charging", boolToInt(charging));
|
||||
putIfPresent(tlm, "is_dcfc", boolToInt(dcfc));
|
||||
@@ -169,7 +161,7 @@ final class TelemetryPayload {
|
||||
* Used only to work out what was left out — see {@link #summarize(String)}.
|
||||
*/
|
||||
private static final String[] ALL_FIELDS = {
|
||||
"utc", "soc", "speed", "power", "est_battery_range", "ext_temp", "cabin_temp",
|
||||
"utc", "soc", "speed", "power", "est_battery_range", "cabin_temp",
|
||||
"batt_temp", "hvac_setpoint", "capacity", "soe", "kwh_charged", "odometer",
|
||||
"is_charging", "is_dcfc", "is_parked", "lat", "lon", "elevation", "heading",
|
||||
"tire_pressure_fl", "tire_pressure_fr", "tire_pressure_rl", "tire_pressure_rr",
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_brand" android:textAppearance="@style/Text.EV.Hero" android:textColor="@color/ev_accent" android:paddingStart="14dp" android:paddingTop="8dp" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_brand_caption" android:textAppearance="@style/Text.EV.Secondary" android:paddingStart="14dp" android:layout_marginBottom="24dp" />
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabVehicle" style="@style/Widget.EV.NavTab" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginBottom="6dp" android:text="@string/tab_vehicle" app:icon="@drawable/ic_tab_vehicle" />
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabConsumption" style="@style/Widget.EV.NavTab" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginBottom="6dp" android:text="@string/tab_consumption" app:icon="@drawable/ic_tab_vehicle" />
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabCharging" style="@style/Widget.EV.NavTab" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginBottom="6dp" android:text="@string/tab_charging" app:icon="@drawable/ic_tab_service" />
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabAbrp" style="@style/Widget.EV.NavTab" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginBottom="6dp" android:text="@string/tab_abrp" app:icon="@drawable/ic_tab_abrp" />
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabService" style="@style/Widget.EV.NavTab" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginBottom="6dp" android:text="@string/tab_service" app:icon="@drawable/ic_tab_service" />
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabWifi" style="@style/Widget.EV.NavTab" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginBottom="6dp" android:text="@string/tab_wifi" app:icon="@drawable/ic_tab_wifi" />
|
||||
|
||||
@@ -9,10 +9,6 @@
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_odometer" />
|
||||
<TextView android:id="@+id/vehicle_odometer" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
<TableRow style="@style/DataRow" android:minHeight="40dp" android:paddingTop="8dp" android:paddingBottom="8dp">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_ext_temp" />
|
||||
<TextView android:id="@+id/vehicle_ext_temp" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
<TableRow style="@style/DataRow" android:minHeight="40dp" android:paddingTop="8dp" android:paddingBottom="8dp">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_hvac" />
|
||||
<TextView android:id="@+id/vehicle_hvac" style="@style/DataValue" />
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
<androidx.viewpager2.widget.ViewPager2 android:id="@+id/content" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"/>
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:padding="8dp" android:background="@drawable/bg_sidebar">
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabVehicle" style="@style/Widget.EV.NavTab.Compact" android:layout_width="0dp" android:layout_height="76dp" android:layout_weight="1" android:text="@string/tab_vehicle" app:icon="@drawable/ic_tab_vehicle"/>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabConsumption" style="@style/Widget.EV.NavTab.Compact" android:layout_width="0dp" android:layout_height="76dp" android:layout_weight="1" android:text="@string/tab_consumption" app:icon="@drawable/ic_tab_vehicle"/>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabCharging" style="@style/Widget.EV.NavTab.Compact" android:layout_width="0dp" android:layout_height="76dp" android:layout_weight="1" android:text="@string/tab_charging" app:icon="@drawable/ic_tab_service"/>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabAbrp" style="@style/Widget.EV.NavTab.Compact" android:layout_width="0dp" android:layout_height="76dp" android:layout_weight="1" android:text="@string/tab_abrp" app:icon="@drawable/ic_tab_abrp"/>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabService" style="@style/Widget.EV.NavTab.Compact" android:layout_width="0dp" android:layout_height="76dp" android:layout_weight="1" android:text="@string/tab_service" app:icon="@drawable/ic_tab_service"/>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabWifi" style="@style/Widget.EV.NavTab.Compact" android:layout_width="0dp" android:layout_height="76dp" android:layout_weight="1" android:text="@string/tab_wifi" app:icon="@drawable/ic_tab_wifi"/>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true">
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp">
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="20dp" android:background="@drawable/bg_glass">
|
||||
<TextView android:id="@+id/charge_status" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Şarj yok" android:textAppearance="@style/Text.EV.Title" android:textColor="@color/ev_cyan"/>
|
||||
<GridLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:columnCount="2" android:layout_marginTop="12dp">
|
||||
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="SOC" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/charge_soc" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="—" android:textAppearance="@style/DataValue"/>
|
||||
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="Batarya voltajı" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/charge_voltage" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="—" android:textAppearance="@style/DataValue"/>
|
||||
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="Batarya akımı" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/charge_current" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="—" android:textAppearance="@style/DataValue"/>
|
||||
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="Şarj gücü" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/charge_power" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="—" android:textAppearance="@style/DataValue"/>
|
||||
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="Alınan enerji" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/charge_energy" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="—" android:textAppearance="@style/DataValue"/>
|
||||
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="Şarj süresi" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/charge_duration" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="—" android:textAppearance="@style/DataValue"/>
|
||||
</GridLayout>
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="20dp" android:layout_marginTop="16dp" android:background="@drawable/bg_glass">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="ŞARJ EĞRİSİ" android:textAppearance="@style/Text.EV.Eyebrow"/>
|
||||
<com.evsuite.abrp.ChargingGraphView android:id="@+id/charge_graph" android:layout_width="match_parent" android:layout_height="280dp" android:layout_marginTop="12dp"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true">
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp">
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="20dp" android:background="@drawable/bg_glass">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="TÜKETİM DÖNEMİ" android:textAppearance="@style/Text.EV.Eyebrow"/>
|
||||
<Spinner android:id="@+id/consumption_period" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginTop="8dp"/>
|
||||
<GridLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:columnCount="2">
|
||||
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="12dp" android:text="Mesafe" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/consumption_distance" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="12dp" android:text="—" android:textAppearance="@style/DataValue"/>
|
||||
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="12dp" android:text="Net enerji" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/consumption_energy" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="12dp" android:text="—" android:textAppearance="@style/DataValue"/>
|
||||
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="12dp" android:text="Ortalama tüketim" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/consumption_average" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="12dp" android:text="—" android:textAppearance="@style/DataValue"/>
|
||||
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="12dp" android:text="Ortalama hız" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/consumption_speed" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="12dp" android:text="—" android:textAppearance="@style/DataValue"/>
|
||||
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="12dp" android:text="Süre" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/consumption_time" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="12dp" android:text="—" android:textAppearance="@style/DataValue"/>
|
||||
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="12dp" android:text="SOC farkı" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/consumption_soc" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="12dp" android:text="—" android:textAppearance="@style/DataValue"/>
|
||||
</GridLayout>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/consumption_reset" style="@style/Widget.EV.Button.Outlined" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginTop="12dp" android:text="Seçili tripi sıfırla" android:visibility="gone"/>
|
||||
</LinearLayout>
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:text="Tüm zamanlar, Trip A ve Trip B cihazda kalıcı saklanır; uygulama ve OTA güncellemeleriyle silinmez." android:textAppearance="@style/Text.EV.Secondary"/>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
@@ -8,10 +8,6 @@
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_odometer" />
|
||||
<TextView android:id="@+id/vehicle_odometer" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
<TableRow style="@style/DataRow" android:minHeight="40dp" android:paddingTop="8dp" android:paddingBottom="8dp">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_ext_temp" />
|
||||
<TextView android:id="@+id/vehicle_ext_temp" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
<TableRow style="@style/DataRow" android:minHeight="40dp" android:paddingTop="8dp" android:paddingBottom="8dp">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_hvac" />
|
||||
<TextView android:id="@+id/vehicle_hvac" style="@style/DataValue" />
|
||||
|
||||
@@ -5,6 +5,12 @@
|
||||
<string name="tab_abrp">ABRP</string>
|
||||
<string name="tab_service">Aktarım</string>
|
||||
<string name="tab_vehicle">Genel</string>
|
||||
<string name="tab_consumption">Tüketim</string>
|
||||
<string name="tab_charging">Şarj</string>
|
||||
<string name="consumption_title">Tüketim</string>
|
||||
<string name="consumption_subtitle">Başlangıç, motor, tüm zamanlar ve trip sayaçları</string>
|
||||
<string name="charging_title">Şarj</string>
|
||||
<string name="charging_subtitle">Canlı batarya değerleri ve SOC/güç eğrisi</string>
|
||||
<string name="tab_wifi">Wi-Fi</string>
|
||||
<string name="tab_log">Kayıtlar</string>
|
||||
|
||||
@@ -28,7 +34,6 @@
|
||||
<string name="vehicle_row_speed">Hız</string>
|
||||
<string name="vehicle_row_odometer">Kilometre</string>
|
||||
<string name="vehicle_row_batt_temp">Batarya Sıcaklığı</string>
|
||||
<string name="vehicle_row_ext_temp">Dış Sıcaklık</string>
|
||||
<string name="vehicle_row_cabin_temp">Kabin Sıcaklığı</string>
|
||||
<string name="vehicle_row_hvac">Klima Ayarı</string>
|
||||
<string name="vehicle_row_capacity">Batarya Kapasitesi</string>
|
||||
|
||||
Reference in New Issue
Block a user