Skip to content

fix(inverter): never degrade an explicit freeze into a real charge or export - #4693

Merged
springfall2008 merged 1 commit into
mainfrom
fix/freeze-service-fallback-safe
Aug 24, 2026
Merged

fix(inverter): never degrade an explicit freeze into a real charge or export#4693
springfall2008 merged 1 commit into
mainfrom
fix/freeze-service-fallback-safe

Conversation

@chalfontchubby

Copy link
Copy Markdown
Collaborator

Summary

Replaces #4435, which took the wrong approach - see below.

adjust_charge_immediate() / adjust_export_immediate() try the configured freeze service and, when none is configured, fall back to a real charge_start_service / discharge_start_service. That's a fresh active command rather than a passive hold, and some inverters treat it as a new instruction each cycle and briefly ramp to full power - the repeated short full-rate import bursts reported in #4424 / #4432.

Fall back to a plain charge_stop / discharge_stop instead. The hold is already established by the caller before these are reached (target SoC written to the current SoC, plus pause_discharge / discharge rate 0 / reserve on the charge side; the charge_stop issued at the top of adjust_export_immediate on the export side), so stopping is the hold - starting is not.

Only the explicit freeze=True path changes. Reaching the target without a freeze request (target_soc == soc_percent) keeps its existing behaviour, so this stays scoped to the reported fault and doesn't drag in the charge-side equivalent of #4464.

Why not #4435

#4435 treated this as "which inverters may plan freeze windows" and switched off the global set_charge_freeze / set_export_freeze flags, scoping which inverters by inferring capability from config. Three commits went into refining that condition. @springfall2008 pushed back on it twice, and the second version would have disabled export freeze for stock GivEnergy/GivTCP (GE/GEC/GEE, plus GS_fb00 and SA) - all of which reach freeze export through the safe local rate=0/pause path in execute.py and never touch a service at all.

The failure was never about planning; it's about how one command is delivered. Fixing it at the point of failure makes the capability question disappear, so no inverter-capability inference is needed. That also answers "why not just add an inverter capability setting?" - we don't need one for this.

Known limitation

For an inverter whose only freeze mechanism was that service (no target SoC, no pause), falling back to stop gives "don't charge" rather than a true hold, while the planner still believes it froze. That's strictly better than a full-power burst, but it's a residual model/execution mismatch of the same family as #4690. If it needs closing, that's where a real capability declaration in INVERTER_DEF belongs - declared, not inferred from whether a config key happens to be set - and it's separable from this safety fix.

Test plan

  • Four new cases (both sides, plus a repeat call each), verified failing first - they show charge_start / discharge_start issued at full rate where a stop was required, and re-issued rather than deduplicated on the next cycle, which is the reported burst symptom
  • ./run_all --test inverter --test execute passes
  • ./run_pre_commit passes (full suite, ruff, black, cspell, markdownlint)

Also gave test_call_adjust_charge_immediate's clear= the same dedup reset test_call_adjust_export_immediate already had, so the new cases are judged fresh rather than against whatever the previous sub-test sent.

Fixes #4424. Fixes #4432.

🤖 Generated with Claude Code

… export

adjust_charge_immediate()/adjust_export_immediate() tried the configured
freeze service and, when none was configured, fell back to a real
charge_start_service / discharge_start_service - a fresh active command
rather than a passive hold. Some inverters treat that as a new instruction
each cycle and briefly ramp to full power, producing the repeated short
full-rate import bursts reported in #4424/#4432.

Fall back to a plain charge_stop / discharge_stop instead. The hold is
already established by the caller before these are reached - target SoC
written to the current SoC, plus pause_discharge / discharge rate 0 /
reserve on the charge side, and the charge_stop issued at the top of
adjust_export_immediate on the export side - so stopping is a genuine
hold, whereas starting is not.

Only the explicit freeze=True path changes. Reaching the target without a
freeze request (target_soc == soc_percent) keeps its existing behaviour,
so this is scoped to the reported fault and leaves the charge-side
equivalent of #4464 alone.

Tests: four new cases covering both sides, verified failing first - they
show charge_start/discharge_start being issued at full rate where a stop
was required, and re-issued rather than deduplicated on the following
cycle, which is the reported burst symptom. Also gave the charge helper's
clear= the same dedup reset the export helper already had, so the new
cases are judged fresh rather than against the previous sub-test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@springfall2008
springfall2008 merged commit 27b21e8 into main Aug 24, 2026
2 checks passed
@springfall2008
springfall2008 deleted the fix/freeze-service-fallback-safe branch August 24, 2026 12:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants