From c68fd275cc7b6912b860eb7331755fad9b1b76de Mon Sep 17 00:00:00 2001 From: glenn schrooyen Date: Wed, 14 Oct 2026 20:27:15 +0200 Subject: [PATCH] PRICE-01 round 3: replace the fixed-tariff notification with a log line and a settings note G1: remove the false 'Fixed tariff not used' notification. One INFO line at start and an info note beside the setting (EN/NL) when price_service has a non-blank static_profile; the old notification id is dismissed at start. DOCS, migration step 5 and DECISIONS rule 7 now say what the code does. S05: whitespace-only static_profile counts as not set. G2: maintenance_enabled read through MaintConfig.from_options(). Co-Authored-By: Claude Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC --- .../DECISIONS/PRICE-01-own-price-server.md | 15 +- goodwe_controller/DOCS.md | 28 ++-- goodwe_controller/app/main.py | 33 ++-- goodwe_controller/app/messages.py | 34 +++-- goodwe_controller/app/price_chain.py | 7 +- goodwe_controller/app/uipage.py | 11 +- goodwe_controller/app/validate.py | 6 + goodwe_controller/app/web.py | 3 +- goodwe_controller/test_maint13.py | 4 +- goodwe_controller/test_price_service.py | 144 ++++++++++++++---- goodwe_controller/test_setup_only.py | 4 +- goodwe_controller/test_watchdog.py | 7 +- 12 files changed, 211 insertions(+), 85 deletions(-) diff --git a/goodwe_controller/DECISIONS/PRICE-01-own-price-server.md b/goodwe_controller/DECISIONS/PRICE-01-own-price-server.md index b009248..5ff08df 100644 --- a/goodwe_controller/DECISIONS/PRICE-01-own-price-server.md +++ b/goodwe_controller/DECISIONS/PRICE-01-own-price-server.md @@ -64,9 +64,18 @@ energy only; the framework adds markup and taxes, as for `generic_dayahead`. classic page draws it as a password field; a save or a validate that sends `********` or nothing keeps the stored value, any other value replaces it. A token can therefore be changed but not cleared from the page. -7. **A fixed tariff beside `price_service` is said, not refused.** It no longer - drives the cheap window or the gate, so the add-on warns once at start - (notification *Fixed tariff not used* and a log line). +7. **A fixed tariff beside `price_service` is allowed and quietly noted.** It is + still the plan's last fallback (`resolve_tariff` -> `_degrade`); only the + cheap window and the gate ignore it, because they read the provider's + prices. Keeping it set is the recommended setup, and clearing it triggers the + "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 + 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. 8. **The no-price words show only with the maintenance charge on** (owner, 2026-10-14): the status row, the Home line and the notification say 'maintenance waits', so with `maintenance_enabled` off none is shown. diff --git a/goodwe_controller/DOCS.md b/goodwe_controller/DOCS.md index 8f03f96..c39ea7e 100644 --- a/goodwe_controller/DOCS.md +++ b/goodwe_controller/DOCS.md @@ -2326,11 +2326,15 @@ Do this on the Configuration tab, in one save, and restart once: the provider they are added to the wholesale price, so they set the prices shown on the page and, if battery modes or trading are on, the prices those decide on. The maintenance charge gate does not depend on them. -5. Keep **Fixed or day/night tariff** (`static_profile`) if you have one. It - is still the tariff used, marked as estimated and without trading, while no - source has a price. It no longer decides *Cheap window* or the maintenance - charge: the provider's prices do. The add-on says so once at start (*Fixed - tariff not used*) and does not refuse the setting. +5. Keep **Fixed or day/night tariff** (`static_profile`) if you have one; it + is fine to leave it set. It is the plan's last fallback: it is used, + marked as estimated and without trading, only when no source has a price. + *Cheap window* and the maintenance charge never read it: the provider's + prices decide them. The add-on logs one INFO line at start ("Fixed tariff + is only used if no prices are available.") and shows the same note beside + the setting. Nothing needs clearing, and clearing it only removes the + fallback (the settings page then warns that the site has no tariff while the + price feed is down). 6. Remove the `price_service` REST and template sensors from Home Assistant when you are satisfied. They are no longer read. @@ -3377,16 +3381,13 @@ battery goes on as before. Check that the add-on can reach the internet and that the price service token is right. The notification goes by itself when a price is back. It is shown only while the monthly battery cycle is switched on. -### Fixed tariff not used - -A fixed or day/night tariff (`static_profile`) is set, and so is the price -service. The price service gives the prices, so the fixed tariff does not -decide *Cheap window* or the monthly battery cycle's charge. The setting is -kept and not refused. Clear it, or choose another price source, to remove the -message. It is shown once each time the add-on starts. - ### For installers: notifications +A fixed tariff beside the price service raises no notification: it is the +plan's last fallback, not a fault. An earlier pre-release build raised a card +with the id `goodwe_fixed_tariff_unused`; the add-on takes that id down at every +start, which does nothing when it is absent. + This part is for installers. The texts live in `app/messages.py` and follow `STYLE.md`. Every notification keeps a fixed `notification_id`, so a repeat replaces the card. The ERROR or WARNING line of the add-on log names the @@ -3406,7 +3407,6 @@ default `info` hides it. | Meter reading may be stuck | alarm | `goodwe_meter_frozen` | SAFETY-18 | | Battery cycle did not finish | warning | `goodwe_maint_failed_` | MAINT-13 | | No prices | warning | `goodwe_no_prices` | PRICE-01 | -| Fixed tariff not used | warning | `goodwe_fixed_tariff_unused` | PRICE-01 | **The Details link.** Home Assistant 2026.2 and newer show add-on pages at `/config/app//documentation`; 2026.5 removed the old diff --git a/goodwe_controller/app/main.py b/goodwe_controller/app/main.py index 5035ed6..d49a4a5 100644 --- a/goodwe_controller/app/main.py +++ b/goodwe_controller/app/main.py @@ -90,7 +90,7 @@ from .p1 import P1Ingest, build_source, is_enabled from .price import Absence, Uncertainty, brussels, price_option, provider_configured from .price import required_spread as price_required_spread from .price import uncertainty_of as price_uncertainty_of -from .price_chain import (PriceWatch, fixed_tariff_unused, gate_for, +from .price_chain import (PriceWatch, fixed_tariff_beside_service, gate_for, uses_chain) from .price_history import PriceHistory from .static_tariff import resolve_tariff, static_gated, static_status @@ -1214,7 +1214,7 @@ class Controller: self.price_gap = False self.price_gap_since = None self._price_gap_announced = False - self._fixed_unused_said = False + self._fixed_note_said = False self._gated_key = None # MAINT-13: (options, hour) of the cache # TARIFF-03. One line of prose for the status page saying WHICH tariff # window we are in and why it is or is not cheap - so "Cheap window: @@ -4035,7 +4035,7 @@ class Controller: # Owner, 2026-10-14: the words say 'maintenance waits', so they show # only while the maintenance charge is on. self.price_gap = ((not gate.covered) and self.tariff_at is not None - and self.o.get("maintenance_enabled") is True) + and MaintConfig.from_options(self.o).enabled) last = self._last_published if gate.covered or last is None: self.price_gap_since = None @@ -5591,7 +5591,7 @@ class Controller: # Not a safety check, so it is one of the cheap ones: an attribute test # and, on a change only, one Home Assistant call. await self.check_price_gap() - await self.check_fixed_unused() + await self.check_fixed_note() # --- SAFETY-31's alarm, and its boundary is the LATCH, not a number --- # Unconstrained where the two above are not: it grades @@ -6229,18 +6229,21 @@ class Controller: await self.hass.call("persistent_notification", "dismiss", messages.prices_back().data()) - async def check_fixed_unused(self) -> None: - """PRICE-01 F2. A `static_profile` beside `price_service` no longer - drives the cheap window or the maintenance gate. Not refused: said once, - at the first cycle, in the log and as one notification.""" - if self._fixed_unused_said: + async def check_fixed_note(self) -> None: + """PRICE-01 G1. At the first cycle only: one INFO line when a + `static_profile` is set beside `price_service` (it is the plan's last + fallback, not a fault, so no notification), and a dismiss of the card an + earlier build raised, which is harmless when absent.""" + if self._fixed_note_said: return - self._fixed_unused_said = True - if fixed_tariff_unused(self.o): - _LOG.warning("The fixed tariff is not used, because the price " - "service gives the prices.") - await self.hass.call("persistent_notification", "create", - messages.fixed_tariff_unused().data()) + self._fixed_note_said = True + if fixed_tariff_beside_service(self.o): + _LOG.info(messages.FIXED_TARIFF_LOG) + try: + await self.hass.call("persistent_notification", "dismiss", + messages.fixed_tariff_card_gone().data()) + except Exception: # noqa: BLE001 - housekeeping never stops a cycle + _LOG.debug("Could not take down the old fixed-tariff card.") def _gap_args(self) -> tuple: """`(since, now)` in Brussels time for the gap's words. `since` is None diff --git a/goodwe_controller/app/messages.py b/goodwe_controller/app/messages.py index e7478f8..d4335ea 100644 --- a/goodwe_controller/app/messages.py +++ b/goodwe_controller/app/messages.py @@ -131,8 +131,6 @@ T_MAINT_FAILED = "Battery cycle did not finish" T_MAINT_FAILED_NL = "Batterijcyclus niet afgerond" T_NO_PRICES = "No prices" T_NO_PRICES_NL = "Prijzen ontbreken" -T_FIXED_UNUSED = "Fixed tariff not used" -T_FIXED_UNUSED_NL = "Vast tarief niet gebruikt" ID_LOOP_BOUND = "goodwe_integrator_bound" ID_OUTPUT_CLAMP = "goodwe_output_clamp" @@ -143,7 +141,9 @@ ID_NOT_DELIVERING = "goodwe_not_delivering_{slug}" ID_METER_FROZEN = "goodwe_meter_frozen" ID_MAINT_FAILED = "goodwe_maint_failed_{slug}" ID_NO_PRICES = "goodwe_no_prices" -ID_FIXED_UNUSED = "goodwe_fixed_tariff_unused" +# PRICE-01: an earlier build raised a card with this id. Nothing raises it now; +# the add-on only dismisses it at start. +OLD_FIXED_CARD_ID = "goodwe_fixed_tariff_unused" # Which limit cut a command (clamp.ClampResult.bound, plus the loop's own). # The setting is named by its label, "Inverter rating (W)". @@ -354,17 +354,6 @@ def no_prices(since=None, now=None) -> Notice: body_nl=f"{T_NO_PRICES_NL}. {nl}") -def fixed_tariff_unused() -> Notice: - """PRICE-01: a fixed tariff is set beside the price service, which wins. - Said once, at start.""" - return Notice( - ID_FIXED_UNUSED, WARNING, T_FIXED_UNUSED, - "The fixed tariff is not used, because the price service gives the " - "prices.", - body_nl=f"{T_FIXED_UNUSED_NL}. Het vaste tarief wordt niet gebruikt, " - "want de prijsdienst levert de prijzen.") - - @dataclass(frozen=True) class Dismiss: """The payload that takes one notification down again.""" @@ -379,6 +368,22 @@ def prices_back() -> Dismiss: return Dismiss(ID_NO_PRICES) +# 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). +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.") +FIXED_TARIFF_NOTE_NL = ("Met de prijsdienst wordt het vaste tarief alleen " + "gebruikt als er geen prijzen zijn.") +# Settings-page notes that have a Dutch twin (uipage.anchors). +NOTE_NL = {FIXED_TARIFF_NOTE: FIXED_TARIFF_NOTE_NL} + + +def fixed_tariff_card_gone() -> Dismiss: + """PRICE-01: takes down the card an earlier build raised.""" + return Dismiss(OLD_FIXED_CARD_ID) + + def row_no_prices(since=None, now=None) -> str: """PRICE-01: the status row while no price covers now.""" if since is not None: @@ -1301,7 +1306,6 @@ def samples() -> list: no_prices(*_SINCE_SAMPLE), no_prices(*_SINCE_OLDER), no_prices(), - fixed_tariff_unused(), ] diff --git a/goodwe_controller/app/price_chain.py b/goodwe_controller/app/price_chain.py index 44cf920..bdf93e2 100644 --- a/goodwe_controller/app/price_chain.py +++ b/goodwe_controller/app/price_chain.py @@ -50,9 +50,10 @@ def uses_chain(opts: dict) -> bool: return bool(cls is not None and (cls.FALLBACKS or cls.KEEPS_LAST_GOOD)) -def fixed_tariff_unused(opts: dict) -> bool: - """PRICE-01 F2: a fixed tariff is set beside `price_service`, which then - decides the cheap window and the gate. Accepted, so it is said, not refused.""" +def fixed_tariff_beside_service(opts: dict) -> bool: + """PRICE-01: a fixed tariff is set beside `price_service`. Not a fault: it + stays the plan's last fallback (`resolve_tariff`), and only the cheap window + and the gate ignore it. A blank or whitespace-only value is not set.""" return (provider_id(opts) == "price_service" and bool(str(price_option(opts, "static_profile")).strip())) diff --git a/goodwe_controller/app/uipage.py b/goodwe_controller/app/uipage.py index 5c3a90c..2e2b832 100644 --- a/goodwe_controller/app/uipage.py +++ b/goodwe_controller/app/uipage.py @@ -12,6 +12,7 @@ import copy import os import re +from . import messages from . import uilayout as L from . import uitext from . import uimodel as U @@ -247,7 +248,7 @@ def derive_messages(errors, problems, notes, model, keys, lang="en"): kept = [p for p in problems if p in kept] notes = [n for n in notes if not set(n.keys or ()) & failed] return ([derive_message(err, lang) for err in errors] - + anchors(kept, notes, model, keys)) + + anchors(kept, notes, model, keys, lang)) # --------------------------------------------------------------------------- # @@ -292,9 +293,10 @@ def targets(key: str, text: str, model: dict) -> list: return [(None, None)] -def anchors(problems, notes, model, keys) -> list: +def anchors(problems, notes, model, keys, lang="en") -> list: """[{field, card, tier, text}] for the refusals and notes. `field` is - None for a sentence that names no setting on the page.""" + None for a sentence that names no setting on the page. A note with a Dutch + twin in `messages.NOTE_NL` is shown in Dutch when `lang` is "nl".""" out, seen = [], set() def add(field, card, tier, text): @@ -312,10 +314,11 @@ def anchors(problems, notes, model, keys) -> list: add(None, None, "bad", text) for note in notes: tier = note.tier if note.tier in _TIERS else "info" + text = messages.NOTE_NL.get(note.text, note.text) if lang == "nl" else note.text for key in note.keys or (None,): for field, card in (targets(key, note.text, model) if key else [(None, None)]): - add(field, card, tier, note.text) + add(field, card, tier, text) # A sentence placed on one inverter's field is not also put on the list. roster = _roster() placed = {m["text"] for m in out diff --git a/goodwe_controller/app/validate.py b/goodwe_controller/app/validate.py index 20ddc28..e554c80 100644 --- a/goodwe_controller/app/validate.py +++ b/goodwe_controller/app/validate.py @@ -1659,6 +1659,12 @@ def _price_problems(opts: dict, notes: list | None = None) -> list[str]: f"{S('static_profile')} is empty, so the site has no tariff " "at all while the price feed is down.") + # PRICE-01: not a fault, so an info note and no log line here (the add-on + # logs it once at start). With `price_service` the fixed tariff is only the + # plan's last fallback; the cheap window and the gate do not read it. + if pid == "price_service" and str(opts.get("static_profile", "") or "").strip(): + _advise(notes, "info", ("static_profile",), messages.FIXED_TARIFF_NOTE) + # ⚠️ OPT-04. THE THREE WAYS `arbitrage_enabled: true` CAN BE A LIE, refused # rather than logged, because a strategy that is armed in the UI and cannot # fire is indistinguishable from one that fires and finds no opportunity - diff --git a/goodwe_controller/app/web.py b/goodwe_controller/app/web.py index a48e0c1..e301e34 100644 --- a/goodwe_controller/app/web.py +++ b/goodwe_controller/app/web.py @@ -312,7 +312,8 @@ def build_app(controller, selfcfg=None) -> web.Application: _keys_of(options), uitext.lang_of(_body.get("lang"))) if errors - else uipage.anchors(problems, notes, model, _keys_of(options))) + else uipage.anchors(problems, notes, model, _keys_of(options), + uitext.lang_of(_body.get("lang")))) payload["steps"] = uipage.step_problems(payload["messages"], model) if _body.get("setup"): payload["setup"] = [n._asdict() for n in await _setup_checks(options)] diff --git a/goodwe_controller/test_maint13.py b/goodwe_controller/test_maint13.py index 07881ed..ab41527 100644 --- a/goodwe_controller/test_maint13.py +++ b/goodwe_controller/test_maint13.py @@ -415,7 +415,9 @@ def put_in_charge(c, which, hours_ago, open_min): def notes(h): - return [x for x in h.calls if x[0] == "persistent_notification"] + # Not the dismiss of PRICE-01's old fixed-tariff card, which every start sends. + return [x for x in h.calls if x[0] == "persistent_notification" + and x[2].get("notification_id") != "goodwe_fixed_tariff_unused"] for cheap in (True, False): diff --git a/goodwe_controller/test_price_service.py b/goodwe_controller/test_price_service.py index 37a58b6..017dcf1 100644 --- a/goodwe_controller/test_price_service.py +++ b/goodwe_controller/test_price_service.py @@ -742,10 +742,16 @@ class Wired: await self.ctl.read_inputs() await self.ctl.cycle() - def notes(self): + def all_notes(self): return [(s, d) for dom, s, d in self.ctl.hass.calls if dom == "persistent_notification"] + def notes(self): + """Without the dismiss of the old fixed-tariff card that every start + makes (section 14 looks at it through `all_notes`).""" + return [x for x in self.all_notes() + if x != ("dismiss", {"notification_id": "goodwe_fixed_tariff_unused"})] + async def _scenario_gap(): out = {} @@ -1477,44 +1483,130 @@ check("battery modes with no price and maintenance off: no price run is " print(" Home plan lines in that state:", _ml) # =========================================================================== # -print("\n14. round 2: a fixed tariff beside price_service is said once (F2)") +print("\n14. round 3: a fixed tariff beside price_service is a log line and a " + "settings note, no notification (G1, S05)") _PROFILE = json.dumps({"periods": [ {"name": "all", "days": "all", "from": "00:00", "to": "24:00", "eur_per_kwh": 0.25}]}) +_LINE = "Fixed tariff is only used if no prices are available." +_NOTE = ("With the price service, the fixed tariff is only used if no prices " + "are available.") +_OLD_ID = "goodwe_fixed_tariff_unused" async def _scenario_fixed(**over): with Wired(NOW, **over) as W: + before = mark() await W.refresh() for k in range(4): await W.tick(NOW + timedelta(seconds=30 * k)) - return [x for x in W.notes() if x[1].get("notification_id") - == "goodwe_fixed_tariff_unused"], W.ctl + said = [m for m in since_mark(before, logging.INFO) if "only used if no prices" in m] + return said, W.all_notes(), W.ctl -_before = mark() -_f, _fc = run(_scenario_fixed(static_profile=_PROFILE)) -_logged = [m for m in since_mark(_before, logging.WARNING) - if "fixed tariff" in m] -check("price_service with a static_profile: ONE notification over four " +def _settings_notes(**over): + base = dict(OPTS_C) + base.update(price_provider="price_service", price_service_token=TOKEN) + base.update(over) + out = [] + validate_startup_config(base, None, None, notes=out) + return [n.text for n in out if n.keys == ("static_profile",) + and "fixed tariff" in n.text.lower()] + + +_said, _fn, _fc = run(_scenario_fixed(static_profile=_PROFILE)) +check("price_service + a set profile: the INFO line appears once over four " "ticks, and the setting is not refused", - len(_f) == 1 and _f[0][0] == "create" - and problems(static_profile=_PROFILE) == []) -check("...in English and Dutch, plain and short", - _f[0][1]["title"] == "Fixed tariff not used" - and "The fixed tariff is not used, because the price service gives the " - "prices." in _f[0][1]["message"] - and "Vast tarief niet gebruikt. Het vaste tarief wordt niet gebruikt, " - "want de prijsdienst levert de prijzen." in _f[0][1]["message"]) -check("...and one WARNING line in the log", - _logged == ["The fixed tariff is not used, because the price service " - "gives the prices."]) -check("no static_profile: nothing is said", - run(_scenario_fixed())[0] == []) -check("another provider with a static_profile: nothing is said (there it is " - "the fallback)", - run(_scenario_fixed(price_provider="generic_dayahead", - static_profile=_PROFILE))[0] == []) + _said == [_LINE] and problems(static_profile=_PROFILE) == []) +check("...no notification is raised, only the old id is dismissed", + [x for x in _fn if x[1].get("notification_id") == _OLD_ID] + == [("dismiss", {"notification_id": _OLD_ID})] + and not [x for x in _fn if x[0] == "create"]) +for _label, _kw in (("no profile", {}), + ("an empty profile", {"static_profile": ""}), + ("a spaces-only profile", {"static_profile": " "}), + ("a line-break-only profile", {"static_profile": "\n \t"}), + ("another provider", {"price_provider": "generic_dayahead", + "static_profile": _PROFILE})): + _s, _n, _c = run(_scenario_fixed(**_kw)) + check(f"{_label}: no log line", _s == []) + check(f"{_label}: the old id is still dismissed at start (harmless)", + [x for x in _n if x[1].get("notification_id") == _OLD_ID] + == [("dismiss", {"notification_id": _OLD_ID})]) +_off_s, _, _ = run(_scenario_fixed(price_provider="off", + static_profile=_PROFILE)) +check("provider off with a profile: no log line", _off_s == []) +check("the log line is the same words as the note's second half", + _NOTE.endswith(_LINE[0].lower() + _LINE[1:])) + +check("settings page: price_service + a set profile shows the one note, " + "beside static_profile", _settings_notes(static_profile=_PROFILE) == [_NOTE]) +check("...not with no, empty, spaces-only or line-break-only profile", + all(_settings_notes(**kw) == [] for kw in ( + {}, {"static_profile": ""}, {"static_profile": " "}, + {"static_profile": "\n \t"}))) +check("...not with another provider", + _settings_notes(price_provider="generic_dayahead", + static_profile=_PROFILE) == [] + and _settings_notes(price_provider="off", + static_profile=_PROFILE) == []) +_nn = [] +validate_startup_config(dict(OPTS_C, price_provider="price_service", + price_service_token=TOKEN, + static_profile=_PROFILE), None, None, notes=_nn) +_NOTE_NL = ("Met de prijsdienst wordt het vaste tarief alleen gebruikt als er " + "geen prijzen zijn.") +_en = _UP.anchors([], _nn, {"price_provider": "price_service"}, + ["price_provider", "static_profile"], "en") +_nl = _UP.anchors([], _nn, {"price_provider": "price_service"}, + ["price_provider", "static_profile"], "nl") +check("the page puts the note beside the fixed-tariff field, in English and " + f"Dutch ({_en})", + [m["text"] for m in _en] == [_NOTE] and [m["text"] for m in _nl] + == [_NOTE_NL] and _en[0]["tier"] == "info" and _en[0]["field"] + and _en[0]["field"] == _nl[0]["field"]) +check("...and the note names no refusal: the options are still accepted", + problems(static_profile=_PROFILE) == []) + + +async def _scenario_profile_roles(profile): + """Prices up: what the cheap window and the gate say. Then every source + down: what the plan falls back to.""" + out = {} + over = {"static_profile": profile} if profile else {} + with Wired(NOW, **over) as W: + c = W.ctl + await W.refresh() + await W.tick() + out["cheap"], out["gate"] = c.cheap, c._charge_gate_open(c.inverters[0]) + out["words"] = c.tariff_words + out["origin_up"] = c.tariff_plan.origin + W.net.down() + later = NOW + timedelta(days=5) # saved prices run out + await W.refresh(later) + await W.tick(later) + out["origin_down"] = c.tariff_plan.origin + out["gate_down"] = c._charge_gate_open(c.inverters[0]) + out["points"] = c.tariff_plan.read.match( + series=lambda r: [pt.eur_per_kwh for pt in r.points][:2], + no_data=lambda r: []) + return out + + +_with = run(_scenario_profile_roles(_PROFILE)) +_without = run(_scenario_profile_roles(None)) +check("with prices up, the cheap window and the gate are the same with or " + "without a fixed tariff set", + (_with["cheap"], _with["gate"], _with["words"]) + == (_without["cheap"], _without["gate"], _without["words"]) + and _with["origin_up"] == "provider") +check("with every source down, the plan falls back to the fixed tariff " + f"({_with['origin_down']}, {_with['points']}) and without it there is none", + _with["origin_down"] == "fallback" and _with["points"] + and set(_with["points"]) == {0.25} + and _without["origin_down"] == "none") +check("...and the gate stays shut on that fallback (it is not a firm price)", + _with["gate_down"] is False) # =========================================================================== # print("\n15. round 2: a token shorter than 16 characters is refused (F5)") diff --git a/goodwe_controller/test_setup_only.py b/goodwe_controller/test_setup_only.py index 3da2d5e..ff1b392 100644 --- a/goodwe_controller/test_setup_only.py +++ b/goodwe_controller/test_setup_only.py @@ -454,7 +454,9 @@ 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) -services = [e for e in ev if e[0] == "SERVICE"] +# Not the dismiss of PRICE-01's old fixed-tariff card, sent at every start. +services = [e for e in ev if e[0] == "SERVICE" + 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 f186039..a3cac3e 100644 --- a/goodwe_controller/test_watchdog.py +++ b/goodwe_controller/test_watchdog.py @@ -1111,8 +1111,11 @@ check("...carrying the reason with it, so the operator who has to decide " check("...and it is still commanding 0 W on the far side of the restart", _b.target == 0.0 and "circulation" in _b.reason) -_notes1 = [c for c in _h1.calls if c[0] == "persistent_notification"] -_notes2 = [c for c in _h2.calls if c[0] == "persistent_notification"] +# Not the dismiss of PRICE-01's old fixed-tariff card, sent at every start. +_notes1 = [c for c in _h1.calls if c[0] == "persistent_notification" + and c[2].get("notification_id") != "goodwe_fixed_tariff_unused"] +_notes2 = [c for c in _h2.calls if c[0] == "persistent_notification" + and c[2].get("notification_id") != "goodwe_fixed_tariff_unused"] 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)