From ea18e19a7f5e123a6e4e3860b887bbcdc4b7c313 Mon Sep 17 00:00:00 2001 From: andig Date: Mon, 12 Jan 2026 09:12:47 +0100 Subject: [PATCH] Loadpoint: fix updating charger soc (#26364) --- core/loadpoint.go | 117 +++++++++++++++-------------- core/loadpoint_charger.go | 9 --- core/loadpoint_plan.go | 5 +- core/loadpoint_test.go | 2 +- core/loadpoint_vehicle.go | 6 +- core/loadpoint_vehicle_test.go | 132 +++++++++++++++++++++++++++++---- core/soc/estimator.go | 114 +++++++++------------------- core/soc/estimator_test.go | 128 +++----------------------------- 8 files changed, 226 insertions(+), 287 deletions(-) diff --git a/core/loadpoint.go b/core/loadpoint.go index 46993bdae..522ae6c64 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -1730,87 +1730,86 @@ func (lp *Loadpoint) publishSocAndRange() { // https://github.com/evcc-io/evcc/issues/16180 socEstimator := lp.socEstimator - // capacity not available - if socEstimator == nil || !lp.vehicleHasSoc() { - if soc, err := lp.chargerSoc(); err == nil { - lp.vehicleSoc = soc - lp.publish(keys.VehicleSoc, lp.vehicleSoc) + socAndLimit := func(typ string, dev any) (*float64, *int64) { + var socR *float64 + var limitR *int64 - if vs, ok := lp.charger.(api.SocLimiter); ok { - if limit, err := vs.GetLimitSoc(); err == nil { - lp.log.DEBUG.Printf("charger soc limit: %d%%", limit) - // https://github.com/evcc-io/evcc/issues/13349 - lp.publish(keys.VehicleLimitSoc, float64(limit)) - } else if !loadpoint.AcceptableError(err) { - lp.log.ERROR.Printf("charger soc limit: %v", err) + if battery, ok := dev.(api.Battery); ok { + if soc, err := soc.Guard(battery.Soc()); err == nil { + socR = &soc + + // don't publish here in case it needs be updated by the estimator + lp.log.DEBUG.Printf("%s soc: %.0f%%", typ, soc) + + if socLimiter, ok := dev.(api.SocLimiter); ok { + if limit, err := socLimiter.GetLimitSoc(); err == nil { + limitR = &limit + + lp.log.DEBUG.Printf("%s soc limit: %d%%", typ, limit) + // https://github.com/evcc-io/evcc/issues/13349 + lp.publish(keys.VehicleLimitSoc, float64(limit)) + } else if !loadpoint.AcceptableError(err) { + lp.log.ERROR.Printf("%s soc limit: %v", typ, err) + } } + } else if !loadpoint.AcceptableError(err) { + lp.log.ERROR.Printf("charger soc: %v", err) } - } else if !loadpoint.AcceptableError(err) { - lp.log.ERROR.Printf("charger soc: %v", err) } - return + return socR, limitR } - // integrated device can bypass the update interval if vehicle is separately configured (legacy) - if lp.chargerHasFeature(api.IntegratedDevice) || lp.vehicleSocPollAllowed() { + soc, limit := socAndLimit("charger", lp.charger) + if soc == nil && (lp.chargerHasFeature(api.IntegratedDevice) || lp.vehicleSocPollAllowed()) { lp.socUpdated = lp.clock.Now() + soc, limit = socAndLimit("vehicle", lp.GetVehicle()) + } - f, err := socEstimator.Soc(lp.GetChargedEnergy()) - if err != nil { - if loadpoint.AcceptableError(err) { - lp.socUpdated = time.Time{} - } else { - lp.log.ERROR.Printf("vehicle soc: %v", err) - } - - return + if soc != nil { + if socEstimator == nil { + lp.vehicleSoc = *soc + } else { + lp.vehicleSoc, _ = socEstimator.Soc(soc, lp.GetChargedEnergy()) + lp.log.DEBUG.Printf("vehicle soc (estimator): %.0f%%", lp.vehicleSoc) } + } + lp.publish(keys.VehicleSoc, lp.vehicleSoc) - lp.vehicleSoc = f - lp.log.DEBUG.Printf("vehicle soc: %.0f%%", lp.vehicleSoc) - lp.publish(keys.VehicleSoc, lp.vehicleSoc) - - // vehicle target soc - // TODO take vehicle api limits into account - apiLimitSoc := 100 - - // vehicle limit - if vs, ok := lp.GetVehicle().(api.SocLimiter); ok { - if limit, err := vs.GetLimitSoc(); err == nil { - apiLimitSoc = int(limit) - lp.log.DEBUG.Printf("vehicle soc limit: %d%%", limit) - // https://github.com/evcc-io/evcc/issues/13349 - lp.publish(keys.VehicleLimitSoc, float64(limit)) - } else if !loadpoint.AcceptableError(err) { - lp.log.ERROR.Printf("vehicle soc limit: %v", err) - } - } + apiLimitSoc := 100 + if limit != nil { + apiLimitSoc = int(*limit) + // https://github.com/evcc-io/evcc/issues/13349 + lp.publish(keys.VehicleLimitSoc, float64(*limit)) + } + if socEstimator != nil { // use minimum of vehicle and loadpoint limitSoc := min(apiLimitSoc, lp.EffectiveLimitSoc()) var d time.Duration if lp.charging() { - d = socEstimator.RemainingChargeDuration(limitSoc, lp.chargePower) + d = socEstimator.RemainingChargeDuration(float64(limitSoc), lp.chargePower) } lp.SetRemainingDuration(d) lp.SetRemainingEnergy(socEstimator.RemainingChargeEnergy(limitSoc)) - - // range - if vs, ok := lp.GetVehicle().(api.VehicleRange); ok { - if rng, err := vs.Range(); err == nil { - lp.log.DEBUG.Printf("vehicle range: %dkm", rng) - lp.publish(keys.VehicleRange, rng) - } else if !loadpoint.AcceptableError(err) { - lp.log.ERROR.Printf("vehicle range: %v", err) - } - } - - // trigger message after variables are updated - lp.bus.Publish(evVehicleSoc, f) } + + // TODO don't rely on vehicle cache + + // range + if vs, ok := lp.GetVehicle().(api.VehicleRange); ok { + if rng, err := vs.Range(); err == nil { + lp.log.DEBUG.Printf("vehicle range: %dkm", rng) + lp.publish(keys.VehicleRange, rng) + } else if !loadpoint.AcceptableError(err) { + lp.log.ERROR.Printf("vehicle range: %v", err) + } + } + + // trigger message after variables are updated + lp.bus.Publish(evVehicleSoc, lp.vehicleSoc) } // addTask adds a single task to the queue diff --git a/core/loadpoint_charger.go b/core/loadpoint_charger.go index 8a09eecb1..a6e798095 100644 --- a/core/loadpoint_charger.go +++ b/core/loadpoint_charger.go @@ -3,7 +3,6 @@ package core import ( "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/core/keys" - "github.com/evcc-io/evcc/core/soc" ) // chargerHasFeature checks availability of charger feature @@ -15,11 +14,3 @@ func (lp *Loadpoint) chargerHasFeature(f api.Feature) bool { func (lp *Loadpoint) publishChargerFeature(f api.Feature) { lp.publish(keys.ChargerFeature+f.String(), lp.chargerHasFeature(f)) } - -// chargerSoc returns charger soc if available -func (lp *Loadpoint) chargerSoc() (float64, error) { - if c, ok := lp.charger.(api.Battery); ok { - return soc.Guard(c.Soc()) - } - return 0, api.ErrNotAvailable -} diff --git a/core/loadpoint_plan.go b/core/loadpoint_plan.go index 7e2839966..4c6d3ce47 100644 --- a/core/loadpoint_plan.go +++ b/core/loadpoint_plan.go @@ -7,6 +7,7 @@ import ( "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/core/keys" "github.com/evcc-io/evcc/core/planner" + "github.com/evcc-io/evcc/core/soc" "github.com/evcc-io/evcc/core/vehicle" "github.com/evcc-io/evcc/tariff" ) @@ -52,9 +53,9 @@ func (lp *Loadpoint) GetPlanRequiredDuration(goal, maxPower float64) time.Durati func (lp *Loadpoint) getPlanRequiredDuration(goal, maxPower float64) time.Duration { if lp.socBasedPlanning() { if lp.socEstimator == nil { - return 0 + return soc.RemainingChargeDuration(goal, maxPower, lp.vehicleSoc, lp.GetVehicle().Capacity()) } - return lp.socEstimator.RemainingChargeDuration(int(goal), maxPower) + return lp.socEstimator.RemainingChargeDuration(goal, maxPower) } energy := lp.remainingPlanEnergy(goal) diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go index 3b6c1975f..8c6a41261 100644 --- a/core/loadpoint_test.go +++ b/core/loadpoint_test.go @@ -390,7 +390,7 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) { // wrap vehicle with estimator expectVehiclePublish(vehicle) - socEstimator := soc.NewEstimator(util.NewLogger("foo"), charger, vehicle, false) + socEstimator := soc.NewEstimator(util.NewLogger("foo"), charger, vehicle) lp := &Loadpoint{ log: util.NewLogger("foo"), diff --git a/core/loadpoint_vehicle.go b/core/loadpoint_vehicle.go index f74af1502..e68d29ea1 100644 --- a/core/loadpoint_vehicle.go +++ b/core/loadpoint_vehicle.go @@ -133,11 +133,9 @@ func (lp *Loadpoint) setActiveVehicle(v api.Vehicle) { lp.socUpdated = time.Time{} // resolve optional config - var estimate bool - if lp.Soc.Estimate == nil || *lp.Soc.Estimate { - estimate = true + if v.Capacity() > 0 && (lp.Soc.Estimate == nil || *lp.Soc.Estimate) { + lp.socEstimator = soc.NewEstimator(lp.log, lp.charger, v) } - lp.socEstimator = soc.NewEstimator(lp.log, lp.charger, v, estimate) lp.publish(keys.VehicleName, vehicle.Settings(lp.log, v).Name()) lp.publish(keys.VehicleTitle, v.GetTitle()) diff --git a/core/loadpoint_vehicle_test.go b/core/loadpoint_vehicle_test.go index 4c1ea4490..c61ddca8a 100644 --- a/core/loadpoint_vehicle_test.go +++ b/core/loadpoint_vehicle_test.go @@ -30,8 +30,6 @@ func TestPublishSocAndRange(t *testing.T) { clck := clock.NewMock() charger := api.NewMockCharger(ctrl) - charger.EXPECT().MaxCurrent(int64(maxA)).AnyTimes() - charger.EXPECT().Enabled().Return(true, nil).AnyTimes() vehicle := api.NewMockVehicle(ctrl) expectVehiclePublish(vehicle) @@ -46,7 +44,7 @@ func TestPublishSocAndRange(t *testing.T) { chargeMeter: &Null{}, // silence nil panics chargeRater: &Null{}, // silence nil panics chargeTimer: &Null{}, // silence nil panics - socEstimator: soc.NewEstimator(log, charger, vehicle, false), + socEstimator: soc.NewEstimator(log, charger, vehicle), minCurrent: minA, maxCurrent: maxA, phases: 1, @@ -59,31 +57,137 @@ func TestPublishSocAndRange(t *testing.T) { assert.Empty(t, lp.socUpdated) - tc := []struct { - status api.ChargeStatus - allowed bool - }{ - {api.StatusB, false}, - {api.StatusC, true}, - } + tc := []api.ChargeStatus{api.StatusB, api.StatusC} for _, tc := range tc { - clck.Add(time.Hour) - lp.status = tc.status + lp.status = tc assert.True(t, lp.vehicleSocPollAllowed()) vehicle.EXPECT().Soc().Return(0.0, errors.New("foo")) lp.publishSocAndRange() clck.Add(time.Second) - assert.Equal(t, tc.allowed, lp.vehicleSocPollAllowed()) - if tc.allowed { + + allowed := tc == api.StatusC + assert.Equal(t, allowed, lp.vehicleSocPollAllowed()) + if allowed { vehicle.EXPECT().Soc().Return(0.0, errors.New("foo")) } lp.publishSocAndRange() } } +func TestPublishSocAndRangeVehiclesAndChargers(t *testing.T) { + ctrl := gomock.NewController(t) + clck := clock.NewMock() + + socVehicle := 70.0 + socCharger := 80.0 + + vehicle := api.NewMockVehicle(ctrl) + vehicle.EXPECT().Soc().Return(socVehicle, nil).AnyTimes() + vehicle.EXPECT().Capacity().Return(8.5).AnyTimes() // enable soc-based planning + vehicle.EXPECT().Features().AnyTimes() + + offlineVehicle := api.NewMockVehicle(ctrl) + offlineVehicle.EXPECT().Soc().AnyTimes() + offlineVehicle.EXPECT().Capacity().Return(8.5).AnyTimes() // enable soc-based planning + offlineVehicle.EXPECT().Features().Return([]api.Feature{api.Offline}).AnyTimes() + + charger := api.NewMockCharger(ctrl) + + chargerSoc := api.NewMockBattery(ctrl) + chargerSoc.EXPECT().Soc().Return(socCharger, nil).AnyTimes() + + isoCharger := struct { + *api.MockCharger + *api.MockBattery + }{ + charger, chargerSoc, + } + + log := util.NewLogger("foo") + + tc := []struct { + name string + charger api.Charger + vehicle api.Vehicle + soc float64 + socBased bool + }{ + { + name: "offline vehicle", + charger: charger, + vehicle: offlineVehicle, + soc: 0.0, + socBased: false, + }, + { + name: "regular vehicle", + charger: charger, + vehicle: vehicle, + soc: socVehicle, + socBased: true, + }, + { + name: "offline vehicle with iso charger", + charger: isoCharger, + vehicle: offlineVehicle, + soc: socCharger, + socBased: true, + }, + { + name: "regular vehicle with iso charger", + charger: isoCharger, + vehicle: vehicle, + soc: socCharger, + socBased: true, + }, + } + + for _, tc := range tc { + lp := &Loadpoint{ + log: log, + bus: evbus.New(), + clock: clck, + charger: tc.charger, + vehicle: tc.vehicle, + chargeMeter: &Null{}, // silence nil panics + chargeRater: &Null{}, // silence nil panics + chargeTimer: &Null{}, // silence nil panics + minCurrent: minA, + maxCurrent: maxA, + phases: 1, + status: api.StatusC, + mode: api.ModeNow, + } + + // populate channels + x, y, z := createChannels(t) + attachChannels(lp, x, y, z) + + test := func(t *testing.T) { + assert.True(t, lp.vehicleSocPollAllowed()) + lp.publishSocAndRange() + assert.Equal(t, tc.soc, lp.vehicleSoc) + + // planner assumptions + assert.Equal(t, tc.socBased, lp.socBasedPlanning()) + + if tc.soc > 0 { + d := time.Duration((1 - tc.soc/100) * float64(time.Hour)) + t.Log("d", d) + assert.True(t, d < lp.GetPlanRequiredDuration(100, 10e3)) + } + } + + t.Run(tc.name+" wo/estimator", test) + + lp.socEstimator = soc.NewEstimator(log, tc.charger, tc.vehicle) + t.Run(tc.name+" w/estimator", test) + } +} + func TestVehicleDetectByID(t *testing.T) { ctrl := gomock.NewController(t) diff --git a/core/soc/estimator.go b/core/soc/estimator.go index 8c2a355d5..e6b1e0fe8 100644 --- a/core/soc/estimator.go +++ b/core/soc/estimator.go @@ -1,23 +1,29 @@ package soc import ( - "errors" "time" "github.com/evcc-io/evcc/api" - "github.com/evcc-io/evcc/core/loadpoint" "github.com/evcc-io/evcc/util" ) -const ChargeEfficiency = 0.85 // assume 85% charge efficiency +const ( + ChargeEfficiency = 0.85 // assume 85% charge efficiency + + minChargePower = 1000.0 // Lowest charge power (just before vehicle stops charging at 100%) + maxChargePower = 50000.0 // default 50 kW + maxChargeSoc = 50.0 // default 50% + minChargeSoc = 100.0 + + gradient = (minChargePower - maxChargePower) / (minChargeSoc - maxChargeSoc) +) // Estimator provides vehicle soc and charge duration // Vehicle Soc can be estimated to provide more granularity type Estimator struct { - log *util.Logger - charger api.Charger - vehicle api.Vehicle - estimate bool + log *util.Logger + charger api.Charger + vehicle api.Vehicle capacity float64 // vehicle capacity in Wh cached to simplify testing virtualCapacity float64 // estimated virtual vehicle capacity in Wh @@ -27,18 +33,14 @@ type Estimator struct { prevSoc float64 // previous vehicle Soc in % prevChargedEnergy float64 // previous charged energy in Wh energyPerSocStep float64 // Energy per Soc percent in Wh - minChargePower float64 // Lowest charge power (just before vehicle stops charging at 100%) - maxChargePower float64 // Highest charge power the battery can handle on any charger - maxChargeSoc float64 // SoC at/after which maxChargePower is degressive } // NewEstimator creates new estimator -func NewEstimator(log *util.Logger, charger api.Charger, vehicle api.Vehicle, estimate bool) *Estimator { +func NewEstimator(log *util.Logger, charger api.Charger, vehicle api.Vehicle) *Estimator { s := &Estimator{ - log: log, - charger: charger, - vehicle: vehicle, - estimate: estimate, + log: log, + charger: charger, + vehicle: vehicle, } s.Reset() @@ -54,38 +56,31 @@ func (s *Estimator) Reset() { s.capacity = s.vehicle.Capacity() * 1e3 // cache to simplify debugging s.virtualCapacity = s.capacity / ChargeEfficiency // initial capacity taking efficiency into account s.energyPerSocStep = s.virtualCapacity / 100 - s.minChargePower = 1000 // default 1 kW - s.maxChargePower = 50000 // default 50 kW - s.maxChargeSoc = 50 // default 50% } // RemainingChargeDuration returns the estimated remaining duration -func (s *Estimator) RemainingChargeDuration(targetSoc int, chargePower float64) time.Duration { - const minChargeSoc = 100 +func (s *Estimator) RemainingChargeDuration(targetSoc, chargePower float64) time.Duration { + return remainingChargeDuration(targetSoc, chargePower, s.vehicleSoc, s.virtualCapacity) +} - dy := s.minChargePower - s.maxChargePower - dx := minChargeSoc - s.maxChargeSoc +func RemainingChargeDuration(targetSoc, chargePower, vehicleSoc, virtualCapacity float64) time.Duration { + return remainingChargeDuration(targetSoc, chargePower, vehicleSoc, virtualCapacity*1e3/ChargeEfficiency) +} - var rrp float64 = 100 - - if dy < 0 && dx > 0 { - m := dy / dx - b := s.minChargePower - m*minChargeSoc - - // Relativer Reduktionspunkt - rrp = (chargePower - b) / m - } +func remainingChargeDuration(targetSoc, chargePower, vehicleSoc, virtualCapacity float64) time.Duration { + // Relativer Reduktionspunkt + rrp := (chargePower-minChargePower)/gradient + minChargeSoc var t1, t2 float64 // Zeit von vehicleSoc bis Reduktionspunkt (linear) - if s.vehicleSoc < rrp { - t1 = (min(float64(targetSoc), rrp) - s.vehicleSoc) / minChargeSoc * s.virtualCapacity / chargePower + if vehicleSoc < rrp { + t1 = (min(float64(targetSoc), rrp) - vehicleSoc) / minChargeSoc * virtualCapacity / chargePower } // Zeit von Reduktionspunkt bis targetSoc (degressiv) if float64(targetSoc) > rrp { - t2 = (float64(targetSoc) - max(s.vehicleSoc, rrp)) / minChargeSoc * s.virtualCapacity / ((chargePower-s.minChargePower)/2 + s.minChargePower) + t2 = (float64(targetSoc) - max(vehicleSoc, rrp)) / minChargeSoc * virtualCapacity / ((chargePower-minChargePower)/2 + minChargePower) } return max(0, time.Duration(float64(time.Hour)*(t1+t2))).Round(time.Second) @@ -104,53 +99,14 @@ func (s *Estimator) RemainingChargeEnergy(targetSoc int) float64 { } // Soc replaces the api.Vehicle.Soc interface to take charged energy into account -func (s *Estimator) Soc(chargedEnergy float64) (float64, error) { - var fetchedSoc *float64 - - if charger, ok := s.charger.(api.Battery); ok { - f, err := Guard(charger.Soc()) - - // if the charger does or could provide Soc, we always use it instead of using the vehicle API - if err == nil || !errors.Is(err, api.ErrNotAvailable) { - if err != nil { - // never received a soc value - if s.prevSoc == 0 { - return 0, err - } - - // recover from temporary api errors - f = s.prevSoc - s.log.WARN.Printf("vehicle soc (charger): %v (ignored by estimator)", err) - } - - fetchedSoc = &f - s.vehicleSoc = f - } +func (s *Estimator) Soc(fetchedSoc *float64, chargedEnergy float64) (float64, error) { + if fetchedSoc != nil { + s.vehicleSoc = *fetchedSoc + } else { + s.log.WARN.Printf("missing vehicle soc- ignored by estimator") } - if fetchedSoc == nil { - f, err := Guard(s.vehicle.Soc()) - if err != nil { - // required for online APIs with refreshkey - if loadpoint.AcceptableError(err) { - return 0, err - } - - // never received a soc value - if s.prevSoc == 0 { - return 0, err - } - - // recover from temporary api errors - f = s.prevSoc - s.log.WARN.Printf("vehicle soc: %v (ignored by estimator)", err) - } - - fetchedSoc = &f - s.vehicleSoc = f - } - - if s.estimate && s.virtualCapacity > 0 { + if s.virtualCapacity > 0 { socDelta := s.vehicleSoc - s.prevSoc energyDelta := max(chargedEnergy, 0) - s.prevChargedEnergy diff --git a/core/soc/estimator_test.go b/core/soc/estimator_test.go index b1bb20bec..ad6b73d5b 100644 --- a/core/soc/estimator_test.go +++ b/core/soc/estimator_test.go @@ -1,7 +1,6 @@ package soc import ( - "errors" "testing" "time" @@ -18,11 +17,11 @@ func TestRemainingChargeDuration(t *testing.T) { // 8.5 kWh userBatCap => 10 kWh virtualBatCap (at 85% efficiency) vehicle.EXPECT().Capacity().Return(float64(8.5)) - ce := NewEstimator(util.NewLogger("foo"), charger, vehicle, false) + ce := NewEstimator(util.NewLogger("foo"), charger, vehicle) ce.vehicleSoc = 20.0 chargePower := 1000.0 - targetSoc := 80 + targetSoc := 80.0 if remaining := ce.RemainingChargeDuration(targetSoc, chargePower); remaining != 6*time.Hour { t.Errorf("wrong remaining charge duration: %v", remaining) @@ -40,11 +39,9 @@ func TestSocEstimation(t *testing.T) { charger := &chargerStruct{api.NewMockCharger(ctrl), api.NewMockBattery(ctrl)} // 8.5 kWh user battery capacity is converted to initial value of 10 kWh virtual capacity (at 85% efficiency) - var capacity float64 = 8.5 - vehicle.EXPECT().Capacity().Return(capacity) + vehicle.EXPECT().Capacity().Return(8.5) - ce := NewEstimator(util.NewLogger("foo"), charger, vehicle, true) - ce.vehicleSoc = 0.0 + ce := NewEstimator(util.NewLogger("foo"), charger, vehicle) tc := []struct { chargedEnergy float64 @@ -71,119 +68,12 @@ func TestSocEstimation(t *testing.T) { {1000, 30.0, 30.0, 10000}, } - for i := 1; i < 3; i++ { - useVehicleSoc := true - if i == 2 { - useVehicleSoc = false - } - for _, tc := range tc { - t.Logf("%+v", tc) - if useVehicleSoc { - charger.MockBattery.EXPECT().Soc().Return(tc.vehicleSoc, nil) - } else { - charger.MockBattery.EXPECT().Soc().Return(0.0, api.ErrNotAvailable) - vehicle.EXPECT().Soc().Return(tc.vehicleSoc, nil) - } - - soc, err := ce.Soc(tc.chargedEnergy) - if err != nil { - t.Error(err) - } - - // validate soc estimate - if tc.estimatedSoc != soc { - t.Errorf("expected estimated soc: %g, got: %g", tc.estimatedSoc, soc) - } - - // validate capacity estimate - if tc.virtualCapacity != ce.virtualCapacity { - t.Errorf("expected virtual capacity: %v, got: %v", tc.virtualCapacity, ce.virtualCapacity) - } - - // validate duration estimate - chargePower := 1e3 - targetSoc := 100 - remainingHours := (float64(targetSoc) - soc) / 100 * tc.virtualCapacity / chargePower - remainingDuration := time.Duration(float64(time.Hour) * remainingHours).Round(time.Second) - - if rm := ce.RemainingChargeDuration(targetSoc, chargePower); rm != remainingDuration { - t.Errorf("expected estimated duration: %v, got: %v", remainingDuration, rm) - } - } - } -} - -func TestSocFromChargerAndVehicleWithErrors(t *testing.T) { - type chargerStruct struct { - *api.MockCharger - *api.MockBattery - } - - ctrl := gomock.NewController(t) - vehicle := api.NewMockVehicle(ctrl) - charger := &chargerStruct{api.NewMockCharger(ctrl), api.NewMockBattery(ctrl)} - - // 8.5 kWh user battery capacity is converted to initial value of 10 kWh virtual capacity (at 85% efficiency) - var capacity float64 = 8.5 - vehicle.EXPECT().Capacity().Return(capacity) - - ce := NewEstimator(util.NewLogger("foo"), charger, vehicle, true) - ce.vehicleSoc = 20.0 - - tc := []struct { - chargedEnergy float64 - vehicleSoc float64 - estimatedSoc float64 - virtualCapacity float64 - expectVehicle bool - chargerError error - vehicleError error - }{ - // start with Soc from charger and errors - {0, 0.0, 0.0, 10000, false, errors.New("some error"), nil}, - {0, 0.0, 20.0, 10000, false, api.ErrMustRetry, nil}, - {0, 20.0, 20.0, 10000, false, nil, nil}, - {123, 20.0, 21.23, 10000, false, nil, nil}, - {123, 0.0, 21.23, 10000, false, errors.New("another error"), nil}, - {1000, 20.0, 30.0, 10000, false, nil, nil}, - {1100, 31.0, 31.0, 10000, false, nil, nil}, - {1200, 32.0, 32.0, 10000, false, nil, nil}, - {1900, 39.0, 39.0, 10000, false, nil, nil}, - {2000, 40.0, 40.0, 10000, false, nil, nil}, - // move to Soc from vehicle - {3000, 0.0, 50.0, 10000, true, api.ErrNotAvailable, errors.New("some error")}, - {3100, 0.0, 51.0, 10000, true, api.ErrNotAvailable, api.ErrMustRetry}, - {5100, 71.0, 71.0, 10000, true, api.ErrNotAvailable, nil}, - {5200, 72.0, 72.0, 10000, true, api.ErrNotAvailable, nil}, - {5300, 0.0, 73.0, 10000, true, api.ErrNotAvailable, errors.New("another error")}, - {5300, 73.0, 73.0, 10000, true, api.ErrNotAvailable, nil}, - {5500, 75.0, 75.0, 10000, true, api.ErrNotAvailable, nil}, - {6000, 80.0, 80.0, 10000, true, api.ErrNotAvailable, nil}, - {0, 25.0, 25.0, 10000, true, api.ErrNotAvailable, nil}, - {2500, 25.0, 50.0, 10000, true, api.ErrNotAvailable, nil}, - {0, 50.0, 50.0, 10000, true, api.ErrNotAvailable, nil}, // -10000 - {4990, 50.0, 99.9, 10000, true, api.ErrNotAvailable, nil}, - {5000, 50.0, 100.0, 10000, true, api.ErrNotAvailable, nil}, - // back to Soc from charger - {5001, 50.0, 100.0, 10000, false, nil, nil}, - {0, 20.0, 20.0, 10000, false, nil, nil}, - {1000, 30.0, 30.0, 10000, false, nil, nil}, - } - for _, tc := range tc { t.Logf("%+v", tc) - charger.MockBattery.EXPECT().Soc().Return(tc.vehicleSoc, tc.chargerError) - if tc.expectVehicle { - vehicle.EXPECT().Soc().Return(tc.vehicleSoc, tc.vehicleError) - } - soc, err := ce.Soc(tc.chargedEnergy) + soc, err := ce.Soc(&tc.vehicleSoc, tc.chargedEnergy) if err != nil { - if (!tc.expectVehicle && err != tc.chargerError) || (tc.expectVehicle && err != tc.vehicleError) { - t.Error(err) - } else { - continue - } + t.Error(err) } // validate soc estimate @@ -198,7 +88,7 @@ func TestSocFromChargerAndVehicleWithErrors(t *testing.T) { // validate duration estimate chargePower := 1e3 - targetSoc := 100 + targetSoc := 100.0 remainingHours := (float64(targetSoc) - soc) / 100 * tc.virtualCapacity / chargePower remainingDuration := time.Duration(float64(time.Hour) * remainingHours).Round(time.Second) @@ -218,7 +108,7 @@ func TestImprovedEstimatorRemainingChargeDuration(t *testing.T) { tc := []struct { capacity float64 soc float64 - targetsoc int + targetsoc float64 chargePower float64 duration time.Duration }{ @@ -237,7 +127,7 @@ func TestImprovedEstimatorRemainingChargeDuration(t *testing.T) { vehicle.EXPECT().Capacity().Return(tc.capacity) - ce := NewEstimator(util.NewLogger("foo"), charger, vehicle, false) + ce := NewEstimator(util.NewLogger("foo"), charger, vehicle) ce.vehicleSoc = tc.soc assert.Equal(t, tc.duration, ce.RemainingChargeDuration(tc.targetsoc, tc.chargePower))