Repository: evcc-io/evcc
Pull request #1469 (2021-09-02 10:59:37)

Add bufferSoc
Refs https://github.com/evcc-io/evcc/discussions/1361

premultiply: Ist das schon sinnvoll testbar?
andig: Ja
premultiply: Ich warte auf passende Testbedingungen (Wetter)
andig: Soll ichs einfach rein schieben? Release ist durch...

Patch:
diff --git a/charger/easee.go b/charger/easee.go
index 790cd8d3..88121c69 100644
--- a/charger/easee.go
+++ b/charger/easee.go
@@ -24,7 +24,6 @@ import (
 
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/charger/easee"
-	"github.com/evcc-io/evcc/core/loadpoint"
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/request"
 	"github.com/evcc-io/evcc/util/sponsor"
@@ -40,12 +39,9 @@ type Easee struct {
 	status        easee.ChargerStatus
 	updated       time.Time
 	cache         time.Duration
-	lp            loadpoint.API
-	//lastSmartCharging bool
-	//lastChargeMode api.ChargeMode
-	log     *util.Logger
-	phases  int
-	current float64
+	log           *util.Logger
+	phases        int
+	current       float64
 }
 
 func init() {
@@ -155,51 +151,6 @@ func (c *Easee) chargerDetails() (res easee.Site, err error) {
 	return res, err
 }
 
-/*
-func (c *Easee) syncSmartCharging() error {
-	if c.lp == nil {
-		return nil
-	}
-
-	if c.lp.GetMode() != c.lastChargeMode {
-		c.log.DEBUG.Printf("charge mode changed by loadpoint: %v -> %v", c.lastChargeMode, c.lp.GetMode())
-		newSmartCharging := false
-		if c.lp.GetMode() == api.ModePV {
-			newSmartCharging = true
-		}
-
-		data := easee.ChargerSettings{
-			SmartCharging: &newSmartCharging,
-		}
-
-		uri := fmt.Sprintf("%s/chargers/%s/settings", easee.API, c.charger)
-		resp, err := c.Post(uri, request.JSONContent, request.MarshalJSON(data))
-		if err == nil {
-			resp.Body.Close()
-		}
-
-		c.updated = time.Time{} // clear cache
-
-		c.lastChargeMode = c.lp.GetMode()
-		c.lastSmartCharging = newSmartCharging
-
-		return err
-	}
-
-	if c.lastSmartCharging != c.status.SmartCharging {
-		c.log.DEBUG.Printf("smart status changed by charger: %v -> %v", c.lastSmartCharging, c.status.SmartCharging)
-		if c.status.SmartCharging {
-			c.lp.SetMode(api.ModePV)
-		} else {
-			c.lp.SetMode(api.ModeNow)
-		}
-		c.lastSmartCharging = c.status.SmartCharging
-		c.lastChargeMode = c.lp.GetMode()
-	}
-	return nil
-}
-*/
-
 func (c *Easee) state() (easee.ChargerStatus, error) {
 	if time.Since(c.updated) < c.cache {
 		return c.status, nil
@@ -209,7 +160,6 @@ func (c *Easee) state() (easee.ChargerStatus, error) {
 	req, err := request.New(http.MethodGet, uri, nil, request.JSONEncoding)
 	if err == nil {
 		if err = c.DoJSON(req, &c.status); err == nil {
-			// err = c.syncSmartCharging()
 			c.updated = time.Now()
 		}
 	}
@@ -347,10 +297,3 @@ func (c *Easee) Currents() (float64, float64, float64, error) {
 		res.CircuitTotalPhaseConductorCurrentL3,
 		err
 }
-
-var _ loadpoint.Controller = (*Easee)(nil)
-
-// LoadpointControl implements loadpoint.Controller
-func (c *Easee) LoadpointControl(lp loadpoint.API) {
-	c.lp = lp
-}
diff --git a/core/loadpoint.go b/core/loadpoint.go
index d69261c1..51d24c08 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -953,7 +953,7 @@ func (lp *LoadPoint) pvScalePhases(availablePower, minCurrent, maxCurrent float6
 }
 
 // pvMaxCurrent calculates the maximum target current for PV mode
-func (lp *LoadPoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64) float64 {
+func (lp *LoadPoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64, batteryBuffered bool) float64 {
 	// read only once to simplify testing
 	minCurrent := lp.GetMinCurrent()
 	maxCurrent := lp.GetMaxCurrent()
@@ -976,7 +976,7 @@ func (lp *LoadPoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64) float6
 	}
 
 	// in MinPV mode return at least minCurrent
-	if mode == api.ModeMinPV && targetCurrent < minCurrent {
+	if (mode == api.ModeMinPV || batteryBuffered) && targetCurrent < minCurrent {
 		return minCurrent
 	}
 
@@ -1206,7 +1206,7 @@ func (lp *LoadPoint) publishSoCAndRange() {
 }
 
 // Update is the main control function. It reevaluates meters and charger state
-func (lp *LoadPoint) Update(sitePower float64, cheap bool) {
+func (lp *LoadPoint) Update(sitePower float64, cheap bool, batteryBuffered bool) {
 	mode := lp.GetMode()
 	lp.publish("mode", mode)
 
@@ -1302,7 +1302,7 @@ func (lp *LoadPoint) Update(sitePower float64, cheap bool) {
 		err = lp.setLimit(targetCurrent, true)
 
 	case mode == api.ModeMinPV || mode == api.ModePV:
-		targetCurrent := lp.pvMaxCurrent(mode, sitePower)
+		targetCurrent := lp.pvMaxCurrent(mode, sitePower, batteryBuffered)
 		lp.log.DEBUG.Printf("pv max charge current: %.3gA", targetCurrent)
 
 		var required bool // false
diff --git a/core/site.go b/core/site.go
index 55176ad4..05ade408 100644
--- a/core/site.go
+++ b/core/site.go
@@ -14,11 +14,9 @@ import (
 	"github.com/evcc-io/evcc/util"
 )
 
-//go:generate mockgen -package mock -destination ../mock/mock_loadpoint.go github.com/evcc-io/evcc/core Updater
-
 // Updater abstracts the LoadPoint implementation for testing
 type Updater interface {
-	Update(float64, bool)
+	Update(availablePower float64, cheapRate bool, batteryBuffered bool)
 }
 
 // Site is the main configuration container. A site can host multiple loadpoints.
@@ -37,6 +35,7 @@ type Site struct {
 	ResidualPower float64      `mapstructure:"residualPower"` // PV meter only: household usage. Grid meter: household safety margin
 	Meters        MetersConfig // Meter references
 	PrioritySoC   float64      `mapstructure:"prioritySoC"` // prefer battery up to this SoC
+	BufferSoC     float64      `mapstructure:"bufferSoC"`   // ignore battery above this SoC
 
 	// meters
 	gridMeter    api.Meter   // Grid usage meter
@@ -47,9 +46,10 @@ type Site struct {
 	loadpoints []*LoadPoint // Loadpoints
 
 	// cached state
-	gridPower    float64 // Grid power
-	pvPower      float64 // PV power
-	batteryPower float64 // Battery charge power
+	gridPower       float64 // Grid power
+	pvPower         float64 // PV power
+	batteryPower    float64 // Battery charge power
+	batteryBuffered bool    // Battery buffer active
 }
 
 // MetersConfig contains the loadpoint's meter configuration
@@ -323,11 +323,14 @@ func (site *Site) sitePower() (float64, error) {
 			site.Lock()
 			defer site.Unlock()
 
-			// if battery is charging give it priority
+			// if battery is charging below prioritySoC give it priority
 			if soc < site.PrioritySoC && batteryPower < 0 {
-				site.log.DEBUG.Printf("giving priority to battery at soc: %.0f", soc)
+				site.log.DEBUG.Printf("giving priority to battery charging at soc: %.0f", soc)
 				batteryPower = 0
 			}
+
+			// if battery is discharging above bufferSoC ignore it
+			site.batteryBuffered = batteryPower > 0 && site.BufferSoC > 0 && soc > site.BufferSoC
 		}
 	}
 
@@ -346,7 +349,7 @@ func (site *Site) update(lp Updater) {
 	}
 
 	if sitePower, err := site.sitePower(); err == nil {
-		lp.Update(sitePower, cheap)
+		lp.Update(sitePower, cheap, site.batteryBuffered)
 		site.Health.Update()
 	}
 }
diff --git a/evcc.dist.yaml b/evcc.dist.yaml
index 50154305..4a09d556 100644
--- a/evcc.dist.yaml
+++ b/evcc.dist.yaml
@@ -61,7 +61,8 @@ site:
     pvs:
     - pv # list of pv inverters/ meters
     battery: battery # battery meter
-  prioritySoC: 60 # give home battery priority up to this soc (0 to disable)
+  prioritySoC: # give home battery priority up to this soc (empty to disable)
+  bufferSoC: # ignore home battery discharge above soc (empty to disable)
 
 # loadpoint describes the charger, charge meter and connected vehicle
 loadpoints:
diff --git a/mock/mock_loadpoint.go b/mock/mock_loadpoint.go
index c0807ccb..7f488e1a 100644
--- a/mock/mock_loadpoint.go
+++ b/mock/mock_loadpoint.go
@@ -34,13 +34,13 @@ func (m *MockUpdater) EXPECT() *MockUpdaterMockRecorder {
 }
 
 // Update mocks base method.
-func (m *MockUpdater) Update(arg0 float64, arg1 bool) {
+func (m *MockUpdater) Update(arg0 float64, arg1, arg2 bool) {
 	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "Update", arg0, arg1)
+	m.ctrl.Call(m, "Update", arg0, arg1, arg2)
 }
 
 // Update indicates an expected call of Update.
-func (mr *MockUpdaterMockRecorder) Update(arg0, arg1 interface{}) *gomock.Call {
+func (mr *MockUpdaterMockRecorder) Update(arg0, arg1, arg2 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockUpdater)(nil).Update), arg0, arg1)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockUpdater)(nil).Update), arg0, arg1, arg2)
 }


Test patch:
diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go
index a5dfcaf9..aace072f 100644
--- a/core/loadpoint_test.go
+++ b/core/loadpoint_test.go
@@ -168,7 +168,7 @@ func TestUpdatePowerZero(t *testing.T) {
 		}
 
 		lp.Mode = tc.mode
-		lp.Update(0, false) // sitePower 0
+		lp.Update(0, false, false) // sitePower 0
 
 		ctrl.Finish()
 	}
@@ -324,7 +324,7 @@ func TestPVHysteresis(t *testing.T) {
 				// charger.EXPECT().Enabled().Return(tc.enabled, nil)
 
 				lp.enabled = tc.enabled
-				current := lp.pvMaxCurrent(api.ModePV, se.site)
+				current := lp.pvMaxCurrent(api.ModePV, se.site, false)
 
 				if current != se.current {
 					t.Errorf("step %d: wanted %.1f, got %.1f", step, se.current, current)
@@ -355,7 +355,7 @@ func TestPVHysteresisForStatusOtherThanC(t *testing.T) {
 
 	// maxCurrent will read enabled state in PV mode
 	sitePower := -float64(lp.Phases)*minA*Voltage + 1 // 1W below min power
-	current := lp.pvMaxCurrent(api.ModePV, sitePower)
+	current := lp.pvMaxCurrent(api.ModePV, sitePower, false)
 
 	if current != 0 {
 		t.Errorf("PV mode could not disable charger as expected. Expected 0, got %.f", current)
@@ -406,7 +406,7 @@ func TestDisableAndEnableAtTargetSoC(t *testing.T) {
 	charger.EXPECT().Status().Return(api.StatusC, nil)
 	charger.EXPECT().Enabled().Return(lp.enabled, nil)
 	charger.EXPECT().MaxCurrent(int64(maxA)).Return(nil)
-	lp.Update(500, false)
+	lp.Update(500, false, false)
 
 	t.Log("charging above target - soc deactivates charger")
 	clock.Add(5 * time.Minute)
@@ -414,20 +414,20 @@ func TestDisableAndEnableAtTargetSoC(t *testing.T) {
 	charger.EXPECT().Status().Return(api.StatusC, nil)
 	charger.EXPECT().Enabled().Return(lp.enabled, nil)
 	charger.EXPECT().Enable(false).Return(nil)
-	lp.Update(500, false)
+	lp.Update(500, false, false)
 
 	t.Log("deactivated charger changes status to B")
 	clock.Add(5 * time.Minute)
 	vehicle.EXPECT().SoC().Return(95.0, nil)
 	charger.EXPECT().Status().Return(api.StatusB, nil)
 	charger.EXPECT().Enabled().Return(lp.enabled, nil)
-	lp.Update(-5000, false)
+	lp.Update(-5000, false, false)
 
 	t.Log("soc has fallen below target - soc update prevented by timer")
 	clock.Add(5 * time.Minute)
 	charger.EXPECT().Status().Return(api.StatusB, nil)
 	charger.EXPECT().Enabled().Return(lp.enabled, nil)
-	lp.Update(-5000, false)
+	lp.Update(-5000, false, false)
 
 	t.Log("soc has fallen below target - soc update timer expired")
 	clock.Add(pollInterval)
@@ -435,7 +435,7 @@ func TestDisableAndEnableAtTargetSoC(t *testing.T) {
 	charger.EXPECT().Status().Return(api.StatusB, nil)
 	charger.EXPECT().Enabled().Return(lp.enabled, nil)
 	charger.EXPECT().Enable(true).Return(nil)
-	lp.Update(-5000, false)
+	lp.Update(-5000, false, false)
 
 	ctrl.Finish()
 }
@@ -475,14 +475,14 @@ func TestSetModeAndSocAtDisconnect(t *testing.T) {
 	charger.EXPECT().Enabled().Return(lp.enabled, nil)
 	charger.EXPECT().Status().Return(api.StatusC, nil)
 	charger.EXPECT().MaxCurrent(int64(maxA)).Return(nil)
-	lp.Update(500, false)
+	lp.Update(500, false, false)
 
 	t.Log("switch off when disconnected")
 	clock.Add(5 * time.Minute)
 	charger.EXPECT().Enabled().Return(lp.enabled, nil)
 	charger.EXPECT().Status().Return(api.StatusA, nil)
 	charger.EXPECT().Enable(false).Return(nil)
-	lp.Update(-3000, false)
+	lp.Update(-3000, false, false)
 
 	if lp.Mode != api.ModeOff {
 		t.Error("unexpected mode", lp.Mode)
@@ -543,14 +543,14 @@ func TestChargedEnergyAtDisconnect(t *testing.T) {
 	rater.EXPECT().ChargedEnergy().Return(0.0, nil)
 	charger.EXPECT().Enabled().Return(lp.enabled, nil)
 	charger.EXPECT().Status().Return(api.StatusC, nil)
-	lp.Update(-1, false)
+	lp.Update(-1, false, false)
 
 	t.Log("at 1:00h charging at 5 kWh")
 	clock.Add(time.Hour)
 	rater.EXPECT().ChargedEnergy().Return(5.0, nil)
 	charger.EXPECT().Enabled().Return(lp.enabled, nil)
 	charger.EXPECT().Status().Return(api.StatusC, nil)
-	lp.Update(-1, false)
+	lp.Update(-1, false, false)
 	expectCache("chargedEnergy", 5000.0)
 
 	t.Log("at 1:00h stop charging at 5 kWh")
@@ -558,7 +558,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) {
 	rater.EXPECT().ChargedEnergy().Return(5.0, nil)
 	charger.EXPECT().Enabled().Return(lp.enabled, nil)
 	charger.EXPECT().Status().Return(api.StatusB, nil)
-	lp.Update(-1, false)
+	lp.Update(-1, false, false)
 	expectCache("chargedEnergy", 5000.0)
 
 	t.Log("at 1:00h restart charging at 5 kWh")
@@ -566,7 +566,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) {
 	rater.EXPECT().ChargedEnergy().Return(5.0, nil)
 	charger.EXPECT().Enabled().Return(lp.enabled, nil)
 	charger.EXPECT().Status().Return(api.StatusC, nil)
-	lp.Update(-1, false)
+	lp.Update(-1, false, false)
 	expectCache("chargedEnergy", 5000.0)
 
 	t.Log("at 1:30h continue charging at 7.5 kWh")
@@ -574,7 +574,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) {
 	rater.EXPECT().ChargedEnergy().Return(7.5, nil)
 	charger.EXPECT().Enabled().Return(lp.enabled, nil)
 	charger.EXPECT().Status().Return(api.StatusC, nil)
-	lp.Update(-1, false)
+	lp.Update(-1, false, false)
 	expectCache("chargedEnergy", 7500.0)
 
 	t.Log("at 2:00h stop charging at 10 kWh")
@@ -582,7 +582,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) {
 	rater.EXPECT().ChargedEnergy().Return(10.0, nil)
 	charger.EXPECT().Enabled().Return(lp.enabled, nil)
 	charger.EXPECT().Status().Return(api.StatusB, nil)
-	lp.Update(-1, false)
+	lp.Update(-1, false, false)
 	expectCache("chargedEnergy", 10000.0)
 
 	ctrl.Finish()