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.
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
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
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>
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.jscycleButtons() (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.
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
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>
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.02026-10-14 01:18:43 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
MAINT-11: start or stop the maintenance cycle per battery.
What changed.
maint_startandmaint_aborttake an optionalunit. 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
79dd077and 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.pypasses 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 review (reviewed
21d8a94, pushedfd05912)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.jscycleButtons()(lines ~355-364) always draws an enabled Start cycle and Stop cycle on every battery at N>=2. Each battery row already hasb.maintenance(its phase,homeview.pyrow key"maintenance") and, with battery modes on,b.use. So the page can know:b.maintenanceis drain, charge or hold. On every other battery it always gets the "is not running a cycle" refusal.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
79dd077ignored 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 baremaint_startcomment ("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
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 runningcycle().79dd077and this head.textContentand text nodes only.test_messages.py.MAINT-11 re-review (round 2): reviewed fd05912..5ee1061, pushed
6aa1c32Finding 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'smaintenanceanduse, 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.b.useis missing, so Start stays enabled. That matches the server, which checks the switch only when modes are on.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:|| true).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.