UX-45: count priced hours in the wizard, fill chart holes by the add-on's cover rule, mark only for price-mode batteries
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
17c6048417
commit
eb8375157d
@@ -22,7 +22,7 @@ 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, next_slot_start
|
||||
from .packmode import CHEAP_HOURS, GRID_CHARGE_MODES, LEFTOVER_SOLAR, SOLAR_FIRST
|
||||
from .price import Absence, Uncertainty, brussels
|
||||
from .price import Absence, Uncertainty, brussels, _cover_table
|
||||
from .static_tariff import static_plan
|
||||
from .tariff import BILLING_TZ
|
||||
from .uimodel import REGION_PRESETS, options_to_ui
|
||||
@@ -157,23 +157,12 @@ def _bar_step(pts):
|
||||
return min(gaps + [timedelta(hours=1)])
|
||||
|
||||
|
||||
def _bar_cover(pts, step):
|
||||
def _bar_cover(pts):
|
||||
"""`[(start, end, point)]`: what each point is the price of on the chart.
|
||||
Its own gap to the next point when that is one step or one hour, else one
|
||||
step, so a missing quarter stays empty and does not stretch its
|
||||
neighbour. A carried (provisional) last point is hourly."""
|
||||
hour = timedelta(hours=1)
|
||||
out = []
|
||||
for i, p in enumerate(pts):
|
||||
gap = pts[i + 1].start - p.start if i + 1 < len(pts) else None
|
||||
if gap in (step, hour):
|
||||
end = p.start + gap
|
||||
elif gap is None and p.provisional:
|
||||
end = p.start + hour
|
||||
else:
|
||||
end = p.start + step
|
||||
out.append((p.start, end, p))
|
||||
return out
|
||||
The add-on's own rule, not a copy: `price._cover_table()` (TARIFF-14), so
|
||||
a point covers until the next one starts and never over an hour, and the
|
||||
last one for the span of the one before it."""
|
||||
return _cover_table(pts)
|
||||
|
||||
|
||||
def price_bars(points, day0, day1, windows=(), now=None, kept=None):
|
||||
@@ -187,7 +176,7 @@ def price_bars(points, day0, day1, windows=(), now=None, kept=None):
|
||||
that lives a day). Display only."""
|
||||
pts = sorted(points, key=lambda p: p.start)
|
||||
step = _bar_step(pts)
|
||||
cover = _bar_cover(pts, step)
|
||||
cover = _bar_cover(pts)
|
||||
n = int((day1 - day0) / step)
|
||||
bars = []
|
||||
for k in range(n):
|
||||
@@ -212,6 +201,17 @@ def price_bars(points, day0, day1, windows=(), now=None, kept=None):
|
||||
return bars
|
||||
|
||||
|
||||
def _price_mode_in_use(c):
|
||||
"""True when at least one battery is on and uses a price mode. The runs
|
||||
belong to those batteries; with all on Solar first or off, nothing acts
|
||||
on a price."""
|
||||
for inv in c.inverters:
|
||||
pm = c.pack_mode(inv)
|
||||
if pm.use and pm.mode in PRICE_MODES:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def price_windows(c, plan, verdict):
|
||||
"""`[(kind, start, end)]`: where the add-on itself buys ("cheap") and
|
||||
gives ("dear"). With battery modes on that is the kept `priceruns.RunPlan`
|
||||
@@ -220,7 +220,7 @@ def price_windows(c, plan, verdict):
|
||||
`trade_runs()` draws. Nothing is classified here."""
|
||||
if getattr(c, "modes_on", False):
|
||||
rp = getattr(c, "mode_runs", None)
|
||||
if rp is None or not rp.on:
|
||||
if rp is None or not rp.on or not _price_mode_in_use(c):
|
||||
return []
|
||||
out = [("cheap", rp.cheap.start, rp.cheap.end)] \
|
||||
if rp.charge_useful() else []
|
||||
@@ -1478,3 +1478,10 @@ def _samples() -> list:
|
||||
("not_covered", "no_prices", "peak", "not_firm", "not_worth",
|
||||
"no_level")]
|
||||
return out
|
||||
|
||||
|
||||
def priced_hours(bars):
|
||||
"""How many clock hours of the day have a price: distinct hours among the
|
||||
bars that have one, so 96 quarters count 24, not 96. The setup wizard's
|
||||
"N of 24 hours". A 23-hour day gives 23, a 25-hour day 24."""
|
||||
return len({b["label"][:2] for b in bars if b.get("eur_kwh") is not None})
|
||||
|
||||
@@ -50,7 +50,7 @@ from aiohttp import web
|
||||
|
||||
from . import messages, price, selfconfig, uipage, uitext
|
||||
from .haentities import EntityCache
|
||||
from .homeview import region_of
|
||||
from .homeview import priced_hours, region_of
|
||||
from .validate import config_notes, setup_checks
|
||||
from .config_page import CONFIG_PAGE
|
||||
from .configui import (changed_keys, field_model, hide_secrets, keep_secrets,
|
||||
@@ -328,7 +328,7 @@ def build_app(controller, selfcfg=None) -> web.Application:
|
||||
try:
|
||||
prices = (controller.status_payload().get("plan") or {}).get(
|
||||
"prices") or []
|
||||
known = sum(1 for p in prices if p.get("eur_kwh") is not None)
|
||||
known = priced_hours(prices)
|
||||
except Exception: # noqa: BLE001 - a display figure, never a failure
|
||||
known = None
|
||||
return setup_checks(options, entities=rows, running=controller.o,
|
||||
|
||||
Reference in New Issue
Block a user