Count driving only and include charging losses

This commit is contained in:
TCFDONMEZ
2026-10-08 15:51:38 +03:00
parent 050fe84d6a
commit ae036b9d65
16 changed files with 104 additions and 41 deletions
+9
View File
@@ -1,5 +1,14 @@
# Changelog # Changelog
## 2.2.38
### Changed
- Charging cost now uses estimated grid energy: battery-side net kWh plus a fixed 10%
charging loss. Both net and estimated grid energy are stored separately.
- Driving consumption, duration, distance and SOC drop now accumulate only while the car
is moving; parked HVAC and accessory use no longer inflate driving statistics.
## 2.2.37 ## 2.2.37
### Added ### Added
+1
View File
@@ -0,0 +1 @@
3efa568d982f2594d596dca7d60bb84e39afd9b1f2db04c844f68c2e9af46ca2 FatihsMG4-unstable-2.2.38.apk
+2 -2
View File
@@ -34,8 +34,8 @@ android {
applicationId "com.evsuite.abrp" applicationId "com.evsuite.abrp"
minSdk 27 minSdk 27
targetSdk 36 targetSdk 36
versionCode 54 versionCode 55
versionName "2.2.37" versionName "2.2.38"
// MongoDB credentials never ship in the APK. The app only talks to the small // MongoDB credentials never ship in the APK. The app only talks to the small
// authenticated API in server/, which owns the database connection. // authenticated API in server/, which owns the database connection.
@@ -24,6 +24,8 @@ import java.util.UUID;
* reading the database needs to place it on a timeline — but nothing is measured from them. * reading the database needs to place it on a timeline — but nothing is measured from them.
*/ */
final class ChargeSessionTracker { final class ChargeSessionTracker {
static final double CHARGING_LOSS_PERCENT = 10d;
static final double GRID_ENERGY_FACTOR = 1d + CHARGING_LOSS_PERCENT / 100d;
private static final String PREFS = "charging_sessions_v1"; private static final String PREFS = "charging_sessions_v1";
private static final String ACTIVE = "active"; private static final String ACTIVE = "active";
private static final String LAST = "last_completed"; private static final String LAST = "last_completed";
@@ -67,6 +69,7 @@ final class ChargeSessionTracker {
final Float startSocPercent; final Float startSocPercent;
final Float currentSocPercent; final Float currentSocPercent;
final double energyKwh; final double energyKwh;
final double gridEnergyKwh;
final double pricePerKwh; final double pricePerKwh;
final double totalCost; final double totalCost;
final List<Point> points; final List<Point> points;
@@ -80,8 +83,9 @@ final class ChargeSessionTracker {
this.startSocPercent = startSocPercent; this.startSocPercent = startSocPercent;
this.currentSocPercent = currentSocPercent; this.currentSocPercent = currentSocPercent;
this.energyKwh = energyKwh; this.energyKwh = energyKwh;
this.gridEnergyKwh = gridEnergyKwh(energyKwh);
this.pricePerKwh = pricePerKwh; this.pricePerKwh = pricePerKwh;
this.totalCost = energyKwh * pricePerKwh; this.totalCost = totalCost(energyKwh, pricePerKwh);
this.points = points; this.points = points;
} }
} }
@@ -140,6 +144,14 @@ final class ChargeSessionTracker {
return Double.longBitsToDouble(prefs.getLong(PRICE, 0L)); return Double.longBitsToDouble(prefs.getLong(PRICE, 0L));
} }
static double gridEnergyKwh(double batteryEnergyKwh) {
return batteryEnergyKwh * GRID_ENERGY_FACTOR;
}
static double totalCost(double batteryEnergyKwh, double pricePerKwh) {
return gridEnergyKwh(batteryEnergyKwh) * pricePerKwh;
}
synchronized List<JSONObject> pendingUploads() { synchronized List<JSONObject> pendingUploads() {
JSONArray array = pendingArray(); JSONArray array = pendingArray();
List<JSONObject> result = new ArrayList<>(); List<JSONObject> result = new ArrayList<>();
@@ -264,8 +276,12 @@ final class ChargeSessionTracker {
} }
JSONObject json = new JSONObject().put("sessionId", id).put("startedAt", startedAtMs) JSONObject json = new JSONObject().put("sessionId", id).put("startedAt", startedAtMs)
.put("endedAt", endedAtMs).put("durationSeconds", activeSeconds()) .put("endedAt", endedAtMs).put("durationSeconds", activeSeconds())
.put("energyKwh", energyKwh).put("pricePerKwh", pricePerKwh) .put("energyKwh", energyKwh)
.put("totalCost", energyKwh * pricePerKwh).put("curve", curve) .put("chargingLossPercent", CHARGING_LOSS_PERCENT)
.put("gridEnergyKwh", gridEnergyKwh(energyKwh))
.put("pricePerKwh", pricePerKwh)
.put("totalCost", totalCost(energyKwh, pricePerKwh))
.put("curve", curve)
.put("meteredSeconds", meteredSeconds) .put("meteredSeconds", meteredSeconds)
.put("curveIntervalMs", curveIntervalMs); .put("curveIntervalMs", curveIntervalMs);
if (startSoc != null) json.put("startSocPercent", startSoc); if (startSoc != null) json.put("startSocPercent", startSoc);
@@ -10,7 +10,9 @@ package com.evsuite.abrp;
* or a regeneration-discarding gross fallback. Three sources, three answers, none of them * 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. * agreeing with the car's dashboard — the counters were not measuring the same quantity.
* *
* What the vehicle reports is net DC battery power: positive out of the pack, negative in. * 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 * 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 * kWh figure does. Discarding the negative half (the old {@code max(0, kW)} fallback) is
* what made our number read high. * what made our number read high.
@@ -69,24 +71,19 @@ final class ConsumptionMath {
: (previousSpeedKmh != null ? Math.abs(previousSpeedKmh) : 0f); : (previousSpeedKmh != null ? Math.abs(previousSpeedKmh) : 0f);
float previous = previousSpeedKmh != null ? Math.abs(previousSpeedKmh) : current; float previous = previousSpeedKmh != null ? Math.abs(previousSpeedKmh) : current;
float effectiveKmh = (previous + current) * 0.5f; float effectiveKmh = (previous + current) * 0.5f;
if (effectiveKmh < SPEED_DEAD_BAND_KMH) effectiveKmh = 0f; if (effectiveKmh < SPEED_DEAD_BAND_KMH) {
else if (effectiveKmh > 90f) effectiveKmh *= 1.0035f; return new Step(0d, 0d, 0d, 0d);
} else if (effectiveKmh > 90f) effectiveKmh *= 1.0035f;
else if (effectiveKmh > 30f) effectiveKmh *= 1.0015f; else if (effectiveKmh > 30f) effectiveKmh *= 1.0015f;
double km = effectiveKmh * hours; double km = effectiveKmh * hours;
// Energy: trapezoidal net DC power, regeneration included with its own sign. // Energy: trapezoidal net DC power, regeneration included with its own sign.
double kwh = 0d; double kwh = 0d;
if (powerKw != null && Math.abs(powerKw) <= MAX_PLAUSIBLE_KW) { if (powerKw != null && Math.abs(powerKw) <= MAX_PLAUSIBLE_KW) {
if (current == 0f && powerKw < 0f) { double effectiveKw = previousPowerKw != null && Math.abs(previousPowerKw) <= MAX_PLAUSIBLE_KW
// Standing still with power flowing in is charging or pre-conditioning ? (previousPowerKw + powerKw) * 0.5d
// recovery, not a drive crediting itself energy it never spent. : powerKw;
kwh = 0d; kwh = effectiveKw * hours;
} else {
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) double socDrop = (!charging && previousSocPercent != null && socPercent != null)
@@ -119,7 +119,7 @@ public class MainActivity extends AppCompatActivity {
private TextInputEditText consumptionSohInput; private TextInputEditText consumptionSohInput;
private java.time.LocalDate selectedConsumptionDate = java.time.LocalDate.now(); private java.time.LocalDate selectedConsumptionDate = java.time.LocalDate.now();
private TextView chargeStatus, chargeSoc, chargeStartSoc, chargeVoltage, chargeCurrent, chargePower, private TextView chargeStatus, chargeSoc, chargeStartSoc, chargeVoltage, chargeCurrent, chargePower,
chargeEnergy, chargeDuration, chargeTotalCost, chargeCurveTable; chargeEnergy, chargeGridEnergy, chargeDuration, chargeTotalCost, chargeCurveTable;
private TextInputLayout chargePriceLayout; private TextInputLayout chargePriceLayout;
private TextInputEditText chargePriceInput; private TextInputEditText chargePriceInput;
private ChargeSessionTracker chargeSessionTracker; private ChargeSessionTracker chargeSessionTracker;
@@ -936,6 +936,7 @@ public class MainActivity extends AppCompatActivity {
chargeCurrent = chargingPane.findViewById(R.id.charge_current); chargeCurrent = chargingPane.findViewById(R.id.charge_current);
chargePower = chargingPane.findViewById(R.id.charge_power); chargePower = chargingPane.findViewById(R.id.charge_power);
chargeEnergy = chargingPane.findViewById(R.id.charge_energy); chargeEnergy = chargingPane.findViewById(R.id.charge_energy);
chargeGridEnergy = chargingPane.findViewById(R.id.charge_grid_energy);
chargeDuration = chargingPane.findViewById(R.id.charge_duration); chargeDuration = chargingPane.findViewById(R.id.charge_duration);
chargeTotalCost = chargingPane.findViewById(R.id.charge_total_cost); chargeTotalCost = chargingPane.findViewById(R.id.charge_total_cost);
chargeGraph = chargingPane.findViewById(R.id.charge_graph); chargeGraph = chargingPane.findViewById(R.id.charge_graph);
@@ -980,6 +981,8 @@ public class MainActivity extends AppCompatActivity {
chargeCurrent.setText(fmt(s.getBatteryCurrentA(), "%.1f A")); chargeCurrent.setText(fmt(s.getBatteryCurrentA(), "%.1f A"));
chargePower.setText(s.getBatteryPowerKw() == null ? "—" : String.format(java.util.Locale.getDefault(), "%.1f kW", Math.abs(s.getBatteryPowerKw()))); chargePower.setText(s.getBatteryPowerKw() == null ? "—" : String.format(java.util.Locale.getDefault(), "%.1f kW", Math.abs(s.getBatteryPowerKw())));
chargeEnergy.setText(session.startedAtMs > 0 ? String.format(java.util.Locale.getDefault(), "%.2f kWh", session.energyKwh) : "—"); chargeEnergy.setText(session.startedAtMs > 0 ? String.format(java.util.Locale.getDefault(), "%.2f kWh", session.energyKwh) : "—");
chargeGridEnergy.setText(session.startedAtMs > 0 ? String.format(
java.util.Locale.getDefault(), "%.2f kWh", session.gridEnergyKwh) : "—");
long sec = session.durationSeconds; long sec = session.durationSeconds;
chargeDuration.setText(session.startedAtMs > 0 ? String.format(java.util.Locale.getDefault(), "%02d:%02d:%02d", sec/3600,(sec%3600)/60,sec%60) : "—"); chargeDuration.setText(session.startedAtMs > 0 ? String.format(java.util.Locale.getDefault(), "%02d:%02d:%02d", sec/3600,(sec%3600)/60,sec%60) : "—");
chargeTotalCost.setText(session.startedAtMs > 0 ? String.format(java.util.Locale.getDefault(), "%.2f ₺", session.totalCost) : "—"); chargeTotalCost.setText(session.startedAtMs > 0 ? String.format(java.util.Locale.getDefault(), "%.2f ₺", session.totalCost) : "—");
+2 -1
View File
@@ -9,7 +9,8 @@
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="Batarya voltajı" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/charge_voltage" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="—" android:textAppearance="@style/DataValue"/> <TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="Batarya voltajı" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/charge_voltage" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="—" android:textAppearance="@style/DataValue"/>
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="Batarya akımı" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/charge_current" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="—" android:textAppearance="@style/DataValue"/> <TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="Batarya akımı" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/charge_current" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="—" android:textAppearance="@style/DataValue"/>
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="Şarj gücü" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/charge_power" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="—" android:textAppearance="@style/DataValue"/> <TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="Şarj gücü" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/charge_power" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="—" android:textAppearance="@style/DataValue"/>
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="Alınan enerji" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/charge_energy" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="—" android:textAppearance="@style/DataValue"/> <TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="Bataryaya giren net enerji" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/charge_energy" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="—" android:textAppearance="@style/DataValue"/>
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="Şebekeden alınan (+%10)" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/charge_grid_energy" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="—" android:textAppearance="@style/DataValue"/>
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="Şarj süresi" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/charge_duration" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="—" android:textAppearance="@style/DataValue"/> <TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="Şarj süresi" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/charge_duration" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="—" android:textAppearance="@style/DataValue"/>
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="Toplam ücret" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/charge_total_cost" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="—" android:textAppearance="@style/DataValue"/> <TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="Toplam ücret" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/charge_total_cost" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="—" android:textAppearance="@style/DataValue"/>
</GridLayout> </GridLayout>
@@ -0,0 +1,13 @@
package com.evsuite.abrp;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class ChargeCostTest {
@Test
public void costIncludesTenPercentChargingLoss() {
assertEquals(55d, ChargeSessionTracker.gridEnergyKwh(50d), 0.000001d);
assertEquals(550d, ChargeSessionTracker.totalCost(50d, 10d), 0.000001d);
}
}
@@ -32,10 +32,13 @@ public class ConsumptionMathTest {
} }
@Test @Test
public void speedBelowTheDeadBandContributesNoDistance() { public void speedBelowTheDeadBandContributesNothingToDrivingTotals() {
ConsumptionMath.Step step = ConsumptionMath.Step step =
ConsumptionMath.integrate(DT, 2f, 2f, null, null, null, null, false); ConsumptionMath.integrate(DT, 2f, 2f, 8f, 8f, 80f, 79f, false);
assertEquals(0d, step.km, TOLERANCE); assertEquals(0d, step.km, TOLERANCE);
assertEquals(0d, step.kwh, TOLERANCE);
assertEquals(0d, step.hours, TOLERANCE);
assertEquals(0d, step.socDrop, TOLERANCE);
} }
@Test @Test
@@ -70,11 +73,11 @@ public class ConsumptionMathTest {
} }
@Test @Test
public void standstillPowerFlowingOutStillCounts() { public void parkedClimateUseIsExcludedFromDrivingConsumption() {
// Parked with the climate running is real consumption.
ConsumptionMath.Step step = ConsumptionMath.Step step =
ConsumptionMath.integrate(DT, 0f, 0f, 2f, 2f, null, null, false); ConsumptionMath.integrate(DT, 0f, 0f, 2f, 2f, null, null, false);
assertEquals(2d * DT, step.kwh, TOLERANCE); assertEquals(0d, step.kwh, TOLERANCE);
assertEquals(0d, step.hours, TOLERANCE);
} }
@Test @Test
+5 -5
View File
@@ -1,5 +1,5 @@
MONGODB_URI mongodb+srv://boklukunefe:9rprDcfHTjBYza7z@cluster0.1aarkvo.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0 MONGODB_URI=mongodb+srv://username:password@cluster.example.mongodb.net/
MONGODB_DATABASE fatihsmg4 MONGODB_DATABASE=fatihsmg4
INGEST_TOKEN 066da6fc58a0fcd8a35deccb29e43ef7a0b3bcda5a3c2b99db86e64a5b982ec6 INGEST_TOKEN=replace-with-a-long-random-write-token
SESSION_SECRET a89b011dfe7bec22c4f44f89e8e0ef647e6ff3289e84ca1b5b68c413ff7f4b77 SESSION_SECRET=replace-with-a-long-random-session-secret
DASHBOARD_PASSWORD <sen belirle, en az 8 karakter> DASHBOARD_PASSWORD=replace-with-a-strong-dashboard-password
+5 -3
View File
@@ -27,11 +27,12 @@ export default async function ChargingPage() {
return ( return (
<Shell active="/charging" title="Şarj" subtitle={`${sessions.length} DC seans`}> <Shell active="/charging" title="Şarj" subtitle={`${sessions.length} DC seans`}>
<div className="tiles"> <div className="tiles">
<Tile label="Toplam alınan" value={precise(total.energyKwh)} unit="kWh" tone="gold" /> <Tile label="Bataryaya giren net" value={precise(total.energyKwh)} unit="kWh" tone="gold" />
<Tile label="Şebekeden alınan (+%10)" value={precise(total.gridEnergyKwh)} unit="kWh" tone="gold" />
<Tile label="Toplam ödenen" value={money(total.cost)} tone="gold" /> <Tile label="Toplam ödenen" value={money(total.cost)} tone="gold" />
<Tile <Tile
label="Ortalama birim" label="Ortalama birim"
value={total.energyKwh > 0 ? money(total.cost / total.energyKwh) : "—"} value={total.gridEnergyKwh > 0 ? money(total.cost / total.gridEnergyKwh) : "—"}
note="kWh başına" note="kWh başına"
tone="teal" tone="teal"
/> />
@@ -78,7 +79,8 @@ export default async function ChargingPage() {
<Row label="Başlangıç" value={dateTime(session.startedAt)} /> <Row label="Başlangıç" value={dateTime(session.startedAt)} />
<Row label="Bitiş" value={dateTime(session.endedAt)} /> <Row label="Bitiş" value={dateTime(session.endedAt)} />
<Row label="Ölçülen süre" value={duration(session.durationSeconds)} /> <Row label="Ölçülen süre" value={duration(session.durationSeconds)} />
<Row label="Alınan enerji" value={`${precise(session.energyKwh)} kWh`} /> <Row label="Bataryaya giren net" value={`${precise(session.energyKwh)} kWh`} />
<Row label="Şebekeden alınan (+%10)" value={`${precise(session.gridEnergyKwh ?? session.energyKwh * 1.1)} kWh`} />
<Row label="Kazanılan doluluk" value={gained === null ? "—" : `%${num(gained)}`} /> <Row label="Kazanılan doluluk" value={gained === null ? "—" : `%${num(gained)}`} />
<Row label="Tepe güç" value={peak > 0 ? `${num(peak)} kW` : "—"} /> <Row label="Tepe güç" value={peak > 0 ? `${num(peak)} kW` : "—"} />
<Row label="Ortalama güç" value={average > 0 ? `${num(average)} kW` : "—"} /> <Row label="Ortalama güç" value={average > 0 ? `${num(average)} kW` : "—"} />
+5 -4
View File
@@ -36,8 +36,8 @@ export default async function OverviewPage() {
> >
<div className="tiles"> <div className="tiles">
<Tile <Tile
label="Toplam alınan" label="Şebekeden alınan"
value={precise(charge.energyKwh)} value={precise(charge.gridEnergyKwh)}
unit="kWh" unit="kWh"
note={`${charge.sessions} DC şarj`} note={`${charge.sessions} DC şarj`}
tone="gold" tone="gold"
@@ -45,7 +45,7 @@ export default async function OverviewPage() {
<Tile <Tile
label="Ödenen" label="Ödenen"
value={money(charge.cost)} value={money(charge.cost)}
note={charge.energyKwh > 0 ? `${money(charge.cost / charge.energyKwh)}/kWh ort.` : undefined} note={charge.gridEnergyKwh > 0 ? `${money(charge.cost / charge.gridEnergyKwh)}/kWh ort.` : undefined}
tone="gold" tone="gold"
/> />
<Tile label="Son 30 gün" value={num(recent.km)} unit="km" note={efficiency(recent.kwh, recent.km)} tone="teal" /> <Tile label="Son 30 gün" value={num(recent.km)} unit="km" note={efficiency(recent.kwh, recent.km)} tone="teal" />
@@ -68,7 +68,8 @@ export default async function OverviewPage() {
<h2 className="card-title">Son şarj</h2> <h2 className="card-title">Son şarj</h2>
<div className="rows"> <div className="rows">
<Row label="Tarih" value={dateTime(lastSession.startedAt)} /> <Row label="Tarih" value={dateTime(lastSession.startedAt)} />
<Row label="Alınan enerji" value={`${precise(lastSession.energyKwh)} kWh`} /> <Row label="Bataryaya giren net" value={`${precise(lastSession.energyKwh)} kWh`} />
<Row label="Şebekeden alınan (+%10)" value={`${precise(lastSession.gridEnergyKwh ?? lastSession.energyKwh * 1.1)} kWh`} />
<Row <Row
label="Doluluk" label="Doluluk"
value={ value={
+3 -1
View File
@@ -47,6 +47,7 @@ export async function loadOverview(dayLimit = 60, sessionLimit = 60): Promise<Ov
export interface ChargeTotals { export interface ChargeTotals {
energyKwh: number; energyKwh: number;
gridEnergyKwh: number;
cost: number; cost: number;
sessions: number; sessions: number;
seconds: number; seconds: number;
@@ -58,6 +59,7 @@ export function sumCharging(sessions: ChargingSession[]): ChargeTotals {
return sessions.reduce<ChargeTotals>( return sessions.reduce<ChargeTotals>(
(total, session) => ({ (total, session) => ({
energyKwh: total.energyKwh + session.energyKwh, energyKwh: total.energyKwh + session.energyKwh,
gridEnergyKwh: total.gridEnergyKwh + (session.gridEnergyKwh ?? session.energyKwh * 1.1),
cost: total.cost + session.totalCost, cost: total.cost + session.totalCost,
sessions: total.sessions + 1, sessions: total.sessions + 1,
seconds: total.seconds + session.durationSeconds, seconds: total.seconds + session.durationSeconds,
@@ -68,7 +70,7 @@ export function sumCharging(sessions: ChargingSession[]): ChargeTotals {
: 0), : 0),
peakKw: Math.max(total.peakKw, ...session.curve.map((point) => point.powerKw), 0), peakKw: Math.max(total.peakKw, ...session.curve.map((point) => point.powerKw), 0),
}), }),
{ energyKwh: 0, cost: 0, sessions: 0, seconds: 0, socGained: 0, peakKw: 0 }, { energyKwh: 0, gridEnergyKwh: 0, cost: 0, sessions: 0, seconds: 0, socGained: 0, peakKw: 0 },
); );
} }
+2
View File
@@ -41,6 +41,8 @@ export interface ChargingSession {
endedAt: Date; endedAt: Date;
durationSeconds: number; durationSeconds: number;
energyKwh: number; energyKwh: number;
chargingLossPercent: number;
gridEnergyKwh: number;
pricePerKwh: number; pricePerKwh: number;
totalCost: number; totalCost: number;
startSocPercent?: number; startSocPercent?: number;
+9 -3
View File
@@ -49,15 +49,21 @@ export function validateChargingSession(body: unknown, pathSessionId: string): C
}); });
const startSocPercent = record.startSocPercent; const startSocPercent = record.startSocPercent;
const endSocPercent = record.endSocPercent; const endSocPercent = record.endSocPercent;
const energyKwh = bounded(record.energyKwh, 0, 1000, "energyKwh");
const pricePerKwh = bounded(record.pricePerKwh, 0, 1_000_000, "pricePerKwh");
const chargingLossPercent = 10;
const gridEnergyKwh = energyKwh * 1.1;
return { return {
installationId: validateUuid(record.installationId, "installationId"), installationId: validateUuid(record.installationId, "installationId"),
sessionId, sessionId,
startedAt: validateDate(record.startedAt, "startedAt"), startedAt: validateDate(record.startedAt, "startedAt"),
endedAt: validateDate(record.endedAt, "endedAt"), endedAt: validateDate(record.endedAt, "endedAt"),
durationSeconds: bounded(record.durationSeconds, 0, 604800, "durationSeconds"), durationSeconds: bounded(record.durationSeconds, 0, 604800, "durationSeconds"),
energyKwh: bounded(record.energyKwh, 0, 1000, "energyKwh"), energyKwh,
pricePerKwh: bounded(record.pricePerKwh, 0, 1_000_000, "pricePerKwh"), chargingLossPercent,
totalCost: bounded(record.totalCost, 0, 1_000_000_000, "totalCost"), gridEnergyKwh,
pricePerKwh,
totalCost: gridEnergyKwh * pricePerKwh,
...(startSocPercent === undefined ? {} : { startSocPercent: bounded(startSocPercent, 0, 100, "startSocPercent") }), ...(startSocPercent === undefined ? {} : { startSocPercent: bounded(startSocPercent, 0, 100, "startSocPercent") }),
...(endSocPercent === undefined ? {} : { endSocPercent: bounded(endSocPercent, 0, 100, "endSocPercent") }), ...(endSocPercent === undefined ? {} : { endSocPercent: bounded(endSocPercent, 0, 100, "endSocPercent") }),
curve, curve,
+7
View File
@@ -52,6 +52,8 @@ test("a charging session must match the id in its path", () => {
endedAt: 1_760_002_000_000, endedAt: 1_760_002_000_000,
durationSeconds: 1800, durationSeconds: 1800,
energyKwh: 31.4, energyKwh: 31.4,
chargingLossPercent: 10,
gridEnergyKwh: 34.54,
pricePerKwh: 7.5, pricePerKwh: 7.5,
totalCost: 235.5, totalCost: 235.5,
startSocPercent: 18, startSocPercent: 18,
@@ -61,6 +63,9 @@ test("a charging session must match the id in its path", () => {
const record = validateChargingSession(body, SESSION); const record = validateChargingSession(body, SESSION);
assert.equal(record.curve.length, 1); assert.equal(record.curve.length, 1);
assert.equal(record.curve[0].powerKw, 84.2); assert.equal(record.curve[0].powerKw, 84.2);
assert.equal(record.chargingLossPercent, 10);
assert.equal(record.gridEnergyKwh, 34.54);
assert.equal(record.totalCost, 259.05);
assert.throws(() => validateChargingSession(body, "11111111-2222-4333-8444-555555555556"), ValidationError); assert.throws(() => validateChargingSession(body, "11111111-2222-4333-8444-555555555556"), ValidationError);
}); });
@@ -79,6 +84,8 @@ test("refuses a curve longer than the car can produce", () => {
endedAt: 2, endedAt: 2,
durationSeconds: 1, durationSeconds: 1,
energyKwh: 1, energyKwh: 1,
chargingLossPercent: 10,
gridEnergyKwh: 1.1,
pricePerKwh: 1, pricePerKwh: 1,
totalCost: 1, totalCost: 1,
curve, curve,