fix(applier): exempt batteries_discharge_mode from the hold-derived 0 W discharge cap - #985
Merged
Conversation
… W discharge cap `_primary_battery_hold()` infers an explicit hold from a near-zero (batteries_charged_kwh, batteries_discharged_kwh) pair. That derivation is unsound for a schedule discharge window: `soc_simulation.py` relabels only the two force modes to wait when the simulated discharge is zero, so a `batteries_discharge_mode` slot whose re-solved discharge rounds below the 0.001 kWh materiality threshold keeps its label and still reads as a hold — and the slot executes as MaximizeSelfConsumption, where the cap is a ceiling the firmware ramps within, not a setpoint. The result was the Huawei maximum-discharge-power register flipping between 0 W and the rated maximum across replans while the published recommendation never changed, so neither plan-level (#372) nor window (#315) hysteresis applied. Adds `applier_caps._primary_battery_cap_hold()` and uses it for the discharge-cap decision only. `_primary_battery_hold()` is unchanged and keeps its meaning for `_held_planned_export_is_authoritative()` and the `batteries_wait_mode` working-mode branch. Every other 0 W path keeps immediate precedence: EV permission gating (#797), solar-charge-only (#922), wait-mode reserve floor (#954), the SoC reserve guard (#592), and the read-only/degraded gates. Fixes #983 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
woopstar
deleted the
fix/983-discharge-mode-zero-cap-oscillation-main
branch
September 13, 2026 05:21
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
Fixes the Huawei
Maximum discharging powerregister oscillating between 0 W and the rated maximumduring a
batteries_discharge_modewindow while the published recommendation never changes.Reported against 6.3.0 by @AndersN76 in #978 (comment)
and confirmed by @Extati in #978 (comment).
This PR targets
main. The identical change is proposed against thev6.3.0-hotfixbranch in#984 so it can also ship on the 6.3.x line while
mainmoves toward 7.0.0. Cherry-picked fromfix/983-discharge-mode-zero-cap-oscillationwith no conflicts —applier.pyandapplier_caps.pyare byte-identical between
v6.3.0andmain.Root cause
applier_caps._primary_battery_hold()derives "the solved plan explicitly holds the primarybattery" from a near-zero
(batteries_charged_kwh, batteries_discharged_kwh)pair, andapplier.pyturns that into an unconditional 0 W discharge cap. Two facts make that unsound for a schedule
discharge window:
planner/soc_simulation.pyrelabels onlyforce_batteries_discharge/force_exporttobatteries_wait_modewhen the simulated discharge is zero —batteries_discharge_modedeliberately keeps its label, because it is the user's configured window, not a forced action. So
a discharge window whose re-solved
batteries_discharged_kwhrounds belowPLANNED_ENERGY_ROUNDING_KWH(0.001 kWh) keeps the discharge label and satisfies the derivedhold, without the plan ever having decided to hold.
batteries_discharge_modeexecutes asMaximizeSelfConsumption, where this cap is a ceilingthe firmware ramps within from live house load — not a setpoint. Writing 0 W there disables the
behaviour the mode exists for and contradicts the
Recommendationsenum's own contract for it("discharge battery to cover house load").
Because the published recommendation never changed, neither existing damping layer applied:
plan-level hysteresis (#372) stabilises candidate selection, and window hysteresis (#315) only
reacts to a recommendation change (and lets any transition involving a neutral recommendation
through immediately anyway). Nothing guarded this final actuator boundary.
This is not #939/#941: that bug wrote rated max then 0 W inside the same apply cycle. Here the
writes are minutes apart across separate replan/apply events, with the #941 single-write-per-cycle
fix present and working.
Changes
custom_components/hsem/custom_sensors/applier_caps.py— new_primary_battery_cap_hold(rec):_primary_battery_hold(rec)and the recommendation is notbatteries_discharge_mode.custom_components/hsem/custom_sensors/applier.py— the discharge-cap decision uses the newwrapper.
_primary_battery_hold()itself is unchanged and keeps its meaning for_held_planned_export_is_authoritative()and thebatteries_wait_modeworking-mode branch (wherethe two are identical, so that branch is untouched).
docs/planner-spec.md— new "Discharge-mode exemption (issue fix(inverter): batteries_discharge_mode slot writes a 0 W Huawei discharge cap and oscillates against the rated maximum #983)" subsection plus a pointer fromthe discharge-power-cap section.
docs/troubleshooting-guide.md— new "7i. Maximum discharging power capped at 0 W" with thelegitimate causes and the
capped max discharge power tolog pattern..github/memories.md— engineering-log entry, including the two rejected alternatives.tests/test_discharge_mode_cap_oscillation.py— new.All other 0 W paths keep immediate precedence and are covered by regression tests: EV permission
gating (#797), the planned-EV rate cap, solar-charge-only (#922), the wait-mode reserve floor (#954),
the
current_required_battery_kwhSoC guard (#592), and the read-only/degraded gates.force_batteries_discharge/force_exportalready bypassed this branch entirely.Tests
tests/test_discharge_mode_cap_oscillation.py— 16 tests:_primary_battery_cap_hold()unit coverage, including theev_smart_chargingrelabel andwait-mode cases
slot still runs
MaximizeSelfConsumptionmateriality boundary, asserting exactly one cap write
solar-charge-only, and a genuine held Wait slot
Like #939's tests these assert the exact list of writes to the entity, not just the final value —
the pre-fix behaviour passes a final-value-only assertion. Verified failing before the fix and
passing after.
Quality gates
./scripts/quality.sh lint./scripts/quality.sh typing./scripts/quality.sh quality./scripts/quality.sh test./scripts/quality.sh translationsChecklist
docs/planner-spec.mdread and updated (semantics of the discharge cap changed)no planner output is mutated
Fixes #983
🤖 Generated with Claude Code