OPT-11 P1: per-battery mode limits, on/off, solar order
checks / unit (pull_request) Successful in 10m59s
infra-pin / fresh (pull_request) Successful in 1s

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:
Claude dev agent
2026-09-28 18:51:29 +02:00
parent a5dddadf45
commit d69fd8fbfd
25 changed files with 1873 additions and 64 deletions
+18 -3
View File
@@ -9,8 +9,9 @@ note, section 3.3.
The plan shows only what the add-on does today: trading on price (the
arbitrage verdict, the same call the control cycle makes), the monthly battery
cycle, the peak limit and "follows the house". Battery modes (OPT-11) are not
built, so nothing here invents a mode plan. Every sentence is in the words
cycle, the peak limit and "follows the house". OPT-11 P1 adds each battery's
switch and mode to its row; the mode plan arrives with P3/P4, so nothing here
invents one. Every sentence is in the words
section at the bottom and is held to STYLE.md by test_messages.py.
"""
@@ -530,9 +531,17 @@ def battery_rows(c, shadow, running, held=False):
running=running, held=held,
would_w=shares.get(inv.name),
settled=at_zero(inv)),
# Per-battery reasons arrive with battery modes (OPT-11).
# Per-battery reasons arrive with the plan (OPT-11 P3/P4).
"why": None,
})
if getattr(c, "modes_on", False):
# 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)
rows[-1].update({"use": pm.use, "mode_key": pm.key,
"mode_choice": pm.mode,
"mode": mode_word(pm.key)})
return rows
@@ -723,6 +732,11 @@ def mode_house() -> str:
return _t("mode_house")
def mode_word(key) -> str:
"""OPT-11: a battery's mode, or Off, in the page's language."""
return _t("modes." + key)
def attn_shadow() -> str:
return _t("attn_shadow")
@@ -964,6 +978,7 @@ def _samples() -> list:
out = [("title", t) for t in words["verdict"].values()]
out += [("title", _t("zero_going")), ("title", _t("zero_held"))]
out += [("title", t) for t in words["attn"].values()]
out += [("title", mode_word(k)) for k in words["modes"]]
out += [("title", mode_house()), ("title", attn_shadow()),
("line", attn_shadow_body())]
for k in ("running", "shadow", "stopped", "alarm", "warning"):