Loadpoint: fix updating charger soc (#26364)

This commit is contained in:
andig 2026-01-12 09:12:47 +01:00 • committed by GitHub
parent 5e9ad5e447
commit ea18e19a7f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 226 additions and 287 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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