Skip to content

fix(inverter): stop the export target sitting below the min reserve SoC - #4384

Open
springfall2008 wants to merge 3 commits into
mainfrom
fix/export-target-below-min-reserve
Open

fix(inverter): stop the export target sitting below the min reserve SoC#4384
springfall2008 wants to merge 3 commits into
mainfrom
fix/export-target-below-min-reserve

Conversation

@springfall2008

@springfall2008 springfall2008 commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Problem

Reported against GE Cloud: the export target ends up set below the minimum reserve SoC.

adjust_force_export only ever pulled the export target register down to reserve_percent:

if current > self.reserve_percent:
    self.write_and_poll_value("discharge_target_soc", ..., int(self.reserve_percent))
else:
    self.log("Inverter {} Current discharge target is already set to {}".format(self.id, current))

A target already below the minimum reserve was treated as acceptable and left alone.

GE Cloud's enable_default_options resets every "lower SoC percent limit" register to 4%, at startup and again every 24 hours. That set overlaps dc_discharge_1_lower_soc_percent_limit — the exact register Predbat configures as discharge_target_soc. So a user with a higher minimum reserve was left with an inverter-side backstop of 4% that Predbat never corrected.

That register is what stops a timed export draining the battery below the reserve between Predbat's 5-minute cycles, so the battery could discharge past the configured minimum reserve.

Reproduced with battery_min_soc: 20 and the register at 4:

reserve_percent = 20
discharge_target_soc before = 4
discharge_target_soc after  = 4     <- never corrected

Fixes

inverter.py — track the reserve in both directions. Compare against the target rather than only testing for a higher value, so the register follows reserve_percent up as well as down. Applied to both the REST v3 and the entity write paths; target_soc is hoisted out of the two branches.

This part is not GE Cloud specific — it applies to every inverter exposing discharge_target_soc (fox, solax, solis, deye, enphase, sigenergy, teslemetry).

gecloud.py — stop resetting the register Predbat owns. enable_default_options now resolves the configured discharge_target_soc entities and skips the matching register before any reset rule runs, rather than fighting adjust_force_export every 24 hours. The match is per device and rebuilds the entity ID the same way publish_registers and async_automatic_config do, so it is exact rather than a substring guess. Registers on an inverter Predbat is not driving, and inverters with no discharge target register at all, keep the existing 4% reset.

Tests

test_discharge_target_tracks_reserve in apps/predbat/tests/test_inverter.py:

  • a target below the reserve is raised to the reserve (the bug)
  • a target above the reserve is still lowered to the reserve (no regression)
  • the same correction on the REST v3 path

_test_enable_default_options_skips_discharge_target in apps/predbat/tests/test_ge_cloud.py:

  • the configured export target register is left untouched, with the changed flag unset
  • other lower SoC registers on the same device are still reset to 4%
  • the same register on a device Predbat is not driving is still reset
  • the reset still applies when no discharge target is configured

Both were written first and confirmed failing before the fix. MockGECloudDirect.get_arg gained **kwargs so it accepts the indirect keyword the real ComponentBase.get_arg takes; the real get_arg was checked separately to confirm indirect=False returns the raw entity list rather than resolving entity states.

./run_all --quick and ./run_pre_commit both exit 0.

Also included

A .gitignore entry for .claude/worktrees/, which was showing up as untracked in every git status. Unrelated to the fix — happy to split it out if you would rather keep this PR single-purpose.

🤖 Generated with Claude Code

adjust_force_export only ever pulled the export target register down to
reserve_percent - a target already below the minimum reserve SoC was
logged as "already set" and left alone. GE Cloud resets that register
(dc_discharge_1_lower_soc_percent_limit) to 4% in enable_default_options
at startup and every 24 hours, so a user with a higher minimum reserve
ended up with an inverter side backstop of 4% that Predbat never
corrected, letting a timed export drain past the reserve between
Predbat cycles.

Compare against the target instead of only testing for a higher value so
the register tracks reserve_percent in both directions, on both the REST
v3 and the entity write paths.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 30, 2026 10:52
.claude/worktrees/ holds locally created git worktrees and was showing up
as untracked in every git status.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

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.

Pull request overview

Fixes Inverter.adjust_force_export() so the inverter-side export target SoC (discharge_target_soc) always matches the configured minimum reserve SoC in both directions (raising targets that are too low as well as lowering targets that are too high). This prevents timed exports from draining the battery below the configured reserve between Predbat’s 5‑minute control cycles.

Changes:

  • Update adjust_force_export() to set the discharge target when current != reserve_target (instead of only when current > reserve_percent) for both REST v3 and HA entity paths.
  • Hoist target_soc = int(self.reserve_percent) to avoid duplicated conversions and keep the logic consistent.
  • Add a regression test covering “below reserve”, “above reserve”, and REST v3 behavior.

Reviewed changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated no comments.

File Description
apps/predbat/inverter.py Ensures discharge_target_soc is corrected to the configured reserve even when it’s below reserve (prevents under-reserve draining during export).
apps/predbat/tests/test_inverter.py Adds regression coverage verifying the target tracks reserve in both directions, including REST v3.

…arget

enable_default_options resets every "lower SoC percent limit" register to
4%, which overlaps the DC discharge lower SoC limit that Predbat has
configured as discharge_target_soc. Predbat tracks the minimum reserve
SoC on that register, so the reset was fighting adjust_force_export.

Resolve the configured discharge_target_soc entities and skip the
matching register. The match is per device and uses the same entity ID
form as publish_registers, so a register on an inverter Predbat is not
driving is still reset, as is the case where the inverter has no
discharge target register at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

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.

Pull request overview

Copilot reviewed 4 out of 5 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

apps/predbat/inverter.py:2499

  • target_soc = int(self.reserve_percent) floors when reserve_percent is a float (it’s read using a float default), which can still set the inverter’s discharge target below the configured minimum reserve (e.g. 20.5% -> 20%). Since this register is a safety backstop, it should be rounded up to avoid dipping under the minimum reserve.
        if force_export:
            target_soc = int(self.reserve_percent)
            if self.rest_data and self.rest_v3:

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.

2 participants