feat(planner): persist forecast trust and battery export permission - #968
feat(planner): persist forecast trust and battery export permission#968frahlg wants to merge 2 commits into
Conversation
Household planner prefs live in SQLite like mode: forecast_trust (cautious/balanced/bold) maps only to pv_forecast_safety_k, and battery_export (unknown/not_allowed/allowed) maps onto the existing passive vs active planner modes. Balanced is today's k=1. Unknown export never sells from the battery. An upgrade from Active arbitrage starts unknown and is forced to passive until confirmed. YAML pv_forecast_safety_k still wins. GET/POST /api/planner/prefs plus the same fields on /api/status. HA and the app mode setters confirm export the same way as /api/mode.
| if mm, ok := control.PlannerMPCMode(mapped); ok && s.deps.MPC != nil { | ||
| s.deps.MPC.SetMode(ctx, mm) | ||
| } | ||
| } |
There was a problem hiding this comment.
Prefs remap clobbers planner modes
High Severity
applyPlannerPrefs remaps control mode whenever IsPlannerMode() is true, including planner_self and planner_cheap. Those modes are not part of the battery_export mapping, so saving prefs (even only to change forecast_trust) forces a switch to planner_passive_arbitrage or planner_arbitrage, which can enable grid charging that planner_self previously forbade.
Reviewed by Cursor Bugbot for commit 53e8777. Configure here.
| if save != nil { | ||
| _ = save(StateKeyBatteryExport, string(export)) | ||
| } | ||
| } |
There was a problem hiding this comment.
Mode confirm can revert trust
Medium Severity
ApplyExportFromMode reads Trust under one lock and writes both fields under another. A concurrent /api/planner/prefs update can land between those calls, so a mode confirm from HA or the app restores the older trust value and drops the user’s slider choice.
Reviewed by Cursor Bugbot for commit 53e8777. Configure here.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 53e8777ff1
ℹ️ 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".
| if err := s.deps.State.SaveConfig(config.StateKeyForecastTrust, string(trust)); err != nil { | ||
| return err | ||
| } | ||
| if err := s.deps.State.SaveConfig(config.StateKeyBatteryExport, string(export)); err != nil { | ||
| return err |
There was a problem hiding this comment.
Downgrade before returning on persistence failure
When changing an actively arbitraging site to unknown or not_allowed, any SQLite write failure (for example, a full disk or closed database) returns here before the control mode is changed at line 91. The in-memory preference has already been updated, so /api/status reports that export is forbidden while planner_arbitrage can continue selling from the battery; apply the fail-safe mode transition before this fallible persistence path or roll back the preference and dispatch state together.
Useful? React with 👍 / 👎.
| s.mu.Lock() | ||
| s.PVForecastSafetyK = k | ||
| request := s.beginReplanLocked(ctx, "safety_k_changed") |
There was a problem hiding this comment.
Snapshot the safety factor under the planner mutex
When a scheduled or otherwise active replan overlaps a preference POST, this assignment is protected by s.mu, but runReplan later reads s.PVForecastSafetyK without that lock at service.go:1202. The new runtime setter therefore introduces a Go data race; include the factor in replanRequest while holding the mutex, as is already done for Defaults, so each generation uses one synchronized value.
AGENTS.md reference: AGENTS.md:L55-L57
Useful? React with 👍 / 👎.
Replace Passive/Active as primary strategy buttons with follow-the-forecast and a battery-export permission. Settings keep house reserve on top and hide optimizer knobs behind a closed disclosure. Weather no longer asks for array orientation on the normal path.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
There are 4 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a54ca30. Configure here.
| }); | ||
| primary.replaceChildren(frags.primary); | ||
| advanced.replaceChildren(frags.advanced); | ||
| primary.hidden = !primary.childElementCount; |
There was a problem hiding this comment.
No return path from manual
High Severity
Filtering all planner_ keys out of the primary mode buttons removes every dashboard control that can enter a planner mode. Manual… still offers idle/self/peak/charge, and /api/planner/prefs only remaps mode when already in a planner mode, so choosing a manual mode leaves the site stuck outside the planner with no web UI way back.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit a54ca30. Configure here.
| syncPrefsUI(); | ||
| } finally { | ||
| prefsPosting = false; | ||
| } |
There was a problem hiding this comment.
Prefs updates silently dropped
Medium Severity
postPlannerPrefs returns immediately while another prefs POST is in flight, so a second action is discarded with no retry. A common sequence—releasing the trust slider then immediately clicking Allow or Keep off on the upgrade banner—can leave the export confirmation ignored even though the UI looked responsive.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit a54ca30. Configure here.




Status
This PR is PR 1 of the household-planner beta. Core prefs are in: SQLite +
/api/planner/prefs+ status fields. No UI yet.A cloud agent should continue with PR 2–4 on this branch (or stacked on it). Do not put planning docs in the repo.
Planning docs stay out of git. The full plan is in this description.
What this commit does
forecast_trust: cautious | balanced | bold → onlypv_forecast_safety_k(2 / 1 / 0)battery_export: unknown | not_allowed | allowed → existingplanner_passive_arbitrage/planner_arbitragek=1)unknown, forced to passive until confirmedpv_forecast_safety_kwins (yaml_custom)/api/planner/prefs; same fields on/api/status/api/modeRemaining work (do this next)
PR 2 — Plan card: slider + export
web/app.jsweb/plan.js: slider “Follow the forecast” (Hold reserve → Trust forecast), export permission, four sentences, upgrade bannerk·σin watts)PR 3 — Settings: simple on top, engine below
web/settings/tabs/planner.js: top = enabled, house reserve, soc_max<details>at the bottom: “Engine controls — leave these unless you are debugging.”engine=pythonor CLARABEL on top. HiGHS is implied.PR 4 — Weather: no array onboarding
PR 5 (optional, do not block beta)
Quality()/SamplesProduct rules (do not break)
control/dispatch.goModeCatalogthis beta (app/HA keep Passive/Active keys)pv_rated_wMapping
Copy
Slider: “Follow the forecast”. Left “Hold reserve”. Right “Trust forecast”.
Help: “Left keeps more in the battery if the sun might miss — closer to using the battery only for the house. Right follows the forecast fully. If the forecast is right, right earns more.”
Export: “Allow the battery to sell to the grid when the plan expects a worthwhile sale. Solar can still export when this is off. Check your electricity contract.”
Unknown: “Not checked — battery export stays off.”
Upgrade banner: “FTW used to sell from the battery on high-price hours. Allow that to continue?”
Plan sentences:
Test plan
go test ./internal/config ./internal/api ./internal/mpc ./cmd/ftwmake verifyon commitNote
Medium Risk
Changes live planner dispatch: PV safety k and whether the battery may sell to the grid, including an upgrade path that pauses Active-arbitrage export until the household confirms.
Overview
Replaces Passive/Active as the household Plan-card knobs with follow-the-forecast (cautious / balanced / bold → PV safety k 2 / 1 / 0) and a battery-export permission (unknown / not allowed / allowed). Balanced is the default. Unknown never sells from the battery. Sites that were on Active arbitrage migrate to unknown and stay on passive until they confirm.
Prefs live in SQLite (YAML is first-boot only).
GET/POST /api/planner/prefsand/api/statusexpose them; setting prefs maps export ontoplanner_passive_arbitrage/planner_arbitrageand updates MPC safety k (explicit YAMLpv_forecast_safety_kstill wins). Mode changes from the web API, HA, and the app confirm export the same way.The Plan card posts trust without flipping export, shows a live k·σ hedge line, an upgrade banner, and a one-line sale/export sentence. Settings put house reserve on top and bury engine knobs; weather keeps the map and hides array geometry behind advanced, with copy that the production pattern is learned from measured solar.
Reviewed by Cursor Bugbot for commit a54ca30. Bugbot is set up for automated code reviews on this repo. Configure here.