Skip to content

fix(loadpoint): start EV in 3Φ when live surplus already covers it - #300

Merged
frahlg merged 4 commits into
masterfrom
fix/ev-3phase-start-live-surplus
May 26, 2026
Merged

fix(loadpoint): start EV in 3Φ when live surplus already covers it#300
frahlg merged 4 commits into
masterfrom
fix/ev-3phase-start-live-surplus

Conversation

@erikarenhill

Copy link
Copy Markdown
Collaborator

Summary

pickSurplusSteps decides 3Φ-only vs all-allowed (1Φ + 3Φ) step set based on forecast peak surplus (nearTermPeakSurplusW for next 30 min, then peakRemainingSurplusW for whole-day). When the forecast under-estimates current sun, the controller picks the all-allowed set, the wake-kick fires at 1380 W (1Φ min), and the EV starts in 1Φ even when live surplus is 5+ kW. Switching the contactor to 3Φ later can interrupt the ramp and stop the EV before steady state.

Adds a live-surplus override: at session start (no prior phase decision recorded today), if the live PV-surplus reader returns ≥ 4140 W, lock the step set to 3Φ-only and record the decision so the dwell-hold logic downstream keeps us there. Forecast-based logic still owns the mid-session, day-rollover, and configured low-PV-day cases.

What this does NOT change

  • Mid-session phase decisions — once committed, the existing phaseSwitchMinHold dwell + day-long lock logic owns the choice.
  • Configured low-PV-day fallback — peakRemainingSurplusW still locks 1Φ for the whole day when today's forecast peak can't sustain 3Φ.
  • Driver-side pick_phases — the Easee driver still gets to pick phaseMode from the operator's mode + the requested W. This just changes which step set the controller offers to snap to.

Test

  • TestPickSurplusSteps_LiveSurplusOverridesForecastAtSessionStart — pessimistic forecast (3 kW peak) but live surplus 5.5 kW → smallest step = 4140 W (3Φ).
  • TestPickSurplusSteps_LowLiveSurplusKeeps1PSteps — same forecast + live surplus 2.5 kW → smallest step = 1380 W (1Φ, preserves current behavior).
  • Full suite green.

Test plan

  • Plug in EV on a sunny morning with cloudy near-term forecast (e.g. just after a cloud passed).
  • Confirm log: loadpoint surplus_only: 3Φ at session start (live surplus override) live_surplus_w=… min_3p_step_w=4140.
  • Confirm Easee writes phaseMode → 3 and the wake-kick fires at 4140 W (not 1380 W).
  • Mid-session: clouds cause live surplus to dip below 3Φ — confirm dwell-hold keeps us in 3Φ until phaseSwitchMinHold elapses (no flap).

🤖 Generated with Claude Code

erikarenhill and others added 2 commits May 25, 2026 09:28
pickSurplusSteps decided 3Φ-only vs all-allowed based on FORECAST
peak surplus (next 30 min, then whole-day). A pessimistic forecast
that underestimates current sun would make the controller pick the
all-allowed step set; the wake-kick then fires at 1380 W (1Φ min)
and the EV starts in 1Φ even when the live meter has 5+ kW free.

Adds a live-surplus override: when there's no prior phase decision
this session AND the live PV-surplus reader returns ≥ 4140 W, lock
to 3Φ-only immediately and record the decision so the dwell logic
downstream keeps us there. Forecast-based logic still owns the
mid-session and "configured low-PV day" cases.

User-observed regression: Easee started in phaseMode=1 → wake-kick
at 1380 W → contactor opened to switch phases → EV gave up before
the 3Φ ramp completed. With this change the first kick is 4140 W
already on a sunny start.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the loadpoint controller’s pickSurplusSteps selection so an EV can start in 3Φ immediately when live PV surplus already meets the 3Φ minimum, even if the forecast (near-term) is pessimistic, avoiding an initial 1Φ start that can later disrupt charging when switching phases.

Changes:

  • Add a session-start live-surplus override that immediately selects the 3Φ-only step set and records a dwell decision.
  • Emit an informational log when the live-surplus override triggers.
  • Add unit tests covering “live surplus high overrides pessimistic near-term forecast” and “live surplus low preserves 1Φ-inclusive steps”.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
go/internal/loadpoint/controller.go Adds live-surplus override and records phase decision to enforce 3Φ at session start.
go/internal/loadpoint/controller_phase_handoff_test.go Adds tests validating step-set selection with live-surplus override behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1564 to +1579
if !hasPrev {
if liveSurplus, ok := c.siteSurplusForEVW(); ok &&
!math.IsNaN(liveSurplus) && !math.IsInf(liveSurplus, 0) &&
liveSurplus >= minStep3 {
c.phaseLockMu.Lock()
if c.phaseSelected3P == nil {
c.phaseSelected3P = map[string]bool{}
c.phaseSelectedAt = map[string]time.Time{}
}
c.phaseSelected3P[lpCfg.ID] = true
c.phaseSelectedAt[lpCfg.ID] = now
c.phaseLockMu.Unlock()
slog.Info("loadpoint surplus_only: 3Φ at session start (live surplus override)",
"lp", lpCfg.ID, "live_surplus_w", liveSurplus, "min_3p_step_w", minStep3)
return steps3
}
Comment on lines +135 to +141
func TestPickSurplusSteps_LiveSurplusOverridesForecastAtSessionStart(t *testing.T) {
cfg := phaseLoadpoint("auto", 0, 0)
dir := &Directive{SlotStart: time.Now(), SlotEnd: time.Now().Add(15 * time.Minute)}
sender := &fakeSender{}
samples := map[string]EVSample{cfg.DriverName: {Connected: true, PowerW: 0}}
c := newTestController(t, []Config{cfg}, dir, samples, sender)

c.phaseSelected3P[lpCfg.ID] = true
c.phaseSelectedAt[lpCfg.ID] = now
c.phaseLockMu.Unlock()
slog.Info("loadpoint surplus_only: 3Φ at session start (live surplus override)",
@frahlg
frahlg merged commit 73f8fd8 into master May 26, 2026
@frahlg
frahlg deleted the fix/ev-3phase-start-live-surplus branch May 26, 2026 17:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants