From 3ae1e18a43351b6c43d55a9ef9f42a923b52695c Mon Sep 17 00:00:00 2001 From: Nils Stinnesbeck Date: Sun, 18 Oct 2026 10:07:08 +0200 Subject: [PATCH] App 2.10.1: show negative usage while recuperating Net regeneration over the window (or the trip) used to hide both estimates behind "needs a few minutes of driving". It is a real reading: the usage now shows as negative kWh/100 km, and the range as "> 999 km" with "while recuperating" beneath it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki --- SPECS.md | 2 ++ app/build.gradle | 4 ++-- .../java/com/evsuite/abrp/MainActivity.java | 2 +- .../java/com/evsuite/abrp/RangeEstimator.java | 22 +++++++++++++------ app/src/main/res/values/strings.xml | 1 + .../com/evsuite/abrp/RangeEstimatorTest.java | 5 ++++- 6 files changed, 25 insertions(+), 11 deletions(-) diff --git a/SPECS.md b/SPECS.md index d5e5754..42912a4 100644 --- a/SPECS.md +++ b/SPECS.md @@ -112,6 +112,8 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does * INGEST_TOKEN) into git and pushed it. Commit amended and force-pushed; both values regenerated in the now gitignored `k8s/secrets.yaml` (since moved to `k8s/secrets/secrets.yaml`, outside `kubectl apply -f k8s/`). Filled-in secrets live only there from now on. +- 2026-10-18 — App 2.10.1: net regeneration (recuperating downhill) shows as negative usage, and + the range as "> 999 km" with "while recuperating", instead of "needs a few minutes of driving". - 2026-10-18 — App 2.10.0: "Range since start" replaces the range at current power. It uses the consumption since the car was switched on, including standing with the climate on, and a new trip starts after 10 minutes without samples or when charging. Usage (kWh/100 km) for both diff --git a/app/build.gradle b/app/build.gradle index 02e8036..791eb08 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,8 +34,8 @@ android { applicationId "com.stinnesbeck.nilsmg4" minSdk 27 targetSdk 36 - versionCode 70 - versionName "2.10.0" + versionCode 71 + versionName "2.10.1" // 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/MainActivity.java b/app/src/main/java/com/evsuite/abrp/MainActivity.java index 883dbe4..6b36f54 100644 --- a/app/src/main/java/com/evsuite/abrp/MainActivity.java +++ b/app/src/main/java/com/evsuite/abrp/MainActivity.java @@ -1041,7 +1041,7 @@ public class MainActivity extends AppCompatActivity { // Coasting at speed costs almost nothing, which would read as thousands of km. range.setText(e.rangeKm > 999 ? getString(R.string.range_over, 999) : String.format(java.util.Locale.US, "%.0f km", e.rangeKm)); - rangeNote.setText(""); + rangeNote.setText(e.kwhPer100Km <= 0 ? getString(R.string.range_recuperating) : ""); usage.setText(getString(R.string.range_consumption, e.kwhPer100Km)); usageNote.setText(basis); } diff --git a/app/src/main/java/com/evsuite/abrp/RangeEstimator.java b/app/src/main/java/com/evsuite/abrp/RangeEstimator.java index 58af824..c3aa60d 100644 --- a/app/src/main/java/com/evsuite/abrp/RangeEstimator.java +++ b/app/src/main/java/com/evsuite/abrp/RangeEstimator.java @@ -15,8 +15,8 @@ import java.util.Iterator; * a new trip; so does charging. * * Remaining energy is SOC x usable capacity: the car publishes no pack energy, so the capacity - * is a setting. Every estimate is null when it cannot mean anything — standing still, - * regenerating, too little data — rather than a misleading number. + * is a setting. An estimate is null when it cannot mean anything — not moved yet, too little + * data — rather than a misleading number. Net regeneration is shown as it is: negative usage. * * Fed by both the service tick (15 s) and the open Vehicle page (2 s); the integration is * time-weighted, so the two rates mix correctly. Times are on the monotonic clock @@ -112,9 +112,8 @@ final class RangeEstimator { /** Range at the consumption since start; null until the trip has some distance behind it. */ synchronized Estimate sinceStart(Double remainingKwh) { - if (remainingKwh == null || tripDistanceKm < MIN_DISTANCE_KM || tripEnergyKwh <= 0) return null; - double kwhPerKm = tripEnergyKwh / tripDistanceKm; - return new Estimate(remainingKwh / kwhPerKm, kwhPerKm * 100, tripDistanceKm); + if (remainingKwh == null || tripDistanceKm < MIN_DISTANCE_KM) return null; + return estimate(remainingKwh, tripEnergyKwh, tripDistanceKm); } synchronized Estimate average(Double remainingKwh, long nowMs) { @@ -136,9 +135,18 @@ final class RangeEstimator { } previous = s; } - if (covered < MIN_WINDOW_MS || distanceKm < MIN_DISTANCE_KM || energyKwh <= 0) return null; + if (covered < MIN_WINDOW_MS || distanceKm < MIN_DISTANCE_KM) return null; + return estimate(remainingKwh, energyKwh, distanceKm); + } + + /** + * Net regeneration (downhill, recuperating) is a real reading: the usage is negative, and + * the range is unbounded rather than negative. + */ + private static Estimate estimate(double remainingKwh, double energyKwh, double distanceKm) { double kwhPerKm = energyKwh / distanceKm; - return new Estimate(remainingKwh / kwhPerKm, kwhPerKm * 100, distanceKm); + double rangeKm = kwhPerKm > 0 ? remainingKwh / kwhPerKm : Double.POSITIVE_INFINITY; + return new Estimate(rangeKm, kwhPerKm * 100, distanceKm); } synchronized void clear() { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d96f844..26b8b1c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -38,6 +38,7 @@ %1$.1f kWh/100 km > %1$d km needs a few minutes of driving + while recuperating once the car has moved over %1$.1f km over the last 5 minutes diff --git a/app/src/test/java/com/evsuite/abrp/RangeEstimatorTest.java b/app/src/test/java/com/evsuite/abrp/RangeEstimatorTest.java index 50104c0..cb442da 100644 --- a/app/src/test/java/com/evsuite/abrp/RangeEstimatorTest.java +++ b/app/src/test/java/com/evsuite/abrp/RangeEstimatorTest.java @@ -110,6 +110,9 @@ public class RangeEstimatorTest { RangeEstimator downhill = new RangeEstimator(); for (long t = 0; t <= 240_000; t += 2_000) downhill.add(t, -5f, 80f); - assertNull("net regeneration", downhill.average(25.4, 240_000)); + RangeEstimator.Estimate regen = downhill.average(25.4, 240_000); + assertNotNull("net regeneration is a reading", regen); + assertEquals(-6.25, regen.kwhPer100Km, 0.01); + assertTrue(Double.isInfinite(regen.rangeKm)); } }