Allow fixed phase configuration (#3714)

This commit is contained in:
andig 2022-06-27 13:39:42 +02:00 • committed by GitHub
parent bed0a26260
commit 89c908b414
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 161 additions and 37 deletions

View file

@ -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
@ -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
@ -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)
}
@ -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
@ -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)
@ -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)
}
@ -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()
@ -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() {

View file

@ -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)
}

View file

@ -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))

View file

@ -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()
@ -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 {
@ -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 {
@ -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)
@ -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,
@ -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()
}
}

View file

@ -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)
@ -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
}
@ -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,
@ -350,7 +350,7 @@ func TestPVHysteresisForStatusOtherThanC(t *testing.T) {
clock: clck,
MinCurrent: minA,
MaxCurrent: maxA,
Phases: phases,
phases: phases,
measuredPhases: phases,
}