diff --git a/goodwe_controller/DOCS.md b/goodwe_controller/DOCS.md index c39ea7e..78747cb 100644 --- a/goodwe_controller/DOCS.md +++ b/goodwe_controller/DOCS.md @@ -3120,8 +3120,9 @@ 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 -Monday and Wednesday only. +save. Prices take five decimals: divide the c€/kWh on your bill by 100, so +34.512 c€/kWh is 0.34512 €/kWh. 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..9c603ef 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: divide c€/kWh from the bill by 100, so 34.512 c€/kWh is 0.34512."}, + "day_price": {"name": "Day price, weekdays 07:00 to 22:00", "description": "All in, as on your bill: divide c€/kWh by 100, so 34.512 c€/kWh is 0.34512."}, + "night_price": {"name": "Night and weekend price", "description": "All in, as on your bill: divide c€/kWh by 100, so 34.512 c€/kWh is 0.34512."}, + "periods": {"name": "Time windows and prices", "description": "One price per kWh for each time window, all in, as on your bill: divide c€/kWh by 100, so 34.512 c€/kWh is 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..6c4763c 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: deel c€/kWh van de factuur door 100, dus 34,512 c€/kWh is 0,34512."}, + "day_price": {"name": "Dagprijs, weekdagen 07:00 tot 22:00", "description": "Alles inbegrepen, zoals op uw factuur: deel c€/kWh door 100, dus 34,512 c€/kWh is 0,34512."}, + "night_price": {"name": "Nacht- en weekendprijs", "description": "Alles inbegrepen, zoals op uw factuur: deel c€/kWh door 100, dus 34,512 c€/kWh is 0,34512."}, + "periods": {"name": "Tijdvakken en prijzen", "description": "Eén prijs per kWh voor elk tijdvak, alles inbegrepen, zoals op uw factuur: deel c€/kWh door 100, dus 34,512 c€/kWh is 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")