TARIFF-24: battery modes start the tariff task (hotfix 0.19.1) #212

Merged
admin merged 5 commits from TARIFF-24 into hotfix-0.19.1 2026-10-28 19:14:45 +01:00
Owner

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.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC

## 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
admin added 2 commits 2026-10-28 01:09:55 +01:00
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>
TARIFF-24: test file follows the repo's suite rules (MUTATION.md pointer, named absence)
checks / unit (pull_request) Successful in 49m23s
infra-pin / fresh (pull_request) Successful in 2s
230ba0b57a
Co-Authored-By: Claude <noreply@anthropic.com>
admin added 2 commits 2026-10-28 15:42:53 +01:00
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>
TARIFF-24 round 2: test_modes_p4 scenarios keep the 0.30 gate they were written for
checks / unit (pull_request) Successful in 49m18s
infra-pin / fresh (pull_request) Successful in 1s
d424b862d8
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>
admin added 1 commit 2026-10-28 17:32:28 +01:00
TARIFF-24 round 3: a run planned with a forecast keeps its gap to its end
checks / unit (pull_request) Successful in 51m57s
infra-pin / fresh (pull_request) Successful in 1s
97686319f3
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>
Author
Owner

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.1 2026-10-28 19:14:45 +01:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: admin/goodwe-addon#212