UX-13 P3: Simple view and Batteries screen
- Batteries: one card per battery with On/Off, the mode list (cheap hours and selling greyed out until OPT-11 P3), level, and Now / Today / Why lines built in homeview.py. Replaces the OPT-11 P1 minimal cards and uses the existing pack_use / pack_mode actions and store keys. - An Off battery the peak limit still needs says so with its figure; no line shows 0 W beside moving power (OPT-11 P1 test F2). Today says "Not planned yet" until OPT-11 P3/P4. - Home's plan leaves Off batteries out, except for the peak limit. - Settings opens on Simple: the layout's simple fields with an applies chip and a note, plus the battery switches and modes. Installer is one tap. - English and Dutch words in app/ui_text; test_batteries_screen.py; browser check tools/ux13_browser_check.py (CHECK=1 ux13_screenshots.sh); new and changed screenshots only. No control, allocator, arbiter or command module changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UBtqPrG1mDLXzTEBnhEkkV
This commit is contained in:
@@ -20,6 +20,7 @@ from datetime import datetime, timedelta, timezone
|
||||
from . import arbitrage, messages, peakguard, uitext
|
||||
from .maintenance import CHARGE as M_CHARGE, DRAIN as M_DRAIN, HOLD as M_HOLD
|
||||
from .maintenance import IDLE as M_IDLE
|
||||
from .packmode import LEFTOVER_SOLAR, SOLAR_FIRST
|
||||
from .price import Absence, Uncertainty, brussels
|
||||
from .static_tariff import static_plan
|
||||
from .tariff import BILLING_TZ
|
||||
@@ -284,6 +285,8 @@ def trade_runs(c, plan, verdict, day0, day1, now, shadow):
|
||||
continue
|
||||
hrs = (end - pt.start).total_seconds() / 3600.0
|
||||
for i, inv in enumerate(c.inverters):
|
||||
if _off(c, inv):
|
||||
continue # OPT-11: an Off battery does not trade
|
||||
kwh = inv.max_w / 1000.0 * hrs
|
||||
cap = inv.capacity_kwh
|
||||
if cap:
|
||||
@@ -317,8 +320,8 @@ def maintenance_runs(c, day0, day1, now, shadow):
|
||||
waiting = []
|
||||
for i, (inv, m) in enumerate(zip(c.inverters, c.maints)):
|
||||
cfg = m.cfg
|
||||
if not cfg.enabled:
|
||||
continue
|
||||
if not cfg.enabled or _off(c, inv):
|
||||
continue # OPT-11: no monthly cycle while it is off
|
||||
cap = inv.capacity_kwh
|
||||
if m.phase != M_IDLE:
|
||||
hours = _cycle_hours(m.phase, inv.soc, cap, cfg)
|
||||
@@ -390,12 +393,13 @@ def peak_run(c, day0, day1, now, shadow, limit_w):
|
||||
for i in range(len(c.inverters))]
|
||||
|
||||
|
||||
def house_runs(n, day0, day1, now, shadow):
|
||||
""""Follows the house" for the rest of the day, drawn under the others."""
|
||||
def house_runs(n, day0, day1, now, shadow, skip=()):
|
||||
""""Follows the house" for the rest of the day, drawn under the others.
|
||||
`skip`: the batteries that are switched off (OPT-11)."""
|
||||
return [_run(i, "house", max(now, day0), day1, day0, day1,
|
||||
run_house_what(shadow), run_house_why(shadow),
|
||||
run_house_amount())
|
||||
for i in range(n)]
|
||||
for i in range(n) if i not in skip]
|
||||
|
||||
|
||||
def region_of(opts):
|
||||
@@ -429,7 +433,8 @@ def build_plan(c, now, shadow, running, held=False, settled=True):
|
||||
# The control cycle trades on the fresh plan only, so this does too.
|
||||
verdict, trade_why = trade_verdict(c, fresh)
|
||||
if running:
|
||||
runs += house_runs(len(c.inverters), day0, day1, now, shadow)
|
||||
off = {i for i, inv in enumerate(c.inverters) if _off(c, inv)}
|
||||
runs += house_runs(len(c.inverters), day0, day1, now, shadow, off)
|
||||
if verdict is not None:
|
||||
trade, later, no_trade = trade_runs(c, fresh, verdict, day0, day1,
|
||||
now, shadow)
|
||||
@@ -507,16 +512,56 @@ def at_zero(inv) -> bool:
|
||||
return inv.last_write is not None and abs(inv.last_write) < 1.0
|
||||
|
||||
|
||||
def _off(c, inv) -> bool:
|
||||
"""OPT-11: is this battery switched off? Only with battery modes on."""
|
||||
return bool(getattr(c, "modes_on", False)) and not c.pack_mode(inv).use
|
||||
|
||||
|
||||
def refusing(c, inv) -> int:
|
||||
"""SAFETY-52/73: the direction this battery refuses (+1 give, -1 charge,
|
||||
0 none). Per inverter at two or more, the site's own at one."""
|
||||
if len(c.inverters) > 1:
|
||||
return int(getattr(inv, "refused", 0) or 0)
|
||||
return int(getattr(c, "batt_refused", 0) or 0)
|
||||
|
||||
|
||||
def peak_engaged(c) -> bool:
|
||||
"""The peak guard is asking the batteries for power this cycle."""
|
||||
pg = getattr(c, "peak_guard", None)
|
||||
return bool(pg is not None and pg.engaged)
|
||||
|
||||
|
||||
def battery_rows(c, shadow, running, held=False):
|
||||
shares = {}
|
||||
alloc = getattr(c, "allocation", None)
|
||||
if shadow and alloc is not None and getattr(alloc, "shares", None):
|
||||
shares = dict(alloc.shares)
|
||||
modes_on = bool(getattr(c, "modes_on", False))
|
||||
pms = {inv.name: c.pack_mode(inv) for inv in c.inverters} \
|
||||
if modes_on else {}
|
||||
peak = peak_engaged(c)
|
||||
rows = []
|
||||
for i, inv in enumerate(c.inverters):
|
||||
m = c.maints[i] if i < len(c.maints) else None
|
||||
phase = m.phase if m is not None else M_IDLE
|
||||
cap = inv.capacity_kwh
|
||||
pm = pms.get(inv.name)
|
||||
off = pm is not None and not pm.use
|
||||
# UX-13 P3: does a Solar first battery that is on come before this one?
|
||||
sf_ahead = any(o.use and o.mode == SOLAR_FIRST
|
||||
for n, o in pms.items() if n != inv.name)
|
||||
# ...and does another battery refuse to give power, so this one
|
||||
# carries its share (SAFETY-73)?
|
||||
other = next((o.name for o in c.inverters if o is not inv
|
||||
and refusing(c, o) > 0 and not _off(c, o)), None)
|
||||
why = battery_why(
|
||||
modes_on=modes_on, mode=None if pm is None else pm.mode, off=off,
|
||||
reading=inv.soc is not None and inv.batt is not None,
|
||||
phase=phase, shadow=shadow, running=running, held=held,
|
||||
refused=refusing(c, inv), peak=peak,
|
||||
moving=flow(inv.batt) in ("giving", "charging"),
|
||||
giving=flow(inv.batt) == "giving", other_refuses=other,
|
||||
sf_ahead=sf_ahead)
|
||||
rows.append({
|
||||
"name": inv.name,
|
||||
"level_pct": None if inv.soc is None else float(inv.soc),
|
||||
@@ -530,21 +575,79 @@ def battery_rows(c, shadow, running, held=False):
|
||||
"now": battery_now(inv.soc, inv.batt, phase=phase, shadow=shadow,
|
||||
running=running, held=held,
|
||||
would_w=shares.get(inv.name),
|
||||
settled=at_zero(inv)),
|
||||
# Per-battery reasons arrive with the plan (OPT-11 P3/P4).
|
||||
"why": None,
|
||||
settled=at_zero(inv), off=off, peak=peak),
|
||||
# UX-13 P3: why it does what "now" says, from what the add-on
|
||||
# knows this cycle. Price and forecast reasons are OPT-11 P3/P4.
|
||||
"why_key": why,
|
||||
"why": why_words(why, name=other),
|
||||
})
|
||||
if getattr(c, "modes_on", False):
|
||||
if modes_on:
|
||||
# OPT-11 P1: the battery's switch and mode, for the Batteries
|
||||
# screen. Only with battery modes on, so the page is unchanged
|
||||
# without them. UX-13 P3 builds the real screen on these keys.
|
||||
pm = c.pack_mode(inv)
|
||||
# without them.
|
||||
rows[-1].update({"use": pm.use, "mode_key": pm.key,
|
||||
"mode_choice": pm.mode,
|
||||
"mode": mode_word(pm.key)})
|
||||
return rows
|
||||
|
||||
|
||||
def battery_why(*, modes_on, mode, off, reading, phase, shadow, running,
|
||||
held, refused, peak, moving, sf_ahead, giving=False,
|
||||
other_refuses=None) -> str:
|
||||
"""The key of one battery's "why" line (`home_words.why`), most urgent
|
||||
first. Pure, so every branch is tested without a controller."""
|
||||
if not reading:
|
||||
return "no_reading"
|
||||
if held:
|
||||
return "held"
|
||||
if not running:
|
||||
return "stopped"
|
||||
if shadow:
|
||||
return "shadow"
|
||||
if phase != M_IDLE:
|
||||
return "cycle_mode" if modes_on else "cycle"
|
||||
if off:
|
||||
# Off gives way to the peak limit only (OPT-11 design section 2).
|
||||
return "off_peak" if peak and moving else "off"
|
||||
if refused:
|
||||
return "refuses_give" if refused > 0 else "refuses_charge"
|
||||
if peak:
|
||||
return "peak"
|
||||
if giving and other_refuses:
|
||||
return "other_refuses"
|
||||
if not modes_on:
|
||||
return "modes_off"
|
||||
if giving:
|
||||
return "covers_house" # both modes cover the house until P3
|
||||
if mode == LEFTOVER_SOLAR:
|
||||
return "leftover" if sf_ahead else "leftover_alone"
|
||||
return "solar_first"
|
||||
|
||||
|
||||
def attach_today(rows, runs, *, modes_on, running=True, held=False,
|
||||
shadow=False) -> None:
|
||||
"""UX-13 P3: each battery's runs today from the plan (the same blocks as
|
||||
Home's chart), or one line saying there are none. With battery modes on
|
||||
and no run, the mode's own plan is OPT-11 P3/P4, so it says so."""
|
||||
for i, row in enumerate(rows):
|
||||
mine = [r for r in runs if r["battery"] == i and r["kind"] != "house"]
|
||||
row["today"] = [{"from": r["from"], "to": r["to"], "kind": r["kind"],
|
||||
"what": r["what"]} for r in mine]
|
||||
if mine:
|
||||
row["today_line"] = ""
|
||||
elif held:
|
||||
row["today_line"] = today_words("held")
|
||||
elif not running:
|
||||
row["today_line"] = today_words("stopped")
|
||||
elif row.get("use") is False:
|
||||
row["today_line"] = today_words("off")
|
||||
elif modes_on:
|
||||
row["today_line"] = today_words("not_planned")
|
||||
else:
|
||||
row["today_line"] = today_words("house_would" if shadow
|
||||
else "house")
|
||||
|
||||
|
||||
# Row id -> the DOCS.md heading under Notifications, where one exists.
|
||||
ATTN_DOCS = {
|
||||
"meter_frozen": messages.T_METER_FROZEN,
|
||||
@@ -640,6 +743,11 @@ def _build(c, *, level, checks, now):
|
||||
running = (bool(c.auto) or shadow) and not held
|
||||
settled = all(at_zero(inv) for inv in c.inverters)
|
||||
ctl = next((r for r in checks if r.get("id") == "controller"), None)
|
||||
rows = battery_rows(c, shadow, running, held)
|
||||
plan = build_plan(c, now, shadow, running, held=held, settled=settled)
|
||||
attach_today(rows, plan["runs"],
|
||||
modes_on=bool(getattr(c, "modes_on", False)),
|
||||
running=running, held=held, shadow=shadow)
|
||||
return {
|
||||
"state": {"kind": kind, "pill": pill(kind),
|
||||
"ran": ctl.get("text", "") if ctl else "",
|
||||
@@ -647,9 +755,9 @@ def _build(c, *, level, checks, now):
|
||||
"now": now_block(grid_w=c.grid, pv_w=c.pv_w, batt_w=c.site_batt_w,
|
||||
has_solar=bool(c.pv_roster), shadow=shadow,
|
||||
settled=settled),
|
||||
"batteries": battery_rows(c, shadow, running, held),
|
||||
"plan": build_plan(c, now, shadow, running, held=held,
|
||||
settled=settled),
|
||||
"batteries": rows,
|
||||
"plan": plan,
|
||||
"modes_on": bool(getattr(c, "modes_on", False)),
|
||||
"attention": ([{"severity": INFO, "id": "shadow",
|
||||
"title": attn_shadow(), "body": attn_shadow_body(),
|
||||
"docs": None}] if shadow else []) + attention(checks),
|
||||
@@ -811,7 +919,7 @@ def tile_batt(batt_w, shadow, settled=True) -> str:
|
||||
|
||||
|
||||
def battery_now(soc, batt_w, *, phase, shadow, running, held=False,
|
||||
would_w=None, settled=True) -> str:
|
||||
would_w=None, settled=True, off=False, peak=False) -> str:
|
||||
if soc is None or batt_w is None:
|
||||
return _t("now.no_reading")
|
||||
zero = _zero(settled)
|
||||
@@ -828,6 +936,14 @@ def battery_now(soc, batt_w, *, phase, shadow, running, held=False,
|
||||
return _t("now." + {M_DRAIN: "cycle_drain", M_CHARGE: "cycle_charge",
|
||||
M_HOLD: "cycle_hold"}.get(phase, "cycle"))
|
||||
st = flow(batt_w)
|
||||
if off:
|
||||
# OPT-11: an Off battery is asked for 0 W, but the peak limit may
|
||||
# still need it. Say which, and never "0 W" beside a power figure.
|
||||
if st == "resting":
|
||||
return _t("now.off_resting")
|
||||
if st == "giving" and peak:
|
||||
return _t("now.off_peak", kw=_kw(batt_w))
|
||||
return _t("now.off_going", kw=_kw(batt_w))
|
||||
if st == "giving":
|
||||
return _t("now.giving", kw=_kw(batt_w))
|
||||
if st == "charging":
|
||||
@@ -835,6 +951,17 @@ def battery_now(soc, batt_w, *, phase, shadow, running, held=False,
|
||||
return _t("now.resting")
|
||||
|
||||
|
||||
def why_words(key, name=None) -> str:
|
||||
"""One battery's "why" line (UX-13 P3), from `battery_why()`'s key.
|
||||
`name`: the battery that refuses, for `other_refuses`."""
|
||||
return _t("why." + key, name=name or "")
|
||||
|
||||
|
||||
def today_words(key) -> str:
|
||||
"""One battery's "today" line when the plan has no run for it."""
|
||||
return _t("today." + key)
|
||||
|
||||
|
||||
def plan_head(key, shadow, settled=True) -> str:
|
||||
if key == "held":
|
||||
return _t("head.held" if settled else "head.held_going")
|
||||
@@ -1039,6 +1166,14 @@ def _samples() -> list:
|
||||
("plan", line_region("flanders", 2026)),
|
||||
("plan", line_region("own", None)),
|
||||
("plan", line_peak_not_covered())]
|
||||
for off in (False, True):
|
||||
for peak in (False, True):
|
||||
for w in (2500, -2500, 0, 500):
|
||||
out.append(("plan", battery_now(62, w, phase=M_IDLE,
|
||||
shadow=False, running=True,
|
||||
off=off, peak=peak)))
|
||||
out += [("plan", why_words(k, name="Battery A")) for k in words["why"]]
|
||||
out += [("plan", today_words(k)) for k in words["today"]]
|
||||
out += [("plan", line_no_trade(k)) for k in
|
||||
("not_covered", "no_prices", "peak", "not_firm", "not_worth",
|
||||
"no_level")]
|
||||
|
||||
@@ -228,3 +228,19 @@ input.txt.time{width:5.5em;text-align:center;font-variant-numeric:tabular-nums}
|
||||
.sev.bad{background:var(--bad-bg);color:var(--bad)} .sev.warn{background:var(--warn-bg);color:var(--warn)}
|
||||
.checklist li{display:flex;gap:8px;align-items:flex-start;overflow-wrap:anywhere}
|
||||
.kettle .row{margin-top:8px}
|
||||
/* UX-13 P3: the Batteries screen and the Simple settings view */
|
||||
.bcard .hd{display:flex;align-items:center;gap:10px;margin-bottom:10px}
|
||||
.bcard .name{font-weight:700;font-size:16px}
|
||||
.bcard .modebox .lbl{display:block;margin-bottom:4px}
|
||||
.bcard .modebox .help{font-size:13px;color:var(--muted);margin-top:4px}
|
||||
.bcard.off .modebox{opacity:.6}
|
||||
.bcard .kv dd{overflow-wrap:anywhere}
|
||||
.switch:disabled{opacity:.45;cursor:not-allowed}
|
||||
select.sel:disabled{opacity:.6}
|
||||
.simple .card>.row{margin-bottom:4px}
|
||||
.vhide{display:none!important}
|
||||
.simple .card>.f{border-top:0;padding-top:0}
|
||||
.simple .f>.head{display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap}
|
||||
.simple .card .lbl{font-weight:600;font-size:15px}
|
||||
.simple .inv .hd{border-bottom:0}
|
||||
.simple .inv .f{border-top:0;padding-top:0}
|
||||
|
||||
@@ -78,10 +78,12 @@
|
||||
if (!r.ok) throw new Error("action " + r.status);
|
||||
return r.json();
|
||||
}
|
||||
async function refresh() {
|
||||
// `force`: redraw the battery cards even under a focused mode list, after
|
||||
// the person used it (P3).
|
||||
async function refresh(force) {
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
try { S = await getStatus(); render(); }
|
||||
try { S = await getStatus(); render(force === true); }
|
||||
catch (e) { offline(); }
|
||||
finally { busy = false; }
|
||||
}
|
||||
@@ -104,7 +106,7 @@
|
||||
$("#pill").replaceChildren(h("span", { class: "dot" + dot, "aria-hidden": "true" }), h("span", {}, words));
|
||||
}
|
||||
|
||||
function render() {
|
||||
function render(force) {
|
||||
renderPill();
|
||||
const structured = S.now && S.plan && !S.home_error;
|
||||
// UX-15: setup-only mode commands nothing and has no control to offer.
|
||||
@@ -114,7 +116,8 @@
|
||||
if (structured) { renderNow(); renderPlan(); renderPeak(); renderAttention(); }
|
||||
renderControl(structured, setup);
|
||||
renderDetails(!structured && !setup);
|
||||
renderBatteries();
|
||||
renderBatteries(force);
|
||||
document.dispatchEvent(new CustomEvent("ux13:status"));
|
||||
}
|
||||
|
||||
/* ---------------- Home: now ---------------- */
|
||||
@@ -308,50 +311,112 @@
|
||||
if (open) $("#c-details").open = true;
|
||||
}
|
||||
|
||||
/* ---------------- Batteries (placeholder until UX-13 P3) ---------------- */
|
||||
// OPT-11 P1: with battery modes on, each card gets an On/Off switch and a
|
||||
// mode choice (`mode_key` in /status). UX-13 P3 replaces these cards.
|
||||
/* ---------------- Batteries (UX-13 P3) ---------------- */
|
||||
// One card per battery. With battery modes on (`mode_key` in /status) each
|
||||
// card has an On/Off switch and a mode list, sent as `pack_use` and
|
||||
// `pack_mode` through /action (OPT-11 P1). Selling and cheap-hours charging
|
||||
// are shown greyed out until OPT-11 P3. The Now, Today and Why lines are
|
||||
// homeview.py's sentences. The Simple settings view draws the same switch
|
||||
// and list into its own card (simpleHost).
|
||||
const MODES = ["solar_first", "leftover_solar"];
|
||||
const say = (text, vars) => Object.keys(vars).reduce((x, k) => x.replace("{" + k + "}", vars[k]), text);
|
||||
let packBusy = false;
|
||||
async function packAct(what, unit, value) {
|
||||
let simpleHost = null;
|
||||
async function packAct(what, b, value) {
|
||||
if (packBusy) return;
|
||||
packBusy = true;
|
||||
try {
|
||||
const r = await fetch("action", { method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: what, unit: unit, value: value }) });
|
||||
body: JSON.stringify({ action: what, unit: b.name, value: value }) });
|
||||
const j = await r.json().catch(() => ({}));
|
||||
toast(r.ok ? t("toast.done") : (j.error || t("toast.failed")));
|
||||
if (!r.ok) toast(j.error || t("toast.failed"));
|
||||
else if (what === "pack_use") toast(say(value ? t("batteries.toast.on") : t("batteries.toast.off"), { name: b.name }));
|
||||
else toast(say(t("batteries.toast.mode"), { name: b.name, mode: t("batteries.choice." + value + ".name") }));
|
||||
} catch (e) { toast(t("toast.failed")); }
|
||||
finally { packBusy = false; }
|
||||
refresh();
|
||||
await refresh(true);
|
||||
}
|
||||
function modeControls(b) {
|
||||
const sw = h("button", { class: "btn" + (b.use ? " primary" : ""), type: "button", role: "switch",
|
||||
"aria-checked": String(b.use), "aria-label": b.name + ": " + t("batteries.use"),
|
||||
onclick: () => packAct("pack_use", b.name, !b.use) },
|
||||
b.use ? t("batteries.on") : t("batteries.off"));
|
||||
const sel = h("select", { class: "sel", "aria-label": b.name + ": " + t("batteries.mode"), disabled: !b.use,
|
||||
onchange: (e) => packAct("pack_mode", b.name, e.target.value) },
|
||||
...MODES.map((k) => h("option", { value: k, selected: b.mode_choice === k }, t("batteries.choice." + k + ".name"))));
|
||||
return [h("div", { class: "row spread", style: "margin-top:8px" }, h("span", { class: "small" }, t("batteries.use")), sw),
|
||||
h("div", { class: "row spread", style: "margin-top:8px" }, h("span", { class: "small" }, t("batteries.mode")), sel),
|
||||
h("p", { class: "tiny muted", style: "margin:6px 0 0" }, t("batteries.choice." + b.mode_key + ".description"))];
|
||||
function onSwitch(b, i, where, lid) {
|
||||
const sw = h("button", { type: "button", class: "switch", role: "switch", id: "bsw-" + where + i,
|
||||
"aria-checked": String(b.use), "aria-labelledby": lid,
|
||||
onclick: () => packAct("pack_use", b, !b.use) });
|
||||
return h("span", { class: "swrap" }, sw, h("span", { class: "swtext", "aria-hidden": "true" }, b.use ? t("batteries.on") : t("batteries.off")));
|
||||
}
|
||||
function renderBatteries() {
|
||||
if (!S.batteries) { $("#bcards").replaceChildren(); return; }
|
||||
// Do not rebuild a card under an open mode list or a request in flight.
|
||||
if (packBusy || ($("#bcards").contains(document.activeElement) && document.activeElement.tagName === "SELECT")) return;
|
||||
const modes = S.batteries.some((b) => b.mode_key != null);
|
||||
$("#batt-desc").textContent = t(modes ? "batteries.with_modes.description" : "batteries.description");
|
||||
$("#bcards").replaceChildren(...S.batteries.map((b) => h("div", { class: "card" },
|
||||
h("div", { class: "row spread" }, h("strong", {}, b.name), h("span", { class: "small muted" }, b.capacity_kwh ? b.capacity_kwh + " kWh" : "")),
|
||||
function modeSelect(b, i, where, lid) {
|
||||
return h("select", { class: "sel", id: "bsel-" + where + i, "aria-labelledby": lid, disabled: !b.use,
|
||||
onchange: (e) => packAct("pack_mode", b, e.target.value) },
|
||||
...MODES.map((k) => h("option", { value: k, selected: b.mode_choice === k }, t("batteries.choice." + k + ".name"))),
|
||||
h("option", { value: "", disabled: true }, t("batteries.later")));
|
||||
}
|
||||
function sellRow(i, where) {
|
||||
const lid = "bsl-" + where + i;
|
||||
return h("div", { class: "f inline" },
|
||||
h("div", {}, h("div", { class: "lbl", id: lid }, t("batteries.sell.name")), h("div", { class: "help" }, t("batteries.sell.description"))),
|
||||
h("div", { class: "ctl" }, h("button", { type: "button", class: "switch", role: "switch", "aria-checked": "false",
|
||||
"aria-labelledby": lid, disabled: true })));
|
||||
}
|
||||
function todayLines(b) {
|
||||
if (!b.today || !b.today.length) return b.today_line || "–";
|
||||
return b.today.map((r) => h("div", {}, r.from + " " + t("plan.time_to") + " " + r.to + ": " + r.what));
|
||||
}
|
||||
function batteryCard(b, i) {
|
||||
const modes = b.mode_key != null;
|
||||
const lid = "bname-" + i;
|
||||
const lvl = b.level_pct == null ? "–" : Math.round(b.level_pct) + " %";
|
||||
const stored = b.stored_kwh != null ? " · " + dec(b.stored_kwh.toFixed(1)) + " kWh " + t("batteries.stored").toLowerCase() : "";
|
||||
return h("div", { class: "card bcard" + (modes && !b.use ? " off" : "") },
|
||||
h("div", { class: "hd" },
|
||||
h("div", { class: "grow" }, h("div", { class: "name", id: lid }, b.name),
|
||||
h("div", { class: "tiny muted" }, b.capacity_kwh ? dec(Number(b.capacity_kwh).toFixed(1)) + " kWh" : "")),
|
||||
modes ? onSwitch(b, i, "c", lid) : null),
|
||||
modes ? h("div", { class: "modebox" },
|
||||
h("label", { class: "lbl small", for: "bsel-c" + i }, t("batteries.mode")),
|
||||
modeSelect(b, i, "c", null),
|
||||
h("div", { class: "help" }, t("batteries.choice." + b.mode_key + ".description")),
|
||||
sellRow(i, "c")) : null,
|
||||
levelBar(b.level_pct, b.name),
|
||||
h("dl", { class: "kv" },
|
||||
h("dt", {}, t("batteries.level")), h("dd", {}, b.level_pct == null ? "–" : Math.round(b.level_pct) + " %"),
|
||||
b.stored_kwh != null ? [h("dt", {}, t("batteries.stored")), h("dd", {}, dec(b.stored_kwh.toFixed(1)) + " kWh")] : null,
|
||||
h("dt", {}, t("batteries.level")), h("dd", {}, lvl + stored),
|
||||
h("dt", {}, t("batteries.now")), h("dd", {}, b.now),
|
||||
b.mode_key == null ? [h("dt", {}, t("batteries.mode")), h("dd", {}, b.mode)] : null),
|
||||
b.mode_key != null ? modeControls(b) : null)));
|
||||
h("dt", {}, t("batteries.today")), h("dd", {}, todayLines(b)),
|
||||
h("dt", {}, t("batteries.why")), h("dd", { class: "muted" }, b.why || "–")));
|
||||
}
|
||||
function simpleRow(b, i) {
|
||||
const lid = "bsname-" + i;
|
||||
return h("div", { class: "inv" },
|
||||
h("div", { class: "hd" }, h("span", { class: "grow", id: lid }, b.name), onSwitch(b, i, "s", lid)),
|
||||
h("div", { class: "f" }, h("label", { class: "lbl", for: "bsel-s" + i }, t("batteries.mode")),
|
||||
h("div", { class: "ctl" }, modeSelect(b, i, "s", null)),
|
||||
h("div", { class: "help" }, b.now + " " + (b.why || ""))));
|
||||
}
|
||||
// Rebuild without losing the focused control, and never under an open list
|
||||
// or a request in flight.
|
||||
function keepFocus(fn) {
|
||||
const id = document.activeElement && document.activeElement.id;
|
||||
fn();
|
||||
if (id && /^bs(w|el)-/.test(id)) { const e = document.getElementById(id); if (e) e.focus(); }
|
||||
}
|
||||
// The modes-off card's button opens Settings on the Installer view, where
|
||||
// the "Battery modes" switch is (settings.js sets window.UX13.settingsView).
|
||||
$("#batt-off a").addEventListener("click", () => { if (window.UX13.settingsView) window.UX13.settingsView("installer"); });
|
||||
function renderBatteries(force) {
|
||||
if (!S || !S.batteries) { $("#bcards").replaceChildren(); return; }
|
||||
const a = document.activeElement;
|
||||
if (!force && (packBusy || (a && a.tagName === "SELECT" && /^bsel-/.test(a.id)))) return;
|
||||
const modes = S.modes_on === true || S.batteries.some((b) => b.mode_key != null);
|
||||
$("#batt-off").hidden = modes;
|
||||
$("#batt-how").hidden = !modes;
|
||||
const docs = $("#batt-docs");
|
||||
if (S.control && S.control.docs_url) { docs.href = S.control.docs_url; docs.hidden = false; } else docs.hidden = true;
|
||||
if (S.state && ["shadow", "stopped", "alarm"].includes(S.state.kind)) banner($("#batt-banner"), S.level, S.state.pill, S.state.sub, S.level === "bad" ? "alert" : null);
|
||||
else $("#batt-banner").replaceChildren();
|
||||
keepFocus(() => {
|
||||
$("#bcards").replaceChildren(...S.batteries.map(batteryCard));
|
||||
if (simpleHost && simpleHost.isConnected) {
|
||||
simpleHost.replaceChildren(...(modes ? S.batteries.map(simpleRow)
|
||||
: [h("p", { class: "small muted", style: "margin:4px 0" }, t("batteries.modes_off.name") + ". " + t("batteries.description"))]));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------- control buttons ---------------- */
|
||||
@@ -385,7 +450,10 @@
|
||||
// Shared with settings.js, which loads after this file.
|
||||
// `first`: the page opened with no route, so settings.js may open the setup
|
||||
// wizard on a new install (UX-13 P4).
|
||||
window.UX13 = { h, ask, toast, t, words: WORDS, lang: LANG, maint: (what) => ask(what, () => simple(what)), first: !location.hash };
|
||||
window.UX13 = { h, ask, toast, t, words: WORDS, lang: LANG, maint: (what) => ask(what, () => simple(what)), first: !location.hash,
|
||||
// P3: the Simple settings view's battery card, drawn from /status.
|
||||
batteries: (host) => { simpleHost = host; if (S) renderBatteries(); refresh(); },
|
||||
status: () => S, kw };
|
||||
|
||||
/* ---------------- tabs: hash routes, arrow keys ---------------- */
|
||||
const TABS = [...document.querySelectorAll(".tab")];
|
||||
|
||||
@@ -94,11 +94,23 @@
|
||||
</section>
|
||||
|
||||
<section class="screen" id="s-batt" role="tabpanel" aria-labelledby="t-batt" hidden>
|
||||
<div id="bcards"></div>
|
||||
<div class="card">
|
||||
<p class="small muted" style="margin:0 0 10px" id="batt-desc" data-t="batteries.description"></p>
|
||||
<div id="batt-banner"></div>
|
||||
<div class="card" id="batt-off" hidden>
|
||||
<h2 data-t="batteries.modes_off.name"></h2>
|
||||
<p class="small muted" style="margin:0 0 10px" data-t="batteries.description"></p>
|
||||
<a class="btn" href="#settings" data-t="batteries.settings"></a>
|
||||
</div>
|
||||
<div id="bcards"></div>
|
||||
<details class="grp" id="batt-how" hidden>
|
||||
<summary><span class="grow"><strong data-t="batteries.how.name"></strong><span class="small muted gdesc" data-t="batteries.how.description"></span></span><span class="chev" aria-hidden="true">›</span></summary>
|
||||
<div class="body small">
|
||||
<p data-t="batteries.how.solar_first.description"></p>
|
||||
<p data-t="batteries.how.leftover_solar.description"></p>
|
||||
<p data-t="batteries.how.off.description"></p>
|
||||
<p class="muted" data-t="batteries.how.rules.description"></p>
|
||||
<p><a id="batt-docs" hidden target="_top" rel="noopener" data-t="control.docs"></a></p>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section class="screen" id="s-set" role="tabpanel" aria-labelledby="t-set" hidden>
|
||||
@@ -106,8 +118,15 @@
|
||||
<h2 style="margin:0" data-s="name"></h2>
|
||||
<a class="small" href="config" id="set-classic" data-s="classic"></a>
|
||||
</div>
|
||||
<p class="small muted" style="margin:0 0 10px" data-s="description"></p>
|
||||
<div class="row" style="margin-bottom:8px">
|
||||
<div class="seg" role="radiogroup" id="set-view" data-s-label="view.name">
|
||||
<button type="button" role="radio" aria-checked="true" data-v="simple" data-s="view.simple"></button>
|
||||
<button type="button" role="radio" aria-checked="false" data-v="installer" tabindex="-1" data-s="view.installer"></button>
|
||||
</div>
|
||||
<span class="tiny muted grow" id="set-desc"></span>
|
||||
</div>
|
||||
<div id="set-banner" aria-live="polite"></div>
|
||||
<div id="simple" class="simple" hidden></div>
|
||||
<div id="set-body">
|
||||
<input class="search" id="set-search" type="search" autocomplete="off" spellcheck="false" data-s-ph="search.description" data-s-label="search.name">
|
||||
<div class="card" id="set-other" hidden><h2 data-s="other"></h2><ul class="plain small" id="set-other-list"></ul></div>
|
||||
|
||||
@@ -32,6 +32,10 @@
|
||||
let FILLED = {}; // field key -> the preset field that fills it
|
||||
let view = null; // "settings" or "setup": the tab drawing V
|
||||
let STEPS = {}; // wizard step -> refusals that block it
|
||||
// P3: "simple" (the default, for home owners) or "installer". Remembered in
|
||||
// this browser only; a blocked storage just means Simple each time.
|
||||
let mode = "simple";
|
||||
try { if (localStorage.getItem("ux13.settings.view") === "installer") mode = "installer"; } catch (e) { /* no storage */ }
|
||||
let uid = 0;
|
||||
const ROWS = [];
|
||||
const OPEN = new Set();
|
||||
@@ -337,7 +341,7 @@
|
||||
function fieldRow(f, card, gid) {
|
||||
if (f.control === "inverter_list") return cardList(f, gid);
|
||||
const lid = "sf" + (++uid);
|
||||
const keys = keyText(f, card);
|
||||
const keys = gid === "simple" ? "" : keyText(f, card); // Simple shows no keys
|
||||
const msg = h("div", { class: "msgs" });
|
||||
const make = CONTROLS[f.control] || text;
|
||||
const row = h("div", { class: "f" + (f.control === "toggle" ? " inline" : ""), "data-key": f.key, "data-card": card },
|
||||
@@ -421,10 +425,79 @@
|
||||
d.append(body);
|
||||
host.append(d);
|
||||
}
|
||||
renderSimple();
|
||||
paint();
|
||||
window.scrollTo(0, y);
|
||||
}
|
||||
|
||||
/* ---------------- Settings, Simple (P3) ---------------- */
|
||||
// The fields the layout marks `simple`, each in its own card with when it
|
||||
// applies, and the batteries' switches and modes (runtime state, applied at
|
||||
// once) drawn by app.js from /status. The Installer rows stay built but
|
||||
// hidden, so the save bar counts every unsaved change in either view.
|
||||
const chip = (applies) => h("span", { class: "chip " + (applies === "now" ? "now" : "rs") }, ROOT.applies[applies].name);
|
||||
// A note with {kw} is filled from /status (app.js sends ux13:status).
|
||||
function fillNotes() {
|
||||
const st = window.UX13.status();
|
||||
const mp = st && st.month_peak;
|
||||
document.querySelectorAll("#simple [data-kw-note]").forEach((e) => {
|
||||
const ok = mp && mp.peak_w != null;
|
||||
e.hidden = !ok;
|
||||
e.textContent = ok ? e.dataset.kwNote.replace("{kw}", window.UX13.kw(mp.peak_w) + " kW") : "";
|
||||
});
|
||||
}
|
||||
document.addEventListener("ux13:status", fillNotes);
|
||||
function simpleField(f) {
|
||||
const row = fieldRow(f, null, "simple");
|
||||
row.querySelector(".head").append(chip(f.applies));
|
||||
const n = f.note && W.simple.note[f.note];
|
||||
if (n && n.description.includes("{kw}")) {
|
||||
row.append(h("div", { class: "small muted", "data-kw-note": n.description, hidden: true }));
|
||||
} else if (n) {
|
||||
// A plain note replaces the option's installer help on this view.
|
||||
const help = row.querySelector(".help");
|
||||
if (help) help.textContent = n.description;
|
||||
}
|
||||
return h("div", { class: "card" }, row);
|
||||
}
|
||||
function renderSimple() {
|
||||
const host = $("#simple");
|
||||
host.replaceChildren();
|
||||
if (mode !== "simple") return;
|
||||
const cards = allFields().filter((f) => f.simple).map(simpleField);
|
||||
const batts = h("div", {});
|
||||
cards.splice(Math.min(1, cards.length), 0, h("div", { class: "card" },
|
||||
h("div", { class: "row spread" }, h("div", { class: "lbl" }, W.simple.batteries.name), chip("now")), batts));
|
||||
host.append(...cards, h("p", { class: "small muted", style: "margin:4px 2px" }, W.simple.more.description + " ",
|
||||
h("button", { type: "button", class: "btn link", onclick: () => setMode("installer", true) }, W.simple.installer)));
|
||||
window.UX13.batteries(batts);
|
||||
fillNotes();
|
||||
}
|
||||
function setMode(v, redraw) {
|
||||
mode = v;
|
||||
try { localStorage.setItem("ux13.settings.view", v); } catch (e) { /* no storage */ }
|
||||
document.querySelectorAll("#set-view [role=radio]").forEach((b) => {
|
||||
const on = b.dataset.v === v;
|
||||
b.setAttribute("aria-checked", String(on)); b.tabIndex = on ? 0 : -1;
|
||||
});
|
||||
$("#set-desc").textContent = v === "simple" ? W.simple.description : W.description;
|
||||
$("#simple").hidden = v !== "simple";
|
||||
["#set-search", "#groups", "#set-nomatch"].forEach((id) => { $(id).classList.toggle("vhide", v === "simple"); });
|
||||
if (redraw && M && view === "settings") render();
|
||||
}
|
||||
document.querySelectorAll("#set-view [role=radio]").forEach((b, i, all) => {
|
||||
b.addEventListener("click", () => setMode(b.dataset.v, true));
|
||||
b.addEventListener("keydown", (e) => {
|
||||
const d = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 }[e.key];
|
||||
if (!d) return;
|
||||
e.preventDefault();
|
||||
const j = all[(i + d + all.length) % all.length];
|
||||
setMode(j.dataset.v, true); j.focus();
|
||||
});
|
||||
});
|
||||
setMode(mode, false);
|
||||
window.UX13.settingsView = (v) => setMode(v, true);
|
||||
|
||||
/* ---------------- messages, visibility, search, badges ---------------- */
|
||||
const ICON = { bad: "×", warn: "!", info: "i" };
|
||||
const RANK = { bad: 0, warn: 1, info: 2 };
|
||||
@@ -434,11 +507,13 @@
|
||||
return m.card == null ? r.card === 0 : m.card === r.card;
|
||||
}
|
||||
function paint() {
|
||||
const q = view === "setup" ? "" : $("#set-search").value.trim().toLowerCase();
|
||||
const q = view === "setup" || mode === "simple" ? "" : $("#set-search").value.trim().toLowerCase();
|
||||
// A message counts as shown only beside a row of the view on screen.
|
||||
const onScreen = (r) => view === "setup" || (mode === "simple") === (r.gid === "simple");
|
||||
const placed = new Set();
|
||||
for (const r of ROWS) {
|
||||
r.visible = visible(r.f);
|
||||
const msgs = MSG.filter((m, i) => { const hit = mine(r, m); if (hit && r.visible) placed.add(i); return hit; })
|
||||
const msgs = MSG.filter((m, i) => { const hit = mine(r, m); if (hit && r.visible && onScreen(r)) placed.add(i); return hit; })
|
||||
.sort((a, z) => RANK[a.tier] - RANK[z.tier]);
|
||||
r.tiers = new Set(msgs.map((m) => m.tier));
|
||||
r.msg.replaceChildren(...msgs.map((m) => h("div", { class: "msg " + m.tier },
|
||||
@@ -562,7 +637,9 @@
|
||||
validate();
|
||||
}
|
||||
function firstBad() {
|
||||
const r = ROWS.find((x) => x.tiers && x.tiers.has("bad") && x.visible);
|
||||
if (view === "settings" && mode === "simple" && !ROWS.some((x) => x.gid === "simple" && x.tiers && x.tiers.has("bad") && x.visible)
|
||||
&& ROWS.some((x) => x.tiers && x.tiers.has("bad") && x.visible)) setMode("installer", true);
|
||||
const r = ROWS.find((x) => x.tiers && x.tiers.has("bad") && x.visible && (view !== "settings" || (mode === "simple") === (x.gid === "simple")));
|
||||
if (!r) { if (!$("#set-other").hidden) $("#set-other").scrollIntoView({ block: "center" }); return; }
|
||||
for (let p = r.el.parentElement; p; p = p.parentElement) if (p.tagName === "DETAILS") p.open = true;
|
||||
r.el.scrollIntoView({ block: "center" });
|
||||
|
||||
@@ -84,13 +84,22 @@
|
||||
"offline": {"name": "Cannot reach the add-on", "description": "The page tries again every 3 seconds."},
|
||||
"setup_only": {"name": "Setup: nothing is controlled yet", "description": "Nothing is sent to any inverter until setup is saved and the add-on restarts.", "control": "Control starts after setup.", "open": "Open Setup"},
|
||||
"batteries": {"name": "Batteries", "description": "To choose a mode for each battery, turn on battery modes in the settings.", "level": "Level", "now": "Now", "stored": "Stored", "mode": "Mode", "settings": "Change inverter settings",
|
||||
"with_modes": {"description": "Each battery follows its mode, after the peak limit and safety stops."},
|
||||
"use": "Use this battery", "on": "On", "off": "Off",
|
||||
"on": "On", "off": "Off",
|
||||
"choice": {
|
||||
"solar_first": {"name": "Solar first", "description": "Charges from solar before the other batteries and covers the house."},
|
||||
"leftover_solar": {"name": "Leftover solar", "description": "Charges from solar the other batteries cannot take, and covers the house."},
|
||||
"off": {"name": "Off", "description": "The add-on asks this battery for 0 W, unless the peak limit needs it."}
|
||||
}},
|
||||
"off": {"name": "Off", "description": "Rests, and helps only when the peak limit needs it."}
|
||||
},
|
||||
"later": "Cheap hours only, coming later",
|
||||
"today": "Today", "why": "Why",
|
||||
"sell": {"name": "Sell in expensive hours", "description": "Selling comes in a later update."},
|
||||
"modes_off": {"name": "Battery modes are off"},
|
||||
"how": {"name": "How the modes work", "description": "Which battery gets solar first, and when each gives power.",
|
||||
"solar_first": {"description": "Solar first gets spare solar before the other batteries, and covers the house at any hour."},
|
||||
"leftover_solar": {"description": "Leftover solar gets the solar the Solar first batteries cannot take, and also covers the house."},
|
||||
"off": {"description": "Off rests the battery and starts no monthly cycle, but the peak limit may still use it."},
|
||||
"rules": {"description": "The peak limit and the safety stops always come first, and charging from the grid and selling come later."}},
|
||||
"toast": {"on": "{name} is on.", "off": "{name} is off.", "mode": "{name}: {mode}."}},
|
||||
"settings": {"name": "Settings", "description": "The settings are grouped by task below.", "open": "Open the settings page"}
|
||||
},
|
||||
"choices_later": {
|
||||
@@ -107,7 +116,13 @@
|
||||
},
|
||||
"settings": {
|
||||
"name": "Settings",
|
||||
"description": "Every setting, grouped by task, and the simple view follows in a later update.",
|
||||
"description": "Every setting, grouped by task.",
|
||||
"view": {"name": "Settings view", "simple": "Simple", "installer": "Installer"},
|
||||
"simple": {"description": "The choices a home owner makes.",
|
||||
"note": {"month_peak": {"description": "This month's peak so far: {kw}."}, "shadow": {"description": "Use Always on while you test a new installation."}},
|
||||
"batteries": {"name": "Batteries"},
|
||||
"more": {"description": "Looking for the meter, inverters or prices?"},
|
||||
"installer": "Open the installer view"},
|
||||
"classic": "Classic settings",
|
||||
"search": {"name": "Find a setting", "description": "Find a setting, for example peak or meter."},
|
||||
"no_match": {"description": "No setting matches your search."},
|
||||
@@ -223,7 +238,6 @@
|
||||
"again": "Check again",
|
||||
"blocked": {"description": "Fix the items marked in red, then check again."}
|
||||
},
|
||||
"shadow": {"name": "Start in shadow mode", "description": "Recommended, because every inverter then stays at 0 W until you turn shadow mode off under Settings."},
|
||||
"dialog": {"name": "Save and restart?", "description": "Every inverter is set to 0 W while the add-on restarts, and shadow mode keeps them at 0 W after that.", "yes": "Save and restart"},
|
||||
"dialog_live": {"name": "Save and restart?", "description": "Every inverter is set to 0 W while the add-on restarts, and then the new settings apply.", "yes": "Save and restart"},
|
||||
"dialog_setup": {"name": "Save and restart?", "description": "The add-on restarts with these settings, and shadow mode keeps every inverter at 0 W.", "yes": "Save and restart"},
|
||||
@@ -290,7 +304,37 @@
|
||||
"cycle": "Monthly cycle under way.",
|
||||
"giving": "Giving {kw} now.",
|
||||
"charging": "Charging {kw} now.",
|
||||
"resting": "Resting now."
|
||||
"resting": "Resting now.",
|
||||
"off_resting": "Off. Resting now.",
|
||||
"off_peak": "Off, but gives {kw} for the peak limit.",
|
||||
"off_going": "Off. Still at {kw}, going to 0 W."
|
||||
},
|
||||
"why": {
|
||||
"no_reading": "No reading, so the add-on cannot tell.",
|
||||
"held": "An alarm stopped control. See Needs attention.",
|
||||
"stopped": "Control is stopped.",
|
||||
"shadow": "Shadow mode: the add-on only shows what it would do.",
|
||||
"cycle": "The monthly cycle keeps its level reading right.",
|
||||
"cycle_mode": "The monthly cycle comes first. Its mode resumes after.",
|
||||
"off": "You switched it off.",
|
||||
"off_peak": "The peak limit comes before Off. It rests again after.",
|
||||
"refuses_give": "The battery refuses to give power. The add-on tries again every 15 minutes.",
|
||||
"refuses_charge": "The battery refuses to charge. The add-on tries again every 15 minutes.",
|
||||
"peak": "The peak limit needs power, so every battery gives.",
|
||||
"modes_off": "Battery modes are off, so it follows the house.",
|
||||
"solar_first": "Solar first: it gets spare solar before the others.",
|
||||
"leftover": "It gets the solar that Solar first batteries cannot take.",
|
||||
"leftover_alone": "No Solar first battery is on, so it gets solar first.",
|
||||
"covers_house": "It gives power to cover the house.",
|
||||
"other_refuses": "{name} refuses to give power, so this battery takes over."
|
||||
},
|
||||
"today": {
|
||||
"not_planned": "Not planned yet.",
|
||||
"off": "Off, so no monthly cycle today.",
|
||||
"house": "Follows the house.",
|
||||
"house_would": "Would follow the house.",
|
||||
"stopped": "Nothing planned while control is stopped.",
|
||||
"held": "Nothing planned until the alarm clears."
|
||||
},
|
||||
"head": {
|
||||
"held": "Every inverter is at 0 W until the alarm clears.",
|
||||
|
||||
@@ -81,13 +81,22 @@
|
||||
"offline": {"name": "Add-on niet bereikbaar", "description": "De pagina probeert het elke 3 seconden opnieuw."},
|
||||
"setup_only": {"name": "Installatie: nog niets wordt geregeld", "description": "Er gaat niets naar een omvormer tot de installatie bewaard is en de add-on herstart.", "control": "De regeling start na de installatie.", "open": "Installatie openen"},
|
||||
"batteries": {"name": "Batterijen", "description": "Zet batterijmodi aan in de instellingen om per batterij een modus te kiezen.", "level": "Laadniveau", "now": "Nu", "stored": "Opgeslagen", "mode": "Modus", "settings": "Instellingen van omvormers wijzigen",
|
||||
"with_modes": {"description": "Elke batterij volgt haar modus, na de piekgrens en de veiligheidsstops."},
|
||||
"use": "Deze batterij gebruiken", "on": "Aan", "off": "Uit",
|
||||
"on": "Aan", "off": "Uit",
|
||||
"choice": {
|
||||
"solar_first": {"name": "Eerst zonne-energie", "description": "Laadt met zonne-energie vóór de andere batterijen en dekt het huis."},
|
||||
"leftover_solar": {"name": "Overschot zonne-energie", "description": "Laadt met zonne-energie die de andere batterijen niet opnemen, en dekt het huis."},
|
||||
"off": {"name": "Uit", "description": "De add-on vraagt deze batterij 0 W, behalve als de piekgrens haar nodig heeft."}
|
||||
}},
|
||||
"off": {"name": "Uit", "description": "Rust, en helpt alleen als de piekgrens haar nodig heeft."}
|
||||
},
|
||||
"later": "Alleen goedkope uren, volgt later",
|
||||
"today": "Vandaag", "why": "Waarom",
|
||||
"sell": {"name": "Verkopen in dure uren", "description": "Verkopen komt in een latere update."},
|
||||
"modes_off": {"name": "Batterijmodi staan uit"},
|
||||
"how": {"name": "Zo werken de modi", "description": "Welke batterij eerst zonne-energie krijgt, en wanneer elke batterij levert.",
|
||||
"solar_first": {"description": "Eerst zonne-energie krijgt het overschot vóór de andere batterijen, en dekt het huis op elk uur."},
|
||||
"leftover_solar": {"description": "Overschot zonne-energie krijgt wat de batterijen met Eerst zonne-energie niet opnemen, en dekt ook het huis."},
|
||||
"off": {"description": "Uit laat de batterij rusten en start geen maandelijkse cyclus, maar de piekgrens mag haar nog gebruiken."},
|
||||
"rules": {"description": "De piekgrens en de veiligheidsstops gaan altijd voor, en laden van het net en verkopen volgen later."}},
|
||||
"toast": {"on": "{name} staat aan.", "off": "{name} staat uit.", "mode": "{name}: {mode}."}},
|
||||
"settings": {"name": "Instellingen", "description": "De instellingen staan hieronder per taak.", "open": "Instellingen openen"}
|
||||
},
|
||||
"choices_later": {"region": {"wallonia": "Wallonië, nog geen standaardwaarde", "brussels": "Brussel, nog geen standaardwaarde"}},
|
||||
@@ -102,7 +111,13 @@
|
||||
},
|
||||
"settings": {
|
||||
"name": "Instellingen",
|
||||
"description": "Elke instelling, per taak, en de eenvoudige weergave volgt in een latere update.",
|
||||
"description": "Elke instelling, per taak.",
|
||||
"view": {"name": "Weergave van de instellingen", "simple": "Eenvoudig", "installer": "Installateur"},
|
||||
"simple": {"description": "De keuzes die een eigenaar maakt.",
|
||||
"note": {"month_peak": {"description": "De piek van deze maand tot nu: {kw}."}, "shadow": {"description": "Kies Altijd aan terwijl u een nieuwe installatie test."}},
|
||||
"batteries": {"name": "Batterijen"},
|
||||
"more": {"description": "Zoekt u de meter, de omvormers of de prijzen?"},
|
||||
"installer": "De installateursweergave openen"},
|
||||
"classic": "Klassieke instellingen",
|
||||
"search": {"name": "Instelling zoeken", "description": "Zoek een instelling, bijvoorbeeld piek of meter."},
|
||||
"no_match": {"description": "Geen instelling past bij uw zoekopdracht."},
|
||||
@@ -197,7 +212,6 @@
|
||||
"needed": {"description": "Doe eerst de waterkokertest, of sla hem over."}
|
||||
},
|
||||
"check": {"name": "Wat de add-on nakeek", "running": {"description": "Bezig met de startcontroles van de add-on."}, "pass": {"description": "De startcontroles van de add-on zijn in orde."}, "failed": {"description": "De controle liep niet, dus probeer opnieuw."}, "again": "Opnieuw nakijken", "blocked": {"description": "Los de rood gemarkeerde punten op en kijk dan opnieuw na."}},
|
||||
"shadow": {"name": "Starten in schaduwmodus", "description": "Aanbevolen, want dan blijft elke omvormer op 0 W tot u de schaduwmodus uitzet onder Instellingen."},
|
||||
"dialog": {"name": "Bewaren en herstarten?", "description": "Elke omvormer gaat naar 0 W terwijl de add-on herstart, en de schaduwmodus houdt ze daarna op 0 W.", "yes": "Bewaren en herstarten"},
|
||||
"dialog_live": {"name": "Bewaren en herstarten?", "description": "Elke omvormer gaat naar 0 W terwijl de add-on herstart, daarna gelden de nieuwe instellingen.", "yes": "Bewaren en herstarten"},
|
||||
"dialog_setup": {"name": "Bewaren en herstarten?", "description": "De add-on herstart met deze instellingen, en de schaduwmodus houdt elke omvormer op 0 W.", "yes": "Bewaren en herstarten"},
|
||||
@@ -232,7 +246,37 @@
|
||||
"cycle": "Maandelijkse cyclus bezig.",
|
||||
"giving": "Levert nu {kw}.",
|
||||
"charging": "Laadt nu {kw}.",
|
||||
"resting": "Rust nu."
|
||||
"resting": "Rust nu.",
|
||||
"off_resting": "Uit. Rust nu.",
|
||||
"off_peak": "Uit, maar levert {kw} voor de piekgrens.",
|
||||
"off_going": "Uit. Nog {kw}, gaat naar 0 W."
|
||||
},
|
||||
"why": {
|
||||
"no_reading": "Geen meting, dus de add-on weet het niet.",
|
||||
"held": "Een alarm stopte de regeling. Zie Vraagt aandacht.",
|
||||
"stopped": "De regeling is gestopt.",
|
||||
"shadow": "Schaduwmodus: de add-on toont alleen wat hij zou doen.",
|
||||
"cycle": "De maandelijkse cyclus houdt de meting van het laadniveau juist.",
|
||||
"cycle_mode": "De maandelijkse cyclus gaat voor. Daarna volgt ze weer haar modus.",
|
||||
"off": "U hebt haar uitgezet.",
|
||||
"off_peak": "De piekgrens gaat voor Uit. Daarna rust ze weer.",
|
||||
"refuses_give": "De batterij weigert te leveren. De add-on probeert het elke 15 minuten opnieuw.",
|
||||
"refuses_charge": "De batterij weigert te laden. De add-on probeert het elke 15 minuten opnieuw.",
|
||||
"peak": "De piekgrens vraagt vermogen, dus elke batterij levert.",
|
||||
"modes_off": "Batterijmodi staan uit, dus ze volgt het huis.",
|
||||
"solar_first": "Eerst zonne-energie: ze krijgt het overschot vóór de andere.",
|
||||
"leftover": "Ze krijgt de zonne-energie die de batterijen met Eerst zonne-energie niet opnemen.",
|
||||
"leftover_alone": "Geen batterij met Eerst zonne-energie staat aan, dus zij krijgt eerst zonne-energie.",
|
||||
"covers_house": "Ze levert stroom om het huis te dekken.",
|
||||
"other_refuses": "{name} weigert te leveren, dus deze batterij neemt het over."
|
||||
},
|
||||
"today": {
|
||||
"not_planned": "Nog niet gepland.",
|
||||
"off": "Uit, dus vandaag geen maandelijkse cyclus.",
|
||||
"house": "Volgt het huis.",
|
||||
"house_would": "Zou het huis volgen.",
|
||||
"stopped": "Niets gepland zolang de regeling gestopt is.",
|
||||
"held": "Niets gepland tot het alarm weg is."
|
||||
},
|
||||
"head": {
|
||||
"held": "Elke omvormer staat op 0 W tot het alarm weg is.",
|
||||
|
||||
@@ -46,6 +46,7 @@ class Field:
|
||||
wizard: int = 0 # setup wizard step 1-6, 0 = not in the wizard
|
||||
mirror: str = "" # a choice also copied to this field, if offered
|
||||
fills: str = "" # a preset choice fills this field (uimodel)
|
||||
note: str = "" # P3: a line under it on Settings, Simple
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
@@ -180,12 +181,13 @@ GROUPS = (
|
||||
Group("safety", (
|
||||
Part("peak", (
|
||||
Field("peak_cap_w", SLIDER, "kW", scale=1000, step=100,
|
||||
simple=True, wizard=5),
|
||||
simple=True, wizard=5, note="month_peak"),
|
||||
Field("peak_forecast_entity", ENTITY, kind="power_w", wizard=5),
|
||||
Field("peak_soc_floor", LOCKED, "%"),
|
||||
)),
|
||||
Part("start", (
|
||||
Field("shadow_mode", SEGMENTED, simple=True, wizard=6),
|
||||
Field("shadow_mode", SEGMENTED, simple=True, wizard=6,
|
||||
note="shadow"),
|
||||
Field("auto_start", TOGGLE),
|
||||
Field("estop_fitted", TOGGLE),
|
||||
Field("stale_input_s", SLIDER, "s"),
|
||||
|
||||
@@ -99,7 +99,7 @@ def _field(fld, schema_ui, translations, lang, T):
|
||||
"help": text, "unit": fld.unit, "scale": fld.scale,
|
||||
"step": fld.step, "kind": fld.kind,
|
||||
"show": [list(c) for c in fld.show], "applies": fld.applies,
|
||||
"wizard": fld.wizard,
|
||||
"wizard": fld.wizard, "simple": fld.simple, "note": fld.note,
|
||||
"type": value_type, "optional": not node.get("required", False),
|
||||
# A choice mirrored into a field also shows (and finds) its keys.
|
||||
"writes": sorted(k for k, how in L.DERIVED.items()
|
||||
|
||||
Reference in New Issue
Block a user