diff --git a/goodwe_controller/test_tariff24.py b/goodwe_controller/test_tariff24.py index 4fccf6f..f5b74a4 100644 --- a/goodwe_controller/test_tariff24.py +++ b/goodwe_controller/test_tariff24.py @@ -13,8 +13,15 @@ the profile itself. DECISIONS/TARIFF-24-modes-need-the-tariff-task.md. Leftover solar and one Cheap hours. 3. The same with a gap below the cycle cost. 4. Home and the Batteries screen agree. -5. Nothing else moved: modes off, the site trade on a static profile, and a - feed site start the same task as before. +5. UX-55: with every battery Solar first Home no longer says "no prices have + arrived" on a static profile, and still does with no profile and no feed. +6. Nothing else moved: modes off starts no task; the gate rows in part 1 keep + the arbitrage and feed cases as before. + +Mutations (MUTATION.md, purged copies, graded over this file): the gate +reverted to `provider or arbitrage` fails 18 checks in parts 1 to 5; `True` +fails the modes-off rows; `bool(opts.get(...))` fails the exact-true row; +dropping the arbitrage term fails the arbitrage rows. """ import ast @@ -32,6 +39,7 @@ sys.path.insert(0, os.path.join(HERE, "tools")) logging.disable(logging.WARNING) import opt11p3_replay as rp # noqa: E402 +from named_absence import first, nth, present # noqa: E402 import app.main as m # noqa: E402 from app import homeview, modetrade, packmode # noqa: E402 from app import price as _price, static_tariff as _st # noqa: E402 @@ -101,8 +109,9 @@ def _guards_of(node, guards=()): _TREE = ast.parse(open(os.path.join(HERE, "app", "main.py"), encoding="utf-8").read()) -_AMAIN = next(n for n in ast.walk(_TREE) - if isinstance(n, ast.AsyncFunctionDef) and n.name == "amain") +_AMAIN = first((n for n in ast.walk(_TREE) + if isinstance(n, ast.AsyncFunctionDef) and n.name == "amain"), + "amain() in main.py") _TASK_GUARDS = [g for g, n in _guards_of(_AMAIN) if n == "run_tariff_refresh"] @@ -252,13 +261,16 @@ def charging_b(rows): def why_of(c, name): - return next(b["why_key"] for b in c.battery_cards if b["name"] == name) + card = first((b for b in c.battery_cards if b["name"] == name), + f"a card for {name}") + return card["why_key"] # --------------------------------------------------------------------------- # print("\n2. end to end, night, modes on, static day/night, no feed, no arbitrage") c1, r1, started1 = drive(opts()) -_last = r1[-1] +_last = nth(r1, -1, "the last cycle") +_first1 = nth(r1, 0, "the first cycle") print(f" tariff task started by amain's guard: {started1}") print(f" tariff_plan: {None if c1.tariff_plan is None else c1.tariff_plan.words}") print(f" mode_runs: {_last['runs']}") @@ -266,41 +278,44 @@ print(f" mode_tick reason / roles: " f"{None if _last['tick'] is None else (_last['tick'].why, dict(_last['tick'].roles))}") print(f" Batteries screen why_key: A {why_of(c1, A)}, B {why_of(c1, B)}") print(f" cycles with B charging: {len(charging_b(r1))} of {len(r1)}, " - f"B soc {r1[0]['soc']['b']:.1f} -> {r1[-1]['soc']['b']:.1f}") + f"B soc {_first1['soc']['b']:.1f} -> {_last['soc']['b']:.1f}") check("the tariff refresh task is started", started1) check("...and it adds no network call: with price_provider off the feed " "loader is never reached (it is replaced by one that fails)", not FETCHES and not _price.provider_configured(opts())) -check("a fresh tariff plan with prices is held", _last["plan"] is not None - and _last["plan"].has_prices) +check("a fresh tariff plan with prices is held", present(_last) + and _last["plan"] is not None and _last["plan"].has_prices) check("the price runs exist (mode_runs is not None) and carry a cheap run " "that holds in the night period", - _last["runs"] is not None and _last["runs"].on + present(_last) and _last["runs"] is not None and _last["runs"].on and _last["runs"].cheap is not None and _last["runs"].cheap.holds(_last["now"])) check("the Cheap hours battery charges from the grid in the night period", len(charging_b(r1)) > len(r1) // 2 - and r1[-1]["soc"]["b"] > r1[0]["soc"]["b"] + 0.3) + and present(_last, _first1) + and _last["soc"]["b"] > _first1["soc"]["b"] + 0.3) check("...and the Leftover solar battery does not (no sun, no forecast)", not any(r["p"]["a"] < -200.0 for r in r1)) check("...the reason is not 'no prices': neither battery reads no_prices", why_of(c1, A) != "no_prices" and why_of(c1, B) != "no_prices") check("...only the Cheap hours battery got the grid charge role", - r1[-1]["tick"] is not None - and r1[-1]["tick"].roles.get(B) == modetrade.CHARGE - and r1[-1]["tick"].roles.get(A) != modetrade.CHARGE) + present(_last) and _last["tick"] is not None + and _last["tick"].roles.get(B) == modetrade.CHARGE + and _last["tick"].roles.get(A) != modetrade.CHARGE) # --------------------------------------------------------------------------- # print("\n3. the same with a gap below the cycle cost") c3, r3, started3 = drive(opts(cost=0.20)) -_l3 = r3[-1] +_l3 = nth(r3, -1, "the last cycle") print(f" started {started3}; mode_runs {_l3['runs']}; " f"why A {why_of(c3, A)}, B {why_of(c3, B)}") check("the task is started and the plan is there, so mode_runs is not None", - started3 and _l3["plan"] is not None and _l3["runs"] is not None) + started3 and present(_l3) and _l3["plan"] is not None + and _l3["runs"] is not None) check("the reason is the gap: no_spread ('Prices differ too little'), " "never no_prices, on both batteries", why_of(c3, A) == "no_spread" and why_of(c3, B) == "no_spread" + and present(_l3) and _l3["runs"] is not None and not _l3["runs"].on) check("...and no battery charges from the grid", not charging_b(r3) and not any(r["p"]["a"] < -200.0 for r in r3))