diff --git a/api/api.go b/api/api.go index 767e0b04c..3bb77c42e 100644 --- a/api/api.go +++ b/api/api.go @@ -9,7 +9,7 @@ import ( "github.com/fatih/structs" ) -//go:generate mockgen -package mock -destination ../mock/mock_api.go github.com/evcc-io/evcc/api Charger,ChargeState,ChargePhases,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery +//go:generate mockgen -package mock -destination ../mock/mock_api.go github.com/evcc-io/evcc/api Charger,ChargeState,ChargePhases,Identifier,Meter,MeterEnergy,Vehicle,VehiclePhases,ChargeRater,Battery // ChargeMode are charge modes modeled after OpenWB type ChargeMode string @@ -170,6 +170,11 @@ type VehiclePosition interface { Position() (float64, float64, error) } +// VehiclePhases returns the number of supported phases +type VehiclePhases interface { + Phases() int +} + // VehicleStartCharge starts the charging session on the vehicle side type VehicleStartCharge interface { StartCharge() error diff --git a/core/loadpoint.go b/core/loadpoint.go index 7b7027bf1..a01b40ed4 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -773,6 +773,8 @@ func (lp *LoadPoint) setActiveVehicle(vehicle api.Vehicle) { lp.publish("vehicleCapacity", lp.vehicle.Capacity()) lp.applyAction(vehicle.OnIdentified()) + + lp.setVehiclePhases() } else { lp.socEstimator = nil @@ -909,6 +911,25 @@ func (lp *LoadPoint) resetPVTimerIfRunning(typ ...string) { lp.publishTimer(pvTimer, 0, timerInactive) } +// setVehiclePhases sets the expected active phases by the vehicle +func (lp *LoadPoint) setVehiclePhases() { + if v, ok := lp.vehicle.(api.VehiclePhases); ok { + if phases := v.Phases(); phases > 0 { + lp.log.DEBUG.Printf("vehicle phases: %dp", phases) + + lp.Lock() + defer lp.Unlock() + + if phases > lp.Phases { + phases = lp.Phases + } + + lp.activePhases = phases + lp.publish("activePhases", lp.activePhases) + } + } +} + // scalePhasesIfAvailable scales if api.ChargePhases is available func (lp *LoadPoint) scalePhasesIfAvailable(phases int) error { err := lp.scalePhases(phases) @@ -927,12 +948,16 @@ func (lp *LoadPoint) setPhases(phases int) { lp.Phases = phases lp.publish("phases", lp.Phases) - // When scaling down, charger will temporarily disable. During this time, activePhases will not be updated - // since all currents are zero. This will lead to inconsistent state when scaling is triggered again - // (1p configured vs 3p active). Update activePhases to reflect the current state of the charger. if phases < lp.activePhases { + // When scaling down, charger will temporarily disable. During this time, activePhases will not be updated + // since all currents are zero. This will lead to inconsistent state when scaling is triggered again + // (1p configured vs 3p active). Update activePhases to reflect the current state of the charger. lp.activePhases = phases lp.publish("activePhases", lp.activePhases) + } else { + // When scaling up, charger will offer more phases than vehicle can use. + // Adjust to enable subsequent PV restart at lower powers than full 3p. + lp.setVehiclePhases() } } } diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go index f06aeda96..1928081e5 100644 --- a/core/loadpoint_test.go +++ b/core/loadpoint_test.go @@ -932,3 +932,50 @@ func TestScalePhases(t *testing.T) { } } } + +func TestVehiclePhases(t *testing.T) { + ctrl := gomock.NewController(t) + vehicle := &struct { + *mock.MockVehicle + *mock.MockVehiclePhases + }{ + mock.NewMockVehicle(ctrl), + mock.NewMockVehiclePhases(ctrl), + } + + tc := []struct { + phases, activePhases, vehiclePhases int + res int + }{ + {1, 1, 0, 1}, // leave as-is + {3, 1, 0, 1}, // leave as-is + {3, 3, 0, 3}, // leave as-is + {1, 1, 1, 1}, // leave as-is + {3, 1, 1, 1}, // leave as-is + {3, 3, 1, 1}, // limit to 1p + {1, 1, 2, 1}, // leave as-is + {3, 1, 2, 2}, // limit to 2p + {3, 3, 2, 2}, // limit to 2p + {1, 1, 3, 1}, // leave as-is + {3, 1, 3, 3}, // limit to 3p + {3, 3, 3, 3}, // leave as-is + } + + for _, tc := range tc { + t.Logf("%+v", tc) + + lp := &LoadPoint{ + log: util.NewLogger("foo"), + vehicle: vehicle, + Phases: tc.phases, + activePhases: tc.activePhases, + } + + vehicle.MockVehiclePhases.EXPECT().Phases().Return(tc.vehiclePhases) + + lp.setVehiclePhases() + if lp.activePhases != tc.res { + t.Errorf("expected %v, got %v", tc.res, lp.activePhases) + } + } +} diff --git a/mock/mock_api.go b/mock/mock_api.go index 60e518c16..6ca0847fa 100644 --- a/mock/mock_api.go +++ b/mock/mock_api.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/evcc-io/evcc/api (interfaces: Charger,ChargeState,ChargePhases,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery) +// Source: github.com/evcc-io/evcc/api (interfaces: Charger,ChargeState,ChargePhases,Identifier,Meter,MeterEnergy,Vehicle,VehiclePhases,ChargeRater,Battery) // Package mock is a generated GoMock package. package mock @@ -375,6 +375,43 @@ func (mr *MockVehicleMockRecorder) Title() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Title", reflect.TypeOf((*MockVehicle)(nil).Title)) } +// MockVehiclePhases is a mock of VehiclePhases interface. +type MockVehiclePhases struct { + ctrl *gomock.Controller + recorder *MockVehiclePhasesMockRecorder +} + +// MockVehiclePhasesMockRecorder is the mock recorder for MockVehiclePhases. +type MockVehiclePhasesMockRecorder struct { + mock *MockVehiclePhases +} + +// NewMockVehiclePhases creates a new mock instance. +func NewMockVehiclePhases(ctrl *gomock.Controller) *MockVehiclePhases { + mock := &MockVehiclePhases{ctrl: ctrl} + mock.recorder = &MockVehiclePhasesMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockVehiclePhases) EXPECT() *MockVehiclePhasesMockRecorder { + return m.recorder +} + +// Phases mocks base method. +func (m *MockVehiclePhases) Phases() int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Phases") + ret0, _ := ret[0].(int) + return ret0 +} + +// Phases indicates an expected call of Phases. +func (mr *MockVehiclePhasesMockRecorder) Phases() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Phases", reflect.TypeOf((*MockVehiclePhases)(nil).Phases)) +} + // MockChargeRater is a mock of ChargeRater interface. type MockChargeRater struct { ctrl *gomock.Controller diff --git a/vehicle/embed.go b/vehicle/embed.go index bfc878eb6..71f9d2a10 100644 --- a/vehicle/embed.go +++ b/vehicle/embed.go @@ -5,6 +5,7 @@ import "github.com/evcc-io/evcc/api" type embed struct { Title_ string `mapstructure:"title"` Capacity_ int64 `mapstructure:"capacity"` + Phases_ int `mapstructure:"phases"` Identifiers_ []string `mapstructure:"identifiers"` OnIdentify api.ActionConfig `mapstructure:"onIdentify"` } @@ -19,6 +20,11 @@ func (v *embed) Capacity() int64 { return v.Capacity_ } +// Phases returns the phases used by the vehicle +func (v *embed) Phases() int { + return v.Phases_ +} + // Identifiers implements the api.Identifier interface func (v *embed) Identifiers() []string { return v.Identifiers_