Skip to content

Commit 8d425d3

Browse files
authored
Phases: allow 1p scale-down when the loadpoint never disables (#33214)
1 parent 10b0c99 commit 8d425d3

2 files changed

Lines changed: 23 additions & 10 deletions

File tree

core/loadpoint.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1438,8 +1438,9 @@ func (lp *Loadpoint) minCharging() error {
14381438
return lp.setLimit(lp.effectiveMinCurrent())
14391439
}
14401440

1441-
// pvScalePhases switches phases if necessary and returns number of phases switched to
1442-
func (lp *Loadpoint) pvScalePhases(sitePower, minCurrent, maxCurrent float64) int {
1441+
// pvScalePhases switches phases if necessary and returns number of phases switched to.
1442+
// mayDisable indicates that insufficient surplus can stop charging via the pv disable timer.
1443+
func (lp *Loadpoint) pvScalePhases(sitePower, minCurrent, maxCurrent float64, mayDisable bool) int {
14431444
phases := lp.GetPhases()
14441445

14451446
// observed phase state inconsistency
@@ -1466,8 +1467,9 @@ func (lp *Loadpoint) pvScalePhases(sitePower, minCurrent, maxCurrent float64) in
14661467
}
14671468

14681469
// while charging, scaling down only helps if 1p is sustainable, otherwise it
1469-
// merely delays the pv disable timer by the phase timer duration
1470-
useful := !lp.enabled || !lp.charging() || powerToCurrent(availablePower, 1) >= minCurrent
1470+
// merely delays the pv disable timer by the phase timer duration. Without a
1471+
// disable to wait for, scaling down is the only way to reduce power (#33208).
1472+
useful := !lp.enabled || !lp.charging() || !mayDisable || powerToCurrent(availablePower, 1) >= minCurrent
14711473
if insufficient && !useful {
14721474
lp.log.DEBUG.Printf("available power %.0fW < %.0fW min 1p threshold, disabling instead of scaling down", availablePower, Voltage*minCurrent)
14731475
}
@@ -1638,10 +1640,14 @@ func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower, batteryPower f
16381640
// push demand to drain battery
16391641
sitePower -= lp.boostPower(batteryPower)
16401642

1643+
// minpv and the battery conditions hold charging at min current, no disable can follow
1644+
battery := batteryStart || batteryBuffered && lp.charging() || lp.GetBatteryBoost() == boostContinue
1645+
mayDisable := mode == api.ModePV && !battery
1646+
16411647
// switch phases up/down
16421648
var scaledTo int
16431649
if lp.hasPhaseSwitching() && lp.phaseSwitchCompleted() {
1644-
scaledTo = lp.pvScalePhases(sitePower, minCurrent, maxCurrent)
1650+
scaledTo = lp.pvScalePhases(sitePower, minCurrent, maxCurrent, mayDisable)
16451651
}
16461652

16471653
// calculate target charge current from delta power and actual current
@@ -1659,7 +1665,7 @@ func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower, batteryPower f
16591665
targetCurrent := max(effectiveCurrent+deltaCurrent, 0)
16601666

16611667
// in MinPV mode or under special conditions return at least minCurrent
1662-
if battery := batteryStart || batteryBuffered && lp.charging() || lp.GetBatteryBoost() == boostContinue; (mode == api.ModeMinPV || battery) && targetCurrent < minCurrent {
1668+
if (mode == api.ModeMinPV || battery) && targetCurrent < minCurrent {
16631669
lp.log.DEBUG.Printf("pv charge current: min %.3gA > %.3gA (%.0fW @ %dp, battery: %t)", minCurrent, targetCurrent, sitePower, activePhases, battery)
16641670
return minCurrent
16651671
}

core/loadpoint_phases_test.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ func testScale(t *testing.T, lp *Loadpoint, sitePower float64, direction string,
178178
// scale-up should only execute when the 1p max current is exceeded
179179
// we're testing this here and remove the upscale expectation for the following test below 1p max current
180180
if maxAmp := -sitePower / Voltage; maxAmp < maxA {
181-
if scaled := lp.pvScalePhases(sitePower, minA, maxAmp-0.0001); scaled != 3 {
181+
if scaled := lp.pvScalePhases(sitePower, minA, maxAmp-0.0001, true); scaled != 3 {
182182
t.Errorf("%v act=%d max=%d missing scale %s at reduced max current %.1fA", tc, act, max, direction, maxAmp)
183183
}
184184

@@ -187,7 +187,7 @@ func testScale(t *testing.T, lp *Loadpoint, sitePower float64, direction string,
187187
}
188188
}
189189

190-
scaled := lp.pvScalePhases(sitePower, minA, maxA)
190+
scaled := lp.pvScalePhases(sitePower, minA, maxA, true)
191191

192192
if strings.Contains(testExpectation, testDirection) {
193193
if scaled == 0 {
@@ -384,6 +384,13 @@ func TestPvScalePhasesTimer(t *testing.T) {
384384
lp.chargePower = 3 * Voltage * minA
385385
}},
386386

387+
// minpv never disables, so scale down even if 1p is not sustainable (#33208)
388+
{"3/3->1, insufficient for 1p, charging, minpv", 3, 3, 0.1, 1, 1, func(lp *Loadpoint) {
389+
lp.phaseTimer = elapsed
390+
lp.enabled = true
391+
lp.mode = api.ModeMinPV
392+
}},
393+
387394
// switch down from 3p/0p while not yet charging
388395
{"3/0->1, not enough power, not charging", 3, 0, 0, 1, 1, func(lp *Loadpoint) {
389396
lp.status = api.StatusB
@@ -437,7 +444,7 @@ func TestPvScalePhasesTimer(t *testing.T) {
437444
charger.MockPhaseSwitcher.EXPECT().Phases1p3p(tc.toPhases).Return(nil)
438445
}
439446

440-
res := lp.pvScalePhases(tc.sitePower, minA, maxA)
447+
res := lp.pvScalePhases(tc.sitePower, minA, maxA, lp.mode != api.ModeMinPV)
441448

442449
require.Equal(t, tc.res, res, tc.desc)
443450
require.Equal(t, tc.toPhases, lp.phases, tc.desc)
@@ -785,7 +792,7 @@ func TestPvScalePhasesCircuitLimits(t *testing.T) {
785792
}{plainCharger, phaseCharger},
786793
}
787794

788-
require.Equal(t, tc.expectedPhases, lp.pvScalePhases(tc.sitePower, lp.minCurrent, lp.maxCurrent))
795+
require.Equal(t, tc.expectedPhases, lp.pvScalePhases(tc.sitePower, lp.minCurrent, lp.maxCurrent, true))
789796

790797
ctrl.Finish()
791798
})

0 commit comments

Comments
 (0)