From 2931b1feeded323c9d2c67cd8ea8f0343b89fd4b Mon Sep 17 00:00:00 2001 From: Nils Stinnesbeck Date: Fri, 23 Oct 2026 22:24:52 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6 --- AGENTS.md | 2 +- SPECS.md | 6 ++++ app/build.gradle | 4 +-- .../java/com/evsuite/abrp/LeaseAllowance.java | 31 ++++++++++++++++ .../java/com/evsuite/abrp/MainActivity.java | 23 ++++++++++-- .../main/res/layout-w1000dp/pane_vehicle.xml | 8 +++++ app/src/main/res/layout/pane_vehicle.xml | 8 +++++ app/src/main/res/values/strings.xml | 5 +++ .../com/evsuite/abrp/LeaseAllowanceTest.java | 36 +++++++++++++++++++ 9 files changed, 118 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/com/evsuite/abrp/LeaseAllowance.java create mode 100644 app/src/test/java/com/evsuite/abrp/LeaseAllowanceTest.java diff --git a/AGENTS.md b/AGENTS.md index c5642d4..d45ea11 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -68,7 +68,7 @@ Repository: `ssh://git@git.stinnesbeck.com:2222/nils/mg4.git` (remote `origin`), | 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 | | 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 | diff --git a/SPECS.md b/SPECS.md index 3f5e586..5ba7203 100644 --- a/SPECS.md +++ b/SPECS.md @@ -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 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). +- 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 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. diff --git a/app/build.gradle b/app/build.gradle index 410c5b4..df5f0ec 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,8 +34,8 @@ android { applicationId "com.stinnesbeck.nilsmg4" minSdk 27 targetSdk 36 - versionCode 108 - versionName "2.19.9" + versionCode 109 + versionName "2.20.0" // 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. diff --git a/app/src/main/java/com/evsuite/abrp/LeaseAllowance.java b/app/src/main/java/com/evsuite/abrp/LeaseAllowance.java new file mode 100644 index 0000000..2aa5c56 --- /dev/null +++ b/app/src/main/java/com/evsuite/abrp/LeaseAllowance.java @@ -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); + } +} diff --git a/app/src/main/java/com/evsuite/abrp/MainActivity.java b/app/src/main/java/com/evsuite/abrp/MainActivity.java index 19889be..0ba1795 100644 --- a/app/src/main/java/com/evsuite/abrp/MainActivity.java +++ b/app/src/main/java/com/evsuite/abrp/MainActivity.java @@ -85,7 +85,7 @@ public class MainActivity extends AppCompatActivity { /** Used only on the overview-read thread. */ private EnergyTelemetryReader vehicleReader; private TextView vehSoc, vehRange, vehPower, vehSpeed, vehOdometer, - vehCharging, vehHvac, + vehCharging, vehHvac, vehLeaseAllowance, vehLeaseDifference, vehTireFl, vehTireFr, vehTireRl, vehTireRr, vehRangeTrip, vehRangeTripNote, vehRangeAverage, vehRangeAverageNote, vehUsageTrip, vehUsageTripNote, vehUsageAverage, vehUsageAverageNote, @@ -1243,6 +1243,8 @@ public class MainActivity extends AppCompatActivity { vehOdometer = vehiclePane.findViewById(R.id.vehicle_odometer); vehCharging = vehiclePane.findViewById(R.id.vehicle_charging); 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); vehTireFr = vehiclePane.findViewById(R.id.vehicle_tire_fr); 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) { try { vehSoc.setText(fmt(s.getSocPercent(), "%.0f %%")); vehRange.setText(fmt(s.getRangeKm(), "%.0f km")); vehPower.setText(fmt(s.getBatteryPowerKw(), "%.1f kW")); vehSpeed.setText(fmt(s.getSpeedKmh(), "%.0f km/h")); - vehOdometer.setText(fmt(s.getOdometerKm(), "%.0f km")); + vehOdometer.setText(fmt(s.getOdometerKm(), "%,.0f km")); + showLease(s.getOdometerKm()); vehHvac.setText(fmt(s.getClimate().getDriverTargetCelsius(), "%.0f °C")); vehCharging.setText(chargingLabel(s)); diff --git a/app/src/main/res/layout-w1000dp/pane_vehicle.xml b/app/src/main/res/layout-w1000dp/pane_vehicle.xml index c7a8966..340cd26 100644 --- a/app/src/main/res/layout-w1000dp/pane_vehicle.xml +++ b/app/src/main/res/layout-w1000dp/pane_vehicle.xml @@ -67,6 +67,14 @@ + + + + + + + + diff --git a/app/src/main/res/layout/pane_vehicle.xml b/app/src/main/res/layout/pane_vehicle.xml index 3245de9..5662991 100644 --- a/app/src/main/res/layout/pane_vehicle.xml +++ b/app/src/main/res/layout/pane_vehicle.xml @@ -63,6 +63,14 @@ + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 91711e6..95d428f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -48,6 +48,11 @@ Enter a value between 10 and 150 kWh Speed Odometer + Lease allowance today + Versus allowance + %1$s km over + %1$s km under + On target Battery Temperature Cabin Temperature Climate Setpoint diff --git a/app/src/test/java/com/evsuite/abrp/LeaseAllowanceTest.java b/app/src/test/java/com/evsuite/abrp/LeaseAllowanceTest.java new file mode 100644 index 0000000..fa648b4 --- /dev/null +++ b/app/src/test/java/com/evsuite/abrp/LeaseAllowanceTest.java @@ -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)); + } +}