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 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
This commit is contained in:
glenn schrooyen
2026-10-16 01:40:40 +02:00
co-authored by Claude
parent a0e091cca3
commit 8f72ab00d7
6 changed files with 74 additions and 14 deletions
+1 -1
View File
@@ -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 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, 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 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. Monday and Wednesday only.
It is one option, holding a JSON object, on one line: It is one option, holding a JSON object, on one line:
+1 -1
View File
@@ -311,7 +311,7 @@
const days = h("select", { class: "sel", "aria-label": say(P.days) }); 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)); 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)); 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) }); value: row.price == null ? "" : row.price, "aria-label": say(P.price) });
price.addEventListener("input", () => set("price", price.value === "" || isNaN(+price.value) ? null : +price.value)); 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); const cell = (label, ...kids) => h("label", { class: "pf" }, h("span", { class: "tiny muted" }, label), ...kids);
+4 -4
View File
@@ -23,10 +23,10 @@
"contract": {"name": "Your contract", "description": "How your supplier prices each kWh you take from the grid."}, "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."}, "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."}, "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."}, "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."}, "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."}, "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."} "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": { "choices": {
"contract": {"none": "No prices", "fixed": "Fixed price", "daynight": "Day and night", "dynamic": "Dynamic, hourly", "custom": "Other time windows"}, "contract": {"none": "No prices", "fixed": "Fixed price", "daynight": "Day and night", "dynamic": "Dynamic, hourly", "custom": "Other time windows"},
+4 -4
View File
@@ -23,10 +23,10 @@
"contract": {"name": "Uw contract", "description": "Hoe uw leverancier elke kWh prijst die u van het net afneemt."}, "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."}, "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."}, "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."}, "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."}, "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."}, "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."} "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": { "choices": {
"contract": {"none": "Geen prijzen", "fixed": "Vaste prijs", "daynight": "Dag en nacht", "dynamic": "Dynamisch, per uur", "custom": "Andere tijdvakken"}, "contract": {"none": "Geen prijzen", "fixed": "Vaste prijs", "daynight": "Dag en nacht", "dynamic": "Dynamisch, per uur", "custom": "Andere tijdvakken"},
+4 -4
View File
@@ -164,13 +164,13 @@ GROUPS = (
wizard=4), wizard=4),
Field("price_export_formula", TEXT), Field("price_export_formula", TEXT),
Field("ui.fallback", SEGMENTED, show=_DYNAMIC, wizard=4), 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), 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), 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), 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), show=(("ui.fallback", "==", "custom"),), wizard=4),
)), )),
Part("selling", ( Part("selling", (
+60
View File
@@ -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) ["ui.periods"] is None and edit(dict(SITE, static_profile=_raw), gain=0.5)
["static_profile"] == _raw) ["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() print()
if fails: if fails:
print(f"{len(fails)} FAILED") print(f"{len(fails)} FAILED")