Skip to content

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
springfall2008:mainfrom
CossieRob:battery-soc-full-hysteresis
Open

Add battery_soc_full_hysteresis to work around inverters that clamp charge current at 100% SoC#4752
CossieRob wants to merge 7 commits into
springfall2008:mainfrom
CossieRob:battery-soc-full-hysteresis

Conversation

@CossieRob

Copy link
Copy Markdown

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_max below 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() in utils.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 existing predbat.status sensor (same pattern as error_count).

C++ kernel

The fast prediction kernel (prediction_kernel.cpp) mirrors this with matching logic, and PK_PARITY_REVISION/KERNEL_PARITY_REVISION are bumped so any stale prebuilt binary falls back safely to the (correct) Python engine until CI rebuilds it.

Testing

  • New tests in tests/test_battery_full_hysteresis.py: find_charge_rate clamp behaviour, the state machine (activate/hold/clear/no false-retrigger), restart persistence, and kernel-vs-Python parity for the hysteresis scenario.
  • Full --quick suite passes (273/273, one pre-existing unrelated failure in test_enphase_api.py confirmed present on a clean main too).
  • Rebuilt and ran kernel_parity (450+ randomized scenarios) and verify_kernel_binary.py against a locally compiled kernel — all bit-identical to the Python engine.
  • black --check / flake8 show no new issues versus a clean checkout.

Default is 0 (off), so this is a no-op for everyone until they opt in.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 PredBat instance 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 in finally.
    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 a finally block.
    # 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.

Comment thread apps/predbat/execute.py Outdated
Comment thread apps/predbat/execute.py Outdated
Comment thread apps/predbat/output.py Outdated
Comment thread apps/predbat/output.py Outdated
Comment thread apps/predbat/prediction.py
Comment thread apps/predbat/prediction_kernel.cpp Outdated
Comment thread apps/predbat/tests/test_battery_full_hysteresis.py Outdated
Comment thread apps/predbat/utils.py Outdated
Comment thread apps/predbat/tests/test_battery_full_hysteresis.py Outdated
Comment thread battery-soc-full-hysteresis.patch Outdated
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Battery max charge hysterisis

2 participants