From 8f72ab00d733800d7c8baa86c5b58df3573f75b2 Mon Sep 17 00:00:00 2001 From: glenn schrooyen Date: Fri, 16 Oct 2026 01:40:40 +0200 Subject: [PATCH] UX-43: tariff price fields take five decimals ui.fixed_price, ui.day_price, ui.night_price and ui.periods step 0.00001 EUR/kWh (the period editor's price input took a hard-coded 0.01). A short c-EUR/kWh hint is added to the four field descriptions, EN and NL. Co-Authored-By: Claude Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC --- goodwe_controller/DOCS.md | 2 +- goodwe_controller/app/static/settings.js | 2 +- goodwe_controller/app/ui_text/en.json | 8 ++-- goodwe_controller/app/ui_text/nl.json | 8 ++-- goodwe_controller/app/uilayout.py | 8 ++-- goodwe_controller/test_uimodel.py | 60 ++++++++++++++++++++++++ 6 files changed, 74 insertions(+), 14 deletions(-) diff --git a/goodwe_controller/DOCS.md b/goodwe_controller/DOCS.md index c39ea7e..e35a271 100644 --- a/goodwe_controller/DOCS.md +++ b/goodwe_controller/DOCS.md @@ -3120,7 +3120,7 @@ configuration rather than "turn the price feature off and hope". On the add-on page, choose **Other time windows** under Settings, Tariff and prices, or in Setup step 4. Add one row per time window, with its start, end, days and price. The page names any overlap, gap or empty price before you can -save. You only need the JSON below for days the page cannot choose, such as +save. Prices take five decimals (0.34512 €/kWh); a bill in c€/kWh divides by 100, so 34.512 c€/kWh is 0.34512. You only need the JSON below for days the page cannot choose, such as Monday and Wednesday only. It is one option, holding a JSON object, on one line: diff --git a/goodwe_controller/app/static/settings.js b/goodwe_controller/app/static/settings.js index 8e5cc2e..d65d00d 100644 --- a/goodwe_controller/app/static/settings.js +++ b/goodwe_controller/app/static/settings.js @@ -311,7 +311,7 @@ const days = h("select", { class: "sel", "aria-label": say(P.days) }); for (const [v, label] of f.days || []) days.append(h("option", { value: v, selected: v === row.days }, label)); days.addEventListener("change", () => set("days", days.value)); - const price = h("input", { class: "num", type: "number", inputmode: "decimal", step: "0.01", + const price = h("input", { class: "num", type: "number", inputmode: "decimal", step: String(f.step || 0.01), value: row.price == null ? "" : row.price, "aria-label": say(P.price) }); price.addEventListener("input", () => set("price", price.value === "" || isNaN(+price.value) ? null : +price.value)); const cell = (label, ...kids) => h("label", { class: "pf" }, h("span", { class: "tiny muted" }, label), ...kids); diff --git a/goodwe_controller/app/ui_text/en.json b/goodwe_controller/app/ui_text/en.json index 0645abd..aac7b42 100644 --- a/goodwe_controller/app/ui_text/en.json +++ b/goodwe_controller/app/ui_text/en.json @@ -23,10 +23,10 @@ "contract": {"name": "Your contract", "description": "How your supplier prices each kWh you take from the grid."}, "region": {"name": "Region", "description": "Fills in grid costs, levies and VAT for the year shown, but not yet for Wallonia or Brussels, so check your bill."}, "fallback": {"name": "Price when the feed is down", "description": "Used when the price source does not answer."}, - "fixed_price": {"name": "Price per kWh, all in", "description": "Energy, supplier costs, grid costs, levies and VAT together."}, - "day_price": {"name": "Day price, weekdays 07:00 to 22:00", "description": "All in, as on your bill."}, - "night_price": {"name": "Night and weekend price", "description": "All in, as on your bill."}, - "periods": {"name": "Time windows and prices", "description": "One price per kWh for each time window, all in, as on your bill."} + "fixed_price": {"name": "Price per kWh, all in", "description": "Energy, supplier costs, grid costs, levies and VAT together. Bill in c€/kWh? Divide by 100: 34.512 c€/kWh = 0.34512."}, + "day_price": {"name": "Day price, weekdays 07:00 to 22:00", "description": "All in, as on your bill. Bill in c€/kWh? Divide by 100: 34.512 c€/kWh = 0.34512."}, + "night_price": {"name": "Night and weekend price", "description": "All in, as on your bill. Bill in c€/kWh? Divide by 100: 34.512 c€/kWh = 0.34512."}, + "periods": {"name": "Time windows and prices", "description": "One price per kWh for each time window, all in, as on your bill. Bill in c€/kWh? Divide by 100: 34.512 c€/kWh = 0.34512."} }, "choices": { "contract": {"none": "No prices", "fixed": "Fixed price", "daynight": "Day and night", "dynamic": "Dynamic, hourly", "custom": "Other time windows"}, diff --git a/goodwe_controller/app/ui_text/nl.json b/goodwe_controller/app/ui_text/nl.json index 4f7102c..b2e0d0e 100644 --- a/goodwe_controller/app/ui_text/nl.json +++ b/goodwe_controller/app/ui_text/nl.json @@ -23,10 +23,10 @@ "contract": {"name": "Uw contract", "description": "Hoe uw leverancier elke kWh prijst die u van het net afneemt."}, "region": {"name": "Regio", "description": "Vult nettarieven, heffingen en btw in voor het getoonde jaar, nog niet voor Wallonië of Brussel, dus kijk uw factuur na."}, "fallback": {"name": "Prijs als de feed wegvalt", "description": "Gebruikt als de prijsbron niet antwoordt."}, - "fixed_price": {"name": "Prijs per kWh, alles inbegrepen", "description": "Energie, kosten van de leverancier, nettarieven, heffingen en btw samen."}, - "day_price": {"name": "Dagprijs, weekdagen 07:00 tot 22:00", "description": "Alles inbegrepen, zoals op uw factuur."}, - "night_price": {"name": "Nacht- en weekendprijs", "description": "Alles inbegrepen, zoals op uw factuur."}, - "periods": {"name": "Tijdvakken en prijzen", "description": "Eén prijs per kWh voor elk tijdvak, alles inbegrepen, zoals op uw factuur."} + "fixed_price": {"name": "Prijs per kWh, alles inbegrepen", "description": "Energie, kosten van de leverancier, nettarieven, heffingen en btw samen. Factuur in c€/kWh? Deel door 100: 34,512 c€/kWh = 0,34512."}, + "day_price": {"name": "Dagprijs, weekdagen 07:00 tot 22:00", "description": "Alles inbegrepen, zoals op uw factuur. Factuur in c€/kWh? Deel door 100: 34,512 c€/kWh = 0,34512."}, + "night_price": {"name": "Nacht- en weekendprijs", "description": "Alles inbegrepen, zoals op uw factuur. Factuur in c€/kWh? Deel door 100: 34,512 c€/kWh = 0,34512."}, + "periods": {"name": "Tijdvakken en prijzen", "description": "Eén prijs per kWh voor elk tijdvak, alles inbegrepen, zoals op uw factuur. Factuur in c€/kWh? Deel door 100: 34,512 c€/kWh = 0,34512."} }, "choices": { "contract": {"none": "Geen prijzen", "fixed": "Vaste prijs", "daynight": "Dag en nacht", "dynamic": "Dynamisch, per uur", "custom": "Andere tijdvakken"}, diff --git a/goodwe_controller/app/uilayout.py b/goodwe_controller/app/uilayout.py index 279563a..fbe9535 100644 --- a/goodwe_controller/app/uilayout.py +++ b/goodwe_controller/app/uilayout.py @@ -164,13 +164,13 @@ GROUPS = ( wizard=4), Field("price_export_formula", TEXT), Field("ui.fallback", SEGMENTED, show=_DYNAMIC, wizard=4), - Field("ui.fixed_price", NUMBER, "€/kWh", step=0.01, + Field("ui.fixed_price", NUMBER, "€/kWh", step=0.00001, show=(("ui.fallback", "==", "fixed"),), wizard=4), - Field("ui.day_price", NUMBER, "€/kWh", step=0.01, + Field("ui.day_price", NUMBER, "€/kWh", step=0.00001, show=(("ui.fallback", "==", "daynight"),), wizard=4), - Field("ui.night_price", NUMBER, "€/kWh", step=0.01, + Field("ui.night_price", NUMBER, "€/kWh", step=0.00001, show=(("ui.fallback", "==", "daynight"),), wizard=4), - Field("ui.periods", PERIOD_LIST, "€/kWh", step=0.01, + Field("ui.periods", PERIOD_LIST, "€/kWh", step=0.00001, show=(("ui.fallback", "==", "custom"),), wizard=4), )), Part("selling", ( diff --git a/goodwe_controller/test_uimodel.py b/goodwe_controller/test_uimodel.py index 092e1da..975f6bd 100644 --- a/goodwe_controller/test_uimodel.py +++ b/goodwe_controller/test_uimodel.py @@ -327,6 +327,66 @@ check("days the editor has no choice for stay in the Configuration tab, and " ["ui.periods"] is None and edit(dict(SITE, static_profile=_raw), gain=0.5) ["static_profile"] == _raw) +print("\n8. UX-43: tariff prices take five decimals") +from app import uilayout as _L # noqa: E402 +_PX = 0.34512 +_TARIFF = ("ui.fixed_price", "ui.day_price", "ui.night_price", "ui.periods") +_steps = {k: _L.field(k).step for k in _TARIFF} +check("the four tariff price fields step 0.00001 EUR/kWh", + sorted(_steps) == sorted(_TARIFF) + and all(v == 0.00001 for v in _steps.values())) +_f5 = edit(DEFAULTS, ui__contract="fixed", ui__fixed_price=_PX) +check("fixed price 0.34512 is saved exactly and read back exactly", + '"eur_per_kwh": 0.34512}' in _f5["static_profile"] + and U.options_to_ui(_f5)["ui.fixed_price"] == _PX + and trip(_f5) == _f5) +_d5 = edit(SITE, ui__contract="daynight", ui__day_price=_PX, + ui__night_price=0.26789) +check("day price 0.34512 and night price 0.26789 are saved and read back exactly", + '"eur_per_kwh": 0.34512' in _d5["static_profile"] + and '"eur_per_kwh": 0.26789' in _d5["static_profile"] + and U.options_to_ui(_d5)["ui.day_price"] == _PX + and U.options_to_ui(_d5)["ui.night_price"] == 0.26789 + and not validate_startup_config(_d5)) +_n5 = edit(SITE, ui__contract="daynight", ui__day_price=0.3, ui__night_price=_PX) +check("night price 0.34512 alone is saved and read back exactly", + U.options_to_ui(_n5)["ui.night_price"] == _PX) +_r3 = rows(("00:00", "07:00", "mon-sun", 0.12345), ("07:00", "22:00", "mon-sun", _PX), + ("22:00", "24:00", "mon-sun", 0.05001)) +_p3 = periods(_r3) +check("three windows with five-decimal prices save and read back exactly", + [p.eur_per_kwh for p in parse_static_profile(_p3["static_profile"]).periods] + == [0.12345, _PX, 0.05001] + and [r["price"] for r in U.options_to_ui(_p3)["ui.periods"]] + == [0.12345, _PX, 0.05001] and trip(_p3) == _p3 + and "e-" not in _p3["static_profile"]) +check("a two-decimal price still works", U.options_to_ui( + edit(DEFAULTS, ui__contract="fixed", ui__fixed_price=0.31)) + ["ui.fixed_price"] == 0.31) +_old = dict(SITE, price_provider="off", static_profile=json.dumps({"periods": [ + {"name": "day", "from": "07:00", "to": "22:00", "days": "mon-fri", + "eur_per_kwh": 0.34}, + {"name": "night", "from": "22:00", "to": "07:00", "days": "mon-fri", + "eur_per_kwh": 0.26}, + {"name": "night", "from": "00:00", "to": "24:00", "days": "sat,sun", + "eur_per_kwh": 0.26}]})) +check("a stored profile with two-decimal prices is unchanged by load and save", + trip(_old) == _old) +_js = open(os.path.join(HERE, "app", "static", "settings.js"), + encoding="utf-8").read() +check("the window editor's price input takes its step from the field, not a " + "hard-coded 0.01", 'step: "0.01"' not in _js + and "step: String(f.step || 0.01)" in _js) +for _lang in ("en", "nl"): + with open(os.path.join(HERE, "app", "ui_text", f"{_lang}.json"), + encoding="utf-8") as _fh: + _flds = json.load(_fh)["fields"] + _hint = "34.512" if _lang == "en" else "34,512" + _eq = "0.34512" if _lang == "en" else "0,34512" + check(f"[{_lang}] the c€/kWh hint sits on all four tariff price fields", + all(_hint in _flds[k]["description"] and _eq in _flds[k]["description"] + for k in ("fixed_price", "day_price", "night_price", "periods"))) + print() if fails: print(f"{len(fails)} FAILED")