Fix limiting PV current when scaling phases up (#13295)

This commit is contained in:
andig 2024-04-04 21:02:49 +02:00 • committed by GitHub
parent cc24f44a61
commit f78913eae5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 52 additions and 55 deletions

View file

@ -1062,8 +1062,8 @@ func (lp *Loadpoint) fastCharging() error {
return err
}
// pvScalePhases switches phases if necessary and returns if switch occurred
func (lp *Loadpoint) pvScalePhases(sitePower, minCurrent, maxCurrent float64) bool {
// pvScalePhases switches phases if necessary and returns number of phases switched to
func (lp *Loadpoint) pvScalePhases(sitePower, minCurrent, maxCurrent float64) int {
phases := lp.GetPhases()
// observed phase state inconsistency
@ -1102,7 +1102,7 @@ func (lp *Loadpoint) pvScalePhases(sitePower, minCurrent, maxCurrent float64) bo
if err := lp.scalePhases(1); err != nil {
lp.log.ERROR.Println(err)
}
return true
return 1
}
waiting = true
@ -1131,7 +1131,10 @@ func (lp *Loadpoint) pvScalePhases(sitePower, minCurrent, maxCurrent float64) bo
if err := lp.scalePhases(3); err != nil {
lp.log.ERROR.Println(err)
}
return true
if err := lp.scalePhases(3); err != nil {
lp.log.ERROR.Println(err)
}
return 3
}
waiting = true
@ -1142,7 +1145,7 @@ func (lp *Loadpoint) pvScalePhases(sitePower, minCurrent, maxCurrent float64) bo
lp.resetPhaseTimer()
}
return false
return 0
}
// TODO move up to timer functions
@ -1174,13 +1177,18 @@ func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64, batter
maxCurrent := lp.effectiveMaxCurrent()
// switch phases up/down
var scaledTo int
if lp.hasPhaseSwitching() && lp.phaseSwitchCompleted() {
_ = lp.pvScalePhases(sitePower, minCurrent, maxCurrent)
scaledTo = lp.pvScalePhases(sitePower, minCurrent, maxCurrent)
}
// calculate target charge current from delta power and actual current
effectiveCurrent := lp.effectiveCurrent()
activePhases := lp.ActivePhases()
effectiveCurrent := lp.effectiveCurrent()
if scaledTo == 3 {
// if we did scale, adjust the effective current to the new phase count
effectiveCurrent /= 3.0
}
deltaCurrent := powerToCurrent(-sitePower, activePhases)
targetCurrent := max(effectiveCurrent+deltaCurrent, 0)

View file

@ -9,6 +9,7 @@ import (
"github.com/benbjohnson/clock"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
@ -115,9 +116,7 @@ func TestMaxActivePhases(t *testing.T) {
expectedPhases = dflt
}
if phs := lp.maxActivePhases(); phs != expectedPhases {
t.Errorf("expected max %d, got %d", expectedPhases, phs)
}
require.Equal(t, expectedPhases, lp.maxActivePhases(), "expected max active phases")
}
}
}
@ -136,7 +135,7 @@ func testScale(t *testing.T, lp *Loadpoint, sitePower float64, direction string,
// scale-up should only execute when the 1p max current is exceeded
// we're testing this here and remove the upscale expectation for the following test below 1p max current
if maxAmp := -sitePower / Voltage; maxAmp < maxA {
if scaled := lp.pvScalePhases(sitePower, minA, maxAmp-0.0001); !scaled {
if scaled := lp.pvScalePhases(sitePower, minA, maxAmp-0.0001); scaled != 3 {
t.Errorf("%v act=%d max=%d missing scale %s at reduced max current %.1fA", tc, act, max, direction, maxAmp)
}
@ -148,10 +147,10 @@ func testScale(t *testing.T, lp *Loadpoint, sitePower float64, direction string,
scaled := lp.pvScalePhases(sitePower, minA, maxA)
if strings.Contains(testExpectation, testDirection) {
if !scaled {
if scaled == 0 {
t.Errorf("%v act=%d max=%d missing scale %s", tc, act, max, direction)
}
} else if scaled {
} else if scaled != 0 {
t.Errorf("%v act=%d max=%d unexpected scale %s", tc, act, max, direction)
}
}
@ -217,16 +216,10 @@ func TestPvScalePhases(t *testing.T) {
t.Fatalf("%v invalid test case", tc)
}
if lp.phases != tc.physical {
t.Error("wrong phases", lp.phases, tc.physical)
}
require.Equal(t, tc.physical, lp.phases, "wrong phases")
require.Equal(t, tc.actExpected, lp.ActivePhases(), "expected active phases")
require.Equal(t, tc.maxExpected, lp.maxActivePhases(), "expected max active phases")
if phs := lp.ActivePhases(); phs != tc.actExpected {
t.Errorf("expected active %d, got %d", tc.actExpected, phs)
}
if phs := lp.maxActivePhases(); phs != tc.maxExpected {
t.Errorf("expected max %d, got %d", tc.maxExpected, phs)
}
ctrl.Finish()
// scaling
@ -276,83 +269,83 @@ func TestPvScalePhasesTimer(t *testing.T) {
phases, measuredPhases int
sitePower float64
toPhases int
res bool
res int
prepare func(lp *Loadpoint)
}{
// switch up from 1p/1p configured/active
{"1/1->3, not enough power", 1, 1, 0, 1, false, nil},
{"1/1->3, kickoff", 1, 1, -3 * Voltage * minA, 1, false, func(lp *Loadpoint) {
{"1/1->3, not enough power", 1, 1, 0, 1, 0, nil},
{"1/1->3, kickoff", 1, 1, -3 * Voltage * minA, 1, 0, func(lp *Loadpoint) {
lp.phaseTimer = time.Time{}
}},
{"1/1->3, timer running", 1, 1, -3 * Voltage * minA, 1, false, func(lp *Loadpoint) {
{"1/1->3, timer running", 1, 1, -3 * Voltage * minA, 1, 0, func(lp *Loadpoint) {
lp.phaseTimer = lp.clock.Now()
}},
{"1/1->3, timer elapsed", 1, 1, -3 * Voltage * minA, 3, true, func(lp *Loadpoint) {
{"1/1->3, timer elapsed", 1, 1, -3 * Voltage * minA, 3, 3, func(lp *Loadpoint) {
lp.phaseTimer = elapsed
}},
// omit to switch up (again) from 3p/1p configured/active
{"3/1->3, not enough power", 3, 1, 0, 3, false, nil},
{"3/1->3, kickoff", 3, 1, -3 * Voltage * minA, 3, false, func(lp *Loadpoint) {
// omit to switch up (again) from 3p/1p configured/a0ctive
{"3/1->3, not enough power", 3, 1, 0, 3, 0, nil},
{"3/1->3, kickoff", 3, 1, -3 * Voltage * minA, 3, 0, func(lp *Loadpoint) {
lp.phaseTimer = time.Time{}
}},
{"3/1->3, timer running", 3, 1, -3 * Voltage * minA, 3, false, func(lp *Loadpoint) {
{"3/1->3, timer running", 3, 1, -3 * Voltage * minA, 3, 0, func(lp *Loadpoint) {
lp.phaseTimer = lp.clock.Now()
}},
{"3/1->3, timer elapsed", 3, 1, -3 * Voltage * minA, 3, false, func(lp *Loadpoint) {
{"3/1->3, timer elapsed", 3, 1, -3 * Voltage * minA, 3, 0, func(lp *Loadpoint) {
lp.phaseTimer = elapsed
}},
// omit to switch down from 3p/1p configured/active
{"3/1->1, not enough power", 3, 1, 0, 3, false, nil},
{"3/1->1, kickoff", 3, 1, -1 * Voltage * minA, 3, false, func(lp *Loadpoint) {
{"3/1->1, not enough power", 3, 1, 0, 3, 0, nil},
{"3/1->1, kickoff", 3, 1, -1 * Voltage * minA, 3, 0, func(lp *Loadpoint) {
lp.phaseTimer = time.Time{}
}},
{"3/1->1, timer running", 3, 1, -1 * Voltage * minA, 3, false, func(lp *Loadpoint) {
{"3/1->1, timer running", 3, 1, -1 * Voltage * minA, 3, 0, func(lp *Loadpoint) {
lp.phaseTimer = lp.clock.Now()
}},
{"3/1->1, timer elapsed", 3, 1, -1 * Voltage * minA, 3, false, func(lp *Loadpoint) {
{"3/1->1, timer elapsed", 3, 1, -1 * Voltage * minA, 3, 0, func(lp *Loadpoint) {
lp.phaseTimer = elapsed
}},
// switch down from 3p/3p configured/active
{"3/3->1, enough power", 3, 3, 0, 3, false, nil},
{"3/3->1, enough power, timer elapsed, load point enabled", 3, 3, 0, 3, false, func(lp *Loadpoint) {
{"3/3->1, enough power", 3, 3, 0, 3, 0, nil},
{"3/3->1, enough power, timer elapsed, load point enabled", 3, 3, 0, 3, 0, func(lp *Loadpoint) {
lp.phaseTimer = elapsed
lp.enabled = true
}},
{"3/3->1, enough power, timer elapsed, load point disabled", 3, 3, 0, 1, true, func(lp *Loadpoint) {
{"3/3->1, enough power, timer elapsed, load point disabled", 3, 3, 0, 1, 1, func(lp *Loadpoint) {
lp.phaseTimer = elapsed
lp.enabled = false
}},
{"3/3->1, kickoff", 3, 3, 0.1, 3, false, func(lp *Loadpoint) {
{"3/3->1, kickoff", 3, 3, 0.1, 3, 0, func(lp *Loadpoint) {
lp.phaseTimer = time.Time{}
}},
{"3/3->1, timer running", 3, 3, 0.1, 3, false, func(lp *Loadpoint) {
{"3/3->1, timer running", 3, 3, 0.1, 3, 0, func(lp *Loadpoint) {
lp.phaseTimer = lp.clock.Now()
}},
{"3/3->1, timer elapsed", 3, 3, 0.1, 1, true, func(lp *Loadpoint) {
{"3/3->1, timer elapsed", 3, 3, 0.1, 1, 1, func(lp *Loadpoint) {
lp.phaseTimer = elapsed
}},
// switch down from 3p/0p while not yet charging
{"3/0->1, not enough power, not charging", 3, 0, 0, 1, true, func(lp *Loadpoint) {
{"3/0->1, not enough power, not charging", 3, 0, 0, 1, 1, func(lp *Loadpoint) {
lp.status = api.StatusB
}},
// switch up from 1p/0p while not yet charging
{"1/0->3, enough power, not charging", 1, 0, -3 * Voltage * minA, 3, true, func(lp *Loadpoint) {
{"1/0->3, enough power, not charging", 1, 0, -3 * Voltage * minA, 3, 3, func(lp *Loadpoint) {
lp.status = api.StatusB
}},
// error states from 1p/3p misconfiguration - no correction for time being (stay at 1p)
{"1/3->1, enough power", 1, 3, -1 * Voltage * maxA, 1, false, nil},
{"1/3->1, kickoff, correct phase setting", 1, 3, 0.1, 1, false, func(lp *Loadpoint) {
{"1/3->1, enough power", 1, 3, -1 * Voltage * maxA, 1, 0, nil},
{"1/3->1, kickoff, correct phase setting", 1, 3, 0.1, 1, 0, func(lp *Loadpoint) {
lp.phaseTimer = time.Time{}
}},
{"1/3->1, timer running, correct phase setting", 1, 3, 0.1, 1, false, func(lp *Loadpoint) {
{"1/3->1, timer running, correct phase setting", 1, 3, 0.1, 1, 0, func(lp *Loadpoint) {
lp.phaseTimer = lp.clock.Now()
}},
{"1/3->1, switch not executed", 1, 3, 0.1, 1, false, func(lp *Loadpoint) {
{"1/3->1, switch not executed", 1, 3, 0.1, 1, 0, func(lp *Loadpoint) {
lp.phaseTimer = elapsed
}},
}
@ -383,18 +376,14 @@ func TestPvScalePhasesTimer(t *testing.T) {
tc.prepare(lp)
}
if tc.res {
if tc.res != 0 {
charger.MockPhaseSwitcher.EXPECT().Phases1p3p(tc.toPhases).Return(nil)
}
res := lp.pvScalePhases(tc.sitePower, minA, maxA)
switch {
case tc.res != res:
t.Errorf("expected %v, got %v", tc.res, res)
case lp.phases != tc.toPhases:
t.Errorf("expected %dp, got %dp", tc.toPhases, lp.phases)
}
require.Equal(t, tc.res, res, tc.desc)
require.Equal(t, tc.toPhases, lp.phases, tc.desc)
}
}