From 5b1eb755d4b456f939f46b039524b9ff283e161a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20He=C3=9F?= Date: Mon, 29 Jan 2024 08:44:50 +0100 Subject: [PATCH] Loadpoint: publish guardTimer independently from pvTimer (#11926) --- core/loadpoint.go | 12 ++-- core/loadpoint_test.go | 132 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 135 insertions(+), 9 deletions(-) diff --git a/core/loadpoint.go b/core/loadpoint.go index ee70d4b1c..276fc2123 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -740,14 +740,14 @@ func (lp *Loadpoint) setLimit(chargeCurrent float64, force bool) error { lp.bus.Publish(evChargeCurrent, chargeCurrent) } + if lp.clock.Since(lp.guardUpdated).Truncate(time.Second) < lp.GuardDuration && !force { + lp.publishTimer(guardTimer, lp.GuardDuration, guardEnable) + return nil + } + lp.elapseGuard() + // set enabled/disabled if enabled := chargeCurrent >= lp.effectiveMinCurrent(); 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() if vv, ok := v.(api.Resurrector); enabled && ok && errors.Is(err, api.ErrAsleep) { diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go index aee70d13f..548920abf 100644 --- a/core/loadpoint_test.go +++ b/core/loadpoint_test.go @@ -10,6 +10,7 @@ import ( "github.com/evcc-io/evcc/core/soc" "github.com/evcc-io/evcc/push" "github.com/evcc-io/evcc/util" + "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" ) @@ -524,13 +525,13 @@ func cacheExpecter(t *testing.T, lp *Loadpoint) (*util.Cache, func(key string, v expect := func(key string, val interface{}) { time.Sleep(100 * time.Millisecond) // wait for cache to catch up + p := cache.Get(key) - t.Logf("%s: %.f", key, p.Val) // REMOVE + t.Logf("%s: %v", key, p.Val) // REMOVE if p.Val != val { - t.Errorf("%s wanted: %.0f, got %v", key, val, p.Val) + t.Errorf("%s wanted: %v, got %v", key, val, p.Val) } } - return cache, expect } @@ -791,3 +792,128 @@ func TestSocPoll(t *testing.T) { } } } + +// test guard timer is properly published during disable cycle +func TestGuardPublish(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + clock := clock.NewMock() + charger := api.NewMockCharger(ctrl) + rater := api.NewMockChargeRater(ctrl) + startPointInTime := clock.Now() + + lp := &Loadpoint{ + log: util.NewLogger("foo"), + bus: evbus.New(), + clock: clock, + charger: charger, + chargeMeter: &Null{}, // silence nil panics + chargeRater: rater, + chargeTimer: &Null{}, // silence nil panics + guardUpdated: startPointInTime, + pvTimer: time.Time{}, + wakeUpTimer: NewTimer(), + sessionEnergy: NewEnergyMetrics(), + minCurrent: minA, + maxCurrent: maxA, + chargeCurrent: maxA, + status: api.StatusC, + mode: api.ModePV, + enabled: true, + Disable: ThresholdConfig{Delay: 2 * time.Minute, Threshold: 0}, // t, W + GuardDuration: 3 * time.Minute, + } + + util.LogLevel("DEBUG", nil) + + attachListeners(t, lp) + // attach cache for verifying values + _, expectCache := cacheExpecter(t, lp) + + rater.EXPECT().ChargedEnergy().AnyTimes() + charger.EXPECT().Enabled().DoAndReturn(func() (bool, error) { return lp.enabled, nil }).AnyTimes() // just follow the loadpoint + charger.EXPECT().Status().Return(api.StatusC, nil).Times(4) + charger.EXPECT().Enable(false).Return(nil) + + tc := []struct { + step string + guardUpdates time.Time + guardTimerRemaining time.Duration + }{ + {"kick off pv disable timer", startPointInTime, 3 * time.Minute}, + {"continue pv disable timer", startPointInTime, 2 * time.Minute}, + {"disable charger prevented by guard", startPointInTime, 1 * time.Minute}, + {"guard elapse, publish new guard time", startPointInTime.Add(3 * time.Minute), time.Duration(0)}, + } + + for _, tc := range tc { + t.Logf("%+v", tc) + lp.Update(15000, false, false, false, 0, nil, nil) + assert.Equal(t, tc.guardUpdates, lp.guardUpdated) + expectCache(guardTimer+"Remaining", tc.guardTimerRemaining) + clock.Add(time.Minute) + } +} + +// test guard timer is properly published if charger disable sequence is aborted +func TestGuardPublishOnDisableAbort(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + clock := clock.NewMock() + charger := api.NewMockCharger(ctrl) + rater := api.NewMockChargeRater(ctrl) + startPointInTime := clock.Now() + + lp := &Loadpoint{ + log: util.NewLogger("foo"), + bus: evbus.New(), + clock: clock, + charger: charger, + chargeMeter: &Null{}, // silence nil panics + chargeRater: rater, + chargeTimer: &Null{}, // silence nil panics + guardUpdated: startPointInTime, + pvTimer: time.Time{}, + wakeUpTimer: NewTimer(), + sessionEnergy: NewEnergyMetrics(), + minCurrent: minA, + maxCurrent: maxA, + chargeCurrent: maxA, + status: api.StatusC, + mode: api.ModePV, + enabled: true, + Disable: ThresholdConfig{Delay: 2 * time.Minute, Threshold: 0}, // t, W + GuardDuration: 4 * time.Minute, + } + + util.LogLevel("DEBUG", nil) + + attachListeners(t, lp) + // attach cache for verifying values + _, expectCache := cacheExpecter(t, lp) + + rater.EXPECT().ChargedEnergy().AnyTimes() + charger.EXPECT().Enabled().DoAndReturn(func() (bool, error) { return lp.enabled, nil }).AnyTimes() // just follow the loadpoint + charger.EXPECT().Status().Return(api.StatusC, nil).Times(5) + + tc := []struct { + step string + sitePower float64 + guardUpdated time.Time + guardTimerRemaining time.Duration + }{ + {"kick off pv disable timer", 15000, startPointInTime, 4 * time.Minute}, + {"continue pv disable timer", 15000, startPointInTime, 3 * time.Minute}, + {"disable charger prevented by guard", 15000, startPointInTime, 2 * time.Minute}, + {"disable charger reset, pv power is back", -1, startPointInTime, 1 * time.Minute}, + {"guard elapsed, publish new guard time", -1, elapsed, time.Duration(0)}, + } + + for _, tc := range tc { + t.Logf("%+v", tc) + lp.Update(tc.sitePower, false, false, false, 0, nil, nil) + assert.Equal(t, tc.guardUpdated, lp.guardUpdated) + expectCache(guardTimer+"Remaining", tc.guardTimerRemaining) + clock.Add(time.Minute) + } +}