UX-54: a Forecast tab with the sun, the house and each battery's level, per hour
checks / unit (pull_request) Successful in 48m29s
infra-pin / fresh (pull_request) Successful in 1s

A new tab beside Home and Batteries for today and tomorrow: the solar
forecast, the solar measured so far, the house's expected use and each
battery's expected level, with the plan's own charge and give blocks under it.

- Blocks are Home's: today's are the plan's `runs`, tomorrow's come from the
  same builders (`homeview.runs_for()`), in the plan lines' words.
- The sun is what solarfc reads; a daily total is spread over the bell and the
  tab says so. The house is HouseProfile.estimator(), as the plan reads it.
- The level is new: `daycurve.curve()` gives each battery the role
  `modetrade.decide()` gives it and moves it hour by hour inside the plan's
  floor and ceiling. It is an estimate and counts on `solar_trust_pct`.
- Built on request (`GET forecast`), no Home Assistant call. Measured solar is
  kept per hour in memory from the reading control already takes.
- A part not known yet is a note: modes off, no forecast, learning, no
  forecast for tomorrow, a monthly cycle, no level, stopped, shadow.

No decision, command, timing or option key changes. Found, not changed:
`dayplan.plan()` over-counts a state-only forecast (DECISIONS/UX-54).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
This commit is contained in:
glenn schrooyen
2026-10-26 19:38:59 +01:00
co-authored by Claude
parent a1d07030f8
commit b6b2d1b184
16 changed files with 1912 additions and 11 deletions
+26
View File
@@ -246,3 +246,29 @@ select.sel:disabled{opacity:.6}
.simple .card .lbl{font-weight:600;font-size:15px}
.simple .inv .hd{border-bottom:0}
.simple .inv .f{border-top:0;padding-top:0}
/* UX-54: the Forecast screen */
.fcline{text-align:right}
.fcnote{background:var(--info-bg);border-radius:10px;padding:8px 10px;margin:0 0 8px}
.fcbars{display:flex;align-items:flex-end;gap:2px;height:110px}
.fcol{flex:1;position:relative;height:100%;min-width:0}
.fcol i{position:absolute;display:block}
.fcol .sun{bottom:0;left:0;right:0;background:var(--solar);border-radius:3px 3px 0 0;opacity:.85}
.fcol .sun.est{opacity:.5}
.fcol .got{bottom:0;left:30%;right:30%;background:repeating-linear-gradient(135deg,var(--ink) 0 3px,var(--card) 3px 5px);border:1px solid var(--ink);border-bottom:0}
.fcol .use{left:-1px;right:-1px;height:3px;background:var(--house)}
.fcols{position:absolute;inset:0;display:flex;gap:0}
.fcols>i{flex:1;min-width:0;display:block}
.lvlchart{position:relative;height:110px;margin:18px 0 4px}
.lvlchart svg{position:absolute;inset:0;width:100%;height:100%;display:block}
.lvlchart .yl{position:absolute;right:2px;font-size:10px;color:var(--faint);pointer-events:none}
.fcnow{position:absolute;top:-4px;bottom:0;width:2px;background:var(--ink);opacity:.7;pointer-events:none}
.fcnow span{position:absolute;top:-15px;left:-10px;font-size:10px}
.legend i.sun{background:var(--solar)}
.legend i.got{background:repeating-linear-gradient(135deg,var(--ink) 0 2px,var(--card) 2px 4px);border:1px solid var(--ink)}
.legend i.use{height:3px;background:var(--house)}
.legend i.lv{height:3px;background:var(--batt)}
table.fct{width:100%;border-collapse:collapse;font-size:12px;font-variant-numeric:tabular-nums}
table.fct th,table.fct td{padding:4px 3px;text-align:right;border-bottom:1px solid var(--line)}
table.fct th:first-child,table.fct td:first-child{text-align:left}
table.fct th{color:var(--muted);font-weight:600}
.ymax{position:absolute;top:-2px;left:2px;font-size:10px;color:var(--faint);pointer-events:none}