App 2.20.0: lease mileage allowance on the Overview
Vehicle info shows the km allowed by today (40,000 km over four years from 2023-05-01, evenly per day) and the odometer against it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
@@ -68,7 +68,7 @@ Repository: `ssh://git@git.stinnesbeck.com:2222/nils/mg4.git` (remote `origin`),
|
|||||||
|
|
||||||
| Tab | Contents |
|
| Tab | Contents |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Overview | value tiles (battery, range, range/usage/energy since start, 5-min averages of range, usage, power, speed), vehicle info, tyre pressures; read every 0.5 s on a background thread |
|
| Overview | value tiles (battery, range, range/usage/energy since start, 5-min averages of range, usage, power, speed), vehicle info (odometer, lease allowance and the odometer against it), tyre pressures; read every 0.5 s on a background thread |
|
||||||
| Consumption / Charging | period totals, daily log, charging sessions and curve |
|
| Consumption / Charging | period totals, daily log, charging sessions and curve |
|
||||||
| Live data | on/off, ABRP API key and user token, upload frequency |
|
| Live data | on/off, ABRP API key and user token, upload frequency |
|
||||||
| Server | URL, pairing by QR, ingest token, installation ID (read-only), config import |
|
| Server | URL, pairing by QR, ingest token, installation ID (read-only), config import |
|
||||||
|
|||||||
@@ -118,6 +118,12 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does *
|
|||||||
confirmed car facts, app structure, UI conventions, build/test/ship, documents. It replaces
|
confirmed car facts, app structure, UI conventions, build/test/ship, documents. It replaces
|
||||||
upstream's EVABRPUploader text. The test emulator is now in the flake: `nix run .#car-emulator`
|
upstream's EVABRPUploader text. The test emulator is now in the flake: `nix run .#car-emulator`
|
||||||
(Android 9 AOSP at 1920×720, 160 dpi; the car build installs as the system user — checked).
|
(Android 9 AOSP at 1920×720, 160 dpi; the car build installs as the system user — checked).
|
||||||
|
- 2026-10-23 — App 2.20.0: the Overview's Vehicle info card shows the lease mileage: "Lease
|
||||||
|
allowance today" (40,000 km over four years from 2023-05-01, accruing evenly per day, in
|
||||||
|
`LeaseAllowance`, unit-tested) and "Versus allowance", the odometer against it ("… km over" /
|
||||||
|
"… km under", "—" when the odometer is unread). The odometer now has a thousands separator,
|
||||||
|
like the new rows. Checked on the emulator with a sample odometer of 25,000 km in a throwaway
|
||||||
|
build (34,798 km allowed on 2026-10-23, 9,798 km under); the Overview still fits.
|
||||||
- 2026-10-23 — App 2.19.9: the Foreground apps card is gone from the Logs tab (it found CarPlay's
|
- 2026-10-23 — App 2.19.9: the Foreground apps card is gone from the Logs tab (it found CarPlay's
|
||||||
package; nothing left to find), with ForegroundProbe and ForegroundHistory. The Logs tab is one
|
package; nothing left to find), with ForegroundProbe and ForegroundHistory. The Logs tab is one
|
||||||
card again — update log, then uploads — with the other tabs' page padding.
|
card again — update log, then uploads — with the other tabs' page padding.
|
||||||
|
|||||||
+2
-2
@@ -34,8 +34,8 @@ android {
|
|||||||
applicationId "com.stinnesbeck.nilsmg4"
|
applicationId "com.stinnesbeck.nilsmg4"
|
||||||
minSdk 27
|
minSdk 27
|
||||||
targetSdk 36
|
targetSdk 36
|
||||||
versionCode 108
|
versionCode 109
|
||||||
versionName "2.19.9"
|
versionName "2.20.0"
|
||||||
|
|
||||||
// Nothing about the server ships in the APK: its URL and ingest token are entered in
|
// Nothing about the server ships in the APK: its URL and ingest token are entered in
|
||||||
// the app's Server tab (ServerConfig), and the database is only reachable by the server.
|
// the app's Server tab (ServerConfig), and the database is only reachable by the server.
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.evsuite.abrp;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The lease's mileage allowance: 40,000 km over four years from 1 May 2023, accruing evenly per
|
||||||
|
* day. The Overview's Vehicle info card shows what is allowed by today and how far the odometer
|
||||||
|
* is above or below it. Pure Java, so it is unit-tested.
|
||||||
|
*/
|
||||||
|
final class LeaseAllowance {
|
||||||
|
|
||||||
|
static final LocalDate START = LocalDate.of(2023, 5, 1);
|
||||||
|
static final LocalDate END = START.plusYears(4);
|
||||||
|
static final double TOTAL_KM = 40_000d;
|
||||||
|
|
||||||
|
private LeaseAllowance() {}
|
||||||
|
|
||||||
|
/** Kilometres allowed by {@code today}: none before the start, all of them from the end on. */
|
||||||
|
static double allowedKm(LocalDate today) {
|
||||||
|
long total = ChronoUnit.DAYS.between(START, END);
|
||||||
|
long elapsed = Math.max(0L, Math.min(total, ChronoUnit.DAYS.between(START, today)));
|
||||||
|
return TOTAL_KM * elapsed / total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Odometer minus allowance: positive over, negative under; null when the odometer is unread. */
|
||||||
|
static Double differenceKm(Float odometerKm, LocalDate today) {
|
||||||
|
if (odometerKm == null || !Float.isFinite(odometerKm)) return null;
|
||||||
|
return odometerKm - allowedKm(today);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,7 +85,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
/** Used only on the overview-read thread. */
|
/** Used only on the overview-read thread. */
|
||||||
private EnergyTelemetryReader vehicleReader;
|
private EnergyTelemetryReader vehicleReader;
|
||||||
private TextView vehSoc, vehRange, vehPower, vehSpeed, vehOdometer,
|
private TextView vehSoc, vehRange, vehPower, vehSpeed, vehOdometer,
|
||||||
vehCharging, vehHvac,
|
vehCharging, vehHvac, vehLeaseAllowance, vehLeaseDifference,
|
||||||
vehTireFl, vehTireFr, vehTireRl, vehTireRr,
|
vehTireFl, vehTireFr, vehTireRl, vehTireRr,
|
||||||
vehRangeTrip, vehRangeTripNote, vehRangeAverage, vehRangeAverageNote,
|
vehRangeTrip, vehRangeTripNote, vehRangeAverage, vehRangeAverageNote,
|
||||||
vehUsageTrip, vehUsageTripNote, vehUsageAverage, vehUsageAverageNote,
|
vehUsageTrip, vehUsageTripNote, vehUsageAverage, vehUsageAverageNote,
|
||||||
@@ -1243,6 +1243,8 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
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);
|
||||||
vehHvac = vehiclePane.findViewById(R.id.vehicle_hvac);
|
vehHvac = vehiclePane.findViewById(R.id.vehicle_hvac);
|
||||||
|
vehLeaseAllowance = vehiclePane.findViewById(R.id.vehicle_lease_allowance);
|
||||||
|
vehLeaseDifference = vehiclePane.findViewById(R.id.vehicle_lease_difference);
|
||||||
vehTireFl = vehiclePane.findViewById(R.id.vehicle_tire_fl);
|
vehTireFl = vehiclePane.findViewById(R.id.vehicle_tire_fl);
|
||||||
vehTireFr = vehiclePane.findViewById(R.id.vehicle_tire_fr);
|
vehTireFr = vehiclePane.findViewById(R.id.vehicle_tire_fr);
|
||||||
vehTireRl = vehiclePane.findViewById(R.id.vehicle_tire_rl);
|
vehTireRl = vehiclePane.findViewById(R.id.vehicle_tire_rl);
|
||||||
@@ -1386,13 +1388,30 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The lease allowance by today and the odometer against it (LeaseAllowance). */
|
||||||
|
private void showLease(Float odometerKm) {
|
||||||
|
java.time.LocalDate today = java.time.LocalDate.now();
|
||||||
|
// Locale.US like fmt(), so the card's numbers share one format.
|
||||||
|
vehLeaseAllowance.setText(String.format(java.util.Locale.US, "%,.0f km",
|
||||||
|
LeaseAllowance.allowedKm(today)));
|
||||||
|
Double diff = LeaseAllowance.differenceKm(odometerKm, today);
|
||||||
|
if (diff == null) {
|
||||||
|
vehLeaseDifference.setText(R.string.no_value);
|
||||||
|
} else {
|
||||||
|
String km = String.format(java.util.Locale.US, "%,.0f", Math.abs(diff));
|
||||||
|
vehLeaseDifference.setText(Math.round(diff) == 0 ? getString(R.string.lease_on_target)
|
||||||
|
: getString(diff > 0 ? R.string.lease_over : R.string.lease_under, km));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void showVehicle(EnergySnapshot s) {
|
private void showVehicle(EnergySnapshot s) {
|
||||||
try {
|
try {
|
||||||
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"));
|
||||||
|
showLease(s.getOdometerKm());
|
||||||
vehHvac.setText(fmt(s.getClimate().getDriverTargetCelsius(), "%.0f °C"));
|
vehHvac.setText(fmt(s.getClimate().getDriverTargetCelsius(), "%.0f °C"));
|
||||||
|
|
||||||
vehCharging.setText(chargingLabel(s));
|
vehCharging.setText(chargingLabel(s));
|
||||||
|
|||||||
@@ -67,6 +67,14 @@
|
|||||||
<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_lease_allowance" />
|
||||||
|
<TextView android:id="@+id/vehicle_lease_allowance" 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_lease_difference" />
|
||||||
|
<TextView android:id="@+id/vehicle_lease_difference" 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" />
|
||||||
|
|||||||
@@ -63,6 +63,14 @@
|
|||||||
<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_lease_allowance" />
|
||||||
|
<TextView android:id="@+id/vehicle_lease_allowance" 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_lease_difference" />
|
||||||
|
<TextView android:id="@+id/vehicle_lease_difference" 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" />
|
||||||
|
|||||||
@@ -48,6 +48,11 @@
|
|||||||
<string name="usable_capacity_invalid">Enter a value between 10 and 150 kWh</string>
|
<string name="usable_capacity_invalid">Enter a value between 10 and 150 kWh</string>
|
||||||
<string name="vehicle_row_speed">Speed</string>
|
<string name="vehicle_row_speed">Speed</string>
|
||||||
<string name="vehicle_row_odometer">Odometer</string>
|
<string name="vehicle_row_odometer">Odometer</string>
|
||||||
|
<string name="vehicle_row_lease_allowance">Lease allowance today</string>
|
||||||
|
<string name="vehicle_row_lease_difference">Versus allowance</string>
|
||||||
|
<string name="lease_over">%1$s km over</string>
|
||||||
|
<string name="lease_under">%1$s km under</string>
|
||||||
|
<string name="lease_on_target">On target</string>
|
||||||
<string name="vehicle_row_batt_temp">Battery Temperature</string>
|
<string name="vehicle_row_batt_temp">Battery Temperature</string>
|
||||||
<string name="vehicle_row_cabin_temp">Cabin Temperature</string>
|
<string name="vehicle_row_cabin_temp">Cabin Temperature</string>
|
||||||
<string name="vehicle_row_hvac">Climate Setpoint</string>
|
<string name="vehicle_row_hvac">Climate Setpoint</string>
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.evsuite.abrp;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
/** The lease mileage allowance — pure JVM. */
|
||||||
|
public class LeaseAllowanceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void accruesEvenlyPerDay() {
|
||||||
|
assertEquals(0d, LeaseAllowance.allowedKm(LocalDate.of(2023, 5, 1)), 1e-9);
|
||||||
|
// 731 of 1461 days (2024 is a leap year).
|
||||||
|
assertEquals(40_000d * 731 / 1461, LeaseAllowance.allowedKm(LocalDate.of(2025, 5, 1)), 1e-9);
|
||||||
|
assertEquals(40_000d, LeaseAllowance.allowedKm(LocalDate.of(2027, 5, 1)), 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void staysWithinTheLease() {
|
||||||
|
assertEquals(0d, LeaseAllowance.allowedKm(LocalDate.of(2023, 1, 1)), 1e-9);
|
||||||
|
assertEquals(40_000d, LeaseAllowance.allowedKm(LocalDate.of(2028, 1, 1)), 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void differenceIsOverPositiveUnderNegative() {
|
||||||
|
LocalDate day = LocalDate.of(2025, 5, 1);
|
||||||
|
double allowed = LeaseAllowance.allowedKm(day);
|
||||||
|
assertEquals(1000d, LeaseAllowance.differenceKm((float) (allowed + 1000), day), 0.01);
|
||||||
|
assertEquals(-500d, LeaseAllowance.differenceKm((float) (allowed - 500), day), 0.01);
|
||||||
|
assertNull(LeaseAllowance.differenceKm(null, day));
|
||||||
|
assertNull(LeaseAllowance.differenceKm(Float.NaN, day));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user