From d9a4152d8c4905c007cfb3a1047ddb841a215a96 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 13 Oct 2024 11:21:28 +0200 Subject: [PATCH] Loadpoint: calculate minimum power taking phases into account (#16274) --- core/loadpoint/api.go | 2 +- core/loadpoint_effective.go | 5 +- core/loadpoint_phases.go | 20 ++++- core/loadpoint_phases_test.go | 153 +++++++++++++++++++++------------- 4 files changed, 118 insertions(+), 62 deletions(-) diff --git a/core/loadpoint/api.go b/core/loadpoint/api.go index 57f9498da..ce12b4781 100644 --- a/core/loadpoint/api.go +++ b/core/loadpoint/api.go @@ -69,7 +69,7 @@ type API interface { EffectivePriority() int // EffectivePlanTime returns the effective plan time EffectivePlanTime() time.Time - // EffectiveMinPower returns the min charging power for a single phase + // EffectiveMinPower returns the min charging power for the minimum active phases EffectiveMinPower() float64 // EffectiveMaxPower returns the max charging power taking active phases into account EffectiveMaxPower() float64 diff --git a/core/loadpoint_effective.go b/core/loadpoint_effective.go index 73ee44682..6d5ffa80b 100644 --- a/core/loadpoint_effective.go +++ b/core/loadpoint_effective.go @@ -124,10 +124,9 @@ func (lp *Loadpoint) effectiveLimitSoc() int { return 100 } -// EffectiveMinPower returns the effective min power for a single phase +// EffectiveMinPower returns the effective min power for the minimum active phases func (lp *Loadpoint) EffectiveMinPower() float64 { - // TODO check if 1p available - return Voltage * lp.effectiveMinCurrent() + return Voltage * lp.effectiveMinCurrent() * float64(lp.minActivePhases()) } // EffectiveMaxPower returns the effective max power taking vehicle capabilities and phase scaling into account diff --git a/core/loadpoint_phases.go b/core/loadpoint_phases.go index e665e4327..6a8d81f1a 100644 --- a/core/loadpoint_phases.go +++ b/core/loadpoint_phases.go @@ -74,7 +74,21 @@ func (lp *Loadpoint) ActivePhases() int { return active } -// maxActivePhases returns the maximum number of active phases for the meter. +// minActivePhases returns the minimum number of active phases for the loadpoint. +func (lp *Loadpoint) minActivePhases() int { + lp.Lock() + configuredPhases := lp.configuredPhases + lp.Unlock() + + // 1p3p supported or limit 1p + if lp.hasPhaseSwitching() || configuredPhases == 1 { + return 1 + } + + return lp.maxActivePhases() +} + +// maxActivePhases returns the maximum number of active phases for the loadpoint. func (lp *Loadpoint) maxActivePhases() int { physical := lp.GetPhases() measured := lp.getMeasuredPhases() @@ -82,13 +96,15 @@ func (lp *Loadpoint) maxActivePhases() int { charger := lp.getChargerPhysicalPhases() // during 1p or unknown config, 1p measured is not a restriction - if physical <= 1 || vehicle == 1 { + if physical <= 1 || vehicle == 1 || charger == 1 { measured = 0 } // if 1p3p supported then assume configured limit or 3p if lp.hasPhaseSwitching() { + lp.Lock() physical = lp.configuredPhases + lp.Unlock() } return min(expect(vehicle), expect(physical), expect(measured), expect(charger)) diff --git a/core/loadpoint_phases_test.go b/core/loadpoint_phases_test.go index a4fdc96f5..2faca534a 100644 --- a/core/loadpoint_phases_test.go +++ b/core/loadpoint_phases_test.go @@ -17,106 +17,147 @@ type testCase struct { // capable=0 signals 1p3p as set during loadpoint init // physical/vehicle=0 signals unknown // measuredPhases<>0 signals previous measurement - capable, physical, vehicle, measuredPhases, actExpected, maxExpected int + capable, physical, vehicle, measuredPhases, actExpected, maxExpected, minExpected int // scaling expectation: d=down, u=up, du=both 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, ""}, + {1, 1, 0, 0, 1, 1, 1, ""}, + {1, 1, 0, 1, 1, 1, 1, ""}, + {1, 1, 1, 0, 1, 1, 1, ""}, + {1, 1, 2, 0, 1, 1, 1, ""}, + {1, 1, 3, 0, 1, 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, ""}, + {3, 3, 0, 0, unknownPhases, 3, 3, ""}, + {3, 3, 0, 1, 1, 1, 1, ""}, + {3, 3, 0, 2, 2, 2, 2, ""}, + {3, 3, 0, 3, 3, 3, 3, ""}, + {3, 3, 1, 0, 1, 1, 1, ""}, + {3, 3, 2, 0, 2, 2, 2, ""}, + {3, 3, 3, 0, 3, 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"}, + {0, 0, 0, 0, unknownPhases, 3, 1, "du"}, + {0, 0, 0, 1, 1, 3, 1, "u"}, + {0, 0, 0, 2, 2, 3, 1, "du"}, + {0, 0, 0, 3, 3, 3, 1, "du"}, + {0, 0, 1, 0, 1, 1, 1, ""}, + {0, 0, 2, 0, 2, 2, 1, "du"}, + {0, 0, 3, 0, 3, 3, 1, "du"}, // 1p3p, 1 currently active - {0, 1, 0, 0, 1, 3, "u"}, - {0, 1, 0, 1, 1, 3, "u"}, + {0, 1, 0, 0, 1, 3, 1, "u"}, + {0, 1, 0, 1, 1, 3, 1, "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"}, + {0, 1, 1, 0, 1, 1, 1, ""}, + {0, 1, 2, 0, 1, 2, 1, "u"}, + {0, 1, 3, 0, 1, 3, 1, "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"}, + {0, 3, 0, 0, unknownPhases, 3, 1, "d"}, + {0, 3, 0, 1, 1, 1, 1, ""}, + {0, 3, 0, 2, 2, 2, 1, "d"}, + {0, 3, 0, 3, 3, 3, 1, "d"}, + {0, 3, 1, 0, 1, 1, 1, ""}, + {0, 3, 2, 0, 2, 2, 1, "d"}, + {0, 3, 3, 0, 3, 3, 1, "d"}, } func TestMaxActivePhases(t *testing.T) { - ctrl := gomock.NewController(t) - // 0 is auto, 1/3 are fixed - for _, dflt := range []int{0, 1, 3} { + for _, configured := range []int{0, 1, 3} { for _, tc := range phaseTests { // skip invalid configs (free scaling for simple charger) - if dflt == 0 && tc.capable != 0 { + if configured == 0 && tc.capable != 0 { continue } - t.Log(dflt, tc) + t.Logf("configured %d %+v", configured, tc) + ctrl := gomock.NewController(t) plainCharger := api.NewMockCharger(ctrl) - // 1p3p - var phaseCharger *api.MockPhaseSwitcher - if tc.capable == 0 { - phaseCharger = api.NewMockPhaseSwitcher(ctrl) - } - vehicle := api.NewMockVehicle(ctrl) vehicle.EXPECT().Phases().Return(tc.vehicle).MinTimes(1) lp := &Loadpoint{ - configuredPhases: dflt, // fixed phases or default + configuredPhases: configured, // fixed phases or default vehicle: vehicle, phases: tc.physical, measuredPhases: tc.measuredPhases, + charger: plainCharger, } - if phaseCharger != nil { + // 1p3p + if tc.capable == 0 { lp.charger = struct { *api.MockCharger *api.MockPhaseSwitcher }{ - plainCharger, phaseCharger, - } - } else { - lp.charger = struct { - *api.MockCharger - }{ - plainCharger, + plainCharger, api.NewMockPhaseSwitcher(ctrl), } } - expectedPhases := tc.maxExpected - // restrict scalable charger by config - if tc.capable == 0 && dflt > 0 && dflt < tc.maxExpected { - expectedPhases = dflt + expectedPhases := tc.maxExpected + if tc.capable == 0 && configured > 0 && configured < tc.maxExpected { + expectedPhases = configured } require.Equal(t, expectedPhases, lp.maxActivePhases(), "expected max active phases") + ctrl.Finish() + } + } +} + +func TestMinActivePhases(t *testing.T) { + // 0 is auto, 1/3 are fixed + for _, configured := range []int{0, 1, 3} { + for _, tc := range phaseTests { + // skip invalid configs (free scaling for simple charger) + if configured == 0 && tc.capable != 0 { + continue + } + + // skip physical config different than configured + if configured != 0 && tc.capable != 0 && configured != tc.physical { + continue + } + + t.Logf("configured %d %+v", configured, tc) + + ctrl := gomock.NewController(t) + plainCharger := api.NewMockCharger(ctrl) + + vehicle := api.NewMockVehicle(ctrl) + vehicle.EXPECT().Phases().Return(tc.vehicle).AnyTimes() + + lp := &Loadpoint{ + configuredPhases: configured, // fixed phases or default + vehicle: vehicle, + phases: tc.physical, + measuredPhases: tc.measuredPhases, + charger: plainCharger, + } + + // 1p3p + if tc.capable == 0 { + lp.charger = struct { + *api.MockCharger + *api.MockPhaseSwitcher + }{ + plainCharger, api.NewMockPhaseSwitcher(ctrl), + } + } + + // restrict scalable charger by config + expectedPhases := tc.minExpected + if tc.capable == 0 && configured > 0 && configured < tc.minExpected { + expectedPhases = configured + } + + require.Equal(t, expectedPhases, lp.minActivePhases(), "expected min active phases") + ctrl.Finish() } } }