Skip to content

Commit c33e87d

Browse files
frahlgclaude
andauthored
fix(cars): keep a one-off goal's deadline when a profile target applies (#1887)
The session seed called SetTarget with the vehicles[] profile target, which clears targetTime. A one-off goal is not re-rolled, so the car lost its ready-by time for the whole session. Apply the profile target only when the car has no goal. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent c51f64b commit c33e87d

3 files changed

Lines changed: 56 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"ftw": patch
3+
---
4+
5+
Keep a one-off charging goal and its ready-by time when the car plugs in. A vehicle profile's `target_soc` now applies to a session only when the car has no goal of its own.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package cars
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/srcfl/ftw/go/internal/loadpoint"
8+
)
9+
10+
// A car merged from a vehicles[] profile with target_soc keeps a one-off
11+
// goal's deadline across a plug-in.
12+
func TestProfileTargetKeepsOneOffDeadline(t *testing.T) {
13+
h := newHarness(t, homeConfig(), nil, nil)
14+
h.start(Profile{ID: "ioniq", Name: "Ioniq", CapacityWh: 58000, Identifiers: []string{"RFID-9"}, TargetSoC: .9})
15+
if c := h.only(); c.Name != "Ioniq" {
16+
t.Fatalf("not merged: %+v", c)
17+
}
18+
// One-off goal: 80 % by 07:00 tomorrow.
19+
if ok, err := h.mgr.SetScheduleChecked("easee", loadpoint.NewSchedule(.8, 420, false, 0, false)); !ok || err != nil {
20+
t.Fatal(ok, err)
21+
}
22+
h.tick(time.Second)
23+
before := h.state("easee")
24+
if before.TargetTime.IsZero() || before.TargetSoC != .8 {
25+
t.Fatalf("goal not applied before plug-in: %.2f by %s", before.TargetSoC, before.TargetTime)
26+
}
27+
h.plug("easee", "s1")
28+
for i := 0; i < 3; i++ {
29+
h.tick(5 * time.Second)
30+
}
31+
after := h.state("easee")
32+
if after.TargetTime.IsZero() || after.TargetSoC != .8 {
33+
t.Fatalf("plug-in erased the one-off goal: before %.2f by %s, after %.2f by %s",
34+
before.TargetSoC, before.TargetTime, after.TargetSoC, after.TargetTime)
35+
}
36+
}
37+
38+
// Without a goal, the profile target still applies to the session.
39+
func TestProfileTargetAppliesWithoutGoal(t *testing.T) {
40+
h := newHarness(t, homeConfig(), nil, nil)
41+
h.start(Profile{ID: "ioniq", Name: "Ioniq", CapacityWh: 58000, Identifiers: []string{"RFID-9"}, TargetSoC: .9})
42+
h.tick(time.Second)
43+
h.plug("easee", "s1")
44+
h.tick(5 * time.Second)
45+
if st := h.state("easee"); st.TargetSoC != .9 || !st.TargetTime.IsZero() {
46+
t.Fatalf("profile target not applied: %.2f by %s", st.TargetSoC, st.TargetTime)
47+
}
48+
}

‎go/internal/cars/tick.go‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -801,7 +801,9 @@ func (r *Runtime) syncLocked(now time.Time) []func() {
801801
s.seeded = true
802802
seed := r.seedLocked(car, s, st)
803803
acts = append(acts, func() { lps.SeedSessionLevel(id, seed) })
804-
if car != nil && car.ProfileTarget > 0 {
804+
// The profile target fills in only for a car without a goal;
805+
// SetTarget clears the deadline of the goal it would replace.
806+
if car != nil && car.ProfileTarget > 0 && !goal.HasTarget() {
805807
target := car.ProfileTarget
806808
acts = append(acts, func() { lps.SetTarget(id, target, time.Time{}) })
807809
}

0 commit comments

Comments
 (0)