OPT-11 P1: per-battery mode limits, on/off, solar order
A battery's mode is a soft unit limit (source `mode`, P_MODE 35) kept
apart from the hard limits in pre_resolve(). The allocator applies it
only while the site target is decided at or below 35 (new
Resolution.deciding_priority), so the peak floor releases it and a mode
`hi` never meets the site `lo`. At N=1 resolve() applies the same rule
to the site target after stage 2. A mode range without 0 W is dropped
as a software fault, so REFUSE_SIGN stays unreachable from a mode.
compute() gets the loop bound (sum over live units of hard limits with
modes applied), so the loop never asks for watts no battery may give
and the saturation freeze never parks on them. While charging, Leftover
solar weighs 0 so Solar first fills first. Store keys pack_use and
pack_mode per battery, set by two new /action values; Off stops and
skips maintenance with a log line. Minimal Batteries cards (EN/NL).
Off by default (battery_modes_enabled); off, commands are byte-identical
to a5dddad at N=1 and N=2 (test_modes.py part 1).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -308,9 +308,41 @@
|
||||
if (open) $("#c-details").open = true;
|
||||
}
|
||||
|
||||
/* ---------------- Batteries (placeholder until P3) ---------------- */
|
||||
/* ---------------- 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.
|
||||
const MODES = ["solar_first", "leftover_solar"];
|
||||
let packBusy = false;
|
||||
async function packAct(what, unit, 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 }) });
|
||||
const j = await r.json().catch(() => ({}));
|
||||
toast(r.ok ? t("toast.done") : (j.error || t("toast.failed")));
|
||||
} catch (e) { toast(t("toast.failed")); }
|
||||
finally { packBusy = false; }
|
||||
refresh();
|
||||
}
|
||||
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 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" : "")),
|
||||
levelBar(b.level_pct, b.name),
|
||||
@@ -318,7 +350,8 @@
|
||||
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.now")), h("dd", {}, b.now),
|
||||
h("dt", {}, t("batteries.mode")), h("dd", {}, b.mode)))));
|
||||
b.mode_key == null ? [h("dt", {}, t("batteries.mode")), h("dd", {}, b.mode)] : null),
|
||||
b.mode_key != null ? modeControls(b) : null)));
|
||||
}
|
||||
|
||||
/* ---------------- control buttons ---------------- */
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
<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" data-t="batteries.description"></p>
|
||||
<p class="small muted" style="margin:0 0 10px" id="batt-desc" data-t="batteries.description"></p>
|
||||
<a class="btn" href="#settings" data-t="batteries.settings"></a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user