Repository: evcc-io/evcc
Pull request #7563 (2023-04-20 08:12:41)

EEBUS: Only adjust currents if active vehicle has none set
This should fix https://github.com/evcc-io/evcc/issues/7560


Patch:
diff --git a/charger/eebus.go b/charger/eebus.go
index 0887fa56..1437d9d8 100644
--- a/charger/eebus.go
+++ b/charger/eebus.go
@@ -170,10 +170,10 @@ func (c *EEBus) setLoadpointMinMaxLimits() {
 	newMin := minLimits[0]
 	newMax := maxLimits[0]
 
-	if c.lp.GetMinCurrent() != newMin && newMin > 0 {
+	if c.lp.GetMinCurrent() != newMin && newMin > 0 && c.lp.GetVehicle().OnIdentified().MinCurrent == nil {
 		c.lp.SetMinCurrent(newMin)
 	}
-	if c.lp.GetMaxCurrent() != newMax && newMax > 0 {
+	if c.lp.GetMaxCurrent() != newMax && newMax > 0 && c.lp.GetVehicle().OnIdentified().MaxCurrent == nil {
 		c.lp.SetMaxCurrent(newMax)
 	}
 }
diff --git a/core/loadpoint_plan.go b/core/loadpoint_plan.go
index baacd961..20d17c7a 100644
--- a/core/loadpoint_plan.go
+++ b/core/loadpoint_plan.go
@@ -38,23 +38,7 @@ func (lp *Loadpoint) planRequiredDuration(maxPower float64) time.Duration {
 		targetSoc = 100
 	}
 
-	requiredDuration := lp.socEstimator.RemainingChargeDuration(targetSoc, maxPower)
-	if requiredDuration <= 0 {
-		return 0
-	}
-
-	// anticipate lower charge rates at end of charging curve
-	var additionalDuration time.Duration
-
-	if targetSoc > 80 && maxPower > 15000 {
-		additionalDuration = time.Duration(float64(targetSoc-80) / (float64(targetSoc) - lp.vehicleSoc) * float64(requiredDuration))
-		lp.log.DEBUG.Printf("add additional charging time %v for soc > 80%%", additionalDuration.Round(time.Minute))
-	} else if targetSoc > 90 && maxPower > 4000 {
-		additionalDuration = time.Duration(float64(targetSoc-90) / (float64(targetSoc) - lp.vehicleSoc) * float64(requiredDuration))
-		lp.log.DEBUG.Printf("add additional charging time %v for soc > 90%%", additionalDuration.Round(time.Minute))
-	}
-
-	return requiredDuration + additionalDuration
+	return lp.socEstimator.RemainingChargeDuration(targetSoc, maxPower)
 }
 
 func (lp *Loadpoint) GetPlannerUnit() string {
diff --git a/core/soc/estimator.go b/core/soc/estimator.go
index 552af9ee..3ff2cc9b 100644
--- a/core/soc/estimator.go
+++ b/core/soc/estimator.go
@@ -27,6 +27,9 @@ type Estimator struct {
 	prevSoc           float64 // previous vehicle Soc in %
 	prevChargedEnergy float64 // previous charged energy in Wh
 	energyPerSocStep  float64 // Energy per Soc percent in Wh
+	minChargePower    float64 // Lowest charge power (just before vehicle stops charging at 100%)
+	maxChargePower    float64 // Highest charge power the battery can handle on any charger
+	maxChargeSoc      float64 // SoC at/after which maxChargePower is degressive
 }
 
 // NewEstimator creates new estimator
@@ -51,15 +54,42 @@ func (s *Estimator) Reset() {
 	s.capacity = float64(s.vehicle.Capacity()) * 1e3  // cache to simplify debugging
 	s.virtualCapacity = s.capacity / ChargeEfficiency // initial capacity taking efficiency into account
 	s.energyPerSocStep = s.virtualCapacity / 100
+	s.minChargePower = 1000  // default 1 kW
+	s.maxChargePower = 50000 // default 50 kW
+	s.maxChargeSoc = 50      // default 50%
 }
 
 // RemainingChargeDuration returns the estimated remaining duration
 func (s *Estimator) RemainingChargeDuration(targetSoc int, chargePower float64) time.Duration {
-	energy := s.RemainingChargeEnergy(targetSoc) * 1e3 / chargePower
-	if math.IsInf(energy, 0) {
-		energy = 0
+	const minChargeSoc = 100
+
+	dy := s.minChargePower - s.maxChargePower
+	dx := minChargeSoc - s.maxChargeSoc
+
+	var rrp float64 = 100
+
+	if dy < 0 && dx > 0 {
+		m := dy / dx
+		b := s.minChargePower - m*minChargeSoc
+
+		// Relativer Reduktionspunkt
+		rrp = (chargePower - b) / m
 	}
-	return time.Duration(float64(time.Hour) * energy).Round(time.Second)
+
+	var t1, t2 float64
+
+	// Zeit von vehicleSoc bis Reduktionspunkt (linear)
+	if s.vehicleSoc < rrp {
+		t1 = (math.Min(float64(targetSoc), rrp) - s.vehicleSoc) / minChargeSoc * s.virtualCapacity / chargePower
+	}
+
+	// Zeit von Reduktionspunkt bis targetSoc (degressiv)
+	if float64(targetSoc) > rrp {
+		t2 = (float64(targetSoc) - math.Max(s.vehicleSoc, rrp)) / minChargeSoc * s.virtualCapacity / ((chargePower-s.minChargePower)/2 + s.minChargePower)
+
+	}
+
+	return time.Duration(float64(time.Hour) * (t1 + t2)).Round(time.Second)
 }
 
 // RemainingChargeEnergy returns the remaining charge energy in kWh


Test patch:
diff --git a/core/soc/estimator_test.go b/core/soc/estimator_test.go
index 4a41033e..66240fe0 100644
--- a/core/soc/estimator_test.go
+++ b/core/soc/estimator_test.go
@@ -9,6 +9,7 @@ import (
 	"github.com/evcc-io/evcc/mock"
 	"github.com/evcc-io/evcc/util"
 	"github.com/golang/mock/gomock"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestRemainingChargeDuration(t *testing.T) {
@@ -207,3 +208,38 @@ func TestSocFromChargerAndVehicleWithErrors(t *testing.T) {
 		}
 	}
 }
+
+func TestImprovedEstimatorRemainingChargeDuration(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	charger := mock.NewMockCharger(ctrl)
+	vehicle := mock.NewMockVehicle(ctrl)
+
+	// https://github.com/evcc-io/evcc/pull/7510#issuecomment-1512688548
+	tc := []struct {
+		capacity    float64
+		soc         float64
+		targetsoc   int
+		chargePower float64
+		duration    time.Duration
+	}{
+		{0.75, 10, 60, 300, 1*time.Hour + 23*time.Minute + 20*time.Second},
+		{0.75, 50, 100, 300, 1*time.Hour + 23*time.Minute + 20*time.Second},
+		{17, 10, 60, 7 * 1e3, 1*time.Hour + 20*time.Minute + 57*time.Second},
+		{17, 50, 100, 7 * 1e3, 1*time.Hour + 28*time.Minute + 23*time.Second},
+		{50, 10, 60, 11 * 1e3, 2*time.Hour + 31*time.Minute + 31*time.Second},
+		{50, 50, 100, 11 * 1e3, 2*time.Hour + 57*time.Minute + 17*time.Second},
+		{80, 10, 60, 22 * 1e3, 2*time.Hour + 01*time.Minute + 13*time.Second},
+		{80, 50, 100, 22 * 1e3, 2*time.Hour + 48*time.Minute + 39*time.Second},
+	}
+
+	for _, tc := range tc {
+		t.Log(tc)
+
+		vehicle.EXPECT().Capacity().Return(tc.capacity)
+
+		ce := NewEstimator(util.NewLogger("foo"), charger, vehicle, false)
+		ce.vehicleSoc = tc.soc
+
+		assert.Equal(t, tc.duration, ce.RemainingChargeDuration(tc.targetsoc, tc.chargePower))
+	}
+}