fix(inverter): never degrade an explicit freeze into a real charge or export - #4693
Merged
Merged
Conversation
… 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 realcharge_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_stopinstead. The hold is already established by the caller before these are reached (target SoC written to the current SoC, pluspause_discharge/ discharge rate 0 / reserve on the charge side; thecharge_stopissued at the top ofadjust_export_immediateon the export side), so stopping is the hold - starting is not.Only the explicit
freeze=Truepath 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_freezeflags, 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, plusGS_fb00andSA) - all of which reach freeze export through the safe local rate=0/pause path inexecute.pyand 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_DEFbelongs - declared, not inferred from whether a config key happens to be set - and it's separable from this safety fix.Test plan
charge_start/discharge_startissued 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 executepasses./run_pre_commitpasses (full suite, ruff, black, cspell, markdownlint)Also gave
test_call_adjust_charge_immediate'sclear=the same dedup resettest_call_adjust_export_immediatealready 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