feat(ev): charge now at a chosen current, and boost from the house battery - #60
Conversation
…ttery Charge now sends the slider's amps as watts — the box page's own body: power_w, hold_s 0, phase_mode from the phase count — instead of the charger's ceiling every time. A running hold shows its current and offers Update and Stop. The battery boost the registry and the box already carry gets a control: a house-battery reserve and a duration, Stop boost while it runs, and the box's stop reason afterwards. Refs #58 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
ftw-webapp | 3a0e33d | Commit Preview URL | Sep 04 2026, 06:10 AM |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3a0e33d6f7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| this.#evBoost = { expiresAtMs, minBatterySoc } | ||
| this.#evBoostStop = null |
There was a problem hiding this comment.
Apply active boosts to the simulated power flow
When a demo user starts a boost while the car is plugged in but outside the simulator's normal 17:00–19:30 charging window, this records an active lease and reports success, but SimBox.#sample() only applies #evHold and never consults #evBoost. Consequently EV power remains zero and the battery does not discharge even though the panel says the battery is charging the car. The boost state needs to affect the simulated reading, including the reserve constraint, rather than only the loadpoint status.
Useful? React with 👍 / 👎.
| let maxA = toA(lp.maxChargeW) || 16 | ||
| if (maxA <= minA) maxA = minA + 1 |
There was a problem hiding this comment.
Preserve a single-value charging range
When a charger reports equal minimum and maximum power, or both bounds round to the same whole amp, this invents an additional current above the reported ceiling. Since ampsToWatts() then caps that position back to maxChargeW, the two slider positions submit the same wattage while the UI labels one as a higher current and may enable an Update that changes nothing. Keep the collapsed range instead of manufacturing minA + 1.
Useful? React with 👍 / 👎.
| </button> | ||
| {/if} | ||
| </div> | ||
| {@render outcome('boost')} |
There was a problem hiding this comment.
Scope boost outcomes to the affected loadpoint
In a household with multiple plugged-in chargers, this render occurs once for every inactive charger, while store.command records only the control type and not the loadpoint ID. Starting, stopping, or failing a boost on one charger therefore displays the same outcome beneath every other charger's boost control as well, falsely implying that those chargers were changed. Carry the target loadpoint ID in the command state and filter the rendered outcome by it.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 3a0e33d. Configure here.
| // The editor closes on the box's yes alone; a refusal stays on screen | ||
| // under the values that were refused. | ||
| if (store.command.kind === 'applied') boostDraft = null | ||
| } |
There was a problem hiding this comment.
Boost editor ignores unconfirmed success
Medium Severity
startBoost treats only applied as the box accepting the lease, so an unconfirmed result leaves boostDraft in place. After that boost later ends, the start form reopens on its own with the old reserve and duration still filled in.
Reviewed by Cursor Bugbot for commit 3a0e33d. Configure here.
| Ends when the time is up, the house battery reaches the reserve, or you stop it. | ||
| </p> | ||
| {@render outcome('boost')} | ||
| </div> |
There was a problem hiding this comment.
Hold does not close boost editor
Low Severity
The closed boost offer hides itself when a hold is running, but an already-open editor does not. After Charge now, Start boost stays on screen and still sends loadpoint.boost, which the box refuses.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 3a0e33d. Configure here.




What changed
min_charge_w,max_charge_w,phasesandvoltage_von/api/loadpoints— with the readout "N A · X.X kW". Charge now sendsloadpoint.holdwithpower_wfor the chosen amps,hold_s: 0andphase_mode"1p"/"3p": the same body the box's own page posts tomanual_hold. The top of the slider sends exactlymax_charge_w(11 000 W rounds to 16 A, and 16 A back would be 11 040 W).manual_charge_w) and offers Update (re-send at the thumb's amps; disabled while nothing changed) and Stop charging. The copy says the hold runs until the car is full, Stop, or an unplug (fix(ev): manual charge runs until the car is full ftw#1056).loadpoint.boostwith a house-battery reserve (min_battery_soc_pct, default 30 %) and a duration (duration_s: 30 min, 1 h, 2 h, 4 h — the box caps a lease at four hours). While it runs the panel says the reserve and end time frombattery_boost.min_battery_socandexpires_at_ms, and offers Stop boost (cancel: true). Afterwards it says why the box stopped it, frombattery_boost.stop_reason, in the app's words.E_UNAVAILABLEnaming the op) and a lease it will not take (E_UNKNOWN_OPnaminglease) get their own sentences instead of "can't reach the charger".manual_charge_wand the box's three-statebattery_booststatus, validates a lease by the box's rules, refuses one under a hold, and withdraws a boost when a hold starts (operator_hold), so tests and the demo agree with the box.contract/registry.yamlor is a body field of an op already there;registry.yamlis untouched.Why
The client asked for full power on every Charge now while the box's own page offers a current, and it drew nothing for a boost the box implements. Both gaps came out of a review of the client against the box today (#58).
Verification
npm run verify: svelte-check 0 errors, 940 tests pass (1 skipped, pre-existing), production build ok.src/lib/format/ev.test.ts— the amp range, the cap at the ceiling, the box page's fallbacks, a single-phase charger, the hold read back in amps, the boost sentences and stop reasons.src/lib/state/loadpoints.test.ts— the exactcmdbodies for a hold at 10 A and for a boost, stopping a boost, a hold ending a boost withoperator_hold, and the refusal sentences.src/views/EvPanel.svelte.test.ts— slider defaults, charging at the chosen amps with the 1 Hz stream agreeing, Update, boost start and stop with the stop reason, no boost offer under a hold, and a viewer seeing neither control.Notes for the reviewer:
phasesandvoltage_vareomitemptyon the box. When absent the panel falls back to 3 × 230 V and 6–16 A, as the box page does.EvPanel.svelte(two style lines: scrim and z-index tokens). These hunks do not overlap it.Closes #58
🤖 Generated with Claude Code
Note
Medium Risk
New dispatch commands (
OP_LOADPOINT_HOLDwith different payloads,OP_LOADPOINT_BOOST) affect live charging and house-battery use; simulator alignment reduces but does not remove box-side edge-case risk.Overview
Charge now is no longer fixed at max power: owners get an amp slider (from charger limits on
/api/loadpoints), sendsloadpoint.holdwithpower_w,hold_s: 0, andphase_mode, and can Update or Stop an active hold. Battery boost addsloadpoint.boostwith reserve % and duration (up to 4 h), Stop boost, and copy for active/stopped state plus boost-specific command errors viaboostHelp.LoadpointsStoregeneralizes command lifecycle per control (hold/boost);ev.tsdecodes richer wire fields and amp/boost formatting; the sim mirrors box boost lease rules (hold cancels boost, refusals).EvPanelwires the UI; tests cover formatting, store commands, and panel behavior.Reviewed by Cursor Bugbot for commit 3a0e33d. Bugbot is set up for automated code reviews on this repo. Configure here.