Repository: evcc-io/evcc
Pull request #9685 (2023-09-02 15:29:36)

Restore vehicle onIdentify behavior for targetSoc from yaml
Partly reverts https://github.com/evcc-io/evcc/pull/9370 so that vehicle values from yaml are respected again.

A proper fix with UI configuration and new API endpoints here #9684

Restored behavior: `vehicle.targetSoc` (onIdentify) will be used again
@VolkerK62 Magst du die Doku anpassen https://docs.evcc.io/docs/reference/configuration/vehicles/#onidentify

Refactoring: yaml based `minSoc` settings will not be used (as today). Structure is kept so that yaml files remain backwards compatible. Removed/clarified deprecation in the source code.


Patch:
diff --git a/api/actionconfig.go b/api/actionconfig.go
index 0080a7b6..ea13b324 100644
--- a/api/actionconfig.go
+++ b/api/actionconfig.go
@@ -15,7 +15,7 @@ type ActionConfig struct {
 	Mode       *ChargeMode `mapstructure:"mode,omitempty"`       // Charge Mode
 	MinCurrent *float64    `mapstructure:"minCurrent,omitempty"` // Minimum Current
 	MaxCurrent *float64    `mapstructure:"maxCurrent,omitempty"` // Maximum Current
-	MinSoc     *int        `mapstructure:"minSoc,omitempty"`     // Minimum Soc
+	MinSoc_    *int        `mapstructure:"minSoc,omitempty"`     // Minimum Soc (deprecated)
 	TargetSoc  *int        `mapstructure:"targetSoc,omitempty"`  // Target Soc
 	Priority   *int        `mapstructure:"priority,omitempty"`   // Priority
 }
diff --git a/charger/wallbe.go b/charger/wallbe.go
index e927abad..8b5a1f4d 100644
--- a/charger/wallbe.go
+++ b/charger/wallbe.go
@@ -3,7 +3,6 @@ package charger
 import (
 	"encoding/binary"
 	"fmt"
-	"time"
 
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/util"
@@ -165,20 +164,6 @@ func (wb *Wallbe) maxCurrentMillis(current float64) error {
 	return err
 }
 
-var _ api.ChargeTimer = (*Wallbe)(nil)
-
-// ChargingTime implements the api.ChargeTimer interface
-func (wb *Wallbe) ChargingTime() (time.Duration, error) {
-	b, err := wb.conn.ReadInputRegisters(wbRegChargeTime, 2)
-	if err != nil {
-		return 0, err
-	}
-
-	// 2 words, least significant word first
-	secs := uint64(b[1]) | uint64(b[0])<<8 | uint64(b[3])<<16 | uint64(b[2])<<24
-	return time.Duration(secs) * time.Second, nil
-}
-
 // currentPower implements the api.Meter interface
 func (wb *Wallbe) currentPower() (float64, error) {
 	b, err := wb.conn.ReadInputRegisters(wbRegPower, 2)
diff --git a/core/loadpoint.go b/core/loadpoint.go
index 178ed9ab..bec4bc3e 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -317,12 +317,13 @@ func (lp *Loadpoint) collectDefaults() {
 		*actionCfg.Mode = lp.GetMode()
 		*actionCfg.MinCurrent = lp.GetMinCurrent()
 		*actionCfg.MaxCurrent = lp.GetMaxCurrent()
-		*actionCfg.MinSoc = lp.GetMinSoc()
-		*actionCfg.TargetSoc = lp.GetTargetSoc()
 		*actionCfg.Priority = lp.GetPriority()
 	} else {
 		lp.log.ERROR.Printf("error allocating action config: %v", err)
 	}
+	// deprecated: do not reapply deprecated lp config values
+	actionCfg.TargetSoc = nil
+	actionCfg.MinSoc_ = nil
 }
 
 // requestUpdate requests site to update this loadpoint
@@ -552,6 +553,9 @@ func (lp *Loadpoint) applyAction(actionCfg api.ActionConfig) {
 	if min := actionCfg.MinCurrent; min != nil && *min >= *lp.onDisconnect.MinCurrent {
 		lp.SetMinCurrent(*min)
 	}
+	if actionCfg.TargetSoc != nil {
+		lp.SetTargetSoc(*actionCfg.TargetSoc)
+	}
 	if max := actionCfg.MaxCurrent; max != nil && *max <= *lp.onDisconnect.MaxCurrent {
 		lp.SetMaxCurrent(*max)
 	}
diff --git a/vehicle/embed.go b/vehicle/embed.go
index deff59fb..d203519b 100644
--- a/vehicle/embed.go
+++ b/vehicle/embed.go
@@ -14,6 +14,16 @@ type embed struct {
 	OnIdentify   api.ActionConfig `mapstructure:"onIdentify"`
 }
 
+// Title implements the api.Vehicle interface
+func (v *embed) fromVehicle(title string, capacity float64) {
+	if v.Title_ == "" {
+		v.Title_ = title
+	}
+	if v.Capacity_ == 0 {
+		v.Capacity_ = capacity
+	}
+}
+
 // Title implements the api.Vehicle interface
 func (v *embed) Title() string {
 	return v.Title_
diff --git a/vehicle/seat-cupra.go b/vehicle/seat-cupra.go
index a967e9fe..c460aa8a 100644
--- a/vehicle/seat-cupra.go
+++ b/vehicle/seat-cupra.go
@@ -74,12 +74,21 @@ func NewCupraFromConfig(other map[string]interface{}) (api.Vehicle, error) {
 
 	api := cupra.NewAPI(log, ts)
 
-	cc.VIN, err = ensureVehicle(cc.VIN, func() ([]string, error) {
-		return api.Vehicles(ui.Subject)
-	})
+	vehicle, err := ensureVehicleEx(
+		cc.VIN, func() ([]cupra.Vehicle, error) {
+			return api.Vehicles(ui.Subject)
+		},
+		func(v cupra.Vehicle) string {
+			return v.VIN
+		},
+	)
 
 	if err == nil {
-		v.Provider = cupra.NewProvider(api, ui.Subject, cc.VIN, cc.Cache)
+		v.fromVehicle(vehicle.VehicleNickname, 0)
+	}
+
+	if err == nil {
+		v.Provider = cupra.NewProvider(api, ui.Subject, vehicle.VIN, cc.Cache)
 	}
 
 	return v, err
diff --git a/vehicle/seat/cupra/api.go b/vehicle/seat/cupra/api.go
index 1697cc27..2b60c152 100644
--- a/vehicle/seat/cupra/api.go
+++ b/vehicle/seat/cupra/api.go
@@ -38,19 +38,15 @@ func NewAPI(log *util.Logger, ts oauth2.TokenSource) *API {
 }
 
 // Vehicles implements the /vehicles response
-func (v *API) Vehicles(userID string) (res []string, err error) {
-	var vehicles struct {
+func (v *API) Vehicles(userID string) ([]Vehicle, error) {
+	var res struct {
 		Vehicles []Vehicle
 	}
 
 	uri := fmt.Sprintf("%s/v1/users/%s/garage/vehicles", BaseURL, userID)
-	err = v.GetJSON(uri, &vehicles)
-
-	for _, v := range vehicles.Vehicles {
-		res = append(res, v.VIN)
-	}
+	err := v.GetJSON(uri, &res)
 
-	return res, err
+	return res.Vehicles, err
 }
 
 // Status implements the /status response
diff --git a/vehicle/skoda-enyaq.go b/vehicle/skoda-enyaq.go
index df427bd7..03d700d4 100644
--- a/vehicle/skoda-enyaq.go
+++ b/vehicle/skoda-enyaq.go
@@ -50,7 +50,7 @@ func NewEnyaqFromConfig(other map[string]interface{}) (api.Vehicle, error) {
 	var err error
 	log := util.NewLogger("enyaq").Redact(cc.User, cc.Password, cc.VIN)
 
-	// use Skoda credentials to resolve list of vehicles
+	// use Skoda api to resolve list of vehicles
 	ts, err := service.TokenRefreshServiceTokenSource(log, skoda.TRSParams, skoda.AuthParams, cc.User, cc.Password)
 	if err != nil {
 		return nil, err
@@ -66,11 +66,8 @@ func NewEnyaqFromConfig(other map[string]interface{}) (api.Vehicle, error) {
 		},
 	)
 
-	if v.Title_ == "" {
-		v.Title_ = vehicle.Name
-	}
-	if v.Capacity_ == 0 {
-		v.Capacity_ = float64(vehicle.Specification.Battery.CapacityInKWh)
+	if err == nil {
+		v.fromVehicle(vehicle.Name, float64(vehicle.Specification.Battery.CapacityInKWh))
 	}
 
 	// use Connect credentials to build provider
diff --git a/vehicle/skoda.go b/vehicle/skoda.go
index c08410c9..ee0caa7a 100644
--- a/vehicle/skoda.go
+++ b/vehicle/skoda.go
@@ -51,20 +51,38 @@ func NewSkodaFromConfig(other map[string]interface{}) (api.Vehicle, error) {
 
 	log := util.NewLogger("skoda").Redact(cc.User, cc.Password, cc.VIN)
 
-	trs := tokenrefreshservice.New(log, skoda.TRSParams)
-	ts, err := service.MbbTokenSource(log, trs, skoda.AuthClientID, skoda.AuthParams, cc.User, cc.Password)
+	// use Skoda api to resolve list of vehicles
+	ts, err := service.TokenRefreshServiceTokenSource(log, skoda.TRSParams, skoda.AuthParams, cc.User, cc.Password)
 	if err != nil {
 		return nil, err
 	}
 
-	api := vw.NewAPI(log, ts, skoda.Brand, skoda.Country)
+	api := skoda.NewAPI(log, ts)
 	api.Client.Timeout = cc.Timeout
 
-	cc.VIN, err = ensureVehicle(cc.VIN, api.Vehicles)
+	vehicle, err := ensureVehicleEx(
+		cc.VIN, api.Vehicles,
+		func(v skoda.Vehicle) string {
+			return v.VIN
+		},
+	)
 
 	if err == nil {
-		if err = api.HomeRegion(cc.VIN); err == nil {
-			v.Provider = vw.NewProvider(api, cc.VIN, cc.Cache)
+		v.fromVehicle(vehicle.Name, float64(vehicle.Specification.Battery.CapacityInKWh))
+	}
+
+	if err == nil {
+		trs := tokenrefreshservice.New(log, skoda.TRSParams)
+		ts, err := service.MbbTokenSource(log, trs, skoda.AuthClientID, skoda.AuthParams, cc.User, cc.Password)
+		if err != nil {
+			return nil, err
+		}
+
+		api := vw.NewAPI(log, ts, skoda.Brand, skoda.Country)
+		api.Client.Timeout = cc.Timeout
+
+		if err = api.HomeRegion(vehicle.VIN); err == nil {
+			v.Provider = vw.NewProvider(api, vehicle.VIN, cc.Cache)
 		}
 	}
 


Test patch:
diff --git a/api/actionconfig_test.go b/api/actionconfig_test.go
index 1286b8ef..e2e401d6 100644
--- a/api/actionconfig_test.go
+++ b/api/actionconfig_test.go
@@ -18,10 +18,8 @@ func TestMerge(t *testing.T) {
 
 	now := ModeNow
 	two := 2
-	three := 3
 	new := ActionConfig{
 		Mode:     &now,
-		MinSoc:   &three,
 		Priority: &two,
 	}
 
@@ -38,7 +36,6 @@ func TestMerge(t *testing.T) {
 	assert.Equal(t, dst, ActionConfig{
 		Mode:       &now,
 		MinCurrent: &six,
-		MinSoc:     &three,
 		Priority:   &two,
 	}, "new wrong")
 }
diff --git a/charger/wallbe_test.go b/charger/wallbe_test.go
deleted file mode 100644
index 496614e2..00000000
--- a/charger/wallbe_test.go
+++ /dev/null
@@ -1,50 +0,0 @@
-package charger
-
-import (
-	"testing"
-
-	"github.com/evcc-io/evcc/api"
-)
-
-func TestWallbeLegacy(t *testing.T) {
-	wbc, err := NewWallbeFromConfig(map[string]interface{}{"legacy": true})
-	if err != nil {
-		t.Error(err)
-	}
-
-	wb, ok := wbc.(*Wallbe)
-	if !ok {
-		t.Error("unexpected type")
-	}
-
-	if wb.factor != 1 {
-		t.Errorf("invalid factor: %d", wb.factor)
-	}
-
-	if _, ok = wbc.(api.ChargeTimer); !ok {
-		t.Error("missing charge timer api")
-	}
-
-	if _, ok = wbc.(api.Diagnosis); !ok {
-		t.Error("missing diagnosis api")
-	}
-}
-
-func TestWallbeEx(t *testing.T) {
-	wbc, err := NewWallbeFromConfig(map[string]interface{}{"legacy": false})
-	if err != nil {
-		t.Error(err)
-	}
-
-	if _, ok := wbc.(api.ChargerEx); !ok {
-		t.Error("missing ChargerEx api")
-	}
-
-	if _, ok := wbc.(api.ChargeTimer); !ok {
-		t.Error("missing ChargeTimer api")
-	}
-
-	if _, ok := wbc.(api.Diagnosis); !ok {
-		t.Error("missing Diagnosis api")
-	}
-}
diff --git a/core/loadpoint_vehicle_test.go b/core/loadpoint_vehicle_test.go
index 33b240bf..9e7473d8 100644
--- a/core/loadpoint_vehicle_test.go
+++ b/core/loadpoint_vehicle_test.go
@@ -148,7 +148,6 @@ func TestDefaultVehicle(t *testing.T) {
 	ctrl := gomock.NewController(t)
 
 	mode := api.ModePV
-	minsoc := 20
 	targetsoc := 80
 
 	dflt := mock.NewMockVehicle(ctrl)
@@ -158,7 +157,6 @@ func TestDefaultVehicle(t *testing.T) {
 	dflt.EXPECT().Phases().AnyTimes()
 	dflt.EXPECT().OnIdentified().Return(api.ActionConfig{
 		Mode:      &mode,
-		MinSoc:    &minsoc,
 		TargetSoc: &targetsoc,
 	}).AnyTimes()
 
@@ -175,13 +173,10 @@ func TestDefaultVehicle(t *testing.T) {
 	// ondisconnect
 	off := api.ModeOff
 	zero := 0
-	hundred := 100
 	onDisconnect := api.ActionConfig{
 		Mode:       &off,
 		MinCurrent: &lp.MinCurrent,
 		MaxCurrent: &lp.MaxCurrent,
-		MinSoc:     &zero,
-		TargetSoc:  &hundred,
 		Priority:   &zero,
 	}
 
@@ -227,13 +222,12 @@ func TestDefaultVehicle(t *testing.T) {
 func TestApplyVehicleDefaults(t *testing.T) {
 	ctrl := gomock.NewController(t)
 
-	newConfig := func(mode api.ChargeMode, minCurrent, maxCurrent float64, minSoc, targetSoc int) api.ActionConfig {
+	newConfig := func(mode api.ChargeMode, minCurrent, maxCurrent float64, targetSoc *int) api.ActionConfig {
 		return api.ActionConfig{
 			Mode:       &mode,
 			MinCurrent: &minCurrent,
 			MaxCurrent: &maxCurrent,
-			MinSoc:     &minSoc,
-			TargetSoc:  &targetSoc,
+			TargetSoc:  targetSoc,
 		}
 	}
 
@@ -244,10 +238,11 @@ func TestApplyVehicleDefaults(t *testing.T) {
 	}
 
 	// onIdentified config
-	oi := newConfig(api.ModePV, 7, 15, 1, 99)
+	targetSoc := 99
+	oi := newConfig(api.ModePV, 7, 15, &targetSoc)
 
 	// onDefault config
-	od := newConfig(api.ModeOff, 6, 16, 2, 98)
+	od := newConfig(api.ModeOff, 6, 16, nil)
 
 	vehicle := mock.NewMockVehicle(ctrl)
 	vehicle.EXPECT().Title().Return("it's me").AnyTimes()
@@ -266,7 +261,7 @@ func TestApplyVehicleDefaults(t *testing.T) {
 	lp.ResetOnDisconnect = true
 
 	// check loadpoint default currents can't be violated
-	lp.applyAction(newConfig(*od.Mode, 5, 17, *od.MinSoc, *od.TargetSoc))
+	lp.applyAction(newConfig(*od.Mode, 5, 17, od.TargetSoc))
 	assertConfig(lp, od)
 
 	// vehicle identified