Losing the solar forecast mid-run used to widen the gap at once and stop a grid charge (and a sensor that went unavailable for a minute stopped and restarted it). A run under way when the forecast is lost now keeps the gap it was planned with until it ends (Controller.weather_held(), recorded by _wx_track() each cycle a forecast is read); the wider gap applies from the next run. The plan key holds the hold state so the plan rebuilds when it ends. Home's mirror (homeview.trade_verdict) asks the same weather_held(now). Tests: test_tariff24_weather.py part 4 (going: the charge continues to the run's end, the next run is not_worth; flap of one and two reads: no stop, no restart; Home agrees) and a test_modes_p4.py scenario that runs a forecast day at a gap between 1x and 1.5x the cycle cost. DECISIONS wording follows. Co-Authored-By: Claude <noreply@anthropic.com>
67 lines
3.7 KiB
Markdown
67 lines
3.7 KiB
Markdown
# TARIFF-24 - battery modes need the tariff task
|
|
|
|
**Status: FIXED in 0.19.1 (hotfix from 0.19.0, owner go 2026-10-28).**
|
|
Follows TARIFF-10 (fetching is not trading). Tests: `test_tariff24.py`.
|
|
|
|
**The defect.** The control cycle gets prices only from
|
|
`Controller.tariff_plan`, which only `run_tariff_refresh()` stores. `amain()`
|
|
starts that task when `tariff_refresh_wanted(opts)`, which was
|
|
`provider_configured(opts) or arbitrage.enabled(opts)`. Battery modes read the
|
|
plan but were not a term. With battery modes on, a day/night or fixed
|
|
`static_profile`, `price_provider: off` and `arbitrage_enabled: false` (the
|
|
site trade switch is not read with modes on) no task ran. Then
|
|
`fresh_tariff_plan()` was None, `_price_runs()` was None and `mode_runs` was
|
|
None, so Leftover solar and Cheap hours never charged from the grid. The
|
|
Batteries screen said "No prices, so it follows the house" while Home priced
|
|
the day from `static_plan()` itself (`homeview.build_plan()`).
|
|
|
|
**The fix** (`main.py`, `tariff_refresh_wanted()`). `packmode.enabled(opts)`
|
|
is a third term. The gate stays a widening: nothing that started the task
|
|
before stops. No HTTPS call is added: with `price_provider: off`,
|
|
`resolve_tariff()` is the pure `static_plan()`.
|
|
|
|
**Not changed.** Modes off, arbitrage off, no feed still starts no task. The
|
|
control law, the price runs and every message are untouched.
|
|
|
|
**One rule, not two.** Gating on "modes on and a static profile" was
|
|
considered. It saves one arithmetic pass every 30 min on a site with modes on
|
|
and no prices at all, and it adds a second condition that has to track what
|
|
`resolve_tariff()` accepts. With no profile and no feed the plan is `no_data`,
|
|
exactly what the cycle sees without the task.
|
|
|
|
**UX-55.** Home said "no prices have arrived" (`mode_runs` None) with every
|
|
battery on Solar first. The same cause, and this fix clears it for a site with
|
|
a static profile (`test_tariff24.py` part 5). A site with no profile and no
|
|
feed still has no prices, and Home still says so.
|
|
|
|
## Round 2 (AUDIT-02 F1): the forecast replaces the "no weather" doubt
|
|
|
|
**Decided 2026-10-28 (owner), in the same hotfix.** The price gap a mode needs
|
|
was `cycle_cost_threshold_eur` x 1.5 (`Uncertainty.NO_WEATHER`) whether or not
|
|
a solar forecast was read. At the shipped cost 0.10 that is 0.15, so a
|
|
day/night gap (0.08) and many dynamic days never traded, and the screens said
|
|
only "Prices differ too little".
|
|
|
|
**Why the doubt goes with a forecast.** The widening stands in for not knowing
|
|
the weather. The forecast is that knowledge, and the day plan already trusts it
|
|
for how much to charge. Trusting it for the plan but doubting it for the gap
|
|
was one input judged twice.
|
|
|
|
**The rule** (`arbitrage.weather_uncertainty()`). With battery modes on and a
|
|
usable forecast (`solar_fc` not None, the day plan's own test) the doubt is
|
|
not added: the gap needed is the cycle cost, widened by the price product only.
|
|
No forecast, or a stale or unusable one: 1.5 x, as before. It is read in three
|
|
places that must agree: the run plan (`_price_runs()`), the charge gate
|
|
(`_mode_roles()`) and Home's mirror (`homeview.trade_verdict()`). The forecast
|
|
is read at most every 60 s and the plan key already holds "forecast or not", so
|
|
the gap follows in the cycle the read changes, both ways, with one exception:
|
|
a run planned with a forecast that is under way keeps that gap to its end
|
|
(`Controller.weather_held()`), so a sensor that goes unavailable does not stop
|
|
or restart a charge. The wider gap applies from the next run.
|
|
|
|
**Not changed.** Modes off (the site trade) keeps the doubt always. The
|
|
shipped cost 0.10 still needs a gap of 0.10 even with a forecast: a 0.08
|
|
day/night site must set *Battery cycle cost* to 0.08 or less. Showing the two
|
|
numbers on the screens (AUDIT-02 F1 option b) is a separate card.
|
|
|