OPT-36 round 4: count only charge control will make; Solar first drain at the night base
Review F1: a slot after B's run counts only if no slot from the expensive
hours on is cheaper (control plans B's charge left in the cheapest window
ahead), and x 0.95 of B's charge is stored. Three-price night: 1ac7fe9 left
the expensive hours 2.61 kWh short; now 0.
Review F2: a Solar first battery's drain until the expensive hours reads each
hour at no less than the night base (the profile is net of the sun). Read net,
a dark morning drained the battery the plan still counted; A's level fell,
the cheap run moved and was lost before it started (+0.40 EUR per 3 days).
The two real-price replays now cost what 0.20.0 does.
F3: the ux54 sweep's peak limit is 15 kW (the option's top). F4: the rest
comes from the grid (EN/NL). F5: CHANGELOG and DECISIONS wording.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
1ac7fe98d7
commit
eb9ccc2730
@@ -240,14 +240,18 @@ def _cheap_give_kwh(runs, now, house, packs, *, floor_pct, ceiling_pct,
|
||||
when the time left cannot get there. That time is the cheap run from now
|
||||
(or the whole run ahead of it) and, given the firm price `slots`, each
|
||||
later slot before the expensive hours (`until`, a give run) still worth
|
||||
charging in: its price plus the spread control asks (`spread`) at most
|
||||
the give run's dearest slot, `priceruns.plan()`'s test. The run is sized
|
||||
at the batteries' ratings (`priceruns.need_hours()`), not the peak limit,
|
||||
so a run too short to fill them is planned again in that time; counting
|
||||
the run alone let A's level fall in the night after A had bought. In
|
||||
that time the batteries together charge at their ratings, held to what
|
||||
the peak aim leaves above the house's use that hour (`aim_w`, None: no
|
||||
limit), shared by rating. No cheap run worth charging in: the level now.
|
||||
charging in (its price plus the spread control asks, `spread`, at most
|
||||
the give run's dearest slot, `priceruns.plan()`'s test) and no dearer
|
||||
than every slot from the start of those hours on: control plans the
|
||||
charge left in the cheapest window ahead, so a slot with a cheaper one
|
||||
after it may never be used (round 4, a three-price night). The run is
|
||||
sized at the batteries' ratings (`priceruns.need_hours()`), not the peak
|
||||
limit, so a run too short to fill them is planned again in that time;
|
||||
counting the run alone let A's level fall in the night after A had
|
||||
bought. In that time the batteries together charge at their ratings,
|
||||
held to what the peak aim leaves above the house's use that hour
|
||||
(`aim_w`, None: no limit), shared by rating; x `EFF` of it is stored.
|
||||
No cheap run worth charging in: the level now.
|
||||
A battery that refuses a direction counts 0."""
|
||||
ps = [p for p in packs if p.mode == CHEAP_HOURS and p.refused == 0]
|
||||
if not ps:
|
||||
@@ -261,10 +265,13 @@ def _cheap_give_kwh(runs, now, house, packs, *, floor_pct, ceiling_pct,
|
||||
top = max((eur for s, _e, eur in slots or ()
|
||||
if until is not None and until.start <= s < until.end),
|
||||
default=None)
|
||||
low = min((eur for s, _e, eur in slots or ()
|
||||
if until is not None and s >= until.start), default=None)
|
||||
if top is not None and spread is not None:
|
||||
spans += [(max(s, now, run.end), min(e, until.start))
|
||||
for s, e, eur in slots
|
||||
if eur + float(spread) <= top + 1e-9
|
||||
and (low is None or eur <= low + 1e-9)
|
||||
and e > run.end and s < until.start]
|
||||
if runs.charge_useful() and total > 0.0:
|
||||
for t0, t1 in spans:
|
||||
@@ -275,7 +282,8 @@ def _cheap_give_kwh(runs, now, house, packs, *, floor_pct, ceiling_pct,
|
||||
- max(house_w(h), base_w, 0.0)))
|
||||
hrs = (e - s).total_seconds() / 3600.0
|
||||
for p in ps:
|
||||
gain[p.name] += w * rate[p.name] / total * hrs / 1000.0
|
||||
gain[p.name] += w * rate[p.name] / total * hrs \
|
||||
/ 1000.0 * EFF
|
||||
out = 0.0
|
||||
for p in ps:
|
||||
cap = float(p.capacity_kwh)
|
||||
@@ -289,15 +297,18 @@ def _solar_first_give_kwh(house, now, start, packs, *, floor_pct):
|
||||
"""OPT-36 (owner, 2026-11-02): what the Solar first batteries in `packs`
|
||||
will still give when the expensive hours start at `start`, in kWh on the
|
||||
house's side. They give at any hour, so the house's use from now to
|
||||
`start` (the plan's own profile) is taken off first, shared by the energy
|
||||
each holds above the floor, and what is left x `EFF` counts. A battery
|
||||
`start` is taken off first, shared by the energy each holds above the
|
||||
floor, and what is left x `EFF` counts. Each hour is read at no less than
|
||||
the night base: the profile is net of the sun the house used, and read
|
||||
net it let a dark day's morning drain a battery that the plan still
|
||||
counted (round 4, real-price replays). A battery
|
||||
that runs down to the floor before then counts 0; one that refuses a
|
||||
direction counts 0."""
|
||||
ps = [p for p in packs if p.mode == SOLAR_FIRST and p.refused == 0]
|
||||
if not ps:
|
||||
return 0.0
|
||||
house_w, _base = house
|
||||
use = sum(max(0.0, house_w(h)) * (e - s).total_seconds() / 3.6e6
|
||||
house_w, base_w = house
|
||||
use = sum(max(0.0, house_w(h), base_w) * (e - s).total_seconds() / 3.6e6
|
||||
for s, e, h in _hour_slices(now, start))
|
||||
have = {p.name: max(0.0, float(p.soc) - float(floor_pct)) / 100.0
|
||||
* float(p.capacity_kwh) for p in ps}
|
||||
|
||||
@@ -1489,7 +1489,8 @@ def line_solar(fc_kwh, count_kwh, at, kept=False) -> str:
|
||||
|
||||
def line_uncovered(kwh) -> str:
|
||||
"""OPT-28: the Leftover solar batteries cannot hold the expensive hours'
|
||||
shortfall. The rest comes from another battery or the grid."""
|
||||
shortfall. The rest comes from the grid: OPT-36 counts the other
|
||||
batteries first."""
|
||||
return _t("solar.uncovered", kwh=_kwh(kwh))
|
||||
|
||||
|
||||
|
||||
@@ -361,7 +361,7 @@
|
||||
"tops_up": "It charges from the grid to {pct}. That covers {kwh} of expensive hours the sun will not.",
|
||||
"no_forecast": "No solar forecast, so it charges from solar only."
|
||||
},
|
||||
"solar": {"line": "Solar still to come today: {kwh}, we count on {count}. Updated {at}.", "uncovered": "The expensive hours need more than Leftover solar can hold. The rest, {kwh}, comes from another battery or the grid.", "kept": "Solar forecast read {at}, kept for tonight.", "no_forecast": "No solar forecast now, so Leftover solar charges from solar only.", "latched": "No solar forecast now. The charge under way goes on to its planned level.", "learning": "The add-on is learning your house's use. The plan starts after one full day."},
|
||||
"solar": {"line": "Solar still to come today: {kwh}, we count on {count}. Updated {at}.", "uncovered": "The expensive hours need more than Leftover solar can hold. The rest, {kwh}, comes from the grid.", "kept": "Solar forecast read {at}, kept for tonight.", "no_forecast": "No solar forecast now, so Leftover solar charges from solar only.", "latched": "No solar forecast now. The charge under way goes on to its planned level.", "learning": "The add-on is learning your house's use. The plan starts after one full day."},
|
||||
"today": {
|
||||
"not_planned": "Not planned yet.",
|
||||
"no_runs": "No cheap or expensive hours left today.",
|
||||
|
||||
@@ -301,7 +301,7 @@
|
||||
"tops_up": "Ze laadt van het net tot {pct}. Dat dekt {kwh} dat de zon in dure uren niet levert.",
|
||||
"no_forecast": "Geen zonneprognose, dus ze laadt alleen met zonne-energie."
|
||||
},
|
||||
"solar": {"line": "Nog verwachte zon vandaag: {kwh}, we rekenen op {count}. Bijgewerkt om {at}.", "uncovered": "De dure uren vragen meer dan Overschot zonne-energie kan opslaan. De rest, {kwh}, komt van een andere batterij of het net.", "kept": "Zonneprognose van {at}, bewaard voor vannacht.", "no_forecast": "Nu geen zonneprognose, dus Overschot zonne-energie laadt alleen met zon.", "latched": "Nu geen zonneprognose. De lopende lading gaat door tot het geplande niveau.", "learning": "De add-on leert eerst het verbruik van uw huis. Het plan start na één volle dag."},
|
||||
"solar": {"line": "Nog verwachte zon vandaag: {kwh}, we rekenen op {count}. Bijgewerkt om {at}.", "uncovered": "De dure uren vragen meer dan Overschot zonne-energie kan opslaan. De rest, {kwh}, komt van het net.", "kept": "Zonneprognose van {at}, bewaard voor vannacht.", "no_forecast": "Nu geen zonneprognose, dus Overschot zonne-energie laadt alleen met zon.", "latched": "Nu geen zonneprognose. De lopende lading gaat door tot het geplande niveau.", "learning": "De add-on leert eerst het verbruik van uw huis. Het plan start na één volle dag."},
|
||||
"today": {
|
||||
"not_planned": "Nog niet gepland.",
|
||||
"no_runs": "Vandaag geen goedkope of dure uren meer.",
|
||||
|
||||
Reference in New Issue
Block a user