Repository navigation
fix(mpc) + feat(web): plumb Loadpoint into diagnostic + EV target UI (#174, #175) - #176
Conversation
When the DP plans EV charging alongside battery dispatch, the plan-table UI would render `BATTERY -5.6 kW` against `LOAD 1.6 kW` and look like a 4 kW battery-driven export. Reality: LOAD 1.6 + EV 4.0 = 5.6 kW total covered by battery, grid ≈ 0. The `gridW` equation in mpc.go:377 already included EV, but the diagnostic row only carried household LoadW. DiagnosticSlot now carries `LoadpointW` and `LoadpointSoCPct` copied from the matching Action, both `omitempty`. The web renderer (`web/diagnose.js`) already had conditional EV W + EV SoC columns gated on `slots.some(x => x.loadpoint_w || x.loadpoint_soc_pct)`; plumbing the fields through is all that was needed to make them appear on plans that actually include a loadpoint. Field-reported by @erikarenhill.
Field test surfaced that without a target_soc_pct + target_time
on a loadpoint, the MPC has no urgency penalty for the EV and
defers charging — especially in arbitrage mode, where idle PV
gets exported for revenue rather than going into the car. Backend
was wired (POST /api/loadpoints/{id}/target, Manager.SetTarget,
replan trigger), but the only way to set the target was curl.
Per loadpoint, the EV modal now shows two rows:
SoC now: [val] [Set] (existing — re-anchors session)
Target: [SoC] % by [datetime] [Save] [Clear] (new)
The target row is reachable even when unplugged — target is user
intent, valid to set ahead of plugging in. Empty datetime → no
deadline (still sets the SoC target, which alone has no effect
since the DP only applies the shortfall penalty when a deadline
is present). Clear posts zeroes so the manager drops both fields.
Implementation notes:
- Vanilla DOM (matches the rest of app.js — no framework)
- parseTargetTime treats anything before year 2000 as "unset" so
Go's omitempty-on-time.Time quirk (zero time still serializes
as 0001-01-01T00:00:00Z) doesn't show a year-1 deadline in the
picker
- toLocalInputValue does the local-TZ shift for the
datetime-local input so the picker shows "Apr 23 07:00" instead
of UTC
Not yet visually verified in a real browser session — the dev
server I had access to was serving a stale app.js from an older
deployment. Syntax-checked with node, fetch payload shape
verified to match handleLoadpointTarget against a live API. UI
verification will land via field test on @erikarenhill's setup.
|
@erikarenhill — this is the follow-up to your field findings from earlier. Both fixes you reported on are in here, would appreciate a verification pass before we merge. What changed#174 — Diagnostic shows EV charging now. When the planner has a loadpoint, the plan table at `/diagnose` should now show two extra columns ("EV W" and "EV SoC") between "SoC end" and "Cost". The "EV in plan" pill in the header should also light up. This is the fix for the "battery discharges 5.6 kW to cover 1.6 kW load" mystery — once you can see the EV column, the math will add up. #175 — Target SoC + deadline UI. Open the EV modal (click the EV card on the dashboard). For each loadpoint you should now see two rows: ``` The target row is reachable whether the car is plugged in or not. Empty datetime = no deadline (charge opportunistically). Clear button drops both fields. What to test#174
#175
CaveatsI couldn't visually verify the UI in a browser before pushing — the local dev server I had was serving a stale `app.js` from an older deployment, so the new function names didn't show up. Syntax-checked with `node --check` and the wire format matches the API, but real browser interaction is on you for this one. Yell if anything's off — I'd rather catch it now than after a flaky deploy. Thanks 🙏 |
Closes #174 and #175. Two related EV-charging finishes Erik flagged in field test, kept in one PR because together they make the planner's EV behaviour comprehensible end-to-end.
#174 — fix(mpc): plumb Loadpoint fields into diagnostic slots
Symptom
Erik's screenshot — a `cheap_charge` plan at 22:00 with `LOAD 1.60 kW`, `BATTERY −5.60 kW`, `GRID 137 W`. Math looks like 4 kW battery-driven export. Mode contract says "never exports via battery." Smells like a broken DP.
Reality
The DP is fine. `mpc.go:377` folds EV into the grid equation:
```go
gridW := slot.LoadW + slot.PVW + battW + evW
```
Slot actually looks like `LOAD 1.6 + EV 4.0 = 5.6 kW covered by battery, grid ≈ 0`. Contract honoured.
Fix
`DiagnosticSlot` didn't carry the EV decision even though `Action.LoadpointW` was sitting right there on the plan. The plan table in `web/diagnose.js` already has conditional EV columns gated on `slots.some(x => x.loadpoint_w || x.loadpoint_soc_pct)` — they just never appeared because the fields were always zero.
Two fields added to `DiagnosticSlot`, both `omitempty`, copied verbatim from `Action` in `buildDiagnostic`. No DP math changes. No UI changes.
Tests
`TestDiagnoseCarriesLoadpointFields` — builds a four-slot cheap-price horizon with a deadline-bound EV, asserts the diagnostic row's `LoadpointW` and `LoadpointSoCPct` match the planner's action for every slot, and that at least one slot reports non-zero charging (otherwise the UI's `lpActive` gate would stay false anyway).
#175 — feat(web): EV target SoC + deadline UI
Why
Without a `target_soc_pct` + `target_time` on a loadpoint, the MPC has no urgency penalty for the EV and rationally defers charging — especially in arbitrage mode, where idle PV gets exported for revenue rather than going into the car. Backend was wired (POST `/api/loadpoints/{id}/target`, `Manager.SetTarget`, replan trigger), but the only way to set the target was curl.
What
Per loadpoint, the EV modal now shows two rows:
```
SoC now: [val] [Set] (existing — re-anchors session)
Target: [SoC] % by [datetime] [Save] [Clear] (new)
```
The target row is reachable even when unplugged — target is user intent, valid to set ahead of plugging in. Empty datetime → no deadline (still sets the SoC target, which alone has no effect since the DP only applies the shortfall penalty when a deadline is present). Clear posts zeroes so the manager drops both fields.
Implementation notes
Verification status
Go side: full 571-test suite green. JS syntax-checked with `node --check`. Wire format verified against a live API (`POST /api/loadpoints/{id}/target` with `{soc_pct, target_time_ms}` returns the expected shape).
UI not yet visually verified in a real browser session — the dev server I had access to was serving a stale `app.js` from an older deployment. Field test on @erikarenhill's setup will close that loop.
Knock-on
These two fixes resolve the symptom-cluster Erik reported:
Field-reported by @erikarenhill.