diff --git a/core/chargerhandler.go b/core/chargerhandler.go index d6bfe827b..cc74c22c7 100644 --- a/core/chargerhandler.go +++ b/core/chargerhandler.go @@ -11,6 +11,18 @@ import ( "github.com/benbjohnson/clock" ) +//go:generate mockgen -package mock -destination ../mock/mock_chargerhandler.go github.com/andig/evcc/core Handler + +// Handler is the charger handler responsible for enabled state, target current and guard durations +type Handler interface { + Prepare() + SyncEnabled() + Enabled() bool + Status() (api.ChargeStatus, error) + TargetCurrent() int64 + Ramp(int64, ...bool) error +} + // HandlerConfig contains the public configuration for the ChargerHandler type HandlerConfig struct { Sensitivity int64 // Step size of current change @@ -74,8 +86,8 @@ func (lp *ChargerHandler) Prepare() { lp.bus.Publish(evChargeCurrent, lp.MinCurrent) } -// SyncSettings synchronizes charger settings to expected state -func (lp *ChargerHandler) SyncSettings() { +// SyncEnabled synchronizes charger settings to expected state +func (lp *ChargerHandler) SyncEnabled() { enabled, err := lp.charger.Enabled() if err == nil && enabled != lp.enabled { lp.log.DEBUG.Printf("sync enabled state to %s", status[lp.enabled]) diff --git a/core/helper.go b/core/helper.go index 778749df4..bc976ca23 100644 --- a/core/helper.go +++ b/core/helper.go @@ -14,8 +14,13 @@ var ( ) // powerToCurrent is a helper function to convert power to per-phase current -func powerToCurrent(power, voltage float64, phases int64) int64 { - return int64(power / (float64(phases) * voltage)) +func powerToCurrent(power float64, phases int64) int64 { + return int64(power / (float64(phases) * Voltage)) +} + +// currentToPower is a helper function to convert phase current to total power +func currentToPower(current, phases int64) float64 { + return float64(current*phases) * Voltage } // consumedPower estimates how much power the charger might have consumed given it was the only load diff --git a/core/loadpoint.go b/core/loadpoint.go index dfaf521e1..689939c93 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -29,18 +29,6 @@ type ThresholdConfig struct { Threshold float64 } -//go:generate mockgen -package mock -destination ../mock/mock_chargerhandler.go github.com/andig/evcc/core Handler - -// Handler is the charger handler responsible for enabled state, target current and guard durations -type Handler interface { - Prepare() - SyncSettings() - Enabled() bool - Status() (api.ChargeStatus, error) - TargetCurrent() int64 - Ramp(int64, ...bool) error -} - // LoadPoint is responsible for controlling charge depending on // SoC needs and power availability. type LoadPoint struct { @@ -73,7 +61,6 @@ type LoadPoint struct { status api.ChargeStatus // Charger status charging bool // Charging cycle chargePower float64 // Charging power - sitePower float64 // Available power from site pvTimer time.Time } @@ -303,13 +290,11 @@ func (lp *LoadPoint) detectPhases() { } } -func (lp *LoadPoint) maxCurrent(mode api.ChargeMode) int64 { - // grid meter will always be available, if as wrapped pv meter - targetPower := lp.chargePower - lp.sitePower - lp.log.DEBUG.Printf("target power: %.0fW = %.0fW charge - %.0fW available", targetPower, lp.chargePower, lp.sitePower) - - // get max charge current - targetCurrent := clamp(powerToCurrent(targetPower, Voltage, lp.Phases), 0, lp.MaxCurrent) +// maxCurrent calculates the maximum target current for PV mode +func (lp *LoadPoint) maxCurrent(mode api.ChargeMode, sitePower float64) int64 { + // calculate target charge current from delta power and actual current + deltaCurrent := powerToCurrent(-sitePower, lp.Phases) + targetCurrent := clamp(lp.handler.TargetCurrent()+deltaCurrent, 0, lp.MaxCurrent) // in MinPV mode return at least minCurrent if mode == api.ModeMinPV && targetCurrent < lp.MinCurrent { @@ -332,8 +317,8 @@ func (lp *LoadPoint) maxCurrent(mode api.ChargeMode) int64 { if mode == api.ModePV && enabled && targetCurrent < lp.MinCurrent { // kick off disable sequence - if lp.sitePower >= lp.Disable.Threshold { - lp.log.DEBUG.Printf("site power %.0f >= disable threshold %.0f", lp.sitePower, lp.Disable.Threshold) + if sitePower >= lp.Disable.Threshold { + lp.log.DEBUG.Printf("site power %.0f >= disable threshold %.0f", sitePower, lp.Disable.Threshold) if lp.pvTimer.IsZero() { lp.log.DEBUG.Println("start pv disable timer") @@ -355,8 +340,8 @@ func (lp *LoadPoint) maxCurrent(mode api.ChargeMode) int64 { if mode == api.ModePV && !enabled { // kick off enable sequence if targetCurrent >= lp.MinCurrent || - (lp.Enable.Threshold != 0 && lp.sitePower <= lp.Enable.Threshold) { - lp.log.DEBUG.Printf("site power %.0f < enable threshold %.0f", lp.sitePower, lp.Enable.Threshold) + (lp.Enable.Threshold != 0 && sitePower <= lp.Enable.Threshold) { + lp.log.DEBUG.Printf("site power %.0f < enable threshold %.0f", sitePower, lp.Enable.Threshold) if lp.pvTimer.IsZero() { lp.log.DEBUG.Println("start pv enable timer") @@ -382,40 +367,25 @@ func (lp *LoadPoint) maxCurrent(mode api.ChargeMode) int64 { return targetCurrent } -// updateMeter updates and publishes single meter -func (lp *LoadPoint) updateMeter(name string, meter api.Meter, power *float64) error { - value, err := meter.CurrentPower() - if err != nil { - return err - } - - *power = value // update value if no error - - lp.log.DEBUG.Printf("%s power: %.1fW", name, *power) - lp.publish(name+"Power", *power) - - return nil -} - -// updateMeter updates and publishes single meter -func (lp *LoadPoint) updateMeters() (err error) { - retryMeter := func(s string, m api.Meter, f *float64) { - if m != nil { - e := retry.Do(func() error { - return lp.updateMeter(s, m, f) - }, retryOptions...) - - if e != nil { - err = errors.Wrapf(e, "updating %s meter", s) - lp.log.ERROR.Printf("%v", err) - } +// updateChargeMete updates and publishes single meter +func (lp *LoadPoint) updateChargeMeter() { + err := retry.Do(func() error { + value, err := lp.chargeMeter.CurrentPower() + if err != nil { + return err } + + lp.chargePower = value // update value if no error + lp.log.DEBUG.Printf("charge power: %.1fW", value) + lp.publish("chargePower", value) + + return nil + }, retryOptions...) + + if err != nil { + err = errors.Wrapf(err, "updating charge meter") + lp.log.ERROR.Printf("%v", err) } - - // read PV meter before charge meter - retryMeter("charge", lp.chargeMeter, &lp.chargePower) - - return err } // chargeDuration returns for how long the charge cycle has been running @@ -480,11 +450,9 @@ func (lp *LoadPoint) publishSoC() { } // Update is the main control function. It reevaluates meters and charger state -func (lp *LoadPoint) Update(mode api.ChargeMode, sitePower float64) float64 { +func (lp *LoadPoint) Update(mode api.ChargeMode, sitePower float64) { // read and publish meters first - _ = lp.updateMeters() - - lp.sitePower = sitePower + lp.updateChargeMeter() // update ChargeRater here to make sure initial meter update is caught lp.bus.Publish(evChargeCurrent, lp.handler.TargetCurrent()) @@ -497,7 +465,7 @@ func (lp *LoadPoint) Update(mode api.ChargeMode, sitePower float64) float64 { // read and publish status if err := retry.Do(lp.updateChargeStatus, retryOptions...); err != nil { lp.log.ERROR.Printf("charge controller error: %v", err) - return lp.chargePower + return } lp.publish("connected", lp.connected()) @@ -505,7 +473,7 @@ func (lp *LoadPoint) Update(mode api.ChargeMode, sitePower float64) float64 { // sync settings with charger if lp.status != api.StatusA { - lp.handler.SyncSettings() + lp.handler.SyncEnabled() } // phase detection - run only when actually charging @@ -530,7 +498,7 @@ func (lp *LoadPoint) Update(mode api.ChargeMode, sitePower float64) float64 { err = lp.handler.Ramp(current, true) case api.ModeMinPV, api.ModePV: - targetCurrent := lp.maxCurrent(mode) + targetCurrent := lp.maxCurrent(mode, sitePower) if !lp.connected() { // ensure minimum current when not connected // https://github.com/andig/evcc/issues/105 @@ -544,6 +512,4 @@ func (lp *LoadPoint) Update(mode api.ChargeMode, sitePower float64) float64 { if err != nil { lp.log.ERROR.Println(err) } - - return lp.chargePower } diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go index 127387e9e..1de8ca529 100644 --- a/core/loadpoint_test.go +++ b/core/loadpoint_test.go @@ -146,10 +146,14 @@ func TestUpdate(t *testing.T) { attachListeners(lp) handler.EXPECT().Status().Return(tc.status, nil) - handler.EXPECT().TargetCurrent().Return(int64(-1)) + handler.EXPECT().TargetCurrent().Return(int64(0)) if tc.status != api.StatusA { - handler.EXPECT().SyncSettings() + handler.EXPECT().SyncEnabled() + } + + if tc.mode == api.ModeMinPV || tc.mode == api.ModePV { + handler.EXPECT().TargetCurrent().Return(int64(0)) } if tc.expect != nil { @@ -326,11 +330,12 @@ func TestPVHysteresisForStatusC(t *testing.T) { for step, se := range tc.series { clck.Set(start.Add(se.delay)) - lp.sitePower = se.site - // maxCurrent will read enabled state in PV mode + // maxCurrent will read actual current and enabled state in PV mode + handler.EXPECT().TargetCurrent().Return(int64(0)) handler.EXPECT().Enabled().Return(tc.enabled) - current := lp.maxCurrent(api.ModePV) + + current := lp.maxCurrent(api.ModePV, se.site) if current != se.current { t.Errorf("step %d: wanted %d, got %d", step, se.current, current) @@ -342,7 +347,6 @@ func TestPVHysteresisForStatusC(t *testing.T) { } func TestPVHysteresisForStatusOtherThanC(t *testing.T) { - clck := clock.NewMock() ctrl := gomock.NewController(t) handler := mock.NewMockHandler(ctrl) @@ -362,9 +366,12 @@ func TestPVHysteresisForStatusOtherThanC(t *testing.T) { // not connected, test PV mode logic short-circuited lp.status = api.StatusA + // maxCurrent will read actual current in PV mode + handler.EXPECT().TargetCurrent().Return(int64(0)) + // maxCurrent will read enabled state in PV mode - lp.sitePower = -float64(minA*lp.Phases)*Voltage + 1 // 1W below min power - current := lp.maxCurrent(api.ModePV) + sitePower := -float64(minA*lp.Phases)*Voltage + 1 // 1W below min power + current := lp.maxCurrent(api.ModePV, sitePower) if current != 0 { t.Errorf("PV mode could not disable charger as expected. Expected 0, got %d", current) diff --git a/core/site.go b/core/site.go index 636c40ccd..cfb709e2c 100644 --- a/core/site.go +++ b/core/site.go @@ -12,11 +12,11 @@ import ( "github.com/pkg/errors" ) -//go:generate mockgen -package mock -destination ../mock/mock_loadpoint.go github.com/andig/evcc/core LoadPointer +//go:generate mockgen -package mock -destination ../mock/mock_loadpoint.go github.com/andig/evcc/core Updater -// LoadPointer abstracts the LoadPoint implementation for testing -type LoadPointer interface { - Update(api.ChargeMode, float64) float64 +// Updater abstracts the LoadPoint implementation for testing +type Updater interface { + Update(api.ChargeMode, float64) } // Site is the main configuration container. A site can host multiple loadpoints. @@ -38,7 +38,7 @@ type Site struct { pvMeter api.Meter // PV generation meter batteryMeter api.Meter // Battery charging meter - loadpoints []LoadPointer // Loadpoints + loadpoints []*LoadPoint // Loadpoints // cached state gridPower float64 // Grid power @@ -136,11 +136,7 @@ func (lp *LoadPoint) hasChargeMeter() bool { // LoadPoints returns the array of associated loadpoints func (site *Site) LoadPoints() []*LoadPoint { - res := make([]*LoadPoint, 0, len(site.loadpoints)) - for _, lp := range site.loadpoints { - res = append(res, lp.(*LoadPoint)) - } - return res + return site.loadpoints } // Configuration returns meter configuration @@ -153,9 +149,7 @@ func (site *Site) Configuration() SiteConfiguration { BatteryMeter: site.batteryMeter != nil, } - for _, lptr := range site.loadpoints { - lp := lptr.(*LoadPoint) - + for _, lp := range site.loadpoints { lpc := LoadpointConfiguration{ Title: lp.Name(), Phases: lp.Phases, @@ -184,8 +178,7 @@ func (site *Site) DumpConfig() { site.log.INFO.Printf(" pv %s", presence[site.pvMeter != nil]) site.log.INFO.Printf(" battery %s", presence[site.batteryMeter != nil]) - for i, lptr := range site.loadpoints { - lp := lptr.(*LoadPoint) + for i, lp := range site.loadpoints { lp.log.INFO.Printf("loadpoint %d config:", i) lp.log.INFO.Printf(" vehicle %s", presence[lp.vehicle != nil]) @@ -302,30 +295,20 @@ func (site *Site) sitePower() (float64, error) { return sitePower, nil } -func (site *Site) update() error { +func (site *Site) update(lp Updater) { mode := site.GetMode() site.publish("mode", string(mode)) - sitePower, err := site.sitePower() - if err != nil { - return err + if sitePower, err := site.sitePower(); err == nil { + lp.Update(mode, sitePower) } - - for idx, lp := range site.loadpoints { - usedPower := lp.Update(mode, sitePower) - remainingPower := sitePower + usedPower - site.log.DEBUG.Printf("lp-%d remaining power: %.0fW = %.0fW - %.0fW", idx+1, remainingPower, sitePower, usedPower) - sitePower = remainingPower - } - - return nil } // Prepare attaches communication channels to site and loadpoints func (site *Site) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Event) { site.uiChan = uiChan - for id, loadPoint := range site.loadpoints { + for id, lp := range site.loadpoints { lpUIChan := make(chan util.Param) lpPushChan := make(chan push.Event) @@ -343,7 +326,16 @@ func (site *Site) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Event) } }(id) - loadPoint.(*LoadPoint).Prepare(lpUIChan, lpPushChan) + lp.Prepare(lpUIChan, lpPushChan) + } +} + +// loopLoadpoints keeps iterating across loadpoints sending the next to the given channel +func (site *Site) loopLoadpoints(next chan<- Updater) { + for { + for _, lp := range site.loadpoints { + next <- lp + } } } @@ -354,14 +346,17 @@ func (site *Site) Run(interval time.Duration) { ticker := time.NewTicker(interval) site.triggerChan <- struct{}{} // start immediately + loadpointChan := make(chan Updater) + go site.loopLoadpoints(loadpointChan) + for { select { case <-ticker.C: - if site.update() != nil { - site.triggerChan <- struct{}{} // restart immediately - } + site.update(<-loadpointChan) case <-site.triggerChan: - _ = site.update() + for range site.loadpoints { + site.update(<-loadpointChan) + } ticker.Stop() ticker = time.NewTicker(interval) } diff --git a/core/site_test.go b/core/site_test.go index b83893468..ad61b2fda 100644 --- a/core/site_test.go +++ b/core/site_test.go @@ -2,11 +2,6 @@ package core import ( "testing" - - "github.com/andig/evcc/api" - "github.com/andig/evcc/mock" - "github.com/andig/evcc/util" - "github.com/golang/mock/gomock" ) func TestSitePower(t *testing.T) { @@ -29,32 +24,3 @@ func TestSitePower(t *testing.T) { } } } - -func TestSiteUpdateMultipleLoadpoints(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - sitePower := -1000.0 - lps := []float64{300.0, 400.0, 500.0} - - gm := mock.NewMockMeter(ctrl) - gm.EXPECT().CurrentPower().Return(sitePower, nil) - - site := &Site{ - log: util.NewLogger("foo"), - Mode: api.ModePV, - gridMeter: gm, - } - - availablePower := sitePower - - for _, usedPower := range lps { - lp := mock.NewMockLoadPointer(ctrl) - site.loadpoints = append(site.loadpoints, lp) - - lp.EXPECT().Update(api.ModePV, availablePower).Return(usedPower) - availablePower += usedPower - } - - _ = site.update() -} diff --git a/mock/mock_chargerhandler.go b/mock/mock_chargerhandler.go index 42e1060cc..c4216f1ad 100644 --- a/mock/mock_chargerhandler.go +++ b/mock/mock_chargerhandler.go @@ -93,16 +93,16 @@ func (mr *MockHandlerMockRecorder) Status() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Status", reflect.TypeOf((*MockHandler)(nil).Status)) } -// SyncSettings mocks base method -func (m *MockHandler) SyncSettings() { +// SyncEnabled mocks base method +func (m *MockHandler) SyncEnabled() { m.ctrl.T.Helper() - m.ctrl.Call(m, "SyncSettings") + m.ctrl.Call(m, "SyncEnabled") } -// SyncSettings indicates an expected call of SyncSettings -func (mr *MockHandlerMockRecorder) SyncSettings() *gomock.Call { +// SyncEnabled indicates an expected call of SyncEnabled +func (mr *MockHandlerMockRecorder) SyncEnabled() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SyncSettings", reflect.TypeOf((*MockHandler)(nil).SyncSettings)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SyncEnabled", reflect.TypeOf((*MockHandler)(nil).SyncEnabled)) } // TargetCurrent mocks base method diff --git a/mock/mock_loadpoint.go b/mock/mock_loadpoint.go index 75e60af71..4b369eebb 100644 --- a/mock/mock_loadpoint.go +++ b/mock/mock_loadpoint.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/andig/evcc/core (interfaces: LoadPointer) +// Source: github.com/andig/evcc/core (interfaces: Updater) // Package mock is a generated GoMock package. package mock @@ -10,39 +10,37 @@ import ( reflect "reflect" ) -// MockLoadPointer is a mock of LoadPointer interface -type MockLoadPointer struct { +// MockUpdater is a mock of Updater interface +type MockUpdater struct { ctrl *gomock.Controller - recorder *MockLoadPointerMockRecorder + recorder *MockUpdaterMockRecorder } -// MockLoadPointerMockRecorder is the mock recorder for MockLoadPointer -type MockLoadPointerMockRecorder struct { - mock *MockLoadPointer +// MockUpdaterMockRecorder is the mock recorder for MockUpdater +type MockUpdaterMockRecorder struct { + mock *MockUpdater } -// NewMockLoadPointer creates a new mock instance -func NewMockLoadPointer(ctrl *gomock.Controller) *MockLoadPointer { - mock := &MockLoadPointer{ctrl: ctrl} - mock.recorder = &MockLoadPointerMockRecorder{mock} +// NewMockUpdater creates a new mock instance +func NewMockUpdater(ctrl *gomock.Controller) *MockUpdater { + mock := &MockUpdater{ctrl: ctrl} + mock.recorder = &MockUpdaterMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use -func (m *MockLoadPointer) EXPECT() *MockLoadPointerMockRecorder { +func (m *MockUpdater) EXPECT() *MockUpdaterMockRecorder { return m.recorder } // Update mocks base method -func (m *MockLoadPointer) Update(arg0 api.ChargeMode, arg1 float64) float64 { +func (m *MockUpdater) Update(arg0 api.ChargeMode, arg1 float64) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Update", arg0, arg1) - ret0, _ := ret[0].(float64) - return ret0 + m.ctrl.Call(m, "Update", arg0, arg1) } // Update indicates an expected call of Update -func (mr *MockLoadPointerMockRecorder) Update(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockUpdaterMockRecorder) Update(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockLoadPointer)(nil).Update), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockUpdater)(nil).Update), arg0, arg1) } diff --git a/server/assets.go b/server/assets.go index b23fcf530..09b659cef 100644 --- a/server/assets.go +++ b/server/assets.go @@ -3625,47 +3625,46 @@ u5/Ve679nt1z7X+G+k8AAAD//9FsvQIdNQAA "/js/app.js": { name: "app.js", local: "../assets/js/app.js", - size: 7032, + size: 6792, modtime: 1566640112, compressed: ` -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 -Pz54GwAA +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 +JiAeF0hccFuj/x0AAP//sWHskogaAAA= `, },