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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
This commit is contained in:
2026-10-18 10:07:08 +02:00
co-authored by Claude Opus 5.5
parent 2a0c4c9675
commit 3ae1e18a43
6 changed files with 25 additions and 11 deletions
@@ -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);
}
@@ -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() {
+1
View File
@@ -38,6 +38,7 @@
<string name="range_consumption">%1$.1f kWh/100 km</string>
<string name="range_over">&gt; %1$d km</string>
<string name="range_waiting">needs a few minutes of driving</string>
<string name="range_recuperating">while recuperating</string>
<string name="range_trip_waiting">once the car has moved</string>
<string name="usage_over_distance">over %1$.1f km</string>
<string name="usage_last_5_min">over the last 5 minutes</string>
@@ -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));
}
}