Skip to content

Commit 0f071ba

Browse files
committed
add screen and capture for statistics
1 parent b6d6099 commit 0f071ba

6 files changed

Lines changed: 76 additions & 8 deletions

File tree

‎app/src/main/java/com/mg4/control/hardware/EnergyProbe.kt‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ object EnergyProbe {
103103
// ── Distance ────────────────────────────────────────────────────────
104104
Signal("Odomètre AOSP", 0x11600204, null),
105105
Signal("Kilométrage total SAIC", 0x21401566, null),
106+
// Candidats à un odomètre plus fin que le kilomètre entier : le premier est dans la liste
107+
// énumérée par le gestionnaire BMS, les deux autres viennent du combiné.
108+
Signal("Odomètre BMS", 0x2140f447, BMS_SERVICE, validity = 0x2120f448),
109+
Signal("Odomètre combiné", 0x21407b13, null),
110+
Signal("Trajet combiné : départ", 0x21407b86, null),
106111
Signal("Trajet combiné : distance", 0x21407b81, null),
107112
Signal("Trajet combiné : vitesse moyenne", 0x21407b80, null),
108113
Signal("Trajet combiné : conso moyenne", 0x21407b82, null),

‎app/src/main/java/com/mg4/control/model/StatsRecords.kt‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ data class Trip(
1313
val startMs: Long,
1414
val endMs: Long,
1515
val distanceKm: Int,
16+
/**
17+
* Énergie **brute** relevée sur le compteur du véhicule : la régénération n'en est PAS déduite.
18+
*
19+
* Vérifié le 2026-09-20 en comparant avec l'écran d'origine — la voiture affichait 14,1
20+
* kWh/100 km là où ce compteur donnait 17,1 sur la même distance, l'écart valant exactement la
21+
* régénération. C'est donc [netEnergyKwh] qu'il faut montrer et facturer.
22+
*/
1623
val energyKwh: Float,
1724
val climateKwh: Float?,
1825
val accessoriesKwh: Float?,
@@ -23,6 +30,15 @@ data class Trip(
2330
) {
2431
val durationMs: Long get() = max(0L, endMs - startMs)
2532

33+
/**
34+
* Énergie réellement sortie de la batterie : brute moins ce que la régénération a rendu.
35+
*
36+
* C'est la convention de tout le monde — la voiture elle-même, Tesla, les planificateurs — et
37+
* c'est la seule qui ait un sens pour le coût : on ne recharge que ce qu'on a vraiment dépensé.
38+
*/
39+
val netEnergyKwh: Float
40+
get() = (energyKwh - (regenKwh ?: 0f)).coerceAtLeast(0f).roundTenth()
41+
2642
/**
2743
* Consommation moyenne en kWh/100 km, **null sous la distance plancher**.
2844
*
@@ -32,7 +48,7 @@ data class Trip(
3248
*/
3349
val consumptionPer100: Float?
3450
get() = if (distanceKm >= MIN_DISTANCE_FOR_RATIO_KM && distanceKm > 0)
35-
energyKwh * 100f / distanceKm else null
51+
netEnergyKwh * 100f / distanceKm else null
3652

3753
/**
3854
* Énergie du moteur : ce qui reste du total une fois la climatisation et les accessoires
@@ -45,6 +61,7 @@ data class Trip(
4561
val motorKwh: Float?
4662
get() {
4763
if (climateKwh == null && accessoriesKwh == null) return null
64+
// Sur le BRUT : moteur + climatisation + accessoires − régénération = énergie nette.
4865
val reste = energyKwh - (climateKwh ?: 0f) - (accessoriesKwh ?: 0f)
4966
return reste.coerceAtLeast(0f).roundTenth()
5067
}

‎app/src/main/java/com/mg4/control/model/StatsSummary.kt‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ data class StatsSummary(
5151
val chargeCost = charges.sumOf { (it.cost(settings) ?: 0f).toDouble() }.toFloat()
5252
// Prix moyen réel : une session corrigée à la main tire donc la moyenne avec elle.
5353
val prixMoyen = if (chargedKwh > 0f) chargeCost / chargedKwh else null
54-
val energie = trips.sumOf { it.energyKwh.toDouble() }.toFloat()
54+
// Net, comme l'affichage d'origine : le compteur du véhicule est brut, régénération
55+
// comprise. Voir [Trip.energyKwh].
56+
val energie = trips.sumOf { it.netEnergyKwh.toDouble() }.toFloat()
5557
return StatsSummary(
5658
tripCount = trips.size,
5759
distanceKm = trips.sumOf { it.distanceKm },

‎app/src/main/java/com/mg4/control/ui/StatsFragment.kt‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ class StatsFragment : Fragment() {
231231
value1 = trip.consumptionPer100?.let { fmt(it) + " " + getString(R.string.stats_unit_per100) } ?: "—",
232232
value2 = listOfNotNull(
233233
trip.averageSpeedKmh?.let { "${it.toInt()} km/h" },
234-
kwh(trip.energyKwh),
234+
kwh(trip.netEnergyKwh),
235235
).joinToString(" · "),
236236
onClick = { expanded = if (expanded == trip.startMs) null else trip.startMs; render() }))
237237
if (expanded == trip.startMs) list.addView(tripDetail(ctx, trip))
@@ -292,7 +292,9 @@ class StatsFragment : Fragment() {
292292
trip.motorKwh?.let { getString(R.string.stats_detail_motor) to kwh(it) },
293293
trip.climateKwh?.let { getString(R.string.stats_detail_climate) to kwh(it) },
294294
trip.accessoriesKwh?.let { getString(R.string.stats_detail_accessories) to kwh(it) },
295-
trip.regenKwh?.let { getString(R.string.stats_detail_regen) to "+ ${kwh(it)}" },
295+
// Signe négatif : dans un détail de consommation, la régénération RETRANCHE. Moteur +
296+
// climatisation + accessoires − récupération donne bien le total affiché sur la ligne.
297+
trip.regenKwh?.let { getString(R.string.stats_detail_regen) to "− ${kwh(it)}" },
296298
// Sous la distance plancher on dit POURQUOI il n'y a pas de ratio, plutôt qu'un tiret muet.
297299
getString(R.string.stats_detail_consumption) to (trip.consumptionPer100
298300
?.let { fmt(it) + " " + getString(R.string.stats_unit_per100) }
@@ -586,10 +588,15 @@ class StatsFragment : Fragment() {
586588
private fun money(value: Float?, s: StatsSettings): String =
587589
value?.let { "${fmt(it)} ${s.currency}" } ?: "—"
588590

591+
/**
592+
* Le véhicule publie le pourcentage au dixième (69,4 et non 69) : l'arrondir à l'entier jetait
593+
* une précision déjà acquise, et rendait invérifiable un trajet court — 1 % vaut plus d'un demi
594+
* kilowattheure sur cette batterie.
595+
*/
589596
private fun soc(start: Float?, end: Float?): String? {
590597
if (start == null && end == null) return null
591-
val a = start?.let { "${it.toInt()} %" } ?: "?"
592-
val b = end?.let { "${it.toInt()} %" } ?: "?"
598+
val a = start?.let { "${fmt(it)} %" } ?: "?"
599+
val b = end?.let { "${fmt(it)} %" } ?: "?"
593600
return "$a → $b"
594601
}
595602

‎app/src/test/java/com/mg4/control/model/StatsSummaryTest.kt‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ class StatsSummaryTest {
1616

1717
private val settings = StatsSettings(priceAc = 0.2f, priceDc = 0.5f, capacityKwh = 60f)
1818

19-
private fun trip(km: Int, kwh: Float, dureeMin: Long = 30) = Trip(
19+
private fun trip(km: Int, kwh: Float, dureeMin: Long = 30, regen: Float = 0f) = Trip(
2020
startMs = 0L,
2121
endMs = dureeMin * 60_000L,
2222
distanceKm = km,
2323
energyKwh = kwh,
2424
climateKwh = null,
2525
accessoriesKwh = null,
26-
regenKwh = 1f,
26+
regenKwh = regen,
2727
socStart = null,
2828
socEnd = null,
2929
outsideTempC = null,
@@ -57,6 +57,15 @@ class StatsSummaryTest {
5757
assertNull(sum.costPer100)
5858
}
5959

60+
@Test
61+
fun `le resume compte l'energie NETTE des trajets`() {
62+
// Le compteur du véhicule est brut : 20 kWh dont 4 rendus par la régénération font 16 nets.
63+
val sum = StatsSummary.of(listOf(trip(100, 20f, regen = 4f)), emptyList(), settings)
64+
assertEquals(16f, sum.energyKwh, 0.01f)
65+
assertEquals(16f, sum.consumptionPer100!!, 0.01f)
66+
assertEquals("la régénération reste affichée à part", 4f, sum.regenKwh, 0.01f)
67+
}
68+
6069
@Test
6170
fun `le prix moyen suit l'energie reellement rechargee`() {
6271
val sum = StatsSummary.of(

‎app/src/test/java/com/mg4/control/model/TripTest.kt‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,24 @@ class TripTest {
5252
assertEquals(0f, trip(kwh = 0.4f, clim = 0.5f, accessoires = 0.5f).motorKwh!!, 0.01f)
5353
}
5454

55+
@Test
56+
fun `l'energie nette retranche la regeneration`() {
57+
val t = Trip(
58+
startMs = 0L, endMs = 2_040_000L, distanceKm = 34, energyKwh = 5.8f,
59+
climateKwh = 0f, accessoriesKwh = 0.1f, regenKwh = 1f,
60+
socStart = 69f, socEnd = 60f, outsideTempC = 25f,
61+
)
62+
assertEquals(4.8f, t.netEnergyKwh, 0.01f)
63+
// Relevé réel du 2026-09-20 : la voiture affichait 14,1 kWh/100 km sur ce trajet, quand le
64+
// compteur brut donnait 17,1. Notre calcul doit tomber du côté de la voiture.
65+
assertEquals(14.1f, t.consumptionPer100!!, 0.4f)
66+
}
67+
68+
@Test
69+
fun `sans regeneration connue le net vaut le brut`() {
70+
assertEquals(20f, trip(kwh = 20f).netEnergyKwh, 0.01f)
71+
}
72+
5573
@Test
5674
fun `sous cinq kilometres aucun ratio n'est annonce`() {
5775
val court = trip(km = 2, kwh = 0.4f)
@@ -63,4 +81,14 @@ class TripTest {
6381
fun `au dela du plancher la consommation se calcule`() {
6482
assertEquals(20f, trip(km = 100, kwh = 20f).consumptionPer100!!, 0.01f)
6583
}
84+
85+
@Test
86+
fun `la consommation suit le net, pas le brut`() {
87+
val t = Trip(
88+
startMs = 0L, endMs = 3_600_000L, distanceKm = 100, energyKwh = 20f,
89+
climateKwh = null, accessoriesKwh = null, regenKwh = 4f,
90+
socStart = null, socEnd = null, outsideTempC = null,
91+
)
92+
assertEquals(16f, t.consumptionPer100!!, 0.01f)
93+
}
6694
}

0 commit comments

Comments
 (0)