Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ev-cut-ends-reservation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ftw": patch
---

Charging starts again promptly after a pause. In 0.155.0, an Easee Cloud charger that was paused and then asked to start waited up to three minutes, shown as limited by the main fuse, while the phases had plenty of room: FTW still counted the higher current it had ordered before the pause as on its way. A pause now ends that reservation, because the charger follows its latest order and the site meter shows its draw until the pause lands. In the reported case the restart now comes after 12 s instead of 2 min 47 s.
14 changes: 11 additions & 3 deletions go/internal/control/ev_fuse_budget.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,11 @@ func (l commandLog) span(now time.Time) (low, high, latest float64, ok bool) {

// unshown is how much more a charger may draw than a reading taken at `at`
// shows, in watts and in current per phase: the highest offer sent since
// settle before the reading, less the lowest offer the reading may show.
// An offer set before that window is in the reading. A cut needs nothing:
// the reading shows the draw until the cut lands. Before our first offer
// settle before the reading and not replaced by a lower one, less the
// lowest offer the reading may show. An offer set before that window is in
// the reading. A cut needs nothing: the reading shows the draw until the
// cut lands, and the charger moves to its latest order, so a cut also ends
// the reservation of the higher offers before it. Before our first offer
// the charger counts as 0.
func (l commandLog) unshown(at time.Time, settle time.Duration) (w, a float64) {
from := at.Add(-settle)
Expand All @@ -102,6 +104,12 @@ func (l commandLog) unshown(at time.Time, settle time.Duration) (w, a float64) {
settledW, settledA = c.w, c.a
continue
}
if c.w < highW {
highW = c.w
}
if c.a < highA {
highA = c.a
}
highW, highA = math.Max(highW, c.w), math.Max(highA, c.a)
if !c.at.After(at) {
lowW, lowA = math.Min(lowW, c.w), math.Min(lowA, c.a)
Expand Down
48 changes: 48 additions & 0 deletions go/internal/control/ev_fuse_restart_after_pause_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package control

import (
"testing"
"time"

"github.com/srcfl/ftw/go/internal/loadpoint"
)

// A field case on v0.155.0-beta.1 (Easee Cloud, 25 A fuse): a charge at
// 10,350 W, a 25 s pause, then a 4,140 W request. Easee declares 180 s, so
// the rise sent before the pause stayed reserved, and the restart waited
// close to three minutes with fuse_limit while the phases read about 1.3 A.
// A cut ends the reservation of the higher orders before it: the charger
// moves to its latest order, and the meter shows its draw until the cut
// lands.
func TestEVHardLimitRestartAfterPauseIgnoresReplacedRise(t *testing.T) {
s := newEVMeterSite(t, 0, 900)
s.chargerDelay, s.telemetryDelay, s.chargerMaxAgeS = 5*time.Second, 10*time.Second, 180
s.run(25 * time.Second)
if s.offer() <= 0 {
t.Fatalf("the car did not start before the pause: offers %v", offerWatts(s.offers))
}
s.ctrl.SetManualHold("garage", loadpoint.ManualHold{PowerW: 0, PhaseMode: "3p", Persistent: true})
s.run(25 * time.Second)
if s.offer() != 0 {
t.Fatalf("the pause did not stop the offer: %v", offerWatts(s.offers))
}
askedAt := s.now
s.ctrl.SetManualHold("garage", loadpoint.ManualHold{PowerW: 4140, PhaseMode: "3p", Persistent: true})
s.run(3 * time.Minute)
var startedAt time.Time
for _, o := range s.offersSince(askedAt) {
if o.w > 0 {
startedAt = o.at
break
}
}
if startedAt.IsZero() {
t.Fatalf("no offer within 3 min after the restart: %v", offerWatts(s.offersSince(askedAt)))
}
// The idle-start dwell (35 s) may still apply; the replaced rise must not.
if wait := startedAt.Sub(askedAt); wait > 60*time.Second {
t.Fatalf("restart waited %v for a 4,140 W offer with the phases near 1.3 A", wait)
} else {
t.Logf("restart offered after %v", wait)
}
}
Loading