Add persistent consumption and charging dashboards

This commit is contained in:
TCFDONMEZ
2026-09-30 11:17:54 +03:00
parent 69ef0eeef6
commit 25abf0da5b
18 changed files with 323 additions and 84 deletions
@@ -584,6 +584,21 @@ object EVHardware {
return volts * amps / 1_000f return volts * amps / 1_000f
} }
/** Raw pack voltage/current used by the OEM charging screen; read-only, SWI68/69. */
fun getVendorBatteryVoltageV(): Float? {
val gen = FirmwareInfo.getGeneration()
if (gen != FirmwareInfo.Gen.SWI68 && gen != FirmwareInfo.Gen.SWI69) return null
return getFloatPropertyCPM(PROP_BMS_PACK_VOLT_SWI68, AREA_GLOBAL)
?.takeIf { it.isFinite() && it in PACK_VOLTAGE_RANGE }
}
fun getVendorBatteryCurrentA(): Float? {
val gen = FirmwareInfo.getGeneration()
if (gen != FirmwareInfo.Gen.SWI68 && gen != FirmwareInfo.Gen.SWI69) return null
return getFloatPropertyCPM(PROP_BMS_PACK_CURRENT_SWI68, AREA_GLOBAL)
?.takeIf { it.isFinite() && kotlin.math.abs(it) <= MAX_PACK_CURRENT_A }
}
/** Vendor range fallback; SWI68 and SWI69 both publish it on the same id. */ /** Vendor range fallback; SWI68 and SWI69 both publish it on the same id. */
fun getVendorRangeKm(): Int? = fun getVendorRangeKm(): Int? =
if (FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI68 || if (FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI68 ||
@@ -31,6 +31,8 @@ data class EnergySnapshot(
val parked: Boolean?, val parked: Boolean?,
val climate: ClimateSnapshot, val climate: ClimateSnapshot,
val tirePressures: TirePressureSnapshot, val tirePressures: TirePressureSnapshot,
val batteryVoltageV: Float? = null,
val batteryCurrentA: Float? = null,
) { ) {
val hasVehicleData: Boolean val hasVehicleData: Boolean
get() = socPercent != null || rangeKm != null || speedKmh != null || get() = socPercent != null || rangeKm != null || speedKmh != null ||
@@ -26,6 +26,8 @@ interface EnergySignalSource {
fun vehicleConsumedKwh(): Float? = null fun vehicleConsumedKwh(): Float? = null
fun vehicleRegeneratedKwh(): Float? = null fun vehicleRegeneratedKwh(): Float? = null
fun parked(): Boolean? fun parked(): Boolean?
fun batteryVoltageV(): Float? = null
fun batteryCurrentA(): Float? = null
fun climate(): ClimateSnapshot fun climate(): ClimateSnapshot
fun tirePressures(): TirePressureSnapshot fun tirePressures(): TirePressureSnapshot
/** Probe-only values excluded from [EnergySnapshot] and normal consumers. */ /** Probe-only values excluded from [EnergySnapshot] and normal consumers. */
@@ -66,6 +68,8 @@ class EvHardwareEnergySignalSource(context: Context) : EnergySignalSource {
override fun vehicleConsumedKwh() = SaicCharging.consumedKwhSinceCharge() override fun vehicleConsumedKwh() = SaicCharging.consumedKwhSinceCharge()
override fun vehicleRegeneratedKwh() = SaicCharging.regeneratedKwhSinceCharge() override fun vehicleRegeneratedKwh() = SaicCharging.regeneratedKwhSinceCharge()
override fun parked() = EVHardware.isVehicleInPark() override fun parked() = EVHardware.isVehicleInPark()
override fun batteryVoltageV() = EVHardware.getVendorBatteryVoltageV()
override fun batteryCurrentA() = EVHardware.getVendorBatteryCurrentA()
override fun tirePressures() = TirePressureSnapshot( override fun tirePressures() = TirePressureSnapshot(
frontLeftKpa = EVHardware.getTirePressureKpa(EVHardware.Wheel.FRONT_LEFT), frontLeftKpa = EVHardware.getTirePressureKpa(EVHardware.Wheel.FRONT_LEFT),
frontRightKpa = EVHardware.getTirePressureKpa(EVHardware.Wheel.FRONT_RIGHT), frontRightKpa = EVHardware.getTirePressureKpa(EVHardware.Wheel.FRONT_RIGHT),
@@ -122,6 +126,8 @@ class EnergyTelemetryReader(private val source: EnergySignalSource) {
parked = source.parked(), parked = source.parked(),
climate = source.climate(), climate = source.climate(),
tirePressures = source.tirePressures(), tirePressures = source.tirePressures(),
batteryVoltageV = source.batteryVoltageV()?.takeIf { it.isFinite() },
batteryCurrentA = source.batteryCurrentA()?.takeIf { it.isFinite() },
) )
/** Reads CP-004 candidates only when an unstable evidence capture explicitly requests them. */ /** Reads CP-004 candidates only when an unstable evidence capture explicitly requests them. */
+1
View File
@@ -0,0 +1 @@
c5b77cde5ece6e7aaffd07dcf74f2259ad4e206eb5bcbb8844549c3470755e78 FatihsMG4-unstable-2.2.19.apk
+2 -2
View File
@@ -34,8 +34,8 @@ android {
applicationId "com.evsuite.abrp" applicationId "com.evsuite.abrp"
minSdk 27 minSdk 27
targetSdk 36 targetSdk 36
versionCode 35 versionCode 36
versionName "2.2.18" versionName "2.2.19"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -129,6 +129,7 @@ public class AbrpUploadService extends Service {
private volatile Location lastLocation; private volatile Location lastLocation;
private EnergyTelemetryReader energyReader; private EnergyTelemetryReader energyReader;
private ConsumptionTracker consumptionTracker;
/** /**
* Keeps the unit on the phone hotspot without a trip into Settings. Attempted on the * 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 * 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. // One shared, firmware-aware EVHardware snapshot owns every vehicle signal.
energyReader = new EnergyTelemetryReader(getApplicationContext()); energyReader = new EnergyTelemetryReader(getApplicationContext());
consumptionTracker = ConsumptionTracker.get(this);
wifi = new WifiAutoConnect(getApplicationContext()); wifi = new WifiAutoConnect(getApplicationContext());
// The head unit's own weather service, bound for one reason: ENV_OUTSIDE_TEMPERATURE // 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 // 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 // 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. // not have the map stack — in which case ext_temp simply stays unreported.
SaicWeather.INSTANCE.connect(getApplicationContext());
registerWeatherReceiver();
requestLocationUpdates(); requestLocationUpdates();
@@ -267,6 +267,7 @@ public class AbrpUploadService extends Service {
public void onDestroy() { public void onDestroy() {
Log.i(TAG, "Service stopping"); Log.i(TAG, "Service stopping");
if (scheduler != null) scheduler.shutdownNow(); if (scheduler != null) scheduler.shutdownNow();
if (consumptionTracker != null) consumptionTracker.persist();
if (locationManager != null) { if (locationManager != null) {
try { locationManager.removeUpdates(locationListener); } catch (Exception ignored) {} try { locationManager.removeUpdates(locationListener); } catch (Exception ignored) {}
} }
@@ -318,6 +319,10 @@ public class AbrpUploadService extends Service {
} }
private void doUpload() { private void doUpload() {
long sampleMs = System.currentTimeMillis();
EnergySnapshot vehicle = energyReader.read(sampleMs);
consumptionTracker.sample(vehicle);
String token = securePrefs.getString(SecurePrefs.KEY_TOKEN, "").trim(); String token = securePrefs.getString(SecurePrefs.KEY_TOKEN, "").trim();
String apiKey = securePrefs.getString(SecurePrefs.KEY_API_KEY, "").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 // 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. // 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(); boolean carUp = vehicle.getHasVehicleData();
Float speedKmh = vehicle.getSpeedKmh(); Float speedKmh = vehicle.getSpeedKmh();
Integer soc = vehicle.getSocPercent() == null Integer soc = vehicle.getSocPercent() == null
? null : Math.round(vehicle.getSocPercent()); ? null : Math.round(vehicle.getSocPercent());
Integer rangeKm = vehicle.getRangeKm() == null Integer rangeKm = vehicle.getRangeKm() == null
? null : Math.round(vehicle.getRangeKm()); ? null : Math.round(vehicle.getRangeKm());
Float extTemp = vehicle.getOutsideTempCelsius();
Float powerKw = vehicle.getBatteryPowerKw(); Float powerKw = vehicle.getBatteryPowerKw();
Float chargeRateKw = powerKw == null ? null : -powerKw; Float chargeRateKw = powerKw == null ? null : -powerKw;
Boolean portConnected = vehicle.getChargePortConnected(); 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 // 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 // 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. // 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); TelemetryPayload tlm = new TelemetryPayload(System.currentTimeMillis() / 1000);
tlm.soc = soc; tlm.soc = soc;
tlm.speedKmh = speedKmh; tlm.speedKmh = speedKmh;
tlm.rangeKm = rangeKm; tlm.rangeKm = rangeKm;
tlm.extTemp = extTemp;
tlm.powerKw = powerKw; tlm.powerKw = powerKw;
tlm.charging = charging; tlm.charging = charging;
tlm.dcfc = dcfc; tlm.dcfc = dcfc;
@@ -450,7 +422,7 @@ public class AbrpUploadService extends Service {
} }
String tlmJson = tlm.build(); String tlmJson = tlm.build();
String summary = TelemetryPayload.summarize(tlmJson) + " [temp " + tempSource + "]"; String summary = TelemetryPayload.summarize(tlmJson);
if (loc != null) { if (loc != null) {
summary += " (fix " + locationAgeMs(loc) / 1000 + "s old" summary += " (fix " + locationAgeMs(loc) / 1000 + "s old"
+ " +/-" + (loc.hasAccuracy() ? Math.round(loc.getAccuracy()) + "m" : "?") + " +/-" + (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 // 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 // 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. // coordinates are wrong, and the two agreeing says ABRP is showing something old.
if (sky != null && !sky.getCity().isEmpty()) summary += ", near " + sky.getCity();
summary += ")"; summary += ")";
} }
// What actually went on the wire. Kept on the log entry as well as in logcat: the // 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}. */ /** Navigation destinations in page order. Parallel to {@link #panes}. */
private static final int[] TAB_IDS = 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 }; R.id.tabLog };
private static final int[] PAGE_TITLES = { 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 R.string.design_service_title, R.string.design_wifi_title, R.string.design_log_title
}; };
private static final int[] PAGE_SUBTITLES = { 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 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 servicePane;
private View logPane; private View logPane;
private View vehiclePane; private View vehiclePane;
private View consumptionPane;
private View chargingPane;
private View wifiPane; private View wifiPane;
/** The five pages in navigation order. Parallel to {@link #TAB_IDS}. */ /** The five pages in navigation order. Parallel to {@link #TAB_IDS}. */
@@ -103,8 +105,18 @@ public class MainActivity extends AppCompatActivity {
*/ */
private EnergyTelemetryReader vehicleReader; private EnergyTelemetryReader vehicleReader;
private TextView vehSoc, vehRange, vehPower, vehSpeed, vehOdometer, private TextView vehSoc, vehRange, vehPower, vehSpeed, vehOdometer,
vehCharging, vehExtTemp, vehHvac, vehSoe, vehCharging, vehHvac, vehSoe,
vehTireFl, vehTireFr, vehTireRl, vehTireRr; 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 * 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(); refreshStatus();
refreshCallLog(); refreshCallLog();
refreshVehicle(); refreshVehicle();
refreshConsumption();
refreshWifi(); refreshWifi();
uiHandler.postDelayed(this, 2_000L); uiHandler.postDelayed(this, 2_000L);
} }
@@ -219,11 +232,15 @@ public class MainActivity extends AppCompatActivity {
abrpPane = inflatePane(R.layout.pane_abrp); abrpPane = inflatePane(R.layout.pane_abrp);
servicePane = inflatePane(R.layout.pane_service); servicePane = inflatePane(R.layout.pane_service);
vehiclePane = inflatePane(R.layout.pane_vehicle); vehiclePane = inflatePane(R.layout.pane_vehicle);
consumptionPane = inflatePane(R.layout.pane_consumption);
chargingPane = inflatePane(R.layout.pane_charging);
wifiPane = inflatePane(R.layout.pane_wifi); wifiPane = inflatePane(R.layout.pane_wifi);
logPane = inflatePane(R.layout.pane_log); 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(); bindVehiclePane();
bindConsumptionPane();
bindChargingPane();
bindWifiPane(); bindWifiPane();
apiKeyLayout = abrpPane.findViewById(R.id.api_key_layout); apiKeyLayout = abrpPane.findViewById(R.id.api_key_layout);
@@ -761,7 +778,6 @@ public class MainActivity extends AppCompatActivity {
vehSpeed = vehiclePane.findViewById(R.id.vehicle_speed); vehSpeed = vehiclePane.findViewById(R.id.vehicle_speed);
vehOdometer = vehiclePane.findViewById(R.id.vehicle_odometer); vehOdometer = vehiclePane.findViewById(R.id.vehicle_odometer);
vehCharging = vehiclePane.findViewById(R.id.vehicle_charging); vehCharging = vehiclePane.findViewById(R.id.vehicle_charging);
vehExtTemp = vehiclePane.findViewById(R.id.vehicle_ext_temp);
vehHvac = vehiclePane.findViewById(R.id.vehicle_hvac); vehHvac = vehiclePane.findViewById(R.id.vehicle_hvac);
vehSoe = vehiclePane.findViewById(R.id.vehicle_soe); vehSoe = vehiclePane.findViewById(R.id.vehicle_soe);
vehTireFl = vehiclePane.findViewById(R.id.vehicle_tire_fl); 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. // The timestamp is passed explicitly: its Kotlin default is not visible from Java.
EnergySnapshot s = vehicleReader.read(System.currentTimeMillis()); EnergySnapshot s = vehicleReader.read(System.currentTimeMillis());
ConsumptionTracker.get(this).sample(s);
refreshCharging(s);
vehSoc.setText(fmt(s.getSocPercent(), "%.0f %%")); vehSoc.setText(fmt(s.getSocPercent(), "%.0f %%"));
vehRange.setText(fmt(s.getRangeKm(), "%.0f km")); vehRange.setText(fmt(s.getRangeKm(), "%.0f km"));
vehPower.setText(fmt(s.getBatteryPowerKw(), "%.1f kW")); vehPower.setText(fmt(s.getBatteryPowerKw(), "%.1f kW"));
vehSpeed.setText(fmt(s.getSpeedKmh(), "%.0f km/h")); vehSpeed.setText(fmt(s.getSpeedKmh(), "%.0f km/h"));
vehOdometer.setText(fmt(s.getOdometerKm(), "%.0f km")); vehOdometer.setText(fmt(s.getOdometerKm(), "%.0f km"));
vehExtTemp.setText(fmt(s.getOutsideTempCelsius(), "%.0f °C"));
vehHvac.setText(fmt(s.getClimate().getDriverTargetCelsius(), "%.0f °C")); vehHvac.setText(fmt(s.getClimate().getDriverTargetCelsius(), "%.0f °C"));
vehSoe.setText(fmt(s.getBatteryEnergyKwh(), "%.1f kWh")); 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. * The charging row: the session state when the car reports one, the port otherwise.
* *
@@ -35,7 +35,6 @@ final class TelemetryPayload {
// --- Lower priority --- // --- Lower priority ---
Integer rangeKm; // km, vehicle's own estimate Integer rangeKm; // km, vehicle's own estimate
Float extTemp; // °C, outside
Float cabinTemp; // °C Float cabinTemp; // °C
Float battTempC; // °C, traction battery Float battTempC; // °C, traction battery
Float capacityKwh; // kWh, usable pack capacity Float capacityKwh; // kWh, usable pack capacity
@@ -63,13 +62,6 @@ final class TelemetryPayload {
putIfPresent(tlm, "soc", soc); putIfPresent(tlm, "soc", soc);
putIfPresent(tlm, "speed", speedKmh == null ? null : Math.round(speedKmh)); putIfPresent(tlm, "speed", speedKmh == null ? null : Math.round(speedKmh));
putIfPresent(tlm, "est_battery_range", rangeKm); 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)); if (powerKw != null) tlm.put("power", round2(powerKw));
putIfPresent(tlm, "is_charging", boolToInt(charging)); putIfPresent(tlm, "is_charging", boolToInt(charging));
putIfPresent(tlm, "is_dcfc", boolToInt(dcfc)); 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)}. * Used only to work out what was left out — see {@link #summarize(String)}.
*/ */
private static final String[] ALL_FIELDS = { 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", "batt_temp", "hvac_setpoint", "capacity", "soe", "kwh_charged", "odometer",
"is_charging", "is_dcfc", "is_parked", "lat", "lon", "elevation", "heading", "is_charging", "is_dcfc", "is_parked", "lat", "lon", "elevation", "heading",
"tire_pressure_fl", "tire_pressure_fr", "tire_pressure_rl", "tire_pressure_rr", "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" 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" /> <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/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/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/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" /> <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 style="@style/DataLabel" android:text="@string/vehicle_row_odometer" />
<TextView android:id="@+id/vehicle_odometer" style="@style/DataValue" /> <TextView android:id="@+id/vehicle_odometer" style="@style/DataValue" />
</TableRow> </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"> <TableRow style="@style/DataRow" android:minHeight="40dp" android:paddingTop="8dp" android:paddingBottom="8dp">
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_hvac" /> <TextView style="@style/DataLabel" android:text="@string/vehicle_row_hvac" />
<TextView android:id="@+id/vehicle_hvac" style="@style/DataValue" /> <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"/> <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"> <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/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/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/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"/> <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"/>
+20
View File
@@ -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>
-4
View File
@@ -8,10 +8,6 @@
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_odometer" /> <TextView style="@style/DataLabel" android:text="@string/vehicle_row_odometer" />
<TextView android:id="@+id/vehicle_odometer" style="@style/DataValue" /> <TextView android:id="@+id/vehicle_odometer" style="@style/DataValue" />
</TableRow> </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"> <TableRow style="@style/DataRow" android:minHeight="40dp" android:paddingTop="8dp" android:paddingBottom="8dp">
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_hvac" /> <TextView style="@style/DataLabel" android:text="@string/vehicle_row_hvac" />
<TextView android:id="@+id/vehicle_hvac" style="@style/DataValue" /> <TextView android:id="@+id/vehicle_hvac" style="@style/DataValue" />
+6 -1
View File
@@ -5,6 +5,12 @@
<string name="tab_abrp">ABRP</string> <string name="tab_abrp">ABRP</string>
<string name="tab_service">Aktarım</string> <string name="tab_service">Aktarım</string>
<string name="tab_vehicle">Genel</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_wifi">Wi-Fi</string>
<string name="tab_log">Kayıtlar</string> <string name="tab_log">Kayıtlar</string>
@@ -28,7 +34,6 @@
<string name="vehicle_row_speed">Hız</string> <string name="vehicle_row_speed">Hız</string>
<string name="vehicle_row_odometer">Kilometre</string> <string name="vehicle_row_odometer">Kilometre</string>
<string name="vehicle_row_batt_temp">Batarya Sıcaklığı</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_cabin_temp">Kabin Sıcaklığı</string>
<string name="vehicle_row_hvac">Klima Ayarı</string> <string name="vehicle_row_hvac">Klima Ayarı</string>
<string name="vehicle_row_capacity">Batarya Kapasitesi</string> <string name="vehicle_row_capacity">Batarya Kapasitesi</string>
@@ -21,7 +21,6 @@ public class TelemetryPayloadTest {
t.soc = 63; t.soc = 63;
t.speedKmh = 48.4f; t.speedKmh = 48.4f;
t.rangeKm = 210; t.rangeKm = 210;
t.extTemp = 12f;
t.powerKw = -14.5f; t.powerKw = -14.5f;
t.charging = false; t.charging = false;
t.dcfc = false; t.dcfc = false;
@@ -46,7 +45,7 @@ public class TelemetryPayloadTest {
public void unreadablePropertiesAreOmittedNotZeroed() { public void unreadablePropertiesAreOmittedNotZeroed() {
String json = noCar(); String json = noCar();
for (String field : new String[]{ for (String field : new String[]{
"soc", "speed", "est_battery_range", "ext_temp", "power", "soc", "speed", "est_battery_range", "power",
"is_charging", "is_dcfc", "is_parked", "cabin_temp", "is_charging", "is_dcfc", "is_parked", "cabin_temp",
"batt_temp", "capacity", "odometer", "hvac_setpoint", "batt_temp", "capacity", "odometer", "hvac_setpoint",
"tire_pressure_fl", "tire_pressure_fr", "tire_pressure_fl", "tire_pressure_fr",
@@ -84,7 +83,6 @@ public class TelemetryPayloadTest {
assertEquals(63, json.getInt("soc")); assertEquals(63, json.getInt("soc"));
assertEquals(48, json.getInt("speed")); // rounded assertEquals(48, json.getInt("speed")); // rounded
assertEquals(210, json.getInt("est_battery_range")); assertEquals(210, json.getInt("est_battery_range"));
assertEquals(12, json.getInt("ext_temp"));
assertEquals(-14.5, json.getDouble("power"), 0.001); assertEquals(-14.5, json.getDouble("power"), 0.001);
assertEquals(0, json.getInt("is_charging")); assertEquals(0, json.getInt("is_charging"));
assertEquals(0, json.getInt("is_dcfc")); assertEquals(0, json.getInt("is_dcfc"));
@@ -296,27 +294,10 @@ public class TelemetryPayloadTest {
new JSONObject(noCar()); new JSONObject(noCar());
} }
@Test
public void unimplementedOutsideTempIsOmittedNotSentAsZero() {
// A VHAL without ENV_OUTSIDE_TEMPERATURE answers 0.0. Sent as ext_temp:0 it is a
// freezing day as far as ABRP's consumption model is concerned.
TelemetryPayload t = fullCar();
t.extTemp = 0f;
assertFalse(t.build().contains("ext_temp"));
}
@Test
public void impossibleOutsideTempIsOmitted() {
TelemetryPayload t = fullCar();
t.extTemp = -273f;
assertFalse(t.build().contains("ext_temp"));
}
@Test @Test
public void summaryListsSentValuesAndOmittedFields() { public void summaryListsSentValuesAndOmittedFields() {
String summary = TelemetryPayload.summarize(fullCar().build()); String summary = TelemetryPayload.summarize(fullCar().build());
assertTrue(summary, summary.contains("soc=63")); assertTrue(summary, summary.contains("soc=63"));
assertTrue(summary, summary.contains("ext_temp=12"));
assertTrue(summary, summary.contains("cabin_temp=22")); assertTrue(summary, summary.contains("cabin_temp=22"));
// Nothing gave us a position, so lat/lon must show up as omitted, not as 0. // Nothing gave us a position, so lat/lon must show up as omitted, not as 0.
assertFalse(summary, summary.contains("lat=")); assertFalse(summary, summary.contains("lat="));