Add optional interpolation of SoC between vehicle api updates (#277)

This commit is contained in:
premultiply 2020-08-17 13:08:47 +02:00 • committed by GitHub
parent 89695b2d9e
commit f691538660
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 202 additions and 52 deletions

View file

@ -0,0 +1,91 @@
package wrapper
import (
"math"
"time"
"github.com/andig/evcc/api"
"github.com/andig/evcc/util"
)
// SocEstimator provides vehicle soc and charge duration
// Vehicle SoC can be estimated to provide more granularity
type SocEstimator struct {
log *util.Logger
vehicle api.Vehicle
estimate bool
capacity float64 // vehicle capacity in Wh cached to simplify testing
socCharge float64 // estimated vehicle SoC
prevSoC float64 // previous vehicle SoC in %
prevChargedEnergy float64 // previous charged energy in Wh
energyPerSocStep float64 // Energy per SoC percent in Wh
}
// NewSocEstimator creates new estimator
func NewSocEstimator(log *util.Logger, vehicle api.Vehicle, estimate bool) *SocEstimator {
s := &SocEstimator{
log: log,
vehicle: vehicle,
estimate: estimate,
}
s.Reset()
return s
}
// Reset resets the estimation process to default values
func (s *SocEstimator) Reset() {
s.prevSoC = 0
s.prevChargedEnergy = 0
s.capacity = float64(s.vehicle.Capacity()) * 1e3 // cache to simplify debugging
s.energyPerSocStep = s.capacity / 100
}
// RemainingChargeDuration returns the remaining duration estimate based on SoC, target and charge power
func (s *SocEstimator) RemainingChargeDuration(chargePower float64, targetSoC int) time.Duration {
if chargePower > 0 {
percentRemaining := float64(targetSoC) - s.socCharge
if percentRemaining <= 0 {
return 0
}
whRemaining := percentRemaining / 100 * s.capacity
return time.Duration(float64(time.Hour) * whRemaining / chargePower).Round(time.Second)
}
return -1
}
// SoC implements Vehicle.ChargeState with addition of given charged energy
func (s *SocEstimator) SoC(chargedEnergy float64) (float64, error) {
f, err := s.vehicle.ChargeState()
if err != nil {
return s.socCharge, err
}
s.socCharge = f
if s.estimate {
socDelta := s.socCharge - s.prevSoC
energyDelta := math.Max(chargedEnergy, 0) - s.prevChargedEnergy
if socDelta != 0 || energyDelta < 0 { // soc value change or unexpected energy reset
// calculate gradient, wh per soc %
if socDelta > 1 && energyDelta > 0 && s.prevSoC > 0 {
s.energyPerSocStep = energyDelta / socDelta
s.log.TRACE.Printf("soc gradient updated: energyPerSocStep: %0.0fWh, virtualBatCap: %0.1fkWh", s.energyPerSocStep, s.energyPerSocStep*100/1e3)
}
// sample charged energy at soc change, reset energy delta
s.prevChargedEnergy = math.Max(chargedEnergy, 0)
s.prevSoC = s.socCharge
} else {
s.socCharge = math.Min(f + energyDelta / s.energyPerSocStep, 100)
s.log.TRACE.Printf("soc estimated: %.2f%% (vehicle: %.2f%%)", s.socCharge, f)
}
}
return s.socCharge, nil
}

View file

@ -0,0 +1,82 @@
package wrapper
import (
"testing"
"time"
"github.com/andig/evcc/mock"
"github.com/andig/evcc/util"
"github.com/golang/mock/gomock"
)
func TestRemainingChargeDuration(t *testing.T) {
ctrl := gomock.NewController(t)
vehicle := mock.NewMockVehicle(ctrl)
vehicle.EXPECT().Capacity().Return(int64(10))
ce := NewSocEstimator(util.NewLogger("foo"), vehicle, false)
ce.socCharge = 20.0
chargePower := 1000.0
targetSoC := 80
if remaining := ce.RemainingChargeDuration(chargePower, targetSoC); remaining != 6*time.Hour {
t.Error("wrong remaining charge duration")
}
}
func TestSoCEstimation(t *testing.T) {
ctrl := gomock.NewController(t)
vehicle := mock.NewMockVehicle(ctrl)
vehicle.EXPECT().Capacity().Return(int64(10))
ce := NewSocEstimator(util.NewLogger("foo"), vehicle, true)
ce.socCharge = 20.0
tc := []struct {
chargedEnergy float64
vehicleSoC float64
estimatedSoC float64
}{
{10, 20.0, 20.0},
{0, 20.0, 20.0},
{123, 20.0, 21.23},
{1000, 20.0, 30.0},
{1100, 31.0, 31.0},
{1200, 32.0, 32.0},
{1900, 39.0, 39.0},
{2000, 40.0, 40.0},
{4000, 50.0, 50.0},
{6000, 60.0, 60.0},
{6500, 65.0, 65.0},
{7000, 65.0, 70.0},
{7100, 71.0, 71.0},
{7300, 72.0, 72.0},
{7400, 73.0, 73.0},
{7700, 75.0, 75.0},
{8200, 80.0, 80.0},
{0, 25.0, 25.0},
{2500, 25.0, 50.0},
{-10000, 50.0, 50.0},
{4990, 50.0, 99.9},
{5000, 50.0, 100.0},
{5001, 50.0, 100.0},
{0, 0.0, 0.0},
{1000, 0.0, 10.0},
}
for _, tc := range tc {
t.Logf("%+v", tc)
vehicle.EXPECT().ChargeState().Return(tc.vehicleSoC, nil)
soc, err := ce.SoC(tc.chargedEnergy)
if err != nil {
t.Error(err)
}
t.Logf("%+v", ce)
if tc.estimatedSoC != soc {
t.Errorf("expected: %g, got: %g", tc.estimatedSoC, soc)
}
}
}