package com.evsuite.abrp; /** * One integration step of distance, energy, time and SOC — pure, Android-free, unit-tested. * * This is DriveHub_Dort's algorithm, which has proven stable on this car over months, and * it is now the *only* algorithm: every period (lifetime, day, trip A, trip B) is fed from * the same step. Earlier builds integrated the lifetime counter from net DC power while * feeding the trip and day counters from either the vehicle's own cumulative kWh register * or a regeneration-discarding gross fallback. Three sources, three answers, none of them * agreeing with the car's dashboard — the counters were not measuring the same quantity. * * Only moving intervals belong to these driving counters. Parked HVAC/accessory use is * deliberately excluded from distance, energy, time and SOC-drop totals. What the vehicle * reports while moving is net DC battery power: positive out of the pack, negative in. * Integrating it as-is means regeneration subtracts, which is exactly what the car's own * kWh figure does. Discarding the negative half (the old {@code max(0, kW)} fallback) is * what made our number read high. */ final class ConsumptionMath { /** * Longest gap we will still integrate across, in hours (6 minutes). * * A head unit that slept, a service that was killed, or a car that sat parked overnight * all show up as one enormous dt. Bridging it would invent a drive that never happened, * so the gap is dropped and the next sample starts a fresh interval. */ static final double MAX_GAP_HOURS = 0.1d; /** Below this the speed signal is noise around a standstill, not motion. */ static final float SPEED_DEAD_BAND_KMH = 2.5f; /** A reading past this is a decode artefact, not power the pack could ever deliver. */ static final float MAX_PLAUSIBLE_KW = 3000f; /** Result of one interval. All fields are increments, never totals. */ static final class Step { final double km, kwh, hours, socDrop; Step(double km, double kwh, double hours, double socDrop) { this.km = km; this.kwh = kwh; this.hours = hours; this.socDrop = socDrop; } } /** * Integrates one interval, or returns null when the interval carries no usable data. * * Every input is nullable because a failed vehicle read is not a zero: a missing power * reading suppresses the energy term for this interval only, and leaves distance and * time — which do not depend on it — intact. * * @param hours elapsed time, from a monotonic clock * @param charging whether the charge port is connected; SOC gained on a charger is not * consumption and must not offset the drive's SOC drop */ static Step integrate(double hours, Float previousSpeedKmh, Float speedKmh, Float previousPowerKw, Float powerKw, Float previousSocPercent, Float socPercent, boolean charging) { if (!(hours > 0d) || hours > MAX_GAP_HOURS) return null; // Distance: trapezoidal speed across the interval. The calibration factors correct // the head unit's speed signal against the odometer at motorway speeds, where the // reported value runs slightly low. float current = speedKmh != null ? Math.abs(speedKmh) : (previousSpeedKmh != null ? Math.abs(previousSpeedKmh) : 0f); float previous = previousSpeedKmh != null ? Math.abs(previousSpeedKmh) : current; float effectiveKmh = (previous + current) * 0.5f; if (effectiveKmh < SPEED_DEAD_BAND_KMH) { return new Step(0d, 0d, 0d, 0d); } else if (effectiveKmh > 90f) effectiveKmh *= 1.0035f; else if (effectiveKmh > 30f) effectiveKmh *= 1.0015f; double km = effectiveKmh * hours; // Energy: trapezoidal net DC power, regeneration included with its own sign. double kwh = 0d; if (powerKw != null && Math.abs(powerKw) <= MAX_PLAUSIBLE_KW) { double effectiveKw = previousPowerKw != null && Math.abs(previousPowerKw) <= MAX_PLAUSIBLE_KW ? (previousPowerKw + powerKw) * 0.5d : powerKw; kwh = effectiveKw * hours; } double socDrop = (!charging && previousSocPercent != null && socPercent != null) ? previousSocPercent - socPercent : 0d; return new Step(km, kwh, hours, socDrop); } private ConsumptionMath() { } }