Improve PV mode stability (#112)
This commit is contained in:
parent
d07b53c06e
commit
33d45e314d
3 changed files with 216 additions and 7 deletions
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue