OPT-34 round 3: the latch ends on time, with the run, with the mode, and with the night
- F1: an expired latch is dropped whatever the run state; plan_targets() and the store loader ignore one that has ended (no stale latch after a restart). - F2: the "charge goes on" line only while a cheap run is on. - F3: a battery that is off or no longer Leftover solar leaves the latch. - F4: a run carries the latch on only if it starts by the bell start (06:00 local) after the latch began (manager's call, 2026-10-31). Tests: test_opt34 scenarios stale/stale_live and part 5; test_modes_p4 asks at a moment inside the latch. 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
57b0eb2f20
commit
bb57a85b35
@@ -16,7 +16,16 @@ known, so the add-on does not depend on it.
|
||||
no-forecast gap does not refuse the next slice of the run (`weather_held()`,
|
||||
as TARIFF-24 does for the run under way). While a
|
||||
forecast is there the latch follows the plan, up or down as a fresh figure moves
|
||||
it. It lasts through every cheap run on and one price slot after the last.
|
||||
it. It lasts through every cheap run on and one price slot after the last, and it
|
||||
ends at once when its end time passes, whatever the run state (it is also
|
||||
ignored at a restart if it has ended). A battery that is switched off or
|
||||
changed to another mode leaves it.
|
||||
*Night bound (manager's call, 2026-10-31):* a run carries the latch on only if
|
||||
it starts by the bell start (06:00 local) after the latch began; that run
|
||||
finishes, plus one slot. Chosen over a fixed 12 h because the night tariff
|
||||
(22:00-07:00) needs the 06:00-07:00 slice and 12 h from a late start would
|
||||
reach into the day. With dynamic prices a chain of cheap slots cannot carry a
|
||||
night's level into the next day.
|
||||
2. *The evening's tomorrow figure is today's until the bell starts*, 06:00 local
|
||||
(`BELL_FROM_H`, `solarfc._bell_start()`), not until midnight + 3 h.
|
||||
Nothing has been produced yet, so it is still the whole day.
|
||||
@@ -37,7 +46,7 @@ plan, the screens and a restart also need the figure itself.
|
||||
06:00 and unchanged. After the figure ends, with a charge still going on: "No
|
||||
solar forecast now. The charge under way goes on to its planned level." /
|
||||
"Nu geen zonneprognose. De lopende lading gaat door tot het geplande niveau."
|
||||
(`home_words.solar.latched`, only while a latched battery is below its level).
|
||||
(`home_words.solar.latched`, only while a cheap run is on and a latched battery is below its level).
|
||||
|
||||
**Not used.** A figure over 15 h old; a morning figure at night (12 h); a figure
|
||||
for another day; a bad value or unit; and no latch for a charge that has not
|
||||
@@ -50,4 +59,4 @@ A fresh figure that LOWERS the level during a charge still lowers it, as before.
|
||||
**Mutants** (all red in `test_opt34.py`): no latch; window back to midnight + 3 h;
|
||||
15 h without a limit; no "usable at midnight" rule; the latch without the narrow
|
||||
gap, without its save, or not kept through the next run; the window 6 h after
|
||||
midnight instead of 06:00 local (DST); 15 h cut to 12 h; the new screen line off.
|
||||
midnight instead of 06:00 local (DST); 15 h cut to 12 h; the new screen line off or shown without a cheap run; no expiry check; no pruning of a changed mode; no night bound; a left-the-plan battery keeping its level.
|
||||
|
||||
@@ -536,8 +536,13 @@ def plan_cover(c, name):
|
||||
|
||||
|
||||
def _latched_below(c) -> bool:
|
||||
"""OPT-34: no forecast, but a battery is still below the level its night
|
||||
charge keeps (`Controller.plan_targets()`), so control goes on charging."""
|
||||
"""OPT-34: no forecast, but a cheap run is on and a battery is still below
|
||||
the level its night charge keeps (`Controller.plan_targets()`), so control
|
||||
goes on charging."""
|
||||
rp = getattr(c, "mode_runs", None)
|
||||
if rp is None or rp.cheap is None or not rp.cheap.holds(
|
||||
datetime.now(timezone.utc)):
|
||||
return False
|
||||
targets = getattr(c, "plan_targets", lambda: None)() or {}
|
||||
return any(inv.soc is not None and inv.name in targets
|
||||
and inv.soc < targets[inv.name] for inv in c.inverters)
|
||||
|
||||
@@ -221,18 +221,29 @@ CHARGE_LATCH_GRACE_S = 900.0
|
||||
FORECAST_READ_S = 60.0
|
||||
|
||||
|
||||
def _load_latch(data):
|
||||
"""`(until, {name: level %})` from the store, or None if it is not a
|
||||
well-formed latch (OPT-34)."""
|
||||
def _load_latch(data, now=None):
|
||||
"""`(until, {name: level %}, born)` from the store, or None if it is not a
|
||||
well-formed latch or has ended (OPT-34)."""
|
||||
try:
|
||||
until = datetime.fromisoformat(data["until"])
|
||||
born = datetime.fromisoformat(data.get("born") or data["until"])
|
||||
levels = {str(n): float(v) for n, v in data["levels"].items()}
|
||||
except (TypeError, KeyError, ValueError, AttributeError):
|
||||
return None
|
||||
if until.tzinfo is None or not levels or not all(
|
||||
if until.tzinfo is None or born.tzinfo is None or not levels or not all(
|
||||
math.isfinite(v) and 0.0 <= v <= 100.0 for v in levels.values()):
|
||||
return None
|
||||
return until, levels
|
||||
if now is not None and now >= until:
|
||||
return None
|
||||
return until, levels, born
|
||||
|
||||
|
||||
def _night_limit(born):
|
||||
"""OPT-34: a run that starts after this cannot carry the latch on: the
|
||||
first bell start (`solarfc.BELL_FROM_H`, local) after `born`."""
|
||||
d0, d1 = solarfc._bounds(born)
|
||||
bell = solarfc._bell_start(d0)
|
||||
return bell if bell > born else solarfc._bell_start(d1)
|
||||
|
||||
|
||||
def why_raised(resolution, claims) -> bool:
|
||||
@@ -1028,7 +1039,8 @@ class Controller:
|
||||
self._plan_targets = None
|
||||
# OPT-34: `(until, {name: level %})` of a night charge under way.
|
||||
self._charge_latch = _load_latch(
|
||||
(getattr(store, "data", None) or {}).get(CHARGE_LATCH_KEY))
|
||||
(getattr(store, "data", None) or {}).get(CHARGE_LATCH_KEY),
|
||||
datetime.now(timezone.utc))
|
||||
self.house_profile = None
|
||||
if self.fc_entity is not None:
|
||||
self.house_profile = dayplan.HouseProfile(
|
||||
@@ -2384,10 +2396,12 @@ class Controller:
|
||||
of a night charge that has started (OPT-34): it keeps that level."""
|
||||
if self.solar_fc is None:
|
||||
lt = self._charge_latch
|
||||
return None if lt is None else dict(lt[1])
|
||||
if lt is None or datetime.now(timezone.utc) >= lt[0]:
|
||||
return None
|
||||
return dict(lt[1])
|
||||
return self._plan_targets
|
||||
|
||||
def _latch_charge(self, now, runs, targets) -> None:
|
||||
def _latch_charge(self, now, runs, targets, packs=None) -> None:
|
||||
"""OPT-34, once per cycle after `modetrade.decide()`. A night grid
|
||||
charge that has started keeps its planned level when the forecast goes
|
||||
mid-charge (the kept figure ends and the sensors are still silent).
|
||||
@@ -2400,15 +2414,26 @@ class Controller:
|
||||
is none, `plan_targets()` gives the latched levels, so the charge goes
|
||||
on to them, and `weather_held()` keeps the narrow price gap. The latch
|
||||
lasts through every cheap run on and one price slot after the last
|
||||
(`CHARGE_LATCH_GRACE_S`); a charge that never started has none."""
|
||||
(`CHARGE_LATCH_GRACE_S`), but a run is carried on only if it starts
|
||||
by the bell start of the night the latch began in (`_night_limit()`;
|
||||
the run under way then finishes). It is dropped when `now` passes its
|
||||
end whatever the run state, and a battery that is off or no longer in
|
||||
a mode with a planned level (`packs`) leaves it. A charge that never
|
||||
started has none."""
|
||||
old = lt = self._charge_latch
|
||||
tick = self.mode_tick
|
||||
run = None if runs is None else runs.cheap
|
||||
in_run = run is not None and run.holds(now)
|
||||
until = None if run is None else run.end + timedelta(
|
||||
seconds=CHARGE_LATCH_GRACE_S)
|
||||
if lt is not None and not in_run and now >= lt[0]:
|
||||
grace = timedelta(seconds=CHARGE_LATCH_GRACE_S)
|
||||
if lt is not None and now >= lt[0]:
|
||||
lt = None
|
||||
if lt is not None and packs is not None:
|
||||
ok = {p.name for p in packs
|
||||
if p.use and p.mode in packmode.PLAN_CHARGE_MODES}
|
||||
lv = {n: v for n, v in lt[1].items() if n in ok}
|
||||
lt = (lt[0], lv, lt[2]) if lv else None
|
||||
# A run may carry the latch on only if it starts by the night's bell.
|
||||
carry = in_run and (lt is None or run.start <= _night_limit(lt[2]))
|
||||
if in_run and self.solar_fc is not None:
|
||||
# Rebuilt from the plan: a battery already latched follows its
|
||||
# level up or down whatever its role this tick (a fresh figure may
|
||||
@@ -2419,14 +2444,20 @@ class Controller:
|
||||
if tick is not None and tick.action == modetrade.CHARGE:
|
||||
lv.update({n: float(tg[n]) for n, r in tick.roles.items()
|
||||
if r == modetrade.CHARGE and n in tg})
|
||||
lt = (max(until, lt[0]) if lt is not None else until, lv) \
|
||||
if lv else None
|
||||
elif lt is not None and in_run:
|
||||
lt = (max(lt[0], until), lt[1])
|
||||
if not lv:
|
||||
lt = None
|
||||
elif lt is None:
|
||||
lt = (run.end + grace, lv, now)
|
||||
else:
|
||||
lt = (max(run.end + grace, lt[0]) if carry else lt[0], lv,
|
||||
lt[2])
|
||||
elif lt is not None and carry:
|
||||
lt = (max(lt[0], run.end + grace), lt[1], lt[2])
|
||||
if lt != old:
|
||||
self._charge_latch = lt
|
||||
self.store.set(CHARGE_LATCH_KEY, None if lt is None else {
|
||||
"until": lt[0].isoformat(), "levels": lt[1]})
|
||||
"until": lt[0].isoformat(), "levels": lt[1],
|
||||
"born": lt[2].isoformat()})
|
||||
|
||||
def _mode_roles(self, now, maint_results):
|
||||
"""OPT-11 P3: each battery's role and range this tick. Grid charging
|
||||
@@ -2500,7 +2531,7 @@ class Controller:
|
||||
blocked=self._charge_blocked_q == quarter,
|
||||
done_charge=done_c, done_sell=done_s, targets=targets,
|
||||
draining=any(r.phase == DRAIN for r in maint_results or ()))
|
||||
self._latch_charge(now, runs, targets)
|
||||
self._latch_charge(now, runs, targets, packs)
|
||||
# A charge the guard or the headroom stopped stays off this period.
|
||||
# OPT-15: only a MEASURED stop latches; the first unmeasured headroom
|
||||
# (None) in a quarter stops this tick only, so a gap cannot cost the
|
||||
|
||||
@@ -810,11 +810,16 @@ check("a lost forecast says so: no forecast, Leftover solar solar only",
|
||||
homeview.solar_block(c, datetime.now(timezone.utc))["state"]
|
||||
== "no_forecast" and homeview.day_key(c, B) == "no_forecast")
|
||||
c._charge_latch = latch
|
||||
# the latch ends at its end time; this replay ran in September, so ask at a
|
||||
# moment inside it
|
||||
_real_dt = rp.m.datetime
|
||||
rp.m.datetime = type("_DT", (datetime,), {"now": classmethod(
|
||||
lambda cls, tz=None: latch[0] - timedelta(hours=1))})
|
||||
check("OPT-34: with a charge under way (B charged in this run), the level "
|
||||
"stays: plan_targets() gives it, the line says the charge goes on",
|
||||
"stays: plan_targets() gives it (the line: test_opt34.py)",
|
||||
latch is not None and c.plan_targets() is not None
|
||||
and B in c.plan_targets() and c.plan_targets()[B] == c._plan_targets[B]
|
||||
and homeview.day_key(c, B) is None)
|
||||
and B in c.plan_targets() and c.plan_targets()[B] == c._plan_targets[B])
|
||||
rp.m.datetime = _real_dt
|
||||
check("the Why line wins over 'saves its power' only while holding",
|
||||
homeview.battery_why(modes_on=True, mode="leftover_solar", off=False,
|
||||
reading=True, phase="idle", shadow=False,
|
||||
|
||||
@@ -262,6 +262,8 @@ def run_night(spec):
|
||||
for k, val in (("pack_use", use), ("pack_mode", mode),
|
||||
("pack_sell", sell)):
|
||||
store.data[unit_key(f"inverter-{u}", k)] = val
|
||||
if spec.get("latch"):
|
||||
store.data[m.CHARGE_LATCH_KEY] = spec["latch"]
|
||||
sens = sensors(silent=spec.get("silent", True),
|
||||
fresh_from=spec.get("fresh_from"),
|
||||
fresh=spec.get("fresh", (1.0, 5.0)),
|
||||
@@ -270,7 +272,7 @@ def run_night(spec):
|
||||
|
||||
class H(rp.Hass):
|
||||
async def state(self, e):
|
||||
return sens(clock[0], e)
|
||||
return GONE if spec.get("gone") else sens(clock[0], e)
|
||||
|
||||
hass = H(("a", "b"), lambda t: 300.0, soc0)
|
||||
out = dict(marks={}, soc_at={})
|
||||
@@ -410,6 +412,18 @@ SCEN = {
|
||||
soc={"a": 30.0, "b": 30.0}, fresh_from=at(4, 3, 10),
|
||||
fresh_until=at(4, 3, 10), fresh=(30.0, 30.0),
|
||||
marks=[at(4, 3, 5), at(4, 3, 20), at(4, 6, 30)]),
|
||||
# F1: the add-on was down when the latch of the night before ended; it is
|
||||
# back inside a cheap run, with the sensors unavailable (no forecast)
|
||||
"stale": dict(start=at(3, 22, 0), hours=0.5, gone=True,
|
||||
latch={"until": at(3, 7, 15).isoformat(),
|
||||
"born": at(2, 23, 0).isoformat(),
|
||||
"levels": {A: 80.0, B: 80.0}},
|
||||
marks=[at(3, 22, 20)]),
|
||||
"stale_live": dict(start=at(3, 22, 0), hours=0.5, gone=True,
|
||||
latch={"until": at(4, 7, 15).isoformat(),
|
||||
"born": at(3, 22, 0).isoformat(),
|
||||
"levels": {A: 80.0, B: 80.0}},
|
||||
marks=[at(3, 22, 20)]),
|
||||
# the same, B already full and a 1.5 kW limit: the run is sized at the
|
||||
# battery's 2.5 kW rating and ends at 06:33, before A is there, so the
|
||||
# next slice is planned after 06:00 and is only A's
|
||||
@@ -537,6 +551,18 @@ check("the sensors go quiet, the forecast ends (06:30): the latch holds the "
|
||||
and l2["target_a"] < l2["soc_a"] and l2["p_a"] > -200.0
|
||||
and lo["soc_at"]["06:55"][0] < lo["soc_at"]["03:30"][0] + 1.0
|
||||
and lo["soc"][1] > lo["soc_at"]["03:30"][1] + 15.0)
|
||||
st_, sl_ = R["stale"], R["stale_live"]
|
||||
check("F1: a latch from the night before (ended 07:15) loaded at a restart "
|
||||
"inside a cheap run, with no forecast: nothing grid-charges "
|
||||
f"(A {st_['soc'][0]:.1f}, B {st_['soc'][1]:.1f} %) and the line does "
|
||||
"not say the charge goes on",
|
||||
st_["soc"][0] < 10.2 and st_["soc"][1] < 10.2
|
||||
and st_["marks"]["22:20"]["state"] == "no_forecast"
|
||||
and not st_["latch"])
|
||||
check("(control: the same latch, still valid until tomorrow 07:15, does "
|
||||
f"charge both: A {sl_['soc'][0]:.1f}, B {sl_['soc'][1]:.1f} %)",
|
||||
sl_["soc"][0] > 11.0 and sl_["soc"][1] > 11.0
|
||||
and sl_["marks"]["22:20"]["state"] == "latched")
|
||||
tb = R["tail_bfull"]
|
||||
check("B full: the run ends at 06:33 before A is at its level, so the next "
|
||||
"slice is only A's, planned after the figure has ended with A's "
|
||||
@@ -573,6 +599,17 @@ print("\n4. The latch alone: a battery that leaves the plan leaves the latch")
|
||||
from types import SimpleNamespace # noqa: E402
|
||||
from app import modetrade, priceruns # noqa: E402
|
||||
|
||||
CLOCK = [at(4, 1, 0)]
|
||||
_dt0 = m.datetime
|
||||
|
||||
|
||||
class _FixedDT(_dt0):
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
return CLOCK[0] if tz is None else CLOCK[0].astimezone(tz)
|
||||
|
||||
|
||||
m.datetime = homeview.datetime = _FixedDT
|
||||
_tmp = tempfile.mkdtemp(prefix="o34-")
|
||||
lc = Controller(dict(opts()), rp.Hass(("a", "b"), lambda t: 300.0,
|
||||
{"a": 40.0, "b": 40.0}),
|
||||
@@ -587,9 +624,10 @@ resting = SimpleNamespace(action=None, roles={A: modetrade.HOLD,
|
||||
B: modetrade.HOLD})
|
||||
|
||||
|
||||
def latch_step(fc, tick, targets, now=n0):
|
||||
def latch_step(fc, tick, targets, now=n0, runs=None, packs=None):
|
||||
CLOCK[0] = now
|
||||
lc.solar_fc, lc.mode_tick = fc, tick
|
||||
lc._latch_charge(now, lruns, targets)
|
||||
lc._latch_charge(now, runs or lruns, targets, packs)
|
||||
return lc._charge_latch, lc.store.data.get(m.CHARGE_LATCH_KEY)
|
||||
|
||||
|
||||
@@ -617,15 +655,88 @@ check("B leaves the plan too: the latch is empty, so it is gone and "
|
||||
latch_step(FC, charging, {A: 80.0})
|
||||
lc.solar_fc = None
|
||||
ia = next((i for i in lc.inverters if i.name == A), None)
|
||||
st_below = st_at = None
|
||||
st_below = st_at = st_after = None
|
||||
lc.mode_runs = lruns
|
||||
if ia is not None:
|
||||
ia.soc = 50.0
|
||||
st_below = homeview.plan_state(lc)
|
||||
CLOCK[0] = at(4, 7, 5) # the run has ended, the latch has not
|
||||
st_after = homeview.plan_state(lc)
|
||||
CLOCK[0] = n0
|
||||
ia.soc = 80.0
|
||||
st_at = homeview.plan_state(lc)
|
||||
check("the screen says the charge goes on only while the latched battery is "
|
||||
f"below its level ({st_below!r} at 50 %, {st_at!r} at 80 %)",
|
||||
st_below == "latched" and st_at == "no_forecast")
|
||||
check("F2: and only while a cheap run is on: at 07:05, the run over and the "
|
||||
f"latch in its grace slot, it does not ({st_after!r})",
|
||||
st_after == "no_forecast")
|
||||
|
||||
print("\n5. The latch ends: expiry, a changed mode, the night's bell")
|
||||
long_run = SimpleNamespace(cheap=priceruns.Run(at(4, 7, 0), at(4, 23, 0), .2))
|
||||
lc._charge_latch = (at(4, 7, 15), {A: 80.0}, at(4, 1, 0))
|
||||
lt, saved = latch_step(None, resting, None, now=at(4, 7, 20), runs=long_run)
|
||||
check("F1: past its end (07:15) the latch is dropped even with a cheap run "
|
||||
"on and no forecast (it was only dropped when no run held)",
|
||||
lt is None and saved is None)
|
||||
lc._charge_latch = (at(4, 7, 15), {A: 80.0}, at(4, 1, 0))
|
||||
CLOCK[0] = at(4, 7, 20)
|
||||
lc.solar_fc = None
|
||||
check("... plan_targets() ignores a latch past its end", lc.plan_targets()
|
||||
is None)
|
||||
CLOCK[0] = at(4, 1, 0)
|
||||
check("... and _load_latch() does not bring one back from the store",
|
||||
m._load_latch({"until": at(4, 7, 15).isoformat(), "born":
|
||||
at(4, 1, 0).isoformat(), "levels": {A: 80.0}},
|
||||
at(4, 7, 20)) is None
|
||||
and m._load_latch({"until": at(4, 7, 15).isoformat(), "born":
|
||||
at(4, 1, 0).isoformat(), "levels": {A: 80.0}},
|
||||
at(4, 6, 0)) is not None)
|
||||
|
||||
# F3: the mode or use of a latched battery changes
|
||||
def packs_of(a_mode, a_use=True):
|
||||
return [modetrade.Pack(A, a_use, a_mode, False, 2500.0, 50.0, 10.0),
|
||||
modetrade.Pack(B, True, "cheap_hours", False, 2500.0, 50.0, 10.0)]
|
||||
|
||||
|
||||
lc._charge_latch = (at(4, 7, 15), {A: 80.0}, at(4, 1, 0))
|
||||
lt, _s = latch_step(None, resting, None, packs=packs_of("leftover_solar"))
|
||||
check("F3: A is still Leftover solar and on: it stays latched",
|
||||
lt is not None and lt[1] == {A: 80.0})
|
||||
lt, saved = latch_step(None, resting, None, packs=packs_of("solar_first"))
|
||||
check("F3: the user changes A to Solar first (no forecast): it leaves the "
|
||||
"latch, which is then gone and cleared, so the narrow gap goes too",
|
||||
lt is None and saved is None)
|
||||
lc._charge_latch = (at(4, 7, 15), {A: 80.0}, at(4, 1, 0))
|
||||
lt, _s = latch_step(None, resting, None,
|
||||
packs=packs_of("leftover_solar", a_use=False))
|
||||
check("F3: or switches A off", lt is None)
|
||||
|
||||
# F4: the night's bell
|
||||
lc._charge_latch = None
|
||||
lt, _s = latch_step(FC, charging, {A: 80.0}, now=at(4, 1, 0))
|
||||
check("F4: the latch began at 01:00, so its night's bell is 06:00 the same "
|
||||
"day", m._night_limit(lt[2]) == at(4, 6, 0)
|
||||
and m._night_limit(at(3, 23, 0)) == at(4, 6, 0)
|
||||
and m._night_limit(at(4, 6, 0)) == at(5, 6, 0))
|
||||
r1 = SimpleNamespace(cheap=priceruns.Run(at(4, 0, 0), at(4, 7, 0), .2))
|
||||
r2 = SimpleNamespace(cheap=priceruns.Run(at(4, 7, 0), at(4, 23, 0), .2))
|
||||
r3 = SimpleNamespace(cheap=priceruns.Run(at(4, 6, 0), at(4, 9, 0), .2))
|
||||
lc._charge_latch = None
|
||||
lt, _s = latch_step(FC, charging, {A: 80.0}, now=at(4, 1, 0), runs=r1)
|
||||
check("F4: first run 00:00-07:00 sets the end at 07:15", lt[0] == at(4, 7, 15))
|
||||
lt, _s = latch_step(None, resting, None, now=at(4, 6, 50), runs=r1)
|
||||
lt2, _s = latch_step(None, resting, None, now=at(4, 7, 5), runs=r2)
|
||||
check("F4: a back-to-back run that starts after the bell (07:00-23:00) does "
|
||||
"NOT carry it on: still 07:15", lt2 is not None
|
||||
and lt2[0] == at(4, 7, 15))
|
||||
lt3, _s = latch_step(None, resting, None, now=at(4, 7, 20), runs=r2)
|
||||
check("F4: and at 07:20 it has ended, though the run goes on", lt3 is None)
|
||||
lc._charge_latch = (at(4, 6, 10), {A: 80.0}, at(4, 1, 0))
|
||||
lt, _s = latch_step(None, resting, None, now=at(4, 6, 5), runs=r3)
|
||||
check("F4: a run that starts AT the bell (06:00-09:00) does carry it on, "
|
||||
"to its end + one slot", lt is not None and lt[0] == at(4, 9, 15))
|
||||
m.datetime = homeview.datetime = _dt0
|
||||
|
||||
print()
|
||||
if fails:
|
||||
|
||||
Reference in New Issue
Block a user