On a meter whose readings come 4 s or more apart, the loop runs at half its
gain after a move of 150 W or more, until the meter has fallen by half of the
move or 2 cycles have passed. With readings 10 s late on a 5 s meter the export
falls 44 % over oven, kettle and hob, owner shape, steps; 1-2 s meters are
untouched.
Co-Authored-By: Claude <noreply@anthropic.com>
test_safety118's "size rule is gone" and test_validate's "no numeric
maintenance_* fallback" read app/main.py only; the code they guard can now
sit in app/ctl_*.py, where a regression passed unseen. Both read
main_files.source() for main.py now. Tests only; no app change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Router T241: the comments commit had put new rationale into
MAIN-orchestration.md and cut comment lines that the note at 56fc41f did not
hold.
- DECISIONS/MAIN-orchestration.md is the 56fc41f text plus the one sentence
that the Controller is split into app/main.py and app/ctl_*.py.
- Comments whose content is not in a DECISIONS note at 56fc41f are back in the
code verbatim: MAINT-05 one config, the SAFETY-02, SAFETY-42/34,
SAFETY-30 site half, SAFETY-31 and SAFETY-18 latch blocks in __init__, the
_grade_store_migration docstring and comment, the trade_power_w docstring,
the rows.py docstrings.
- Kept: cuts of text already in the note, the SMA-10 / SAFETY-137 / SMA-2
pointers, the SAFETY-32 line fix.
No code change: maint21_same_ast OK, maint20_same_code -OO same.
Co-Authored-By: Claude <noreply@anthropic.com>
No code change: tools/maint21_same_ast.py and tools/maint20_same_code.py HEAD~
both print OK, and the -OO code objects are the same.
- Trims 56 comment and docstring lines (2088 -> 2032) in app/main.py and the
new files that only restated their MAIN-orchestration.md bullet, each cut to
its first sentence and a pointer. What the cut comments said and the bullet
did not is now in the bullet: why a SAFETY-02 latch is restored, why the
SAFETY-30 site check is not armed at one inverter, the SAFETY-18 re-derive,
the MAINT-05 one-config rule, the OPT-09 sizing rules, the "no plan" row,
the 5 400 s age bound, and the guard being the only latch repair.
- Adds the missing DECISIONS pointers on the SMA-10, SAFETY-137 and SMA-2
comments (FALLBACK_BUDGET_S, write_setpoint, _await_inflight,
_release_last, the retry wait, the failed-release branch, release_zero_sent,
the in-flight write, the stop_event hand-back, the SMA-2 read and driver).
- DECISIONS/SAFETY-32: the stale `main.py:932-936` now names
Controller.pack_floor_soc.
- main.py's docstring and import block say where the Controller's parts are.
Co-Authored-By: Claude <noreply@anthropic.com>
Moves the methods named in the card and the constants only they read out of app/main.py unchanged. TasksMixin: run_control, run_tariff_refresh, fresh_tariff_plan and the store-migration grade. tools/maint21_same_ast.py: OK.
Co-Authored-By: Claude <noreply@anthropic.com>
Moves the methods named in the card and the constants only they read out of app/main.py unchanged. StatusMixin: checks, status_payload, forecast_payload, publish, mqtt_command, handle_action, why_words. tools/maint21_same_ast.py: OK.
Co-Authored-By: Claude <noreply@anthropic.com>
Moves the methods named in the card and the constants only they read out of app/main.py unchanged. AlarmsMixin: the check_* alarms and the loop-alarm helpers. tools/maint21_same_ast.py: OK.
Co-Authored-By: Claude <noreply@anthropic.com>
Moves the methods named in the card and the constants only they read out of app/main.py unchanged. CycleMixin: cycle() and _shadow_settle(). cycle() is not cut up. tools/maint21_same_ast.py: OK.
Co-Authored-By: Claude <noreply@anthropic.com>
Moves the methods named in the card and the constants only they read out of app/main.py unchanged. TradeMixin: trade sizing, the mode runs, the day plan, the peak floors, drain and charge limits, pack actions. tools/maint21_same_ast.py: OK.
Co-Authored-By: Claude <noreply@anthropic.com>
Moves the methods named in the card and the constants only they read out of app/main.py unchanged. WritesMixin: write_setpoint, write_refusal, _write_unit, _hand_back, the refusal evidence, heartbeats and the site release/zero commands. tools/maint21_same_ast.py: OK.
Co-Authored-By: Claude <noreply@anthropic.com>
Moves the methods named in the card and the constants only they read out of app/main.py unchanged. InputsMixin: read_inputs, _read_unit, live_units and the other readers of the meter, packs, PV and forecast. tools/maint21_same_ast.py: OK.
Co-Authored-By: Claude <noreply@anthropic.com>
Moves why_raised, why_key, tariff_plan_row and arbitrage_row out of app/main.py unchanged. main.py imports them, so app.main still exports them. tools/maint21_same_ast.py: OK.
Co-Authored-By: Claude <noreply@anthropic.com>
Prepares the split of app/main.py's Controller into files by job, with no app
change. tools/main_files.py gives a test the text and the AST of main.py plus
the files split from it, and `live`, which sets a global in every file whose
functions read it (and raises when none does). Tests that read main.py's
source, and tests and replay tools that patch a main global (datetime,
compute, resolve, _mono, WRITE_BUDGET_S, ...), go through it, so they pass on
this layout and on the split one. test_split_patches.py fails when a test or
tool patches an app.main attribute that main.py no longer reads, or that
another file also reads. tools/maint21_same_ast.py proves every function,
method and module-level assignment of main.py at the base is found unchanged
in its new file.
Co-Authored-By: Claude <noreply@anthropic.com>
- check_peak_floor_limited: restore the SAFETY-25 line (the reserve sentence is
in messages.peak_reserve() on every branch, unreadable SoC included).
- Three status-page comments that no longer said anything (SAFETY-20 row,
AC 5 delivery row, banner-vs-text) now say their rule in one line.
- tariff_refresh_wanted docstring: no orphan "TARIFF-24)" line.
- MAIN-orchestration.md: blank line before the first heading.
- MAIN-orchestration-archive.md: refilled intro; says how to find a section
from a main.py pointer.
No code change: maint20_same_code.py 16029d7 -> SAME CODE, -OO same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nine conflicts in app/main.py, all in comments around new code: every code
line is release/1.0's, every comment follows the part 2 rule.
Co-Authored-By: Claude <noreply@anthropic.com>
SAFETY-136: the line said 10 to 15 s late is not caught, then that 15 s late
is caught after about 4 minutes. It now says 10 s is not caught and 15 s only
after about 4 minutes, as the card says.
SAFETY-138: the 30 s timer that zeroes a late write is the ESPHome board's
watchdog (README), not the inverter's own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Minor bump, no option changes. Folds "Not in a release yet" into a new
0.21.0 section, adds the contents table for the nine merges on release/1.0
since master 0.20.1, updates the known issues, and rewrites the 0.20.1
SAFETY-128 known issue to say it ships in 0.21.0.
Co-Authored-By: Claude <noreply@anthropic.com>
The 0 W of shutdown and of the Stop button now waits (bounded, 2 s) for a
write already on its way to the same unit, for every driver, GoodWe included.
Once shutdown has begun, cycle, heartbeat and safety-stop writes are dropped
right before the write is created. One shared wait (_await_inflight) serves
the 0 W and SMA-10's _release_last.
Co-Authored-By: Claude <noreply@anthropic.com>
The NL default for a hand-back driver without words (handback._BACK_NL) was
changed to "de eigen sturing" in round 2 at the PR #239 review's request
(comment 2495: the article as in the generic clause). The brief does not name
it, so DECISIONS now says where it came from and a test checks the exact Dutch
sentence. No app code change.
Co-Authored-By: Claude <noreply@anthropic.com>
When control takes the unit again, release_fails, release_retry_at and
release_zero_sent are reset, so the next Stop is a new streak with its own one
0 W, error line and event. Tests: a streak that Start interrupts without a
recovery; a driver that overrides release_fallback(). _BACK_NL "de eigen
sturing" (round 2) is kept.
Co-Authored-By: Claude <noreply@anthropic.com>
When release() fails (False, raises or hangs past RELEASE_BUDGET_S) the unit
gets one bounded write(0) through the new Driver.release_fallback() hook
(default write(0)), then nothing more, so the inverter's own fallback timer is
not reset. The release is retried once per heartbeat, not on every idle
write. One error line and one action-log event per failure streak, one line on
recovery. Dutch default "terug naar de eigen sturing". DECISIONS says what a
unit is left at.
test_messages.py: the RELEASE_BUDGET_S data entry is gone again (the line
that needed it is gone).
Co-Authored-By: Claude <noreply@anthropic.com>
A driver now says whether its release() hands the inverter back
(Driver.hands_back; GoodWe is False, so release() stays write(0) and the
Controller never calls it). command_site_release() sits next to
command_site_zero(): the Stop button and shutdown() (so SIGTERM, restart,
control task death and a late config refusal) hand a hand-back unit back,
once, and zero every other unit as before. Safety stops and refusals still
hold 0 W for every unit. Shadow mode releases a hand-back unit. After a
release nothing is written to the unit (heartbeat and cycle included) until
control starts again. release() is bounded by RELEASE_BUDGET_S (= the write
budget), runs concurrently per unit, and a raising or hanging release on one
unit does not stop the others.
The "set to 0 W" sentences for Stop, restart, shadow mode and the stopped
banner are built from the roster (app/handback.py, EN and NL, words from the
driver); a GoodWe-only roster keeps every old literal. A handed-back unit
shows "Back under <its control>" on Home.
Existing test edited: test_messages.py (two classification entries for the
CRITICAL line that moved into a roster builder, and the new release bound).
Co-Authored-By: Claude <noreply@anthropic.com>
Owner decision 2026-11-07: clients see nothing new until SMA-4. config.yaml,
translations, labels and the page layout are back to 23ff9d0's. The validation
of the keys stays for callers that skip the schema.
Co-Authored-By: Claude <noreply@anthropic.com>
app/drivers.py holds one Driver per roster entry (configured, actuator_id,
limits, read, write, release, probe, link_stale_s, batt_bound, settle_s, brand,
words). GoodWeEsphomeDriver makes the calls the code it replaced made, in the
same order. Inverter gets driver; has_writer, is_live and link_note read it. The
SoC/power read, the write, the device range (run_control, amain, the settings
panel), rule 2 per unit and the SAFETY-28 census go through it.
setpoint_entity is optional per entry beside driver, host, port, unit_id and
maintenance; only goodwe_esphome is selectable, the rest are checked and unused.
Co-Authored-By: Claude <noreply@anthropic.com>
Before the bell ends it replaced today's Home line and the event log with
the kept-for-tonight wording. stand_in() docstring covers the tomorrow case.
Co-Authored-By: Claude <noreply@anthropic.com>
One stale Forecast.Solar tomorrow sensor dropped the whole OPT-17 sum from
17:54 to 20:00. Today readings and the OPT-34 morning rules are unchanged.
Co-Authored-By: Claude <noreply@anthropic.com>
At a 5 s meter the slow meter fit now finds 30 s late first, and the hold leaves
the batteries' fit nothing to measure, so the row says it without a figure.
Co-Authored-By: Claude <noreply@anthropic.com>
The meter's delay fit needed 16 new values in 60 s, so at the owner's 5 s P1
only the batteries' fit ran and 30 s late was found after about 3 minutes
(export to -420 W). A meter that slow is now also fitted over 120 s; it calls
a delay late only over 16 s, on two fits in a row, with the same corroboration
as before, and never ends a verdict, so a slow battery (R2-1) runs as before.
Found about 90 s after the start, export above -200 W. 10 s late at a 5 s
meter is not found (nothing swings; it cannot be told from a 10 s battery).
Co-Authored-By: Claude <noreply@anthropic.com>
The cheap-run roll (cheaproll._Charge) shared the peak room by rating, control
shares it by room left to full (packshare.weights). With unequal levels one
battery's drawn block ended too early (rig F-1). The roll now uses
packshare.weights over the simulated levels, each battery capped at its rating,
a full one dropping out. test_ux77 gains two unequal-level walks and a
by-rating mutant; its walks model control's split by room too.
Co-Authored-By: Claude <noreply@anthropic.com>
The cheap run is sized at the full rating, so under the peak limit a battery is
still short when it ends, and control plans the next run then. Home drew only
the run under way. `cheaproll` rolls the run on the way control does
(`modetrade.energy()` + `priceruns.plan()` at each run end, lifted from
`dayview._levels()`), reading the levels at the charge power the peak limit
leaves (peak aim less the house, else the rating). `homeview.mode_runs()` and
`price_windows()` use it for today: the grid block, the Today line and the
chart's cheap bars run to where control stops, each battery's block to its
own stop. Display only: no control change.
test_ux54: today's grid block of the night-before scenario now follows the roll.
Co-Authored-By: Claude <noreply@anthropic.com>
Brings OPT-36 (#229) and TEST-37 (#231) onto release/1.0, which keeps
SAFETY-128 (#228). No textual conflicts. CHANGELOG: the SAFETY-128 entry
moved out of the released 0.20.0 section into "Not in a release yet" above
## 0.20.1; the 0.20.0 and 0.20.1 sections are identical to master.
Co-Authored-By: Claude <noreply@anthropic.com>