Skip to content

fix(inverter): don't press the update button on idle non-export cycles - #4713

Merged
chalfontchubby merged 2 commits into
fix/solis-redundant-button-press-4709from
fix/solis-non-export-button-press
Aug 28, 2026
Merged

fix(inverter): don't press the update button on idle non-export cycles#4713
chalfontchubby merged 2 commits into
fix/solis-redundant-button-press-4709from
fix/solis-non-export-button-press

Conversation

@chalfontchubby

@chalfontchubby chalfontchubby commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

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 to main once #4711 lands.

Problem

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 branch 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 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 idle adjust_force_export(False) calls:

             GS      GS_fb00
main         4/4     4/4
+#4711       1       4/4     <- #4711 closes the is_hm_format / changed_start_end route
+#4713       1       1       <- this PR closes the None-comparison route

An earlier revision of this description said plain GS "was never affected". That was measured with #4711 already applied rather than against main, 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 None by the time we reach the comparison. That keeps the GE path's behaviour byte-for-byte unchanged, and is what adjust_force_export1 in 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:

ERROR: GS_fb00 pressed the button 4 times over 4 idle cycles, expected at most 1

Full --quick suite 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

@chalfontchubby

Copy link
Copy Markdown
Collaborator Author

Marking as ready for review on the strength of the auto triage on the source issue.

@chalfontchubby
chalfontchubby marked this pull request as draft August 24, 2026 19:46
@chalfontchubby
chalfontchubby force-pushed the fix/solis-non-export-button-press branch from fe7f215 to 4bd8cb1 Compare August 24, 2026 20:22
@springfall2008
springfall2008 marked this pull request as ready for review August 25, 2026 10:06
Copilot AI lite review requested due to automatic review settings August 25, 2026 10:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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_fb00 and GS and 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.

Comment thread apps/predbat/tests/test_inverter.py Outdated
Comment on lines +2529 to +2552
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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread apps/predbat/tests/test_inverter.py Outdated
Comment on lines +2515 to +2516
Plain GS takes the midnight-override path and was never affected, so it is checked here too to pin
the difference down.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed - verified GS's charge_time_format is "H M" (config.py:1813), the same format #4711's unconditional-write bug hit, so the blanket "was never affected" was misleading. Reworded to be specific to this test's None-comparison bug, with a note pointing at the separate #4711 issue GS did hit.

chalfontchubby and others added 2 commits August 28, 2026 14:32
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
@chalfontchubby
chalfontchubby force-pushed the fix/solis-non-export-button-press branch from 3a58a94 to 7009184 Compare August 28, 2026 15:31
@chalfontchubby
chalfontchubby merged commit dabd591 into fix/solis-redundant-button-press-4709 Aug 28, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants