feat(plan): five planning styles instead of the k slider - #1486
Conversation
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: 2435fe5f9f
ℹ️ 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".
| trust, export, safetyK, missingPrefs := config.ResolvePlannerPrefs(storedTrust, storedExport, storedSafetyK, string(ctrl.Mode), yamlTrust, yamlExport, yamlK) | ||
| stylesDone, _ := st.LoadConfig(config.StateKeyPlanningStyles) | ||
| if k, first := config.MigrateOldDefaultSafetyK(safetyK, stylesDone); first { |
There was a problem hiding this comment.
Preserve an explicitly configured k=1 on first startup
When SQLite has no planner preferences yet and config.yaml explicitly sets pv_forecast_safety_k: 1, ResolvePlannerPrefs correctly seeds safetyK to 1, but the absent styles marker makes this unconditional migration replace it with 0.3. Thus a new or reset installation silently runs a substantially less conservative margin than the operator configured; migration should distinguish a stored pre-styles default from a value just sourced from YAML.
AGENTS.md reference: AGENTS.md:L20-L21
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 7b39638: the one-time move to Balanced now applies only to a k already stored in SQLite. A k seeded from the settings document on a new box stays. Test case seeded from YAML in TestMigrateOldDefaultSafetyKMovesOnlyTheOldDefaultOnce.
| const start = Math.max(a.slot_start_ms, a.execution_start_ms || 0, from); | ||
| const end = Math.min(a.slot_start_ms + a.slot_len_min * 60_000, until); | ||
| if (end <= start) continue; | ||
| if (!Number.isFinite(a.live_pv_surplus_soc_cap)) return null; |
There was a problem hiding this comment.
Expose the surplus cap before depending on it
For every real /api/mpc/plan response, this check returns null: the handler serializes mpc.Plan directly, and mpc.Action has no live_pv_surplus_soc_cap JSON field (the value currently exists only on runtime SlotDirective). Consequently plan-style-extra is always hidden, so the newly advertised explanation of whether extra sun is stored or exported never appears. Add the per-slot value to the plan API or derive the message from fields that the endpoint actually returns.
Useful? React with 👍 / 👎.
2435fe5 to
801d76f
Compare
7b39638 to
cb941a7
Compare
A beta tester asked why the plan would not put the day's solar surplus into the battery. The answer was the forecast margin, but the Plan card showed it as "Follow the forecast" with a raw k from 0 to 2 that rose as trust fell, and a chart whose dashed and solid lines were hard to tell apart. The card now offers five planning styles, Very careful to Very bold, on the same stored safety_k. Under them, one line says how much of the spare sun the plan counts on for the window the chart shows, and another says whether extra sun goes into the battery or to the grid, from Core's per-slot live_pv_surplus_soc_cap when the box sends it. The chart shades the margin. Settings → Planner fine-tunes k and saves at once, and shows the margin as sun held back and use added. Balanced (k 0.3) is the new default. A closed-loop replay of a week of the home box's recorded plans found k up to 0.3 cost the same within noise, while k 1 cost 3.6–5.4 % more. A box that still runs the old default k 1 moves to Balanced once; any other stored value stays. Refs #1482 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
On a new box, k can come from the settings seed this boot. That value is the operator's choice, not the old default, so the one-time move to Balanced now applies only to a k that was already stored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Four findings from a local Codex review: - Settings fine-tuning sent the export permission seen when the tab opened, so a margin change could turn battery sales back on after another client turned them off. Each save now reads the box's current choice first. - Saves could overlap, and an older reply could reset the slider to an older value and call it saved. Saves now go one at a time, and a reply only updates the slider when no newer value waits and the slider still shows what was sent. - The extra-sun line ignored the operator's surplus cap, which dispatch prefers to the plan's per-slot cap. /api/status now reports pv_surplus_absorb_soc_cap, and the line follows dispatch's rule, including that a discharge slot never stores. - The Settings margin line kept old numbers when the plan went away. It now hides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Four findings from a second local Codex review: - Reading the export choice before a margin save still left a race with another client. POST /api/planner/prefs now accepts safety_k alone or battery_export alone, and keeps the other under the same write lock, so two clients changing different preferences never undo each other. The Plan card and Settings send only what they change. - Reopening the Planner tab started a second save queue that could race the first. Settings now keeps one queue for the page. - Where the plan caps the panels (pv_curtail_active or pv_limit_w), extra sun may be held back rather than exported. The extra-sun line now says nothing for such windows. - Before the box answered, arrow keys moved from Balanced rather than the focused style. They now move from the focused style. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Three findings from a third local Codex review: - The Plan card and Settings had separate save queues, so two writes could reach the box out of order. All preference writes on the page now go through one queue (prefsQueue), and each confirmed write is announced with the box's answer. Settings follows a style picked on the card. - Reopening Settings while a margin save was on its way showed the old stored value and then ignored the reply. The pending value now survives the redraw, and the reply updates the open tab unless a drag is under way. - In a slot that imports, extra sun lowers the import before anything is exported. The line now speaks only of sun beyond what the home and the plan need. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
cb941a7 to
14d8b13
Compare
Two findings from a fourth local Codex review, both from Settings keeping its own save queue and reads on top of the page's queue: - A pending Settings value could reach the box after a newer style picked on the Plan card. Each released slider value now goes straight into the page's queue, so writes reach the box in the order they were made. - A read started before a confirmed write could replace it. The slider now paints one page model (marginModel): the value the box last confirmed, from any control, unless a save from here is on its way. A read older than a confirmed write changes nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Two findings from a fifth local Codex review: - A style picked while another card write was on its way waited outside the page queue, so a later Settings choice could be sent first and then lose to the older pick. Card picks now go straight into the queue, and the queue lets a newer change to the same preference replace one still waiting; both callers get the newer answer. This also replaces the card's 400 ms delay. - Chrome sends no change when a drag ends where it began, so the slider stayed in drag mode and hid a failed save. A drag now ends when the pointer is released. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Problem
Refs #1482.
A beta tester asked why the plan would not put the day's solar surplus into the battery. The cause was the forecast margin (
safety_k), but the Plan card made that hard to see:Change
Plan card
safety_k, laid out like a fund's risk scale: Very careful (k 1), Careful (0.6), Balanced (0.3), Bold (0.15), Very bold (0). One sentence per style. The word "risk" stays off the card, as the existing test requires: on an energy box it reads as danger.pv_surplus_absorb_soc_capwhen set, otherwise Core's per-slotlive_pv_surplus_soc_cap(feat(plan): show when live solar may charge the battery #1485), and never during a discharge slot. It says stored, sent to the grid, or some of each. It shows only in the two household planner modes, only when the box sends both caps, and not where the plan caps the panels, so it never guesses.safety_k; the export switch sends onlybattery_export.Chart: the margin is shaded between the forecast line and the value the plan counts on, for sun and for use. Tooltip rows read "PV the plan counts on" and "Load the plan prepares for".
Settings → Planner: a live "Forecast margin (k)" control, 0–2 in steps of 0.05, that saves at once with only
safety_k, names the nearest style, and shows the margin as sun held back and use added. The margin line hides when there is no plan.One write path on the page: every preference write, from the card and from Settings, goes through one queue (
prefsQueueinweb/plan-prefs.js), so the box applies them in the order people made them. Each confirmed write is announced asftw-planner-prefswith the box's answer. The Settings slider paints one page model (marginModel): the value the box last confirmed from any control, unless a save from Settings is on its way; a read that started before a confirmed write changes nothing. The old YAML-bound field is gone. It only seeded the first boot and edited nothing after that; a YAML value now gets a one-line note saying so.Core
SafetyKDefault1 → 0.3, and the legacy enumbalancedmaps to it, so an old client asking for balanced gets the card's default.POST /api/planner/prefsacceptssafety_kalone orbattery_exportalone and keeps the other as the box holds it, under the same write lock as every preference write. Two clients changing different preferences no longer undo each other. A request with neither is still a 400./api/statusreportspv_surplus_absorb_soc_cap(0 when unset).MigrateOldDefaultSafetyK, marker keyplanner_planning_styles). k 1 was both the first-boot default and the old three-step "balanced", so a stored 1 means "the middle". Any other stored value stays, so a 0.15 is still Bold. A later choice of k 1 (Very careful) is never moved, and neither is a k a new box seeds from its settings.Why Balanced is 0.3
The owner asked for a middle that leans toward the forecast, and for evidence on whether k 1 is too careful. I replayed the home box's recorded plans in closed loop: each step re-solves with the Core DP from that variant's own state of charge, dispatches the first slot by Core's planner_arbitrage energy-path rules (idle slots hold the battery still and may only absorb surprise export up to the capture cap, charge slots back off when the meter imports more than planned, cover-load slots chase grid 0), and is charged against measured PV, house load, the car, and published prices.
Checks: with PV loss 0.7 × PV, k 0.3 is −0.17 and k 1 +8.14; with a 201-level DP grid, k 0.3 is +0.41 and k 1 +5.69. The simulation books 119.9 SEK for the recorded k where the meter booked 128.6 SEK; every variant shares that error, so differences below about 2 SEK are noise.
Reading: up to 0.3 costs the same within noise; above 0.5 the cost grows with k; k 1 cost 3.6–5.4 % more. No style, not even Very bold, bought at the dearest prices with an empty battery this week, so the extra caution had nothing to prevent. Once a box has a week of scored forecasts, Core sets the margin from the net forecast error instead. In this week's data that band comes out close to zero, because the forecasts already ran pessimistic, so k would change little there. The margin matters most for new installs, which is where k 1 hurt.
Robustness: the same sweep with corrected forecasts. This week's forecasts leaned pessimistic for a reason that is being fixed separately: the box's forecast models were reset by every update (night load came out about 2 kW too high). The forecast archive also holds the Energyplan load forecast, which was nearly unbiased that week (+38 W at night). Replaying with it instead:
(SEK for the week, after valuing the energy left in the pack.) With unbiased forecasts a moderate margin pays: k 0.3 saves 7.6 SEK against k 0, and imports at the dearest prices with an empty battery fall from 1.2 to 0.4 kWh. Balanced at 0.3 costs nothing with today's forecasts and keeps most of that gain once they are fixed, so it is the right middle in both cases.
Limits: one house, one late-September week in which the forecasts leaned pessimistic. In a week where the sun disappoints, a careful style earns its keep, and Very careful stays one tap away.
Verification
npm test: 682 pass. New tests cover the style table and its Go default, nearest-style naming, the sun and extra-sun lines, the margin split, the legacy enum, and the markup.go test ./internal/config/... ./internal/api/... ./cmd/ftw/...pass, with a new test for the one-time move.make verifypassed on this commit.web/with API answers captured read-only from the home box (style saves answered locally, never forwarded): desktop and 375 px, dark and light, all five styles, the sun line moving with the style, keyboard, a failed save, Settings fine-tune between two styles, and the Settings margin line following a save. The home box runs older Core, so the extra-sun line was checked with injected capture values.{"safety_k": 0.3}only; three quick Settings changes (0.6, 0.8, 1) sent 0.6 then 1, each with onlysafety_k, and ended on "Saved. Very careful."; the Settings margin line hid when the plan went away; no script errors.Review
Codex reviewed this PR on GitHub and four times locally. Findings fixed here: a k seeded from settings on a new box was moved to Balanced; Settings sent a stale export permission, and later a read-then-write race (now partial updates under the server's lock); overlapping and stale save replies, across tab redraws and between the card and Settings (now one page queue and one page model); the extra-sun line ignored the operator's cap, PV caps and planned imports; arrow keys started from Balanced before the box answered; a stale Settings margin line.
Browser checks with delayed answers: Settings follows a style picked on the card; a redraw during a slow save shows the value on its way and ends on "Saved."; a late read does not replace a confirmed value; Settings 0.8 and 1 followed by the card's Bold reach the box in that order, so Bold wins.
Related
live_pv_surplus_soc_cap.applyPlannerPrefs, which stays; the handler now goes throughapplyPlannerChange.web/index.htmland fix(web): pause plan, heating, settings, and card polls when hidden #1177 editsweb/plan.js, in other sections.The changeset is
minor: a visible change of the default that the owner approves here.🤖 Generated with Claude Code