- Tomorrow's grid block is drawn to where control stops charging. A cheap run is sized for all batteries together; a slower one is still below `charge_to()` when it ends and control plans the next cheap slots. The tab asks `modetrade.energy()` and `priceruns.plan()` with the packs at the curve's level and extends the run, so the curve charges as long. - Each battery's block ends where it is at its own level (grid) or at the floor (give). The curve puts a point where a battery reaches its charge level inside a step. - `homeview.mode_runs(ahead=f, rp=...)` reads each block at the level the curve has when it starts, with Home's own filter and MIN_KWH: no give block at the floor, no grid block at the planned level. Home never passes either. - `no_tomorrow` names the hour from `solarfc.BELL_FROM_H`, where the curve stops. - test_ux54.py part 7 compares control (`_mode_roles()` every 15 minutes) with the tab with no tolerance, and adds the cloudy / floor case. No decision, command, timing or option key changes. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
103 lines
6.1 KiB
Markdown
103 lines
6.1 KiB
Markdown
# 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.
|
|
- **Tomorrow is read ahead.** Home sizes a block from the battery's level now
|
|
and draws none when there is no room or energy (`room()` and `plan_need()`
|
|
in `homeview.mode_runs()`, the leg sizing in `trade_runs()`). For a day that
|
|
has not started that level says nothing, so tomorrow's blocks are built with
|
|
`ahead` (Home never passes it): `mode_runs()` reads each block at the level
|
|
the curve has when it starts, with its own filter and `MIN_KWH`, so there is
|
|
no give block where the battery is at the floor and no grid block where it
|
|
is at its planned level. Where the curve has no level yet, a charge is read
|
|
from the floor and a give from the ceiling. The blocks and the curve follow
|
|
each other and are taken to a fixed point. The gate (`mode_gate`, UX-53) is
|
|
not bypassed.
|
|
- **A block lasts as long as control acts.** A cheap run is sized for all the
|
|
batteries charging together, so a slower one is still below
|
|
`modetrade.charge_to()` when it ends, and control plans the next cheap slots
|
|
(`Controller._price_runs()`). `dayview._levels()` asks the same functions,
|
|
`modetrade.energy()` and `priceruns.plan()`, with the packs at the curve's
|
|
level, and draws the grid block to where control stops. Each battery's block
|
|
then ends where the curve has it at its own level (the curve puts a point
|
|
there), and a give block where it reaches the floor (a step end, within the
|
|
0.1 % the tab shows). Control, walked every 15 minutes through tomorrow
|
|
(`test_ux54.py` part 7, no tolerance), charges and gives in exactly the
|
|
quarters the tab has a block.
|
|
- **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. Spare solar is `trust x max(0, sun - house)` and what is stored
|
|
loses `EFF`, the plan's own `trust x EFF x max(0, sun - house)` (10 kWh of
|
|
sun, 2 kWh of house, 70 %: 5.32 kWh stored). A shortfall is not scaled by
|
|
trust. The tab says it counts on `solar_trust_pct`. 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 a block of
|
|
tomorrow later than 24 h from now, or one that needs prices not yet
|
|
published, appears as the day comes near. The tab says so.
|
|
- Today's blocks are Home's, and are not extended or shortened: control may
|
|
plan more once they end, and Home draws it then. The curve stops charging
|
|
at its level (`charge_to()`) and giving at the floor.
|
|
- Today, a give run in the plan on a battery that is empty now has no block on
|
|
Home (no energy to give). The tab equals Home, and says so: "The add-on plans
|
|
to give power. No block shows while the battery has no energy to give." The
|
|
curve gives in that run if the sun fills the battery first.
|
|
- With no forecast for tomorrow the curve stops at the first hour of the sun,
|
|
`solarfc.BELL_FROM_H` (06:00): `Forecast.between()` has no answer past it. The
|
|
note names that hour from the constant.
|
|
- 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.
|