Allow chargers to provide vehicle SoC via ISO15118 (#1283)

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.
This commit is contained in:
Andreas Linde 2021-08-14 13:51:26 +02:00 • committed by GitHub
parent 5e66729983
commit 410f62316a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 129 additions and 46 deletions

View file

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

View file

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

View file

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

View file

@ -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
@ -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,
}
@ -91,23 +93,37 @@ 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
}
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
}
f = s.prevSoC // recover last received soc
}
s.socCharge = 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)
// 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
}
fetchedSoC = &f
s.socCharge = f
}
if s.estimate {
socDelta := s.socCharge - s.prevSoC
@ -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)
}
}

View file

@ -4,6 +4,7 @@ import (
"testing"
"time"
"github.com/andig/evcc/api"
"github.com/andig/evcc/mock"
"github.com/andig/evcc/util"
"github.com/golang/mock/gomock"
@ -11,11 +12,12 @@ import (
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
@ -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 {
@ -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)
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)
}
// validate soc estimate
if tc.estimatedSoC != soc {
t.Errorf("expected estimated soc: %g, got: %g", tc.estimatedSoC, soc)
}
soc, err := ce.SoC(tc.chargedEnergy)
if err != nil {
t.Error(err)
}
// validate capacity estimate
if tc.virtualCapacity != ce.virtualCapacity {
t.Errorf("expected virtual capacity: %v, got: %v", tc.virtualCapacity, ce.virtualCapacity)
}
// validate soc estimate
if tc.estimatedSoC != soc {
t.Errorf("expected estimated soc: %g, got: %g", tc.estimatedSoC, soc)
}
// 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)
// validate capacity estimate
if tc.virtualCapacity != ce.virtualCapacity {
t.Errorf("expected virtual capacity: %v, got: %v", tc.virtualCapacity, ce.virtualCapacity)
}
if rm := ce.RemainingChargeDuration(chargePower, targetSoC); rm != remainingDuration {
t.Errorf("expected estimated duration: %v, got: %v", remainingDuration, rm)
// 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)
}
}
}
}

View file

@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/andig/evcc/api (interfaces: Charger,ChargeState,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater)
// Source: github.com/andig/evcc/api (interfaces: Charger,ChargeState,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery)
// Package mock is a generated GoMock package.
package mock
@ -362,3 +362,41 @@ func (mr *MockChargeRaterMockRecorder) ChargedEnergy() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChargedEnergy", reflect.TypeOf((*MockChargeRater)(nil).ChargedEnergy))
}
// MockBattery is a mock of Battery interface.
type MockBattery struct {
ctrl *gomock.Controller
recorder *MockBatteryMockRecorder
}
// MockBatteryMockRecorder is the mock recorder for MockBattery.
type MockBatteryMockRecorder struct {
mock *MockBattery
}
// NewMockBattery creates a new mock instance.
func NewMockBattery(ctrl *gomock.Controller) *MockBattery {
mock := &MockBattery{ctrl: ctrl}
mock.recorder = &MockBatteryMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockBattery) EXPECT() *MockBatteryMockRecorder {
return m.recorder
}
// SoC mocks base method.
func (m *MockBattery) SoC() (float64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SoC")
ret0, _ := ret[0].(float64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SoC indicates an expected call of SoC.
func (mr *MockBatteryMockRecorder) SoC() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SoC", reflect.TypeOf((*MockBattery)(nil).SoC))
}