Add battery_soc_full_hysteresis to work around inverters that clamp charge current at 100% SoC - #4752
Open
CossieRob wants to merge 7 commits into
Open
Add battery_soc_full_hysteresis to work around inverters that clamp charge current at 100% SoC#4752CossieRob wants to merge 7 commits into
CossieRob wants to merge 7 commits into
Conversation
…harge current at 100% SoC
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Critical multi-inverter handling and multiple prediction, control, display, persistence, and test correctness issues remain unresolved.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds configurable full-SoC charge hysteresis for inverters that temporarily reject charging after reaching 100%.
Changes:
- Tracks and persists hysteresis state across restarts.
- Applies charging clamps in live control and Python/C++ prediction paths.
- Adds configuration, documentation, dashboard output, and tests.
File summaries
| File | Description |
|---|---|
docs/customisation.md |
Documents hysteresis configuration and behavior. |
battery-soc-full-hysteresis.patch |
Duplicates the source and test changes as an unused patch artifact. |
apps/predbat/utils.py |
Adds the shared charge-rate clamp; modeled accepted current incorrectly retains the configured minimum. |
apps/predbat/unit_test.py |
Registers the new tests. |
apps/predbat/tests/test_battery_full_hysteresis.py |
Tests state, persistence, and parity; coverage misses the active clamp and shared state is not restored. |
apps/predbat/prediction.py |
Models hysteresis in Python; uses rounded SoC and does not clamp every charging path. |
apps/predbat/prediction_kernel.py |
Extends kernel context and parity revision. |
apps/predbat/prediction_kernel.cpp |
Mirrors prediction behavior; uses rounded SoC, models nonzero delivery, and misses some charging paths. |
apps/predbat/predbat.py |
Initializes hysteresis fields. |
apps/predbat/output.py |
Publishes state and rates; display and startup persistence can diverge from actual state. |
apps/predbat/fetch.py |
Loads the configured threshold. |
apps/predbat/execute.py |
Tracks and controls hysteresis; rounded aggregate SoC is incorrect for fractional thresholds and multi-inverter fleets. |
apps/predbat/config.py |
Defines the expert-mode setting. |
Review details
Suppressed comments (2)
apps/predbat/tests/test_battery_full_hysteresis.py:208
- These helpers run against one shared
PredBatinstance for the entire registry, but this test permanently resets inverter/rate state, enables hysteresis, changes SoC, and disables the kernel. Later tests therefore depend on this test's order. Run this scenario with a fresh instance or restore every mutated PredBat and HA-interface field infinally.
reset_inverter(my_predbat)
reset_rates(my_predbat, 10.0, 5.0)
my_predbat.battery_rate_max_export = my_predbat.battery_rate_max_discharge
my_predbat.battery_soc_full_hysteresis = 3.0
my_predbat.battery_full_hysteresis_active = True # battery is already sitting at 100%, per the scenario below
apps/predbat/tests/test_battery_full_hysteresis.py:157
- This persistence test mutates both the shared PredBat fields and
ha_interface.dummy_items, then leaves the status entity removed. Because the runner passes the same fixture to every test, this can change later tests' starting state. Save and restore the original fields and status entry in afinallyblock.
# Simulate a previous run having published "active" before this process started.
my_predbat.ha_interface.dummy_items[status_entity] = {"state": "Idle", "battery_full_hysteresis_active": True}
my_predbat.battery_full_hysteresis_active = None
my_predbat.battery_soc_full_hysteresis = 3.0
my_predbat.soc_max = 10.0
my_predbat.soc_percent = 98.0 # inside the band either way, so the restored value is what decides
- Files reviewed: 13/13 changed files
- Comments generated: 13
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…ieRob/batpred into battery-soc-full-hysteresis # Conflicts: # battery-soc-full-hysteresis.patch
… (including PV/export in the C++ kernel), fix clamp value, SoC precision, and a test that never exercised the clamp
…steresis # Conflicts: # apps/predbat/inverter.py # apps/predbat/utils.py
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.
Problem
Some inverters (reported on Fox and others) drop max charge current to 0A once the battery reaches 100% SoC, and won't accept charge current again until SoC has dropped a few percent (e.g. below 97%). Predbat isn't aware of this, so it can plan and command charging that the inverter silently won't deliver — wasting charge windows and producing an inaccurate forecast. The only current workaround is capping
best_soc_maxbelow 100%, which sacrifices usable capacity.Closes #2513
Fix
Adds
input_number.predbat_battery_soc_full_hysteresis(0–20%, default 0/off, expert mode). When set to match your inverter's behaviour (e.g. 3 for a 97% resume threshold), Predbat tracks whether the battery is within that band below 100% and, while so, both plans and live-controls as if charge current is unavailable — via a single shared choke point,find_charge_rate()inutils.py, used by the simulation, live control, and dashboard display.State is tracked in
PredBat.update_battery_full_hysteresis(), called each cycle, and persists across restarts as an attribute on the existingpredbat.statussensor (same pattern aserror_count).C++ kernel
The fast prediction kernel (
prediction_kernel.cpp) mirrors this with matching logic, andPK_PARITY_REVISION/KERNEL_PARITY_REVISIONare bumped so any stale prebuilt binary falls back safely to the (correct) Python engine until CI rebuilds it.Testing
tests/test_battery_full_hysteresis.py:find_charge_rateclamp behaviour, the state machine (activate/hold/clear/no false-retrigger), restart persistence, and kernel-vs-Python parity for the hysteresis scenario.--quicksuite passes (273/273, one pre-existing unrelated failure intest_enphase_api.pyconfirmed present on a cleanmaintoo).kernel_parity(450+ randomized scenarios) andverify_kernel_binary.pyagainst a locally compiled kernel — all bit-identical to the Python engine.black --check/flake8show no new issues versus a clean checkout.Default is 0 (off), so this is a no-op for everyone until they opt in.