Add DC anti-sleep floor (min_dc_output) for DC-coupled batteries#428
Closed
tomquist wants to merge 3 commits into
Closed
Add DC anti-sleep floor (min_dc_output) for DC-coupled batteries#428tomquist wants to merge 3 commits into
tomquist wants to merge 3 commits into
Conversation
Some DC-coupled batteries (e.g. Marstek B2500) shut their inverter off and get stuck asleep when AstraMeter commands their output to 0 W under PV surplus. Add a configurable floor that, when such a battery would be steered to 0 W in charge territory, instead holds it at a small charge-direction target so the inverter stays awake. A DC battery can't actually AC-charge, so no energy is moved. The floor only ever affects non-AC-chargeable batteries (reuses _is_ac_chargeable); AC-chargeable Venus models absorb surplus by charging and are never touched. It also respects intentional parking (efficiency deprioritization and distribution_weight=0), and gates its skip on the battery's actual reported output so a B2500 that can't follow a large negative command is still floored. Exposed two ways: - global [CT002]/[CT003] MIN_DC_OUTPUT config value (default 0 = off); - per-battery override via a new "Min DC Output" MQTT/HA number entity, published for DC batteries only (mirrors distribution_weight). Mirrored across the Python and ESPHome C++ stacks (balancer, CT002 consumer state, MQTT insights, HA discovery, config schema, test hooks) with Python and C++ unit tests, a differential parity scenario, and a byte-identical wire e2e for the cross-talk fields. https://claude.ai/code/session_016kDeCqYZ9nAKCsAVxT99uS
- Fix a Python↔C++ divergence: `_apply_dc_floor` now early-returns when the queried consumer is absent from the report snapshot, matching the C++ `apply_dc_floor_` (which already returned the unfloored value). Previously Python fabricated a `-floor` reading on phase A for a consumer it had no device_type/phase for. Add a regression test. - Clamp the global `min_dc_output` to [0, 100] in both BalancerConfig implementations, matching the per-consumer setter, MQTT handler, HA entity, ESPHome schema and web cap (was unbounded on the Python side). - Close the per-consumer-override parity gap: the differential balancer harness wire format now carries a per-report `min_dc_output` token, and the DC-floor scenario exercises an override, a global fallback and a per-battery disable across both stacks. - Clarify the README wording for the per-battery "Min DC Output" entity. https://claude.ai/code/session_016kDeCqYZ9nAKCsAVxT99uS
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Contributor
|
The anti-sleep floor must keep the battery's *output/discharge* inverter running, so it has to command a small DISCHARGE (positive target = feed-in), not a charge. A negative (charge) command to a DC-coupled battery that can't AC-charge leaves its output at 0, so the inverter still sleeps — which is the exact failure mode #425 reports. Sign convention (verified via test_balancer_distribution_weight and the simulator's `new_output = current + grid_reading`): positive grid reading = discharge. The floor now emits `grid_reading = floor - reported`, which lands net output at exactly +floor and naturally settles to a 0 reading once there (holds, no oscillation back to 0 W). Dropped the charge-side early-return accordingly. Updated both stacks (balancer.py / balancer.cpp), all unit tests, the differential parity scenario, and the config/README/CHANGELOG wording (it's a small feed-in, with the minor battery-drain trade-off, not a zero-energy charge nudge). https://claude.ai/code/session_016kDeCqYZ9nAKCsAVxT99uS
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
Implements a configurable anti-sleep floor for DC-coupled batteries (e.g. Marstek B2500) that prevents their inverters from shutting off and getting stuck asleep when commanded to 0 W under PV surplus. When enabled, the balancer holds such batteries at a small charge-direction output instead of letting them idle at 0 W.
Key Changes
min_dc_outputparameter (watts, 0–100, default 0 = disabled) toBalancerConfigin both Python and C++min_dc_outputfield to consumer reports, allowing per-battery overrides of the global floor via MQTT_apply_dc_floor()(Python) andapply_dc_floor_()(C++) methods that:last_targetunchanged to avoid false saturation scoringmin_dc_outputfield to consumer snapshots and Home Assistant discovery (published for DC batteries only)MIN_DC_OUTPUTsetting toconfig.ini.exampleand web config schematest_balancer_min_dc_output.py) and C++ (LoadBalancer.AutoFloor*tests), plus parity tests comparing both stacksImplementation Details
The floor is applied at two points in the balancer pipeline:
_steer_to_zero()for charge-blind batteries_split_by_phase()for fair-share targetsThis ensures the floor catches both cases where a DC battery would be commanded toward 0 W. The implementation intentionally does NOT update
last_targetto avoid making the saturation tracker think a DC battery (which physically cannot AC-charge) is "unable to follow" its target.Mirrors the C++ host test
LoadBalancer.AutoFloor*behavior exactly; parity verified via differential fuzzing.https://claude.ai/code/session_016kDeCqYZ9nAKCsAVxT99uS