diff --git a/assets/js/components/Sessions/SessionDetailsModal.vue b/assets/js/components/Sessions/SessionDetailsModal.vue index 59b99efad..c44a373ed 100644 --- a/assets/js/components/Sessions/SessionDetailsModal.vue +++ b/assets/js/components/Sessions/SessionDetailsModal.vue @@ -113,6 +113,14 @@ {{ fmtCo2Medium(session.co2PerKWh) }} + + + {{ $t("session.soc") }} + + + {{ socRange }} + + {{ $t("session.odometer") }} @@ -205,6 +213,14 @@ export default defineComponent({ solarEnergy() { return this.chargedEnergy * (this.session.solarPercentage / 100); }, + socRange(): string { + const { socStart, socEnd } = this.session; + if (socStart == null || socEnd == null) { + return ""; + } + const added = socEnd - socStart; + return `${this.fmtPercentage(added, 0, true)} (${this.fmtNumber(socStart, 0)} – ${this.fmtPercentage(socEnd, 0)})`; + }, vehicleOptions(): SelectOption[] { return this.vehicles.map((v) => ({ name: v.title, diff --git a/assets/js/components/Sessions/SessionTable.vue b/assets/js/components/Sessions/SessionTable.vue index ce4a4fbe3..5c81e44f8 100644 --- a/assets/js/components/Sessions/SessionTable.vue +++ b/assets/js/components/Sessions/SessionTable.vue @@ -215,6 +215,18 @@ export default defineComponent({ value: (session) => session.chargedEnergy, format: (value) => this.fmtWh(value * 1e3, POWER_UNIT.KW, false), }, + { + name: "chargedSoc", + unit: "%", + total: this.chargedSoc, + value: (session) => { + if (session.socStart != null && session.socEnd != null) { + return Math.max(0, session.socEnd - session.socStart); + } + return null; + }, + format: (value) => `+${Math.round(value)}`, + }, { name: "solar", unit: "%", @@ -340,6 +352,16 @@ export default defineComponent({ chargedEnergy() { return this.filteredSessions.reduce((total, s) => total + s.chargedEnergy, 0); }, + chargedSoc() { + const sessions = this.filteredSessions.filter( + (s) => s.socStart != null && s.socEnd != null + ); + if (!sessions.length) return null; + return sessions.reduce( + (total, s) => total + Math.max(0, (s.socEnd || 0) - (s.socStart || 0)), + 0 + ); + }, chargeDuration() { return this.filteredSessions.reduce((total, s) => total + s.chargeDuration, 0); }, diff --git a/assets/js/components/Sessions/types.ts b/assets/js/components/Sessions/types.ts index c15755aaa..beb03b156 100644 --- a/assets/js/components/Sessions/types.ts +++ b/assets/js/components/Sessions/types.ts @@ -10,6 +10,8 @@ export interface Session { meterStop: number; chargedEnergy: number; chargeDuration: number; + socStart: number | null; + socEnd: number | null; solarPercentage: number; price: number | null; pricePerKWh: number | null; diff --git a/core/loadpoint.go b/core/loadpoint.go index 9ad9002dc..572304546 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -493,6 +493,10 @@ func (lp *Loadpoint) evChargeStartHandler() { if session.Created.IsZero() { session.Created = lp.clock.Now() } + // capture start soc once available (may not be present at session start) + if soc := lp.vehicleSoc; session.SocStart == nil && soc > 0 && !lp.chargerHasFeature(api.Heating) { + session.SocStart = &soc + } }) } diff --git a/core/loadpoint_session.go b/core/loadpoint_session.go index d30315ab8..843bdcde3 100644 --- a/core/loadpoint_session.go +++ b/core/loadpoint_session.go @@ -70,6 +70,10 @@ func (lp *Loadpoint) applyEnergyMetrics(s *session.Session) { s.MeterStop = &meterStop } + if soc := lp.vehicleSoc; soc > 0 && !lp.chargerHasFeature(api.Heating) { + s.SocEnd = &soc + } + s.SolarPercentage = new(lp.energyMetrics.SolarPercentage()) s.Price = lp.energyMetrics.Price() s.PricePerKWh = lp.energyMetrics.PricePerKWh() diff --git a/core/session/session.go b/core/session/session.go index 619c52477..82a538933 100644 --- a/core/session/session.go +++ b/core/session/session.go @@ -22,6 +22,8 @@ type Session struct { MeterStop *float64 `json:"meterStop" csv:"Meter Stop (kWh)" gorm:"column:meter_end_kwh"` ChargedEnergy float64 `json:"chargedEnergy" csv:"Charged Energy (kWh)" gorm:"column:charged_kwh"` 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"` 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 b8a8c4d2c..e0ebf2a22 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -1413,6 +1413,7 @@ "meterstop": "Endzählerstand", "odometer": "Kilometerstand", "price": "Kosten", + "soc": "Ladestand", "started": "Startzeit", "title": "Ladevorgang" }, @@ -1420,6 +1421,7 @@ "avgPower": "⌀ Leistung", "avgPrice": "⌀ Preis", "chargeDuration": "Ladedauer", + "chargedSoc": "Ladung", "chartTitle": { "avgCo2ByGroup": "⌀ CO₂ {byGroup}", "avgPriceByGroup": "⌀ Preis {byGroup}", @@ -1453,6 +1455,8 @@ "odometer": "Kilometerstand (km)", "price": "Kosten", "priceperkwh": "Preis/kWh", + "socend": "Ladestand Ende (%)", + "socstart": "Ladestand Start (%)", "solarpercentage": "Sonne (%)", "vehicle": "Fahrzeug" }, diff --git a/i18n/en.json b/i18n/en.json index a1c853986..73f528edd 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1413,6 +1413,7 @@ "meterstop": "Meter stop", "odometer": "Mileage", "price": "Cost", + "soc": "Vehicle SoC", "started": "Started", "title": "Charging Session" }, @@ -1420,6 +1421,7 @@ "avgPower": "⌀ Power", "avgPrice": "⌀ Price", "chargeDuration": "Duration", + "chargedSoc": "Charge", "chartTitle": { "avgCo2ByGroup": "⌀ CO₂ {byGroup}", "avgPriceByGroup": "⌀ Price {byGroup}", @@ -1453,6 +1455,8 @@ "odometer": "Mileage (km)", "price": "Cost", "priceperkwh": "Price/kWh", + "socend": "SoC end (%)", + "socstart": "SoC start (%)", "solarpercentage": "Solar (%)", "vehicle": "Vehicle" },