Loadpoint: fix updating charger soc (#26364)
This commit is contained in:
parent
5e9ad5e447
commit
ea18e19a7f
8 changed files with 226 additions and 287 deletions
|
|
@ -1730,73 +1730,73 @@ 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)
|
||||
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("charger soc limit: %v", err)
|
||||
lp.log.ERROR.Printf("%s soc limit: %v", typ, err)
|
||||
}
|
||||
}
|
||||
} else if !loadpoint.AcceptableError(err) {
|
||||
lp.log.ERROR.Printf("charger soc: %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// integrated device can bypass the update interval if vehicle is separately configured (legacy)
|
||||
if lp.chargerHasFeature(api.IntegratedDevice) || lp.vehicleSocPollAllowed() {
|
||||
return socR, limitR
|
||||
}
|
||||
|
||||
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{}
|
||||
if soc != nil {
|
||||
if socEstimator == nil {
|
||||
lp.vehicleSoc = *soc
|
||||
} else {
|
||||
lp.log.ERROR.Printf("vehicle soc: %v", err)
|
||||
lp.vehicleSoc, _ = socEstimator.Soc(soc, lp.GetChargedEnergy())
|
||||
lp.log.DEBUG.Printf("vehicle soc (estimator): %.0f%%", lp.vehicleSoc)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
if limit != nil {
|
||||
apiLimitSoc = int(*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)
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
||||
// TODO don't rely on vehicle cache
|
||||
|
||||
// range
|
||||
if vs, ok := lp.GetVehicle().(api.VehicleRange); ok {
|
||||
|
|
@ -1809,8 +1809,7 @@ func (lp *Loadpoint) publishSocAndRange() {
|
|||
}
|
||||
|
||||
// trigger message after variables are updated
|
||||
lp.bus.Publish(evVehicleSoc, f)
|
||||
}
|
||||
lp.bus.Publish(evVehicleSoc, lp.vehicleSoc)
|
||||
}
|
||||
|
||||
// addTask adds a single task to the queue
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,22 @@
|
|||
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
|
||||
|
|
@ -17,7 +24,6 @@ type Estimator struct {
|
|||
log *util.Logger
|
||||
charger api.Charger
|
||||
vehicle api.Vehicle
|
||||
estimate bool
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
var rrp float64 = 100
|
||||
|
||||
if dy < 0 && dx > 0 {
|
||||
m := dy / dx
|
||||
b := s.minChargePower - m*minChargeSoc
|
||||
func RemainingChargeDuration(targetSoc, chargePower, vehicleSoc, virtualCapacity float64) time.Duration {
|
||||
return remainingChargeDuration(targetSoc, chargePower, vehicleSoc, virtualCapacity*1e3/ChargeEfficiency)
|
||||
}
|
||||
|
||||
func remainingChargeDuration(targetSoc, chargePower, vehicleSoc, virtualCapacity float64) time.Duration {
|
||||
// Relativer Reduktionspunkt
|
||||
rrp = (chargePower - b) / m
|
||||
}
|
||||
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
|
||||
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")
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,21 +68,10 @@ 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)
|
||||
soc, err := ce.Soc(&tc.vehicleSoc, tc.chargedEnergy)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
|
@ -102,103 +88,7 @@ func TestSocEstimation(t *testing.T) {
|
|||
|
||||
// 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)
|
||||
if err != nil {
|
||||
if (!tc.expectVehicle && err != tc.chargerError) || (tc.expectVehicle && err != tc.vehicleError) {
|
||||
t.Error(err)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
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))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue