Sessions: record added range per charging session (#32509)

This commit is contained in:
Michael Geers 2026-08-04 14:07:15 +02:00 • committed by GitHub
parent addbda0c43
commit 3ec61c1851
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 56 additions and 10 deletions

View file

@ -121,6 +121,12 @@
{{ socRange }}
</td>
</tr>
<tr v-if="session.addedRange != null" data-testid="session-details-added-range">
<th class="align-baseline">
{{ $t("session.addedRange") }}
</th>
<td>+{{ formatKm(session.addedRange) }}</td>
</tr>
<tr data-testid="session-details-odometer">
<th class="align-middle">
{{ $t("session.odometer") }}

View file

@ -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);
},

View file

@ -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;

View file

@ -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)

View file

@ -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()

View file

@ -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)

View file

@ -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"`

View file

@ -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",

View file

@ -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",

View file

@ -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");
});

View file

@ -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");
});

View file

@ -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);