Skip to content

Commit

Permalink
Allow chargers to provide vehicle SoC via ISO15118 (evcc-io#1283)
Browse files Browse the repository at this point in the history
Some chargers (e.g. Porsche/Audi Mobile Charger Connect) can provide the EVs current SoC via the ISO15118 connection to the car.

This change allows chargers to implement the SoC method and return the SoC or api.ErrNotAvailable if the currently connected car does not provide the data.
  • Loading branch information
DerAndereAndi authored Aug 14, 2021
1 parent 5e66729 commit 410f623
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 47 deletions.
2 changes: 1 addition & 1 deletion api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package api

import "time"

//go:generate mockgen -package mock -destination ../mock/mock_api.go github.com/andig/evcc/api Charger,ChargeState,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater
//go:generate mockgen -package mock -destination ../mock/mock_api.go github.com/andig/evcc/api Charger,ChargeState,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery

// ChargeMode are charge modes modeled after OpenWB
type ChargeMode string
Expand Down
14 changes: 12 additions & 2 deletions core/loadpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,7 @@ func (lp *LoadPoint) setActiveVehicle(vehicle api.Vehicle) {
lp.log.INFO.Printf("vehicle updated: %s -> %s", from, to)

if lp.vehicle = vehicle; vehicle != nil {
lp.socEstimator = soc.NewEstimator(lp.log, vehicle, lp.SoC.Estimate)
lp.socEstimator = soc.NewEstimator(lp.log, lp.charger, vehicle, lp.SoC.Estimate)

lp.publish("hasVehicle", true)
lp.publish("socTitle", lp.vehicle.Title())
Expand Down Expand Up @@ -975,13 +975,23 @@ func (lp *LoadPoint) socPollAllowed() bool {
return lp.charging() || honourUpdateInterval && (remaining <= 0) || lp.connected() && lp.socUpdated.IsZero()
}

// checks if the connected charger can provide SoC to the connected vehicle
func (lp *LoadPoint) socProvidedByCharger() bool {
if charger, ok := lp.charger.(api.Battery); ok {
if _, err := charger.SoC(); err == nil {
return true
}
}
return false
}

// publish state of charge, remaining charge duration and range
func (lp *LoadPoint) publishSoCAndRange() {
if lp.socEstimator == nil {
return
}

if lp.socPollAllowed() {
if lp.socPollAllowed() || lp.socProvidedByCharger() {
lp.socUpdated = lp.clock.Now()

f, err := lp.socEstimator.SoC(lp.chargedEnergy)
Expand Down
2 changes: 1 addition & 1 deletion core/loadpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ func TestDisableAndEnableAtTargetSoC(t *testing.T) {

// wrap vehicle with estimator
vehicle.EXPECT().Capacity().Return(int64(10))
socEstimator := soc.NewEstimator(util.NewLogger("foo"), vehicle, false)
socEstimator := soc.NewEstimator(util.NewLogger("foo"), charger, vehicle, false)

lp := &LoadPoint{
log: util.NewLogger("foo"),
Expand Down
44 changes: 30 additions & 14 deletions core/soc/estimator.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const chargeEfficiency = 0.9 // assume charge 90% efficiency
// Vehicle SoC can be estimated to provide more granularity
type Estimator struct {
log *util.Logger
charger api.Charger
vehicle api.Vehicle
estimate bool

Expand All @@ -27,9 +28,10 @@ type Estimator struct {
}

// NewEstimator creates new estimator
func NewEstimator(log *util.Logger, vehicle api.Vehicle, estimate bool) *Estimator {
func NewEstimator(log *util.Logger, charger api.Charger, vehicle api.Vehicle, estimate bool) *Estimator {
s := &Estimator{
log: log,
charger: charger,
vehicle: vehicle,
estimate: estimate,
}
Expand Down Expand Up @@ -91,24 +93,38 @@ 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) {
f, err := s.vehicle.SoC()
if err != nil {
if errors.Is(err, api.ErrMustRetry) {
return 0, err
var fetchedSoC *float64

if charger, ok := s.charger.(api.Battery); ok {
f, err := charger.SoC()

if err == nil {
s.socCharge = f
fetchedSoC = &f
}
}

if fetchedSoC == nil {
f, err := s.vehicle.SoC()
if err != nil {
if errors.Is(err, api.ErrMustRetry) {
return 0, err
}

s.log.WARN.Printf("updating soc failed: %v", err)

s.log.WARN.Printf("updating soc failed: %v", err)
// try to recover from temporary vehicle-api errors
if s.prevSoC == 0 { // never received a soc value
return s.socCharge, err
}

// try to recover from temporary vehicle-api errors
if s.prevSoC == 0 { // never received a soc value
return s.socCharge, err
f = s.prevSoC // recover last received soc
}

f = s.prevSoC // recover last received soc
fetchedSoC = &f
s.socCharge = f
}

s.socCharge = f

if s.estimate {
socDelta := s.socCharge - s.prevSoC
energyDelta := math.Max(chargedEnergy, 0) - s.prevChargedEnergy
Expand All @@ -126,8 +142,8 @@ func (s *Estimator) SoC(chargedEnergy float64) (float64, error) {
s.prevChargedEnergy = math.Max(chargedEnergy, 0)
s.prevSoC = s.socCharge
} else {
s.socCharge = math.Min(f+energyDelta/s.energyPerSocStep, 100)
s.log.DEBUG.Printf("soc estimated: %.2f%% (vehicle: %.2f%%)", s.socCharge, f)
s.socCharge = math.Min(*fetchedSoC+energyDelta/s.energyPerSocStep, 100)
s.log.DEBUG.Printf("soc estimated: %.2f%% (vehicle: %.2f%%)", s.socCharge, *fetchedSoC)
}
}

Expand Down
75 changes: 47 additions & 28 deletions core/soc/estimator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@ import (
"testing"
"time"

"github.com/andig/evcc/api"
"github.com/andig/evcc/mock"
"github.com/andig/evcc/util"
"github.com/golang/mock/gomock"
)

func TestRemainingChargeDuration(t *testing.T) {
ctrl := gomock.NewController(t)
charger := mock.NewMockCharger(ctrl)
vehicle := mock.NewMockVehicle(ctrl)
//9 kWh userBatCap => 10 kWh virtualBatCap
vehicle.EXPECT().Capacity().Return(int64(9))

ce := NewEstimator(util.NewLogger("foo"), vehicle, false)
ce := NewEstimator(util.NewLogger("foo"), charger, vehicle, false)
ce.socCharge = 20.0

chargePower := 1000.0
Expand All @@ -27,14 +29,20 @@ func TestRemainingChargeDuration(t *testing.T) {
}

func TestSoCEstimation(t *testing.T) {
type chargerStruct struct {
*mock.MockCharger
*mock.MockBattery
}

ctrl := gomock.NewController(t)
vehicle := mock.NewMockVehicle(ctrl)
charger := &chargerStruct{mock.NewMockCharger(ctrl), mock.NewMockBattery(ctrl)}

// 9 kWh user battery capacity is converted to initial value of 10 kWh virtual capacity
var capacity int64 = 9
vehicle.EXPECT().Capacity().Return(capacity)

ce := NewEstimator(util.NewLogger("foo"), vehicle, true)
ce := NewEstimator(util.NewLogger("foo"), charger, vehicle, true)
ce.socCharge = 20.0

tc := []struct {
Expand Down Expand Up @@ -70,33 +78,44 @@ func TestSoCEstimation(t *testing.T) {
{1000, 0.0, 10.0, 10000},
}

for _, tc := range tc {
t.Logf("%+v", tc)
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)
for i := 1; i < 3; i++ {
useVehicleSoC := true
if i == 2 {
useVehicleSoC = false
}

// 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(chargePower, targetSoC); rm != remainingDuration {
t.Errorf("expected estimated duration: %v, got: %v", remainingDuration, rm)
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(chargePower, targetSoC); rm != remainingDuration {
t.Errorf("expected estimated duration: %v, got: %v", remainingDuration, rm)
}
}
}
}
40 changes: 39 additions & 1 deletion mock/mock_api.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 410f623

Please sign in to comment.