Count driving only and include charging losses
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
3efa568d982f2594d596dca7d60bb84e39afd9b1f2db04c844f68c2e9af46ca2 FatihsMG4-unstable-2.2.38.apk
|
||||||
+2
-2
@@ -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,25 +71,20 @@ 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) {
|
|
||||||
// Standing still with power flowing in is charging or pre-conditioning
|
|
||||||
// recovery, not a drive crediting itself energy it never spent.
|
|
||||||
kwh = 0d;
|
|
||||||
} else {
|
|
||||||
double effectiveKw = previousPowerKw != null && Math.abs(previousPowerKw) <= MAX_PLAUSIBLE_KW
|
double effectiveKw = previousPowerKw != null && Math.abs(previousPowerKw) <= MAX_PLAUSIBLE_KW
|
||||||
? (previousPowerKw + powerKw) * 0.5d
|
? (previousPowerKw + powerKw) * 0.5d
|
||||||
: powerKw;
|
: powerKw;
|
||||||
kwh = effectiveKw * hours;
|
kwh = effectiveKw * hours;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
double socDrop = (!charging && previousSocPercent != null && socPercent != null)
|
double socDrop = (!charging && previousSocPercent != null && socPercent != null)
|
||||||
? previousSocPercent - socPercent
|
? previousSocPercent - socPercent
|
||||||
|
|||||||
@@ -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) : "—");
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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` : "—"} />
|
||||||
|
|||||||
@@ -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
@@ -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 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user