MAINT-11: start or stop the maintenance cycle per battery #189

Merged
admin merged 4 commits from MAINT-11 into release/1.0 2026-10-14 01:18:43 +02:00
Owner

MAINT-11: start or stop the maintenance cycle per battery.

What changed. maint_start and maint_abort take an optional unit. With no unit the old code runs unchanged. At two or more batteries each Batteries card has Start cycle and Stop cycle buttons (EN and NL). Scheduler, control and the phase machine are untouched.

Decision: refuse, not queue. A start while any battery holds the maintenance slot is refused with a short message naming the cycling battery. Also refused: unknown unit, start on a switched-off battery, stop on a battery that is not cycling. See DECISIONS/MAINT-11-cycle-per-battery.md.

N=1 identity. The same transcript (status EN/NL, bare start/stop, log, wire writes, store, payload) on 79dd077 and on this head gives byte-identical output. The page file itself differs; the buttons are drawn only when there are 2+ batteries.

Tests. test_maint11.py, 76 checks at N=2 and N=3. Local suite: 74 of 75 pass; test_freeze.py passes without the CI-only HWSIM variable.

Mutations. 17 deliberate breakages, all caught (one first survived a loose check; tightened, then caught). Table in the dev report.

Note: the bare button never refused anything before, so there was no existing refusal to copy; see the report.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC

MAINT-11: start or stop the maintenance cycle per battery. **What changed.** `maint_start` and `maint_abort` take an optional `unit`. With no unit the old code runs unchanged. At two or more batteries each Batteries card has Start cycle and Stop cycle buttons (EN and NL). Scheduler, control and the phase machine are untouched. **Decision: refuse, not queue.** A start while any battery holds the maintenance slot is refused with a short message naming the cycling battery. Also refused: unknown unit, start on a switched-off battery, stop on a battery that is not cycling. See `DECISIONS/MAINT-11-cycle-per-battery.md`. **N=1 identity.** The same transcript (status EN/NL, bare start/stop, log, wire writes, store, payload) on 79dd077 and on this head gives byte-identical output. The page file itself differs; the buttons are drawn only when there are 2+ batteries. **Tests.** `test_maint11.py`, 76 checks at N=2 and N=3. Local suite: 74 of 75 pass; `test_freeze.py` passes without the CI-only HWSIM variable. **Mutations.** 17 deliberate breakages, all caught (one first survived a loose check; tightened, then caught). Table in the dev report. Note: the bare button never refused anything before, so there was no existing refusal to copy; see the report. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
admin added 1 commit 2026-10-13 22:15:16 +02:00
MAINT-11: start or stop the maintenance cycle per battery
checks / unit (pull_request) Successful in 38m46s
infra-pin / fresh (pull_request) Successful in 1s
21d8a944bc
maint_start and maint_abort take an optional unit. With two or more batteries
each Batteries card has Start cycle and Stop cycle buttons. One slot per site
stays: a start while a battery cycles is refused and names it (no queue). A
battery that is switched off, an unknown unit, and a stop on a battery that is
not cycling are refused with a plain message. A request with no unit is the
old call. The scheduler and the phase machine are unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
admin added 1 commit 2026-10-13 23:06:49 +02:00
MAINT-11 review: test an empty unit at the web layer and the page's own refusal words; drop a stale comment
checks / unit (pull_request) Successful in 38m45s
infra-pin / fresh (pull_request) Successful in 2s
fd059126cb
Two reviewer mutations survived the suite: the web layer routing only a
truthy unit (an empty unit then started the bare pick), and the page
showing the server's English instead of its own sentence by code. Both
are now checked. The bare maint_start comment that said a per-battery
unit would ship half a surface is replaced by a pointer to
_maint_unit_action(). No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

MAINT-11 review (reviewed 21d8a94, pushed fd05912)

Verdict: not merged. One finding changes behaviour, so it goes back to dev.

Finding 1 (medium, behaviour): buttons are not disabled when they cannot work

app/static/app.js cycleButtons() (lines ~355-364) always draws an enabled Start cycle and Stop cycle on every battery at N>=2. Each battery row already has b.maintenance (its phase, homeview.py row key "maintenance") and, with battery modes on, b.use. So the page can know:

  • Stop cycle can only work on the battery whose b.maintenance is drain, charge or hold. On every other battery it always gets the "is not running a cycle" refusal.
  • Start cycle cannot work while any battery is cycling, or on a battery with S.modes_on && b.use === false.

Fix, page only: disable (or hide) the button in those cases. Keep the server refusals as the backstop. No payload change is needed. Add a test for it.

Finding 2 (low, no change needed): a unit at N=1 is now honoured

At N=1, a request that carries a unit goes down the unit path. A second start is then refused as "busy", where 79dd077 ignored the unit and re-entered the drain. No caller sends a unit at N=1 (the page draws no buttons there), and the bare contract is byte-identical (the transcript reproduces). Please mention it in DECISIONS "What this does not cover".

Finding 3 (low, follow-up): starting a battery in a safety stop or one that is not reachable

The unit start starts these, the same as the bare button does. The drain then waits, times out after 12 h and raises a failed-cycle notice. That is consistent with the bare button, but a refusal would be friendlier. Suggest a follow-up card, not this PR.

Pushed by review (fd05912, tests and a comment only)

  • test_maint11.py: the web layer refuses an empty unit (400) and does not start anything; the page shows a refusal in its own language by code. These two reviewer mutations had survived.
  • main.py: replaced the bare maint_start comment ("inventing a unit parameter here would ship half a surface"), which is no longer true, with a pointer to _maint_unit_action().

What checks out

  • Slot rule: busy refusal names the holder. No await between the check and force_start, so two requests cannot race. After a restart the stored phase still holds the slot. A scheduled start while the operator's cycle runs is deferred (arbitrate), checked by running cycle().
  • A stop by unit is the same as the bare stop: log, wire, targets, phases and store match in every phase and for every holder at N=2 and N=3.
  • N=1 transcript: byte-identical between 79dd077 and this head.
  • No HTML injection: names reach the page through textContent and text nodes only.
  • Strings: short, plain, EN and NL, pass test_messages.py.
  • Full suite in goodwe-addon-checks:1: 75/75 pass.
  • Reviewer mutations: 12 tried. 9 caught, 2 survived and are now caught by the tests above, and 1 is equivalent.
  • Refusing battery (SAFETY-73) starts and waits, like the bare button. I agree: it can recover while it waits, and refusing it would add a rule the bare button does not have.
## MAINT-11 review (reviewed 21d8a94, pushed fd05912) **Verdict: not merged.** One finding changes behaviour, so it goes back to dev. ### Finding 1 (medium, behaviour): buttons are not disabled when they cannot work `app/static/app.js` `cycleButtons()` (lines ~355-364) always draws an enabled **Start cycle** and **Stop cycle** on every battery at N>=2. Each battery row already has `b.maintenance` (its phase, `homeview.py` row key `"maintenance"`) and, with battery modes on, `b.use`. So the page can know: - **Stop cycle** can only work on the battery whose `b.maintenance` is drain, charge or hold. On every other battery it always gets the "is not running a cycle" refusal. - **Start cycle** cannot work while any battery is cycling, or on a battery with `S.modes_on && b.use === false`. Fix, page only: disable (or hide) the button in those cases. Keep the server refusals as the backstop. No payload change is needed. Add a test for it. ### Finding 2 (low, no change needed): a unit at N=1 is now honoured At N=1, a request that carries a unit goes down the unit path. A second start is then refused as "busy", where 79dd077 ignored the unit and re-entered the drain. No caller sends a unit at N=1 (the page draws no buttons there), and the bare contract is byte-identical (the transcript reproduces). Please mention it in DECISIONS "What this does not cover". ### Finding 3 (low, follow-up): starting a battery in a safety stop or one that is not reachable The unit start starts these, the same as the bare button does. The drain then waits, times out after 12 h and raises a failed-cycle notice. That is consistent with the bare button, but a refusal would be friendlier. Suggest a follow-up card, not this PR. ### Pushed by review (fd05912, tests and a comment only) - `test_maint11.py`: the web layer refuses an empty unit (400) and does not start anything; the page shows a refusal in its own language by code. These two reviewer mutations had survived. - `main.py`: replaced the bare `maint_start` comment ("inventing a unit parameter here would ship half a surface"), which is no longer true, with a pointer to `_maint_unit_action()`. ### What checks out - Slot rule: busy refusal names the holder. No await between the check and `force_start`, so two requests cannot race. After a restart the stored phase still holds the slot. A scheduled start while the operator's cycle runs is deferred (`arbitrate`), checked by running `cycle()`. - A stop by unit is the same as the bare stop: log, wire, targets, phases and store match in every phase and for every holder at N=2 and N=3. - N=1 transcript: byte-identical between 79dd077 and this head. - No HTML injection: names reach the page through `textContent` and text nodes only. - Strings: short, plain, EN and NL, pass `test_messages.py`. - Full suite in goodwe-addon-checks:1: 75/75 pass. - Reviewer mutations: 12 tried. 9 caught, 2 survived and are now caught by the tests above, and 1 is equivalent. - Refusing battery (SAFETY-73) starts and waits, like the bare button. I agree: it can recover while it waits, and refusing it would add a rule the bare button does not have.
admin added 1 commit 2026-10-13 23:20:54 +02:00
MAINT-11 round 2: disable the cycle buttons that cannot work
checks / unit (pull_request) Successful in 38m43s
infra-pin / fresh (pull_request) Successful in 1s
5ee1061398
Stop cycle is disabled on a battery that is not cycling. Start cycle is
disabled while any battery cycles and on a switched-off battery. Page only;
the add-on still refuses each of these. DECISIONS notes the one-battery case.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
admin added 1 commit 2026-10-14 00:39:41 +02:00
MAINT-11 review round 2: pin each cycle button to its own disabled flag
checks / unit (pull_request) Successful in 38m46s
infra-pin / fresh (pull_request) Successful in 1s
6aa1c32cbb
Reviewer mutations survived the round-2 page checks: Start forced always
disabled (|| true), and the two flags swapped between Start and Stop,
because the checks only looked for each flag somewhere in the file. The
check now pins each flag to its own button. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

MAINT-11 re-review (round 2): reviewed fd05912..5ee1061, pushed 6aa1c32

  • Finding 1 is fixed. Stop is disabled unless this battery is cycling, and Start is disabled while any battery cycles or when b.use === false. The page uses only the row's maintenance and use, the payload is unchanged, and the add-on's refusals are still there.

    • !== "idle" matches the server's busy rule: the only phases are idle, drain, charge and hold, and the last three are POWER_MOVING.
    • With modes off, b.use is missing, so Start stays enabled. That matches the server, which checks the switch only when modes are on.
    • I found no case where a button is disabled but the action would work.
  • Finding 2 is fixed. The DECISIONS line is correct.

  • Pushed 6aa1c32 (test only). Two of my mutations survived the 4 new page checks, because each check only looked for a flag somewhere in the file:

    • Start forced always disabled (|| true).
    • Start's and Stop's flags fully swapped.

    The new check pins each flag to its own button, and all 7 mutations are now caught.

  • Other checks: full suite 75/75 in goodwe-addon-checks:1. The N=1 transcript is byte-identical to 79dd077.

## MAINT-11 re-review (round 2): reviewed fd05912..5ee1061, pushed 6aa1c32 - **Finding 1 is fixed.** Stop is disabled unless this battery is cycling, and Start is disabled while any battery cycles or when `b.use === false`. The page uses only the row's `maintenance` and `use`, the payload is unchanged, and the add-on's refusals are still there. - `!== "idle"` matches the server's busy rule: the only phases are idle, drain, charge and hold, and the last three are POWER_MOVING. - With modes off, `b.use` is missing, so Start stays enabled. That matches the server, which checks the switch only when modes are on. - I found no case where a button is disabled but the action would work. - **Finding 2 is fixed.** The DECISIONS line is correct. - **Pushed 6aa1c32 (test only).** Two of my mutations survived the 4 new page checks, because each check only looked for a flag somewhere in the file: - Start forced always disabled (`|| true`). - Start's and Stop's flags fully swapped. The new check pins each flag to its own button, and all 7 mutations are now caught. - **Other checks:** full suite 75/75 in goodwe-addon-checks:1. The N=1 transcript is byte-identical to 79dd077.
admin merged commit 0ce7d35f80 into release/1.0 2026-10-14 01:18:43 +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#189