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
@@ -0,0 +1,68 @@
# UX-54 - a Forecast tab: the sun, the house and each battery's level, per hour
**Status: DONE 2026-10-26 (owner go 2026-10-26: "Day curve", "New tab").**
## Decision
A tab beside Home and Batteries shows today and tomorrow, per hour: the solar
forecast, the solar measured so far, the house's expected use and each
battery's expected level with its blocks. It follows the add-on, as Home does
(UX-53), and decides nothing.
- **Blocks are Home's.** Today's are the `runs` of the same plan Home draws
(grid, give, monthly cycle). Tomorrow's come from the same builders
(`homeview.runs_for()`). The words are the plan lines'. A block is on the tab
exactly where Home has one, so the UX-53 gate is followed too.
- **The sun** is what `solarfc` reads. An hourly split (Solcast
`detailedForecast`, Open-Meteo `wh_period`) is used as given. A daily total
(Forecast.Solar, and any other state-only sensor, the owner's three "today
remaining" and three "tomorrow") is spread over the 06:00-20:00 bell, and the
tab says so.
- **The house** is `HouseProfile.estimator()`, never below the night median, as
the plan reads it.
- **The level did not exist** (the plan holds targets and run blocks only), so
`daycurve.curve()` derives it: each battery has the role `modetrade.decide()`
gives (follow the house, hold, give, charge from the grid, rest), spare solar
goes to Solar first batteries first, a shortfall is met by those that may
give, both at `dayplan.EFF`, inside the plan's floor and ceiling, from the
level now. It counts on `solar_trust_pct` of the sun, as the plan does, and
says so. Grid charging goes to `modetrade.charge_to()`. It is an estimate.
- **Measured solar** is kept per local hour in memory (`SolarToday`), from the
reading the control cycle already takes; nothing is read back.
- **On request.** `GET forecast` builds the tab (`Controller.forecast_payload()`
reads `/status`' plan first, so today's blocks are the very ones Home has). No
Home Assistant call, nothing cached, nothing in the cycle but one in-memory
add. The page asks every 30 s while the tab shows.
- **Not known yet is a note, not an empty chart:** modes off, no forecast set,
forecast missing or old, learning, no tomorrow forecast, a monthly cycle, no
level reading, control stopped, shadow.
## What does not change
No decision, command, timing or option key. The one write in the control path
is `SolarToday.add()` after the solar read, in a `try`, in memory.
## Limits
- Prices are planned about a day ahead (`priceruns.HORIZON`), so tomorrow's
blocks appear as the day comes near. The tab says so.
- A give run drawn on a battery that is empty now has no block on Home (no
energy to give), but the curve gives in it if the sun fills it first.
- Tomorrow shows price blocks only. A monthly cycle is drawn today, where the
curve then stops for that battery.
- The curve uses the arbitrage floor and ceiling, as the plan does, not the
battery's own cut-out level. The house profile is a median per hour, net of
the solar the house used.
- Measured solar starts when the add-on starts; the tab says from when.
## Found, not changed
`dayplan.plan()` reads the forecast hour by hour with `Forecast.between(s, e)`.
For a state-only forecast `between()` takes its start as the moment the "left
today" figure was read, so each later hour is spread over what is left of the
bell from that hour: on the owner's shape 14.4 kWh left gives 57 kWh over the
hours to midnight (`test_ux54.py` part 1). The plan's sun is overcounted for
those sites. It does not show on a site whose batteries are all Solar first
(no level is planned for them). The tab measures both ends from now
(`dayview.sun_kwh()`), which keeps the total and gives the same hours for an
hourly split. A card should fix the plan.