fix(inverter): don't press the update button on idle non-export cycles - #4713
Conversation
|
Marking as ready for review on the strength of the auto triage on the source issue. |
fe7f215 to
4bd8cb1
Compare
There was a problem hiding this comment.
🟡 Changes recommended
The new regression test mutates shared test state (my_predbat.args / ha.dummy_items) without fully restoring it, risking order-dependent failures in subsequent tests.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR addresses excessive inverter “update button” presses during idle (non-export) cycles by ensuring adjust_force_export(False) doesn’t treat “no caller-supplied times” as a schedule change, and adds a regression test to prevent the issue from returning.
Changes:
- Track whether export times were supplied by the caller and only compare start/end times when they were, avoiding
None != "HH:MM:SS"idle-change detection. - Add a regression test that simulates repeated idle cycles for
GS_fb00andGSand asserts button presses do not repeat every cycle. - Wire the new regression test into the inverter test runner.
File summaries
| File | Description |
|---|---|
| apps/predbat/inverter.py | Avoids schedule-change detection (and resulting commits) when adjust_force_export(False) is called with no times. |
| apps/predbat/tests/test_inverter.py | Adds and runs a regression test ensuring idle cycles don’t trigger repeated update-button presses. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| ha.dummy_items["select.discharge_start_time"] = "00:00:00" | ||
| ha.dummy_items["select.discharge_end_time"] = "00:00:00" | ||
| ha.dummy_items["switch.scheduled_discharge_enable"] = "off" | ||
| ha.dummy_items["select.inverter_mode"] = "Eco" | ||
| my_predbat.args["discharge_start_time"] = "select.discharge_start_time" | ||
| my_predbat.args["discharge_end_time"] = "select.discharge_end_time" | ||
| my_predbat.args["scheduled_discharge_enable"] = "switch.scheduled_discharge_enable" | ||
|
|
||
| presses = [] | ||
| # Must report success, as a real press does - a falsy return means "not committed, retry" | ||
| inv.press_and_poll_button = lambda side="both", _p=presses: (_p.append(side), True)[1] | ||
|
|
||
| # Several cycles with nothing to export - the first may commit, the rest must be silent | ||
| for _ in range(4): | ||
| inv.adjust_force_export(False) | ||
|
|
||
| if len(presses) > expected_presses: | ||
| print(f"ERROR: {test_name}: {inverter_type} pressed the button {len(presses)} times over 4 idle cycles, expected at most {expected_presses}") | ||
| failed = True | ||
| finally: | ||
| if saved_type is None: | ||
| my_predbat.args.pop("inverter_type", None) | ||
| else: | ||
| my_predbat.args["inverter_type"] = saved_type |
There was a problem hiding this comment.
Confirmed - now saves/restores all four args (inverter_type, discharge_start_time, discharge_end_time, scheduled_discharge_enable) and all four dummy_items keys this test sets, using the sentinel-based save/restore pattern already established in this file (test_press_and_poll_button_side_scoping, line ~1409-1411).
| Plain GS takes the midnight-override path and was never affected, so it is checked here too to pin | ||
| the difference down. |
There was a problem hiding this comment.
execute.py calls adjust_force_export(False) with no times at all whenever nothing is being exported. On an inverter with has_discharge_enable_time set the midnight override is skipped, so new_start/new_end stay None while the inverter still reports a real time - and None never compares equal, so every idle cycle looked like a schedule change and pressed the update button. That is most of the day, not just export windows (#2328). Only compare times the caller actually asked us to set. A genuine transition out of export is still caught by force_export != old_discharge_enable, and the GE branch that deliberately clears the times keeps its existing behaviour - it is distinguished by whether the caller supplied any times, not by the times being None once we are past that branch. Affects GS_fb00 and the FoxCloud/TESLA/Enphase/Deye/Sunsynk/AlphaESS cloud types; plain GS takes the midnight-override path and was never affected. Not yet confirmed against a live install - awaiting logs on #4709. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ss_every_cycle - save/restore the discharge_* args and dummy_items this test overwrites, not just inverter_type, so nothing leaks into later tests - reword the docstring: plain GS wasn't affected by this test's specific None-comparison bug, but it does use the same "H M" time format that #4711's separate unconditional-write bug affected
3a58a94 to
7009184
Compare
dabd591
into
fix/solis-redundant-button-press-4709
Fixes #4712
Draft — stacked on #4711, and not yet confirmed against a live install.
Base is
fix/solis-redundant-button-press-4709, so this PR shows only its own commit. Retarget tomainonce #4711 lands.Problem
execute.pycallsadjust_force_export(False)with no times at all whenever nothing is being exported. On an inverter withhas_discharge_enable_timeset, the midnight-override branch is skipped, sonew_start/new_endstayNonewhile the inverter still reports a real time — andNonenever compares equal, so every idle cycle looks like a schedule change and presses the update button.That is most of the day, not just export windows: roughly 288 presses/day rather than a handful. On Solis each press is a non-volatile register write (#2328) and also zeroes the timed current registers (#4415 / #4709).
This PR is necessary but not sufficient on its own
There are two independent routes from an idle cycle to the button press, and this PR closes one of them. Measured on unmodified
main(b08b7716), four idleadjust_force_export(False)calls:GSischarge_time_format: "H M", so onmainit presses every idle cycle viachanged_start_end. fix(inverter): stop re-committing a stable export window every cycle (Solis) #4711 fixes that — a wider benefit than fix(inverter): stop re-committing a stable export window every cycle (Solis) #4711's own description claims.GS_fb00and the cloud types (FoxCloud,TESLA,EnphaseCloud,DeyeCloud,SunsynkCloud,AlphaESSCloud) additionally hit theNonecomparison. Theirold_start/old_endare never overwritten — nothing is written whennew_startisNone— so that route stays true forever past fix(inverter): stop re-committing a stable export window every cycle (Solis) #4711, and needs this PR.An earlier revision of this description said plain
GS"was never affected". That was measured with #4711 already applied rather than againstmain, and was wrong; corrected above. Thanks to the automated triage on #4712 for catching it.Fix
Only compare times the caller actually asked us to set.
The distinction matters: the GE branch deliberately clears the times to signal "we're using immediate controls", which is a different thing from the caller never supplying any. So the check keys off whether the caller supplied times, captured before either override runs — not off the times being
Noneby the time we reach the comparison. That keeps the GE path's behaviour byte-for-byte unchanged, and is whatadjust_force_export1in the existing suite pins down.A genuine transition out of export is still caught by
force_export != old_discharge_enable.Testing
New regression test covering both types, verified to fail without the fix:
Full
--quicksuite green.Why draft
Unit-level reproduction only; no live confirmation yet, and six of the eight affected types are cloud inverters that are hard to test here. Logs requested on #4712 and #4709 — the specific question is whether presses continue at the same 5-minute cadence when no export is scheduled.
🤖 Generated with Claude Code