Fix reducing to 1p when vehicle doesn't utilize allowed current (foll… (#9581)

This commit is contained in:
andig 2023-10-08 14:42:20 +02:00 • committed by GitHub
parent 4e6253caf8
commit c369817dbf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 86 additions and 79 deletions

View file

@ -1025,7 +1025,7 @@ func (lp *Loadpoint) fastCharging() error {
}
// pvScalePhases switches phases if necessary and returns if switch occurred
func (lp *Loadpoint) pvScalePhases(availablePower, minCurrent, maxCurrent float64) bool {
func (lp *Loadpoint) pvScalePhases(sitePower, minCurrent, maxCurrent float64) bool {
phases := lp.GetPhases()
// observed phase state inconsistency
@ -1042,9 +1042,11 @@ func (lp *Loadpoint) pvScalePhases(availablePower, minCurrent, maxCurrent float6
var waiting bool
activePhases := lp.activePhases()
availablePower := lp.chargePower - sitePower
scalable := (sitePower > 0 || !lp.enabled) && activePhases > 1 && lp.ConfiguredPhases < 3
// scale down phases
if targetCurrent := powerToCurrent(availablePower, activePhases); targetCurrent < minCurrent && activePhases > 1 && lp.ConfiguredPhases < 3 {
if targetCurrent := powerToCurrent(availablePower, activePhases); targetCurrent < minCurrent && scalable {
lp.log.DEBUG.Printf("available power %.0fW < %.0fW min %dp threshold", availablePower, float64(activePhases)*Voltage*minCurrent, activePhases)
if !lp.charging() { // scale immediately if not charging
@ -1070,7 +1072,7 @@ func (lp *Loadpoint) pvScalePhases(availablePower, minCurrent, maxCurrent float6
maxPhases := lp.maxActivePhases()
target1pCurrent := powerToCurrent(availablePower, 1)
scalable := maxPhases > 1 && phases < maxPhases && target1pCurrent > maxCurrent
scalable = maxPhases > 1 && phases < maxPhases && target1pCurrent > maxCurrent
// scale up phases
if targetCurrent := powerToCurrent(availablePower, maxPhases); targetCurrent >= minCurrent && scalable {
@ -1138,8 +1140,7 @@ func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64, batter
// switch phases up/down
if _, ok := lp.charger.(api.PhaseSwitcher); ok {
availablePower := -sitePower + lp.chargePower
_ = lp.pvScalePhases(availablePower, minCurrent, maxCurrent)
_ = lp.pvScalePhases(sitePower, minCurrent, maxCurrent)
}
// calculate target charge current from delta power and actual current

View file

@ -22,48 +22,46 @@ type testCase struct {
scale string
}
var (
phaseTests = []testCase{
// 1p
{1, 1, 0, 0, 1, 1, ""},
{1, 1, 0, 1, 1, 1, ""},
{1, 1, 1, 0, 1, 1, ""},
{1, 1, 2, 0, 1, 1, ""},
{1, 1, 3, 0, 1, 1, ""},
// 3p
{3, 3, 0, 0, unknownPhases, 3, ""},
{3, 3, 0, 1, 1, 1, ""},
{3, 3, 0, 2, 2, 2, ""},
{3, 3, 0, 3, 3, 3, ""},
{3, 3, 1, 0, 1, 1, ""},
{3, 3, 2, 0, 2, 2, ""},
{3, 3, 3, 0, 3, 3, ""},
// 1p3p initial
{0, 0, 0, 0, unknownPhases, 3, "du"},
{0, 0, 0, 1, 1, 3, "u"},
{0, 0, 0, 2, 2, 3, "du"},
{0, 0, 0, 3, 3, 3, "du"},
{0, 0, 1, 0, 1, 1, ""},
{0, 0, 2, 0, 2, 2, "du"},
{0, 0, 3, 0, 3, 3, "du"},
// 1p3p, 1 currently active
{0, 1, 0, 0, 1, 3, "u"},
{0, 1, 0, 1, 1, 3, "u"},
// {0, 1, 0, 2, 2,2,"u"}, // 2p active > 1p configured must not happen
// {0, 1, 0, 3, 3,3,"u"}, // 3p active > 1p configured must not happen
{0, 1, 1, 0, 1, 1, ""},
{0, 1, 2, 0, 1, 2, "u"},
{0, 1, 3, 0, 1, 3, "u"},
// 1p3p, 3 currently active
{0, 3, 0, 0, unknownPhases, 3, "d"},
{0, 3, 0, 1, 1, 1, ""},
{0, 3, 0, 2, 2, 2, "d"},
{0, 3, 0, 3, 3, 3, "d"},
{0, 3, 1, 0, 1, 1, ""},
{0, 3, 2, 0, 2, 2, "d"},
{0, 3, 3, 0, 3, 3, "d"},
}
)
var phaseTests = []testCase{
// 1p
{1, 1, 0, 0, 1, 1, ""},
{1, 1, 0, 1, 1, 1, ""},
{1, 1, 1, 0, 1, 1, ""},
{1, 1, 2, 0, 1, 1, ""},
{1, 1, 3, 0, 1, 1, ""},
// 3p
{3, 3, 0, 0, unknownPhases, 3, ""},
{3, 3, 0, 1, 1, 1, ""},
{3, 3, 0, 2, 2, 2, ""},
{3, 3, 0, 3, 3, 3, ""},
{3, 3, 1, 0, 1, 1, ""},
{3, 3, 2, 0, 2, 2, ""},
{3, 3, 3, 0, 3, 3, ""},
// 1p3p initial
{0, 0, 0, 0, unknownPhases, 3, "du"},
{0, 0, 0, 1, 1, 3, "u"},
{0, 0, 0, 2, 2, 3, "du"},
{0, 0, 0, 3, 3, 3, "du"},
{0, 0, 1, 0, 1, 1, ""},
{0, 0, 2, 0, 2, 2, "du"},
{0, 0, 3, 0, 3, 3, "du"},
// 1p3p, 1 currently active
{0, 1, 0, 0, 1, 3, "u"},
{0, 1, 0, 1, 1, 3, "u"},
// {0, 1, 0, 2, 2,2,"u"}, // 2p active > 1p configured must not happen
// {0, 1, 0, 3, 3,3,"u"}, // 3p active > 1p configured must not happen
{0, 1, 1, 0, 1, 1, ""},
{0, 1, 2, 0, 1, 2, "u"},
{0, 1, 3, 0, 1, 3, "u"},
// 1p3p, 3 currently active
{0, 3, 0, 0, unknownPhases, 3, "d"},
{0, 3, 0, 1, 1, 1, ""},
{0, 3, 0, 2, 2, 2, "d"},
{0, 3, 0, 3, 3, 3, "d"},
{0, 3, 1, 0, 1, 1, ""},
{0, 3, 2, 0, 2, 2, "d"},
{0, 3, 3, 0, 3, 3, "d"},
}
func TestMaxActivePhases(t *testing.T) {
ctrl := gomock.NewController(t)
@ -125,7 +123,7 @@ func TestMaxActivePhases(t *testing.T) {
}
}
func testScale(t *testing.T, lp *Loadpoint, power float64, direction string, tc testCase) {
func testScale(t *testing.T, lp *Loadpoint, sitePower float64, direction string, tc testCase) {
act := lp.activePhases()
max := lp.maxActivePhases()
@ -136,8 +134,8 @@ func testScale(t *testing.T, lp *Loadpoint, power float64, direction string, tc
if testDirection == "u" && strings.Contains(testExpectation, testDirection) {
// 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 := power / Voltage; maxAmp < maxA {
if scaled := lp.pvScalePhases(power, minA, maxAmp-0.0001); !scaled {
if maxAmp := -sitePower / Voltage; maxAmp < maxA {
if scaled := lp.pvScalePhases(sitePower, minA, maxAmp-0.0001); !scaled {
t.Errorf("%v act=%d max=%d missing scale %s at reduced max current %.1fA", tc, act, max, direction, maxAmp)
}
@ -146,7 +144,7 @@ func testScale(t *testing.T, lp *Loadpoint, power float64, direction string, tc
}
}
scaled := lp.pvScalePhases(power, minA, maxA)
scaled := lp.pvScalePhases(sitePower, minA, maxA)
if strings.Contains(testExpectation, testDirection) {
if !scaled {
@ -232,7 +230,7 @@ func TestPvScalePhases(t *testing.T) {
// scaling
if phaseCharger != nil {
// scale down
min1p := 1 * minA * Voltage
min1p := 0.1
lp.phaseTimer = time.Time{}
plainCharger.EXPECT().Enable(false).Return(nil).MaxTimes(1)
@ -252,7 +250,7 @@ func TestPvScalePhases(t *testing.T) {
plainCharger.EXPECT().Enable(false).Return(nil).MaxTimes(1)
phaseCharger.EXPECT().Phases1p3p(3).Return(nil).MaxTimes(1)
testScale(t, lp, min3p, "up", tc)
testScale(t, lp, -min3p, "up", tc)
ctrl.Finish()
}
}
@ -274,57 +272,65 @@ func TestPvScalePhasesTimer(t *testing.T) {
tc := []struct {
desc string
phases, measuredPhases int
availablePower float64
sitePower float64
toPhases int
res bool
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, kickoff", 1, 1, -3 * Voltage * minA, 1, false, 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, false, func(lp *Loadpoint) {
lp.phaseTimer = lp.clock.Now()
}},
{"1/1->3, timer elapsed", 1, 1, 3 * Voltage * minA, 3, true, func(lp *Loadpoint) {
lp.phaseTimer = lp.clock.Now().Add(-dt)
{"1/1->3, timer elapsed", 1, 1, -3 * Voltage * minA, 3, true, 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) {
{"3/1->3, kickoff", 3, 1, -3 * Voltage * minA, 3, false, 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, false, func(lp *Loadpoint) {
lp.phaseTimer = lp.clock.Now()
}},
{"3/1->3, timer elapsed", 3, 1, 3 * Voltage * minA, 3, false, func(lp *Loadpoint) {
lp.phaseTimer = lp.clock.Now().Add(-dt)
{"3/1->3, timer elapsed", 3, 1, -3 * Voltage * minA, 3, false, 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, kickoff", 3, 1, -1 * Voltage * minA, 3, false, 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, false, func(lp *Loadpoint) {
lp.phaseTimer = lp.clock.Now()
}},
{"3/1->1, timer elapsed", 3, 1, 1 * Voltage * minA, 3, false, func(lp *Loadpoint) {
lp.phaseTimer = lp.clock.Now().Add(-dt)
{"3/1->1, timer elapsed", 3, 1, -1 * Voltage * minA, 3, false, func(lp *Loadpoint) {
lp.phaseTimer = elapsed
}},
// switch down from 3p/3p configured/active
{"3/3->1, enough power", 3, 3, 1 * Voltage * maxA, 3, false, nil},
{"3/3->1, kickoff", 3, 3, 1 * Voltage * maxA, 3, false, func(lp *Loadpoint) {
{"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) {
lp.phaseTimer = elapsed
lp.enabled = true
}},
{"3/3->1, enough power, timer elapsed, load point disabled", 3, 3, 0, 1, true, func(lp *Loadpoint) {
lp.phaseTimer = elapsed
lp.enabled = false
}},
{"3/3->1, kickoff", 3, 3, 0.1, 3, false, func(lp *Loadpoint) {
lp.phaseTimer = time.Time{}
}},
{"3/3->1, timer running", 3, 3, 1 * Voltage * maxA, 3, false, func(lp *Loadpoint) {
{"3/3->1, timer running", 3, 3, 0.1, 3, false, func(lp *Loadpoint) {
lp.phaseTimer = lp.clock.Now()
}},
{"3/3->1, timer elapsed", 3, 3, 1 * Voltage * maxA, 1, true, func(lp *Loadpoint) {
lp.phaseTimer = lp.clock.Now().Add(-dt)
{"3/3->1, timer elapsed", 3, 3, 0.1, 1, true, func(lp *Loadpoint) {
lp.phaseTimer = elapsed
}},
// switch down from 3p/0p while not yet charging
@ -332,20 +338,20 @@ func TestPvScalePhasesTimer(t *testing.T) {
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, true, func(lp *Loadpoint) {
lp.status = api.StatusB
}},
// error states from 1p/3p misconfig - 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, 1 * Voltage * maxA, 1, false, func(lp *Loadpoint) {
// 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) {
lp.phaseTimer = time.Time{}
}},
{"1/3->1, timer running, correct phase setting", 1, 3, 1 * Voltage * maxA, 1, false, func(lp *Loadpoint) {
{"1/3->1, timer running, correct phase setting", 1, 3, 0.1, 1, false, func(lp *Loadpoint) {
lp.phaseTimer = lp.clock.Now()
}},
{"1/3->1, switch not executed", 1, 3, 1 * Voltage * maxA, 1, false, func(lp *Loadpoint) {
lp.phaseTimer = lp.clock.Now().Add(-dt)
{"1/3->1, switch not executed", 1, 3, 0.1, 1, false, func(lp *Loadpoint) {
lp.phaseTimer = elapsed
}},
}
@ -379,7 +385,7 @@ func TestPvScalePhasesTimer(t *testing.T) {
charger.MockPhaseSwitcher.EXPECT().Phases1p3p(tc.toPhases).Return(nil)
}
res := lp.pvScalePhases(tc.availablePower, minA, maxA)
res := lp.pvScalePhases(tc.sitePower, minA, maxA)
switch {
case tc.res != res: