Skip to content

Commit

Permalink
Improve PV mode stability
Browse files Browse the repository at this point in the history
  • Loading branch information
andig committed May 11, 2020
1 parent f0303f4 commit 4450a2f
Show file tree
Hide file tree
Showing 3 changed files with 222 additions and 6 deletions.
79 changes: 73 additions & 6 deletions core/loadpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ type Config struct {
VehicleRef string `mapstructure:"vehicle"` // Vehicle reference

Meters MetersConfig // Meter references

Enable, Disable ThresholdConfig
}

// ThresholdConfig defines enable/disable hysteresis paramters
type ThresholdConfig struct {
Delay time.Duration
Threshold float64
}

// MetersConfig contains the loadpoint's meter configuration
Expand Down Expand Up @@ -86,6 +94,8 @@ type LoadPoint struct {
pvPower float64 // PV power
batteryPower float64 // Battery charge power
chargePower float64 // Charging power

pvTimer time.Time
}

// configProvider gives access to configuration repository
Expand Down Expand Up @@ -355,15 +365,69 @@ func (lp *LoadPoint) maxCurrent(mode api.ChargeMode) int64 {

// get max charge current
targetCurrent := clamp(powerToCurrent(targetPower, lp.Voltage, lp.Phases), 0, lp.MaxCurrent)
if targetCurrent < lp.MinCurrent {
switch mode {
case api.ModeMinPV:
targetCurrent = lp.MinCurrent
case api.ModePV:
targetCurrent = 0

if mode == api.ModeMinPV && targetCurrent < lp.MinCurrent {
return lp.MinCurrent
}

if mode == api.ModePV && lp.enabled && targetCurrent < lp.MinCurrent {
sitePower := lp.gridPower + lp.batteryPower
log.DEBUG.Printf("%s site power: %.0fW", lp.Name, sitePower)

// kick off disable sequence
if sitePower >= lp.Disable.Threshold {
log.DEBUG.Printf("%s site power %.0f >= disable threshold %.0f", lp.Name, sitePower, lp.Disable.Threshold)

if lp.pvTimer.IsZero() {
log.DEBUG.Printf("%s start disable timer", lp.Name)
lp.pvTimer = lp.clock.Now()
return lp.MinCurrent
}

if lp.clock.Since(lp.pvTimer) >= lp.Disable.Delay {
log.DEBUG.Printf("%s disable timer elapsed", lp.Name)
return 0
}
} else {
// reset timer
lp.pvTimer = lp.clock.Now()
}

return lp.MinCurrent
}

if mode == api.ModePV && !lp.enabled {
sitePower := lp.gridPower + lp.batteryPower
log.DEBUG.Printf("%s site power: %.0fW", lp.Name, sitePower)

if targetCurrent >= lp.MinCurrent ||
(lp.Enable.Threshold != 0 && sitePower <= lp.Enable.Threshold) {
log.DEBUG.Printf("%s site power %.0f < enable threshold %.0f", lp.Name, sitePower, lp.Enable.Threshold)

if lp.pvTimer.IsZero() {
log.DEBUG.Printf("%s start enable timer", lp.Name)

lp.pvTimer = lp.clock.Now()
return 0
}

if lp.clock.Since(lp.pvTimer) >= lp.Enable.Delay {
log.DEBUG.Printf("%s enable timer elapsed", lp.Name)
return lp.MinCurrent
}
} else {
// reset timer
lp.pvTimer = lp.clock.Now()
}

return 0
}

log.DEBUG.Printf("%s timer reset", lp.Name)

// reset pv timer
lp.pvTimer = time.Time{}

return targetCurrent
}

Expand Down Expand Up @@ -436,6 +500,9 @@ func (lp *LoadPoint) resetGuard() {

// update is the main control function. It reevaluates meters and charger state
func (lp *LoadPoint) update() {
log.DEBUG.Println(">", lp.Name)
defer log.DEBUG.Println("<", lp.Name)

if err := retry.Do(lp.updateChargeStatus, retry.Attempts(3)); err != nil {
log.ERROR.Printf("%s charger error: %v", lp.Name, err)
return
Expand Down
143 changes: 143 additions & 0 deletions core/loadpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,3 +341,146 @@ func TestConsumedPower(t *testing.T) {
}
}
}

func TestPVHysteresis(t *testing.T) {
dt := time.Minute
type se struct {
site float64
delay time.Duration // test case delay since start
current int64
}
tc := []struct {
enabled bool
enable, disable float64
series []se
}{
// keep disabled
{false, 0, 0, []se{
{0, 0, 0},
{0, 1, 0},
{0, dt - 1, 0},
{0, dt + 1, 0},
}},
// enable when threshold not configured but min power met
{false, 0, 0, []se{
{-6 * 100 * 10, 0, 0},
{-6 * 100 * 10, 1, 0},
{-6 * 100 * 10, dt - 1, 0},
{-6 * 100 * 10, dt + 1, lpMinCurrent},
}},
// keep disabled when threshold not configured
{false, 0, 0, []se{
{-400, 0, 0},
{-400, 1, 0},
{-400, dt - 1, 0},
{-400, dt + 1, 0},
}},
// keep disabled when threshold not met
{false, -500, 0, []se{
{-400, 0, 0},
{-400, 1, 0},
{-400, dt - 1, 0},
{-400, dt + 1, 0},
}},
// enable when threshold met
{false, -500, 0, []se{
{-500, 0, 0},
{-500, 1, 0},
{-500, dt - 1, 0},
{-500, dt + 1, lpMinCurrent},
}},
// keep enabled at max
{true, 500, 0, []se{
{-16 * 100 * 10, 0, lpMaxCurrent},
{-16 * 100 * 10, 1, lpMaxCurrent},
{-16 * 100 * 10, dt - 1, lpMaxCurrent},
{-16 * 100 * 10, dt + 1, lpMaxCurrent},
}},
// keep enabled at min
{true, 500, 0, []se{
{-6 * 100 * 10, 0, lpMinCurrent},
{-6 * 100 * 10, 1, lpMinCurrent},
{-6 * 100 * 10, dt - 1, lpMinCurrent},
{-6 * 100 * 10, dt + 1, lpMinCurrent},
}},
// keep enabled at min (negative threshold)
{true, 0, 500, []se{
{-500, 0, lpMinCurrent},
{-500, 1, lpMinCurrent},
{-500, dt - 1, lpMinCurrent},
{-500, dt + 1, lpMinCurrent},
}},
// disable when threshold met
{true, 0, 500, []se{
{500, 0, lpMinCurrent},
{500, 1, lpMinCurrent},
{500, dt - 1, lpMinCurrent},
{500, dt + 1, 0},
}},
// reset enable timer when threshold not met while timer active
{false, -500, 0, []se{
{-500, 0, 0},
{-500, 1, 0},
{-499, dt - 1, 0}, // should reset timer
{-500, dt + 1, 0}, // new begin of timer
{-500, 2*dt - 2, 0},
{-500, 2*dt - 1, lpMinCurrent},
}},
// reset enable timer when threshold not met while timer active and threshold not configured
{false, 0, 0, []se{
{-6*100*10 - 1, dt + 1, 0},
{-6 * 100 * 10, dt + 1, 0},
{-6 * 100 * 10, dt + 2, 0},
{-6 * 100 * 10, 2 * dt, 0},
{-6 * 100 * 10, 2*dt + 2, lpMinCurrent},
}},
// reset disable timer when threshold not met while timer active
{true, 0, 500, []se{
{500, 0, lpMinCurrent},
{500, 1, lpMinCurrent},
{499, dt - 1, lpMinCurrent}, // reset timer
{500, dt + 1, lpMinCurrent}, // within reset timer duration
{500, 2*dt - 2, lpMinCurrent}, // still within reset timer duration
{500, 2*dt - 1, 0}, // reset timer elapsed
}},
}

for _, tc := range tc {
t.Log(tc)

clck := clock.NewMock()
lp := LoadPoint{
clock: clck,
ChargerHandler: ChargerHandler{
MinCurrent: lpMinCurrent,
MaxCurrent: lpMaxCurrent,
enabled: tc.enabled,
},
Config: Config{
Voltage: 100,
Phases: 10,
Enable: ThresholdConfig{
Threshold: tc.enable,
Delay: dt,
},
Disable: ThresholdConfig{
Threshold: tc.disable,
Delay: dt,
},
},
gridPower: 0,
}

start := clck.Now()

for step, se := range tc.series {
clck.Set(start.Add(se.delay))
lp.gridPower = se.site
current := lp.maxCurrent(api.ModePV)

if current != se.current {
t.Errorf("step %d: wanted %d, got %d", step, se.current, current)
}
}
}
}
6 changes: 6 additions & 0 deletions evcc.dist.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,12 @@ loadpoints:
pv: pv # pv meter
battery: battery # battery meter
charge: charge # charge meter
enable: # pv mode enable behavior
delay: 1m # threshold must be exceeded for this long
threshold: 0 # minimum export power (W). If zero, export must exceeds minimum charge power to enable
disable: # pv mode disable behavior
delay: 5m # threshold must be exceeded for this long
threshold: 200 # maximum import power (W)
guardduration: 10m # switch charger contactor not more often than this (default 10m)
maxcurrent: 16 # maximum charge current (default 16A)
phases: 3 # ev phases (default 3)
Expand Down

0 comments on commit 4450a2f

Please sign in to comment.