UX-54's Forecast tab (release/1.0) re-plans tomorrow's cheap run as control will, with the gap `required`. It still added the "no weather" x1.5 always, while control (TARIFF-24, from master) drops it when a forecast is read. In the UX-54 scenarios the tab asked 0.225 where control needs 0.15. It now asks `arbitrage.weather_uncertainty(True, c.solar_fc)`, so the two agree again, as they did on release/1.0 alone. Display only; no control change. The tab's blocks in every UX-54 scenario, every 2 h, are identical to release/1.0. test_ux54.py pins it (the old line fails it). The TARIFF-24 DECISIONS note names the tab as the fourth place that reads the rule. Co-Authored-By: Claude <noreply@anthropic.com>
71 lines
4.0 KiB
Markdown
71 lines
4.0 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 four
|
|
places that must agree: the run plan (`_price_runs()`), the charge gate
|
|
(`_mode_roles()`), Home's mirror (`homeview.trade_verdict()`) and the Forecast
|
|
tab's longer cheap run (`dayview.py`, from release/1.0). 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 Cheap hours charge (a Leftover solar battery still stops: without
|
|
a forecast it has no planned level). The wider gap applies from the next run.
|
|
A run that has started runs to its end: a give run that starts during the hold
|
|
is kept after it (`keep_started()`), with no grid charge or sale.
|
|
|
|
**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.
|
|
|