Files
mg4/app/src/test/java/com/evsuite/abrp/ChargingStateTest.java
T
TCFDONMEZandClaude Opus 5 e9bfa1217b Meter DC charging only, on a monotonic clock
The charging session meter used the head unit's wall clock, which jumps when it
picks up GPS or NTP time. One forward jump mid-charge invented both kWh and
hours. The consumption counters already integrate on elapsedRealtime for exactly
this reason; the session meter now does too, and keeps wall-clock values only as
the start and end stamps a reader needs to place a session on a timeline.

Intervals longer than five minutes are no longer bridged. A head unit that slept
mid-charge came back and billed the whole gap at the last power reading, and a
session interrupted overnight reported the night as charging time. Reported
duration is metered charging time now, not the span from start to end.

Sessions are metered on DC alone, at or above 10 kW. AC charging on this car is
an overnight affair the head unit sleeps through, so its curve, duration and
energy were guesses either way. The charging screen says so rather than letting
the previous DC session's figures be read as the current AC charge.

Also:

- A daily consumption record uploaded after an offline spell carries the
  lifetime total as it stood at the close of that day. Every backlogged day used
  to be stamped with the current lifetime, which plots as a flat line and one
  cliff.
- The offline session queue is capped at 50. It is a single SharedPreferences
  string read into memory in full, and nothing drained it while the cloud
  endpoint was unconfigured.
- A long charge keeps the start of its curve: at 720 points the sample interval
  doubles and the curve is thinned, rather than dropping the oldest point.
- The cloud sync throttle runs on the monotonic clock, and catch-up sends up to
  10 days and 10 sessions per cycle rather than one and three.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-08 14:20:29 +03:00

92 lines
3.6 KiB
Java

package com.evsuite.abrp;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
public class ChargingStateTest {
@Test
public void vendorStatusIsConclusiveOnItsOwn() {
// The port says false and the power is unreadable: status still decides.
assertTrue(ChargingState.isCharging(1, Boolean.FALSE, null, null));
assertTrue(ChargingState.isCharging(10, Boolean.FALSE, null, null));
}
@Test
public void anIdleStatusDoesNotByItselfRuleChargingOut() {
// Status 0 with the cable in and power flowing in is still a charge; the vendor
// code is one signal among several, not a veto.
assertTrue(ChargingState.isCharging(0, Boolean.TRUE, -7f, 0f));
}
@Test
public void inboundPowerAtAStandstillIsACharge() {
// This is the case the old port-only test missed: plugged in, port reports false.
assertTrue(ChargingState.isCharging(null, Boolean.FALSE, -7f, 0f));
assertTrue(ChargingState.isCharging(null, null, -45f, 0f));
}
@Test
public void inboundPowerWhileMovingIsRegenerationNotCharging() {
assertFalse(ChargingState.isCharging(null, Boolean.FALSE, -20f, 60f));
assertFalse(ChargingState.isCharging(null, null, -20f, 60f));
}
@Test
public void thePortStillCountsWhenItDoesAnswer() {
assertTrue(ChargingState.isCharging(null, Boolean.TRUE, -7f, 0f));
}
@Test
public void outboundPowerIsNeverACharge() {
assertFalse(ChargingState.isCharging(null, Boolean.TRUE, 20f, 60f));
assertFalse(ChargingState.isCharging(0, Boolean.TRUE, 2f, 0f));
}
@Test
public void noiseAroundZeroIsNotACharge() {
assertFalse(ChargingState.isCharging(null, Boolean.TRUE, -0.1f, 0f));
assertFalse(ChargingState.isCharging(null, Boolean.TRUE, 0f, 0f));
}
@Test
public void unreadablePowerWithoutAStatusIsNotAssumedToBeACharge() {
assertFalse(ChargingState.isCharging(null, Boolean.TRUE, null, 0f));
assertFalse(ChargingState.isCharging(null, null, null, null));
}
@Test
public void unreadableSpeedWithInboundPowerFallsBackToCharging() {
// Conservative on purpose: without a speed we cannot tell a charge from regen, and
// treating it as a charge only suppresses a SOC delta. The opposite choice invents
// negative consumption, which is the bug this class was written to stop.
assertTrue(ChargingState.isCharging(null, null, -7f, null));
}
@Test
public void creepingSpeedIsStillAStandstill() {
// Same dead band the integrator uses, so the two cannot disagree about motion.
assertTrue(ChargingState.isCharging(null, null, -7f, 1f));
assertFalse(ChargingState.isCharging(null, null, -7f, 10f));
}
@Test
public void onlyDcSpeedChargingIsMetered() {
// The session meter runs on DC alone: 7 kW is a home charge the head unit sleeps
// through, so its curve and duration would be fiction.
assertFalse(ChargingState.isDcCharging(1, Boolean.TRUE, -7f, 0f));
assertTrue(ChargingState.isDcCharging(10, Boolean.TRUE, -50f, 0f));
assertTrue(ChargingState.isDcCharging(null, null, -10f, 0f));
}
@Test
public void dcMeteringNeedsARealPowerReading() {
// Unlike isCharging, which stays permissive so the consumption counters do not
// book a charge as a drive, this gates a measurement. No reading, no session.
assertFalse(ChargingState.isDcCharging(10, Boolean.TRUE, null, 0f));
assertFalse(ChargingState.isDcCharging(null, Boolean.TRUE, 20f, 0f));
}
}