Repository: evcc-io/evcc
Pull request #13534 (2024-04-21 13:06:11)

Loadpoint: add welcomecharge feature
Fix https://github.com/evcc-io/evcc/issues/13423

This PR adds a `kickcharge` feature (are there better names)? When a vehicle connection is detected, and any of the vehicles that are currently _not_ connected to any charger has this feature, then charging will start immediately if in PV mode.

This works such that vehicle defaults (like resetting mode) are only applied after starting the charging session.

VolkerK62: wenn kein Überschuss vorhanden, schaltet `disable.delay` das wieder aus?
 
andig: Das ist die Idee- deshalb wäre nur einschalten neu. 
VolkerK62: wäre ein kurzer fester Wert ein Problem?
Ich kann mir vorstellen, dass es "Beschwerden" gibt .... "warum 3 Minuten, wenn doch 30 Sekunden reichen"
andig: Viel mehr Logik -> erstmal nein. 
VolkerK62: recognitionCharge?
preCharge?


Patch:
diff --git a/api/api.go b/api/api.go
index 6dab022f..77500df0 100644
--- a/api/api.go
+++ b/api/api.go
@@ -93,7 +93,7 @@ type Diagnosis interface {
 
 // ChargeTimer provides current charge cycle duration
 type ChargeTimer interface {
-	ChargingTime() (time.Duration, error)
+	ChargeDuration() (time.Duration, error)
 }
 
 // ChargeRater provides charged energy amount in kWh
diff --git a/api/feature.go b/api/feature.go
index 15d7e200..44b5240c 100644
--- a/api/feature.go
+++ b/api/feature.go
@@ -10,4 +10,5 @@ const (
 	IntegratedDevice
 	Heating
 	Retryable
+	WelcomeCharge
 )
diff --git a/api/feature_enumer.go b/api/feature_enumer.go
index 858b77ac..73628408 100644
--- a/api/feature_enumer.go
+++ b/api/feature_enumer.go
@@ -7,11 +7,11 @@ import (
 	"strings"
 )
 
-const _FeatureName = "OfflineCoarseCurrentIntegratedDeviceHeatingRetryable"
+const _FeatureName = "OfflineCoarseCurrentIntegratedDeviceHeatingRetryableWelcomeCharge"
 
-var _FeatureIndex = [...]uint8{0, 7, 20, 36, 43, 52}
+var _FeatureIndex = [...]uint8{0, 7, 20, 36, 43, 52, 65}
 
-const _FeatureLowerName = "offlinecoarsecurrentintegrateddeviceheatingretryable"
+const _FeatureLowerName = "offlinecoarsecurrentintegrateddeviceheatingretryablewelcomecharge"
 
 func (i Feature) String() string {
 	i -= 1
@@ -30,9 +30,10 @@ func _FeatureNoOp() {
 	_ = x[IntegratedDevice-(3)]
 	_ = x[Heating-(4)]
 	_ = x[Retryable-(5)]
+	_ = x[WelcomeCharge-(6)]
 }
 
-var _FeatureValues = []Feature{Offline, CoarseCurrent, IntegratedDevice, Heating, Retryable}
+var _FeatureValues = []Feature{Offline, CoarseCurrent, IntegratedDevice, Heating, Retryable, WelcomeCharge}
 
 var _FeatureNameToValueMap = map[string]Feature{
 	_FeatureName[0:7]:        Offline,
@@ -45,6 +46,8 @@ var _FeatureNameToValueMap = map[string]Feature{
 	_FeatureLowerName[36:43]: Heating,
 	_FeatureName[43:52]:      Retryable,
 	_FeatureLowerName[43:52]: Retryable,
+	_FeatureName[52:65]:      WelcomeCharge,
+	_FeatureLowerName[52:65]: WelcomeCharge,
 }
 
 var _FeatureNames = []string{
@@ -53,6 +56,7 @@ var _FeatureNames = []string{
 	_FeatureName[20:36],
 	_FeatureName[36:43],
 	_FeatureName[43:52],
+	_FeatureName[52:65],
 }
 
 // FeatureString retrieves an enum value from the enum constants string name.
diff --git a/assets/js/views/App.vue b/assets/js/views/App.vue
index a6bdb162..662fbf20 100644
--- a/assets/js/views/App.vue
+++ b/assets/js/views/App.vue
@@ -22,9 +22,9 @@ import HelpModal from "../components/HelpModal.vue";
 import collector from "../mixins/collector";
 import { updateAuthStatus } from "../auth";
 
-// assume offline if not data received for 60 seconds
+// assume offline if not data received for 5 minutes
 let lastDataReceived = new Date();
-const maxDataAge = 60 * 1000;
+const maxDataAge = 60 * 1000 * 5;
 setInterval(() => {
 	if (new Date() - lastDataReceived > maxDataAge) {
 		console.log("no data received, assume we are offline");
diff --git a/assets/js/views/StartupError.vue b/assets/js/views/StartupError.vue
index b2c0f51b..ae727155 100644
--- a/assets/js/views/StartupError.vue
+++ b/assets/js/views/StartupError.vue
@@ -77,20 +77,26 @@
 import "@h2d2/shopicons/es/regular/car1";
 import api from "../api";
 import collector from "../mixins/collector";
+import store from "../store";
 
 export default {
 	name: "StartupError",
 	mixins: [collector],
 	props: {
-		fatal: Array,
-		config: String,
-		file: String,
-		line: Number,
 		offline: Boolean,
 	},
 	computed: {
 		errors() {
-			return this.fatal || [];
+			return store.state.fatal || [];
+		},
+		config() {
+			return store.state.config;
+		},
+		file() {
+			return store.state.file;
+		},
+		line() {
+			return store.state.line;
 		},
 	},
 	methods: {
diff --git a/charger/bender.go b/charger/bender.go
index d50bc12e..1267103a 100644
--- a/charger/bender.go
+++ b/charger/bender.go
@@ -203,8 +203,8 @@ func (wb *BenderCC) MaxCurrent(current int64) error {
 
 var _ api.ChargeTimer = (*BenderCC)(nil)
 
-// ChargingTime implements the api.ChargeTimer interface
-func (wb *BenderCC) ChargingTime() (time.Duration, error) {
+// ChargeDuration implements the api.ChargeTimer interface
+func (wb *BenderCC) ChargeDuration() (time.Duration, error) {
 	if wb.legacy {
 		b, err := wb.conn.ReadHoldingRegisters(bendRegChargingDurationLegacy, 1)
 		if err != nil {
diff --git a/charger/easee.go b/charger/easee.go
index 457888ad..6b0c3c10 100644
--- a/charger/easee.go
+++ b/charger/easee.go
@@ -50,7 +50,6 @@ type Easee struct {
 	log                     *util.Logger
 	mux                     sync.RWMutex
 	lastEnergyPollMux       sync.Mutex
-	done                    chan struct{}
 	dynamicChargerCurrent   float64
 	current                 float64
 	chargerEnabled          bool
@@ -71,7 +70,7 @@ type Easee struct {
 	obsC       chan easee.Observation
 	obsTime    map[easee.ObservationID]time.Time
 	stopTicker chan struct{}
-	once       sync.Once
+	startDone  func()
 }
 
 func init() {
@@ -109,13 +108,15 @@ func NewEasee(user, password, charger string, timeout time.Duration, authorize b
 		return nil, api.ErrSponsorRequired
 	}
 
+	done := make(chan struct{})
+
 	c := &Easee{
 		Helper:    request.NewHelper(log),
 		charger:   charger,
 		authorize: authorize,
 		log:       log,
 		current:   6, // default current
-		done:      make(chan struct{}),
+		startDone: sync.OnceFunc(func() { close(done) }),
 		cmdC:      make(chan easee.SignalRCommandResponse),
 		obsC:      make(chan easee.Observation),
 		obsTime:   make(map[easee.ObservationID]time.Time),
@@ -187,7 +188,7 @@ func NewEasee(user, password, charger string, timeout time.Duration, authorize b
 
 	// wait for first update
 	select {
-	case <-c.done:
+	case <-done:
 	case <-time.After(request.Timeout):
 		err = os.ErrDeadlineExceeded
 	}
@@ -364,7 +365,7 @@ func (c *Easee) ProductUpdate(i json.RawMessage) {
 		c.opMode = opMode
 
 		// startup completed
-		c.once.Do(func() { close(c.done) })
+		c.startDone()
 
 	case easee.REASON_FOR_NO_CURRENT:
 		c.reasonForNoCurrent = value.(int)
diff --git a/charger/em2go-home.go b/charger/em2go-home.go
index c65acb8b..d0a185c6 100644
--- a/charger/em2go-home.go
+++ b/charger/em2go-home.go
@@ -233,8 +233,8 @@ func (wb *Em2GoHome) ChargedEnergy() (float64, error) {
 
 var _ api.ChargeTimer = (*Em2GoHome)(nil)
 
-// ChargingTime implements the api.ChargeTimer interface
-func (wb *Em2GoHome) ChargingTime() (time.Duration, error) {
+// ChargeDuration implements the api.ChargeTimer interface
+func (wb *Em2GoHome) ChargeDuration() (time.Duration, error) {
 	b, err := wb.conn.ReadHoldingRegisters(em2GoHomeRegChargeDuration, 2)
 	if err != nil {
 		return 0, err
diff --git a/charger/em2go.go b/charger/em2go.go
index 501b2da2..23cef988 100644
--- a/charger/em2go.go
+++ b/charger/em2go.go
@@ -221,8 +221,8 @@ func (wb *Em2Go) ChargedEnergy() (float64, error) {
 
 var _ api.ChargeTimer = (*Em2Go)(nil)
 
-// ChargingTime implements the api.ChargeTimer interface
-func (wb *Em2Go) ChargingTime() (time.Duration, error) {
+// ChargeDuration implements the api.ChargeTimer interface
+func (wb *Em2Go) ChargeDuration() (time.Duration, error) {
 	b, err := wb.conn.ReadHoldingRegisters(em2GoRegChargeDuration, 2)
 	if err != nil {
 		return 0, err
diff --git a/charger/etrel.go b/charger/etrel.go
index 92ac23bb..0e5e807b 100644
--- a/charger/etrel.go
+++ b/charger/etrel.go
@@ -221,8 +221,8 @@ func (wb *Etrel) Currents() (float64, float64, float64, error) {
 
 // var _ api.ChargeTimer = (*Etrel)(nil)
 //
-// // ChargingTime implements the api.ChargeTimer interface
-// func (wb *Etrel) ChargingTime() (time.Duration, error) {
+// // ChargeDuration implements the api.ChargeTimer interface
+// func (wb *Etrel) ChargeDuration() (time.Duration, error) {
 // 	b, err := wb.conn.ReadInputRegisters(wb.base+etrelRegChargeTime, 4)
 // 	if err != nil {
 // 		return 0, err
diff --git a/charger/hesotec.go b/charger/hesotec.go
index 1b591087..9a6b913e 100644
--- a/charger/hesotec.go
+++ b/charger/hesotec.go
@@ -154,8 +154,8 @@ func (wb *Hesotec) CurrentPower() (float64, error) {
 
 var _ api.ChargeTimer = (*Hesotec)(nil)
 
-// ChargingTime implements the api.ChargeTimer interface
-func (wb *Hesotec) ChargingTime() (time.Duration, error) {
+// ChargeDuration implements the api.ChargeTimer interface
+func (wb *Hesotec) ChargeDuration() (time.Duration, error) {
 	b, err := wb.conn.ReadHoldingRegisters(hesotecRegDuration, 2)
 	if err != nil {
 		return 0, err
diff --git a/charger/mcc.go b/charger/mcc.go
index 4dc7cca0..d3784247 100644
--- a/charger/mcc.go
+++ b/charger/mcc.go
@@ -340,8 +340,8 @@ func (mcc *MobileConnect) ChargedEnergy() (float64, error) {
 
 var _ api.ChargeTimer = (*MobileConnect)(nil)
 
-// ChargingTime implements the api.ChargeTimer interface
-func (mcc *MobileConnect) ChargingTime() (time.Duration, error) {
+// ChargeDuration implements the api.ChargeTimer interface
+func (mcc *MobileConnect) ChargeDuration() (time.Duration, error) {
 	var currentSession MCCCurrentSession
 	if err := mcc.getEscapedJSON(mcc.apiURL(mccAPICurrentSession), &currentSession); err != nil {
 		return 0, err
diff --git a/charger/mennekes-compact.go b/charger/mennekes-compact.go
index b713d0cc..36415328 100644
--- a/charger/mennekes-compact.go
+++ b/charger/mennekes-compact.go
@@ -248,8 +248,8 @@ func (wb *MennekesCompact) ChargedEnergy() (float64, error) {
 
 var _ api.ChargeTimer = (*MennekesCompact)(nil)
 
-// ChargingTime implements the api.ChargeTimer interface
-func (wb *MennekesCompact) ChargingTime() (time.Duration, error) {
+// ChargeDuration implements the api.ChargeTimer interface
+func (wb *MennekesCompact) ChargeDuration() (time.Duration, error) {
 	b, err := wb.conn.ReadHoldingRegisters(mennekesRegDurationSession, 2)
 	if err != nil {
 		return 0, err
diff --git a/charger/ocpp/connector_core.go b/charger/ocpp/connector_core.go
index 453550b7..fbdc3c52 100644
--- a/charger/ocpp/connector_core.go
+++ b/charger/ocpp/connector_core.go
@@ -20,7 +20,7 @@ func (conn *Connector) timestampValid(t time.Time) bool {
 	}
 
 	// reject older values than we already have
-	return t.After(conn.status.Timestamp.Time)
+	return !t.Before(conn.status.Timestamp.Time)
 }
 
 func (conn *Connector) StatusNotification(request *core.StatusNotificationRequest) (*core.StatusNotificationConfirmation, error) {
diff --git a/charger/openevse.go b/charger/openevse.go
index 9aacdc0c..a0cab295 100644
--- a/charger/openevse.go
+++ b/charger/openevse.go
@@ -205,7 +205,7 @@ func (c *OpenEVSE) ChargedEnergy() (float64, error) {
 
 var _ api.ChargeTimer = (*OpenEVSE)(nil)
 
-func (c *OpenEVSE) ChargingTime() (time.Duration, error) {
+func (c *OpenEVSE) ChargeDuration() (time.Duration, error) {
 	res, err := c.statusG.Get()
 	if err != nil {
 		return 0, err
diff --git a/charger/phoenix-charx.go b/charger/phoenix-charx.go
index 7b3357cb..4aa61a07 100644
--- a/charger/phoenix-charx.go
+++ b/charger/phoenix-charx.go
@@ -180,8 +180,8 @@ func (wb *PhoenixCharx) MaxCurrent(current int64) error {
 
 var _ api.ChargeTimer = (*PhoenixCharx)(nil)
 
-// ChargingTime implements the api.ChargeTimer interface
-func (wb *PhoenixCharx) ChargingTime() (time.Duration, error) {
+// ChargeDuration implements the api.ChargeTimer interface
+func (wb *PhoenixCharx) ChargeDuration() (time.Duration, error) {
 	b, err := wb.conn.ReadHoldingRegisters(wb.register(charxRegChargeTime), 2)
 	if err != nil {
 		return 0, err
diff --git a/charger/phoenix-em-eth.go b/charger/phoenix-em-eth.go
index 7000834f..7d02b531 100644
--- a/charger/phoenix-em-eth.go
+++ b/charger/phoenix-em-eth.go
@@ -128,8 +128,8 @@ func (wb *PhoenixEMEth) MaxCurrent(current int64) error {
 
 var _ api.ChargeTimer = (*PhoenixEMEth)(nil)
 
-// ChargingTime implements the api.ChargeTimer interface
-func (wb *PhoenixEMEth) ChargingTime() (time.Duration, error) {
+// ChargeDuration implements the api.ChargeTimer interface
+func (wb *PhoenixEMEth) ChargeDuration() (time.Duration, error) {
 	b, err := wb.conn.ReadInputRegisters(phxEMEthRegChargeTime, 2)
 	if err != nil {
 		return 0, err
diff --git a/charger/schneider-v3.go b/charger/schneider-v3.go
index f743b064..569a8b5a 100644
--- a/charger/schneider-v3.go
+++ b/charger/schneider-v3.go
@@ -227,8 +227,8 @@ func (wb *Schneider) getPhaseValues(reg uint16) (float64, float64, float64, erro
 
 var _ api.ChargeTimer = (*Schneider)(nil)
 
-// ChargingTime implements the api.ChargeTimer interface
-func (wb *Schneider) ChargingTime() (time.Duration, error) {
+// ChargeDuration implements the api.ChargeTimer interface
+func (wb *Schneider) ChargeDuration() (time.Duration, error) {
 	b, err := wb.conn.ReadHoldingRegisters(schneiderRegSessionChargingTime, 2)
 	if err != nil {
 		return 0, err
diff --git a/charger/twc3.go b/charger/twc3.go
index 416e8848..9cb49ae3 100644
--- a/charger/twc3.go
+++ b/charger/twc3.go
@@ -168,8 +168,8 @@ func (v *Twc3) ChargedEnergy() (float64, error) {
 
 var _ api.ChargeTimer = (*Twc3)(nil)
 
-// ChargingTime implements the api.ChargeTimer interface
-func (v *Twc3) ChargingTime() (time.Duration, error) {
+// ChargeDuration implements the api.ChargeTimer interface
+func (v *Twc3) ChargeDuration() (time.Duration, error) {
 	res, err := v.vitalsG()
 	return time.Duration(res.SessionS) * time.Second, err
 }
diff --git a/charger/vestel.go b/charger/vestel.go
index 210b7d5d..f67be0d4 100644
--- a/charger/vestel.go
+++ b/charger/vestel.go
@@ -182,8 +182,8 @@ func (wb *Vestel) MaxCurrent(current int64) error {
 
 var _ api.ChargeTimer = (*Vestel)(nil)
 
-// ChargingTime implements the api.ChargeTimer interface
-func (wb *Vestel) ChargingTime() (time.Duration, error) {
+// ChargeDuration implements the api.ChargeTimer interface
+func (wb *Vestel) ChargeDuration() (time.Duration, error) {
 	b, err := wb.conn.ReadInputRegisters(vestelRegChargeTime, 2)
 	if err != nil {
 		return 0, err
diff --git a/charger/webasto-next.go b/charger/webasto-next.go
index d827ec90..4f009df3 100644
--- a/charger/webasto-next.go
+++ b/charger/webasto-next.go
@@ -181,8 +181,8 @@ func (wb *WebastoNext) MaxCurrent(current int64) error {
 
 var _ api.ChargeTimer = (*WebastoNext)(nil)
 
-// ChargingTime implements the api.ChargeTimer interface
-func (wb *WebastoNext) ChargingTime() (time.Duration, error) {
+// ChargeDuration implements the api.ChargeTimer interface
+func (wb *WebastoNext) ChargeDuration() (time.Duration, error) {
 	b, err := wb.conn.ReadHoldingRegisters(tqRegChargingTime, 2)
 	if err != nil {
 		return 0, err
diff --git a/cmd/configure/helper.go b/cmd/configure/helper.go
index 47a5de13..9a0bcea9 100644
--- a/cmd/configure/helper.go
+++ b/cmd/configure/helper.go
@@ -253,7 +253,7 @@ func (c *CmdConfigure) askSponsortoken(required, feature bool) error {
 	return err
 }
 
-func (c *CmdConfigure) configureMQTT(templateItem templates.Template) (map[string]interface{}, error) {
+func (c *CmdConfigure) configureMQTT(_ templates.Template) (map[string]interface{}, error) {
 	fmt.Println()
 	fmt.Println("-- MQTT Broker ----------------------------")
 
diff --git a/cmd/dumper.go b/cmd/dumper.go
index 3b82d1f4..afc0e525 100644
--- a/cmd/dumper.go
+++ b/cmd/dumper.go
@@ -133,7 +133,7 @@ func (d *dumper) Dump(name string, v interface{}) {
 	}
 
 	if v, ok := v.(api.ChargeTimer); ok {
-		if duration, err := v.ChargingTime(); err != nil {
+		if duration, err := v.ChargeDuration(); err != nil {
 			fmt.Fprintf(w, "Duration:\t%v\n", err)
 		} else {
 			fmt.Fprintf(w, "Duration:\t%v\n", duration.Truncate(time.Second))
diff --git a/core/coordinator/adapter.go b/core/coordinator/adapter.go
index 1642e750..e7008281 100644
--- a/core/coordinator/adapter.go
+++ b/core/coordinator/adapter.go
@@ -19,8 +19,8 @@ func NewAdapter(lp loadpoint.API, c *Coordinator) API {
 	}
 }
 
-func (a *adapter) GetVehicles() []api.Vehicle {
-	return a.c.GetVehicles()
+func (a *adapter) GetVehicles(availableOnly bool) []api.Vehicle {
+	return a.c.GetVehicles(availableOnly)
 }
 
 func (a *adapter) Owner(v api.Vehicle) loadpoint.API {
diff --git a/core/coordinator/api.go b/core/coordinator/api.go
index 6bb19632..5e2cc913 100644
--- a/core/coordinator/api.go
+++ b/core/coordinator/api.go
@@ -7,8 +7,8 @@ import (
 
 // API is the coordinator API
 type API interface {
-	// GetVehicles returns the list of all vehicles
-	GetVehicles() []api.Vehicle
+	// GetVehicles returns the list of all vehicles, filtered by availability
+	GetVehicles(availableOnly bool) []api.Vehicle
 
 	// Owner returns the loadpoint that currently owns the vehicle
 	Owner(api.Vehicle) loadpoint.API
diff --git a/core/coordinator/coordinator.go b/core/coordinator/coordinator.go
index 3eee8dad..01b4e4f3 100644
--- a/core/coordinator/coordinator.go
+++ b/core/coordinator/coordinator.go
@@ -1,7 +1,6 @@
 package coordinator
 
 import (
-	"slices"
 	"sync"
 
 	"github.com/evcc-io/evcc/api"
@@ -27,11 +26,18 @@ func New(log *util.Logger, vehicles []api.Vehicle) *Coordinator {
 }
 
 // GetVehicles returns the list of all vehicles
-func (c *Coordinator) GetVehicles() []api.Vehicle {
+func (c *Coordinator) GetVehicles(availableOnly bool) []api.Vehicle {
 	c.mu.RLock()
 	defer c.mu.RUnlock()
 
-	return slices.Clone(c.vehicles)
+	res := make([]api.Vehicle, 0, len(c.vehicles))
+	for _, v := range c.vehicles {
+		if _, tracked := c.tracked[v]; !availableOnly || availableOnly && !tracked {
+			res = append(res, v)
+		}
+	}
+
+	return res
 }
 
 // Owner returns the loadpoint that currently owns the vehicle
diff --git a/core/coordinator/dummy.go b/core/coordinator/dummy.go
index 07eaee80..fddb037a 100644
--- a/core/coordinator/dummy.go
+++ b/core/coordinator/dummy.go
@@ -12,7 +12,7 @@ func NewDummy() API {
 	return new(dummy)
 }
 
-func (a *dummy) GetVehicles() []api.Vehicle {
+func (a *dummy) GetVehicles(_ bool) []api.Vehicle {
 	return nil
 }
 
diff --git a/core/loadpoint.go b/core/loadpoint.go
index 7f9aba93..b5c76e86 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"math"
 	"reflect"
+	"slices"
 	"strings"
 	"sync"
 	"testing"
@@ -470,6 +471,9 @@ func (lp *Loadpoint) evVehicleConnectHandler() {
 		lp.socEstimator.Reset()
 	}
 
+	// get pv mode before vehicle defaults are applied
+	pvMode := lp.GetMode() == api.ModePV || lp.GetMode() == api.ModeMinPV
+
 	// set default or start detection
 	if !lp.chargerHasFeature(api.IntegratedDevice) {
 		lp.vehicleDefaultOrDetect()
@@ -478,6 +482,18 @@ func (lp *Loadpoint) evVehicleConnectHandler() {
 	// immediately allow pv mode activity
 	lp.elapsePVTimer()
 
+	// Enable charging on connect if any available vehicle requires it. We're using the PV timer
+	// to disable after the welcome, hence this must be placed after elapsePVTimer.
+	// TODO check is this doesn't conflict with vehicle defaults like mode: off
+	if pvMode {
+		for _, v := range lp.availableVehicles() {
+			if slices.Contains(v.Features(), api.WelcomeCharge) {
+				lp.setLimit(lp.effectiveMinCurrent())
+				break
+			}
+		}
+	}
+
 	// create charging session
 	lp.createSession()
 }
@@ -1394,7 +1410,7 @@ func (lp *Loadpoint) publishChargeProgress() {
 		lp.log.ERROR.Printf("charge rater: %v", err)
 	}
 
-	if d, err := lp.chargeTimer.ChargingTime(); err == nil {
+	if d, err := lp.chargeTimer.ChargeDuration(); err == nil {
 		lp.chargeDuration = d.Round(time.Second)
 	} else {
 		lp.log.ERROR.Printf("charge timer: %v", err)
diff --git a/core/loadpoint_vehicle.go b/core/loadpoint_vehicle.go
index a8b5b502..c2afa4ca 100644
--- a/core/loadpoint_vehicle.go
+++ b/core/loadpoint_vehicle.go
@@ -20,12 +20,20 @@ const (
 	vehicleDetectDuration = 10 * time.Minute
 )
 
+// availableVehicles is the slice of vehicles from the coordinator that are available
+func (lp *Loadpoint) availableVehicles() []api.Vehicle {
+	if lp.coordinator == nil {
+		return nil
+	}
+	return lp.coordinator.GetVehicles(true)
+}
+
 // coordinatedVehicles is the slice of vehicles from the coordinator
 func (lp *Loadpoint) coordinatedVehicles() []api.Vehicle {
 	if lp.coordinator == nil {
 		return nil
 	}
-	return lp.coordinator.GetVehicles()
+	return lp.coordinator.GetVehicles(false)
 }
 
 // setVehicleIdentifier updated the vehicle id as read from the charger
diff --git a/core/wrapper/chargetimer.go b/core/wrapper/chargetimer.go
index bf3f44ce..8279fe88 100644
--- a/core/wrapper/chargetimer.go
+++ b/core/wrapper/chargetimer.go
@@ -48,8 +48,8 @@ func (m *ChargeTimer) StopCharge() {
 	m.duration += m.clck.Since(m.start)
 }
 
-// ChargingTime implements the api.ChargeTimer interface
-func (m *ChargeTimer) ChargingTime() (time.Duration, error) {
+// ChargeDuration implements the api.ChargeTimer interface
+func (m *ChargeTimer) ChargeDuration() (time.Duration, error) {
 	m.Lock()
 	defer m.Unlock()
 
diff --git a/go.mod b/go.mod
index a2bd4f8d..ad37684d 100644
--- a/go.mod
+++ b/go.mod
@@ -63,7 +63,7 @@ require (
 	github.com/mabunixda/wattpilot v1.6.6
 	github.com/mitchellh/go-homedir v1.1.0
 	github.com/mitchellh/mapstructure v1.5.0
-	github.com/mlnoga/rct v0.1.2-0.20230731074838-03eacb926f99
+	github.com/mlnoga/rct v0.1.2-0.20240421173556-1c5b75037e2f
 	github.com/muka/go-bluetooth v0.0.0-20240115085408-dfdf79b8f61d
 	github.com/mxschmitt/golang-combinations v1.1.0
 	github.com/nicksnyder/go-i18n/v2 v2.4.0
@@ -197,5 +197,3 @@ require (
 )
 
 replace github.com/spf13/viper => github.com/spf13/viper v1.18.1
-
-replace github.com/mlnoga/rct => github.com/andig/rct v0.1.2-0.20240419102452-054695c3e7c2
diff --git a/go.sum b/go.sum
index 1c816b4b..0d73a3ca 100644
--- a/go.sum
+++ b/go.sum
@@ -35,8 +35,6 @@ github.com/andig/gosunspec v0.0.0-20231205122018-1daccfa17912 h1:pphS+WUa9zK0ZH/
 github.com/andig/gosunspec v0.0.0-20231205122018-1daccfa17912/go.mod h1:c6P6szcR+ROkqZruOR4f6qbDKFjZX6OitPpj+yJ/r8k=
 github.com/andig/mbserver v0.0.0-20230310211055-1d29cbb5820e h1:m/NTP3JWpR7M0ljLxiQU4fzR25jjhe1LDtxLMNcoNJQ=
 github.com/andig/mbserver v0.0.0-20230310211055-1d29cbb5820e/go.mod h1:4VtYzTm//oUipwvO3yh0g/udTE7pYJM+U/kyAuFDsgM=
-github.com/andig/rct v0.1.2-0.20240419102452-054695c3e7c2 h1:hAQe9ESVfjlXi8NTyy+BnYtlIseP40GemlxTKmQCq4g=
-github.com/andig/rct v0.1.2-0.20240419102452-054695c3e7c2/go.mod h1:0lfd2mmBnBzIvuzYtdhG+2371u+cUfIxsYErm4P9KRI=
 github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
 github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
 github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
@@ -436,6 +434,8 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
 github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/mlnoga/rct v0.1.2-0.20240421173556-1c5b75037e2f h1:63hbvjkIpccli0EA60fqvzokvU1fH1ZPiNzlF/vUmvM=
+github.com/mlnoga/rct v0.1.2-0.20240421173556-1c5b75037e2f/go.mod h1:0lfd2mmBnBzIvuzYtdhG+2371u+cUfIxsYErm4P9KRI=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
diff --git a/meter/goodwe/server.go b/meter/goodwe/server.go
index 6a3cc327..80c0ce8c 100644
--- a/meter/goodwe/server.go
+++ b/meter/goodwe/server.go
@@ -110,7 +110,7 @@ func (m *Server) listen() {
 					return float64(int16(binary.BigEndian.Uint16(buf[u:]))) *
 						float64(int16(binary.BigEndian.Uint16(buf[i:]))) / 100
 				}
-				inverter.PvPower = ui(11, 13) + ui(19, 21)
+				inverter.PvPower = ui(11, 13) + ui(19, 21) + ui(27, 29) + ui(35, 37)
 				inverter.BatteryPower = ui(165, 167)
 				inverter.NetPower = -float64(int32(binary.BigEndian.Uint32(buf[83:])))
 			}
diff --git a/templates/definition/charger/daheimladen.yaml b/templates/definition/charger/daheimladen.yaml
index c028f0c7..2916e6fa 100644
--- a/templates/definition/charger/daheimladen.yaml
+++ b/templates/definition/charger/daheimladen.yaml
@@ -1,4 +1,5 @@
 template: daheimladen
+deprecated: true
 products:
   - brand: DaheimLaden
     description:
diff --git a/templates/definition/charger/keba-udp.yaml b/templates/definition/charger/keba-udp.yaml
index 5c0bd9a9..27f56928 100644
--- a/templates/definition/charger/keba-udp.yaml
+++ b/templates/definition/charger/keba-udp.yaml
@@ -1,4 +1,5 @@
 template: keba
+deprecated: true
 products:
   - brand: KEBA
     description:
diff --git a/templates/definition/charger/victron.yaml b/templates/definition/charger/victron.yaml
index 9b85113b..7dd26adc 100644
--- a/templates/definition/charger/victron.yaml
+++ b/templates/definition/charger/victron.yaml
@@ -6,8 +6,8 @@ products:
 requirements:
   evcc: ["sponsorship"]
   description:
-    en: Charger has to be in manual mode and Modbus has to be configured for ID 100.
-    de: Wallbox muss sich im Modus "Manual" befinden und Modbus ID 100 konfiguriert sein.
+    en: Enter the host of the GX device (not the charger). The charger has to be in manual mode and Modbus has to be configured for ID 100.
+    de: Trage den Host des GX-Gerätes (nicht der Wallbox) ein. Die Wallbox muss sich im Modus "Manual" befinden und Modbus ID 100 konfiguriert sein.
 params:
   - name: modbus
     choice: ["tcpip"]
diff --git a/templates/definition/charger/wallbe-meter.yaml b/templates/definition/charger/wallbe-meter.yaml
index 5e8b4cff..e76915e3 100644
--- a/templates/definition/charger/wallbe-meter.yaml
+++ b/templates/definition/charger/wallbe-meter.yaml
@@ -1,4 +1,5 @@
 template: wallbe-meter
+deprecated: true
 products:
   - brand: Wallbe
     description:
diff --git a/templates/definition/charger/wallbe-pre2019-meter.yaml b/templates/definition/charger/wallbe-pre2019-meter.yaml
index ff591d73..26c6dbd6 100644
--- a/templates/definition/charger/wallbe-pre2019-meter.yaml
+++ b/templates/definition/charger/wallbe-pre2019-meter.yaml
@@ -1,4 +1,5 @@
 template: wallbe-pre2019-meter
+deprecated: true
 products:
   - brand: Wallbe
     description:
diff --git a/templates/definition/charger/wallbe-pre2019.yaml b/templates/definition/charger/wallbe-pre2019.yaml
index 6fbaeddb..2d29e328 100644
--- a/templates/definition/charger/wallbe-pre2019.yaml
+++ b/templates/definition/charger/wallbe-pre2019.yaml
@@ -1,4 +1,5 @@
 template: wallbe-pre2019
+deprecated: true
 products:
   - brand: Wallbe
     description:
diff --git a/templates/definition/charger/wallbe.yaml b/templates/definition/charger/wallbe.yaml
index f5e20ed6..8560d24b 100644
--- a/templates/definition/charger/wallbe.yaml
+++ b/templates/definition/charger/wallbe.yaml
@@ -1,4 +1,5 @@
 template: wallbe
+deprecated: true
 products:
   - brand: Wallbe
     description:
diff --git a/templates/definition/vehicle/hyundai.yaml b/templates/definition/vehicle/hyundai.yaml
index 2e2621b7..ccd1df9e 100644
--- a/templates/definition/vehicle/hyundai.yaml
+++ b/templates/definition/vehicle/hyundai.yaml
@@ -3,6 +3,10 @@ products:
   - brand: Hyundai
     description:
       generic: Bluelink
+requirements:
+  description:
+    en: Some models (e.g. Kona) switch internally to 2 phases at low charging currents (< 8A). In cases where the wallbox also measures the phase currents, this leads to undesirable fluctuations in the charging power. The remedy here is to set the minimum charging current to 8A.
+    de: Manche Modelle (z.B. Kona) schalten bei geringen Ladeströmen (< 8A) intern auf 2 Phasen um. In den Fällen, in denen die Wallbox auch die Phasenströme misst, führt das zu unerwünschten Schwankungen der Ladeleistung. Abhilfe schafft hier, den Mindestladestrom auf 8A zu setzen.
 params:
   - preset: vehicle-base
   - preset: vehicle-language
diff --git a/util/templates/init.go b/util/templates/init.go
index 5dccb352..65a9461c 100644
--- a/util/templates/init.go
+++ b/util/templates/init.go
@@ -11,6 +11,7 @@ import (
 	"text/template"
 
 	"github.com/evcc-io/evcc/templates/definition"
+	"github.com/samber/lo"
 	"gopkg.in/yaml.v3"
 )
 
@@ -109,10 +110,32 @@ func EncoderLanguage(lang string) {
 	encoderLanguage = lang
 }
 
-func ByClass(class Class) []Template {
-	return templates[class]
+type filterFunc func([]Template) []Template
+
+// WithDeprecated returns a filterFunc that includes all templates
+func WithDeprecated() filterFunc {
+	return func(t []Template) []Template {
+		return t
+	}
+}
+
+// ByClass returns templates for class excluding deprecated templates
+func ByClass(class Class, opt ...filterFunc) []Template {
+	res := templates[class]
+	if len(opt) == 0 {
+		opt = append(opt, func(t []Template) []Template {
+			return lo.Filter(t, func(t Template, _ int) bool {
+				return !t.Deprecated
+			})
+		})
+	}
+	for _, o := range opt {
+		res = o(res)
+	}
+	return res
 }
 
+// ByClass returns templates for class and name including deprecated templates
 func ByName(class Class, name string) (Template, error) {
 	for _, tmpl := range templates[class] {
 		if tmpl.Template == name || slices.Contains(tmpl.Covers, name) {
diff --git a/util/templates/types.go b/util/templates/types.go
index a1fc6c39..42c7158e 100644
--- a/util/templates/types.go
+++ b/util/templates/types.go
@@ -253,6 +253,7 @@ func (p Product) Title(lang string) string {
 // TemplateDefinition contains properties of a device template
 type TemplateDefinition struct {
 	Template     string
+	Deprecated   bool             `json:"-"`
 	Group        string           `json:",omitempty"` // the group this template belongs to, references groupList entries
 	Covers       []string         `json:",omitempty"` // list of covered outdated template names
 	Products     []Product        `json:",omitempty"` // list of products this template is compatible with


Test patch:
diff --git a/charger/mcc_test.go b/charger/mcc_test.go
index 9720f6a7..1cfa41ba 100644
--- a/charger/mcc_test.go
+++ b/charger/mcc_test.go
@@ -369,13 +369,13 @@ func TestMobileConnectChargingTime(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			mcc := NewTestMobileConnect(t, tc.responses)
 
-			got, err := mcc.ChargingTime()
+			got, err := mcc.ChargeDuration()
 			if (err != nil) != tc.wantErr {
-				t.Errorf("MobileConnect.ChargingTime() error = %v, wantErr %v", err, tc.wantErr)
+				t.Errorf("MobileConnect.ChargeDuration() error = %v, wantErr %v", err, tc.wantErr)
 				return
 			}
 			if got != tc.want {
-				t.Errorf("MobileConnect.ChargingTime() = %v, want %v", got, tc.want)
+				t.Errorf("MobileConnect.ChargeDuration() = %v, want %v", got, tc.want)
 			}
 		})
 	}
diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go
index ecd6e472..92ecbaf1 100644
--- a/core/loadpoint_test.go
+++ b/core/loadpoint_test.go
@@ -29,7 +29,7 @@ func (n *Null) ChargedEnergy() (float64, error) {
 	return 0, nil
 }
 
-func (n *Null) ChargingTime() (time.Duration, error) {
+func (n *Null) ChargeDuration() (time.Duration, error) {
 	return 0, nil
 }
 
diff --git a/core/wrapper/chargetimer_test.go b/core/wrapper/chargetimer_test.go
index a75192ca..f9746166 100644
--- a/core/wrapper/chargetimer_test.go
+++ b/core/wrapper/chargetimer_test.go
@@ -17,7 +17,7 @@ func TestTimer(t *testing.T) {
 	ct.StopCharge()
 	clck.Add(time.Hour)
 
-	if d, err := ct.ChargingTime(); d != 1*time.Hour || err != nil {
+	if d, err := ct.ChargeDuration(); d != 1*time.Hour || err != nil {
 		t.Error(d, err)
 	}
 
@@ -26,7 +26,7 @@ func TestTimer(t *testing.T) {
 	clck.Add(2 * time.Hour)
 	ct.StopCharge()
 
-	if d, err := ct.ChargingTime(); d != 3*time.Hour || err != nil {
+	if d, err := ct.ChargeDuration(); d != 3*time.Hour || err != nil {
 		t.Error(d, err)
 	}
 }
diff --git a/util/templates/render_testing.go b/util/templates/render_testing.go
index a37db319..17b272c8 100644
--- a/util/templates/render_testing.go
+++ b/util/templates/render_testing.go
@@ -36,7 +36,7 @@ func test(t *testing.T, tmpl Template, values map[string]interface{}, cb func(va
 func TestClass(t *testing.T, class Class, instantiate func(t *testing.T, values map[string]interface{})) {
 	t.Parallel()
 
-	for _, tmpl := range ByClass(class) {
+	for _, tmpl := range ByClass(class, WithDeprecated()) {
 		tmpl := tmpl
 
 		// set default values for all params