Sessions: record added range per charging session (#32509)
This commit is contained in:
parent
addbda0c43
commit
3ec61c1851
12 changed files with 56 additions and 10 deletions
|
|
@ -121,6 +121,12 @@
|
||||||
{{ socRange }}
|
{{ socRange }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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">
|
<tr data-testid="session-details-odometer">
|
||||||
<th class="align-middle">
|
<th class="align-middle">
|
||||||
{{ $t("session.odometer") }}
|
{{ $t("session.odometer") }}
|
||||||
|
|
|
||||||
|
|
@ -260,6 +260,13 @@ export default defineComponent({
|
||||||
value: (session) => session.chargeDuration,
|
value: (session) => session.chargeDuration,
|
||||||
format: (value) => this.fmtDurationNs(value, false, "h"),
|
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",
|
name: "odometer",
|
||||||
unit: distanceUnit(),
|
unit: distanceUnit(),
|
||||||
|
|
@ -363,6 +370,11 @@ export default defineComponent({
|
||||||
chargeDuration() {
|
chargeDuration() {
|
||||||
return this.filteredSessions.reduce((total, s) => total + s.chargeDuration, 0);
|
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() {
|
price() {
|
||||||
return this.filteredSessions.reduce((total, s) => total + (s.price || 0), 0);
|
return this.filteredSessions.reduce((total, s) => total + (s.price || 0), 0);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export interface Session {
|
||||||
chargeDuration: number;
|
chargeDuration: number;
|
||||||
socStart: number | null;
|
socStart: number | null;
|
||||||
socEnd: number | null;
|
socEnd: number | null;
|
||||||
|
addedRange: number | null;
|
||||||
solarPercentage: number;
|
solarPercentage: number;
|
||||||
price: number | null;
|
price: number | null;
|
||||||
pricePerKWh: number | null;
|
pricePerKWh: number | null;
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,7 @@ type Loadpoint struct {
|
||||||
|
|
||||||
// charge progress
|
// charge progress
|
||||||
vehicleSoc float64 // Vehicle or charger soc
|
vehicleSoc float64 // Vehicle or charger soc
|
||||||
|
vehicleRange int64 // Vehicle range in km
|
||||||
chargeDuration time.Duration // Charge duration
|
chargeDuration time.Duration // Charge duration
|
||||||
connectedDuration time.Duration // Connection duration
|
connectedDuration time.Duration // Connection duration
|
||||||
energyMetrics EnergyMetrics // Stats for charged energy by session
|
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 vs, ok := api.Cap[api.VehicleRange](lp.GetVehicle()); ok {
|
||||||
if rng, err := vs.Range(); err == nil {
|
if rng, err := vs.Range(); err == nil {
|
||||||
lp.log.DEBUG.Printf("vehicle range: %dkm", rng)
|
lp.log.DEBUG.Printf("vehicle range: %dkm", rng)
|
||||||
|
lp.vehicleRange = rng
|
||||||
lp.publish(keys.VehicleRange, rng)
|
lp.publish(keys.VehicleRange, rng)
|
||||||
} else if !loadpoint.AcceptableError(err) {
|
} else if !loadpoint.AcceptableError(err) {
|
||||||
lp.log.ERROR.Printf("vehicle range: %v", err)
|
lp.log.ERROR.Printf("vehicle range: %v", err)
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,12 @@ func (lp *Loadpoint) applyEnergyMetrics(s *session.Session) {
|
||||||
s.SocEnd = &soc
|
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.SolarPercentage = new(lp.energyMetrics.SolarPercentage())
|
||||||
s.Price = lp.energyMetrics.Price()
|
s.Price = lp.energyMetrics.Price()
|
||||||
s.PricePerKWh = lp.energyMetrics.PricePerKWh()
|
s.PricePerKWh = lp.energyMetrics.PricePerKWh()
|
||||||
|
|
|
||||||
|
|
@ -224,6 +224,7 @@ func (lp *Loadpoint) unpublishVehicleIdentity() {
|
||||||
// unpublishVehicle resets published vehicle data
|
// unpublishVehicle resets published vehicle data
|
||||||
func (lp *Loadpoint) unpublishVehicle() {
|
func (lp *Loadpoint) unpublishVehicle() {
|
||||||
lp.vehicleSoc = 0
|
lp.vehicleSoc = 0
|
||||||
|
lp.vehicleRange = 0
|
||||||
|
|
||||||
lp.publish(keys.VehicleClimaterActive, nil)
|
lp.publish(keys.VehicleClimaterActive, nil)
|
||||||
lp.publish(keys.VehicleSoc, 0.0)
|
lp.publish(keys.VehicleSoc, 0.0)
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ type Session struct {
|
||||||
ChargeDuration *time.Duration `json:"chargeDuration" csv:"Charge Duration" gorm:"column:charge_duration"`
|
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"`
|
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"`
|
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"`
|
SolarPercentage *float64 `json:"solarPercentage" csv:"Solar (%)" gorm:"column:solar_percentage"`
|
||||||
Price *float64 `json:"price" csv:"Price" gorm:"column:price"`
|
Price *float64 `json:"price" csv:"Price" gorm:"column:price"`
|
||||||
PricePerKWh *float64 `json:"pricePerKWh" csv:"Price/kWh" gorm:"column:price_per_kwh"`
|
PricePerKWh *float64 `json:"pricePerKWh" csv:"Price/kWh" gorm:"column:price_per_kwh"`
|
||||||
|
|
|
||||||
|
|
@ -1582,6 +1582,7 @@
|
||||||
},
|
},
|
||||||
"session": {
|
"session": {
|
||||||
"addOdometer": "Wert eintragen",
|
"addOdometer": "Wert eintragen",
|
||||||
|
"addedRange": "Geladene Reichweite",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"co2": "CO₂",
|
"co2": "CO₂",
|
||||||
"date": "Zeitraum",
|
"date": "Zeitraum",
|
||||||
|
|
@ -1597,6 +1598,7 @@
|
||||||
"title": "Ladevorgang"
|
"title": "Ladevorgang"
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
|
"addedRange": "Reichweite",
|
||||||
"avgPower": "⌀ Leistung",
|
"avgPower": "⌀ Leistung",
|
||||||
"avgPrice": "⌀ Preis",
|
"avgPrice": "⌀ Preis",
|
||||||
"chargeDuration": "Ladedauer",
|
"chargeDuration": "Ladedauer",
|
||||||
|
|
@ -1622,6 +1624,7 @@
|
||||||
},
|
},
|
||||||
"co2": "⌀ CO₂",
|
"co2": "⌀ CO₂",
|
||||||
"csv": {
|
"csv": {
|
||||||
|
"addedrange": "Geladene Reichweite (km)",
|
||||||
"chargedenergy": "Energie (kWh)",
|
"chargedenergy": "Energie (kWh)",
|
||||||
"chargeduration": "Ladedauer",
|
"chargeduration": "Ladedauer",
|
||||||
"co2perkwh": "CO₂/kWh",
|
"co2perkwh": "CO₂/kWh",
|
||||||
|
|
|
||||||
|
|
@ -1582,6 +1582,7 @@
|
||||||
},
|
},
|
||||||
"session": {
|
"session": {
|
||||||
"addOdometer": "Add value",
|
"addOdometer": "Add value",
|
||||||
|
"addedRange": "Added range",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"co2": "CO₂",
|
"co2": "CO₂",
|
||||||
"date": "Period",
|
"date": "Period",
|
||||||
|
|
@ -1592,11 +1593,12 @@
|
||||||
"meterstop": "Meter stop",
|
"meterstop": "Meter stop",
|
||||||
"odometer": "Mileage",
|
"odometer": "Mileage",
|
||||||
"price": "Cost",
|
"price": "Cost",
|
||||||
"soc": "Vehicle SoC",
|
"soc": "Charge",
|
||||||
"started": "Started",
|
"started": "Started",
|
||||||
"title": "Charging Session"
|
"title": "Charging Session"
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
|
"addedRange": "Range",
|
||||||
"avgPower": "⌀ Power",
|
"avgPower": "⌀ Power",
|
||||||
"avgPrice": "⌀ Price",
|
"avgPrice": "⌀ Price",
|
||||||
"chargeDuration": "Duration",
|
"chargeDuration": "Duration",
|
||||||
|
|
@ -1622,6 +1624,7 @@
|
||||||
},
|
},
|
||||||
"co2": "⌀ CO₂",
|
"co2": "⌀ CO₂",
|
||||||
"csv": {
|
"csv": {
|
||||||
|
"addedrange": "Added range (km)",
|
||||||
"chargedenergy": "Energy (kWh)",
|
"chargedenergy": "Energy (kWh)",
|
||||||
"chargeduration": "Duration",
|
"chargeduration": "Duration",
|
||||||
"co2perkwh": "CO₂/kWh",
|
"co2perkwh": "CO₂/kWh",
|
||||||
|
|
|
||||||
|
|
@ -56,10 +56,11 @@ test("soc range", async ({ page }) => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await expect(page.getByTestId("vehicle-status")).toContainText("Charging");
|
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 page.goto(simulatorUrl());
|
||||||
await loadpoint.getByLabel("Energy").fill("10");
|
await loadpoint.getByLabel("Energy").fill("10");
|
||||||
await vehicle.getByLabel("SoC").fill("80");
|
await vehicle.getByLabel("SoC").fill("80");
|
||||||
|
await vehicle.getByLabel("Range").fill("400");
|
||||||
await simulatorApply(page);
|
await simulatorApply(page);
|
||||||
|
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
|
|
@ -79,4 +80,7 @@ test("soc range", async ({ page }) => {
|
||||||
const modal = page.getByTestId("session-details");
|
const modal = page.getByTestId("session-details");
|
||||||
await expectModalVisible(modal);
|
await expectModalVisible(modal);
|
||||||
await expect(modal.getByTestId("session-details-soc")).toContainText("20 – 80%");
|
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");
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -55,12 +55,16 @@ test.describe("basics", async () => {
|
||||||
await expect(page.getByTestId("sessions-nodata")).toHaveCount(0);
|
await expect(page.getByTestId("sessions-nodata")).toHaveCount(0);
|
||||||
await expect(page.getByRole("table")).toBeVisible();
|
await expect(page.getByRole("table")).toBeVisible();
|
||||||
await expect(page.getByTestId("sessions-head")).toHaveCount(1);
|
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-head-energy")).toContainText("ChargedkWh");
|
||||||
await expect(page.getByTestId("sessions-foot-energy")).toBeVisible();
|
await expect(page.getByTestId("sessions-foot-energy")).toBeVisible();
|
||||||
await expect(page.getByTestId("sessions-foot-energy")).toHaveText("20.0");
|
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-head-solar")).toContainText("Solar%");
|
||||||
await expect(page.getByTestId("sessions-foot-solar")).toBeVisible();
|
await expect(page.getByTestId("sessions-foot-solar")).toBeVisible();
|
||||||
await expect(page.getByTestId("sessions-foot-solar")).toHaveText("67.3");
|
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.setViewportSize({ width: 800, height: 800 });
|
||||||
await page.goto("/#/sessions?year=2023&month=3");
|
await page.goto("/#/sessions?year=2023&month=3");
|
||||||
await expect(
|
await expect(
|
||||||
page.getByTestId("sessions-head-avgPrice").locator("option[value=odometer]")
|
page.getByTestId("sessions-head-price").locator("option[value=odometer]")
|
||||||
).toHaveCount(1);
|
).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-head-odometer")).toBeVisible();
|
||||||
await expect(page.getByTestId("sessions-entry").nth(0)).toContainText("12,345");
|
await expect(page.getByTestId("sessions-entry").nth(0)).toContainText("12,345");
|
||||||
await expect(page.getByTestId("sessions-foot-odometer")).toHaveText("");
|
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-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-price")).toContainText("2.50 € 50.0 ct/kWh");
|
||||||
await expect(modal.getByTestId("session-details-co2")).toHaveCount(0);
|
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-odometer")).toContainText("Add value");
|
||||||
await expect(modal.getByTestId("session-details-meter")).toHaveCount(0);
|
await expect(modal.getByTestId("session-details-meter")).toHaveCount(0);
|
||||||
await expect(modal.getByTestId("session-details-delete")).toContainText("Delete");
|
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-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("3 kg");
|
||||||
await expect(modal.getByTestId("session-details-co2")).toContainText("300 g/kWh");
|
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");
|
await expect(modal.getByTestId("session-details-odometer")).toContainText("12,345 km");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,12 @@ CREATE TABLE `sessions` (
|
||||||
`price_per_kwh` real,
|
`price_per_kwh` real,
|
||||||
`co2_per_kwh` real,
|
`co2_per_kwh` real,
|
||||||
`charge_duration` integer,
|
`charge_duration` integer,
|
||||||
|
`added_range` real,
|
||||||
PRIMARY KEY (`id`)
|
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 (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);
|
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);
|
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);
|
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);
|
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);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue