Skip to content

Commit 792ef29

Browse files
frahlgclaude
andauthored
fix(loadpoint): the deadline reserve holds to the plan's limits, not the charger's raw table (#1860)
With no plan, withDeadlineReserve took its on-power from DeadlineNeed, which held it to LimitPower(st, st.AllowedStepsW): the charger's whole table. On a table with one-phase and three-phase levels, a car whose own maximum is below the lowest three-phase level (a 3,680 W plug-in hybrid) got a one-phase level, 3,680 W. Planned charging runs on three phases, so the controller snapped with the three-phase minimum, 4,140 W, and sent 0 W. The car is marked one-phase only once it draws, so it never was. The plan path gets PlanningLimits and orders 4,140 W. DeadlineNeed now takes the limits. Controller.PlanningLimits is PlanningLimits on the loadpoint's phase mode and the site fuse, read under the controller's and the manager's own locks. The controller's reserve check uses it, and main.go wires it into the new mpc.Service.PlanningLimits; nil keeps the charger's table. A marked one-phase car is reserved the third of a three-phase level it takes, so a 2.3 kW car on a three-phase table is ordered 6,900 W, not the charger's 11,040 W. A charger without a table still takes any power in its range. Tests fail on master 286ac14: DeadlineNeed on mixed and three-phase tables, the reserve through SlotDirectiveAt, and the controller's order on mixed tables (unset, auto, 3p). Of twelve new noplan field-matrix rows, the seven on mixed tables go idle on master (order 0 W); the five on three-phase tables pass on both. The known list stays empty. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent e5edf34 commit 792ef29

12 files changed

Lines changed: 232 additions & 24 deletions
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+
A car whose own charger takes less than the charger's three-phase levels now charges toward its goal when no plan is available. On a step table with both one-phase and three-phase levels, the reserve FTW falls back to without a plan gave such a car a one-phase level, which no three-phase order has, and the charger was ordered 0 W. An example is a 3.7 kW plug-in hybrid on a charger whose table runs 1.4-3.7 kW on one phase and 4.1-11 kW on three. The reserve now holds to the same limits as the plan: such a car is ordered the lowest three-phase level, and once FTW sees it draw on one phase, three times the level it takes.

‎go/cmd/ftw/main.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1716,6 +1716,11 @@ func main() {
17161716
}
17171717
return states
17181718
}
1719+
// The deadline reserve holds to the plan's limits. The controller
1720+
// reads them under its own locks; it is assigned further down.
1721+
mpcSvc.PlanningLimits = func(st loadpoint.State) (minW, maxW float64, steps []float64) {
1722+
return lpController.PlanningLimits(st)
1723+
}
17191724
mpcSvc.Loadpoints = func(slotLenMin int) []*mpc.LoadpointSpec {
17201725
specs := make([]*mpc.LoadpointSpec, 0)
17211726
for _, st := range lpMgr.States() {

‎go/internal/cars/reserve_test.go‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ func fallback(t *testing.T, h *harness) (ok bool, maxW float64) {
2020
return d.LoadpointFallback["easee"], d.LoadpointMaxPowerW["easee"]
2121
}
2222

23+
// reserveNeed is the reserve's need for a session on the charger's own
24+
// table, as fallback's service holds it.
25+
func reserveNeed(st loadpoint.State) (energyWh, powerW float64, ok bool) {
26+
minW, maxW, steps := loadpoint.LimitPower(st, st.AllowedStepsW)
27+
return loadpoint.DeadlineNeed(st, minW, maxW, steps)
28+
}
29+
2330
// A failed replan keeps the session car's goal charging, held to that car's
2431
// own charger and level, the moment the box knows the car; never a guess for
2532
// a car it does not know.
@@ -91,7 +98,7 @@ func TestAFailedReplanKeepsTheSessionCarsGoal(t *testing.T) {
9198
t.Fatal(err)
9299
}
93100
st := h.state("easee")
94-
need, peak, ok := loadpoint.DeadlineNeed(st)
101+
need, peak, ok := reserveNeed(st)
95102
want := (0.6 - 0.3) * 40000 / loadpoint.DefaultChargeEfficiency
96103
if !ok || st.VehicleCapacityWh != 40000 || math.Abs(st.CurrentSoC-.3) > 1e-9 || math.Abs(need-want) > 1 || peak > 7400 {
97104
t.Fatalf("the Volvo's need %v (want %v) at %v W, capacity %v, level %v", need, want, peak, st.VehicleCapacityWh, st.CurrentSoC)
@@ -117,7 +124,7 @@ func TestAFailedReplanKeepsTheSessionCarsGoal(t *testing.T) {
117124
if ok, _ := fallback(t, h); !ok {
118125
t.Fatal("a visitor's goal got no reserve")
119126
}
120-
if need, _, _ := loadpoint.DeadlineNeed(st); math.Abs(need-0.5*76000/loadpoint.DefaultChargeEfficiency) > 1 {
127+
if need, _, _ := reserveNeed(st); math.Abs(need-0.5*76000/loadpoint.DefaultChargeEfficiency) > 1 {
121128
t.Fatalf("a visitor's need %v, want it from empty", need)
122129
}
123130
h.tick(time.Second)
@@ -145,7 +152,7 @@ func TestTheReserveStopsAtTheCarsChargeLimit(t *testing.T) {
145152
h.obs["tesla-ha"] = telemetry.VehicleObservation{Driver: "tesla-ha", SoC: .5, ChargeLimit: .7, At: h.now, ReportedAt: h.now}
146153
h.tick(time.Second)
147154
st := h.state("easee")
148-
need, _, ok := loadpoint.DeadlineNeed(st)
155+
need, _, ok := reserveNeed(st)
149156
if want := (0.7 - 0.5) * 76000 / loadpoint.DefaultChargeEfficiency; !ok || st.CarChargeLimit != .7 || math.Abs(need-want) > 50 {
150157
t.Fatalf("need %v (want about %v), limit %v", need, want, st.CarChargeLimit)
151158
}

‎go/internal/control/ev_deadline_runtime_test.go‎

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -206,16 +206,40 @@ func TestJoinedMixedPhaseNoCreditBeforeMeasuredReduction(t *testing.T) {
206206
// Without a plan, a car whose own charger takes less than a stepless
207207
// charger's minimum (a 3.7 kW plug-in hybrid on an 11 or 22 kW charger) is
208208
// ordered that minimum, as the plan orders it: never 0 W because the car's
209-
// own maximum is lower.
209+
// own maximum is lower. On a table with one-phase and three-phase levels it
210+
// is ordered the lowest three-phase level, not a one-phase level the
211+
// three-phase order cannot run (review of #1807).
210212
func TestJoinedNoPlanGoalSmallCarGetsChargerMinimum(t *testing.T) {
211213
ctx := context.Background()
212-
for _, maxW := range []float64{11040, 22080} {
214+
mixed := []float64{0}
215+
for _, phases := range []float64{1, 3} {
216+
for a := 6.0; a <= 16; a++ {
217+
mixed = append(mixed, a*230*phases)
218+
}
219+
}
220+
for _, ch := range []struct {
221+
name string
222+
mode string
223+
minW, maxW float64
224+
steps []float64
225+
carMaxW float64
226+
}{
227+
{"11 kW", "", 4140, 11040, nil, 3680},
228+
{"22 kW", "", 4140, 22080, nil, 3680},
229+
{"mixed table", "", 1380, 11040, mixed, 3680},
230+
{"mixed table, auto", "auto", 1380, 11040, mixed, 3680},
231+
{"mixed table, 3p", "3p", 1380, 11040, mixed, 3680},
232+
// The charger's minimum above the car's maximum, and that above
233+
// the lowest one-phase level: the car's own limits leave no level.
234+
{"mixed table, minimum 2,000 W, a 1.5 kW car", "", 2000, 11040, mixed, 1500},
235+
} {
213236
now := time.Now()
214-
cfg := loadpoint.Config{ID: "garage", DriverName: "charger", VehicleCapacityWh: 15000, PluginSoC: .3, MinChargeW: 4140, MaxChargeW: maxW}
237+
cfg := loadpoint.Config{ID: "garage", DriverName: "charger", VehicleCapacityWh: 15000, PluginSoC: .3, PhaseMode: ch.mode,
238+
MinChargeW: ch.minW, MaxChargeW: ch.maxW, AllowedStepsW: ch.steps}
215239
mgr := loadpoint.NewManager()
216240
mgr.Load([]loadpoint.Config{cfg})
217241
mgr.Observe(cfg.ID, true, 0, 0, true)
218-
if !mgr.SetCarBinding(cfg.ID, loadpoint.CarBinding{ID: "phev", MaxW: 3680}) ||
242+
if !mgr.SetCarBinding(cfg.ID, loadpoint.CarBinding{ID: "phev", MaxW: ch.carMaxW}) ||
219243
!mgr.SetCurrentSoC(cfg.ID, .3) || !mgr.SetTarget(cfg.ID, .8, now.Add(3*time.Hour)) {
220244
t.Fatal("goal fixture rejected")
221245
}
@@ -256,11 +280,12 @@ func TestJoinedNoPlanGoalSmallCarGetsChargerMinimum(t *testing.T) {
256280
return nil
257281
})
258282
controller.SetSiteFuse(loadpoint.SiteFuse{MaxAmps: 25, Voltage: 230, PhaseCnt: 3})
283+
svc.PlanningLimits = controller.PlanningLimits // as main.go wires it
259284
controller.SetFuseEVMax(func() (float64, bool) { return EVFuseBudget(tel, st, mgr.States(), controller.FuseConfigs(), 17250) })
260285
controller.SetFusePhaseRoomA(func() (float64, bool) { return EVFusePhaseRoomA(st) })
261286
controller.TickWithDispatch(ctx, now, true)
262287
if len(commands) == 0 || commands[len(commands)-1] != 4140 {
263-
t.Fatalf("%.0f W charger: orders %v, want the charger's minimum 4140 W", maxW, commands)
288+
t.Errorf("%s: orders %v, want 4140 W", ch.name, commands)
264289
}
265290
}
266291
}

‎go/internal/control/sitesim_field_test.go‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,20 @@ func probeCases() []fieldCase {
302302
c.carPhases = 1
303303
out = append(out, c)
304304
}
305+
for _, fuse := range []float64{16, 25} {
306+
for _, ms := range []struct{ mode, steps string }{{"", "mixed"}, {"auto", "mixed"}, {"3p", "mixed"}, {"", "3p"}, {"auto", "3p"}} {
307+
// The same car on a table with three-phase levels: the reserve
308+
// orders one of them, as the plan does.
309+
out = append(out, fieldCase{report: "probe", fuseA: fuse, mode: ms.mode, steps: ms.steps, timing: timingEasee, carPhases: 1, use: useNoPlan,
310+
house: houseBalanced, houseName: "balanced"})
311+
}
312+
}
313+
for _, steps := range []string{"mixed", "3p"} {
314+
// A 2.3 kW one-phase car, below the charger's three-phase levels
315+
// over three: marked, it is ordered three times its own level.
316+
out = append(out, fieldCase{report: "car2300W", fuseA: 25, steps: steps, timing: timingFast, carPhases: 1, use: useNoPlan,
317+
house: houseBalanced, houseName: "balanced", car: func(car *sitesim.Car) { car.MaxW = 2300 }})
318+
}
305319
c := helpReport(usePlanned, timingEasee)
306320
c.houseStep = 30 * time.Minute
307321
return append(out, c)
@@ -755,7 +769,8 @@ func runFieldCase(c fieldCase, trace func(string, ...any)) (*simSite, *fieldRun)
755769
s.plan = f.plan.directive
756770
case useNoPlan:
757771
run, kettle = time.Hour, []time.Duration{20 * time.Minute}
758-
svc := &mpc.Service{LoadpointStates: func() []loadpoint.State { return s.mgr.States() }}
772+
svc := &mpc.Service{LoadpointStates: func() []loadpoint.State { return s.mgr.States() },
773+
PlanningLimits: func(st loadpoint.State) (float64, float64, []float64) { return s.ctrl.PlanningLimits(st) }} // as main.go wires it
759774
s.plan = func(now time.Time) (loadpoint.Directive, bool) {
760775
d, ok := svc.SlotDirectiveAt(now)
761776
return d.LoadpointDirective(), ok

‎go/internal/loadpoint/controller.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2626,7 +2626,8 @@ func (c *Controller) computeCommandForBudget(now time.Time, lpCfg Config, curren
26262626
}
26272627
if d.LoadpointFallback[lpCfg.ID] {
26282628
st, known := c.manager.State(lpCfg.ID)
2629-
if _, _, needed := DeadlineNeed(st); !known || !needed {
2629+
minW, maxW, steps := c.PlanningLimits(st)
2630+
if _, _, needed := DeadlineNeed(st, minW, maxW, steps); !known || !needed {
26302631
return 0, true, false
26312632
}
26322633
}

‎go/internal/loadpoint/deadline_need.go‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import (
44
"math"
55
)
66

7-
// DeadlineNeed is the remaining AC energy and legal on-power of a saved goal,
8-
// held to the session car's own charger and charge limit as the plan is. It
9-
// does not promise future site capacity or authorize a hardware command.
10-
func DeadlineNeed(st State) (energyWh, powerW float64, ok bool) {
7+
// DeadlineNeed is the remaining AC energy of a saved goal, held to the car's
8+
// charge limit, and its legal on-power within minW-maxW and steps: the limits
9+
// the plan gets for the session (PlanningLimits). It does not promise future
10+
// site capacity or authorize a hardware command.
11+
func DeadlineNeed(st State, minW, maxW float64, steps []float64) (energyWh, powerW float64, ok bool) {
1112
if !st.PluggedIn || st.ChargingDeclined || st.GoalComplete || st.TargetTime.IsZero() ||
1213
!finite(st.CurrentSoC) || st.CurrentSoC < 0 || st.CurrentSoC > 1 ||
1314
!finite(st.TargetSoC) || st.TargetSoC < 0 || st.TargetSoC > 1 ||
@@ -27,7 +28,6 @@ func DeadlineNeed(st State) (energyWh, powerW float64, ok bool) {
2728
return 0, 0, false
2829
}
2930
}
30-
minW, maxW, steps := LimitPower(st, st.AllowedStepsW)
3131
for _, step := range steps {
3232
if step >= minW {
3333
powerW = math.Max(powerW, step)

‎go/internal/loadpoint/deadline_need_test.go‎

Lines changed: 85 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,33 +29,112 @@ func TestLimitPowerKeepsTheChargersLowestStep(t *testing.T) {
2929
}
3030
}
3131

32+
// deadlineNeed is the reserve's need within the limits the plan gets on a
33+
// 3 × 25 A site, phase mode unset.
34+
func deadlineNeed(st State) (energyWh, powerW float64, ok bool) {
35+
cfg := Config{MinChargeW: st.MinChargeW, MaxChargeW: st.MaxChargeW, AllowedStepsW: st.AllowedStepsW}
36+
minW, maxW, steps := PlanningLimits(st, cfg, SiteFuse{MaxAmps: 25, Voltage: 230, PhaseCnt: 3})
37+
return DeadlineNeed(st, minW, maxW, steps)
38+
}
39+
3240
// The reserve's need and on-power hold to the session car's own charger and
3341
// its charge limit, as the plan does.
3442
func TestDeadlineNeedHoldsToTheCar(t *testing.T) {
3543
st := State{PluggedIn: true, VehicleCapacityWh: 40000, CurrentSoC: .3, TargetSoC: .8, TargetTime: time.Now().Add(3 * time.Hour),
3644
MinChargeW: 4140, MaxChargeW: 11000, AllowedStepsW: []float64{0, 4140, 7360, 11000}}
37-
if _, w, ok := DeadlineNeed(st); !ok || w != 11000 {
45+
if _, w, ok := deadlineNeed(st); !ok || w != 11000 {
3846
t.Fatalf("no car limit: %v %v", w, ok)
3947
}
4048
st.CarMaxW = 7400
41-
if _, w, ok := DeadlineNeed(st); !ok || w != 7360 {
49+
if _, w, ok := deadlineNeed(st); !ok || w != 7360 {
4250
t.Fatalf("a 7.4 kW car: %v %v", w, ok)
4351
}
4452
st.AllowedStepsW = nil
45-
if _, w, ok := DeadlineNeed(st); !ok || w != 7400 {
53+
if _, w, ok := deadlineNeed(st); !ok || w != 7400 {
4654
t.Fatalf("a 7.4 kW car on a stepless charger: %v %v", w, ok)
4755
}
4856
st.CarMaxW = 3680
49-
if _, w, ok := DeadlineNeed(st); !ok || w != 4140 {
57+
if _, w, ok := deadlineNeed(st); !ok || w != 4140 {
5058
t.Fatalf("a 3.7 kW car on a stepless charger: %v %v, want the charger's minimum", w, ok)
5159
}
5260
st.CarMaxW = 7400
5361
st.CarChargeLimit = .6
54-
if need, _, ok := DeadlineNeed(st); !ok || math.Abs(need-(.6-.3)*40000/DefaultChargeEfficiency) > 1e-6 {
62+
if need, _, ok := deadlineNeed(st); !ok || math.Abs(need-(.6-.3)*40000/DefaultChargeEfficiency) > 1e-6 {
5563
t.Fatalf("a 60 %% limit: %v %v", need, ok)
5664
}
5765
st.CurrentSoC = .6
58-
if _, _, ok := DeadlineNeed(st); ok {
66+
if _, _, ok := deadlineNeed(st); ok {
5967
t.Fatal("a car at its own limit still needs energy")
6068
}
6169
}
70+
71+
// Review of #1807: on a table with one-phase and three-phase levels, planned
72+
// charging runs on three phases. A car whose own maximum is below the
73+
// lowest three-phase level got a one-phase level as its on-power, which no
74+
// three-phase order has, and was ordered 0 W. It gets that lowest level, as
75+
// the plan gives it. A car marked one-phase gets the third of a three-phase
76+
// level it takes, up to its own maximum, on that table and on one with
77+
// three-phase levels only.
78+
func TestDeadlineNeedRunsOnThePlansPhases(t *testing.T) {
79+
var mixed, threePhase []float64
80+
for a := 6.0; a <= 16; a++ {
81+
mixed, threePhase = append(mixed, a*230), append(threePhase, a*690)
82+
}
83+
mixed = append(append([]float64{0}, mixed...), threePhase...)
84+
threePhase = append([]float64{0}, threePhase...)
85+
for _, c := range []struct {
86+
name string
87+
minW float64
88+
steps []float64
89+
carMaxW float64
90+
carPhases int
91+
want float64
92+
}{
93+
{"mixed table, a 3.7 kW car", 1380, mixed, 3680, 0, 4140},
94+
{"mixed table, a 3.7 kW car marked one-phase", 1380, mixed, 3680, 1, 3680},
95+
{"mixed table, a 2.3 kW car marked one-phase", 1380, mixed, 2300, 1, 2300},
96+
{"three-phase table, a 3.7 kW car", 4140, threePhase, 3680, 0, 4140},
97+
{"three-phase table, a 2.3 kW car marked one-phase", 4140, threePhase, 2300, 1, 2300},
98+
} {
99+
st := State{PluggedIn: true, VehicleCapacityWh: 40000, CurrentSoC: .3, TargetSoC: .8, TargetTime: time.Now().Add(3 * time.Hour),
100+
MinChargeW: c.minW, MaxChargeW: 11040, AllowedStepsW: c.steps, CarMaxW: c.carMaxW, CarPhases: c.carPhases}
101+
if _, w, ok := deadlineNeed(st); !ok || w != c.want {
102+
t.Errorf("%s: on-power %v ok=%v, want %v", c.name, w, ok, c.want)
103+
}
104+
}
105+
}
106+
107+
// The controller's limits read the loadpoint's phase mode and the site fuse
108+
// as they stand. A three-phase site keeps a mixed table's three-phase levels;
109+
// a 1p pin or a one-phase site keeps its one-phase levels.
110+
func TestControllerPlanningLimitsReadPhaseModeAndSite(t *testing.T) {
111+
mixed := []float64{0}
112+
for _, phases := range []float64{1, 3} {
113+
for a := 6.0; a <= 16; a++ {
114+
mixed = append(mixed, a*230*phases)
115+
}
116+
}
117+
st := State{ID: "garage", MinChargeW: 1380, MaxChargeW: 11040, AllowedStepsW: mixed, CarMaxW: 3680}
118+
for _, c := range []struct {
119+
name string
120+
mode string
121+
phases int
122+
want float64
123+
}{
124+
{"three-phase site", "", 3, 4140},
125+
{"three-phase site, auto", "auto", 3, 4140},
126+
{"pinned 1p", "1p", 3, 3680},
127+
{"one-phase site", "", 1, 3680},
128+
} {
129+
m := NewManager()
130+
m.Load([]Config{{ID: "garage", DriverName: "easee", PhaseMode: c.mode, MinChargeW: 1380, MaxChargeW: 11040, AllowedStepsW: mixed}})
131+
ctrl := NewController(m, nil, nil, nil)
132+
ctrl.SetSiteFuse(SiteFuse{MaxAmps: 25, Voltage: 230, PhaseCnt: c.phases})
133+
if _, maxW, _ := ctrl.PlanningLimits(st); maxW != c.want {
134+
t.Errorf("%s: top %v W, want %v W", c.name, maxW, c.want)
135+
}
136+
}
137+
if _, maxW, _ := (*Controller)(nil).PlanningLimits(st); maxW != 3680 {
138+
t.Errorf("no controller: top %v W, want the charger's own table", maxW)
139+
}
140+
}

‎go/internal/loadpoint/planning_steps.go‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,22 @@ func PlanningLimits(st State, cfg Config, site SiteFuse) (minW, maxW float64, st
7373
return LimitPower(st, planning)
7474
}
7575

76+
// PlanningLimits is PlanningLimits on this loadpoint's charger and the site
77+
// fuse, as both stand now. The deadline reserve holds to them, as the plan
78+
// does. Without a controller the reserve holds to the charger's own table.
79+
func (c *Controller) PlanningLimits(st State) (minW, maxW float64, steps []float64) {
80+
if c == nil {
81+
return LimitPower(st, st.AllowedStepsW)
82+
}
83+
cfg := Config{MinChargeW: st.MinChargeW, MaxChargeW: st.MaxChargeW, AllowedStepsW: st.AllowedStepsW}
84+
for _, lp := range c.manager.Configs() {
85+
if lp.ID == st.ID {
86+
cfg.PhaseMode = lp.PhaseMode
87+
}
88+
}
89+
return PlanningLimits(st, cfg, c.siteFuse())
90+
}
91+
7692
// ThreePhaseCapable reports whether a charger can run three phases at 6 A
7793
// each; see threePhaseCapable.
7894
func ThreePhaseCapable(cfg Config, site SiteFuse) bool { return threePhaseCapable(cfg, site) }

‎go/internal/mpc/deadline_directive.go‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,21 @@ func (s *Service) withDeadlineReserve(now time.Time, d SlotDirective, accepted b
1414
return d, accepted
1515
}
1616
states := s.LoadpointStates() // No control lock: the caller may hold it.
17+
limits := s.PlanningLimits
18+
if limits == nil {
19+
limits = func(st loadpoint.State) (float64, float64, []float64) {
20+
return loadpoint.LimitPower(st, st.AllowedStepsW)
21+
}
22+
}
1723
if !accepted {
1824
d = SlotDirective{SlotStart: now, SlotEnd: now.Add(time.Minute), EVOnly: true}
1925
}
2026
for _, st := range states {
2127
if _, hasBudget := d.LoadpointEnergyWh[st.ID]; hasBudget && !st.ManualActive {
2228
continue // An accepted zero budget is a price decision, too.
2329
}
24-
need, peak, ok := loadpoint.DeadlineNeed(st)
30+
minW, maxW, steps := limits(st)
31+
need, peak, ok := loadpoint.DeadlineNeed(st, minW, maxW, steps)
2532
if st.ManualActive && st.PluggedIn {
2633
// Manual intent has already passed the charger's own clamps.
2734
// Reserve its ceiling, including an explicit Stop, without
@@ -38,8 +45,12 @@ func (s *Service) withDeadlineReserve(now time.Time, d SlotDirective, accepted b
3845
if !st.ManualActive {
3946
// Near the goal use the smallest legal offer that can deliver
4047
// this minute's need, rather than reserving the charger's full
41-
// rating for a final few Wh.
42-
peak = loadpoint.SnapChargeW(need/d.SlotEnd.Sub(now).Hours(), st.MinChargeW, peak, st.AllowedStepsW)
48+
// rating for a final few Wh. A charger without a table takes
49+
// any power in its range.
50+
if len(st.AllowedStepsW) == 0 {
51+
steps = nil
52+
}
53+
peak = loadpoint.SnapChargeW(need/d.SlotEnd.Sub(now).Hours(), minW, peak, steps)
4354
}
4455
if d.LoadpointFallback == nil {
4556
d.LoadpointFallback = map[string]bool{}

0 commit comments

Comments
 (0)