Repository navigation
feat(planner): persist forecast trust and battery export permission - #968
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.
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.
miravoss26
left a comment
There was a problem hiding this comment.
Household planner prefs: forecast trust (cautious/balanced/bold → PV safety k) and a battery-export permission (unknown/not_allowed/allowed), replacing the implicit Passive/Active split as the source of truth for whether the battery may sell.
Correctness — the safety-relevant bit is the migration: any site that was on planner_arbitrage before this ships has no battery_export row in SQLite yet, so ResolvePlannerPrefs/DeriveBatteryExport maps it to unknown, and main.go's boot check (ctrl.Mode == ModePlannerArbitrage && export != Allowed) force-switches it to passive until the household explicitly confirms via app/API/HA. Traced this through and it holds — a household that already has allowed stored is untouched, only the never-confirmed ones get paused. Good test coverage on the resolution matrix (TestResolvePlannerPrefsActiveUpgradeAsks etc.) and the API tier wiring (POST /api/planner/prefs is Actuate, matching /api/mode).
Minor, non-blocking — applyPlannerPrefs recreates s.deps.PlannerPrefs if it's nil with no lock around the field write; two concurrent POST /api/planner/prefs on a nil-prefs server could race. Looks dead in practice since main.go always constructs Deps with a non-nil PlannerPrefs, so flagging for awareness rather than asking for a change.
CI green across the board. Safe to merge from my read.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 9205e02. Configure here.
| 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 save always reapplies mode
High Severity
applyPlannerPrefs always calls ApplyMode whenever the box is already in a planner mode, including when the mapped mode is unchanged. ApplyMode clears a battery manual hold and resets the PI on every Plan-card save, so moving only the forecast slider can drop a hold and glitch dispatch. The same path also remaps planner_self and planner_cheap onto planner_passive_arbitrage or planner_arbitrage.
Reviewed by Cursor Bugbot for commit 9205e02. Configure here.
…seed (#1018) Owner report (#1017): the Plan card's forecast-trust slider was permanently disabled on any site with an explicit pv_forecast_safety_k in config.yaml, with a note telling the operator to go edit the file — a config file acting as a user interface. The reporter's box had pv_forecast_safety_k: 1, the same value as "balanced", so the lock bought nothing at all. The field now follows the same stored-wins contract forecast_trust got in #968: on a first boot with nothing stored it seeds the nearest trust step (k ≤ 0.25 → bold, < 1.5 → balanced, else cautious) and is never consulted again — EffectiveSafetyK is the trust mapping, YAMLCustomK and the dead PVSafetyK helper are gone, the API stops reporting yaml_custom/planner_yaml_custom, and the UI loses the disable + the "config.yaml wins" note. Closes #1017. Claude-Session: https://claude.ai/code/session_01C1d4uknzs7QUv7nE9rW4fi Co-authored-by: Claude Fable 5 <noreply@anthropic.com>




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 which planner mode runs (battery grid export) and MPC PV hedging on boot, API, HA, and UI; includes a migration that forces passive arbitrage until export is confirmed.
Overview
Adds household planner preferences — follow-the-forecast (
cautious/balanced/bold) and battery export permission (unknown/not_allowed/allowed) — persisted in SQLite with YAML first-boot defaults, exposed onGET/POST /api/planner/prefsand/api/status, and wired through app link, HA, and/api/modeso export permission stays in sync with planner modes.Forecast trust maps to PV downside safety k (2 / 1 / 0) via
EffectiveSafetyK, with explicit YAMLpv_forecast_safety_kstill winning; changing trust triggersmpc.SetSafetyKand a replan. Export permission maps toplanner_passive_arbitragevsplanner_arbitrage;unknownnever sells from the battery. Upgrades from active arbitrage without stored export getunknownand are downgraded to passive until the user confirms.Plan card UI replaces Passive/Active as primary strategy controls with the forecast slider, export checkbox/upgrade banner, live k·σ hedge line, and plan export sentences;
renderModeCatalogskipsplanner_*keys (HA/app catalogs unchanged). Settings puts house reserve and enable on top and hides engine/optimizer knobs behind a closed Engine controls disclosure; weather keeps the map on the normal path and moves array geometry editing into advanced copy focused on measured solar.Reviewed by Cursor Bugbot for commit 9205e02. Bugbot is set up for automated code reviews on this repo. Configure here.