diff --git a/assets/js/components/Sessions/SessionDetailsModal.vue b/assets/js/components/Sessions/SessionDetailsModal.vue
index 59a655922..3c2ac3fba 100644
--- a/assets/js/components/Sessions/SessionDetailsModal.vue
+++ b/assets/js/components/Sessions/SessionDetailsModal.vue
@@ -121,6 +121,12 @@
{{ socRange }}
+
+ |
+ {{ $t("session.addedRange") }}
+ |
+ +{{ formatKm(session.addedRange) }} |
+
|
{{ $t("session.odometer") }}
diff --git a/assets/js/components/Sessions/SessionTable.vue b/assets/js/components/Sessions/SessionTable.vue
index f9f1a16d3..beb0a4ec8 100644
--- a/assets/js/components/Sessions/SessionTable.vue
+++ b/assets/js/components/Sessions/SessionTable.vue
@@ -260,6 +260,13 @@ export default defineComponent({
value: (session) => session.chargeDuration,
format: (value) => this.fmtDurationNs(value, false, "h"),
},
+ {
+ name: "addedRange",
+ unit: distanceUnit(),
+ total: this.addedRange,
+ value: (session) => session.addedRange || null,
+ format: (value) => `+${this.fmtNumber(distanceValue(value), 0)}`,
+ },
{
name: "odometer",
unit: distanceUnit(),
@@ -363,6 +370,11 @@ export default defineComponent({
chargeDuration() {
return this.filteredSessions.reduce((total, s) => total + s.chargeDuration, 0);
},
+ addedRange() {
+ const sessions = this.filteredSessions.filter((s) => s.addedRange != null);
+ if (!sessions.length) return null;
+ return sessions.reduce((total, s) => total + (s.addedRange || 0), 0);
+ },
price() {
return this.filteredSessions.reduce((total, s) => total + (s.price || 0), 0);
},
diff --git a/assets/js/components/Sessions/types.ts b/assets/js/components/Sessions/types.ts
index beb03b156..24550f826 100644
--- a/assets/js/components/Sessions/types.ts
+++ b/assets/js/components/Sessions/types.ts
@@ -12,6 +12,7 @@ export interface Session {
chargeDuration: number;
socStart: number | null;
socEnd: number | null;
+ addedRange: number | null;
solarPercentage: number;
price: number | null;
pricePerKWh: number | null;
diff --git a/core/loadpoint.go b/core/loadpoint.go
index 1859b9d8b..66b1bd625 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -174,6 +174,7 @@ type Loadpoint struct {
// charge progress
vehicleSoc float64 // Vehicle or charger soc
+ vehicleRange int64 // Vehicle range in km
chargeDuration time.Duration // Charge duration
connectedDuration time.Duration // Connection duration
energyMetrics EnergyMetrics // Stats for charged energy by session
@@ -1979,6 +1980,7 @@ func (lp *Loadpoint) publishSocAndRange() {
if vs, ok := api.Cap[api.VehicleRange](lp.GetVehicle()); ok {
if rng, err := vs.Range(); err == nil {
lp.log.DEBUG.Printf("vehicle range: %dkm", rng)
+ lp.vehicleRange = rng
lp.publish(keys.VehicleRange, rng)
} else if !loadpoint.AcceptableError(err) {
lp.log.ERROR.Printf("vehicle range: %v", err)
diff --git a/core/loadpoint_session.go b/core/loadpoint_session.go
index 843bdcde3..d566d9b27 100644
--- a/core/loadpoint_session.go
+++ b/core/loadpoint_session.go
@@ -74,6 +74,12 @@ func (lp *Loadpoint) applyEnergyMetrics(s *session.Session) {
s.SocEnd = &soc
}
+ // added range by linear projection from current range and soc delta
+ if rng := float64(lp.vehicleRange); rng > 0 && s.SocStart != nil && s.SocEnd != nil && *s.SocEnd > *s.SocStart {
+ added := rng * (*s.SocEnd - *s.SocStart) / *s.SocEnd
+ s.AddedRange = &added
+ }
+
s.SolarPercentage = new(lp.energyMetrics.SolarPercentage())
s.Price = lp.energyMetrics.Price()
s.PricePerKWh = lp.energyMetrics.PricePerKWh()
diff --git a/core/loadpoint_vehicle.go b/core/loadpoint_vehicle.go
index e9947b2b5..c7d55c67d 100644
--- a/core/loadpoint_vehicle.go
+++ b/core/loadpoint_vehicle.go
@@ -224,6 +224,7 @@ func (lp *Loadpoint) unpublishVehicleIdentity() {
// unpublishVehicle resets published vehicle data
func (lp *Loadpoint) unpublishVehicle() {
lp.vehicleSoc = 0
+ lp.vehicleRange = 0
lp.publish(keys.VehicleClimaterActive, nil)
lp.publish(keys.VehicleSoc, 0.0)
diff --git a/core/session/session.go b/core/session/session.go
index f8e874ff9..f1292aca9 100644
--- a/core/session/session.go
+++ b/core/session/session.go
@@ -21,6 +21,7 @@ type Session struct {
ChargeDuration *time.Duration `json:"chargeDuration" csv:"Charge Duration" gorm:"column:charge_duration"`
SocStart *float64 `json:"socStart" csv:"SoC Start (%)" gorm:"column:soc_start" format:"int"`
SocEnd *float64 `json:"socEnd" csv:"SoC End (%)" gorm:"column:soc_end" format:"int"`
+ AddedRange *float64 `json:"addedRange" csv:"Added Range (km)" gorm:"column:added_range" format:"int"`
SolarPercentage *float64 `json:"solarPercentage" csv:"Solar (%)" gorm:"column:solar_percentage"`
Price *float64 `json:"price" csv:"Price" gorm:"column:price"`
PricePerKWh *float64 `json:"pricePerKWh" csv:"Price/kWh" gorm:"column:price_per_kwh"`
diff --git a/i18n/de.json b/i18n/de.json
index 56e8cad87..2bdca69d2 100644
--- a/i18n/de.json
+++ b/i18n/de.json
@@ -1582,6 +1582,7 @@
},
"session": {
"addOdometer": "Wert eintragen",
+ "addedRange": "Geladene Reichweite",
"cancel": "Abbrechen",
"co2": "CO₂",
"date": "Zeitraum",
@@ -1597,6 +1598,7 @@
"title": "Ladevorgang"
},
"sessions": {
+ "addedRange": "Reichweite",
"avgPower": "⌀ Leistung",
"avgPrice": "⌀ Preis",
"chargeDuration": "Ladedauer",
@@ -1622,6 +1624,7 @@
},
"co2": "⌀ CO₂",
"csv": {
+ "addedrange": "Geladene Reichweite (km)",
"chargedenergy": "Energie (kWh)",
"chargeduration": "Ladedauer",
"co2perkwh": "CO₂/kWh",
diff --git a/i18n/en.json b/i18n/en.json
index 62e2b9088..b608c7a20 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -1582,6 +1582,7 @@
},
"session": {
"addOdometer": "Add value",
+ "addedRange": "Added range",
"cancel": "Cancel",
"co2": "CO₂",
"date": "Period",
@@ -1592,11 +1593,12 @@
"meterstop": "Meter stop",
"odometer": "Mileage",
"price": "Cost",
- "soc": "Vehicle SoC",
+ "soc": "Charge",
"started": "Started",
"title": "Charging Session"
},
"sessions": {
+ "addedRange": "Range",
"avgPower": "⌀ Power",
"avgPrice": "⌀ Price",
"chargeDuration": "Duration",
@@ -1622,6 +1624,7 @@
},
"co2": "⌀ CO₂",
"csv": {
+ "addedrange": "Added range (km)",
"chargedenergy": "Energy (kWh)",
"chargeduration": "Duration",
"co2perkwh": "CO₂/kWh",
diff --git a/tests/session-soc.spec.ts b/tests/session-soc.spec.ts
index 54b7369ff..ae45dd389 100644
--- a/tests/session-soc.spec.ts
+++ b/tests/session-soc.spec.ts
@@ -56,10 +56,11 @@ test("soc range", async ({ page }) => {
await page.goto("/");
await expect(page.getByTestId("vehicle-status")).toContainText("Charging");
- // update SoC to 80% and energy to 10 kWh
+ // update SoC to 80%, range to 400 km and energy to 10 kWh
await page.goto(simulatorUrl());
await loadpoint.getByLabel("Energy").fill("10");
await vehicle.getByLabel("SoC").fill("80");
+ await vehicle.getByLabel("Range").fill("400");
await simulatorApply(page);
await page.goto("/");
@@ -79,4 +80,7 @@ test("soc range", async ({ page }) => {
const modal = page.getByTestId("session-details");
await expectModalVisible(modal);
await expect(modal.getByTestId("session-details-soc")).toContainText("20 – 80%");
+
+ // added range: 400 km at 80% soc -> 400 * (80 - 20) / 80 = 300 km
+ await expect(modal.getByTestId("session-details-added-range")).toContainText("+300 km");
});
diff --git a/tests/sessions.spec.ts b/tests/sessions.spec.ts
index 6755ce66d..725c25aba 100644
--- a/tests/sessions.spec.ts
+++ b/tests/sessions.spec.ts
@@ -55,12 +55,16 @@ test.describe("basics", async () => {
await expect(page.getByTestId("sessions-nodata")).toHaveCount(0);
await expect(page.getByRole("table")).toBeVisible();
await expect(page.getByTestId("sessions-head")).toHaveCount(1);
- await expect(page.getByTestId("sessions-head").locator("th")).toHaveCount(10);
+ await expect(page.getByTestId("sessions-head").locator("th")).toHaveCount(11);
await expect(page.getByTestId("sessions-head-energy")).toContainText("ChargedkWh");
await expect(page.getByTestId("sessions-foot-energy")).toBeVisible();
await expect(page.getByTestId("sessions-foot-energy")).toHaveText("20.0");
+ await expect(page.getByTestId("sessions-head-addedRange")).toContainText("Rangekm");
+ await expect(page.getByTestId("sessions-foot-addedRange")).toBeVisible();
+ await expect(page.getByTestId("sessions-foot-addedRange")).toHaveText("+80");
+
await expect(page.getByTestId("sessions-head-solar")).toContainText("Solar%");
await expect(page.getByTestId("sessions-foot-solar")).toBeVisible();
await expect(page.getByTestId("sessions-foot-solar")).toHaveText("67.3");
@@ -251,9 +255,9 @@ test.describe("columns desktop", async () => {
await page.setViewportSize({ width: 800, height: 800 });
await page.goto("/#/sessions?year=2023&month=3");
await expect(
- page.getByTestId("sessions-head-avgPrice").locator("option[value=odometer]")
+ page.getByTestId("sessions-head-price").locator("option[value=odometer]")
).toHaveCount(1);
- await page.getByTestId("sessions-head-avgPrice").getByRole("combobox").selectOption("odometer");
+ await page.getByTestId("sessions-head-price").getByRole("combobox").selectOption("odometer");
await expect(page.getByTestId("sessions-head-odometer")).toBeVisible();
await expect(page.getByTestId("sessions-entry").nth(0)).toContainText("12,345");
await expect(page.getByTestId("sessions-foot-odometer")).toHaveText("");
@@ -391,6 +395,7 @@ test.describe("session details", async () => {
await expect(modal.getByTestId("session-details-solar")).toContainText("0.0% (0.0 kWh)");
await expect(modal.getByTestId("session-details-price")).toContainText("2.50 € 50.0 ct/kWh");
await expect(modal.getByTestId("session-details-co2")).toHaveCount(0);
+ await expect(modal.getByTestId("session-details-added-range")).toContainText("+25 km");
await expect(modal.getByTestId("session-details-odometer")).toContainText("Add value");
await expect(modal.getByTestId("session-details-meter")).toHaveCount(0);
await expect(modal.getByTestId("session-details-delete")).toContainText("Delete");
@@ -413,6 +418,7 @@ test.describe("session details", async () => {
await expect(modal.getByTestId("session-details-price")).toContainText("2.00 € 20.0 ct/kWh");
await expect(modal.getByTestId("session-details-co2")).toContainText("3 kg");
await expect(modal.getByTestId("session-details-co2")).toContainText("300 g/kWh");
+ await expect(modal.getByTestId("session-details-added-range")).toContainText("+60 km");
await expect(modal.getByTestId("session-details-odometer")).toContainText("12,345 km");
});
diff --git a/tests/sessions.sql b/tests/sessions.sql
index 72c789ec4..3108a49ae 100644
--- a/tests/sessions.sql
+++ b/tests/sessions.sql
@@ -15,11 +15,12 @@ CREATE TABLE `sessions` (
`price_per_kwh` real,
`co2_per_kwh` real,
`charge_duration` integer,
+ `added_range` real,
PRIMARY KEY (`id`)
);
-INSERT INTO `sessions` VALUES (1,'2023-03-01 08:00:00.0+02:00','2023-05-02 12:00:00.0+02:00','Carport',NULL,'blauer e-Golf',NULL,NULL,10,12345,100,2,0.2,300,3600000000000);
-INSERT INTO `sessions` VALUES (2,'2023-05-02 08:00:00.0+02:00','2023-05-02 12:00:00.0+02:00','Carport',NULL,'blauer e-Golf',NULL,NULL,10,NULL,100,2,0.2,NULL,1800000000000);
-INSERT INTO `sessions` VALUES (3,'2023-05-02 08:00:00.0+02:00','2023-05-02 12:00:00.0+02:00','Carport',NULL,'blauer e-Golf',NULL,NULL,2.5,NULL,88.21,0.75,0.3,NULL,NULL);
-INSERT INTO `sessions` VALUES (4,'2023-05-03 16:00:00.0+02:00','2023-05-03 20:00:00.0+02:00','Carport',NULL,'weißes Model 3',NULL,NULL,2.5,NULL,50,0.25,0.1,NULL,NULL);
-INSERT INTO `sessions` VALUES (5,'2023-05-04 22:00:00.0+02:00','2023-05-05 06:00:00.0+02:00','Garage',NULL,'weißes Model 3',NULL,NULL,5,NULL,0,2.5,0.5,null,3600000000000);
+INSERT INTO `sessions` VALUES (1,'2023-03-01 08:00:00.0+02:00','2023-05-02 12:00:00.0+02:00','Carport',NULL,'blauer e-Golf',NULL,NULL,10,12345,100,2,0.2,300,3600000000000,60);
+INSERT INTO `sessions` VALUES (2,'2023-05-02 08:00:00.0+02:00','2023-05-02 12:00:00.0+02:00','Carport',NULL,'blauer e-Golf',NULL,NULL,10,NULL,100,2,0.2,NULL,1800000000000,55);
+INSERT INTO `sessions` VALUES (3,'2023-05-02 08:00:00.0+02:00','2023-05-02 12:00:00.0+02:00','Carport',NULL,'blauer e-Golf',NULL,NULL,2.5,NULL,88.21,0.75,0.3,NULL,NULL,NULL);
+INSERT INTO `sessions` VALUES (4,'2023-05-03 16:00:00.0+02:00','2023-05-03 20:00:00.0+02:00','Carport',NULL,'weißes Model 3',NULL,NULL,2.5,NULL,50,0.25,0.1,NULL,NULL,NULL);
+INSERT INTO `sessions` VALUES (5,'2023-05-04 22:00:00.0+02:00','2023-05-05 06:00:00.0+02:00','Garage',NULL,'weißes Model 3',NULL,NULL,5,NULL,0,2.5,0.5,null,3600000000000,25);
|