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:
@@ -862,7 +862,7 @@ DATA = {
|
||||
# hass.py: the _moan() wrapper and what its callers pass
|
||||
"hass.py:msg", "hass.py:*args", "hass.py:entity_id", "hass.py:err",
|
||||
"hass.py:obj", "hass.py:raw", "hass.py:domain", "hass.py:service",
|
||||
"hass.py:resp.status", "hass.py:body[:200]", "hass.py:what",
|
||||
"hass.py:resp.status", "hass.py:said", "hass.py:entity", "hass.py:what",
|
||||
# main.py
|
||||
"main.py:'cancelled' if task.cancelled() else repr(exc) if exc else 'returned'",
|
||||
"main.py:EXIT_CONTROL_DIED", "main.py:first.name", "main.py:OPTIONS_PATH",
|
||||
@@ -975,7 +975,9 @@ def _clamp_texts():
|
||||
i_w=4500.0)
|
||||
write = [(n, _clamp.clamp_to_rating(w, 5000.0, -2500.0, 2500.0))
|
||||
for n, w in (("inverter-a", 3000.0), ("inverter-b", -7412.0))]
|
||||
# One inverter is not named (main.py passes None), several are.
|
||||
return [_clamp.loop_clamp_note(over), _clamp.loop_clamp_note(at),
|
||||
_clamp.write_clamp_note([(None, write[0][1])]),
|
||||
_clamp.write_clamp_note(write[:1]), _clamp.write_clamp_note(write)]
|
||||
|
||||
|
||||
@@ -1098,6 +1100,20 @@ def reason_problems(text):
|
||||
return out + line_problems(text)
|
||||
|
||||
|
||||
def logged_shape(line):
|
||||
"""UX-10 review: the line as logged, wrapper and reason together, has the
|
||||
red-alarm shape too. The reason alone passing is not enough: the 2d-style
|
||||
"title: reason. tail. tail." line read as 4 sentences with a 3-sentence
|
||||
reason inside it."""
|
||||
out = []
|
||||
if sentences(line) > 3:
|
||||
out.append(f"over 3 sentences ({sentences(line)})")
|
||||
long = [x for x in re.split(r"(?<=[.!?])\s+", line) if len(words(x)) > 30]
|
||||
if long:
|
||||
out.append(f"a sentence over 30 words ({len(words(long[0]))})")
|
||||
return out
|
||||
|
||||
|
||||
def line_problems(text):
|
||||
"""What no WARNING+ line may carry, whatever its length."""
|
||||
out = []
|
||||
@@ -1145,7 +1161,7 @@ def scan_log_lines(files, text=None, data=None):
|
||||
lambda m: next(slots, m.group(0)), template)
|
||||
line = PLACEHOLDER.sub("X", line).replace("\0", sample)
|
||||
p += [f"as logged: {x}" for x in line_problems(line)
|
||||
if x not in p]
|
||||
+ logged_shape(line) if x not in p]
|
||||
if p:
|
||||
bad.append((key, sample, p))
|
||||
unknown = sorted(seen - set(data) - set(text))
|
||||
@@ -1174,6 +1190,36 @@ check(f"no WARNING+ line puts a plural noun after a bare count, which reads "
|
||||
f"'1 times' (use messages.times() and friends; found: {_counts})",
|
||||
not _counts)
|
||||
|
||||
# UX-10 review: the two clamp lines the owner quoted, as logged for one
|
||||
# inverter, fit the red shape the tight way: at most 3 sentences and 30 words
|
||||
# in all, code and numbers kept. The real literals from main.py, filled with
|
||||
# the real notes.
|
||||
def _clamp_line(opening):
|
||||
for n in ast.walk(dict(_FILES)["main.py"]):
|
||||
if isinstance(n, ast.Call) and LOG_CALL.search(ast.unparse(n.func)) \
|
||||
and n.args and (render(n.args[0]) or "").startswith(opening):
|
||||
return n.args[0].value
|
||||
return None
|
||||
|
||||
|
||||
_over, _at, _w1, _w1n, _w2 = _clamp_texts()
|
||||
_clamp_lines = {
|
||||
"error, loop": (_clamp_line("Power request too high"), _over),
|
||||
"error, write": (_clamp_line("Power request too high"), _w1),
|
||||
"warning, loop": (_clamp_line("The battery gives less than asked"), _at),
|
||||
}
|
||||
_clamp_shown = {k: (f % (r, messages.times(1)) if f else None)
|
||||
for k, (f, r) in _clamp_lines.items()}
|
||||
for k, v in _clamp_shown.items():
|
||||
print(f" {k}: {v!r} ({len(words(v or ''))} words)")
|
||||
check("the clamp lines for one inverter are at most 3 sentences and 30 words, "
|
||||
"and keep OUTPUT_CLAMP and the numbers",
|
||||
all(v and sentences(v) <= 3 and len(words(v)) <= 30
|
||||
and "OUTPUT_CLAMP" in v and "1 time so far" in v
|
||||
for v in _clamp_shown.values())
|
||||
and all(x in _clamp_shown["error, loop"]
|
||||
for x in ("+9000 W", "+5000 W", "+2000 W", "software fault")))
|
||||
|
||||
# Mutations: the scan catches what the 2d scan missed.
|
||||
_OLD_CLAMP = ("OUTPUT_CLAMP: the loop computed +2148 W, over the 2000 W rating, "
|
||||
"and it was clamped to +2000 W; the loop's command this cycle was "
|
||||
@@ -1197,6 +1243,13 @@ check("...and '1 times', '(s)', a semicolon and a doubled full stop in a "
|
||||
"Cut back to +2000 W.. Only this.",
|
||||
"index 0, 1, .... Those are left out.",
|
||||
"moved to one part: . Problems")))
|
||||
check("...and so is the 2d-style line, a 2-sentence reason inside a title and "
|
||||
"two tails, which reads as 4 sentences although the reason passes",
|
||||
logged_shape("Power request too high: OUTPUT_CLAMP: cut to +5000 W. The "
|
||||
"command was +1000 W. Control is still running. Cut back "
|
||||
"1 time so far.")
|
||||
and not reason_problems("OUTPUT_CLAMP: cut to +5000 W. The command was "
|
||||
"+1000 W"))
|
||||
check("...while '1 s' and '1 time' are fine",
|
||||
not line_problems("Held for 1 s, cut back 1 time so far."))
|
||||
# INFO lines are not held to the length rule, but the 2d test found "§8.3",
|
||||
@@ -1317,6 +1370,56 @@ check("...and the library's own text is kept, at DEBUG",
|
||||
and not any("8:1000" in m or "Bad Gateway" in m for m in _warn + _info))
|
||||
|
||||
|
||||
class _Resp400:
|
||||
def __init__(self, body):
|
||||
self.status, self.reason, self._body = 400, "Bad Request", body
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *a):
|
||||
return False
|
||||
|
||||
async def text(self):
|
||||
return self._body
|
||||
|
||||
|
||||
class _Session400:
|
||||
def __init__(self, body):
|
||||
self.body = body
|
||||
|
||||
def post(self, *a, **k):
|
||||
return _Resp400(self.body)
|
||||
|
||||
|
||||
def _refused(body):
|
||||
ha = _hass.HomeAssistant(_Session400(body), token="t")
|
||||
ok = []
|
||||
recs = _captured("goodwe.hass", lambda: ok.append(asyncio.run(
|
||||
ha.call("number", "set_value", {"entity_id": "number.sp", "value": 1}))))
|
||||
return (ok[0] if ok else None), [r.getMessage() for r in recs
|
||||
if r.levelno == logging.WARNING]
|
||||
|
||||
|
||||
def _one(res):
|
||||
"""(write result, the one WARNING line) or a named absence that fails."""
|
||||
ok, lines = res
|
||||
return ok, (lines[0] if len(lines) == 1 else f"<{len(lines)} lines>")
|
||||
|
||||
|
||||
_bare_ok, _bare = _one(_refused("400: Bad Request"))
|
||||
_full_ok, _full = _one(_refused(
|
||||
'{"message": "Value 6000 for number.sp is outside valid range"}'))
|
||||
check(f"a write Home Assistant refuses while it loads is one plain WARNING, "
|
||||
f"still a failed write, without the empty '400: Bad Request' ({_bare!r})",
|
||||
_bare_ok is False
|
||||
and _bare.startswith("Home Assistant refused number.set_value for "
|
||||
"number.sp (error 400, no details).")
|
||||
and "Bad Request" not in _bare)
|
||||
check(f"...while a body that says more is still quoted ({_full!r})",
|
||||
_full_ok is False and "outside valid range" in _full)
|
||||
|
||||
|
||||
class _WS:
|
||||
def __init__(self, kind):
|
||||
self.kind = kind
|
||||
|
||||
Reference in New Issue
Block a user