Improve PV mode stability (#112)

This commit is contained in:
andig 2020-05-12 18:31:41 +02:00 • committed by GitHub
parent d07b53c06e
commit 33d45e314d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 216 additions and 7 deletions

View file

@ -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
@ -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
@ -355,15 +365,65 @@ 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
// 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()
}
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
// kick off enable sequence
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()
}
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
}

View file

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

View file

@ -211,6 +211,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)