Skip to content

Commit 04b10b7

Browse files
committed
feat(dashboard): show consumption readings
1 parent 0f0524c commit 04b10b7

8 files changed

Lines changed: 76 additions & 10 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ All notable changes follow [Keep a Changelog](https://keepachangelog.com/en/1.1.
66

77
### Added
88

9+
- **The dashboard now separates immediate and trip consumption without competing with SOC.**
10+
Instant kWh/100 km uses EVHardware's five-second smoothing and stays unavailable below
11+
5 km/h; regeneration remains negative. The adjacent trip average keeps the accumulator's
12+
integrated consumed-energy/distance definition, and both numbers are labelled as arithmetic
13+
derived from vehicle readings rather than measurements or predictions.
14+
915
- **Completed trips can now leave the head unit without granting broad storage access.** The
1016
ledger exports either one trip or all retained trips as a documented CSV summary or a
1117
schema-versioned JSON document with sample tracks. Files are bounded, written atomically to

‎app/src/main/java/com/evsuite/chargepilot/DashboardReadings.kt‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ data class DashboardReadings(
2222
val power: Provenanced<Float>,
2323
val outsideTemp: Provenanced<Float>,
2424
val batteryTemp: Provenanced<Float>,
25+
val instantConsumption: Provenanced<Double>,
2526
val tripDuration: Provenanced<Long>,
2627
val tripDistance: Provenanced<Double>,
2728
val tripEnergy: Provenanced<Double>,
@@ -34,13 +35,17 @@ data class DashboardReadings(
3435
fun <T : Any> gap() = Provenanced.unavailable<T>(UnavailableReason.SIGNAL_ABSENT)
3536
return DashboardReadings(
3637
soc = gap(), range = gap(), speed = gap(), power = gap(),
37-
outsideTemp = gap(), batteryTemp = gap(),
38+
outsideTemp = gap(), batteryTemp = gap(), instantConsumption = gap(),
3839
tripDuration = gap(), tripDistance = gap(), tripEnergy = gap(),
3940
tripRegen = gap(), tripConsumption = gap(),
4041
)
4142
}
4243

43-
fun of(snapshot: EnergySnapshot, trip: EnergyTripSummary?): DashboardReadings {
44+
fun of(
45+
snapshot: EnergySnapshot,
46+
trip: EnergyTripSummary?,
47+
instantConsumption: Provenanced<Double>,
48+
): DashboardReadings {
4449
// An unrecognised generation is not a car that stopped publishing: it is a car
4550
// this build was never taught to read. The screen says which.
4651
val absent = if (snapshot.firmware == FirmwareInfo.Gen.UNKNOWN) {
@@ -55,11 +60,12 @@ data class DashboardReadings(
5560
power = Provenanced.measured(snapshot.batteryPowerKw, absent),
5661
outsideTemp = Provenanced.measured(snapshot.outsideTempCelsius, absent),
5762
batteryTemp = Provenanced.measured(snapshot.batteryTempCelsius, absent),
63+
instantConsumption = instantConsumption,
5864
// The trip figures are arithmetic over those readings, so they are derived,
5965
// and they are missing for a different reason: no trip is being recorded, or
6066
// no usable interval has accumulated yet.
6167
tripDuration = Provenanced.derived(trip?.durationMs),
62-
tripDistance = Provenanced.derived(trip?.distanceKm),
68+
tripDistance = Provenanced.derived(trip?.recordedDistanceKm),
6369
tripEnergy = Provenanced.derived(trip?.consumedKwh),
6470
tripRegen = Provenanced.derived(trip?.regeneratedKwh),
6571
tripConsumption = Provenanced.derived(trip?.averageConsumptionKwhPer100Km),

‎app/src/main/java/com/evsuite/chargepilot/MainActivity.kt‎

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,19 @@ import com.evsuite.hardware.AppLogger
1414
import com.evsuite.hardware.EVHardware
1515
import com.evsuite.hardware.FirmwareInfo
1616
import com.evsuite.hardware.diag.CrashLogger
17+
import com.evsuite.hardware.telemetry.ConsumptionCalculator
1718
import com.evsuite.hardware.telemetry.EnergySnapshot
1819
import com.evsuite.hardware.telemetry.EnergyTripSession
1920
import com.evsuite.hardware.telemetry.Provenanced
21+
import com.evsuite.hardware.telemetry.UnavailableReason
2022
import java.util.Locale
2123
import java.util.concurrent.Executors
2224

2325
class MainActivity : AppCompatActivity() {
2426

2527
private lateinit var binding: ActivityMainBinding
2628
private lateinit var provenance: ProvenanceText
29+
private val consumption = ConsumptionCalculator()
2730
/** Ten binder reads and a disk read for the diagnostics report; not the main thread's work. */
2831
private val background = Executors.newSingleThreadExecutor { runnable ->
2932
Thread(runnable, "chargepilot-diagnostics")
@@ -105,7 +108,17 @@ class MainActivity : AppCompatActivity() {
105108
}
106109

107110
private fun render(value: EnergySnapshot) {
108-
val readings = DashboardReadings.of(value, EnergyTripSession.current(value.timestampMs))
111+
val missingReason = if (value.firmware == FirmwareInfo.Gen.UNKNOWN) {
112+
UnavailableReason.UNSUPPORTED_FIRMWARE
113+
} else {
114+
UnavailableReason.SIGNAL_ABSENT
115+
}
116+
val consumptionReading = consumption.add(value, missingReason)
117+
val readings = DashboardReadings.of(
118+
value,
119+
EnergyTripSession.current(value.timestampMs),
120+
consumptionReading.smoothedInstantaneous,
121+
)
109122
latestReadings = readings
110123
renderReadings(readings)
111124
binding.climateValue.text = climate(value)
@@ -138,11 +151,17 @@ class MainActivity : AppCompatActivity() {
138151
bind(binding.powerValue, R.string.label_power, readings.power, PATTERN_POWER, POWER_UNAVAILABLE)
139152
bind(binding.outsideTempValue, R.string.label_outside_temp, readings.outsideTemp, PATTERN_TEMP)
140153
bind(binding.batteryTempValue, R.string.label_battery_temp, readings.batteryTemp, PATTERN_TEMP)
154+
bind(
155+
binding.instantConsumptionValue,
156+
R.string.label_instant_consumption,
157+
readings.instantConsumption,
158+
PATTERN_CONSUMPTION,
159+
)
141160
bind(binding.tripDistanceValue, R.string.label_distance, readings.tripDistance, PATTERN_DISTANCE)
142161
bind(binding.tripEnergyValue, R.string.label_energy_used, readings.tripEnergy, PATTERN_ENERGY)
143162
bind(binding.tripRegenValue, R.string.label_regenerated, readings.tripRegen, PATTERN_ENERGY)
144163
bind(
145-
binding.tripConsumptionValue, R.string.label_consumption,
164+
binding.tripConsumptionValue, R.string.label_trip_average_consumption,
146165
readings.tripConsumption, PATTERN_CONSUMPTION,
147166
)
148167
val recorded = provenance.renderWith(readings.tripDuration, transform = ::duration)
@@ -171,6 +190,7 @@ class MainActivity : AppCompatActivity() {
171190

172191
private fun renderUnavailable() {
173192
latestReadings = DashboardReadings.empty()
193+
consumption.reset()
174194
renderReadings(latestReadings)
175195
binding.climateValue.text = DASH
176196
binding.dataStatus.text = getString(R.string.status_waiting_for_vehicle)
@@ -235,10 +255,19 @@ class MainActivity : AppCompatActivity() {
235255
Triple(R.string.label_power, readings.power, PATTERN_POWER),
236256
Triple(R.string.label_outside_temp, readings.outsideTemp, PATTERN_TEMP),
237257
Triple(R.string.label_battery_temp, readings.batteryTemp, PATTERN_TEMP),
258+
Triple(
259+
R.string.label_instant_consumption,
260+
readings.instantConsumption,
261+
PATTERN_CONSUMPTION,
262+
),
238263
Triple(R.string.label_distance, readings.tripDistance, PATTERN_DISTANCE),
239264
Triple(R.string.label_energy_used, readings.tripEnergy, PATTERN_ENERGY),
240265
Triple(R.string.label_regenerated, readings.tripRegen, PATTERN_ENERGY),
241-
Triple(R.string.label_consumption, readings.tripConsumption, PATTERN_CONSUMPTION),
266+
Triple(
267+
R.string.label_trip_average_consumption,
268+
readings.tripConsumption,
269+
PATTERN_CONSUMPTION,
270+
),
242271
).map { (label, value, pattern) ->
243272
provenance.describe(getString(label), value, provenance.render(value, pattern))
244273
}

‎app/src/main/java/com/evsuite/chargepilot/ProvenanceText.kt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class ProvenanceText(private val context: Context) {
6565
UnavailableReason.UNVALIDATED_FIRMWARE -> R.string.reason_unvalidated_firmware
6666
UnavailableReason.SIGNAL_ABSENT -> R.string.reason_signal_absent
6767
UnavailableReason.INSUFFICIENT_SAMPLES -> R.string.reason_insufficient_samples
68+
UnavailableReason.SPEED_TOO_LOW -> R.string.reason_speed_too_low
6869
UnavailableReason.MODEL_NOT_TRAINED -> R.string.reason_model_not_trained
6970
}
7071

‎app/src/main/res/layout/activity_main.xml‎

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -228,9 +228,27 @@
228228
android:layout_width="0dp"
229229
android:layout_height="match_parent"
230230
android:layout_weight="1"
231-
android:layout_marginStart="@dimen/spacing_sm">
232-
<TextView style="@style/Text.EV.ReadoutLabel" android:text="@string/label_consumption" />
233-
<TextView android:id="@+id/tripConsumptionValue" style="@style/Text.EV.ReadoutValue" />
231+
android:layout_marginStart="@dimen/spacing_sm"
232+
android:baselineAligned="false"
233+
android:orientation="horizontal">
234+
235+
<LinearLayout
236+
android:layout_width="0dp"
237+
android:layout_height="wrap_content"
238+
android:layout_weight="1"
239+
android:orientation="vertical">
240+
<TextView style="@style/Text.EV.ReadoutLabel" android:text="@string/label_instant_consumption" />
241+
<TextView android:id="@+id/instantConsumptionValue" style="@style/Text.EV.TripValue" />
242+
</LinearLayout>
243+
244+
<LinearLayout
245+
android:layout_width="0dp"
246+
android:layout_height="wrap_content"
247+
android:layout_weight="1"
248+
android:orientation="vertical">
249+
<TextView style="@style/Text.EV.ReadoutLabel" android:text="@string/label_trip_average_consumption" />
250+
<TextView android:id="@+id/tripConsumptionValue" style="@style/Text.EV.TripValue" />
251+
</LinearLayout>
234252
</LinearLayout>
235253
</LinearLayout>
236254
</LinearLayout>

‎app/src/main/res/values-fr/strings.xml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
<string name="label_outside_temp">EXTÉRIEUR</string>
1414
<string name="label_battery_temp">TEMP. BATTERIE</string>
1515
<string name="label_consumption">MOYENNE</string>
16+
<string name="label_instant_consumption">INSTANTANÉE</string>
17+
<string name="label_trip_average_consumption">MOYENNE TRAJET</string>
1618
<string name="label_current_trip">Trajet en cours</string>
1719
<string name="label_duration">ENREGISTRÉ</string>
1820
<string name="label_distance">DISTANCE</string>
@@ -59,6 +61,7 @@
5961
<string name="reason_unvalidated_firmware">le signal n\'est pas encore validé sur ce firmware</string>
6062
<string name="reason_signal_absent">le véhicule ne publie pas ce signal actuellement</string>
6163
<string name="reason_insufficient_samples">pas encore assez d\'échantillons</string>
64+
<string name="reason_speed_too_low">la vitesse est trop faible pour une consommation stable rapportée à la distance</string>
6265
<string name="reason_model_not_trained">le modèle n\'a aucun ajustement pour prédire</string>
6366
<string name="diagnostics_provenance">Ce que vaut chaque valeur, et pourquoi elle manque</string>
6467
<string name="label_soc">État de charge</string>

‎app/src/main/res/values/strings.xml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
<string name="label_outside_temp">OUTSIDE</string>
1717
<string name="label_battery_temp">BATTERY TEMP.</string>
1818
<string name="label_consumption">AVERAGE</string>
19+
<string name="label_instant_consumption">INSTANT</string>
20+
<string name="label_trip_average_consumption">TRIP AVERAGE</string>
1921
<string name="label_current_trip">Current trip</string>
2022
<string name="label_duration">RECORDED</string>
2123
<string name="label_distance">DISTANCE</string>
@@ -63,6 +65,7 @@
6365
<string name="reason_unvalidated_firmware">the signal is not yet validated on this firmware</string>
6466
<string name="reason_signal_absent">the vehicle is not publishing the signal right now</string>
6567
<string name="reason_insufficient_samples">not enough samples yet</string>
68+
<string name="reason_speed_too_low">vehicle speed is too low for stable distance-normalized consumption</string>
6669
<string name="reason_model_not_trained">the model has no fit to predict from</string>
6770
<string name="diagnostics_provenance">What each reading is, and why it is missing</string>
6871
<string name="label_soc">State of charge</string>

0 commit comments

Comments
 (0)