- DECISIONS: the Cheap hours change and why; the no-sun replay (the gain is Leftover solar buying the evening's shortfall; the forecast added 0.14 EUR); the reconstruction bias stated plainly. - DOCS: Cheap hours ignores the forecast; a reading from before midnight is stale. - The forecast line: "Solar still to come today: ..." (it is what is left, and it stays within the 12-word limit). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8.5 KiB
8.5 KiB
OPT-11 - battery modes, per battery
A battery's mode (on/off, Solar first, Leftover solar) is a soft unit limit at
P_MODE = 35 that always holds 0 W. A SET above 35 (a safety stop) overrides
it; a limit above 35 (the peak guard) makes it give way only as far as that
limit needs, and a battery its mode holds back then carries only what the
others cannot (arbiter._unit_modes(), _site_mode()). Design and owner answers: goodwe-session/OPT-11-design.md (2026-09-27);
P1 is its section 6 P1, and the pull request lists what was measured.
- Off by default (
battery_modes_enabled); off, commands are byte-identical toa5dddad(test_modes.pypart 1). - Off is a mode range, so the peak guard can still use an Off battery when the others cannot cover the floor (design sections 2-3). Whether Off should be absolute is an open owner question.
- Review (PR #149): releasing only when the peak floor bound S let a trade won
above the floor keep the modes and place less than the floor, and let a
capped grid charge release an Off battery. Both are graded in
test_arbiter.pypart 11. The loop bound leaves a side that reaches the rail to the rail, so SAFETY-03 still flags a loop winding into it. - P2 (HA entities): a select and a switch per battery, only while modes are
on; commands go through
handle_action(), the page's path. A retained command is dropped, not applied: the store already holds the last choice, and a replay after a restart could otherwise undo a newer one. The config topics announced are kept in the store (mqtt_mode_configs), so turning modes off removes exactly those, and an install that never turned them on publishes whated739d4did (test_mqtt.pypart 10). A removed topic stays in that list and its empty config is re-sent on each start, so a start with no broker cannot lose it; re-sending is harmless. The HA options are the page's English names, pinned by a test, and are not renamed with the page: automations send them. - In P1 only Off closes a direction; Leftover solar differs by charging last.
- P3 (price runs,
priceruns.py,modetrade.py): each battery's range is narrowed by its role this tick (hold, give, charge, rest, sell); main.py files the intersection with the switch, so it still holds 0 W. With modes on the site trade is filed by the modes, sized from the loop bound (so no battery a mode or a refusal holds is asked for watts, P1's unmet share) and the guard's headroom;arbitrage.decide()runs only with modes off, which stays byte-identical to7a00f12(test_modes_p3.pypart 1). - SAFETY-46 item 2 is decided as the owner answered (2026-09-25): a trade is a run of consecutive firm slots. It applies with battery modes on; the modes-off trade keeps its one-period shape, because that path must not move.
- Choices that were measured, not assumed (tools/opt11p3_replay.py):
- a sale needs the guard to have measured the quarter, not to be clear: a 5 kW discharge alone reads "engaged" on export and flipped the sale every 5 s;
- a grid charge makes the others rest, so it starts only at 500 W and, once the guard or the headroom stops it, stays off for that 15-minute period (without: 13 flips in 4 minutes under a 3.5 kW house);
- stored energy is valued at the last cheap run for 12 h, and a charge needs its own run to clear the spread (without: a night charge was spent on flat morning prices, then charging went on at them);
- a run edge reseeds the loop at the house's level (a 3 kW import spike removed; about 0.1 x rating still overshoots as export).
- Replay (tools/opt11p3_replay.py, 16 recorded days, EPEX hourly prices a
year earlier, modelled batteries), net EUR, lower is better: P2 19.33, P3
Solar first + Leftover solar 19.57, with a sale 19.60, the old one-period
trade 18.14. Review of #153 (same model, with a per-action split):
- the cheapest hours were at midday on 15 of 16 days, so Leftover solar (acting as Cheap hours until P4) bought 12.2 kWh there that the sun would have given: 7 kWh more exported, 12 kWh less solar stored. Holding for the expensive hours saved 4 kWh of import, not enough. With no grid charge on Leftover solar: 19.44;
- the old trade charged one hour, so the afternoon sun still fit;
- a sale earned the wholesale price (2.27 EUR for 11.4 kWh) and the house then bought the same energy back (2.29 EUR): a wash before wear. It pays only when the sale hour's wholesale price beats the later import price, levies included (about 0.13 EUR/kWh above that hour's wholesale here);
- valuing the charge left at the end, wear and real Flemish levies make selling worse and do not change the order.
- Owner decision 2026-09-29 (option A): selling is off until it can be judged
on the price the grid pays (
packmode.SELLING_ENABLED, code kept), and Leftover solar takes solar only until P4's forecast (packmode.GRID_CHARGE_MODES); Cheap hours only still charges from the grid. Replayed (16 days, as above): Solar first + Leftover solar 19.44 EUR net against P2's 19.33; Solar first + Cheap hours only 19.57. - Not done in P3: the forecast (P4), and a headroom too short for the run moving the rest to the next cheap periods.
P4: the solar forecast and the day plan (2026-09-29)
- Input: a Home Assistant forecast sensor (
pv_forecast_entity, kWh left today;pv_forecast_tomorrow_entityoptional), read byapp/solarfc.py. Supported shapes: Forecast.Solar (state only), Solcast (detailedForecast) and Open-Meteo Solar Forecast (wh_period). The unit is read, not guessed; older than 3 h is stale (one missed hourly poll must not flag it). - Trust:
solar_trust_pct, default 70 (owner answer 5). - House use: nothing the add-on recorded fitted.
tariff.dbholds grid import per quarter, which the batteries shape. Sodayplan.HouseProfilekeeps grid + battery per local hour for 7 days in the store (house_profile), median per hour. It is net of the solar the house used, so the solar sum never takes it below the night median. Solar readings are not used (TEL-11). The first plan needs one day of it. - Verdict (
dayplan.plan()): trust x sum max(0, forecast - house) x 0.95 until the expensive run the cheap run is for. Solar first takes it first.- Leftover solar may then charge from the grid up to min(ceiling, floor + the house's use in the expensive runs / 0.95), less its sun share: only the shortfall.
- Cheap hours only ignores the plan and fills to the ceiling (design section 1; review of #156, see the replay below).
- Solar first never charges from the grid (owner, 2026-09-29).
- The level is kept unless it moves by min(1 kWh, 10 %). With no plan
(no, stale or failed forecast, no house profile, no capacity) every mode
is P3 option A exactly:
test_modes_p4.pypart 1 pins 12 digests from5a4cdc2(modes on, with and without an unavailable forecast). - The plan is made with the runs (every 15 min, or at once when the forecast comes or goes). A lost forecast drops the levels at the next cycle.
- Not done: the design's scaling by measured solar from 10:00 (it needs the
solar readings TEL-11 keeps out), the HA
goodwe_<inv>_plansensor, and FCST-01. - Replay (
tools/opt11p4_replay.py, 16 recorded days, Solar first + Leftover solar, net EUR, lower is better; site solar rebuilt from the recording, forecast errors from Elia's day-ahead Belgian solar forecast):- year-earlier hourly prices: P2 19.33, P3 option A 19.44, old one-period trade 18.14, P4 19.15 (perfect forecast 19.14, 1.5 x too high 19.16, missing 19.44);
- the days' real 2026 quarter-hourly prices (held out): P2 32.26, option A 32.01, old trade 31.40, P4 29.19 (perfect 29.21, too high 29.28, missing 32.01).
- Solar first + Cheap hours only, skipping its sun share (the first build): 19.57 (same as P3) and 29.72 against P3's 29.41. The shorter, moved runs left B lower, so later midday top-ups rested A while the sun shone. Cheap hours now ignores the plan, which is P3 exactly.
- A forecast of no sun at all gives 19.14 and 29.33. So nearly all of the gain is Leftover solar buying the evening's shortfall in the cheap run; the forecast itself was worth 0.14 EUR in 16 September days.
- Bias: the site's solar is rebuilt from the same net use the replay plays, and "recorded" keeps its hourly shape and adds only the national daily error. Both favour P4; "national" (no house leakage) gives 29.25. No threshold was tuned to these days. The 2026-09-24 PV recording could not be replayed: it is only in the live Home Assistant.