Skip to content

Commit

Permalink
Allow fixed phase configuration (evcc-io#3714)
Browse files Browse the repository at this point in the history
  • Loading branch information
andig authored and dontbyte committed Aug 2, 2022
1 parent 52dd0b4 commit db2b5e5
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 37 deletions.
22 changes: 14 additions & 8 deletions core/loadpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ type LoadPoint struct {
Mode api.ChargeMode `mapstructure:"mode"` // Charge mode, guarded by mutex

Title string `mapstructure:"title"` // UI title
Phases int `mapstructure:"phases"` // Charger enabled phases
DefaultPhases int `mapstructure:"phases"` // Charger enabled phases
ChargerRef string `mapstructure:"charger"` // Charger reference
VehicleRef string `mapstructure:"vehicle"` // Vehicle reference
VehiclesRef []string `mapstructure:"vehicles"` // Vehicles reference
Expand All @@ -112,6 +112,7 @@ type LoadPoint struct {
GuardDuration time.Duration // charger enable/disable minimum holding time

enabled bool // Charger enabled state
phases int // Charger active phases, guarded by mutex
measuredPhases int // Charger physically measured phases
chargeCurrent float64 // Charger current limit
guardUpdated time.Time // Charger enabled/disabled timestamp
Expand Down Expand Up @@ -213,9 +214,10 @@ func NewLoadPointFromConfig(log *util.Logger, cp configProvider, other map[strin
lp.charger = cp.Charger(lp.ChargerRef)
lp.configureChargerType(lp.charger)

// TODO handle delayed scale-down
// setup fixed phases
if _, ok := lp.charger.(api.ChargePhases); ok && lp.GetPhases() != 0 {
lp.log.WARN.Printf("ignoring phases config (%dp) for switchable charger", lp.GetPhases())
lp.log.WARN.Printf("locking phase config to %dp for switchable charger", lp.GetPhases())
// set to unknown since we don't know the charger's setting yet and don't want to interrupt charging
lp.setPhases(0)
}

Expand All @@ -240,7 +242,7 @@ func NewLoadPoint(log *util.Logger) *LoadPoint {
clock: clock, // mockable time
bus: bus, // event bus
Mode: api.ModeOff,
Phases: 3,
phases: 3,
status: api.StatusNone,
MinCurrent: 6, // A
MaxCurrent: 16, // A
Expand Down Expand Up @@ -512,7 +514,7 @@ func (lp *LoadPoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even
lp.publish("title", lp.Title)
lp.publish("minCurrent", lp.MinCurrent)
lp.publish("maxCurrent", lp.MaxCurrent)
lp.publish("phases", lp.Phases)
lp.publish("phases", lp.phases)
lp.publish("activePhases", lp.activePhases())
lp.publish("hasVehicle", len(lp.vehicles) > 0)

Expand Down Expand Up @@ -975,6 +977,10 @@ func (lp *LoadPoint) resetPVTimerIfRunning(typ ...string) {

// scalePhasesIfAvailable scales if api.ChargePhases is available
func (lp *LoadPoint) scalePhasesIfAvailable(phases int) error {
if lp.DefaultPhases != 0 {
phases = lp.DefaultPhases
}

if _, ok := lp.charger.(api.ChargePhases); ok {
return lp.scalePhases(phases)
}
Expand All @@ -986,11 +992,11 @@ func (lp *LoadPoint) scalePhasesIfAvailable(phases int) error {
func (lp *LoadPoint) setPhases(phases int) {
if lp.GetPhases() != phases {
lp.Lock()
lp.Phases = phases
lp.phases = phases
lp.phaseTimer = time.Time{}
lp.Unlock()

lp.publish("phases", lp.Phases)
lp.publish("phases", lp.phases)
lp.publishTimer(phaseTimer, 0, timerInactive)

lp.resetMeasuredPhases()
Expand Down Expand Up @@ -1043,7 +1049,7 @@ func (lp *LoadPoint) pvScalePhases(availablePower, minCurrent, maxCurrent float6
activePhases := lp.activePhases()

// scale down phases
if targetCurrent := powerToCurrent(availablePower, activePhases); targetCurrent < minCurrent && activePhases > 1 {
if targetCurrent := powerToCurrent(availablePower, activePhases); targetCurrent < minCurrent && activePhases > 1 && lp.DefaultPhases < 3 {
lp.log.DEBUG.Printf("available power %.0fW < %.0fW min %dp threshold", availablePower, float64(activePhases)*Voltage*minCurrent, activePhases)

if lp.phaseTimer.IsZero() {
Expand Down
11 changes: 8 additions & 3 deletions core/loadpoint_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,16 +102,21 @@ func (lp *LoadPoint) SetMinSoC(soc int) {
func (lp *LoadPoint) GetPhases() int {
lp.Lock()
defer lp.Unlock()
return lp.Phases
return lp.phases
}

// SetPhases sets loadpoint enabled phases
func (lp *LoadPoint) SetPhases(phases int) error {
if phases != 1 && phases != 3 {
// limit auto mode (phases=0) to scalable charger
if _, ok := lp.charger.(api.ChargePhases); !ok && phases == 0 {
return fmt.Errorf("invalid number of phases: %d", phases)
}

if _, ok := lp.charger.(api.ChargePhases); ok {
if phases != 0 && phases != 1 && phases != 3 {
return fmt.Errorf("invalid number of phases: %d", phases)
}

if _, ok := lp.charger.(api.ChargePhases); ok && phases > 0 {
return lp.scalePhases(phases)
}

Expand Down
7 changes: 5 additions & 2 deletions core/loadpoint_phases.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,12 @@ func (lp *LoadPoint) maxActivePhases() int {
measured = 0
}

// if 1p3p supported then assume 3p
// if 1p3p supported then assume configured limit or 3p
if _, ok := lp.charger.(api.ChargePhases); ok {
physical = 3
physical = lp.DefaultPhases
if physical == 0 {
physical = 3
}
}

return min(expect(vehicle), expect(physical), expect(measured))
Expand Down
148 changes: 129 additions & 19 deletions core/loadpoint_phases_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,66 @@ var (
}
)

func TestMaxActivePhases(t *testing.T) {
ctrl := gomock.NewController(t)

// 0 is auto, 1/3 are fixed
for _, dflt := range []int{0, 1, 3} {
for _, tc := range phaseTests {
// skip invalid configs (free scaling for simple charger)
if dflt == 0 && tc.capable != 0 {
continue
}

t.Log(dflt, tc)

plainCharger := mock.NewMockCharger(ctrl)

// 1p3p
var phaseCharger *mock.MockChargePhases
if tc.capable == 0 {
phaseCharger = mock.NewMockChargePhases(ctrl)
}

vehicle := mock.NewMockVehicle(ctrl)
vehicle.EXPECT().Phases().Return(tc.vehicle).MinTimes(1)

lp := &LoadPoint{
DefaultPhases: dflt, // fixed phases or default
vehicle: vehicle,
phases: tc.physical,
measuredPhases: tc.measuredPhases,
}

if phaseCharger != nil {
lp.charger = struct {
*mock.MockCharger
*mock.MockChargePhases
}{
plainCharger, phaseCharger,
}
} else {
lp.charger = struct {
*mock.MockCharger
}{
plainCharger,
}
}

expectedPhases := tc.maxExpected

// restrict scalable charger by config
if tc.capable == 0 && dflt > 0 && dflt < tc.maxExpected {
expectedPhases = dflt
}

if phs := lp.maxActivePhases(); phs != expectedPhases {
t.Errorf("expected max %d, got %d", expectedPhases, phs)
}
}
}
}

func testScale(t *testing.T, lp *LoadPoint, power float64, direction string, tc testCase) {
act := lp.activePhases()
max := lp.maxActivePhases()
Expand Down Expand Up @@ -118,19 +178,20 @@ func TestPvScalePhases(t *testing.T) {
vehicle.EXPECT().Phases().Return(tc.vehicle).MinTimes(1)

lp := &LoadPoint{
log: util.NewLogger("foo"),
bus: evbus.New(),
clock: clock,
chargeMeter: &Null{}, // silence nil panics
chargeRater: &Null{}, // silence nil panics
chargeTimer: &Null{}, // silence nil panics
progress: NewProgress(0, 10), // silence nil panics
wakeUpTimer: NewTimer(), // silence nil panics
Mode: api.ModeNow,
MinCurrent: minA,
MaxCurrent: maxA,
vehicle: vehicle,
Phases: tc.physical,
log: util.NewLogger("foo"),
bus: evbus.New(),
clock: clock,
chargeMeter: &Null{}, // silence nil panics
chargeRater: &Null{}, // silence nil panics
chargeTimer: &Null{}, // silence nil panics
progress: NewProgress(0, 10), // silence nil panics
wakeUpTimer: NewTimer(), // silence nil panics
Mode: api.ModeNow,
MinCurrent: minA,
MaxCurrent: maxA,
vehicle: vehicle,
DefaultPhases: 0, // allow switching
phases: tc.physical,
}

if phaseCharger != nil {
Expand All @@ -155,8 +216,8 @@ 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)
if lp.phases != tc.physical {
t.Error("wrong phases", lp.phases, tc.physical)
}

if phs := lp.activePhases(); phs != tc.actExpected {
Expand Down Expand Up @@ -184,7 +245,7 @@ func TestPvScalePhases(t *testing.T) {
lp.phaseTimer = time.Time{}

// reset to initial state
lp.Phases = tc.physical
lp.phases = tc.physical
lp.measuredPhases = tc.measuredPhases

plainCharger.EXPECT().Enable(false).Return(nil).MaxTimes(1)
Expand Down Expand Up @@ -289,7 +350,7 @@ func TestPvScalePhasesTimer(t *testing.T) {
charger: charger,
MinCurrent: minA,
MaxCurrent: maxA,
Phases: tc.phases,
phases: tc.phases,
measuredPhases: tc.measuredPhases,
Enable: ThresholdConfig{
Delay: dt,
Expand All @@ -312,8 +373,57 @@ func TestPvScalePhasesTimer(t *testing.T) {
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)
case lp.phases != tc.toPhases:
t.Errorf("expected %dp, got %dp", tc.toPhases, lp.phases)
}
}
}

func TestScalePhasesIfAvailable(t *testing.T) {
ctrl := gomock.NewController(t)

tc := []struct {
dflt, physical, maxExpected int
}{
{0, 0, 3},
{0, 1, 3},
{0, 3, 3},
{1, 0, 1},
{1, 1, 1},
{1, 3, 1},
{3, 0, 3},
{3, 1, 3},
{3, 3, 3},
}

for _, tc := range tc {
t.Log(tc)

plainCharger := mock.NewMockCharger(ctrl)
phaseCharger := mock.NewMockChargePhases(ctrl)

lp := &LoadPoint{
log: util.NewLogger("foo"),
clock: clock.NewMock(),
charger: struct {
*mock.MockCharger
*mock.MockChargePhases
}{
plainCharger,
phaseCharger,
},
MinCurrent: minA,
DefaultPhases: tc.dflt, // fixed phases or default
phases: tc.physical, // current phase status
}

// restrict scalable charger by config
if tc.dflt == 0 || tc.dflt != tc.physical {
phaseCharger.EXPECT().Phases1p3p(tc.maxExpected).Return(nil)
}

_ = lp.scalePhasesIfAvailable(3)

ctrl.Finish()
}
}
10 changes: 5 additions & 5 deletions core/loadpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ func attachListeners(t *testing.T, lp *LoadPoint) {
func TestNew(t *testing.T) {
lp := NewLoadPoint(util.NewLogger("foo"))

if lp.Phases != 3 {
t.Errorf("Phases %v", lp.Phases)
if lp.phases != 3 {
t.Errorf("Phases %v", lp.phases)
}
if lp.MinCurrent != minA {
t.Errorf("MinCurrent %v", lp.MinCurrent)
Expand Down Expand Up @@ -154,7 +154,7 @@ func TestUpdatePowerZero(t *testing.T) {
wakeUpTimer: NewTimer(),
MinCurrent: minA,
MaxCurrent: maxA,
Phases: 1,
phases: 1,
status: tc.status, // no status change
}

Expand Down Expand Up @@ -302,7 +302,7 @@ func TestPVHysteresis(t *testing.T) {
charger: charger,
MinCurrent: minA,
MaxCurrent: maxA,
Phases: phases,
phases: phases,
measuredPhases: phases,
Enable: ThresholdConfig{
Threshold: tc.enable,
Expand Down Expand Up @@ -350,7 +350,7 @@ func TestPVHysteresisForStatusOtherThanC(t *testing.T) {
clock: clck,
MinCurrent: minA,
MaxCurrent: maxA,
Phases: phases,
phases: phases,
measuredPhases: phases,
}

Expand Down

0 comments on commit db2b5e5

Please sign in to comment.