Allow fixed phase configuration (#3714)
This commit is contained in:
parent
bed0a26260
commit
89c908b414
5 changed files with 161 additions and 37 deletions
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue