Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Loadpoint: calculate minimum power taking phases into account #16274

Merged
merged 5 commits into from
Oct 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/loadpoint/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions core/loadpoint_effective.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 18 additions & 2 deletions core/loadpoint_phases.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,21 +74,37 @@ 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()
vehicle := lp.getVehiclePhases()
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))
Expand Down
147 changes: 94 additions & 53 deletions core/loadpoint_phases_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
plainCharger, api.NewMockPhaseSwitcher(ctrl),
}
} else {
}

// restrict scalable charger by config
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,
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.minExpected
if tc.capable == 0 && configured > 0 && configured < tc.minExpected {
expectedPhases = configured
}

require.Equal(t, expectedPhases, lp.maxActivePhases(), "expected max active phases")
require.Equal(t, expectedPhases, lp.minActivePhases(), "expected min active phases")
ctrl.Finish()
}
}
}
Expand Down
Loading