diff --git a/core/loadpoint.go b/core/loadpoint.go index eb5d58589..bdd1fa3f4 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -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) diff --git a/core/loadpoint_phases_test.go b/core/loadpoint_phases_test.go index 9e05d37d6..a4fdc96f5 100644 --- a/core/loadpoint_phases_test.go +++ b/core/loadpoint_phases_test.go @@ -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) } }