UX-10: the last plain-language leftovers (log lines, two table rows, 'Why: Other', red at 0 s) #138

Merged
admin merged 3 commits from UX-10-plain-leftovers into release/1.0 2026-09-27 12:18:23 +02:00
Owner

Trello UX-10 (P2): the last plain-language leftovers from the UX-08 2d test (/home/homelab/UX-08-2d-test-report.md). The choices are recorded in DECISIONS/UX-08-short-notifications.md, section UX-10.

Why the 2d allowlist scan missed the clamp lines

Part 3 of test_messages.py reads a log call's own literal and counts %s as one word. The clamp lines are "Power request too high, which is a software fault: %s …", and the text inside %s was built somewhere else, by loop_clamp_note() / ClampResult.describe(). The scan passed the literal and never saw the text that was logged. The same held for every quoted reason (STALE_READING was 129 words, in capitals).

New part 6 closes the gap:

  • Every dynamic part of every WARNING+ line in app/ is classified, either as data (a number, a name, a path, a library error) or as a built text. A new part that is in neither list fails.
  • Every built text is produced by the real code (watchdogs, decide(), loop_clamp_note(), MigrationReport.explain()), or, for a family of builders, by every template of it (PriceRead.no_data, Gate.block). The text and the whole line as logged are then checked.
  • Mutations show it works. The 2d clamp text, injected, is caught on both clamp lines. A new line quoting an unclassified self.new_wd.reason is caught. "1 times", "(s)", semicolons and doubled full stops are caught.

A quoted reason keeps its code and gets STYLE.md's red-alarm shape (3 sentences of at most 30 words). INFO literals may not carry §n, ISO timestamps or capitals.

Before / after

The "after" texts are real output from the test suite, or from a capture of every WARNING+ line the tests log.

Before After
Clamp ERROR …: 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 +440 W. This is a BUG SIGNAL … Cut back 1 times so far. Power request too high, which is a software fault: OUTPUT_CLAMP: the add-on computed +5400 W, over the 5000 W rating, and cut it back to +5000 W. The command this cycle was +1000 W. Control is still running. Cut back 1 time so far.
Clamp WARNING …: OUTPUT_CLAMP: the loop's integrator reached its +2000 W anti-windup bound; … Held back 1 times so far. The battery gives less than asked: OUTPUT_CLAMP: the correction the add-on builds up reached its +2000 W limit. The command this cycle was +440 W. Held back 1 time so far.
Price history row 2026-09-26 (96 prices over 24 hours); 2026-09-27 (…) 96 prices over 24 hours for 14 Jan (test_price_history.py). Several days are joined by commas.
Tariff row …160 published and 8 estimated, 900+3600 s apart, not final yet …160 published and 8 estimated, one every 15 minutes or every hour
Why (latched stop) Why: Other: 0 W Safety stop, power went the wrong way: 0 W
Meter row at start red Meter: no reading for 0 seconds, limit 30 seconds… amber Meter: waiting for the first reading, limit 30 seconds (red past the limit, as before)
Why, reading missing A reading is missing for 0 seconds, control waits (frozen until the next cycle, up to 15 s) the age counts: …for 7 seconds, control waits
Shadow, every 2 s INFO SHADOW MODE: not commanding +220 W to inverter-a … DEBUG Shadow mode: would send +220 W to inverter-a, and holds +0 W. …
Shadow on/off WARNING INFO (same text)
Page polls INFO aiohttp.access … GET /status only at log level debug
static_profile warning twice per start once
HA restart read sensor.x failed: 502, message='Bad Gateway', url=URL(…), P1 HA websocket: Received message 8:1000 is not str, … one line: Home Assistant is not answering (error 502), perhaps it is restarting. The add-on keeps trying. and at INFO Home Assistant answers again. Raw text on DEBUG.
INFO §8.3 price history: …, closed a stale block from 2026-09-27T06:30:00+00:00 … banked, …keys ARE that pack's history… Price history: …, closed the 15-minute period from 1 Sep at 12:15 at 1322 W, partly measured, and saved it (test_tariff.py), one inverter, so its maintenance history is read where it is and nothing moves
Error texts NOT_OBSERVED, did NOT land, down AND static_profile plain sentences

Other reasons rewritten to the same rule: METER_FROZEN, CIRCULATING_POWER, STALE_READING (SAFETY-56's two explanations and "cannot tell which" kept, in order), PEAK_GUARD / PEAK_GUARD_FLOOR_LIMITED / PEAK_GUARD_BLIND (the codes are now single tokens), the migration report, and the price feed, price history and trading-gate reasons.

Display bug causes

  • Why: Other. main.py turns a safety winner into its claim's reason, and "sign disagreement" (and "unsplit") had no entry in messages.WHY_WINNER. The mapping is now main.why_key(). Part 7 finds every Claim.set() in the code and fails if any key it can produce has no words.
  • Red at 0 s. Until the first reading arrives, P1Ingest.stale is true, and the age is the time since start, so the row was red at 0 s inside its own 30 s limit. Colour change, justified: red at 0 s contradicted the limit the same row prints, and once readings exist the row is red only past that limit. It is amber, not green, because control waits for the reading. stale itself, which gates control, is unchanged.
  • Stuck at 0. With the meter reading gone, grid stops changing, so run_control() runs a cycle only every cycle_floor_s, and the line was computed once per cycle. The age is now counted when the page asks. The hold and its escalation are unchanged.

Unchanged

Safety behaviour, alarm log levels (WARNING/ERROR), notification ids, triggers, cadence, and the banner cascade are unchanged. All 36 test files pass locally in the CI image.

Companion: admin/goodwe#61 (the resolved witness push, "after a gap" → "after no answers from"). A pin bump follows that one.

🤖 Generated with Claude Code

Trello UX-10 (P2): the last plain-language leftovers from the UX-08 2d test (`/home/homelab/UX-08-2d-test-report.md`). The choices are recorded in `DECISIONS/UX-08-short-notifications.md`, section UX-10. ## Why the 2d allowlist scan missed the clamp lines Part 3 of `test_messages.py` reads a log call's own literal and counts `%s` as one word. The clamp lines are `"Power request too high, which is a software fault: %s …"`, and the text inside `%s` was built somewhere else, by `loop_clamp_note()` / `ClampResult.describe()`. The scan passed the literal and never saw the text that was logged. The same held for every quoted reason (STALE_READING was 129 words, in capitals). **New part 6** closes the gap: - Every dynamic part of every WARNING+ line in `app/` is classified, either as data (a number, a name, a path, a library error) or as a built text. A new part that is in neither list fails. - Every built text is produced by the real code (watchdogs, `decide()`, `loop_clamp_note()`, `MigrationReport.explain()`), or, for a family of builders, by every template of it (`PriceRead.no_data`, `Gate.block`). The text and the whole line as logged are then checked. - Mutations show it works. The 2d clamp text, injected, is caught on both clamp lines. A new line quoting an unclassified `self.new_wd.reason` is caught. "1 times", "(s)", semicolons and doubled full stops are caught. A quoted reason keeps its code and gets STYLE.md's red-alarm shape (3 sentences of at most 30 words). INFO literals may not carry `§n`, ISO timestamps or capitals. ## Before / after The "after" texts are real output from the test suite, or from a capture of every WARNING+ line the tests log. | | Before | After | |---|---|---| | Clamp ERROR | `…: 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 +440 W. This is a BUG SIGNAL … Cut back 1 times so far.` | `Power request too high, which is a software fault: OUTPUT_CLAMP: the add-on computed +5400 W, over the 5000 W rating, and cut it back to +5000 W. The command this cycle was +1000 W. Control is still running. Cut back 1 time so far.` | | Clamp WARNING | `…: OUTPUT_CLAMP: the loop's integrator reached its +2000 W anti-windup bound; … Held back 1 times so far.` | `The battery gives less than asked: OUTPUT_CLAMP: the correction the add-on builds up reached its +2000 W limit. The command this cycle was +440 W. Held back 1 time so far.` | | Price history row | `2026-09-26 (96 prices over 24 hours); 2026-09-27 (…)` | `96 prices over 24 hours for 14 Jan` (test_price_history.py). Several days are joined by commas. | | Tariff row | `…160 published and 8 estimated, 900+3600 s apart, not final yet` | `…160 published and 8 estimated, one every 15 minutes or every hour` | | Why (latched stop) | `Why: Other: 0 W` | `Safety stop, power went the wrong way: 0 W` | | Meter row at start | red `Meter: no reading for 0 seconds, limit 30 seconds…` | amber `Meter: waiting for the first reading, limit 30 seconds` (red past the limit, as before) | | Why, reading missing | `A reading is missing for 0 seconds, control waits` (frozen until the next cycle, up to 15 s) | the age counts: `…for 7 seconds, control waits` | | Shadow, every 2 s | INFO `SHADOW MODE: not commanding +220 W to inverter-a …` | DEBUG `Shadow mode: would send +220 W to inverter-a, and holds +0 W. …` | | Shadow on/off | WARNING | INFO (same text) | | Page polls | INFO `aiohttp.access … GET /status` | only at log level debug | | static_profile warning | twice per start | once | | HA restart | `read sensor.x failed: 502, message='Bad Gateway', url=URL(…)`, `P1 HA websocket: Received message 8:1000 is not str`, … | one line: `Home Assistant is not answering (error 502), perhaps it is restarting. The add-on keeps trying.` and at INFO `Home Assistant answers again.` Raw text on DEBUG. | | INFO | `§8.3 price history: …`, `closed a stale block from 2026-09-27T06:30:00+00:00 … banked`, `…keys ARE that pack's history…` | `Price history: …`, `closed the 15-minute period from 1 Sep at 12:15 at 1322 W, partly measured, and saved it` (test_tariff.py), `one inverter, so its maintenance history is read where it is and nothing moves` | | Error texts | `NOT_OBSERVED`, `did NOT land`, `down AND static_profile` | plain sentences | Other reasons rewritten to the same rule: METER_FROZEN, CIRCULATING_POWER, STALE_READING (SAFETY-56's two explanations and "cannot tell which" kept, in order), PEAK_GUARD / PEAK_GUARD_FLOOR_LIMITED / PEAK_GUARD_BLIND (the codes are now single tokens), the migration report, and the price feed, price history and trading-gate reasons. ## Display bug causes - **Why: Other.** `main.py` turns a safety winner into its claim's reason, and "sign disagreement" (and "unsplit") had no entry in `messages.WHY_WINNER`. The mapping is now `main.why_key()`. Part 7 finds every `Claim.set()` in the code and fails if any key it can produce has no words. - **Red at 0 s.** Until the first reading arrives, `P1Ingest.stale` is true, and the age is the time since start, so the row was red at 0 s inside its own 30 s limit. **Colour change, justified:** red at 0 s contradicted the limit the same row prints, and once readings exist the row is red only past that limit. It is amber, not green, because control waits for the reading. `stale` itself, which gates control, is unchanged. - **Stuck at 0.** With the meter reading gone, `grid` stops changing, so `run_control()` runs a cycle only every `cycle_floor_s`, and the line was computed once per cycle. The age is now counted when the page asks. The hold and its escalation are unchanged. ## Unchanged Safety behaviour, alarm log levels (WARNING/ERROR), notification ids, triggers, cadence, and the banner cascade are unchanged. All 36 test files pass locally in the CI image. Companion: admin/goodwe#61 (the resolved witness push, "after a gap" → "after no answers from"). A pin bump follows that one. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
admin added 1 commit 2026-09-27 10:59:10 +02:00
UX-10: the last plain-language leftovers: log lines, two table rows, 'Why: Other', red at 0 s
checks / unit (pull_request) Successful in 9m3s
infra-pin / fresh (pull_request) Successful in 2s
23a3a3f5b5
- test_messages.py part 6: every dynamic part of a WARNING+ line is classified
  as data or as a built text, and every built text is produced by the real
  code and checked. This is the gap the 2d scan had: it counted `%s` as one
  word, so reasons built by loop_clamp_note() and friends were never seen.
- Quoted reasons rewritten to STYLE.md (red-alarm shape, codes kept):
  OUTPUT_CLAMP, METER_FROZEN, CIRCULATING_POWER, STALE_READING, the peak
  guard (codes now PEAK_GUARD*), the migration report, price feed, price
  history and gate reasons. Plurals via messages.times()/count().
- Top table: Price history "96 prices over 24 hours for 26 Sep", Tariff
  "one every 15 minutes or every hour", "not final yet" dropped.
- Levels and noise: per-cycle shadow line DEBUG, shadow on/off INFO,
  aiohttp access log only at log level debug, settings warnings once per
  start, one plain line per Home Assistant outage (hass.Outage).
- INFO jargon and error texts: no "§8.3", ISO times, "banked", "keys ARE",
  "NOT_OBSERVED", "did NOT land", "AND".
- Display bugs: WHY_WINNER covers every winner and safety reason
  (main.why_key(), part 7 finds them in the code); the meter row is amber
  "waiting for the first reading" inside its limit after a start; the
  missing-reading age is counted when the page asks.
- STYLE.md and DECISIONS/UX-08-short-notifications.md (UX-10 section).

No change to safety behaviour, alarm levels, notification ids, triggers,
cadence or the banner cascade. One row colour changes (meter at start),
justified in DECISIONS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
admin added 1 commit 2026-09-27 11:03:33 +02:00
UX-10: two INFO lines read better on TEST-23 (hours count, maintenance history)
checks / unit (pull_request) Successful in 9m2s
infra-pin / fresh (pull_request) Successful in 2s
f1a660881c
"11 of 48 hours use yesterday's prices", and the maintenance-history line no
longer says "maintenance history" twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
admin added 1 commit 2026-09-27 12:08:46 +02:00
UX-10 review: the line as logged gets the red-alarm shape, clamp lines at 30 words, plain HA 400
checks / unit (pull_request) Successful in 9m3s
infra-pin / fresh (pull_request) Successful in 2s
e0b578756f
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>
Author
Owner

Review (UX-10), round 1

1. The owner's bar on the new log lines: not met as logged. Fixed in a pushed commit.

Part 6 checked the quoted reason alone for the red-alarm shape. The line as logged was never checked. So a title, a 2-sentence reason and two tails still went out as 4+ sentences:

Power request too high, which is a software fault: OUTPUT_CLAMP: the add-on computed +9000 W, over the 5000 W rating, and cut it back to +5000 W. The command this cycle was +2000 W. Control is still running. Cut back 1 time so far.

That is 4 sentences and 43 words. With the check added, 25 lines failed it: the clamp lines, stale reading, the peak guard, circulation, the price feed fallback (4 sentences), and direction, delivery, frozen meter, allocator, arbiter and gate (a sentence over 30 words).

Fix (pushed):

  • Part 6 now also checks the whole line: at most 3 sentences, none over 30 words. A mutation shows that the 2d-style line is caught although its reason passes.
  • Each wrapper is now one sentence.
  • The circulation and peak guard reasons are now two sentences.
  • STALE_READING's fixed limits moved to a DEBUG line beside the ERROR. The release clause, the two explanations and "cannot tell which" stay, as test_freshness pins them.
  • The clamp lines for one inverter are pinned at ≤ 3 sentences and ≤ 30 words, with the code and the numbers kept:
    • Power request too high, a software fault: OUTPUT_CLAMP: computed +9000 W, cut to the +5000 W rating, command now +2000 W. Cut back 1 time so far, control runs on. (30)
    • Power request too high, a software fault: OUTPUT_CLAMP: asked +3000 W and sent +2500 W, the inverter's own limit. Cut back 1 time so far, control runs on. (28)
    • The battery gives less than asked: OUTPUT_CLAMP: the correction the add-on builds up reached its +5000 W limit, command now +2000 W. Held back 1 time so far. (28)
  • Several clamped inverters now read as one sentence of asked/sent pairs, so 3 or more inverters no longer make 4+ sentences. The limit that cut back each inverter is still on its own action-log row and in the notification.
  • describe() drops the rating: the sent value is the limit that bound (it equals the rating for max_w, and the rating did not bind for device).

Not done, follow-up: some lines are still over 30 words in all, up to about 74. These are direction, delivery, frozen meter, stale and peak. They meet the 3-sentence and 30-per-sentence shape. Shortening them further means changing reasons that are persisted in state.json (direction, delivery), and the direction reason is pinned byte for byte in test_watchdog.

2. Safety

  • Strings blanked, logic diffed (AST, every string set to ''). The logic changes are only these:

    • why_key(), a pure extraction;
    • the why_words property;
    • the meter-row branch;
    • Outage and its calls, and _receive_json;
    • the _WarnOnce filter;
    • access_log();
    • write_clamp_note();
    • shadow INFO→DEBUG and shadow on/off WARNING→INFO;
    • count()/when()/day_words()/spacing_words().

    No trigger, latch, claim, threshold, notification id or cadence changed. No alarm log level changed. The two shadow lines are not alarms.

  • Shadow line to DEBUG: nothing an operator needs is lost at INFO. The "Shadow mode is on/off" INFO line, the banner, the Shadow mode row, and the action log's "SHADOW: would have commanded N W" still show it. DOCS §Shadow mode points to the status page and action log, not the add-on log. Only the per-inverter split of the would-be command is DEBUG-only now. The site total is on the row and in the action log.

  • Access log: only web.start() uses it. Nothing in admin/goodwe or /home/homelab/*.sh greps aiohttp.access or GET /status.

  • Meter row:

    • P1Ingest.stale is untouched, and it gates control.
    • Amber shows only while age_s() is None, which means no reading ever arrived, and published_age_s (the time since start) is ≤ the limit.
    • A site that never gets a reading goes red after the limit (part 7 drives 31 s).
    • A reading that arrived and then stopped is red, as before.
  • Age on page request: _why_waiting_since is initialised by the setter in __init__ ("Starting"). Every later why_words = clears it. Nothing runs in threads (no executor or to_thread in app/), so the property's two reads cannot race. It is also clamped at ≥ 0.

  • Outage note (no change asked):

    • A 502/503/504 or connection error now returns the same None or False, but it skips _moan, which is correct.
    • One side effect: a websocket fault that is not a restart (a ClientError that persists) is said only once, as "perhaps it is restarting". The raw text is on DEBUG. The meter row goes red after 30 s in any case.

3. Scan (part 6/7): verified by injection

  • A semicolon plus "THIS IS BAD" injected into the circulation reason is caught: FAIL every built text … (2 bad).
  • A Claim.set("safety", …, "battery too hot") is caught: FAIL … missing: ['battery too hot'].
  • A new source Claim.set("thermal", …) is caught: missing: ['thermal'].
  • A non-constant reason becomes the key '', which also fails. That is safe.
  • DATA classification: every entry is a number, a name, a path, an option value or a library/exception text. None is a free-text reason. The key is file:expression, so a future builder that assigns free text to a variable named err in main.py would pass. That is a naming limit, not an abuse today. hass.py:what is add-on text: 3 short fixed phrases, all fine.

4. PEAK GUARD → PEAK_GUARD

  • admin/goodwe: 0 hits. /home/homelab/*.sh: 0 hits for either form.
  • In goodwe-addon, the old form is left only in prose: comments, DOCS/CHANGELOG/DECISIONS history, and the test_peakguard.py fixture strings reason="PEAK GUARD FLOOR-LIMITED: x" (inputs, never matched) and its print at :2355.
  • No code or test matches the reason text on it.

5. Leftover: number.set_value rejected (400): 400: Bad Request. Fixed.

  • New text: "Home Assistant refused number.set_value for number.sp (error 400, no details). The entity may still be loading, or the value is out of range."
  • A body with more than the bare status line is still quoted (for example, HA's out-of-range message).
  • Still WARNING, still once a minute, still a failed write (False). Part 7 pins both cases.

Pushed as e0b5787 (ls-remote verified). Local full suite in the CI image with the infra repo at the pin: 36/36 PASS. Waiting for CI.

## Review (UX-10), round 1 ### 1. The owner's bar on the new log lines: not met as logged. Fixed in a pushed commit. Part 6 checked the **quoted reason** alone for the red-alarm shape. The **line as logged** was never checked. So a title, a 2-sentence reason and two tails still went out as 4+ sentences: > Power request too high, which is a software fault: OUTPUT_CLAMP: the add-on computed +9000 W, over the 5000 W rating, and cut it back to +5000 W. The command this cycle was +2000 W. Control is still running. Cut back 1 time so far. That is 4 sentences and 43 words. With the check added, 25 lines failed it: the clamp lines, stale reading, the peak guard, circulation, the price feed fallback (4 sentences), and direction, delivery, frozen meter, allocator, arbiter and gate (a sentence over 30 words). **Fix (pushed):** - Part 6 now also checks the whole line: at most 3 sentences, none over 30 words. A mutation shows that the 2d-style line is caught although its reason passes. - Each wrapper is now one sentence. - The circulation and peak guard reasons are now two sentences. - `STALE_READING`'s fixed limits moved to a DEBUG line beside the ERROR. The release clause, the two explanations and "cannot tell which" stay, as test_freshness pins them. - The clamp lines for one inverter are pinned at ≤ 3 sentences and ≤ 30 words, with the code and the numbers kept: - `Power request too high, a software fault: OUTPUT_CLAMP: computed +9000 W, cut to the +5000 W rating, command now +2000 W. Cut back 1 time so far, control runs on.` (30) - `Power request too high, a software fault: OUTPUT_CLAMP: asked +3000 W and sent +2500 W, the inverter's own limit. Cut back 1 time so far, control runs on.` (28) - `The battery gives less than asked: OUTPUT_CLAMP: the correction the add-on builds up reached its +5000 W limit, command now +2000 W. Held back 1 time so far.` (28) - Several clamped inverters now read as one sentence of asked/sent pairs, so 3 or more inverters no longer make 4+ sentences. The limit that cut back each inverter is still on its own action-log row and in the notification. - `describe()` drops the rating: the sent value *is* the limit that bound (it equals the rating for `max_w`, and the rating did not bind for `device`). **Not done, follow-up:** some lines are still over 30 words in all, up to about 74. These are direction, delivery, frozen meter, stale and peak. They meet the 3-sentence and 30-per-sentence shape. Shortening them further means changing reasons that are persisted in state.json (direction, delivery), and the direction reason is pinned byte for byte in test_watchdog. ### 2. Safety - **Strings blanked, logic diffed** (AST, every string set to ''). The logic changes are only these: - `why_key()`, a pure extraction; - the `why_words` property; - the meter-row branch; - `Outage` and its calls, and `_receive_json`; - the `_WarnOnce` filter; - `access_log()`; - `write_clamp_note()`; - shadow INFO→DEBUG and shadow on/off WARNING→INFO; - `count()`/`when()`/`day_words()`/`spacing_words()`. No trigger, latch, claim, threshold, notification id or cadence changed. No alarm log level changed. The two shadow lines are not alarms. - **Shadow line to DEBUG:** nothing an operator needs is lost at INFO. The "Shadow mode is on/off" INFO line, the banner, the `Shadow mode` row, and the action log's "SHADOW: would have commanded N W" still show it. DOCS §Shadow mode points to the status page and action log, not the add-on log. Only the per-inverter split of the would-be command is DEBUG-only now. The site total is on the row and in the action log. - **Access log:** only `web.start()` uses it. Nothing in admin/goodwe or `/home/homelab/*.sh` greps `aiohttp.access` or `GET /status`. - **Meter row:** - `P1Ingest.stale` is untouched, and it gates control. - Amber shows only while `age_s() is None`, which means no reading ever arrived, and `published_age_s` (the time since start) is ≤ the limit. - A site that never gets a reading goes red after the limit (part 7 drives 31 s). - A reading that arrived and then stopped is red, as before. - **Age on page request:** `_why_waiting_since` is initialised by the setter in `__init__` ("Starting"). Every later `why_words =` clears it. Nothing runs in threads (no executor or `to_thread` in app/), so the property's two reads cannot race. It is also clamped at ≥ 0. - **`Outage` note (no change asked):** - A 502/503/504 or connection error now returns the same None or False, but it skips `_moan`, which is correct. - One side effect: a websocket fault that is *not* a restart (a `ClientError` that persists) is said only once, as "perhaps it is restarting". The raw text is on DEBUG. The meter row goes red after 30 s in any case. ### 3. Scan (part 6/7): verified by injection - A semicolon plus "THIS IS BAD" injected into the circulation reason is caught: `FAIL every built text … (2 bad)`. - A `Claim.set("safety", …, "battery too hot")` is caught: `FAIL … missing: ['battery too hot']`. - A new source `Claim.set("thermal", …)` is caught: `missing: ['thermal']`. - A non-constant reason becomes the key `''`, which also fails. That is safe. - **DATA classification:** every entry is a number, a name, a path, an option value or a library/exception text. None is a free-text reason. The key is `file:expression`, so a future builder that assigns free text to a variable named `err` in main.py would pass. That is a naming limit, not an abuse today. `hass.py:what` is add-on text: 3 short fixed phrases, all fine. ### 4. `PEAK GUARD` → `PEAK_GUARD` - admin/goodwe: 0 hits. `/home/homelab/*.sh`: 0 hits for either form. - In goodwe-addon, the old form is left only in prose: comments, DOCS/CHANGELOG/DECISIONS history, and the `test_peakguard.py` fixture strings `reason="PEAK GUARD FLOOR-LIMITED: x"` (inputs, never matched) and its print at :2355. - No code or test matches the reason text on it. ### 5. Leftover: `number.set_value rejected (400): 400: Bad Request`. Fixed. - New text: "Home Assistant refused number.set_value for number.sp (error 400, no details). The entity may still be loading, or the value is out of range." - A body with more than the bare status line is still quoted (for example, HA's out-of-range message). - Still WARNING, still once a minute, still a failed write (`False`). Part 7 pins both cases. Pushed as **e0b5787** (ls-remote verified). Local full suite in the CI image with the infra repo at the pin: 36/36 PASS. Waiting for CI.
Author
Owner

Review, round 2: every point resolved

  • 1. The owner's bar, fixed in e0b5787.
    • The line as logged is now checked (at most 3 sentences, none over 30 words).
    • The clamp lines are pinned at 30 words or fewer.
    • A longer total on the direction, delivery, frozen, stale and peak lines is a follow-up, recorded in DECISIONS/UX-08 item 7.
  • 2. Safety: no defect found.
    • The blank-string diff of e0b5787 changes only write_clamp_note's single-unit branch, the 400 body filter and a new DEBUG line.
    • No level, id, trigger or cadence changed.
  • 3. The scan: verified by injection, with no hiding path in the DATA list.
  • 4. PEAK GUARD: no hits in code, admin/goodwe or /home/homelab/*.sh. The only hits are prose and test fixture inputs.
  • 5. The HA 400 leftover: fixed and pinned in part 7.

CI on e0b5787 is green (checks / unit and infra-pin / fresh). release/1.0 has not moved (125b070). Merging with head_commit_id pinned.

## Review, round 2: every point resolved - **1. The owner's bar, fixed in e0b5787.** - The line as logged is now checked (at most 3 sentences, none over 30 words). - The clamp lines are pinned at 30 words or fewer. - A longer total on the direction, delivery, frozen, stale and peak lines is a follow-up, recorded in DECISIONS/UX-08 item 7. - **2. Safety: no defect found.** - The blank-string diff of e0b5787 changes only `write_clamp_note`'s single-unit branch, the 400 body filter and a new DEBUG line. - No level, id, trigger or cadence changed. - **3. The scan: verified by injection**, with no hiding path in the DATA list. - **4. `PEAK GUARD`:** no hits in code, admin/goodwe or `/home/homelab/*.sh`. The only hits are prose and test fixture inputs. - **5. The HA 400 leftover:** fixed and pinned in part 7. CI on e0b5787 is green (`checks / unit` and `infra-pin / fresh`). release/1.0 has not moved (125b070). Merging with `head_commit_id` pinned.
admin merged commit 2b14b88cb4 into release/1.0 2026-09-27 12:18:23 +02:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: admin/goodwe-addon#138