From a57aeeb63464de56147cfa4ec3fd1fa44a739e44 Mon Sep 17 00:00:00 2001 From: andig Date: Mon, 10 Apr 2023 12:42:46 +0200 Subject: [PATCH 1/9] Split charger into current- and power controllers --- api/api.go | 18 +++++++++++------- cmd/charger.go | 8 ++++++-- cmd/charger_ramp.go | 21 ++++++++++++++++----- core/loadpoint.go | 10 +++++----- 4 files changed, 38 insertions(+), 19 deletions(-) diff --git a/api/api.go b/api/api.go index 5cd88569f7..9627da9fb9 100644 --- a/api/api.go +++ b/api/api.go @@ -124,7 +124,7 @@ type ChargeState interface { Status() (ChargeStatus, error) } -// CurrentLimiter provides settings charging maximum charging current +// CurrentLimiter limits maximum charging current type CurrentLimiter interface { MaxCurrent(current int64) error } @@ -134,17 +134,21 @@ type CurrentGetter interface { GetMaxCurrent() (float64, error) } +// ChargerEx provides milli-amp precision charger current control +type ChargerEx interface { + MaxCurrentMillis(current float64) error +} + +// PowerLimiter limits maximum charging power +type PowerLimiter interface { + MaxPower(power float64) error +} + // Charger provides current charging status and enable/disable charging type Charger interface { ChargeState Enabled() (bool, error) Enable(enable bool) error - CurrentLimiter -} - -// ChargerEx provides milli-amp precision charger current control -type ChargerEx interface { - MaxCurrentMillis(current float64) error } // PhaseSwitcher provides 1p3p switching diff --git a/cmd/charger.go b/cmd/charger.go index 3c918cb9a5..107f7acd58 100644 --- a/cmd/charger.go +++ b/cmd/charger.go @@ -76,8 +76,12 @@ func runCharger(cmd *cobra.Command, args []string) { if current != noCurrent { flagUsed = true - if err := v.MaxCurrent(current); err != nil { - log.ERROR.Println("set current:", err) + if vv, ok := v.(api.CurrentLimiter); ok { + if err := vv.MaxCurrent(current); err != nil { + log.ERROR.Println("set current:", err) + } + } else { + log.ERROR.Printf("current limiter: not implemented") } } diff --git a/cmd/charger_ramp.go b/cmd/charger_ramp.go index e0eb89b0c8..71fbd02d75 100644 --- a/cmd/charger_ramp.go +++ b/cmd/charger_ramp.go @@ -26,7 +26,12 @@ func init() { chargerRampCmd.Flags().StringP(flagDelay, "", "1s", "ramp delay") } -func ramp(c api.Charger, digits int, delay time.Duration) { +type currentControllable interface { + api.Charger + api.CurrentLimiter +} + +func ramp(c currentControllable, digits int, delay time.Duration) { steps := math.Pow10(digits) delta := 1 / steps @@ -96,11 +101,17 @@ func runChargerRamp(cmd *cobra.Command, args []string) { chargers := config.Chargers().Devices() - for _, v := range config.Instances(chargers) { - if _, ok := v.(api.ChargerEx); digits > 0 && !ok { - log.ERROR.Fatalln("charger does not support mA control") + for _, c := range config.Instances(chargers) { + cc, ok := c.(currentControllable) + if !ok { + log.ERROR.Fatalf("current limiter: not implemented") } - ramp(v, digits, delay) + + if _, ok := cc.(api.ChargerEx); digits > 0 && !ok { + log.ERROR.Fatalln("charger ex: not implemented") + } + + ramp(cc, digits, delay) } // wait for shutdown diff --git a/core/loadpoint.go b/core/loadpoint.go index ca44001b1d..27c4b71fb1 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -698,11 +698,11 @@ func (lp *Loadpoint) setLimit(chargeCurrent float64, force bool) error { // set current if chargeCurrent != lp.chargeCurrent && chargeCurrent >= lp.GetMinCurrent() { var err error - if charger, ok := lp.charger.(api.ChargerEx); ok { - err = charger.MaxCurrentMillis(chargeCurrent) - } else { - err = lp.charger.MaxCurrent(int64(chargeCurrent)) - } + // if charger, ok := lp.charger.(api.ChargerEx); ok { + // err = charger.MaxCurrentMillis(chargeCurrent) + // } else { + // err = lp.charger.MaxCurrent(int64(chargeCurrent)) + // } if err != nil { v := lp.GetVehicle() From adb41f4dddfdb5c7621c548950a3c54fab0faa12 Mon Sep 17 00:00:00 2001 From: andig Date: Mon, 10 Apr 2023 12:56:08 +0200 Subject: [PATCH 2/9] wip --- api/api.go | 20 ++++++++++++++++---- cmd/charger.go | 2 +- cmd/charger_ramp.go | 9 ++------- core/current_controller.go | 14 ++++++++++++++ core/loadpoint.go | 12 +++++++++++- 5 files changed, 44 insertions(+), 13 deletions(-) create mode 100644 core/current_controller.go diff --git a/api/api.go b/api/api.go index 9627da9fb9..121bdd11bf 100644 --- a/api/api.go +++ b/api/api.go @@ -124,8 +124,8 @@ type ChargeState interface { Status() (ChargeStatus, error) } -// CurrentLimiter limits maximum charging current -type CurrentLimiter interface { +// CurrentController limits maximum charging current +type CurrentController interface { MaxCurrent(current int64) error } @@ -139,8 +139,8 @@ type ChargerEx interface { MaxCurrentMillis(current float64) error } -// PowerLimiter limits maximum charging power -type PowerLimiter interface { +// PowerController limits maximum charging power +type PowerController interface { MaxPower(power float64) error } @@ -151,6 +151,18 @@ type Charger interface { Enable(enable bool) error } +// CurrentControllable combines Charger with CurrentController +type CurrentControllable interface { + Charger + CurrentController +} + +// PowerControllable combines Charger with PowerController +type PowerControllable interface { + Charger + PowerController +} + // PhaseSwitcher provides 1p3p switching type PhaseSwitcher interface { Phases1p3p(phases int) error diff --git a/cmd/charger.go b/cmd/charger.go index 107f7acd58..0c9415febf 100644 --- a/cmd/charger.go +++ b/cmd/charger.go @@ -76,7 +76,7 @@ func runCharger(cmd *cobra.Command, args []string) { if current != noCurrent { flagUsed = true - if vv, ok := v.(api.CurrentLimiter); ok { + if vv, ok := v.(api.CurrentController); ok { if err := vv.MaxCurrent(current); err != nil { log.ERROR.Println("set current:", err) } diff --git a/cmd/charger_ramp.go b/cmd/charger_ramp.go index 71fbd02d75..29df61313e 100644 --- a/cmd/charger_ramp.go +++ b/cmd/charger_ramp.go @@ -26,12 +26,7 @@ func init() { chargerRampCmd.Flags().StringP(flagDelay, "", "1s", "ramp delay") } -type currentControllable interface { - api.Charger - api.CurrentLimiter -} - -func ramp(c currentControllable, digits int, delay time.Duration) { +func ramp(c api.CurrentControllable, digits int, delay time.Duration) { steps := math.Pow10(digits) delta := 1 / steps @@ -102,7 +97,7 @@ func runChargerRamp(cmd *cobra.Command, args []string) { chargers := config.Chargers().Devices() for _, c := range config.Instances(chargers) { - cc, ok := c.(currentControllable) + cc, ok := c.(api.CurrentControllable) if !ok { log.ERROR.Fatalf("current limiter: not implemented") } diff --git a/core/current_controller.go b/core/current_controller.go new file mode 100644 index 0000000000..876b3472f1 --- /dev/null +++ b/core/current_controller.go @@ -0,0 +1,14 @@ +package core + +import "github.com/evcc-io/evcc/api" + +type currentController struct { + charger api.CurrentController +} + +var _ api.PowerController = (*currentController)(nil) + +// currentController implements the api.PowerController interface +func (c *currentController) MaxPower(power float64) error { + return api.ErrNotAvailable +} diff --git a/core/loadpoint.go b/core/loadpoint.go index 27c4b71fb1..039c44196b 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -141,7 +141,8 @@ type Loadpoint struct { charger api.Charger chargeTimer api.ChargeTimer chargeRater api.ChargeRater - chargedAtStartup float64 // session energy at startup + chargeController api.PowerController // Power controller (proxy for CurrentController) + chargedAtStartup float64 // session energy at startup chargeMeter api.Meter // Charger usage meter vehicle api.Vehicle // Currently active vehicle @@ -338,6 +339,15 @@ func (lp *Loadpoint) requestUpdate() { func (lp *Loadpoint) configureChargerType(charger api.Charger) { var integrated bool + switch c := charger.(type) { + case api.CurrentControllable: + lp.chargeController = ¤tController{c} + case api.PowerController: + lp.chargeController = c + default: + panic("charger does not implement api.(Power|Current)Controller") + } + // ensure charge meter exists if lp.chargeMeter == nil { integrated = true From 2647a203bb360f5ac57490b65f7c4cbaaa2f2064 Mon Sep 17 00:00:00 2001 From: andig Date: Mon, 10 Apr 2023 12:56:45 +0200 Subject: [PATCH 3/9] wip --- charger/twc3.go | 2 +- vehicle/tesla.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/charger/twc3.go b/charger/twc3.go index c086ca9b08..2b7c989b0a 100644 --- a/charger/twc3.go +++ b/charger/twc3.go @@ -130,7 +130,7 @@ func (c *Twc3) MaxCurrent(current int64) error { return errors.New("loadpoint not initialized") } - v, ok := c.lp.GetVehicle().(api.CurrentLimiter) + v, ok := c.lp.GetVehicle().(api.CurrentController) if !ok { return errors.New("vehicle not capable of current control") } diff --git a/vehicle/tesla.go b/vehicle/tesla.go index 6b79294d96..cf739f846b 100644 --- a/vehicle/tesla.go +++ b/vehicle/tesla.go @@ -195,7 +195,7 @@ func (v *Tesla) TargetSoc() (float64, error) { return float64(res.Response.ChargeState.ChargeLimitSoc), nil } -var _ api.CurrentLimiter = (*Tesla)(nil) +var _ api.CurrentController = (*Tesla)(nil) // StartCharge implements the api.VehicleChargeController interface func (v *Tesla) MaxCurrent(current int64) error { From 7b3fb58d6de5401b34084c63e6d404f6a482af47 Mon Sep 17 00:00:00 2001 From: andig Date: Mon, 10 Apr 2023 13:08:54 +0200 Subject: [PATCH 4/9] Update mocks --- api/api.go | 4 +- mock/mock_api.go | 234 +------------------------------------------ mock/mock_charger.go | 190 +++++++++++++++++++++++++++++++++++ mock/mock_meter.go | 125 +++++++++++++++++++++++ 4 files changed, 319 insertions(+), 234 deletions(-) create mode 100644 mock/mock_charger.go create mode 100644 mock/mock_meter.go diff --git a/api/api.go b/api/api.go index 121bdd11bf..7c3e925d96 100644 --- a/api/api.go +++ b/api/api.go @@ -9,7 +9,9 @@ import ( "time" ) -//go:generate mockgen -package mock -destination ../mock/mock_api.go github.com/evcc-io/evcc/api Charger,ChargeState,PhaseSwitcher,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery,Tariff +//go:generate mockgen -package mock -destination ../mock/mock_api.go github.com/evcc-io/evcc/api ChargeState,Identifier,Vehicle,Battery,Tariff +//go:generate mockgen -package mock -destination ../mock/mock_meter.go github.com/evcc-io/evcc/api Meter,MeterEnergy,ChargeRater +//go:generate mockgen -package mock -destination ../mock/mock_charger.go github.com/evcc-io/evcc/api Charger,CurrentController,PowerController,PhaseSwitcher // ChargeMode is the charge operation mode. Valid values are off, now, minpv and pv type ChargeMode string diff --git a/mock/mock_api.go b/mock/mock_api.go index a52dabde71..ed48e4f2b6 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,PhaseSwitcher,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery,Tariff) +// Source: github.com/evcc-io/evcc/api (interfaces: ChargeState,Identifier,Vehicle,Battery,Tariff) // Package mock is a generated GoMock package. package mock @@ -11,87 +11,6 @@ import ( gomock "github.com/golang/mock/gomock" ) -// MockCharger is a mock of Charger interface. -type MockCharger struct { - ctrl *gomock.Controller - recorder *MockChargerMockRecorder -} - -// MockChargerMockRecorder is the mock recorder for MockCharger. -type MockChargerMockRecorder struct { - mock *MockCharger -} - -// NewMockCharger creates a new mock instance. -func NewMockCharger(ctrl *gomock.Controller) *MockCharger { - mock := &MockCharger{ctrl: ctrl} - mock.recorder = &MockChargerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockCharger) EXPECT() *MockChargerMockRecorder { - return m.recorder -} - -// Enable mocks base method. -func (m *MockCharger) Enable(arg0 bool) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Enable", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// Enable indicates an expected call of Enable. -func (mr *MockChargerMockRecorder) Enable(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Enable", reflect.TypeOf((*MockCharger)(nil).Enable), arg0) -} - -// Enabled mocks base method. -func (m *MockCharger) Enabled() (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Enabled") - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Enabled indicates an expected call of Enabled. -func (mr *MockChargerMockRecorder) Enabled() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Enabled", reflect.TypeOf((*MockCharger)(nil).Enabled)) -} - -// MaxCurrent mocks base method. -func (m *MockCharger) MaxCurrent(arg0 int64) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "MaxCurrent", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// MaxCurrent indicates an expected call of MaxCurrent. -func (mr *MockChargerMockRecorder) MaxCurrent(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MaxCurrent", reflect.TypeOf((*MockCharger)(nil).MaxCurrent), arg0) -} - -// Status mocks base method. -func (m *MockCharger) Status() (api.ChargeStatus, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Status") - ret0, _ := ret[0].(api.ChargeStatus) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Status indicates an expected call of Status. -func (mr *MockChargerMockRecorder) Status() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Status", reflect.TypeOf((*MockCharger)(nil).Status)) -} - // MockChargeState is a mock of ChargeState interface. type MockChargeState struct { ctrl *gomock.Controller @@ -130,43 +49,6 @@ func (mr *MockChargeStateMockRecorder) Status() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Status", reflect.TypeOf((*MockChargeState)(nil).Status)) } -// MockPhaseSwitcher is a mock of PhaseSwitcher interface. -type MockPhaseSwitcher struct { - ctrl *gomock.Controller - recorder *MockPhaseSwitcherMockRecorder -} - -// MockPhaseSwitcherMockRecorder is the mock recorder for MockPhaseSwitcher. -type MockPhaseSwitcherMockRecorder struct { - mock *MockPhaseSwitcher -} - -// NewMockPhaseSwitcher creates a new mock instance. -func NewMockPhaseSwitcher(ctrl *gomock.Controller) *MockPhaseSwitcher { - mock := &MockPhaseSwitcher{ctrl: ctrl} - mock.recorder = &MockPhaseSwitcherMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockPhaseSwitcher) EXPECT() *MockPhaseSwitcherMockRecorder { - return m.recorder -} - -// Phases1p3p mocks base method. -func (m *MockPhaseSwitcher) Phases1p3p(arg0 int) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Phases1p3p", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// Phases1p3p indicates an expected call of Phases1p3p. -func (mr *MockPhaseSwitcherMockRecorder) Phases1p3p(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Phases1p3p", reflect.TypeOf((*MockPhaseSwitcher)(nil).Phases1p3p), arg0) -} - // MockIdentifier is a mock of Identifier interface. type MockIdentifier struct { ctrl *gomock.Controller @@ -205,82 +87,6 @@ func (mr *MockIdentifierMockRecorder) Identify() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Identify", reflect.TypeOf((*MockIdentifier)(nil).Identify)) } -// MockMeter is a mock of Meter interface. -type MockMeter struct { - ctrl *gomock.Controller - recorder *MockMeterMockRecorder -} - -// MockMeterMockRecorder is the mock recorder for MockMeter. -type MockMeterMockRecorder struct { - mock *MockMeter -} - -// NewMockMeter creates a new mock instance. -func NewMockMeter(ctrl *gomock.Controller) *MockMeter { - mock := &MockMeter{ctrl: ctrl} - mock.recorder = &MockMeterMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockMeter) EXPECT() *MockMeterMockRecorder { - return m.recorder -} - -// CurrentPower mocks base method. -func (m *MockMeter) CurrentPower() (float64, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CurrentPower") - ret0, _ := ret[0].(float64) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CurrentPower indicates an expected call of CurrentPower. -func (mr *MockMeterMockRecorder) CurrentPower() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CurrentPower", reflect.TypeOf((*MockMeter)(nil).CurrentPower)) -} - -// MockMeterEnergy is a mock of MeterEnergy interface. -type MockMeterEnergy struct { - ctrl *gomock.Controller - recorder *MockMeterEnergyMockRecorder -} - -// MockMeterEnergyMockRecorder is the mock recorder for MockMeterEnergy. -type MockMeterEnergyMockRecorder struct { - mock *MockMeterEnergy -} - -// NewMockMeterEnergy creates a new mock instance. -func NewMockMeterEnergy(ctrl *gomock.Controller) *MockMeterEnergy { - mock := &MockMeterEnergy{ctrl: ctrl} - mock.recorder = &MockMeterEnergyMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockMeterEnergy) EXPECT() *MockMeterEnergyMockRecorder { - return m.recorder -} - -// TotalEnergy mocks base method. -func (m *MockMeterEnergy) TotalEnergy() (float64, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "TotalEnergy") - ret0, _ := ret[0].(float64) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// TotalEnergy indicates an expected call of TotalEnergy. -func (mr *MockMeterEnergyMockRecorder) TotalEnergy() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TotalEnergy", reflect.TypeOf((*MockMeterEnergy)(nil).TotalEnergy)) -} - // MockVehicle is a mock of Vehicle interface. type MockVehicle struct { ctrl *gomock.Controller @@ -415,44 +221,6 @@ func (mr *MockVehicleMockRecorder) Title() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Title", reflect.TypeOf((*MockVehicle)(nil).Title)) } -// MockChargeRater is a mock of ChargeRater interface. -type MockChargeRater struct { - ctrl *gomock.Controller - recorder *MockChargeRaterMockRecorder -} - -// MockChargeRaterMockRecorder is the mock recorder for MockChargeRater. -type MockChargeRaterMockRecorder struct { - mock *MockChargeRater -} - -// NewMockChargeRater creates a new mock instance. -func NewMockChargeRater(ctrl *gomock.Controller) *MockChargeRater { - mock := &MockChargeRater{ctrl: ctrl} - mock.recorder = &MockChargeRaterMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockChargeRater) EXPECT() *MockChargeRaterMockRecorder { - return m.recorder -} - -// ChargedEnergy mocks base method. -func (m *MockChargeRater) ChargedEnergy() (float64, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ChargedEnergy") - ret0, _ := ret[0].(float64) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ChargedEnergy indicates an expected call of ChargedEnergy. -func (mr *MockChargeRaterMockRecorder) ChargedEnergy() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChargedEnergy", reflect.TypeOf((*MockChargeRater)(nil).ChargedEnergy)) -} - // MockBattery is a mock of Battery interface. type MockBattery struct { ctrl *gomock.Controller diff --git a/mock/mock_charger.go b/mock/mock_charger.go new file mode 100644 index 0000000000..765608ac26 --- /dev/null +++ b/mock/mock_charger.go @@ -0,0 +1,190 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/evcc-io/evcc/api (interfaces: Charger,CurrentController,PowerController,PhaseSwitcher) + +// Package mock is a generated GoMock package. +package mock + +import ( + reflect "reflect" + + api "github.com/evcc-io/evcc/api" + gomock "github.com/golang/mock/gomock" +) + +// MockCharger is a mock of Charger interface. +type MockCharger struct { + ctrl *gomock.Controller + recorder *MockChargerMockRecorder +} + +// MockChargerMockRecorder is the mock recorder for MockCharger. +type MockChargerMockRecorder struct { + mock *MockCharger +} + +// NewMockCharger creates a new mock instance. +func NewMockCharger(ctrl *gomock.Controller) *MockCharger { + mock := &MockCharger{ctrl: ctrl} + mock.recorder = &MockChargerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCharger) EXPECT() *MockChargerMockRecorder { + return m.recorder +} + +// Enable mocks base method. +func (m *MockCharger) Enable(arg0 bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Enable", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Enable indicates an expected call of Enable. +func (mr *MockChargerMockRecorder) Enable(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Enable", reflect.TypeOf((*MockCharger)(nil).Enable), arg0) +} + +// Enabled mocks base method. +func (m *MockCharger) Enabled() (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Enabled") + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Enabled indicates an expected call of Enabled. +func (mr *MockChargerMockRecorder) Enabled() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Enabled", reflect.TypeOf((*MockCharger)(nil).Enabled)) +} + +// Status mocks base method. +func (m *MockCharger) Status() (api.ChargeStatus, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Status") + ret0, _ := ret[0].(api.ChargeStatus) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Status indicates an expected call of Status. +func (mr *MockChargerMockRecorder) Status() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Status", reflect.TypeOf((*MockCharger)(nil).Status)) +} + +// MockCurrentController is a mock of CurrentController interface. +type MockCurrentController struct { + ctrl *gomock.Controller + recorder *MockCurrentControllerMockRecorder +} + +// MockCurrentControllerMockRecorder is the mock recorder for MockCurrentController. +type MockCurrentControllerMockRecorder struct { + mock *MockCurrentController +} + +// NewMockCurrentController creates a new mock instance. +func NewMockCurrentController(ctrl *gomock.Controller) *MockCurrentController { + mock := &MockCurrentController{ctrl: ctrl} + mock.recorder = &MockCurrentControllerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCurrentController) EXPECT() *MockCurrentControllerMockRecorder { + return m.recorder +} + +// MaxCurrent mocks base method. +func (m *MockCurrentController) MaxCurrent(arg0 int64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MaxCurrent", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// MaxCurrent indicates an expected call of MaxCurrent. +func (mr *MockCurrentControllerMockRecorder) MaxCurrent(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MaxCurrent", reflect.TypeOf((*MockCurrentController)(nil).MaxCurrent), arg0) +} + +// MockPowerController is a mock of PowerController interface. +type MockPowerController struct { + ctrl *gomock.Controller + recorder *MockPowerControllerMockRecorder +} + +// MockPowerControllerMockRecorder is the mock recorder for MockPowerController. +type MockPowerControllerMockRecorder struct { + mock *MockPowerController +} + +// NewMockPowerController creates a new mock instance. +func NewMockPowerController(ctrl *gomock.Controller) *MockPowerController { + mock := &MockPowerController{ctrl: ctrl} + mock.recorder = &MockPowerControllerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPowerController) EXPECT() *MockPowerControllerMockRecorder { + return m.recorder +} + +// MaxPower mocks base method. +func (m *MockPowerController) MaxPower(arg0 float64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MaxPower", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// MaxPower indicates an expected call of MaxPower. +func (mr *MockPowerControllerMockRecorder) MaxPower(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MaxPower", reflect.TypeOf((*MockPowerController)(nil).MaxPower), arg0) +} + +// MockPhaseSwitcher is a mock of PhaseSwitcher interface. +type MockPhaseSwitcher struct { + ctrl *gomock.Controller + recorder *MockPhaseSwitcherMockRecorder +} + +// MockPhaseSwitcherMockRecorder is the mock recorder for MockPhaseSwitcher. +type MockPhaseSwitcherMockRecorder struct { + mock *MockPhaseSwitcher +} + +// NewMockPhaseSwitcher creates a new mock instance. +func NewMockPhaseSwitcher(ctrl *gomock.Controller) *MockPhaseSwitcher { + mock := &MockPhaseSwitcher{ctrl: ctrl} + mock.recorder = &MockPhaseSwitcherMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPhaseSwitcher) EXPECT() *MockPhaseSwitcherMockRecorder { + return m.recorder +} + +// Phases1p3p mocks base method. +func (m *MockPhaseSwitcher) Phases1p3p(arg0 int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Phases1p3p", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Phases1p3p indicates an expected call of Phases1p3p. +func (mr *MockPhaseSwitcherMockRecorder) Phases1p3p(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Phases1p3p", reflect.TypeOf((*MockPhaseSwitcher)(nil).Phases1p3p), arg0) +} diff --git a/mock/mock_meter.go b/mock/mock_meter.go new file mode 100644 index 0000000000..4538a2ceb9 --- /dev/null +++ b/mock/mock_meter.go @@ -0,0 +1,125 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/evcc-io/evcc/api (interfaces: Meter,MeterEnergy,ChargeRater) + +// Package mock is a generated GoMock package. +package mock + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockMeter is a mock of Meter interface. +type MockMeter struct { + ctrl *gomock.Controller + recorder *MockMeterMockRecorder +} + +// MockMeterMockRecorder is the mock recorder for MockMeter. +type MockMeterMockRecorder struct { + mock *MockMeter +} + +// NewMockMeter creates a new mock instance. +func NewMockMeter(ctrl *gomock.Controller) *MockMeter { + mock := &MockMeter{ctrl: ctrl} + mock.recorder = &MockMeterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockMeter) EXPECT() *MockMeterMockRecorder { + return m.recorder +} + +// CurrentPower mocks base method. +func (m *MockMeter) CurrentPower() (float64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CurrentPower") + ret0, _ := ret[0].(float64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CurrentPower indicates an expected call of CurrentPower. +func (mr *MockMeterMockRecorder) CurrentPower() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CurrentPower", reflect.TypeOf((*MockMeter)(nil).CurrentPower)) +} + +// MockMeterEnergy is a mock of MeterEnergy interface. +type MockMeterEnergy struct { + ctrl *gomock.Controller + recorder *MockMeterEnergyMockRecorder +} + +// MockMeterEnergyMockRecorder is the mock recorder for MockMeterEnergy. +type MockMeterEnergyMockRecorder struct { + mock *MockMeterEnergy +} + +// NewMockMeterEnergy creates a new mock instance. +func NewMockMeterEnergy(ctrl *gomock.Controller) *MockMeterEnergy { + mock := &MockMeterEnergy{ctrl: ctrl} + mock.recorder = &MockMeterEnergyMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockMeterEnergy) EXPECT() *MockMeterEnergyMockRecorder { + return m.recorder +} + +// TotalEnergy mocks base method. +func (m *MockMeterEnergy) TotalEnergy() (float64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TotalEnergy") + ret0, _ := ret[0].(float64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// TotalEnergy indicates an expected call of TotalEnergy. +func (mr *MockMeterEnergyMockRecorder) TotalEnergy() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TotalEnergy", reflect.TypeOf((*MockMeterEnergy)(nil).TotalEnergy)) +} + +// MockChargeRater is a mock of ChargeRater interface. +type MockChargeRater struct { + ctrl *gomock.Controller + recorder *MockChargeRaterMockRecorder +} + +// MockChargeRaterMockRecorder is the mock recorder for MockChargeRater. +type MockChargeRaterMockRecorder struct { + mock *MockChargeRater +} + +// NewMockChargeRater creates a new mock instance. +func NewMockChargeRater(ctrl *gomock.Controller) *MockChargeRater { + mock := &MockChargeRater{ctrl: ctrl} + mock.recorder = &MockChargeRaterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockChargeRater) EXPECT() *MockChargeRaterMockRecorder { + return m.recorder +} + +// ChargedEnergy mocks base method. +func (m *MockChargeRater) ChargedEnergy() (float64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ChargedEnergy") + ret0, _ := ret[0].(float64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ChargedEnergy indicates an expected call of ChargedEnergy. +func (mr *MockChargeRaterMockRecorder) ChargedEnergy() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChargedEnergy", reflect.TypeOf((*MockChargeRater)(nil).ChargedEnergy)) +} From 65195a2fdf789248ff1e7f162147cb6dfd425c09 Mon Sep 17 00:00:00 2001 From: andig Date: Mon, 10 Apr 2023 14:15:33 +0200 Subject: [PATCH 5/9] Introduce setPowerLimit --- api/api.go | 2 +- core/current_controller.go | 9 +- core/loadpoint.go | 673 ++++++++++++++++++++++++------------- core/loadpoint_api.go | 2 + core/loadpoint_test.go | 5 +- 5 files changed, 448 insertions(+), 243 deletions(-) diff --git a/api/api.go b/api/api.go index 7c3e925d96..8d100369b4 100644 --- a/api/api.go +++ b/api/api.go @@ -143,7 +143,7 @@ type ChargerEx interface { // PowerController limits maximum charging power type PowerController interface { - MaxPower(power float64) error + MaxPower(power float64) (float64, error) } // Charger provides current charging status and enable/disable charging diff --git a/core/current_controller.go b/core/current_controller.go index 876b3472f1..4ec7e4d93e 100644 --- a/core/current_controller.go +++ b/core/current_controller.go @@ -1,9 +1,14 @@ package core -import "github.com/evcc-io/evcc/api" +import ( + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/util" +) type currentController struct { - charger api.CurrentController + log *util.Logger + charger api.CurrentController + minCurrent, maxCurrent float64 } var _ api.PowerController = (*currentController)(nil) diff --git a/core/loadpoint.go b/core/loadpoint.go index 039c44196b..e91e1d9094 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -130,7 +130,7 @@ type Loadpoint struct { enabled bool // Charger enabled state phases int // Charger enabled phases, guarded by mutex measuredPhases int // Charger physically measured phases - chargeCurrent float64 // Charger current limit + maxPower float64 // Charger power limit guardUpdated time.Time // Charger enabled/disabled timestamp socUpdated time.Time // Soc updated timestamp (poll: connected) vehicleDetect time.Time // Vehicle connected timestamp @@ -163,8 +163,8 @@ type Loadpoint struct { chargeCurrents []float64 // Phase currents connectedTime time.Time // Time when vehicle was connected pvTimer time.Time // PV enabled/disable timer - phaseTimer time.Time // 1p3p switch timer - wakeUpTimer *Timer // Vehicle wake-up timeout + // phaseTimer time.Time // 1p3p switch timer + wakeUpTimer *Timer // Vehicle wake-up timeout // charge progress vehicleSoc float64 // Vehicle Soc @@ -341,10 +341,16 @@ func (lp *Loadpoint) configureChargerType(charger api.Charger) { switch c := charger.(type) { case api.CurrentControllable: - lp.chargeController = ¤tController{c} + lp.chargeController = ¤tController{ + log: lp.log, + charger: c, + minCurrent: lp.MinCurrent, + maxCurrent: lp.MaxCurrent, + } case api.PowerController: lp.chargeController = c default: + // TODO leave empty panic("charger does not implement api.(Power|Current)Controller") } @@ -630,7 +636,8 @@ func (lp *Loadpoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even if lp.enabled = enabled; enabled { lp.guardUpdated = lp.clock.Now() // set defined current for use by pv mode - _ = lp.setLimit(lp.GetMinCurrent(), false) + // _ = lp.setLimit(lp.GetMinCurrent(), false) + _ = lp.setPowerLimit(lp.GetMinPower(), false) } } else { lp.log.ERROR.Printf("charger: %v", err) @@ -667,21 +674,21 @@ func (lp *Loadpoint) syncCharger() error { // status in sync if enabled == lp.enabled { // sync max current - if charger, ok := lp.charger.(api.CurrentGetter); ok && enabled { - current, err := charger.GetMaxCurrent() - if err != nil { - return err - } - - if lp.chargeCurrent != current { - if lp.guardGracePeriodElapsed() { - lp.log.WARN.Printf("charger logic error: current mismatch (got %.3gA, expected %.3gA)", current, lp.chargeCurrent) - } - - lp.chargeCurrent = current - lp.bus.Publish(evChargeCurrent, lp.chargeCurrent) - } - } + // if charger, ok := lp.charger.(api.CurrentGetter); ok && enabled { + // current, err := charger.GetMaxCurrent() + // if err != nil { + // return err + // } + + // if lp.chargeCurrent != current { + // if lp.guardGracePeriodElapsed() { + // lp.log.WARN.Printf("charger logic error: current mismatch (got %.3gA, expected %.3gA)", current, lp.chargeCurrent) + // } + + // lp.chargeCurrent = current + // lp.bus.Publish(evChargeCurrent, lp.chargeCurrent) + // } + // } return nil } @@ -695,25 +702,114 @@ func (lp *Loadpoint) syncCharger() error { return nil } + if !enabled && lp.charging() { + if lp.guardGracePeriodElapsed() { + lp.log.WARN.Println("charger logic error: disabled but charging") + } + return nil + } + + // // TODO move somewhere else + // if charger, ok := lp.charger.(api.CurrentGetter); ok { + // current, err := charger.GetMaxCurrent() + // if err != nil { + // return err + // } + + // if enabled && lp.chargeCurrent != current { + // if lp.guardGracePeriodElapsed() { + // lp.log.WARN.Printf("charger logic error: current mismatch (got %.3gA, expected %.3gA)", current, lp.chargeCurrent) + // } + + // if charger, ok := lp.charger.(api.ChargerEx); ok { + // return charger.MaxCurrentMillis(lp.chargeCurrent) + // } + // return lp.charger.MaxCurrent(int64(lp.chargeCurrent)) + // } + // } + return nil } // setLimit applies charger current limits and enables/disables accordingly -func (lp *Loadpoint) setLimit(chargeCurrent float64, force bool) error { - // full amps only? - if _, ok := lp.charger.(api.ChargerEx); !ok || lp.vehicleHasFeature(api.CoarseCurrent) { - chargeCurrent = math.Trunc(chargeCurrent) - } - - // set current - if chargeCurrent != lp.chargeCurrent && chargeCurrent >= lp.GetMinCurrent() { - var err error - // if charger, ok := lp.charger.(api.ChargerEx); ok { - // err = charger.MaxCurrentMillis(chargeCurrent) - // } else { - // err = lp.charger.MaxCurrent(int64(chargeCurrent)) - // } - +// func (lp *Loadpoint) setLimit(chargeCurrent float64, force bool) error { +// // full amps only? +// if _, ok := lp.charger.(api.ChargerEx); !ok || lp.vehicleHasFeature(api.CoarseCurrent) { +// chargeCurrent = math.Trunc(chargeCurrent) +// } + +// // set current +// if chargeCurrent != lp.chargeCurrent && chargeCurrent >= lp.GetMinCurrent() { +// var err error +// if charger, ok := lp.charger.(api.ChargerEx); ok { +// err = charger.MaxCurrentMillis(chargeCurrent) +// } else { +// err = lp.charger.MaxCurrent(int64(chargeCurrent)) +// } + +// if err != nil { +// return fmt.Errorf("max charge current %.3gA: %w", chargeCurrent, err) +// } + +// lp.log.DEBUG.Printf("max charge current: %.3gA", chargeCurrent) +// lp.chargeCurrent = chargeCurrent +// lp.bus.Publish(evChargeCurrent, chargeCurrent) +// } + +// // set enabled/disabled +// if enabled := chargeCurrent >= lp.GetMinCurrent(); enabled != lp.enabled { +// if remaining := (lp.GuardDuration - lp.clock.Since(lp.guardUpdated)).Truncate(time.Second); remaining > 0 && !force { +// lp.publishTimer(guardTimer, lp.GuardDuration, guardEnable) +// return nil +// } +// lp.elapseGuard() + +// // remote stop +// // TODO https://github.com/evcc-io/evcc/discussions/1929 +// // if car, ok := lp.vehicle.(api.VehicleChargeController); !enabled && ok { +// // // log but don't propagate +// // if err := car.StopCharge(); err != nil { +// // lp.log.ERROR.Printf("vehicle remote charge stop: %v", err) +// // } +// // } + +// if err := lp.charger.Enable(enabled); err != nil { +// return fmt.Errorf("charger %s: %w", status[enabled], err) +// } + +// lp.log.DEBUG.Printf("charger %s", status[enabled]) +// lp.enabled = enabled +// lp.guardUpdated = lp.clock.Now() + +// lp.bus.Publish(evChargeCurrent, chargeCurrent) + +// // start/stop vehicle wake-up timer +// if enabled { +// lp.startWakeUpTimer() +// } else { +// lp.stopWakeUpTimer() +// } + +// // remote start +// // TODO https://github.com/evcc-io/evcc/discussions/1929 +// // if car, ok := lp.vehicle.(api.VehicleChargeController); enabled && ok { +// // // log but don't propagate +// // if err := car.StartCharge(); err != nil { +// // lp.log.ERROR.Printf("vehicle remote charge start: %v", err) +// // } +// // } +// } + +// return nil +// } + +// setLimit applies charger power limits and enables/disables accordingly +func (lp *Loadpoint) setPowerLimit(powerLimit float64, force bool) error { + minPower := lp.GetMinPower() + + // set power if controllable + if lp.chargeController != nil && powerLimit != lp.maxPower && powerLimit >= minPower { + actualPowerLimit, err := lp.chargeController.MaxPower(powerLimit) if err != nil { v := lp.GetVehicle() if vv, ok := v.(api.Resurrector); ok && errors.Is(err, api.ErrAsleep) { @@ -725,21 +821,22 @@ func (lp *Loadpoint) setLimit(chargeCurrent float64, force bool) error { } } - return fmt.Errorf("max charge current %.3gA: %w", chargeCurrent, err) + return fmt.Errorf("max charge power %.1fW: %w", powerLimit, err) } - lp.log.DEBUG.Printf("max charge current: %.3gA", chargeCurrent) - lp.chargeCurrent = chargeCurrent - lp.bus.Publish(evChargeCurrent, chargeCurrent) + lp.maxPower = actualPowerLimit + lp.log.DEBUG.Printf("max charge power: %.1fW (-> %.1fW)", powerLimit, lp.maxPower) + lp.bus.Publish(evChargePower, lp.maxPower) + + return nil } // set enabled/disabled - if enabled := chargeCurrent >= lp.GetMinCurrent(); enabled != lp.enabled { + if enabled := powerLimit >= minPower; enabled != lp.enabled { if remaining := (lp.GuardDuration - lp.clock.Since(lp.guardUpdated)).Truncate(time.Second); remaining > 0 && !force { lp.publishTimer(guardTimer, lp.GuardDuration, guardEnable) return nil } - lp.elapseGuard() if err := lp.charger.Enable(enabled); err != nil { v := lp.GetVehicle() @@ -755,12 +852,12 @@ func (lp *Loadpoint) setLimit(chargeCurrent float64, force bool) error { return fmt.Errorf("charger %s: %w", status[enabled], err) } - lp.log.DEBUG.Printf("charger %s", status[enabled]) lp.enabled = enabled lp.publish("enabled", lp.enabled) + lp.log.DEBUG.Printf("charger %s", status[enabled]) lp.guardUpdated = lp.clock.Now() - lp.bus.Publish(evChargeCurrent, chargeCurrent) + lp.bus.Publish(evChargePower, powerLimit) // start/stop vehicle wake-up timer if enabled { @@ -838,15 +935,15 @@ func (lp *Loadpoint) minSocNotReached() bool { // disableUnlessClimater disables the charger unless climate is active func (lp *Loadpoint) disableUnlessClimater() error { - var current float64 // zero disables + var power float64 // zero disables if lp.vehicleClimateActive() { - current = lp.GetMinCurrent() + power = lp.GetMinPower() } // reset plan once charge goal is met lp.setPlanActive(false) - return lp.setLimit(current, true) + return lp.setPowerLimit(power, true) } // remoteControlled returns true if remote control status is active @@ -911,26 +1008,27 @@ func (lp *Loadpoint) updateChargerStatus() error { } // update whenever there is a state change - lp.bus.Publish(evChargeCurrent, lp.chargeCurrent) + // TODO add test case + lp.bus.Publish(evChargePower, lp.maxPower) } return nil } // effectiveCurrent returns the currently effective charging current -func (lp *Loadpoint) effectiveCurrent() float64 { - if !lp.charging() { - return 0 - } +// func (lp *Loadpoint) effectiveCurrent() float64 { +// if !lp.charging() { +// return 0 +// } - // adjust actual current for vehicles like Zoe where it remains below target - if lp.chargeCurrents != nil { - cur := max(lp.chargeCurrents[0], lp.chargeCurrents[1], lp.chargeCurrents[2]) - return min(cur+2.0, lp.chargeCurrent) - } +// // adjust actual current for vehicles like Zoe where it remains below target +// if lp.chargeCurrents != nil { +// cur := max(lp.chargeCurrents[0], lp.chargeCurrents[1], lp.chargeCurrents[2]) +// return min(cur+2.0, lp.chargeCurrent) +// } - return lp.chargeCurrent -} +// return lp.chargeCurrent +// } // elapsePVTimer puts the pv enable/disable timer into elapsed state func (lp *Loadpoint) elapsePVTimer() { @@ -962,156 +1060,155 @@ func (lp *Loadpoint) resetPVTimer(typ ...string) { lp.publishTimer(pvTimer, 0, timerInactive) } -// resetPhaseTimer resets the phase switch timer to disabled state -func (lp *Loadpoint) resetPhaseTimer() { - if lp.phaseTimer.IsZero() { - return - } - - lp.phaseTimer = time.Time{} - lp.publishTimer(phaseTimer, 0, timerInactive) -} - -// scalePhasesRequired validates if fixed phase configuration matches enabled phases -func (lp *Loadpoint) scalePhasesRequired() bool { - _, ok := lp.charger.(api.PhaseSwitcher) - return ok && lp.ConfiguredPhases != 0 && lp.ConfiguredPhases != lp.GetPhases() -} - -// scalePhasesIfAvailable scales if api.PhaseSwitcher is available -func (lp *Loadpoint) scalePhasesIfAvailable(phases int) error { - want := phases - if lp.ConfiguredPhases != 0 { - phases = lp.ConfiguredPhases - } - - if _, ok := lp.charger.(api.PhaseSwitcher); ok { - lp.log.DEBUG.Printf("!! scalePhasesIfAvailable: %dp -> %dp", want, phases) - return lp.scalePhases(phases) - } - - return nil -} - -// scalePhases adjusts the number of active phases and returns the appropriate charging current. -// Returns api.ErrNotAvailable if api.PhaseSwitcher is not available. -func (lp *Loadpoint) scalePhases(phases int) error { - cp, ok := lp.charger.(api.PhaseSwitcher) - if !ok { - panic("charger does not implement api.PhaseSwitcher") - } - - lp.log.DEBUG.Printf("!! scalePhases: GetPhases %dp <> phases %dp", lp.GetPhases(), phases) - if lp.GetPhases() != phases { - // switch phases - if err := cp.Phases1p3p(phases); err != nil { - return fmt.Errorf("switch phases: %w", err) - } - - // prevent premature measurement of active phases - lp.phasesSwitched = lp.clock.Now() - - // update setting and reset timer - lp.setPhases(phases) - } - - return nil -} +// // resetPhaseTimer resets the phase switch timer to disabled state +// func (lp *Loadpoint) resetPhaseTimer() { +// if lp.phaseTimer.IsZero() { +// return +// } + +// lp.phaseTimer = time.Time{} +// lp.publishTimer(phaseTimer, 0, timerInactive) +// } + +// // scalePhasesRequired validates if fixed phase configuration matches enabled phases +// func (lp *Loadpoint) scalePhasesRequired() bool { +// _, ok := lp.charger.(api.PhaseSwitcher) +// return ok && lp.ConfiguredPhases != 0 && lp.ConfiguredPhases != lp.GetPhases() +// } + +// // scalePhasesIfAvailable scales if api.PhaseSwitcher is available +// func (lp *Loadpoint) scalePhasesIfAvailable(phases int) error { +// if lp.ConfiguredPhases != 0 { +// phases = lp.ConfiguredPhases +// } + +// if _, ok := lp.charger.(api.PhaseSwitcher); ok { +// return lp.scalePhases(phases) +// } + +// return nil +// } + +// // scalePhases adjusts the number of active phases and returns the appropriate charging current. +// // Returns api.ErrNotAvailable if api.PhaseSwitcher is not available. +// func (lp *Loadpoint) scalePhases(phases int) error { +// cp, ok := lp.charger.(api.PhaseSwitcher) +// if !ok { +// panic("charger does not implement api.PhaseSwitcher") +// } + +// if lp.GetPhases() != phases { +// // switch phases +// if err := cp.Phases1p3p(phases); err != nil { +// return fmt.Errorf("switch phases: %w", err) +// } + +// // prevent premature measurement of active phases +// lp.phasesSwitched = lp.clock.Now() + +// // update setting and reset timer +// lp.setPhases(phases) +// } + +// return nil +// } // fastCharging scales to 3p if available and sets maximum current +// TODO handle nil powerController func (lp *Loadpoint) fastCharging() error { - err := lp.scalePhasesIfAvailable(3) - if err == nil { - err = lp.setLimit(lp.GetMaxCurrent(), true) - } - return err + // if err := lp.scalePhasesIfAvailable(3); err != nil { + // return err + // } + + // TODO differentiate between controller types + return lp.setPowerLimit(lp.GetMaxPower(), true) } // pvScalePhases switches phases if necessary and returns if switch occurred -func (lp *Loadpoint) pvScalePhases(availablePower, minCurrent, maxCurrent float64) bool { - phases := lp.GetPhases() - - // observed phase state inconsistency - // - https://github.com/evcc-io/evcc/issues/1572 - // - https://github.com/evcc-io/evcc/issues/2230 - // - https://github.com/evcc-io/evcc/issues/2613 - measuredPhases := lp.getMeasuredPhases() - if phases > 0 && phases < measuredPhases { - if lp.guardGracePeriodElapsed() { - lp.log.WARN.Printf("ignoring inconsistent phases: %dp < %dp observed active", phases, measuredPhases) - } - lp.resetMeasuredPhases() - } - - var waiting bool - activePhases := lp.activePhases() - - // scale down phases - if targetCurrent := powerToCurrent(availablePower, activePhases); targetCurrent < minCurrent && activePhases > 1 && lp.ConfiguredPhases < 3 { - lp.log.DEBUG.Printf("available power %.0fW < %.0fW min %dp threshold", availablePower, float64(activePhases)*Voltage*minCurrent, activePhases) - - if lp.phaseTimer.IsZero() { - lp.log.DEBUG.Printf("start phase %s timer", phaseScale1p) - lp.phaseTimer = lp.clock.Now() - } - - lp.publishTimer(phaseTimer, lp.Disable.Delay, phaseScale1p) - - if elapsed := lp.clock.Since(lp.phaseTimer); elapsed >= lp.Disable.Delay { - lp.log.DEBUG.Printf("phase %s timer elapsed", phaseScale1p) - if err := lp.scalePhases(1); err == nil { - lp.log.DEBUG.Printf("switched phases: 1p @ %.0fW", availablePower) - } else { - lp.log.ERROR.Println(err) - } - return true - } - - waiting = true - } - - maxPhases := lp.maxActivePhases() - target1pCurrent := powerToCurrent(availablePower, 1) - scalable := maxPhases > 1 && phases < maxPhases && target1pCurrent > maxCurrent - - // scale up phases - if targetCurrent := powerToCurrent(availablePower, maxPhases); targetCurrent >= minCurrent && scalable { - lp.log.DEBUG.Printf("available power %.0fW > %.0fW min %dp threshold", availablePower, 3*Voltage*minCurrent, maxPhases) - - if lp.phaseTimer.IsZero() { - lp.log.DEBUG.Printf("start phase %s timer", phaseScale3p) - lp.phaseTimer = lp.clock.Now() - } - - lp.publishTimer(phaseTimer, lp.Enable.Delay, phaseScale3p) - - if elapsed := lp.clock.Since(lp.phaseTimer); elapsed >= lp.Enable.Delay { - lp.log.DEBUG.Printf("phase %s timer elapsed", phaseScale3p) - if err := lp.scalePhases(3); err == nil { - lp.log.DEBUG.Printf("switched phases: 3p @ %.0fW", availablePower) - } else { - lp.log.ERROR.Println(err) - } - return true - } - - waiting = true - } - - // reset timer to disabled state - if !waiting && !lp.phaseTimer.IsZero() { - lp.resetPhaseTimer() - } - - return false -} +// func (lp *Loadpoint) pvScalePhases(availablePower, minCurrent, maxCurrent float64) bool { +// phases := lp.GetPhases() + +// // observed phase state inconsistency +// // - https://github.com/evcc-io/evcc/issues/1572 +// // - https://github.com/evcc-io/evcc/issues/2230 +// // - https://github.com/evcc-io/evcc/issues/2613 +// measuredPhases := lp.getMeasuredPhases() +// if phases > 0 && phases < measuredPhases { +// if lp.guardGracePeriodElapsed() { +// lp.log.WARN.Printf("ignoring inconsistent phases: %dp < %dp observed active", phases, measuredPhases) +// } +// lp.resetMeasuredPhases() +// } + +// var waiting bool +// activePhases := lp.activePhases() + +// // scale down phases +// if targetCurrent := powerToCurrent(availablePower, activePhases); targetCurrent < minCurrent && activePhases > 1 && lp.ConfiguredPhases < 3 { +// lp.log.DEBUG.Printf("available power %.0fW < %.0fW min %dp threshold", availablePower, float64(activePhases)*Voltage*minCurrent, activePhases) + +// if lp.phaseTimer.IsZero() { +// lp.log.DEBUG.Printf("start phase %s timer", phaseScale1p) +// lp.phaseTimer = lp.clock.Now() +// } + +// lp.publishTimer(phaseTimer, lp.Disable.Delay, phaseScale1p) + +// if elapsed := lp.clock.Since(lp.phaseTimer); elapsed >= lp.Disable.Delay { +// lp.log.DEBUG.Printf("phase %s timer elapsed", phaseScale1p) +// if err := lp.scalePhases(1); err == nil { +// lp.log.DEBUG.Printf("switched phases: 1p @ %.0fW", availablePower) +// } else { +// lp.log.ERROR.Println(err) +// } +// return true +// } + +// waiting = true +// } + +// maxPhases := lp.maxActivePhases() +// target1pCurrent := powerToCurrent(availablePower, 1) +// scalable := maxPhases > 1 && phases < maxPhases && target1pCurrent > maxCurrent + +// // scale up phases +// if targetCurrent := powerToCurrent(availablePower, maxPhases); targetCurrent >= minCurrent && scalable { +// lp.log.DEBUG.Printf("available power %.0fW > %.0fW min %dp threshold", availablePower, 3*Voltage*minCurrent, maxPhases) + +// if lp.phaseTimer.IsZero() { +// lp.log.DEBUG.Printf("start phase %s timer", phaseScale3p) +// lp.phaseTimer = lp.clock.Now() +// } + +// lp.publishTimer(phaseTimer, lp.Enable.Delay, phaseScale3p) + +// if elapsed := lp.clock.Since(lp.phaseTimer); elapsed >= lp.Enable.Delay { +// lp.log.DEBUG.Printf("phase %s timer elapsed", phaseScale3p) +// if err := lp.scalePhases(3); err == nil { +// lp.log.DEBUG.Printf("switched phases: 3p @ %.0fW", availablePower) +// } else { +// lp.log.ERROR.Println(err) +// } +// return true +// } + +// waiting = true +// } + +// // reset timer to disabled state +// if !waiting && !lp.phaseTimer.IsZero() { +// lp.resetPhaseTimer() +// } + +// return false +// } // TODO move up to timer functions func (lp *Loadpoint) publishTimer(name string, delay time.Duration, action string) { timer := lp.pvTimer - if name == phaseTimer { - timer = lp.phaseTimer - } + // if name == phaseTimer { + // timer = lp.phaseTimer + // } if name == guardTimer { timer = lp.guardUpdated } @@ -1132,33 +1229,131 @@ func (lp *Loadpoint) publishTimer(name string, delay time.Duration, action strin } // pvMaxCurrent calculates the maximum target current for PV mode -func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64, batteryBuffered, batteryStart bool) float64 { +// func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64, batteryBuffered, batteryStart bool) float64 { +// // read only once to simplify testing +// minCurrent := lp.GetMinCurrent() +// maxCurrent := lp.GetMaxCurrent() + +// // switch phases up/down +// if _, ok := lp.charger.(api.PhaseSwitcher); ok { +// availablePower := -sitePower + lp.chargePower + +// // in case of scaling, keep charger disabled for this cycle +// if lp.pvScalePhases(availablePower, minCurrent, maxCurrent) { +// return 0 +// } +// } + +// // calculate target charge current from delta power and actual current +// effectiveCurrent := lp.effectiveCurrent() +// activePhases := lp.activePhases() +// deltaCurrent := powerToCurrent(-sitePower, activePhases) +// targetCurrent := math.Max(effectiveCurrent+deltaCurrent, 0) + +// lp.log.DEBUG.Printf("pv charge current: %.3gA = %.3gA + %.3gA (%.0fW @ %dp)", targetCurrent, effectiveCurrent, deltaCurrent, sitePower, activePhases) + +// // in MinPV mode or under special conditions return at least minCurrent +// if (mode == api.ModeMinPV || batteryBuffered && lp.charging()) && targetCurrent < minCurrent { +// return minCurrent +// } + +// if mode == api.ModePV && lp.enabled && targetCurrent < minCurrent { +// // kick off disable sequence +// if sitePower >= lp.Disable.Threshold && lp.phaseTimer.IsZero() { +// lp.log.DEBUG.Printf("site power %.0fW >= %.0fW disable threshold", sitePower, lp.Disable.Threshold) + +// if lp.pvTimer.IsZero() { +// lp.log.DEBUG.Printf("pv disable timer start: %v", lp.Disable.Delay) +// lp.pvTimer = lp.clock.Now() +// } + +// lp.publishTimer(pvTimer, lp.Disable.Delay, pvDisable) + +// elapsed := lp.clock.Since(lp.pvTimer) +// if elapsed >= lp.Disable.Delay { +// lp.log.DEBUG.Println("pv disable timer elapsed") +// return 0 +// } + +// // suppress duplicate log message after timer started +// if elapsed > time.Second { +// lp.log.DEBUG.Printf("pv disable timer remaining: %v", (lp.Disable.Delay - elapsed).Round(time.Second)) +// } +// } else { +// // reset timer +// lp.resetPVTimer("disable") +// } + +// // lp.log.DEBUG.Println("pv disable timer: keep enabled") +// return minCurrent +// } + +// if mode == api.ModePV && !lp.enabled { +// // kick off enable sequence +// if (lp.Enable.Threshold == 0 && targetCurrent >= minCurrent) || +// (lp.Enable.Threshold != 0 && sitePower <= lp.Enable.Threshold) { +// lp.log.DEBUG.Printf("site power %.0fW <= %.0fW enable threshold", sitePower, lp.Enable.Threshold) + +// if lp.pvTimer.IsZero() { +// lp.log.DEBUG.Printf("pv enable timer start: %v", lp.Enable.Delay) +// lp.pvTimer = lp.clock.Now() +// } + +// lp.publishTimer(pvTimer, lp.Enable.Delay, pvEnable) + +// elapsed := lp.clock.Since(lp.pvTimer) +// if elapsed >= lp.Enable.Delay { +// lp.log.DEBUG.Println("pv enable timer elapsed") +// return minCurrent +// } + +// // suppress duplicate log message after timer started +// if elapsed > time.Second { +// lp.log.DEBUG.Printf("pv enable timer remaining: %v", (lp.Enable.Delay - elapsed).Round(time.Second)) +// } +// } else { +// // reset timer +// lp.resetPVTimer("enable") +// } + +// // lp.log.DEBUG.Println("pv enable timer: keep disabled") +// return 0 +// } + +// // reset timer to disabled state +// lp.resetPVTimer() + +// // cap at maximum current +// targetCurrent = math.Min(targetCurrent, maxCurrent) + +// return targetCurrent +// } + +// pvMaxPower calculates the maximum power for PV mode +func (lp *Loadpoint) pvMaxPower(mode api.ChargeMode, sitePower float64, batteryBuffered, batteryStart bool) float64 { // read only once to simplify testing - minCurrent := lp.GetMinCurrent() - maxCurrent := lp.GetMaxCurrent() + minPower := lp.GetMinPower() + maxPower := lp.GetMaxPower() // switch phases up/down - if _, ok := lp.charger.(api.PhaseSwitcher); ok { - availablePower := -sitePower + lp.chargePower - _ = lp.pvScalePhases(availablePower, minCurrent, maxCurrent) - } + // if _, ok := lp.charger.(api.PhaseSwitcher); ok { + // availablePower := -sitePower + lp.chargePower + // _ = lp.pvScalePhases(availablePower, minCurrent, maxCurrent) + // } - // calculate target charge current from delta power and actual current - effectiveCurrent := lp.effectiveCurrent() - activePhases := lp.activePhases() - deltaCurrent := powerToCurrent(-sitePower, activePhases) - targetCurrent := max(effectiveCurrent+deltaCurrent, 0) + targetPower := lp.maxPower - sitePower - lp.log.DEBUG.Printf("pv charge current: %.3gA = %.3gA + %.3gA (%.0fW @ %dp)", targetCurrent, effectiveCurrent, deltaCurrent, sitePower, activePhases) + lp.log.DEBUG.Printf("pv charge power: %.1fW = %.1fW + %.1fW (%.0fW @ %dp)", targetPower, lp.chargePower, sitePower) - // in MinPV mode or under special conditions return at least minCurrent - if (mode == api.ModeMinPV || batteryStart || batteryBuffered && lp.charging()) && targetCurrent < minCurrent { - return minCurrent + // in MinPV mode or under special conditions return at least minPower + if (mode == api.ModeMinPV || batteryBuffered && lp.charging()) && targetPower < minPower { + return minPower } - if mode == api.ModePV && lp.enabled && targetCurrent < minCurrent { + if mode == api.ModePV && lp.enabled && targetPower < minPower { // kick off disable sequence - if sitePower >= lp.Disable.Threshold && lp.phaseTimer.IsZero() { + // TODO && lp.phaseTimer.IsZero() + if sitePower >= lp.Disable.Threshold { lp.log.DEBUG.Printf("site power %.0fW >= %.0fW disable threshold", sitePower, lp.Disable.Threshold) if lp.pvTimer.IsZero() { @@ -1184,12 +1379,12 @@ func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64, batter } // lp.log.DEBUG.Println("pv disable timer: keep enabled") - return minCurrent + return minPower } if mode == api.ModePV && !lp.enabled { // kick off enable sequence - if (lp.Enable.Threshold == 0 && targetCurrent >= minCurrent) || + if (lp.Enable.Threshold == 0 && targetPower >= minPower) || (lp.Enable.Threshold != 0 && sitePower <= lp.Enable.Threshold) { lp.log.DEBUG.Printf("site power %.0fW <= %.0fW enable threshold", sitePower, lp.Enable.Threshold) @@ -1203,7 +1398,7 @@ func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64, batter elapsed := lp.clock.Since(lp.pvTimer) if elapsed >= lp.Enable.Delay { lp.log.DEBUG.Println("pv enable timer elapsed") - return minCurrent + return minPower } // suppress duplicate log message after timer started @@ -1222,10 +1417,10 @@ func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64, batter // reset timer to disabled state lp.resetPVTimer() - // cap at maximum current - targetCurrent = min(targetCurrent, maxCurrent) + // cap at maximum Power + targetPower = math.Min(targetPower, maxPower) - return targetCurrent + return targetPower } // UpdateChargePower updates charge meter power @@ -1505,7 +1700,7 @@ func (lp *Loadpoint) Update(sitePower float64, autoCharge, batteryBuffered, batt lp.sessionEnergy.SetEnvironment(greenShare, effPrice, effCo2) // update ChargeRater here to make sure initial meter update is caught - lp.bus.Publish(evChargeCurrent, lp.chargeCurrent) + // lp.bus.Publish(evChargeCurrent, lp.chargeCurrent) lp.bus.Publish(evChargePower, lp.chargePower) // update progress and soc before status is updated @@ -1555,12 +1750,12 @@ func (lp *Loadpoint) Update(sitePower float64, autoCharge, batteryBuffered, batt case !lp.connected(): // always disable charger if not connected // https://github.com/evcc-io/evcc/issues/105 - err = lp.setLimit(0, false) + err = lp.setPowerLimit(0, false) - case lp.scalePhasesRequired(): - if err = lp.scalePhases(lp.ConfiguredPhases); err == nil { - lp.log.DEBUG.Printf("switched phases: %dp", lp.ConfiguredPhases) - } + // case lp.scalePhasesRequired(): + // if err = lp.scalePhases(lp.ConfiguredPhases); err == nil { + // lp.log.DEBUG.Printf("switched phases: %dp", lp.ConfiguredPhases) + // } case lp.targetEnergyReached(): lp.log.DEBUG.Printf("targetEnergy reached: %.0fkWh > %0.1fkWh", lp.getChargedEnergy()/1e3, lp.targetEnergy) @@ -1575,7 +1770,7 @@ func (lp *Loadpoint) Update(sitePower float64, autoCharge, batteryBuffered, batt fallthrough case mode == api.ModeOff: - err = lp.setLimit(0, true) + err = lp.setPowerLimit(0, true) // immediate charging case mode == api.ModeNow: @@ -1584,34 +1779,34 @@ func (lp *Loadpoint) Update(sitePower float64, autoCharge, batteryBuffered, batt // minimum or target charging case lp.minSocNotReached() || lp.plannerActive(): err = lp.fastCharging() - lp.resetPhaseTimer() + // lp.resetPhaseTimer() lp.elapsePVTimer() // let PV mode disable immediately afterwards case mode == api.ModeMinPV || mode == api.ModePV: // cheap tariff if autoCharge && lp.GetTargetTime().IsZero() { err = lp.fastCharging() - lp.resetPhaseTimer() + // lp.resetPhaseTimer() lp.elapsePVTimer() // let PV mode disable immediately afterwards break } - targetCurrent := lp.pvMaxCurrent(mode, sitePower, batteryBuffered, batteryStart) + maxPower := lp.pvMaxPower(mode, sitePower, batteryBuffered, batteryStart) - var required bool // false - if targetCurrent == 0 && lp.vehicleClimateActive() { - targetCurrent = lp.GetMinCurrent() - required = true + var force bool // false + if maxPower == 0 && lp.vehicleClimateActive() { + maxPower = lp.GetMinPower() + force = true } // Sunny Home Manager if lp.remoteControlled(loadpoint.RemoteSoftDisable) { remoteDisabled = loadpoint.RemoteSoftDisable - targetCurrent = 0 - required = true + maxPower = 0 + force = true } - err = lp.setLimit(targetCurrent, required) + err = lp.setPowerLimit(maxPower, force) } // Wake-up checks diff --git a/core/loadpoint_api.go b/core/loadpoint_api.go index 0ef979abbc..f0b4ade52b 100644 --- a/core/loadpoint_api.go +++ b/core/loadpoint_api.go @@ -359,11 +359,13 @@ func (lp *Loadpoint) SetMaxCurrent(current float64) { } // GetMinPower returns the min loadpoint power for a single phase +// TODO differentiate between controller types func (lp *Loadpoint) GetMinPower() float64 { return Voltage * lp.GetMinCurrent() } // GetMaxPower returns the max loadpoint power taking vehicle capabilities and phase scaling into account +// TODO differentiate between controller types func (lp *Loadpoint) GetMaxPower() float64 { return Voltage * lp.GetMaxCurrent() * float64(lp.maxActivePhases()) } diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go index bb97743687..f7eafb5cb9 100644 --- a/core/loadpoint_test.go +++ b/core/loadpoint_test.go @@ -73,9 +73,12 @@ func attachListeners(t *testing.T, lp *Loadpoint) { if charger, ok := lp.charger.(*mock.MockCharger); ok && charger != nil { charger.EXPECT().Enabled().Return(true, nil) - charger.EXPECT().MaxCurrent(int64(lp.MinCurrent)).Return(nil) } + // if charger, ok := lp.chargeController.(*mock.MockCurrentController); ok && charger != nil { + // charger.EXPECT().MaxCurrent(int64(lp.MinCurrent)).Return(nil) + // } + uiChan, pushChan, lpChan := createChannels(t) lp.Prepare(uiChan, pushChan, lpChan) } From cf2b47abeab42dc64b8dabad47fe2a37d28156a6 Mon Sep 17 00:00:00 2001 From: andig Date: Mon, 10 Apr 2023 14:26:49 +0200 Subject: [PATCH 6/9] Minimise diff --- api/api.go | 20 ++++++++++---------- charger/twc3.go | 2 +- cmd/charger.go | 2 +- cmd/charger_ramp.go | 4 ++-- core/current_controller.go | 12 ++++++------ core/loadpoint.go | 8 ++++---- vehicle/tesla.go | 2 +- 7 files changed, 25 insertions(+), 25 deletions(-) diff --git a/api/api.go b/api/api.go index 8d100369b4..e6d4e8d3f0 100644 --- a/api/api.go +++ b/api/api.go @@ -126,8 +126,8 @@ type ChargeState interface { Status() (ChargeStatus, error) } -// CurrentController limits maximum charging current -type CurrentController interface { +// CurrentLimiter limits maximum charging current +type CurrentLimiter interface { MaxCurrent(current int64) error } @@ -141,8 +141,8 @@ type ChargerEx interface { MaxCurrentMillis(current float64) error } -// PowerController limits maximum charging power -type PowerController interface { +// PowerLimiter limits maximum charging power +type PowerLimiter interface { MaxPower(power float64) (float64, error) } @@ -153,16 +153,16 @@ type Charger interface { Enable(enable bool) error } -// CurrentControllable combines Charger with CurrentController -type CurrentControllable interface { +// CurrentController combines Charger with CurrentLimiter +type CurrentController interface { Charger - CurrentController + CurrentLimiter } -// PowerControllable combines Charger with PowerController -type PowerControllable interface { +// PowerController combines Charger with PowerLimiter +type PowerController interface { Charger - PowerController + PowerLimiter } // PhaseSwitcher provides 1p3p switching diff --git a/charger/twc3.go b/charger/twc3.go index 2b7c989b0a..c086ca9b08 100644 --- a/charger/twc3.go +++ b/charger/twc3.go @@ -130,7 +130,7 @@ func (c *Twc3) MaxCurrent(current int64) error { return errors.New("loadpoint not initialized") } - v, ok := c.lp.GetVehicle().(api.CurrentController) + v, ok := c.lp.GetVehicle().(api.CurrentLimiter) if !ok { return errors.New("vehicle not capable of current control") } diff --git a/cmd/charger.go b/cmd/charger.go index 0c9415febf..107f7acd58 100644 --- a/cmd/charger.go +++ b/cmd/charger.go @@ -76,7 +76,7 @@ func runCharger(cmd *cobra.Command, args []string) { if current != noCurrent { flagUsed = true - if vv, ok := v.(api.CurrentController); ok { + if vv, ok := v.(api.CurrentLimiter); ok { if err := vv.MaxCurrent(current); err != nil { log.ERROR.Println("set current:", err) } diff --git a/cmd/charger_ramp.go b/cmd/charger_ramp.go index 29df61313e..855cdbf68e 100644 --- a/cmd/charger_ramp.go +++ b/cmd/charger_ramp.go @@ -26,7 +26,7 @@ func init() { chargerRampCmd.Flags().StringP(flagDelay, "", "1s", "ramp delay") } -func ramp(c api.CurrentControllable, digits int, delay time.Duration) { +func ramp(c api.CurrentController, digits int, delay time.Duration) { steps := math.Pow10(digits) delta := 1 / steps @@ -97,7 +97,7 @@ func runChargerRamp(cmd *cobra.Command, args []string) { chargers := config.Chargers().Devices() for _, c := range config.Instances(chargers) { - cc, ok := c.(api.CurrentControllable) + cc, ok := c.(api.CurrentController) if !ok { log.ERROR.Fatalf("current limiter: not implemented") } diff --git a/core/current_controller.go b/core/current_controller.go index 4ec7e4d93e..2a78dd26c6 100644 --- a/core/current_controller.go +++ b/core/current_controller.go @@ -5,15 +5,15 @@ import ( "github.com/evcc-io/evcc/util" ) -type currentController struct { +type currentLimiter struct { log *util.Logger - charger api.CurrentController + charger api.CurrentLimiter minCurrent, maxCurrent float64 } -var _ api.PowerController = (*currentController)(nil) +var _ api.PowerLimiter = (*currentLimiter)(nil) -// currentController implements the api.PowerController interface -func (c *currentController) MaxPower(power float64) error { - return api.ErrNotAvailable +// currentLimiter implements the api.PowerLimiter interface +func (c *currentLimiter) MaxPower(power float64) (float64, error) { + return 0, api.ErrNotAvailable } diff --git a/core/loadpoint.go b/core/loadpoint.go index e91e1d9094..173aeef508 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -141,8 +141,8 @@ type Loadpoint struct { charger api.Charger chargeTimer api.ChargeTimer chargeRater api.ChargeRater - chargeController api.PowerController // Power controller (proxy for CurrentController) - chargedAtStartup float64 // session energy at startup + chargeController api.PowerLimiter // Power controller (proxy for CurrentController) + chargedAtStartup float64 // session energy at startup chargeMeter api.Meter // Charger usage meter vehicle api.Vehicle // Currently active vehicle @@ -340,8 +340,8 @@ func (lp *Loadpoint) configureChargerType(charger api.Charger) { var integrated bool switch c := charger.(type) { - case api.CurrentControllable: - lp.chargeController = ¤tController{ + case api.CurrentLimiter: + lp.chargeController = ¤tLimiter{ log: lp.log, charger: c, minCurrent: lp.MinCurrent, diff --git a/vehicle/tesla.go b/vehicle/tesla.go index cf739f846b..6b79294d96 100644 --- a/vehicle/tesla.go +++ b/vehicle/tesla.go @@ -195,7 +195,7 @@ func (v *Tesla) TargetSoc() (float64, error) { return float64(res.Response.ChargeState.ChargeLimitSoc), nil } -var _ api.CurrentController = (*Tesla)(nil) +var _ api.CurrentLimiter = (*Tesla)(nil) // StartCharge implements the api.VehicleChargeController interface func (v *Tesla) MaxCurrent(current int64) error { From 52ce67549302a1254036b0ffd68e39e9169dd026 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 13 Aug 2023 12:55:52 +0200 Subject: [PATCH 7/9] wip --- api/api.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/api/api.go b/api/api.go index e6d4e8d3f0..e5dbaa25dc 100644 --- a/api/api.go +++ b/api/api.go @@ -131,11 +131,7 @@ type CurrentLimiter interface { MaxCurrent(current int64) error } -// CurrentGetter provides getting charging maximum charging current for validation -type CurrentGetter interface { - GetMaxCurrent() (float64, error) -} - +// TODO rename to CurrentLimiterEx/MaxCurrentFloat // ChargerEx provides milli-amp precision charger current control type ChargerEx interface { MaxCurrentMillis(current float64) error @@ -146,6 +142,11 @@ type PowerLimiter interface { MaxPower(power float64) (float64, error) } +// CurrentGetter provides getting charging maximum charging current for validation +type CurrentGetter interface { + GetMaxCurrent() (float64, error) +} + // Charger provides current charging status and enable/disable charging type Charger interface { ChargeState From 80947407d64a1234b06b0b3e8c3ef0e67ef4498a Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 17 Sep 2023 14:19:15 +0200 Subject: [PATCH 8/9] wip --- core/loadpoint_api.go | 2 +- core/loadpoint_phases.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/loadpoint_api.go b/core/loadpoint_api.go index f0b4ade52b..131934fdb2 100644 --- a/core/loadpoint_api.go +++ b/core/loadpoint_api.go @@ -51,7 +51,7 @@ func (lp *Loadpoint) SetMode(mode api.ChargeMode) { // reset timers switch mode { case api.ModeNow, api.ModeOff: - lp.resetPhaseTimer() + // lp.resetPhaseTimer() lp.resetPVTimer() lp.setPlanActive(false) case api.ModeMinPV: diff --git a/core/loadpoint_phases.go b/core/loadpoint_phases.go index 16f9ce727f..2ed1786a3f 100644 --- a/core/loadpoint_phases.go +++ b/core/loadpoint_phases.go @@ -30,7 +30,7 @@ func (lp *Loadpoint) setPhases(phases int) { lp.publish(phasesEnabled, lp.phases) // reset timer to disabled state - lp.resetPhaseTimer() + // lp.resetPhaseTimer() // measure phases after switching lp.resetMeasuredPhases() From 527dc825a0365e7b87dee4b0fbd92b8f8a044ede Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 17 Sep 2023 14:29:38 +0200 Subject: [PATCH 9/9] wip --- core/current_controller.go | 10 +++++----- core/loadpoint.go | 22 +++++++++++----------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/core/current_controller.go b/core/current_controller.go index 2a78dd26c6..6fc7286af8 100644 --- a/core/current_controller.go +++ b/core/current_controller.go @@ -5,15 +5,15 @@ import ( "github.com/evcc-io/evcc/util" ) -type currentLimiter struct { +type currentAdapter struct { + api.CurrentController log *util.Logger - charger api.CurrentLimiter minCurrent, maxCurrent float64 } -var _ api.PowerLimiter = (*currentLimiter)(nil) +var _ api.PowerLimiter = (*currentAdapter)(nil) -// currentLimiter implements the api.PowerLimiter interface -func (c *currentLimiter) MaxPower(power float64) (float64, error) { +// currentAdapter implements the api.PowerLimiter interface +func (c *currentAdapter) MaxPower(power float64) (float64, error) { return 0, api.ErrNotAvailable } diff --git a/core/loadpoint.go b/core/loadpoint.go index 173aeef508..1f9d777bc3 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -141,7 +141,7 @@ type Loadpoint struct { charger api.Charger chargeTimer api.ChargeTimer chargeRater api.ChargeRater - chargeController api.PowerLimiter // Power controller (proxy for CurrentController) + powerLimiter api.PowerLimiter // Power controller (proxy for CurrentController) chargedAtStartup float64 // session energy at startup chargeMeter api.Meter // Charger usage meter @@ -340,15 +340,15 @@ func (lp *Loadpoint) configureChargerType(charger api.Charger) { var integrated bool switch c := charger.(type) { - case api.CurrentLimiter: - lp.chargeController = ¤tLimiter{ - log: lp.log, - charger: c, - minCurrent: lp.MinCurrent, - maxCurrent: lp.MaxCurrent, - } case api.PowerController: - lp.chargeController = c + lp.powerLimiter = c + case api.CurrentController: + lp.powerLimiter = ¤tAdapter{ + CurrentController: c, + log: lp.log, + minCurrent: lp.MinCurrent, + maxCurrent: lp.MaxCurrent, + } default: // TODO leave empty panic("charger does not implement api.(Power|Current)Controller") @@ -808,8 +808,8 @@ func (lp *Loadpoint) setPowerLimit(powerLimit float64, force bool) error { minPower := lp.GetMinPower() // set power if controllable - if lp.chargeController != nil && powerLimit != lp.maxPower && powerLimit >= minPower { - actualPowerLimit, err := lp.chargeController.MaxPower(powerLimit) + if lp.powerLimiter != nil && powerLimit != lp.maxPower && powerLimit >= minPower { + actualPowerLimit, err := lp.powerLimiter.MaxPower(powerLimit) if err != nil { v := lp.GetVehicle() if vv, ok := v.(api.Resurrector); ok && errors.Is(err, api.ErrAsleep) {