diff --git a/core/loadpoint.go b/core/loadpoint.go index 20be0a02d..5b8bebff3 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -1590,6 +1590,16 @@ func (lp *Loadpoint) boostPower(batteryPower float64) float64 { delta += lp.EffectiveStepPower() } + // bridge the power gap between 1p max and 3p min so pvScalePhases can trigger a scale-up + if lp.hasPhaseSwitching() && lp.phaseSwitchCompleted() && lp.site.GetBatteryMaxDischargePower() != nil { + if activePhases, maxPhases := lp.ActivePhases(), lp.MaxActivePhases(); activePhases < maxPhases && + lp.circuitAllowsPhases(maxPhases, lp.effectiveMinCurrent()) { + // max power actually achievable on the active phases + activeMaxPower := min(lp.EffectiveMaxPower(), Voltage*lp.effectiveMaxCurrent()*float64(activePhases)) + delta += max(0, lp.EffectiveMinPower()*float64(maxPhases)-activeMaxPower) + } + } + // start boosting by setting maximum power if boost == boostStart { delta = lp.EffectiveMaxPower() diff --git a/core/loadpoint_boost_test.go b/core/loadpoint_boost_test.go index df0bad18a..c05d917dc 100644 --- a/core/loadpoint_boost_test.go +++ b/core/loadpoint_boost_test.go @@ -2,11 +2,13 @@ package core import ( "testing" + "time" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/core/site" "github.com/evcc-io/evcc/util" "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" ) type mockSite struct { @@ -104,3 +106,121 @@ func TestBoostPower(t *testing.T) { // res = max(0, -2000) + 790 + 0 = 790W assert.Equal(t, 790.0, res) } + +type plainCharger struct { + api.Charger +} + +type phaseSwitchCharger struct { + api.Charger +} + +func (phaseSwitchCharger) Phases1p3p(int) error { return nil } + +func TestBoostPowerPhaseSwitchGapBridging(t *testing.T) { + Voltage = 230 + lp := &Loadpoint{ + log: util.NewLogger("lp"), + status: api.StatusC, + charger: phaseSwitchCharger{}, + batteryBoost: boostContinue, + minCurrent: 6, + maxCurrent: 16, + phases: 1, + phasesConfigured: 3, + } + s := &mockSite{} + lp.site = s + + // boostContinue on 1p with phase switching: delta must cover the gap + // between 1p@16A (3680W) and 3p@6A (4140W) = 460W, plus the + // base delta (100W) and coarse step power (1p: 230W) + limit := 10000.0 + s.maxDischargePower = &limit + s.residualPower = 0 + res := lp.boostPower(0) + // delta = 100 (base) + 230 (step@1p) + 460 (gap) = 790 + // res = 0 + 790 + 0 = 790 + assert.Equal(t, 790.0, res) + // verify gap alone exceeds the 3p minimum threshold + // available_power ≈ chargePower(3680) + boostReturn(790) = 4470 > 4140 + assert.Greater(t, Voltage*16+res, Voltage*6*3, "boost must bridge 1p-3p gap") + + // already on 3p: no phase gap added, only base + step + lp.phases = 3 + res = lp.boostPower(0) + // delta = 100 + 690 (step@3p) = 790 + assert.Equal(t, 790.0, res) +} + +func TestBoostPowerPhaseSwitchGapBridgingExclusions(t *testing.T) { + Voltage = 230 + limit10k := 10000.0 + now := time.Now() + + for _, tc := range []struct { + name string + charger api.Charger + phasesSwitched time.Time + maxDischargePower *float64 + circuitPower float64 + expected float64 + }{ + { + name: "no phase switching", + charger: plainCharger{}, + maxDischargePower: &limit10k, + circuitPower: 10000, + expected: 330, + }, + { + name: "phase switch not completed", + charger: phaseSwitchCharger{}, + phasesSwitched: now, + maxDischargePower: &limit10k, + circuitPower: 10000, + expected: 330, + }, + { + name: "no max discharge power limit", + charger: phaseSwitchCharger{}, + maxDischargePower: nil, + circuitPower: 10000, + expected: 330, + }, + { + name: "circuit not allowing 3p", + charger: phaseSwitchCharger{}, + maxDischargePower: &limit10k, + circuitPower: 0, + expected: 330, + }, + } { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + + circuit := api.NewMockCircuit(ctrl) + circuit.EXPECT().ValidatePower(gomock.Any(), gomock.Any()).Return(tc.circuitPower).AnyTimes() + + lp := &Loadpoint{ + log: util.NewLogger("lp"), + status: api.StatusC, + charger: tc.charger, + batteryBoost: boostContinue, + minCurrent: 6, + maxCurrent: 16, + phases: 1, + phasesConfigured: 3, + phasesSwitched: tc.phasesSwitched, + circuit: circuit, + } + s := &mockSite{ + maxDischargePower: tc.maxDischargePower, + } + lp.site = s + + res := lp.boostPower(0) + assert.Equal(t, tc.expected, res) + }) + } +}