PRICE-01 review 3: tests for the round 3 note and dismiss; doc wording
checks / unit (pull_request) Successful in 39m4s
infra-pin / fresh (pull_request) Successful in 2s

- test_price_secret.py: the fixed-tariff note on the real /config/validate
  route, EN and NL, and none for a spaces-only profile (web.py lang wiring).
- test_price_service.py: the start line is INFO, not WARNING; a dismiss that
  raises does not stop the cycle.
- test_maint13/test_setup_only/test_watchdog: ignore only the dismiss of the
  old card, so a create with that id is still counted.
- DECISIONS rule 7 quotes the settings note as it is; messages.py comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
This commit is contained in:
2026-10-14 22:07:09 +02:00
co-authored by Claude Opus 5.5
parent c68fd275cc
commit 6862dea52a
7 changed files with 86 additions and 8 deletions
@@ -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.
+1 -1
View File
@@ -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.")
+3 -1
View File
@@ -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):
+40 -1
View File
@@ -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()
+29
View File
@@ -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:]))
+3 -1
View File
@@ -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 "
+8 -2
View File
@@ -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)