Skip to content

Commit

Permalink
Loadpoint: publish guardTimer independently from pvTimer (evcc-io#11926)
Browse files Browse the repository at this point in the history
  • Loading branch information
GrimmiMeloni authored Jan 29, 2024
1 parent 0cc666c commit 5b1eb75
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 9 deletions.
12 changes: 6 additions & 6 deletions core/loadpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
132 changes: 129 additions & 3 deletions core/loadpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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)
}
}

0 comments on commit 5b1eb75

Please sign in to comment.