PRICE-01: own price server as a price source, with fallbacks #190

Merged
admin merged 7 commits from PRICE-01 into release/1.0 2026-10-14 22:47:44 +02:00
12 changed files with 211 additions and 85 deletions
Showing only changes of commit c68fd275cc - Show all commits
@@ -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 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. `********` or nothing keeps the stored value, any other value replaces it.
A token can therefore be changed but not cleared from the page. 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 7. **A fixed tariff beside `price_service` is allowed and quietly noted.** It is
drives the cheap window or the gate, so the add-on warns once at start still the plan's last fallback (`resolve_tariff` -> `_degrade`); only the
(notification *Fixed tariff not used* and a log line). 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, 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 2026-10-14): the status row, the Home line and the notification say
'maintenance waits', so with `maintenance_enabled` off none is shown. 'maintenance waits', so with `maintenance_enabled` off none is shown.
+14 -14
View File
@@ -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 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 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. 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 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 is fine to leave it set. It is the plan's last fallback: it is used,
source has a price. It no longer decides *Cheap window* or the maintenance marked as estimated and without trading, only when no source has a price.
charge: the provider's prices do. The add-on says so once at start (*Fixed *Cheap window* and the maintenance charge never read it: the provider's
tariff not used*) and does not refuse the setting. 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 6. Remove the `price_service` REST and template sensors from Home Assistant
when you are satisfied. They are no longer read. 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 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. 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 ### 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 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 `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 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 | | Meter reading may be stuck | alarm | `goodwe_meter_frozen` | SAFETY-18 |
| Battery cycle did not finish | warning | `goodwe_maint_failed_<name>` | MAINT-13 | | Battery cycle did not finish | warning | `goodwe_maint_failed_<name>` | MAINT-13 |
| No prices | warning | `goodwe_no_prices` | PRICE-01 | | 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 **The Details link.** Home Assistant 2026.2 and newer show add-on pages at
`/config/app/<slug>/documentation`; 2026.5 removed the old `/config/app/<slug>/documentation`; 2026.5 removed the old
+18 -15
View File
@@ -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 Absence, Uncertainty, brussels, price_option, provider_configured
from .price import required_spread as price_required_spread from .price import required_spread as price_required_spread
from .price import uncertainty_of as price_uncertainty_of 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) uses_chain)
from .price_history import PriceHistory from .price_history import PriceHistory
from .static_tariff import resolve_tariff, static_gated, static_status from .static_tariff import resolve_tariff, static_gated, static_status
@@ -1214,7 +1214,7 @@ class Controller:
self.price_gap = False self.price_gap = False
self.price_gap_since = None self.price_gap_since = None
self._price_gap_announced = False 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 self._gated_key = None # MAINT-13: (options, hour) of the cache
# TARIFF-03. One line of prose for the status page saying WHICH tariff # 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: # 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 # Owner, 2026-10-14: the words say 'maintenance waits', so they show
# only while the maintenance charge is on. # only while the maintenance charge is on.
self.price_gap = ((not gate.covered) and self.tariff_at is not None 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 last = self._last_published
if gate.covered or last is None: if gate.covered or last is None:
self.price_gap_since = 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 # Not a safety check, so it is one of the cheap ones: an attribute test
# and, on a change only, one Home Assistant call. # and, on a change only, one Home Assistant call.
await self.check_price_gap() 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 --- # --- SAFETY-31's alarm, and its boundary is the LATCH, not a number ---
# Unconstrained where the two above are not: it grades # Unconstrained where the two above are not: it grades
@@ -6229,18 +6229,21 @@ class Controller:
await self.hass.call("persistent_notification", "dismiss", await self.hass.call("persistent_notification", "dismiss",
messages.prices_back().data()) messages.prices_back().data())
async def check_fixed_unused(self) -> None: async def check_fixed_note(self) -> None:
"""PRICE-01 F2. A `static_profile` beside `price_service` no longer """PRICE-01 G1. At the first cycle only: one INFO line when a
drives the cheap window or the maintenance gate. Not refused: said once, `static_profile` is set beside `price_service` (it is the plan's last
at the first cycle, in the log and as one notification.""" fallback, not a fault, so no notification), and a dismiss of the card an
if self._fixed_unused_said: earlier build raised, which is harmless when absent."""
if self._fixed_note_said:
return return
self._fixed_unused_said = True self._fixed_note_said = True
if fixed_tariff_unused(self.o): if fixed_tariff_beside_service(self.o):
_LOG.warning("The fixed tariff is not used, because the price " _LOG.info(messages.FIXED_TARIFF_LOG)
"service gives the prices.") try:
await self.hass.call("persistent_notification", "create", await self.hass.call("persistent_notification", "dismiss",
messages.fixed_tariff_unused().data()) 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: def _gap_args(self) -> tuple:
"""`(since, now)` in Brussels time for the gap's words. `since` is None """`(since, now)` in Brussels time for the gap's words. `since` is None
+19 -15
View File
@@ -131,8 +131,6 @@ T_MAINT_FAILED = "Battery cycle did not finish"
T_MAINT_FAILED_NL = "Batterijcyclus niet afgerond" T_MAINT_FAILED_NL = "Batterijcyclus niet afgerond"
T_NO_PRICES = "No prices" T_NO_PRICES = "No prices"
T_NO_PRICES_NL = "Prijzen ontbreken" 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_LOOP_BOUND = "goodwe_integrator_bound"
ID_OUTPUT_CLAMP = "goodwe_output_clamp" ID_OUTPUT_CLAMP = "goodwe_output_clamp"
@@ -143,7 +141,9 @@ ID_NOT_DELIVERING = "goodwe_not_delivering_{slug}"
ID_METER_FROZEN = "goodwe_meter_frozen" ID_METER_FROZEN = "goodwe_meter_frozen"
ID_MAINT_FAILED = "goodwe_maint_failed_{slug}" ID_MAINT_FAILED = "goodwe_maint_failed_{slug}"
ID_NO_PRICES = "goodwe_no_prices" 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). # Which limit cut a command (clamp.ClampResult.bound, plus the loop's own).
# The setting is named by its label, "Inverter rating (W)". # 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}") 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) @dataclass(frozen=True)
class Dismiss: class Dismiss:
"""The payload that takes one notification down again.""" """The payload that takes one notification down again."""
@@ -379,6 +368,22 @@ def prices_back() -> Dismiss:
return Dismiss(ID_NO_PRICES) 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: def row_no_prices(since=None, now=None) -> str:
"""PRICE-01: the status row while no price covers now.""" """PRICE-01: the status row while no price covers now."""
if since is not None: if since is not None:
@@ -1301,7 +1306,6 @@ def samples() -> list:
no_prices(*_SINCE_SAMPLE), no_prices(*_SINCE_SAMPLE),
no_prices(*_SINCE_OLDER), no_prices(*_SINCE_OLDER),
no_prices(), no_prices(),
fixed_tariff_unused(),
] ]
+4 -3
View File
@@ -50,9 +50,10 @@ def uses_chain(opts: dict) -> bool:
return bool(cls is not None and (cls.FALLBACKS or cls.KEEPS_LAST_GOOD)) return bool(cls is not None and (cls.FALLBACKS or cls.KEEPS_LAST_GOOD))
def fixed_tariff_unused(opts: dict) -> bool: def fixed_tariff_beside_service(opts: dict) -> bool:
"""PRICE-01 F2: a fixed tariff is set beside `price_service`, which then """PRICE-01: a fixed tariff is set beside `price_service`. Not a fault: it
decides the cheap window and the gate. Accepted, so it is said, not refused.""" 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" return (provider_id(opts) == "price_service"
and bool(str(price_option(opts, "static_profile")).strip())) and bool(str(price_option(opts, "static_profile")).strip()))
+7 -4
View File
@@ -12,6 +12,7 @@ import copy
import os import os
import re import re
from . import messages
from . import uilayout as L from . import uilayout as L
from . import uitext from . import uitext
from . import uimodel as U 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] kept = [p for p in problems if p in kept]
notes = [n for n in notes if not set(n.keys or ()) & failed] notes = [n for n in notes if not set(n.keys or ()) & failed]
return ([derive_message(err, lang) for err in errors] 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)] 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 """[{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() out, seen = [], set()
def add(field, card, tier, text): def add(field, card, tier, text):
@@ -312,10 +314,11 @@ def anchors(problems, notes, model, keys) -> list:
add(None, None, "bad", text) add(None, None, "bad", text)
for note in notes: for note in notes:
tier = note.tier if note.tier in _TIERS else "info" 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 key in note.keys or (None,):
for field, card in (targets(key, note.text, model) if key for field, card in (targets(key, note.text, model) if key
else [(None, None)]): 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. # A sentence placed on one inverter's field is not also put on the list.
roster = _roster() roster = _roster()
placed = {m["text"] for m in out placed = {m["text"] for m in out
+6
View File
@@ -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 " f"{S('static_profile')} is empty, so the site has no tariff "
"at all while the price feed is down.") "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 # ⚠️ 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 # 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 - # fire is indistinguishable from one that fires and finds no opportunity -
+2 -1
View File
@@ -312,7 +312,8 @@ def build_app(controller, selfcfg=None) -> web.Application:
_keys_of(options), _keys_of(options),
uitext.lang_of(_body.get("lang"))) uitext.lang_of(_body.get("lang")))
if errors 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) payload["steps"] = uipage.step_problems(payload["messages"], model)
if _body.get("setup"): if _body.get("setup"):
payload["setup"] = [n._asdict() for n in await _setup_checks(options)] payload["setup"] = [n._asdict() for n in await _setup_checks(options)]
+3 -1
View File
@@ -415,7 +415,9 @@ def put_in_charge(c, which, hours_ago, open_min):
def notes(h): 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): for cheap in (True, False):
+118 -26
View File
@@ -742,10 +742,16 @@ class Wired:
await self.ctl.read_inputs() await self.ctl.read_inputs()
await self.ctl.cycle() await self.ctl.cycle()
def notes(self): def all_notes(self):
return [(s, d) for dom, s, d in self.ctl.hass.calls return [(s, d) for dom, s, d in self.ctl.hass.calls
if dom == "persistent_notification"] 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(): async def _scenario_gap():
out = {} 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(" 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": [ _PROFILE = json.dumps({"periods": [
{"name": "all", "days": "all", "from": "00:00", "to": "24:00", {"name": "all", "days": "all", "from": "00:00", "to": "24:00",
"eur_per_kwh": 0.25}]}) "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): async def _scenario_fixed(**over):
with Wired(NOW, **over) as W: with Wired(NOW, **over) as W:
before = mark()
await W.refresh() await W.refresh()
for k in range(4): for k in range(4):
await W.tick(NOW + timedelta(seconds=30 * k)) await W.tick(NOW + timedelta(seconds=30 * k))
return [x for x in W.notes() if x[1].get("notification_id") said = [m for m in since_mark(before, logging.INFO) if "only used if no prices" in m]
== "goodwe_fixed_tariff_unused"], W.ctl return said, W.all_notes(), W.ctl
_before = mark() def _settings_notes(**over):
_f, _fc = run(_scenario_fixed(static_profile=_PROFILE)) base = dict(OPTS_C)
_logged = [m for m in since_mark(_before, logging.WARNING) base.update(price_provider="price_service", price_service_token=TOKEN)
if "fixed tariff" in m] base.update(over)
check("price_service with a static_profile: ONE notification over four " 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", "ticks, and the setting is not refused",
len(_f) == 1 and _f[0][0] == "create" _said == [_LINE] and problems(static_profile=_PROFILE) == [])
and problems(static_profile=_PROFILE) == []) check("...no notification is raised, only the old id is dismissed",
check("...in English and Dutch, plain and short", [x for x in _fn if x[1].get("notification_id") == _OLD_ID]
_f[0][1]["title"] == "Fixed tariff not used" == [("dismiss", {"notification_id": _OLD_ID})]
and "The fixed tariff is not used, because the price service gives the " and not [x for x in _fn if x[0] == "create"])
"prices." in _f[0][1]["message"] for _label, _kw in (("no profile", {}),
and "Vast tarief niet gebruikt. Het vaste tarief wordt niet gebruikt, " ("an empty profile", {"static_profile": ""}),
"want de prijsdienst levert de prijzen." in _f[0][1]["message"]) ("a spaces-only profile", {"static_profile": " "}),
check("...and one WARNING line in the log", ("a line-break-only profile", {"static_profile": "\n \t"}),
_logged == ["The fixed tariff is not used, because the price service " ("another provider", {"price_provider": "generic_dayahead",
"gives the prices."]) "static_profile": _PROFILE})):
check("no static_profile: nothing is said", _s, _n, _c = run(_scenario_fixed(**_kw))
run(_scenario_fixed())[0] == []) check(f"{_label}: no log line", _s == [])
check("another provider with a static_profile: nothing is said (there it is " check(f"{_label}: the old id is still dismissed at start (harmless)",
"the fallback)", [x for x in _n if x[1].get("notification_id") == _OLD_ID]
run(_scenario_fixed(price_provider="generic_dayahead", == [("dismiss", {"notification_id": _OLD_ID})])
static_profile=_PROFILE))[0] == []) _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)") print("\n15. round 2: a token shorter than 16 characters is refused (F5)")
+3 -1
View File
@@ -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:") print("\n after the restart, on the saved settings:")
rc, ev, err, files = run_child("after", SAVED or {}, SAVED_STATE) 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 writes = [e[3].get("value") for e in services
if e[1:3] == ["number", "set_value"]] if e[1:3] == ["number", "set_value"]]
print(f" after: exit {rc}, {len(services)} service calls, set_value " print(f" after: exit {rc}, {len(services)} service calls, set_value "
+5 -2
View File
@@ -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", check("...and it is still commanding 0 W on the far side of the restart",
_b.target == 0.0 and "circulation" in _b.reason) _b.target == 0.0 and "circulation" in _b.reason)
_notes1 = [c for c in _h1.calls if c[0] == "persistent_notification"] # Not the dismiss of PRICE-01's old fixed-tariff card, sent at every start.
_notes2 = [c for c in _h2.calls if c[0] == "persistent_notification"] _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, " print(f" notifications raised: {len(_notes1)} before the restart, "
f"{len(_notes2)} after it") f"{len(_notes2)} after it")
check("the alarm was raised in the first process", len(_notes1) >= 1) check("the alarm was raised in the first process", len(_notes1) >= 1)