Files
goodwe-addon/goodwe_controller/DECISIONS/UX-54-forecast-tab.md
T
glenn schrooyenandClaude 5ab8622265
checks / unit (pull_request) Successful in 48m22s
infra-pin / fresh (pull_request) Successful in 1s
UX-54 round 3: tomorrow's blocks last as long as control acts, give blocks checked at run start
- 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
2026-10-27 00:53:58 +01:00

6.1 KiB

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.