TARIFF-24 (hotfix 0.19.1): battery modes with a day/night tariff and no price feed never get prices
Defect. With battery modes on, a day/night or fixed static_profile, price_provider: off and arbitrage_enabled: false, tariff_refresh_wanted() was false, so amain() never started run_tariff_refresh(). tariff_plan, fresh_tariff_plan() and mode_runs stayed None: Leftover solar / Cheap hours never charged from the grid, and the Batteries screen said "No prices, so it follows the house" while Home priced the day from static_plan() itself.
Fix.main.pytariff_refresh_wanted(): packmode.enabled(opts) is a third term. With price_provider off, resolve_tariff() is the pure static_plan(): no HTTPS call is added (the test replaces the feed loader with one that fails). Docstring shortened to what is true now; history stays in DECISIONS.
Tests (test_tariff24.py, 37 checks): the gate by configuration, read from amain()'s own guard; the real Controller + real run_tariff_refresh() over a night (cheap run, Cheap hours battery charges, Leftover solar does not, no no_prices); a gap below the cycle cost gives no_spread; Home and the Batteries screen agree; UX-55. Reproduced on 793a676 (18 failures there, 0 here). Mutation: reverting the gate fails 18 checks.
Not touched: control, price runs, messages, master, release/1.0. No merge, no tag.
Round 2: AUDIT-02 F1 (owner, 2026-10-28)
With battery modes on and a usable solar forecast (solar_fc not None), the price gap a mode needs is Battery cycle cost (widened by the price product only), no longer x1.5 for "no weather". Without a forecast it stays 1.5 x. One helper, arbitrage.weather_uncertainty(), read by _price_runs(), _mode_roles() and Home's mirror homeview.trade_verdict(); the modes-off trade path is untouched. Tests: test_tariff24_weather.py; four test_modes_p4.py scenarios keep the 0.30 gate they were written for. DOCS, CHANGELOG 0.19.1 and the DECISIONS note updated.
## TARIFF-24 (hotfix 0.19.1): battery modes with a day/night tariff and no price feed never get prices
**Defect.** With battery modes on, a day/night or fixed `static_profile`, `price_provider: off` and `arbitrage_enabled: false`, `tariff_refresh_wanted()` was false, so `amain()` never started `run_tariff_refresh()`. `tariff_plan`, `fresh_tariff_plan()` and `mode_runs` stayed None: Leftover solar / Cheap hours never charged from the grid, and the Batteries screen said "No prices, so it follows the house" while Home priced the day from `static_plan()` itself.
**Fix.** `main.py` `tariff_refresh_wanted()`: `packmode.enabled(opts)` is a third term. With `price_provider` off, `resolve_tariff()` is the pure `static_plan()`: no HTTPS call is added (the test replaces the feed loader with one that fails). Docstring shortened to what is true now; history stays in DECISIONS.
**Tests** (`test_tariff24.py`, 37 checks): the gate by configuration, read from `amain()`'s own guard; the real Controller + real `run_tariff_refresh()` over a night (cheap run, Cheap hours battery charges, Leftover solar does not, no `no_prices`); a gap below the cycle cost gives `no_spread`; Home and the Batteries screen agree; UX-55. Reproduced on 793a676 (18 failures there, 0 here). Mutation: reverting the gate fails 18 checks.
**Release parts.** `config.yaml` 0.19.1, CHANGELOG 0.19.1, `DECISIONS/TARIFF-24-modes-need-the-tariff-task.md`.
**Not touched:** control, price runs, messages, `master`, `release/1.0`. No merge, no tag.
## Round 2: AUDIT-02 F1 (owner, 2026-10-28)
With battery modes on and a usable solar forecast (`solar_fc` not None), the price gap a mode needs is *Battery cycle cost* (widened by the price product only), no longer x1.5 for "no weather". Without a forecast it stays 1.5 x. One helper, `arbitrage.weather_uncertainty()`, read by `_price_runs()`, `_mode_roles()` and Home's mirror `homeview.trade_verdict()`; the modes-off trade path is untouched. Tests: `test_tariff24_weather.py`; four `test_modes_p4.py` scenarios keep the 0.30 gate they were written for. DOCS, CHANGELOG 0.19.1 and the DECISIONS note updated.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
With battery modes on, a day/night or fixed static_profile, no price feed and
arbitrage off, tariff_refresh_wanted() was false: no tariff task, no
tariff_plan, mode_runs None, so Leftover solar / Cheap hours never charged from
the grid and the Batteries screen said "No prices". packmode.enabled(opts) is
now a third term of the gate. With price_provider off the task is pure
arithmetic on static_profile, so no HTTPS call is added.
Also: version 0.19.1, CHANGELOG entry, DECISIONS note, test_tariff24.py, and a
shorter tariff_refresh_wanted() docstring (history stays in DECISIONS).
Co-Authored-By: Claude <noreply@anthropic.com>
With battery modes on, the price gap a mode needs was always widened x1.5 by
Uncertainty.NO_WEATHER, even with a solar forecast read, so a 0.08 day/night
gap never traded at the shipped cycle cost. The doubt is now added only when
no usable forecast is read (solar_fc is None, the day plan's own test), in the
run plan (_price_runs), the charge gate (_mode_roles) and Home's mirror
(homeview.trade_verdict). The rule is one helper, arbitrage.weather_uncertainty();
the modes-off trade path (main.py cycle) keeps its literal NO_WEATHER.
DOCS "Battery modes" sentence, CHANGELOG 0.19.1 line and the DECISIONS note
follow. Tests: test_tariff24_weather.py.
Co-Authored-By: Claude <noreply@anthropic.com>
With a forecast read the gap needed is the cycle cost (0.20), not 1.5 x (0.30),
so a midday 0.21 gap now plans a grid top-up that four scenarios (sunny, Cheap
hours partly, forecast too high, refusal) are not about. They ask for cost 0.30
with the forecast; the no-forecast leg keeps 0.20 (= 0.30 widened).
Co-Authored-By: Claude <noreply@anthropic.com>
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>
Review TARIFF-24 (Opus reviewer), head 97686319f38d73c39bee8652e16d2e786c11aa39
Verdict: approve. Merging into hotfix-0.19.1 with head_commit_id pinned to this sha. CI run 1422 (checks / unit) is green on this sha (84 PASS, graded as the merge into hotfix-0.19.1), and so is infra-pin / fresh (run 1423). My own offline suite on this head: 84 PASS, 0 FAIL (--network none, GOODWE_REQUIRE_HWSIM=1, hwsim at pin 3b4ff84).
What I checked myself:
Gate: I reproduced the defect on 793a676: the task does not start, mode_runs is None, both cards say no_prices, no night charge. On the head the task starts, the run plan is on and the Cheap hours battery charges. With price_provider off, resolve_tariff() returns static_plan(), which is pure, so no network or HA call is added. Modes off and arbitrage-on runs are byte-identical to base.
Rule: the run plan, the charge gate and Home's mirror all use weather_uncertainty(). The modes-off path (main.py:4929) is unchanged. A tree with the doubt forced back reproduces base's digests for every forecast case, so the gap is the only thing that changed.
Hold: I probed restart, DST night, stale or rebuilt plan, forecast lost before the run, back-to-back runs and churn. The hold cannot outlive its recorded run end. It never applies without a forecast sensor, and nothing in it can raise. The plan rebuilds at most once more per hold.
Tests: the four GATE_030 scenarios are row-for-row identical to base at 0.20 (same gate). The 30-min task restart in the harness matches the real refresh. 5 of the dev's mutations reproduced (caught). I added 3 of my own: 1 caught, 2 survived (see below).
Findings (none block the hotfix):
Low, test gap (main.py:2235): take out the if self.solar_fc is None: return guard in _wx_track() and all of test_tariff24_weather.py still passes. But the code is then wrong. Example: the forecast is lost at 21:45 during the day give run, and the hold flows on into the 22:00 cheap run. The battery then charges from the grid on the narrow gap with no forecast. The guard in this PR prevents that; no test pins it. Add a check for that case in the forward-port.
Low, behaviour (priceruns.py:193 via main.py:2271): a give run that starts during a hold, on a plan rebuilt with the narrow gap, is kept past the hold's end by keep_started(). Measured: a cheap run until 07:45, then a give run 07:00-10:00. The give run stayed on until 10:00. The battery role is give instead of house, with the same range (-r, r). There is no grid charge or sale (both gates use the wide gap). Only the Home and Batteries display and the discharge order change.
Low, text (DECISIONS/TARIFF-24-modes-need-the-tariff-task.md:59): "a sensor that goes unavailable does not stop or restart a charge" is true for Cheap hours only. A Leftover solar charge still stops when the forecast goes: it has no planned level (test_modes_p4.py:683, unchanged). Fix this in the forward-port.
Info (homeview.py:271, :221): with modes on, Home does not draw trade_verdict()'s result. price_windows() and mode_runs() read c.mode_runs. With arbitrage off, trade_verdict() returns off before the mirror. The mirror change is harmless, but the "Home's mirror" checks test a value Home does not show (the test turns arbitrage on to reach it).
Info (main.py:2271):held in the plan key catches no mutation. Run ends sit on quarter boundaries, and the slot is already in the key. Keeping it is correct for profiles with off-quarter boundaries.
Info (CHANGELOG.md:12 vs DECISIONS:64): the CHANGELOG says 0.07 or less, the DECISIONS note says 0.08 or less. Measured on 0.34/0.26: 0.08 still trades, 0.0801 does not. Both are true; the CHANGELOG is the safer advice.
UX-55: only partly cleared. "No prices have arrived" is gone. But with every battery on Solar first, on 0.34/0.26 at cost 0.10 (or 0.07 without a forecast), Home now says "No trading on price today. The price gap is too small." UX-55 wants "no battery trades on price". Keep UX-55 open.
No strings changed by me. Full report: reports/TARIFF-24-review-report.md.
## Review TARIFF-24 (Opus reviewer), head `97686319f38d73c39bee8652e16d2e786c11aa39`
**Verdict: approve. Merging into `hotfix-0.19.1` with `head_commit_id` pinned to this sha.** CI run 1422 (`checks / unit`) is green on this sha (84 PASS, graded as the merge into `hotfix-0.19.1`), and so is `infra-pin / fresh` (run 1423). My own offline suite on this head: 84 PASS, 0 FAIL (`--network none`, `GOODWE_REQUIRE_HWSIM=1`, hwsim at pin 3b4ff84).
What I checked myself:
- **Gate:** I reproduced the defect on 793a676: the task does not start, `mode_runs` is None, both cards say `no_prices`, no night charge. On the head the task starts, the run plan is on and the Cheap hours battery charges. With `price_provider` off, `resolve_tariff()` returns `static_plan()`, which is pure, so no network or HA call is added. Modes off and arbitrage-on runs are byte-identical to base.
- **Rule:** the run plan, the charge gate and Home's mirror all use `weather_uncertainty()`. The modes-off path (main.py:4929) is unchanged. A tree with the doubt forced back reproduces base's digests for every forecast case, so the gap is the only thing that changed.
- **Hold:** I probed restart, DST night, stale or rebuilt plan, forecast lost before the run, back-to-back runs and churn. The hold cannot outlive its recorded run end. It never applies without a forecast sensor, and nothing in it can raise. The plan rebuilds at most once more per hold.
- **Tests:** the four `GATE_030` scenarios are row-for-row identical to base at 0.20 (same gate). The 30-min task restart in the harness matches the real refresh. 5 of the dev's mutations reproduced (caught). I added 3 of my own: 1 caught, 2 survived (see below).
Findings (none block the hotfix):
- **Low, test gap (main.py:2235):** take out the `if self.solar_fc is None: return` guard in `_wx_track()` and all of `test_tariff24_weather.py` still passes. But the code is then wrong. Example: the forecast is lost at 21:45 during the day give run, and the hold flows on into the 22:00 cheap run. The battery then charges from the grid on the narrow gap with no forecast. The guard in this PR prevents that; no test pins it. Add a check for that case in the forward-port.
- **Low, behaviour (priceruns.py:193 via main.py:2271):** a give run that starts during a hold, on a plan rebuilt with the narrow gap, is kept past the hold's end by `keep_started()`. Measured: a cheap run until 07:45, then a give run 07:00-10:00. The give run stayed on until 10:00. The battery role is `give` instead of `house`, with the same range (-r, r). There is no grid charge or sale (both gates use the wide gap). Only the Home and Batteries display and the discharge order change.
- **Low, text (DECISIONS/TARIFF-24-modes-need-the-tariff-task.md:59):** "a sensor that goes unavailable does not stop or restart a charge" is true for Cheap hours only. A Leftover solar charge still stops when the forecast goes: it has no planned level (test_modes_p4.py:683, unchanged). Fix this in the forward-port.
- **Info (homeview.py:271, :221):** with modes on, Home does not draw `trade_verdict()`'s result. `price_windows()` and `mode_runs()` read `c.mode_runs`. With arbitrage off, `trade_verdict()` returns `off` before the mirror. The mirror change is harmless, but the "Home's mirror" checks test a value Home does not show (the test turns arbitrage on to reach it).
- **Info (main.py:2271):** `held` in the plan key catches no mutation. Run ends sit on quarter boundaries, and the slot is already in the key. Keeping it is correct for profiles with off-quarter boundaries.
- **Info (CHANGELOG.md:12 vs DECISIONS:64):** the CHANGELOG says 0.07 or less, the DECISIONS note says 0.08 or less. Measured on 0.34/0.26: 0.08 still trades, 0.0801 does not. Both are true; the CHANGELOG is the safer advice.
- **UX-55:** only partly cleared. "No prices have arrived" is gone. But with every battery on Solar first, on 0.34/0.26 at cost 0.10 (or 0.07 without a forecast), Home now says "No trading on price today. The price gap is too small." UX-55 wants "no battery trades on price". Keep UX-55 open.
No strings changed by me. Full report: `reports/TARIFF-24-review-report.md`.
admin
merged commit 7e77c723b9 into hotfix-0.19.12026-10-28 19:14:45 +01:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
TARIFF-24 (hotfix 0.19.1): battery modes with a day/night tariff and no price feed never get prices
Defect. With battery modes on, a day/night or fixed
static_profile,price_provider: offandarbitrage_enabled: false,tariff_refresh_wanted()was false, soamain()never startedrun_tariff_refresh().tariff_plan,fresh_tariff_plan()andmode_runsstayed None: Leftover solar / Cheap hours never charged from the grid, and the Batteries screen said "No prices, so it follows the house" while Home priced the day fromstatic_plan()itself.Fix.
main.pytariff_refresh_wanted():packmode.enabled(opts)is a third term. Withprice_provideroff,resolve_tariff()is the purestatic_plan(): no HTTPS call is added (the test replaces the feed loader with one that fails). Docstring shortened to what is true now; history stays in DECISIONS.Tests (
test_tariff24.py, 37 checks): the gate by configuration, read fromamain()'s own guard; the real Controller + realrun_tariff_refresh()over a night (cheap run, Cheap hours battery charges, Leftover solar does not, nono_prices); a gap below the cycle cost givesno_spread; Home and the Batteries screen agree; UX-55. Reproduced on793a676(18 failures there, 0 here). Mutation: reverting the gate fails 18 checks.Release parts.
config.yaml0.19.1, CHANGELOG 0.19.1,DECISIONS/TARIFF-24-modes-need-the-tariff-task.md.Not touched: control, price runs, messages,
master,release/1.0. No merge, no tag.Round 2: AUDIT-02 F1 (owner, 2026-10-28)
With battery modes on and a usable solar forecast (
solar_fcnot None), the price gap a mode needs is Battery cycle cost (widened by the price product only), no longer x1.5 for "no weather". Without a forecast it stays 1.5 x. One helper,arbitrage.weather_uncertainty(), read by_price_runs(),_mode_roles()and Home's mirrorhomeview.trade_verdict(); the modes-off trade path is untouched. Tests:test_tariff24_weather.py; fourtest_modes_p4.pyscenarios keep the 0.30 gate they were written for. DOCS, CHANGELOG 0.19.1 and the DECISIONS note updated.🤖 Generated with Claude Code
https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
Review TARIFF-24 (Opus reviewer), head
97686319f38d73c39bee8652e16d2e786c11aa39Verdict: approve. Merging into
hotfix-0.19.1withhead_commit_idpinned to this sha. CI run 1422 (checks / unit) is green on this sha (84 PASS, graded as the merge intohotfix-0.19.1), and so isinfra-pin / fresh(run 1423). My own offline suite on this head: 84 PASS, 0 FAIL (--network none,GOODWE_REQUIRE_HWSIM=1, hwsim at pin 3b4ff84).What I checked myself:
793a676: the task does not start,mode_runsis None, both cards sayno_prices, no night charge. On the head the task starts, the run plan is on and the Cheap hours battery charges. Withprice_provideroff,resolve_tariff()returnsstatic_plan(), which is pure, so no network or HA call is added. Modes off and arbitrage-on runs are byte-identical to base.weather_uncertainty(). The modes-off path (main.py:4929) is unchanged. A tree with the doubt forced back reproduces base's digests for every forecast case, so the gap is the only thing that changed.GATE_030scenarios are row-for-row identical to base at 0.20 (same gate). The 30-min task restart in the harness matches the real refresh. 5 of the dev's mutations reproduced (caught). I added 3 of my own: 1 caught, 2 survived (see below).Findings (none block the hotfix):
if self.solar_fc is None: returnguard in_wx_track()and all oftest_tariff24_weather.pystill passes. But the code is then wrong. Example: the forecast is lost at 21:45 during the day give run, and the hold flows on into the 22:00 cheap run. The battery then charges from the grid on the narrow gap with no forecast. The guard in this PR prevents that; no test pins it. Add a check for that case in the forward-port.keep_started(). Measured: a cheap run until 07:45, then a give run 07:00-10:00. The give run stayed on until 10:00. The battery role isgiveinstead ofhouse, with the same range (-r, r). There is no grid charge or sale (both gates use the wide gap). Only the Home and Batteries display and the discharge order change.trade_verdict()'s result.price_windows()andmode_runs()readc.mode_runs. With arbitrage off,trade_verdict()returnsoffbefore the mirror. The mirror change is harmless, but the "Home's mirror" checks test a value Home does not show (the test turns arbitrage on to reach it).heldin the plan key catches no mutation. Run ends sit on quarter boundaries, and the slot is already in the key. Keeping it is correct for profiles with off-quarter boundaries.No strings changed by me. Full report:
reports/TARIFF-24-review-report.md.