Files
mg4/app/src/test/java/com/evsuite/abrp/TelemetryPayloadTest.java
T
nilsandClaude Opus 5.5 29064cf8db Translate the remaining French and Turkish
mise.toml, the comments in app/build.gradle (corrected where they no
longer described the build: the car build is signed by build-car, and
the updater reads the version from build-car's file name), OTA.md's
message table (now the updater's current English messages), the Turkish
messages quoted in CHANGELOG.md and OTA.md (glossed in English), two
test assertion messages, an icon comment, and the ignition-state comment
in EVHardware.kt. The vendored EVHardware library still has about 120
half-French comment lines; noted in SPECS.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
2026-10-18 17:45:56 +02:00

356 lines
13 KiB
Java

package com.evsuite.abrp;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.json.JSONObject;
import org.junit.Test;
/**
* [T-913] Telemetry payload construction — pure JVM, no car and no Android needed.
*
* The rule under test: a property that could not be read is OMITTED. Emitting 0 instead
* tells ABRP the car really is at 0 % / 0 km/h and corrupts the user's route plan.
*/
public class TelemetryPayloadTest {
/** Everything readable, no location. */
private TelemetryPayload fullCar() {
TelemetryPayload t = new TelemetryPayload(1_700_000_000L);
t.soc = 63;
t.speedKmh = 48.4f;
t.rangeKm = 210;
t.powerKw = -14.5f;
t.charging = false;
t.dcfc = false;
t.parked = false;
t.cabinTemp = 21.5f;
return t;
}
/** Nothing readable at all — the car link is down. */
private String noCar() {
return new TelemetryPayload(1_700_000_000L).build();
}
@Test
public void utcIsAlwaysPresent() throws Exception {
JSONObject json = new JSONObject(noCar());
assertEquals(1, json.length());
assertEquals(1_700_000_000L, json.getLong("utc"));
}
@Test
public void unreadablePropertiesAreOmittedNotZeroed() {
String json = noCar();
for (String field : new String[]{
"soc", "speed", "est_battery_range", "power",
"is_charging", "is_dcfc", "is_parked", "cabin_temp",
"batt_temp", "capacity", "odometer", "hvac_setpoint",
"tire_pressure_fl", "tire_pressure_fr",
"tire_pressure_rl", "tire_pressure_rr"}) {
assertFalse("field " + field + " should have been omitted", json.contains(field));
}
}
@Test
public void aSingleUnreadablePropertyDoesNotSuppressTheOthers() throws Exception {
// SOC throws (null) but speed reads fine: speed must still be sent.
TelemetryPayload t = new TelemetryPayload(1L);
t.speedKmh = 48.4f;
JSONObject json = new JSONObject(t.build());
assertFalse("an unreadable soc should have been omitted", json.has("soc"));
assertEquals(48, json.getInt("speed"));
}
@Test
public void aGenuineZeroIsStillSent() throws Exception {
// 0 % battery is a real reading and must not be confused with "unreadable".
TelemetryPayload t = new TelemetryPayload(1L);
t.soc = 0;
t.speedKmh = 0f;
t.rangeKm = 0;
JSONObject json = new JSONObject(t.build());
assertEquals(0, json.getInt("soc"));
assertEquals(0, json.getInt("speed"));
assertEquals(0, json.getInt("est_battery_range"));
}
@Test
public void readablePropertiesAreEmitted() throws Exception {
JSONObject json = new JSONObject(fullCar().build());
assertEquals(63, json.getInt("soc"));
assertEquals(48, json.getInt("speed")); // rounded
assertEquals(210, json.getInt("est_battery_range"));
assertEquals(-14.5, json.getDouble("power"), 0.001);
assertEquals(0, json.getInt("is_charging"));
assertEquals(0, json.getInt("is_dcfc"));
assertEquals(0, json.getInt("is_parked"));
}
@Test
public void powerUsesDotDecimalSeparatorRegardlessOfDefaultLocale() throws Exception {
java.util.Locale previous = java.util.Locale.getDefault();
try {
java.util.Locale.setDefault(java.util.Locale.FRANCE); // comma locale
assertTrue(fullCar().build().contains("-14.5"));
} finally {
java.util.Locale.setDefault(previous);
}
}
@Test
public void implausibleCabinTempIsOmitted() {
// 0.0 means the property is unsupported on this VHAL.
TelemetryPayload zero = new TelemetryPayload(1L);
zero.cabinTemp = 0f;
assertFalse(zero.build().contains("cabin_temp"));
TelemetryPayload hot = new TelemetryPayload(1L);
hot.cabinTemp = 120f;
assertFalse(hot.build().contains("cabin_temp"));
}
@Test
public void plausibleCabinTempIsSent() throws Exception {
assertEquals(22, new JSONObject(fullCar().build()).getInt("cabin_temp")); // 21.5 rounds up
}
// ---------- Fields added for the ABRP lower-priority set ----------
@Test
public void batteryTemperatureIsSentWhenPlausible() throws Exception {
TelemetryPayload t = new TelemetryPayload(1L);
t.battTempC = 24.6f;
assertEquals(25, new JSONObject(t.build()).getInt("batt_temp"));
}
@Test
public void unsupportedBatteryTemperatureIsOmitted() {
// A VHAL without the property answers 0.0; "pack at 0 °C" changes ABRP's plan.
TelemetryPayload t = new TelemetryPayload(1L);
t.battTempC = 0f;
assertFalse(t.build().contains("batt_temp"));
}
@Test
public void capacityIsSentInKwhWithTwoDecimals() throws Exception {
TelemetryPayload t = new TelemetryPayload(1L);
t.capacityKwh = 61.734f;
assertEquals(61.73, new JSONObject(t.build()).getDouble("capacity"), 0.001);
}
@Test
public void zeroCapacityIsOmitted() {
TelemetryPayload t = new TelemetryPayload(1L);
t.capacityKwh = 0f;
assertFalse(t.build().contains("capacity"));
}
@Test
public void odometerIsRoundedToWholeKm() throws Exception {
TelemetryPayload t = new TelemetryPayload(1L);
t.odometerKm = 24518.7f;
assertEquals(24519, new JSONObject(t.build()).getInt("odometer"));
}
@Test
public void zeroOdometerIsOmitted() {
TelemetryPayload t = new TelemetryPayload(1L);
t.odometerKm = 0f;
assertFalse(t.build().contains("odometer"));
}
@Test
public void hvacSetpointIsSentWhenSet() throws Exception {
TelemetryPayload t = new TelemetryPayload(1L);
t.hvacSetpointC = 21f;
assertEquals(21, new JSONObject(t.build()).getInt("hvac_setpoint"));
}
@Test
public void unsupportedHvacSetpointIsOmitted() {
TelemetryPayload t = new TelemetryPayload(1L);
t.hvacSetpointC = 0f;
assertFalse(t.build().contains("hvac_setpoint"));
}
@Test
public void stateOfEnergyIsSentInKwh() throws Exception {
TelemetryPayload t = fullCar();
t.soeKwh = 41.256f;
assertEquals(41.26, new JSONObject(t.build()).getDouble("soe"), 0.001);
}
@Test
public void unsupportedStateOfEnergyIsOmitted() {
TelemetryPayload t = fullCar();
t.soeKwh = 0f;
assertFalse(t.build().contains("soe"));
}
@Test
public void chargedEnergyIsSentIncludingZeroAtSessionStart() throws Exception {
TelemetryPayload started = fullCar();
started.kwhCharged = 0.0;
assertEquals(0.0, new JSONObject(started.build()).getDouble("kwh_charged"), 0.001);
TelemetryPayload running = fullCar();
running.kwhCharged = 12.34;
assertEquals(12.34, new JSONObject(running.build()).getDouble("kwh_charged"), 0.001);
}
@Test
public void chargedEnergyIsOmittedOutsideACharge() {
assertFalse(fullCar().build().contains("kwh_charged"));
}
@Test
public void tyrePressuresAreSentPerWheelInKpa() throws Exception {
TelemetryPayload t = new TelemetryPayload(1L);
t.tirePressureFlKpa = 240f;
t.tirePressureFrKpa = 241.4f;
t.tirePressureRlKpa = 250.6f;
t.tirePressureRrKpa = 249f;
JSONObject json = new JSONObject(t.build());
assertEquals(240, json.getInt("tire_pressure_fl"));
assertEquals(241, json.getInt("tire_pressure_fr"));
assertEquals(251, json.getInt("tire_pressure_rl"));
assertEquals(249, json.getInt("tire_pressure_rr"));
}
@Test
public void aWheelWithoutASensorIsOmittedWithoutHidingTheOthers() throws Exception {
// 0 kPa is not a flat tyre, it is a missing sensor: a flat still reads well above 0.
TelemetryPayload t = new TelemetryPayload(1L);
t.tirePressureFlKpa = 0f;
t.tirePressureFrKpa = 240f;
JSONObject json = new JSONObject(t.build());
assertFalse(json.has("tire_pressure_fl"));
assertEquals(240, json.getInt("tire_pressure_fr"));
}
// ---------- Location ----------
@Test
public void locationIsIndependentOfTheCarLink() throws Exception {
// No car data at all, but a GPS fix: ABRP should still see the vehicle online.
TelemetryPayload t = new TelemetryPayload(1L);
t.lat = 48.85;
t.lon = 2.35;
JSONObject json = new JSONObject(t.build());
assertEquals(48.85, json.getDouble("lat"), 0.0001);
assertEquals(2.35, json.getDouble("lon"), 0.0001);
assertFalse(json.has("soc"));
}
@Test
public void missingLocationOmitsAllLocationFields() {
String json = fullCar().build();
assertFalse(json.contains("lat"));
assertFalse(json.contains("lon"));
assertFalse(json.contains("elevation"));
assertFalse(json.contains("heading"));
}
@Test
public void elevationAndHeadingAreOptional() throws Exception {
TelemetryPayload flat = new TelemetryPayload(1L);
flat.lat = 48.85;
flat.lon = 2.35;
JSONObject flatJson = new JSONObject(flat.build());
assertFalse(flatJson.has("elevation"));
assertFalse(flatJson.has("heading"));
TelemetryPayload full = new TelemetryPayload(1L);
full.lat = 48.85;
full.lon = 2.35;
full.elevation = 35.4;
full.heading = 271.6f;
JSONObject fullJson = new JSONObject(full.build());
assertEquals(35, fullJson.getInt("elevation"));
assertEquals(272, fullJson.getInt("heading"));
}
@Test
public void booleanFlagsAreEmittedAsIntegers() throws Exception {
TelemetryPayload t = new TelemetryPayload(1L);
t.soc = 50;
t.powerKw = 62f;
t.charging = true;
t.dcfc = true;
t.parked = true;
JSONObject json = new JSONObject(t.build());
assertEquals(1, json.getInt("is_charging"));
assertEquals(1, json.getInt("is_dcfc"));
assertEquals(1, json.getInt("is_parked"));
}
@Test
public void thePayloadIsValidJson() throws Exception {
// The hand-built string could not guarantee this; JSONObject can.
new JSONObject(fullCar().build());
new JSONObject(noCar());
}
@Test
public void summaryListsSentValuesAndOmittedFields() {
String summary = TelemetryPayload.summarize(fullCar().build());
assertTrue(summary, summary.contains("soc=63"));
assertTrue(summary, summary.contains("cabin_temp=22"));
// Nothing gave us a position, so lat/lon must show up as omitted, not as 0.
assertFalse(summary, summary.contains("lat="));
assertTrue(summary, summary.contains("| omitted: "));
assertTrue(summary, summary.substring(summary.indexOf("| omitted: ")).contains("lat"));
}
@Test
public void summaryOfAnEmptyPayloadReportsEverythingOmitted() {
String summary = TelemetryPayload.summarize(noCar());
assertTrue(summary, summary.startsWith("utc=1700000000"));
assertTrue(summary, summary.contains("| omitted: soc,speed,"));
}
// ---------- The server's ranges (server/model.go) ----------
@Test
public void reversingSendsSpeedAsAMagnitude() throws Exception {
TelemetryPayload t = new TelemetryPayload(1_700_000_000L);
t.speedKmh = -4.2f;
assertEquals(4, new JSONObject(t.build()).getInt("speed"));
}
@Test
public void valuesOutsideTheServersRangeAreLeftOut() throws Exception {
TelemetryPayload t = new TelemetryPayload(1_700_000_000L);
t.soc = 55;
t.extTempC = 72f; // plausible to the car's guard, above the server's 70
t.hvacSetpointC = 50f; // a HI sentinel, above the server's 40
t.rangeKm = 2500;
JSONObject json = new JSONObject(t.build());
assertEquals(55, json.getInt("soc"));
assertFalse(json.has("ext_temp"));
assertFalse(json.has("hvac_setpoint"));
assertFalse(json.has("est_battery_range"));
}
@Test
public void theAppsLimitsMatchEveryNumericServerField() {
String[] serverFields = {"soc", "speed", "power", "est_battery_range", "capacity", "soe",
"kwh_charged", "odometer", "lat", "lon", "elevation", "heading", "batt_temp",
"cabin_temp", "ext_temp", "hvac_setpoint", "voltage", "current",
"tire_pressure_fl", "tire_pressure_fr", "tire_pressure_rl", "tire_pressure_rr"};
for (String field : serverFields) {
assertTrue(field, TelemetryPayload.SERVER_LIMITS.containsKey(field));
}
}
@Test
public void aClockBefore2001IsNotATime() {
assertFalse(TelemetryPayload.isPlausibleUtc(0L));
assertFalse(TelemetryPayload.isPlausibleUtc(999_999_999L));
assertTrue(TelemetryPayload.isPlausibleUtc(1_790_000_000L));
}
}