App 2.13.0: tyre pressures in bar; 5-minute average power
Tyre pressures show in bar by default; Settings → Display offers kPa and psi as well. Display only: the car reports kPa and the server keeps storing kPa. A "Power, 5-min average" card under Power: the time-weighted mean battery power over the last five minutes, standing included, from the same samples as the 5-minute range. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
This commit is contained in:
@@ -93,7 +93,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
vehTireFl, vehTireFr, vehTireRl, vehTireRr,
|
||||
vehRangeTrip, vehRangeTripNote, vehRangeAverage, vehRangeAverageNote,
|
||||
vehUsageTrip, vehUsageTripNote, vehUsageAverage, vehUsageAverageNote,
|
||||
vehEnergyTrip, vehEnergyTripNote;
|
||||
vehEnergyTrip, vehEnergyTripNote, vehPowerAverage, vehPowerAverageNote;
|
||||
|
||||
/** Usable pack capacity for the range estimates; the car does not publish it. */
|
||||
static final String USABLE_KWH_KEY = "usable_kwh";
|
||||
@@ -977,6 +977,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
voiceKeySwitch.setChecked(UploadSettings.voiceKeyOpensApp(prefs));
|
||||
voiceKeySwitch.setOnCheckedChangeListener((btn, checked) ->
|
||||
prefs.edit().putBoolean(UploadSettings.KEY_VOICE_KEY_OPENS, checked).apply());
|
||||
bindTireUnit();
|
||||
fullScreenSwitch = settingsPane.findViewById(R.id.full_screen_switch);
|
||||
fullScreenSwitch.setChecked(UploadSettings.fullScreen(prefs));
|
||||
fullScreenSwitch.setOnCheckedChangeListener((btn, checked) -> {
|
||||
@@ -1057,6 +1058,26 @@ public class MainActivity extends AppCompatActivity {
|
||||
}, "abrp-test").start();
|
||||
}
|
||||
|
||||
/** Tyre pressure unit: display only, read on every refresh of the Overview. */
|
||||
private void bindTireUnit() {
|
||||
android.widget.Spinner spinner = settingsPane.findViewById(R.id.tire_unit_spinner);
|
||||
PressureUnit[] units = PressureUnit.values();
|
||||
String[] labels = new String[units.length];
|
||||
for (int i = 0; i < units.length; i++) labels[i] = units[i].label;
|
||||
android.widget.ArrayAdapter<String> adapter = new android.widget.ArrayAdapter<>(
|
||||
this, R.layout.spinner_value, labels);
|
||||
adapter.setDropDownViewResource(R.layout.spinner_dropdown);
|
||||
spinner.setAdapter(adapter);
|
||||
spinner.setSelection(PressureUnit.fromPref(prefs.getString(PressureUnit.PREF_KEY, null)).ordinal());
|
||||
spinner.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(android.widget.AdapterView<?> parent, View view, int position, long id) {
|
||||
prefs.edit().putString(PressureUnit.PREF_KEY, units[position].name()).apply();
|
||||
}
|
||||
@Override public void onNothingSelected(android.widget.AdapterView<?> parent) { }
|
||||
});
|
||||
}
|
||||
|
||||
/** The service reads these settings on every tick, so a change applies without a restart. */
|
||||
private void bindCadenceControls() {
|
||||
UploadSettings current = UploadSettings.from(prefs);
|
||||
@@ -1190,6 +1211,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
vehUsageAverageNote = vehiclePane.findViewById(R.id.vehicle_usage_average_note);
|
||||
vehEnergyTrip = vehiclePane.findViewById(R.id.vehicle_energy_trip);
|
||||
vehEnergyTripNote = vehiclePane.findViewById(R.id.vehicle_energy_trip_note);
|
||||
vehPowerAverage = vehiclePane.findViewById(R.id.vehicle_power_average);
|
||||
vehPowerAverageNote = vehiclePane.findViewById(R.id.vehicle_power_average_note);
|
||||
}
|
||||
|
||||
private double usableKwh() {
|
||||
@@ -1207,6 +1230,9 @@ public class MainActivity extends AppCompatActivity {
|
||||
estimator.add(monotonicMs, s.getBatteryPowerKw(), s.getSpeedKmh());
|
||||
Double remaining = RangeEstimator.remainingKwh(s.getSocPercent(), usableKwh());
|
||||
RangeEstimator.Estimate trip = estimator.sinceStart(remaining);
|
||||
Double power = estimator.averagePowerKw(monotonicMs);
|
||||
vehPowerAverage.setText(power == null ? "—" : getString(R.string.power_kw, power));
|
||||
vehPowerAverageNote.setText(power == null ? R.string.power_waiting : R.string.usage_last_5_min);
|
||||
Double energy = estimator.energySinceStart();
|
||||
vehEnergyTrip.setText(energy == null ? "—" : getString(R.string.energy_kwh, energy));
|
||||
vehEnergyTripNote.setText(energy == null ? R.string.energy_trip_waiting : R.string.energy_trip_note);
|
||||
@@ -1264,10 +1290,11 @@ public class MainActivity extends AppCompatActivity {
|
||||
vehCharging.setText(chargingLabel(s));
|
||||
refreshRangeEstimates(s);
|
||||
|
||||
vehTireFl.setText(fmt(s.getTirePressures().getFrontLeftKpa(), "%.0f kPa"));
|
||||
vehTireFr.setText(fmt(s.getTirePressures().getFrontRightKpa(), "%.0f kPa"));
|
||||
vehTireRl.setText(fmt(s.getTirePressures().getRearLeftKpa(), "%.0f kPa"));
|
||||
vehTireRr.setText(fmt(s.getTirePressures().getRearRightKpa(), "%.0f kPa"));
|
||||
PressureUnit unit = PressureUnit.fromPref(prefs.getString(PressureUnit.PREF_KEY, null));
|
||||
vehTireFl.setText(unit.format(s.getTirePressures().getFrontLeftKpa()));
|
||||
vehTireFr.setText(unit.format(s.getTirePressures().getFrontRightKpa()));
|
||||
vehTireRl.setText(unit.format(s.getTirePressures().getRearLeftKpa()));
|
||||
vehTireRr.setText(unit.format(s.getTirePressures().getRearRightKpa()));
|
||||
} catch (Throwable ignored) {
|
||||
// Leave the last good reading on screen.
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* The unit tyre pressures are shown in. The car reports kPa; this only changes the display.
|
||||
* Android-free, unit-tested.
|
||||
*/
|
||||
enum PressureUnit {
|
||||
BAR("bar", "%.1f bar", 0.01),
|
||||
KPA("kPa", "%.0f kPa", 1.0),
|
||||
PSI("psi", "%.1f psi", 0.145038);
|
||||
|
||||
static final String PREF_KEY = "tire_pressure_unit";
|
||||
static final PressureUnit DEFAULT = BAR;
|
||||
|
||||
final String label;
|
||||
private final String pattern;
|
||||
private final double perKpa;
|
||||
|
||||
PressureUnit(String label, String pattern, double perKpa) {
|
||||
this.label = label;
|
||||
this.pattern = pattern;
|
||||
this.perKpa = perKpa;
|
||||
}
|
||||
|
||||
/** A reading in this unit, or a dash for none. */
|
||||
String format(Float kpa) {
|
||||
if (kpa == null || kpa.isNaN() || kpa.isInfinite()) return "—";
|
||||
return String.format(Locale.US, pattern, kpa * perKpa);
|
||||
}
|
||||
|
||||
/** The stored choice; anything unknown falls back to bar. */
|
||||
static PressureUnit fromPref(String name) {
|
||||
for (PressureUnit u : values()) {
|
||||
if (u.name().equals(name)) return u;
|
||||
}
|
||||
return DEFAULT;
|
||||
}
|
||||
}
|
||||
@@ -154,6 +154,29 @@ final class RangeEstimator {
|
||||
return new Estimate(rangeKm, kwhPerKm * 100, distanceKm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mean battery power over the last five minutes, time-weighted, standing included; null with
|
||||
* less than a minute of samples. Positive is drawn from the pack, as the Power tile shows it.
|
||||
*/
|
||||
synchronized Double averagePowerKw(long nowMs) {
|
||||
double energyKwh = 0;
|
||||
long covered = 0;
|
||||
Sample previous = null;
|
||||
for (Sample s : samples) {
|
||||
if (s.timeMs < nowMs - WINDOW_MS) continue;
|
||||
if (previous != null) {
|
||||
long dt = s.timeMs - previous.timeMs;
|
||||
if (dt > 0 && dt <= MAX_GAP_MS) {
|
||||
energyKwh += (previous.powerKw + s.powerKw) / 2 * (dt / 3_600_000.0);
|
||||
covered += dt;
|
||||
}
|
||||
}
|
||||
previous = s;
|
||||
}
|
||||
if (covered < MIN_WINDOW_MS) return null;
|
||||
return energyKwh / (covered / 3_600_000.0);
|
||||
}
|
||||
|
||||
synchronized void clear() {
|
||||
samples.clear();
|
||||
tripEnergyKwh = 0;
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_full_screen" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/voice_key_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_voice_key" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_voice_key" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/label_tire_unit" android:textAppearance="@style/Text.EV.Body" android:layout_marginTop="20dp" />
|
||||
<Spinner android:id="@+id/tire_unit_spinner" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:layout_marginTop="12dp" android:background="@drawable/bg_input_panel" android:popupBackground="@color/ev_surface_raised" android:contentDescription="@string/label_tire_unit" android:spinnerMode="dropdown" />
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp" android:background="@drawable/bg_glass" android:layout_marginTop="16dp">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_battery_settings_title" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||
|
||||
@@ -47,7 +47,11 @@
|
||||
<TextView android:id="@+id/vehicle_usage_average" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:layout_marginTop="6dp" android:textAppearance="@style/Text.EV.Metric.Compact" android:maxLines="1" android:autoSizeTextType="uniform" android:autoSizeMinTextSize="18sp" android:autoSizeMaxTextSize="28sp" android:autoSizeStepGranularity="1sp" />
|
||||
<TextView android:id="@+id/vehicle_usage_average_note" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="" android:textAppearance="@style/Text.EV.Secondary" android:textColor="@color/ev_cyan" android:textSize="16sp" android:maxLines="1" android:ellipsize="end" />
|
||||
</LinearLayout>
|
||||
<Space android:layout_width="0dp" android:layout_height="1dp" android:layout_weight="1" android:layout_marginEnd="10dp" />
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:layout_marginEnd="10dp" android:orientation="vertical" android:minHeight="112dp" android:padding="16dp" android:background="@drawable/bg_glass">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/vehicle_row_power_average" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
<TextView android:id="@+id/vehicle_power_average" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:layout_marginTop="6dp" android:textAppearance="@style/Text.EV.Metric.Compact" android:maxLines="1" android:autoSizeTextType="uniform" android:autoSizeMinTextSize="18sp" android:autoSizeMaxTextSize="28sp" android:autoSizeStepGranularity="1sp" />
|
||||
<TextView android:id="@+id/vehicle_power_average_note" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="" android:textAppearance="@style/Text.EV.Secondary" android:textColor="@color/ev_cyan" android:textSize="16sp" android:maxLines="1" android:ellipsize="end" />
|
||||
</LinearLayout>
|
||||
<Space android:layout_width="0dp" android:layout_height="1dp" android:layout_weight="1" android:layout_marginEnd="10dp" />
|
||||
</LinearLayout>
|
||||
<Space android:layout_width="1dp" android:layout_height="16dp" />
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_full_screen" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/voice_key_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_voice_key" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_voice_key" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/label_tire_unit" android:textAppearance="@style/Text.EV.Body" android:layout_marginTop="20dp" />
|
||||
<Spinner android:id="@+id/tire_unit_spinner" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:layout_marginTop="12dp" android:background="@drawable/bg_input_panel" android:popupBackground="@color/ev_surface_raised" android:contentDescription="@string/label_tire_unit" android:spinnerMode="dropdown" />
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp" android:background="@drawable/bg_glass" android:layout_marginTop="16dp">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_battery_settings_title" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||
|
||||
@@ -40,6 +40,11 @@
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/vehicle_row_power" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
<TextView android:id="@+id/vehicle_power" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:layout_marginTop="6dp" android:textAppearance="@style/Text.EV.Metric.Compact" android:maxLines="1" android:autoSizeTextType="uniform" android:autoSizeMinTextSize="18sp" android:autoSizeMaxTextSize="28sp" android:autoSizeStepGranularity="1sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_columnWeight="1" android:layout_margin="4dp" android:orientation="vertical" android:minHeight="112dp" android:padding="16dp" android:background="@drawable/bg_glass">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/vehicle_row_power_average" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
<TextView android:id="@+id/vehicle_power_average" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:layout_marginTop="6dp" android:textAppearance="@style/Text.EV.Metric.Compact" android:maxLines="1" android:autoSizeTextType="uniform" android:autoSizeMinTextSize="18sp" android:autoSizeMaxTextSize="28sp" android:autoSizeStepGranularity="1sp" />
|
||||
<TextView android:id="@+id/vehicle_power_average_note" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="" android:textAppearance="@style/Text.EV.Secondary" android:textColor="@color/ev_cyan" android:textSize="16sp" android:maxLines="1" android:ellipsize="end" />
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:orientation="vertical" android:minHeight="112dp" android:padding="16dp" android:background="@drawable/bg_glass" android:layout_columnWeight="1" android:layout_margin="4dp">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/vehicle_row_speed" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
<TextView android:id="@+id/vehicle_speed" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:layout_marginTop="6dp" android:textAppearance="@style/Text.EV.Metric.Compact" android:maxLines="1" android:autoSizeTextType="uniform" android:autoSizeMinTextSize="18sp" android:autoSizeMaxTextSize="28sp" android:autoSizeStepGranularity="1sp" />
|
||||
|
||||
@@ -35,6 +35,10 @@
|
||||
<string name="vehicle_row_range_average">Range at 5-min average</string>
|
||||
<string name="vehicle_row_usage_trip">Usage since start</string>
|
||||
<string name="vehicle_row_energy_trip">Energy since start</string>
|
||||
<string name="vehicle_row_power_average">Power, 5-min average</string>
|
||||
<string name="power_kw">%1$.1f kW</string>
|
||||
<string name="power_waiting">needs a minute of data</string>
|
||||
<string name="label_tire_unit">Tyre pressure unit</string>
|
||||
<string name="energy_kwh">%1$.1f kWh</string>
|
||||
<string name="energy_trip_note">net, after recuperation</string>
|
||||
<string name="energy_trip_waiting">once the car is on</string>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
/** Tyre pressure display units — pure JVM. */
|
||||
public class PressureUnitTest {
|
||||
|
||||
@Test
|
||||
public void kpaFromTheCarInEachUnit() {
|
||||
assertEquals("2.5 bar", PressureUnit.BAR.format(250f));
|
||||
assertEquals("250 kPa", PressureUnit.KPA.format(250f));
|
||||
assertEquals("36.3 psi", PressureUnit.PSI.format(250f));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anUnreadPressureStaysADash() {
|
||||
assertEquals("—", PressureUnit.BAR.format(null));
|
||||
assertEquals("—", PressureUnit.PSI.format(Float.NaN));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void barUnlessSomethingElseWasChosen() {
|
||||
assertEquals(PressureUnit.BAR, PressureUnit.fromPref(null));
|
||||
assertEquals(PressureUnit.BAR, PressureUnit.fromPref("nonsense"));
|
||||
assertEquals(PressureUnit.PSI, PressureUnit.fromPref("PSI"));
|
||||
}
|
||||
}
|
||||
@@ -128,4 +128,16 @@ public class RangeEstimatorTest {
|
||||
for (long t = 615_000; t <= 735_000; t += 15_000) r.add(t, -20f, 60f); // downhill
|
||||
assertTrue(r.energySinceStart() < 5.0); // recuperation counts back
|
||||
}
|
||||
|
||||
@Test
|
||||
public void averagePowerOverTheLastFiveMinutes() {
|
||||
RangeEstimator r = new RangeEstimator();
|
||||
r.add(0, 20f, 50f);
|
||||
assertNull(r.averagePowerKw(0)); // too little yet
|
||||
for (long t = 15_000; t <= 300_000; t += 15_000) r.add(t, 20f, 50f);
|
||||
assertEquals(20.0, r.averagePowerKw(300_000), 1e-6);
|
||||
// Standing with the climate on counts: half the window at 2 kW.
|
||||
for (long t = 315_000; t <= 450_000; t += 15_000) r.add(t, 2f, 0f);
|
||||
assertEquals(11.0, r.averagePowerKw(450_000), 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user