Add bufferSoc (#1469)

This commit is contained in:
andig 2021-09-19 21:59:40 +02:00 • committed by GitHub
parent 39c805eb92
commit 6c97801523
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 38 additions and 34 deletions

View file

@ -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

View file

@ -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()

View file

@ -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()
}
}

View file

@ -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:

View file

@ -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)
}