diff --git a/goodwe_controller/DECISIONS/PRICE-01-own-price-server.md b/goodwe_controller/DECISIONS/PRICE-01-own-price-server.md index 5ff08df..996a374 100644 --- a/goodwe_controller/DECISIONS/PRICE-01-own-price-server.md +++ b/goodwe_controller/DECISIONS/PRICE-01-own-price-server.md @@ -71,8 +71,8 @@ energy only; the framework adds markup and taxes, as for `generic_dayahead`. "static_profile is empty" warning, so no notification (round 2's *Fixed tariff not used* was false and fired on every sound start; round 3 removed it). Instead one INFO line at start (English, as every log line) and an info - note beside the setting (EN and NL), saying "Fixed tariff is only used if no - prices are available.", both + note beside the setting (EN and NL, "With the price service, the fixed + tariff is only used if no prices are available."), both only with a non-blank `static_profile` (whitespace counts as blank). At start the add-on dismisses the old notification id, in case a pre-release build raised it. diff --git a/goodwe_controller/app/messages.py b/goodwe_controller/app/messages.py index d4335ea..48646f4 100644 --- a/goodwe_controller/app/messages.py +++ b/goodwe_controller/app/messages.py @@ -369,7 +369,7 @@ def prices_back() -> Dismiss: # PRICE-01: a fixed tariff beside the price service. One true sentence, for the -# start-up log and the settings page (neither has a Dutch layer). +# start-up log (English only, as every log line) and the settings page (EN, NL). FIXED_TARIFF_LOG = "Fixed tariff is only used if no prices are available." FIXED_TARIFF_NOTE = ("With the price service, the fixed tariff is only used if " "no prices are available.") diff --git a/goodwe_controller/test_maint13.py b/goodwe_controller/test_maint13.py index ab41527..2e76f48 100644 --- a/goodwe_controller/test_maint13.py +++ b/goodwe_controller/test_maint13.py @@ -416,8 +416,10 @@ def put_in_charge(c, which, hours_ago, open_min): def notes(h): # Not the dismiss of PRICE-01's old fixed-tariff card, which every start sends. + # Only that dismiss: a `create` with the old id would still be counted. return [x for x in h.calls if x[0] == "persistent_notification" - and x[2].get("notification_id") != "goodwe_fixed_tariff_unused"] + and not (x[1] == "dismiss" + and x[2].get("notification_id") == "goodwe_fixed_tariff_unused")] for cheap in (True, False): diff --git a/goodwe_controller/test_price_secret.py b/goodwe_controller/test_price_secret.py index b284120..34f92bb 100644 --- a/goodwe_controller/test_price_secret.py +++ b/goodwe_controller/test_price_secret.py @@ -358,7 +358,46 @@ async def named_scenario(): check("the price token is hidden by name even when the schema does not mark " "it as a password", TOKEN not in asyncio.run(named_scenario())) -print("\n5. the shipped config.yaml") +print("\n5. PRICE-01 round 3: the fixed-tariff note on the real validate route") +_PROFILE = json.dumps({"periods": [ + {"name": "all", "days": "all", "from": "00:00", "to": "24:00", + "eur_per_kwh": 0.25}]}) +_NOTE = {"en": ("With the price service, the fixed tariff is only used if no " + "prices are available."), + "nl": ("Met de prijsdienst wordt het vaste tarief alleen gebruikt als " + "er geen prijzen zijn.")} + + +async def note_scenario(site): + runner, root = await serve(Sup(site)) + out = {} + try: + async with ClientSession() as s: + for lang in ("en", "nl"): + async with s.get(root + f"config/model?lang={lang}") as r: + m = await r.json() + async with s.post(root + "config/validate", + json={"model": m["ui"], "lang": lang}) as r: + b = await r.json() + out[lang] = [x for x in b.get("messages", []) + if x["text"] in _NOTE.values()] + finally: + await runner.cleanup() + return out + + +_with = asyncio.run(note_scenario(dict(SITE, static_profile=_PROFILE))) +check("an English page gets the note in English, as info, beside a field", + [(x["text"], x["tier"]) for x in _with["en"]] == [(_NOTE["en"], "info")] + and _with["en"][0]["field"]) +check("...and a Dutch page gets it in Dutch, beside the same field", + [(x["text"], x["tier"]) for x in _with["nl"]] == [(_NOTE["nl"], "info")] + and _with["nl"][0]["field"] == _with["en"][0]["field"]) +_blank = asyncio.run(note_scenario(dict(SITE, static_profile=" "))) +check("a spaces-only fixed tariff: no note on either page", + _blank == {"en": [], "nl": []}) + +print("\n6. the shipped config.yaml") cfg = open(os.path.join(HERE, "config.yaml"), encoding="utf-8").read() schema_text = cfg[cfg.index("\nschema:"):] pw = {line.split(":")[0].strip() for line in schema_text.splitlines() diff --git a/goodwe_controller/test_price_service.py b/goodwe_controller/test_price_service.py index 017dcf1..063337f 100644 --- a/goodwe_controller/test_price_service.py +++ b/goodwe_controller/test_price_service.py @@ -1536,6 +1536,35 @@ for _label, _kw in (("no profile", {}), _off_s, _, _ = run(_scenario_fixed(price_provider="off", static_profile=_PROFILE)) check("provider off with a profile: no log line", _off_s == []) + + +async def _scenario_fixed_level_and_raise(): + """The line's level, and a dismiss that raises: the cycle goes on.""" + with Wired(NOW, static_profile=_PROFILE) as W: + real = W.ctl.hass.call + + async def call(domain, service, data): + if service == "dismiss" and data.get("notification_id") == _OLD_ID: + raise RuntimeError("Home Assistant said no") + return await real(domain, service, data) + W.ctl.hass.call = call + before = mark() + await W.refresh() + try: + for k in range(2): + await W.tick(NOW + timedelta(seconds=30 * k)) + done = True + except Exception as e: # noqa: BLE001 - the check reports it + done = repr(e) + mine = [(r.levelno, r.getMessage()) for r in TAP.records[before:] + if "only used if no prices" in r.getMessage()] + return done, mine + + +_done, _lvl = run(_scenario_fixed_level_and_raise()) +check(f"the start line is INFO, not a warning ({_lvl})", + _lvl == [(logging.INFO, _LINE)]) +check(f"a dismiss that raises does not stop the cycle ({_done})", _done is True) check("the log line is the same words as the note's second half", _NOTE.endswith(_LINE[0].lower() + _LINE[1:])) diff --git a/goodwe_controller/test_setup_only.py b/goodwe_controller/test_setup_only.py index ff1b392..0a1e6d9 100644 --- a/goodwe_controller/test_setup_only.py +++ b/goodwe_controller/test_setup_only.py @@ -455,8 +455,10 @@ check("the saved blob passes the normal gate: no refusal, not setup-only", print("\n after the restart, on the saved settings:") rc, ev, err, files = run_child("after", SAVED or {}, SAVED_STATE) # Not the dismiss of PRICE-01's old fixed-tariff card, sent at every start. +# Only that dismiss: a `create` with the old id would still be counted. services = [e for e in ev if e[0] == "SERVICE" - and e[3].get("notification_id") != "goodwe_fixed_tariff_unused"] + and not (e[1:3] == ["persistent_notification", "dismiss"] + and e[3].get("notification_id") == "goodwe_fixed_tariff_unused")] writes = [e[3].get("value") for e in services if e[1:3] == ["number", "set_value"]] print(f" after: exit {rc}, {len(services)} service calls, set_value " diff --git a/goodwe_controller/test_watchdog.py b/goodwe_controller/test_watchdog.py index a3cac3e..7389bf3 100644 --- a/goodwe_controller/test_watchdog.py +++ b/goodwe_controller/test_watchdog.py @@ -1112,10 +1112,16 @@ check("...and it is still commanding 0 W on the far side of the restart", _b.target == 0.0 and "circulation" in _b.reason) # Not the dismiss of PRICE-01's old fixed-tariff card, sent at every start. +# Only that dismiss: a `create` with the old id would still be counted. +def _not_old_dismiss(c): + return not (c[1] == "dismiss" + and c[2].get("notification_id") == "goodwe_fixed_tariff_unused") + + _notes1 = [c for c in _h1.calls if c[0] == "persistent_notification" - and c[2].get("notification_id") != "goodwe_fixed_tariff_unused"] + and _not_old_dismiss(c)] _notes2 = [c for c in _h2.calls if c[0] == "persistent_notification" - and c[2].get("notification_id") != "goodwe_fixed_tariff_unused"] + and _not_old_dismiss(c)] print(f" notifications raised: {len(_notes1)} before the restart, " f"{len(_notes2)} after it") check("the alarm was raised in the first process", len(_notes1) >= 1)