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:
parent
5e66729983
commit
410f62316a
6 changed files with 129 additions and 46 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue