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:
2026-10-23 22:24:52 +02:00
co-authored by Claude Opus 5.5
parent 71aceaa626
commit 2931b1feed
9 changed files with 118 additions and 5 deletions
+1 -1
View File
@@ -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 |
+6
View File
@@ -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
View File
@@ -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" />
+8
View File
@@ -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" />
+5
View File
@@ -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));
}
}