UX-10 review: the line as logged gets the red-alarm shape, clamp lines at 30 words, plain HA 400
Part 6 checked the quoted reason alone, so title + reason + tails logged as 4+ sentences. It now also checks the whole line (at most 3 sentences, none over 30 words); 25 lines failed and are rewritten (one-sentence wrappers, two-sentence circulation and peak reasons, STALE_READING's fixed limits on a DEBUG line). The clamp lines for one inverter are pinned at 30 words with OUTPUT_CLAMP and the numbers kept. Home Assistant's bare '400: Bad Request' while it loads becomes one plain line; still WARNING, throttled, a failed write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -246,9 +246,9 @@ class Allocation:
|
||||
block the failsafe is worse than the hole it closes.
|
||||
"""
|
||||
if tick is None or self.tick is None or self.tick != tick:
|
||||
return (f"the allocation record on hand was made on tick "
|
||||
f"{self.tick!r} and this is tick {tick!r} - a record whose "
|
||||
f"envelopes have moved cannot authorise a write")
|
||||
return (f"the power split on hand is from tick {self.tick!r}, "
|
||||
f"not this tick {tick!r}, and an old split cannot allow a "
|
||||
f"write")
|
||||
if not self.ok:
|
||||
return f"no power split was possible this tick ({self.explain()})"
|
||||
if unit not in self.shares:
|
||||
|
||||
@@ -328,9 +328,10 @@ class CirculationWatchdog:
|
||||
# UX-10: plain sentences, every figure kept (test_messages.py part 6).
|
||||
self.reason = (
|
||||
f"CIRCULATING_POWER: {circ_w:.0f} W measured going round between "
|
||||
f"the inverters for {held:.0f} s, peaking at {self.peak_w:.0f} W. "
|
||||
f"The meter cannot see it, because it nets to zero there. Limits: "
|
||||
f"over {self.cfg.min_w:.0f} W, held over {self.cfg.hold_s:.0f} s")
|
||||
f"the inverters for {held:.0f} s, peaking at {self.peak_w:.0f} W, "
|
||||
f"over the limits of {self.cfg.min_w:.0f} W and "
|
||||
f"{self.cfg.hold_s:.0f} s. The meter cannot see it, because it "
|
||||
f"nets to zero there")
|
||||
return True
|
||||
|
||||
def held_s(self, now: datetime) -> float:
|
||||
|
||||
@@ -239,8 +239,8 @@ SUSTAINED_CYCLES = 3
|
||||
# UX-10: the reasons below are quoted by a WARNING or ERROR line, so they
|
||||
# follow STYLE.md's rule for a quoted reason (test_messages.py part 6): no
|
||||
# final full stop, the line adds it. The line around them says "software
|
||||
# fault" for an over-rating, so the reason does not.
|
||||
|
||||
# fault" for an over-rating, so the reason does not. Each is one sentence, so
|
||||
# the logged line stays within 3 sentences and about 30 words.
|
||||
|
||||
|
||||
def loop_clamp_note(d) -> str:
|
||||
@@ -251,21 +251,29 @@ def loop_clamp_note(d) -> str:
|
||||
decision's own, i.e. from the tick that clamped.
|
||||
"""
|
||||
if d.clamped:
|
||||
return (f"{REASON_CODE}: the add-on computed {d.requested_w:+.0f} W, "
|
||||
f"over the {abs(d.clamped_w):.0f} W rating, and cut it back to "
|
||||
f"{d.clamped_w:+.0f} W. The command this cycle was "
|
||||
f"{d.target_w:+.0f} W")
|
||||
return (f"{REASON_CODE}: computed {d.requested_w:+.0f} W, cut to the "
|
||||
f"{d.clamped_w:+.0f} W rating, command now {d.target_w:+.0f} W")
|
||||
if d.i_clamped:
|
||||
return (f"{REASON_CODE}: the correction the add-on builds up reached "
|
||||
f"its {d.i_bound_w:+.0f} W limit. The command this cycle was "
|
||||
f"its {d.i_bound_w:+.0f} W limit, command now "
|
||||
f"{d.target_w:+.0f} W")
|
||||
return ""
|
||||
|
||||
|
||||
def write_clamp_note(named) -> str:
|
||||
"""The write-side alarm text: `named` is `[(inverter name, ClampResult)]`
|
||||
for every inverter whose write was cut back this cycle."""
|
||||
return ". ".join(f"{name}: {r.describe()}" for name, r in named)
|
||||
"""The write-side alarm text: `named` is `[(inverter name or None,
|
||||
ClampResult)]` for every inverter whose write was cut back this cycle.
|
||||
|
||||
One inverter reads as `describe()`. Several read as one sentence of
|
||||
asked/sent pairs, so the line stays one sentence however many inverters
|
||||
there are. Which limit cut each one back is on its own action-log row
|
||||
(`describe()`) and in the notification."""
|
||||
if len(named) == 1:
|
||||
name, r = named[0]
|
||||
return f"{name}: {r.describe()}" if name else r.describe()
|
||||
return f"{REASON_CODE}: " + ", ".join(
|
||||
f"{name} asked {r.requested_w:+.0f} W and sent {r.applied_w:+.0f} W"
|
||||
for name, r in named)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -284,10 +292,12 @@ class ClampResult:
|
||||
bound: str = "" # "" | "max_w" | "device" - which stage bit
|
||||
|
||||
def describe(self) -> str:
|
||||
# The sent value IS the limit that bound, so the rating is not
|
||||
# repeated: on "max_w" it equals the rating, and on "device" the
|
||||
# rating was not what cut it back.
|
||||
return (f"{REASON_CODE}: asked {self.requested_w:+.0f} W and sent "
|
||||
f"{self.applied_w:+.0f} W, cut back by "
|
||||
f"{messages.LIMIT_WORDS.get(self.bound, self.bound)}, with a "
|
||||
f"{self.limit_w:.0f} W rating")
|
||||
f"{self.applied_w:+.0f} W, "
|
||||
f"{messages.LIMIT_WORDS.get(self.bound, self.bound)}")
|
||||
|
||||
|
||||
def is_commandable(value) -> bool:
|
||||
|
||||
@@ -594,10 +594,12 @@ class Freshness:
|
||||
# ("what did the register do" against "can I trust this entity"). One
|
||||
# helper serving both would have to drop whichever half the other does
|
||||
# not need, and the half it dropped would be the operator's next step.
|
||||
# UX-10: three plain sentences (test_messages.py part 6). SAFETY-56's
|
||||
# UX-10: two plain sentences (test_messages.py part 6). SAFETY-56's
|
||||
# two explanations stay, in this order, and so does the admission
|
||||
# that this check cannot tell them apart. The reading is treated as
|
||||
# missing either way, which the log line around this says.
|
||||
# missing either way, which the log line around this says. The
|
||||
# limits (`self.bound`) are on main.py's DEBUG line beside it, so the
|
||||
# logged line stays within 3 sentences.
|
||||
# ⚠️ SAFETY-57, AND THE THREE DURATIONS ARE THREE DIFFERENT FACTS, each
|
||||
# in its own slot: the register's unchanged time, the WINDOW this
|
||||
# verdict actually rests on after a release (`regraded`), and how many
|
||||
@@ -611,11 +613,7 @@ class Freshness:
|
||||
f"STALE_READING: {self.held:+.0f} W unchanged for {shown_s:.0f} s "
|
||||
f"while the command moved {moved_w:.0f} W to {standing_w:+.0f} W"
|
||||
f"{regraded}. The reading may have stopped, or the battery may be "
|
||||
f"unable to follow the command, and this check cannot tell which. "
|
||||
f"Limits: "
|
||||
f"unchanged over {self.bound.unchanged_s:.0f} s, command over "
|
||||
f"{self.bound.move_w:.0f} W and moved by over "
|
||||
f"{self.bound.move_w:.0f} W")
|
||||
f"unable to follow the command, and this check cannot tell which")
|
||||
|
||||
def unchanged_s(self, now_mono) -> float | None:
|
||||
"""Seconds since an arrival carried a different number. Status page.
|
||||
|
||||
@@ -164,9 +164,20 @@ class HomeAssistant:
|
||||
# value is rejected outright, not clamped. Always log it:
|
||||
# silently dropped commands are how a controller ends up
|
||||
# believing something the hardware never did.
|
||||
# UX-10: plain words. While Home Assistant loads its
|
||||
# entities it answers 400 with only "400: Bad Request",
|
||||
# which says nothing, so only a body with more is quoted.
|
||||
said = body.strip()[:200]
|
||||
if said.lower() in (f"{resp.status}: {resp.reason}".lower(),
|
||||
""):
|
||||
said = "no details"
|
||||
entity = (data.get("entity_id", "no entity")
|
||||
if isinstance(data, dict) else "no entity")
|
||||
self._moan(f"svc:{domain}.{service}",
|
||||
"service %s.%s rejected (%s): %s",
|
||||
domain, service, resp.status, body[:200])
|
||||
"Home Assistant refused %s.%s for %s (error %s, "
|
||||
"%s). The entity may still be loading, or the "
|
||||
"value is out of range.", domain, service,
|
||||
entity, resp.status, said)
|
||||
return False
|
||||
return True
|
||||
except aiohttp.ClientConnectionError as err:
|
||||
|
||||
@@ -612,8 +612,8 @@ class Controller:
|
||||
for inv in self.inverters:
|
||||
if inv.sign_wd.tripped:
|
||||
_LOG.error("%s stays at 0 W after the restart because its "
|
||||
"power went the wrong way: %s. Start control once "
|
||||
"you know why.", inv.name,
|
||||
"power went the wrong way, so find out why before "
|
||||
"you start control. %s.", inv.name,
|
||||
inv.sign_wd.reason or "(reason not recorded)")
|
||||
|
||||
# --- SAFETY-42 + SAFETY-34 -------------------------------------------
|
||||
@@ -635,9 +635,9 @@ class Controller:
|
||||
for inv in self.inverters:
|
||||
if inv.delivery_wd.tripped:
|
||||
_LOG.error(
|
||||
"%s is still marked as not delivering after the restart: "
|
||||
"%s. Control keeps running, and only stopping and starting "
|
||||
"control clears this.",
|
||||
"%s is still marked as not delivering after the restart "
|
||||
"while control keeps running, and only stopping and "
|
||||
"starting control clears this. %s.",
|
||||
inv.name, inv.delivery_wd.reason or "(reason not recorded)")
|
||||
|
||||
# --- SAFETY-30: THE SITE HALF OF THE SIGN CHECK ----------------------
|
||||
@@ -667,9 +667,9 @@ class Controller:
|
||||
reason=str(store.data.get("site_sign_reason", "") or ""))
|
||||
if self.site_sign_wd.tripped:
|
||||
_LOG.error(
|
||||
"Every inverter stays at 0 W after the restart because the "
|
||||
"total power went the wrong way: %s. Start control once you "
|
||||
"know why.",
|
||||
"After the restart every inverter stays at 0 W, as the total "
|
||||
"power went the wrong way, so find out why before starting "
|
||||
"control. %s.",
|
||||
self.site_sign_wd.reason or "(reason not recorded)")
|
||||
# And the per-unit detectors are told which question they are asking,
|
||||
# here as well as in `cycle()`. `checks()` and the status page read
|
||||
@@ -720,9 +720,9 @@ class Controller:
|
||||
self._announced_mono = {}
|
||||
if self.circ_wd.tripped:
|
||||
_LOG.error(
|
||||
"Every inverter stays at 0 W after the restart because the "
|
||||
"inverters worked against each other: %s. Start control once "
|
||||
"you know why.",
|
||||
"After the restart every inverter stays at 0 W, as the "
|
||||
"inverters worked against each other, so find out why before "
|
||||
"starting control. %s.",
|
||||
self.circ_wd.reason or "(reason not recorded)")
|
||||
|
||||
# SAFETY-03. The output clamp's alarm - see clamp.py for why the clamp
|
||||
@@ -1228,8 +1228,8 @@ class Controller:
|
||||
first.store_set("sign_reason", first.sign_wd.reason)
|
||||
first.store_set("sign_tripped", True)
|
||||
_LOG.error(
|
||||
"%s is set to 0 W again, because an older saved safety stop was "
|
||||
"found: %s. Start control once you know why.",
|
||||
"%s is set to 0 W again because an older saved safety stop was "
|
||||
"found, so find out why before you start control. %s.",
|
||||
first.name, first.sign_wd.reason)
|
||||
_LOG.debug(
|
||||
"SAFETY-02 latch guard: the pre-DEV-02 flat key says the "
|
||||
@@ -1953,11 +1953,18 @@ class Controller:
|
||||
# learns to skim it.
|
||||
# UX-08 2d: plain, with SAFETY-57's escape clause kept as an
|
||||
# ordered pair (time, then power), and the reason still in `%s`.
|
||||
_LOG.error("%s: battery power reading stopped changing. Inverters "
|
||||
"go to 0 W until it changes, or %.0f s after the "
|
||||
"command drops below %.0f W: %s.",
|
||||
# UX-10: one sentence, then the reason's two. Its limits are on
|
||||
# the DEBUG line below, to keep the line within 3 sentences.
|
||||
_LOG.error("%s: battery power reading stopped changing, set to "
|
||||
"0 W until it changes, or %.0f s after the command "
|
||||
"drops below %.0f W. %s.",
|
||||
inv.name, inv.batt_fresh.bound.release_s,
|
||||
inv.batt_fresh.bound.move_w, inv.batt_fresh.reason)
|
||||
_LOG.debug("%s: the limits are unchanged over %.0f s, a command "
|
||||
"over %.0f W and a move by over %.0f W.", inv.name,
|
||||
inv.batt_fresh.bound.unchanged_s,
|
||||
inv.batt_fresh.bound.move_w,
|
||||
inv.batt_fresh.bound.move_w)
|
||||
elif inv.batt_fresh.releases > was_releases:
|
||||
# ⚠️ SAFETY-57. REPORTED, NEVER SILENT. A latch that lets go on a
|
||||
# timer and says nothing is a latch no operator can audit - and this
|
||||
@@ -2187,8 +2194,8 @@ class Controller:
|
||||
self.log_event(
|
||||
f"{inv.name}: WRITE REFUSED {value:+.0f} W - commanding 0 W",
|
||||
hint=messages.hint("write_blocked", unit=inv.name))
|
||||
_LOG.error("Refused to send %+.0f W to %s (%s), so it is set to "
|
||||
"0 W instead. This is a software fault.",
|
||||
_LOG.error("Refused to send %+.0f W to %s, so it is set to 0 W "
|
||||
"instead, which is a software fault. The reason: %s.",
|
||||
value, inv.name, refusal)
|
||||
value = 0.0
|
||||
|
||||
@@ -2483,8 +2490,8 @@ class Controller:
|
||||
self.store.set("sign_tripped", True)
|
||||
self.log_event(f"FAILSAFE {inv.name}: " + inv.sign_wd.reason,
|
||||
hint=messages.hint("direction", unit=inv.name))
|
||||
_LOG.error("%s is set to 0 W because its power went the wrong "
|
||||
"way: %s. Only starting control clears this.",
|
||||
_LOG.error("%s is set to 0 W until control is started, "
|
||||
"because its power went the wrong way. %s.",
|
||||
inv.name, inv.sign_wd.reason)
|
||||
# ⚠️ THE SITE'S OWN STANDING COMMAND, WHICH IS THE SUM OF THE UNITS'.
|
||||
# SAFETY-07 and SAFETY-18 both grade a SITE quantity - the quarter-hour
|
||||
@@ -2531,9 +2538,9 @@ class Controller:
|
||||
self.store.set("site_sign_tripped", True)
|
||||
self.log_event("FAILSAFE site: " + self.site_sign_wd.reason,
|
||||
hint=messages.hint("site_direction"))
|
||||
_LOG.error("Every inverter is set to 0 W because the total "
|
||||
"power went the wrong way: %s. Only starting "
|
||||
"control clears this.", self.site_sign_wd.reason)
|
||||
_LOG.error("Every inverter is set to 0 W until control is "
|
||||
"started, because the total power went the wrong "
|
||||
"way. %s.", self.site_sign_wd.reason)
|
||||
|
||||
# --- SAFETY-31, AND IT RUNS BESIDE SAFETY-02 FOR THE SAME REASON -----
|
||||
# Evaluated EVERY cycle, unconditionally, above the "inputs missing but
|
||||
@@ -2564,8 +2571,8 @@ class Controller:
|
||||
self.log_event("CIRCULATING POWER - " + self.circ_wd.reason,
|
||||
hint=messages.hint("circulating"))
|
||||
_LOG.error(
|
||||
"Every inverter is set to 0 W because the inverters worked "
|
||||
"against each other: %s. Only starting control clears this.",
|
||||
"Every inverter is set to 0 W until control is started, "
|
||||
"because the inverters worked against each other. %s.",
|
||||
self.circ_wd.reason)
|
||||
|
||||
# --- SAFETY-42, AND IT RUNS HERE FOR SAFETY-02's AND SAFETY-31's REASON
|
||||
@@ -2672,9 +2679,9 @@ class Controller:
|
||||
hint=messages.hint("not_delivering",
|
||||
unit=inv.name))
|
||||
_LOG.error(
|
||||
"%s is not delivering: %s. Nothing was changed and control "
|
||||
"keeps running, and only stopping and starting control "
|
||||
"clears this.", inv.name, inv.delivery_wd.reason)
|
||||
"%s is not delivering while control keeps running, and "
|
||||
"only stopping and starting control clears this. %s.",
|
||||
inv.name, inv.delivery_wd.reason)
|
||||
_LOG.debug(
|
||||
"SAFETY-42 reports and does not act: commanding a unit "
|
||||
"that is not listening achieves nothing (HW-01), and the "
|
||||
@@ -3522,7 +3529,7 @@ class Controller:
|
||||
# other unit never gets. Loud, every cycle, and it does not change
|
||||
# the resolution - the arbiter already routed by `actuator`.
|
||||
_LOG.error("A limit was filed on the wrong inverter, which is a "
|
||||
"software fault: %s", " / ".join(resolution.scope_errors))
|
||||
"software fault. %s.", " / ".join(resolution.scope_errors))
|
||||
if resolution.contradiction:
|
||||
self.log_event("ARBITER CONTRADICTION - commanding 0 W, see the log",
|
||||
hint=messages.hint("clash"))
|
||||
@@ -3814,9 +3821,10 @@ class Controller:
|
||||
# SAFETY-40: only an over-rating is a bug; the integrator bound is not.
|
||||
bug = engaged_write or loop_bug
|
||||
if engaged_write:
|
||||
reason = write_clamp_note([(inv.name, inv.clamp_last)
|
||||
for inv in clamped_units])
|
||||
_named = len(self.inverters) > 1
|
||||
reason = write_clamp_note([(inv.name if _named else None,
|
||||
inv.clamp_last)
|
||||
for inv in clamped_units])
|
||||
facts = [messages.Clamp(inv.name if _named else None,
|
||||
inv.clamp_last.requested_w,
|
||||
inv.clamp_last.applied_w,
|
||||
@@ -3872,8 +3880,8 @@ class Controller:
|
||||
messages.loop_bound().data())
|
||||
return
|
||||
_LOG.error(
|
||||
"Power request too high, which is a software fault: %s. Control is "
|
||||
"still running. Cut back %s so far.",
|
||||
"Power request too high, a software fault: %s. Cut back %s so "
|
||||
"far, control runs on.",
|
||||
self.clamp_wd.reason, messages.times(self.clamp_wd.engagements))
|
||||
_LOG.debug(
|
||||
"SAFETY-03: silently clipping this is how a winding integrator "
|
||||
@@ -3922,9 +3930,8 @@ class Controller:
|
||||
self.log_event("PEAK_GUARD_SATURATED - " + g.reason,
|
||||
hint=messages.hint("peak_saturated"))
|
||||
_LOG.error(
|
||||
"The battery is asked for all it can give, but this 15-minute "
|
||||
"period still goes over the peak limit: %s. Switch off large "
|
||||
"loads.", g.reason)
|
||||
"Switch off large loads, the battery alone cannot keep this "
|
||||
"15-minute period under the peak limit. %s.", g.reason)
|
||||
# A fixed notification_id, and a DIFFERENT one from every other alarm
|
||||
# here - check_meter_frozen() gives the reasoning. A saturated peak guard
|
||||
# is not a frozen meter and is not an out-of-range setpoint.
|
||||
@@ -3980,9 +3987,8 @@ class Controller:
|
||||
# reason carries the word in the middle of a sentence, not at the front.
|
||||
self.log_event(g.reason, hint=messages.hint("peak_reserve"))
|
||||
_LOG.error(
|
||||
"The peak guard may not draw the battery lower, so this 15-minute "
|
||||
"period will go over the peak limit: %s. Switch off large loads.",
|
||||
g.reason)
|
||||
"Switch off large loads, the peak guard may not draw the battery "
|
||||
"lower. %s.", g.reason)
|
||||
# A fixed notification_id, and a DIFFERENT one from every other alarm
|
||||
# here - check_meter_frozen() gives the reasoning. A battery at its
|
||||
# energy floor is not a saturated inverter and is not a frozen meter,
|
||||
@@ -4142,8 +4148,8 @@ class Controller:
|
||||
hint=messages.hint("meter_frozen"))
|
||||
_LOG.error(
|
||||
"The meter shows the same value while the battery is asked for "
|
||||
"full power: %s. Control still follows it, so check the meter.",
|
||||
self.freeze_wd.reason)
|
||||
"full power, and control still follows it, so check the meter. "
|
||||
"%s.", self.freeze_wd.reason)
|
||||
_LOG.debug(
|
||||
"SAFETY-18: nothing else stops this. The meter still answers, so "
|
||||
"its age is fresh, the firmware watchdog is fed and the arbiter "
|
||||
@@ -4257,10 +4263,10 @@ class Controller:
|
||||
and standing is not None and standing.from_feed)
|
||||
if keep:
|
||||
_LOG.warning(
|
||||
"The price update was incomplete (%s), so the plan "
|
||||
"from %s is kept until it is %s old.",
|
||||
plan.words, messages.when(brussels(self.tariff_at)),
|
||||
messages.duration(TARIFF_MAX_AGE_S))
|
||||
"The price update was incomplete, so the plan from %s "
|
||||
"is kept until it is %s old. The update held %s.",
|
||||
messages.when(brussels(self.tariff_at)),
|
||||
messages.duration(TARIFF_MAX_AGE_S), plan.words)
|
||||
else:
|
||||
self.tariff_plan, self.tariff_at = plan, now
|
||||
_LOG.info("tariff refreshed: %s", plan.words)
|
||||
|
||||
@@ -414,8 +414,8 @@ CEILING_MONTH_PEAK = "this month's peak"
|
||||
# SAFETY-24. Why the forced discharge may not be sized. Reasons, not booleans,
|
||||
# for BLIND_*'s reason: the status page, the action log and the operator
|
||||
# notification all have to say WHICH, and "floor-limited" alone is unactionable.
|
||||
FLOOR_SOC_UNREADABLE = ("the battery level was not read this cycle, and an "
|
||||
"unread level is not a full battery")
|
||||
FLOOR_SOC_UNREADABLE = ("the battery level was not read this cycle, and "
|
||||
"unread is not full")
|
||||
# ⚠️ SAFETY-25. "the peak reserve", not "the SoC floor", and the name is
|
||||
# load-bearing: this string is what the action log and the add-on log print,
|
||||
# and `peak_soc_floor` (“Peak reserve”) bounds THIS GUARD ONLY. Widening it to
|
||||
@@ -818,8 +818,8 @@ def decide(projection, *, batt_w, cap_w, reading, achievable_w, soc, soc_floor,
|
||||
+ ("" if soc is None else f", at {float(soc):.0f} %")
|
||||
+ ("" if soc is None or soc_floor is None
|
||||
else f" against {float(soc_floor):.0f} %")
|
||||
+ ". So there is no forced discharge and no charging, floor "
|
||||
"0 W, and this period will go over the limit"))
|
||||
+ ", so no forced discharge and no charging, and this period "
|
||||
"will go over the limit"))
|
||||
|
||||
floor = min(wanted, ceil_top)
|
||||
saturated = floor < wanted
|
||||
@@ -833,8 +833,10 @@ def decide(projection, *, batt_w, cap_w, reading, achievable_w, soc, soc_floor,
|
||||
f"{QUARTER_S - projection.elapsed_s:.0f} s left, so {ex:.0f} W must "
|
||||
f"come off the meter. The battery measured {float(batt_w):+.0f} W "
|
||||
f"and is asked for {floor:+.0f} W"
|
||||
+ (f". {wanted:+.0f} W was needed, but the inverters give at most "
|
||||
f"{ceil_top:.0f} W" if saturated else "")))
|
||||
# Saturated means floor == ceil_top: the ask IS the most the
|
||||
# inverters give, so one clause says both.
|
||||
+ (f", the most the inverters give, but {wanted:+.0f} W was needed"
|
||||
if saturated else "")))
|
||||
|
||||
|
||||
def charge_headroom_w(decision) -> float | None:
|
||||
|
||||
@@ -591,9 +591,9 @@ def _walk(series, hours, action) -> Gate:
|
||||
return Gate.block(
|
||||
f"{action.name} needs prices for {action.commits_h:g} hours "
|
||||
f"from {messages.when(brussels(hours[0]))}, and the price for "
|
||||
f"{messages.when(brussels(u))} is only estimated, from "
|
||||
f"yesterday or from the fixed tariff. It can start once the "
|
||||
f"day-ahead prices are published, around 14:00 to 16:00")
|
||||
f"{messages.when(brussels(u))} is only estimated. It can start "
|
||||
f"once the day-ahead prices are published, around 14:00 to "
|
||||
f"16:00")
|
||||
pts.append(p)
|
||||
return Gate.permit(action, pts)
|
||||
|
||||
|
||||
@@ -1358,12 +1358,12 @@ def _degrade(opts, read, since, until, stamp) -> TariffPlan:
|
||||
"no tariff at all.", e)
|
||||
profile = None
|
||||
if profile is None:
|
||||
_LOG.warning("The price feed failed: %s. No fixed tariff is set to "
|
||||
"fall back to, so trading is off. Peak limiting and home "
|
||||
"use go on.", read.reason)
|
||||
_LOG.warning("The price feed failed and no fixed tariff is set, so "
|
||||
"trading is off, while peak limiting and home use go on. "
|
||||
"%s.", read.reason)
|
||||
return TariffPlan(read, None, "none")
|
||||
_LOG.warning("The price feed failed: %s. The fixed tariff is used "
|
||||
"instead, marked as estimated, and trading is off.",
|
||||
_LOG.warning("The price feed failed, so the fixed tariff is used "
|
||||
"instead, marked as estimated, and trading is off. %s.",
|
||||
read.reason)
|
||||
points = profile.series(since, until, provisional=True)
|
||||
return TariffPlan(
|
||||
|
||||
Reference in New Issue
Block a user