OPT-23, OPT-28: the day plan counts the sun right, and the night charge covers the expensive-hour shortfall
OPT-23: a state-only forecast is read from now for every span (Forecast.between since=), so the plan's per-hour sun adds up to the day's figure, today and tomorrow. Hourly splits are unchanged. The Forecast tab reads spans the same way. OPT-28: a Leftover solar battery buys, from the floor, the sum over the expensive hours of max(0, house - trust x sun), / EFF. Several Leftover solar batteries hold it together by capacity, the ceiling passes the rest on, and what none can hold is said (Plan.uncovered_kwh). Home's grid block, the Batteries line and the mode help (EN + NL) say the level, what it covers, and that Leftover solar buys only what the sun will not give. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
This commit is contained in:
co-authored by
Claude
parent
4a1f6cc2d9
commit
995028bc5d
@@ -20,6 +20,7 @@ import contextlib
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from . import arbitrage, messages, peakguard, uitext
|
||||
from .dayplan import EFF
|
||||
from .maintenance import CHARGE as M_CHARGE, DRAIN as M_DRAIN, HOLD as M_HOLD
|
||||
from .maintenance import IDLE as M_IDLE, next_slot_start
|
||||
from .packmode import CHEAP_HOURS, GRID_CHARGE_MODES, LEFTOVER_SOLAR, SOLAR_FIRST
|
||||
@@ -471,9 +472,15 @@ def mode_runs(c, day0, day1, now, shadow, ahead=False, rp=None):
|
||||
continue
|
||||
price = run.mean_eur_kwh
|
||||
if kind == "grid":
|
||||
cov = plan_cover(c, inv.name)
|
||||
what, why, amount = (run_grid_what(kwh, shadow),
|
||||
run_cheap_why(price),
|
||||
run_grid_amount(kwh * price))
|
||||
if cov is not None:
|
||||
# OPT-28: with a day plan the block says how far it
|
||||
# charges and what that covers.
|
||||
what = run_grid_what_to(kwh, cov[0], shadow)
|
||||
why += " " + run_covers(cov[1])
|
||||
elif kind == "sell":
|
||||
# No money line: the price is what the house pays, and a sale
|
||||
# earns the lower injection price (OPT-11 P3 review).
|
||||
@@ -506,6 +513,24 @@ def plan_need(c, name, soc=None):
|
||||
inv.capacity_kwh)
|
||||
|
||||
|
||||
def plan_cover(c, name):
|
||||
"""OPT-28: `(level %, kWh)` for a battery the day plan charges from the
|
||||
grid: the level it charges to, and the expensive-hour use of the house
|
||||
that charge covers and the sun will not. Read from the level control
|
||||
holds (`plan_targets()`), so Home says what control does. None when the
|
||||
plan buys nothing for it."""
|
||||
dp = getattr(c, "day_plan", None)
|
||||
targets = c.plan_targets() if hasattr(c, "plan_targets") else None
|
||||
inv = next((i for i in c.inverters if i.name == name), None)
|
||||
if dp is None or not targets or name not in targets or inv is None \
|
||||
or not inv.capacity_kwh or targets[name] <= 0.0:
|
||||
return None
|
||||
floor, _ceil = arbitrage.soc_bounds(c.o)
|
||||
cap = float(inv.capacity_kwh)
|
||||
top = targets[name] + dp.share_kwh.get(name, 0.0) / cap * 100.0
|
||||
return targets[name], max(0.0, top - floor) / 100.0 * cap * EFF
|
||||
|
||||
|
||||
def plan_state(c):
|
||||
"""P4: `"plan"` (a day plan is made), `"learning"` (no house profile
|
||||
yet), `"no_forecast"` (set but missing or stale), or None (not set, or
|
||||
@@ -540,6 +565,9 @@ def solar_block(c, now):
|
||||
elif fc is not None:
|
||||
out["line"] = line_solar(fc.today.kwh, out["count_kwh"],
|
||||
out["updated"])
|
||||
dp = getattr(c, "day_plan", None)
|
||||
if st == "plan" and dp is not None and dp.uncovered_kwh >= MIN_KWH:
|
||||
out["line"] += " " + line_uncovered(dp.uncovered_kwh)
|
||||
return out
|
||||
|
||||
|
||||
@@ -863,7 +891,8 @@ def battery_rows(c, shadow, running, held=False):
|
||||
# knows this cycle. Price and forecast reasons are OPT-11 P3/P4.
|
||||
"why_key": why,
|
||||
"why": why_words(why, name=cycling if why == "other_cycle"
|
||||
else other, at=at),
|
||||
else other, at=at,
|
||||
**plan_words(c, inv.name, why)),
|
||||
})
|
||||
if modes_on:
|
||||
# OPT-11 P1: the battery's switch and mode, for the Batteries
|
||||
@@ -1160,6 +1189,10 @@ def _kwh(v) -> str:
|
||||
return _dec(f"{float(v):.1f}") + " kWh"
|
||||
|
||||
|
||||
def _pct(v) -> str:
|
||||
return _dec(f"{float(v):.0f}") + " %"
|
||||
|
||||
|
||||
# Row id -> the title on Home. The row's own sentence is the body, in the
|
||||
# page's language (UX-46, `messages.pick`). Five titles are the notification's
|
||||
# own title, which test_ui_text.py checks.
|
||||
@@ -1292,11 +1325,18 @@ def battery_now(soc, batt_w, *, phase, shadow, running, held=False,
|
||||
return _t("now.resting")
|
||||
|
||||
|
||||
def why_words(key, name=None, at="") -> str:
|
||||
def why_words(key, name=None, at="", pct="", kwh="") -> str:
|
||||
"""One battery's "why" line (UX-13 P3), from `battery_why()`'s key.
|
||||
`name`: the battery that refuses, for `other_refuses`; `at`: the
|
||||
give run's time, for `saves`."""
|
||||
return _t("why." + key, name=name or "", at=at)
|
||||
give run's time, for `saves`; `pct` and `kwh`: the planned level and
|
||||
what it covers, for `tops_up` (OPT-28)."""
|
||||
return _t("why." + key, name=name or "", at=at, pct=pct, kwh=kwh)
|
||||
|
||||
|
||||
def plan_words(c, name, why) -> dict:
|
||||
"""OPT-28: the figures of a `tops_up` line, or nothing."""
|
||||
cov = plan_cover(c, name) if why == "tops_up" else None
|
||||
return {} if cov is None else {"pct": _pct(cov[0]), "kwh": _kwh(cov[1])}
|
||||
|
||||
|
||||
def today_words(key) -> str:
|
||||
@@ -1318,6 +1358,16 @@ def run_grid_what(kwh, shadow) -> str:
|
||||
return _w(shadow, "run.grid_what", kwh=_kwh(kwh))
|
||||
|
||||
|
||||
def run_grid_what_to(kwh, pct, shadow) -> str:
|
||||
"""OPT-28: a grid block of the day plan, with the level it charges to."""
|
||||
return _w(shadow, "run.grid_what_to", kwh=_kwh(kwh), pct=_pct(pct))
|
||||
|
||||
|
||||
def run_covers(kwh) -> str:
|
||||
"""OPT-28: what a planned grid charge covers."""
|
||||
return _t("run.covers", kwh=_kwh(kwh))
|
||||
|
||||
|
||||
# The trade legs and the price line use the feed's own points (a 15-minute
|
||||
# period on a quarter-hourly feed), so both name the same price and time.
|
||||
def run_grid_why(price) -> str:
|
||||
@@ -1395,6 +1445,11 @@ def line_solar(fc_kwh, count_kwh, at) -> str:
|
||||
return _t("solar.line", kwh=_kwh(fc_kwh), count=_kwh(count_kwh), at=at)
|
||||
|
||||
|
||||
def line_uncovered(kwh) -> str:
|
||||
"""OPT-28: the batteries cannot hold the expensive hours' shortfall."""
|
||||
return _t("solar.uncovered", kwh=_kwh(kwh))
|
||||
|
||||
|
||||
def line_no_trade(key) -> str:
|
||||
return "" if key == "off" else _t("no_trade." + key)
|
||||
|
||||
@@ -1516,6 +1571,7 @@ def _samples() -> list:
|
||||
for k in VERDICT:
|
||||
out.append(("plan", plan_head(k, sh)))
|
||||
out += [("plan", run_grid_what(12.34, sh)),
|
||||
("plan", run_grid_what_to(12.34, 82, sh)),
|
||||
("plan", run_give_what(9.6, sh)),
|
||||
("plan", run_cycle_what(M_IDLE, sh)),
|
||||
("plan", run_cycle_what(M_DRAIN, sh)),
|
||||
@@ -1530,6 +1586,7 @@ def _samples() -> list:
|
||||
("plan", control_text(True, sh)),
|
||||
("plan", control_text(False, sh))]
|
||||
out += [("plan", run_grid_why(0.1412)), ("plan", run_grid_amount(1.23)),
|
||||
("plan", run_covers(9.4)), ("plan", line_uncovered(2.5)),
|
||||
("plan", run_give_why(0.3612)), ("plan", run_give_amount(3.47)),
|
||||
("plan", run_cycle_why()), ("plan", run_cycle_why(True)),
|
||||
("plan", run_cycle_amount(None)),
|
||||
@@ -1560,7 +1617,8 @@ def _samples() -> list:
|
||||
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", why_words(k, name="Battery A", pct="82 %",
|
||||
kwh="9.4 kWh")) for k in words["why"]]
|
||||
out += [("plan", today_words(k)) for k in words["today"]]
|
||||
out += [("plan", line_solar(9.2, 6.4, "10:15")),
|
||||
("plan", _t("solar.no_forecast")), ("plan", _t("solar.learning"))]
|
||||
|
||||
Reference in New Issue
Block a user