The pages' scroll bars never faded on the car: a ScrollView shows its bar on every layout, and the live values lay the page out every two seconds. StableScrollView, the root of every page, now has none. A new "Energy since start" tile, left of "Usage since start", shows the net kWh taken from the pack since the trip began, recuperation counted back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
132 lines
5.8 KiB
Java
132 lines
5.8 KiB
Java
package com.evsuite.abrp;
|
|
|
|
import static org.junit.Assert.assertEquals;
|
|
import static org.junit.Assert.assertNotNull;
|
|
import static org.junit.Assert.assertNull;
|
|
import static org.junit.Assert.assertTrue;
|
|
|
|
import org.junit.Test;
|
|
|
|
/** Range estimates from power and speed — pure JVM. */
|
|
public class RangeEstimatorTest {
|
|
|
|
private static final double USABLE = 50.8;
|
|
|
|
@Test
|
|
public void remainingEnergyIsSocTimesUsableCapacity() {
|
|
assertEquals(25.4, RangeEstimator.remainingKwh(50f, USABLE), 1e-9);
|
|
assertEquals(50.8, RangeEstimator.remainingKwh(120f, USABLE), 1e-9); // clamped
|
|
assertNull(RangeEstimator.remainingKwh(null, USABLE));
|
|
}
|
|
|
|
@Test
|
|
public void sinceStartCoversTheWholeTripNotJustFiveMinutes() {
|
|
RangeEstimator r = new RangeEstimator();
|
|
// 10 minutes at 30 kW then 10 minutes at 10 kW, both at 100 km/h: 20 kWh/100 km overall.
|
|
for (long t = 0; t <= 600_000; t += 15_000) r.add(t, 30f, 100f);
|
|
for (long t = 615_000; t <= 1_200_000; t += 15_000) r.add(t, 10f, 100f);
|
|
RangeEstimator.Estimate trip = r.sinceStart(25.4);
|
|
assertNotNull(trip);
|
|
assertEquals(20.0, trip.kwhPer100Km, 0.2); // plus the 15 s step between the two
|
|
assertEquals(127.0, trip.rangeKm, 1.0);
|
|
assertEquals(33.3, trip.distanceKm, 0.1);
|
|
// The 5-minute average only sees the economical end.
|
|
assertEquals(10.0, r.average(25.4, 1_200_000).kwhPer100Km, 0.1);
|
|
}
|
|
|
|
@Test
|
|
public void standingWithTheClimateOnCountsTowardsTheTrip() {
|
|
RangeEstimator r = new RangeEstimator();
|
|
for (long t = 0; t <= 120_000; t += 15_000) r.add(t, 3f, 0f); // warming up
|
|
for (long t = 135_000; t <= 735_000; t += 15_000) r.add(t, 15f, 60f);
|
|
assertTrue(r.sinceStart(25.4).kwhPer100Km > 15.0);
|
|
}
|
|
|
|
@Test
|
|
public void aLongGapOrChargingStartsANewTrip() {
|
|
RangeEstimator r = new RangeEstimator();
|
|
for (long t = 0; t <= 600_000; t += 15_000) r.add(t, 30f, 100f);
|
|
long later = 600_000 + RangeEstimator.TRIP_RESET_GAP_MS + 1; // the car was off
|
|
for (long t = later; t <= later + 600_000; t += 15_000) r.add(t, 10f, 100f);
|
|
assertEquals(10.0, r.sinceStart(25.4).kwhPer100Km, 0.1);
|
|
|
|
long charge = later + 615_000;
|
|
r.add(charge, -7f, 0f); // plugged in
|
|
assertNull(r.sinceStart(25.4));
|
|
}
|
|
|
|
@Test
|
|
public void noSinceStartBeforeAnyDistanceOrWithoutSoc() {
|
|
RangeEstimator r = new RangeEstimator();
|
|
assertNull(r.sinceStart(25.4));
|
|
for (long t = 0; t <= 60_000; t += 15_000) r.add(t, 2f, 0f);
|
|
assertNull(r.sinceStart(25.4)); // not moved yet
|
|
for (long t = 75_000; t <= 300_000; t += 15_000) r.add(t, 15f, 60f);
|
|
assertNull(r.sinceStart(null));
|
|
}
|
|
|
|
@Test
|
|
public void averageIntegratesEnergyOverDistance() {
|
|
RangeEstimator r = new RangeEstimator();
|
|
// 4 minutes at a steady 20 kW and 100 km/h, sampled every 2 s: 20 kWh/100 km.
|
|
for (long t = 0; t <= 240_000; t += 2_000) r.add(t, 20f, 100f);
|
|
RangeEstimator.Estimate e = r.average(25.4, 240_000);
|
|
assertNotNull(e);
|
|
assertEquals(20.0, e.kwhPer100Km, 1e-6);
|
|
assertEquals(127.0, e.rangeKm, 1e-6);
|
|
}
|
|
|
|
@Test
|
|
public void averageMixesTheServiceAndScreenRates() {
|
|
RangeEstimator r = new RangeEstimator();
|
|
// A slow 15 s service tick for two minutes, then the 2 s screen tick: still 20 kWh/100 km.
|
|
for (long t = 0; t < 120_000; t += 15_000) r.add(t, 20f, 100f);
|
|
for (long t = 120_000; t <= 240_000; t += 2_000) r.add(t, 20f, 100f);
|
|
assertEquals(20.0, r.average(25.4, 240_000).kwhPer100Km, 1e-6);
|
|
}
|
|
|
|
@Test
|
|
public void averageOnlyCoversTheLastFiveMinutes() {
|
|
RangeEstimator r = new RangeEstimator();
|
|
for (long t = 0; t <= 300_000; t += 2_000) r.add(t, 40f, 100f); // thirsty start
|
|
for (long t = 302_000; t <= 900_000; t += 2_000) r.add(t, 10f, 100f); // then economical
|
|
assertEquals(10.0, r.average(25.4, 900_000).kwhPer100Km, 1e-6);
|
|
}
|
|
|
|
@Test
|
|
public void noAverageWithTooLittleDataOrDistanceOrAcrossAGap() {
|
|
RangeEstimator r = new RangeEstimator();
|
|
for (long t = 0; t <= 30_000; t += 2_000) r.add(t, 20f, 100f);
|
|
assertNull("30 s is too little", r.average(25.4, 30_000));
|
|
|
|
RangeEstimator parked = new RangeEstimator();
|
|
for (long t = 0; t <= 240_000; t += 2_000) parked.add(t, 1f, 0f);
|
|
assertNull("no distance", parked.average(25.4, 240_000));
|
|
|
|
RangeEstimator gap = new RangeEstimator();
|
|
gap.add(0, 20f, 100f);
|
|
gap.add(200_000, 20f, 100f); // the app was closed in between
|
|
assertNull("a gap is not integrated", gap.average(25.4, 200_000));
|
|
|
|
RangeEstimator downhill = new RangeEstimator();
|
|
for (long t = 0; t <= 240_000; t += 2_000) downhill.add(t, -5f, 80f);
|
|
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));
|
|
}
|
|
|
|
@Test
|
|
public void energySinceStartIsTheNetEnergyOfTheTrip() {
|
|
RangeEstimator r = new RangeEstimator();
|
|
assertNull(r.energySinceStart());
|
|
r.add(0, 3f, 0f);
|
|
assertEquals(0.0, r.energySinceStart(), 1e-9); // the trip has begun
|
|
for (long t = 15_000; t <= 600_000; t += 15_000) r.add(t, 30f, 100f);
|
|
// 10 minutes at 30 kW = 5 kWh, plus the first 15 s ramp from 3 kW.
|
|
assertEquals(5.0, r.energySinceStart(), 0.1);
|
|
for (long t = 615_000; t <= 735_000; t += 15_000) r.add(t, -20f, 60f); // downhill
|
|
assertTrue(r.energySinceStart() < 5.0); // recuperation counts back
|
|
}
|
|
}
|