Skip to content

feat(sigenergy): model the grid export limit from the plant's backfeed limit - #5681

Merged
springfall2008 merged 3 commits into
mainfrom
feat/sigenergy-export-limit
Oct 21, 2026
Merged

springfall2008 merged 3 commits into
mainfrom
feat/sigenergy-export-limit

Conversation

@springfall2008

Copy link
Copy Markdown
Owner

Summary

The Sigenergy Cloud component already parsed gridMaxBackfeedPowerW from the openapi/change MQTT message into systems[id]["gridMaxBackfeedPower"], but nothing read it. automatic_config wired inverter_limit and never export_limit, so on a plant whose grid connection is capped below the inverter's AC limit (e.g. 5 kW backfeed on a 12 kW inverter), Predbat planned exports the plant can't deliver.

  • New _get_export_limit_w() returns the backfeed limit in W, or 99999 when the plant hasn't reported one. That is the same "no limit" convention the gateway component uses.
  • publish_system_entities publishes sensor.<prefix>_sigenergy_<system>_export_limit (W, device_class: power).
  • automatic_config sets export_limit to those sensors, one per system.
  • docs/components.md: a note on where the limit comes from and the 99999 fallback.

Things to check

  • The limit only arrives when the plant's configuration changes. gridMaxBackfeedPowerW only comes in an openapi/change message. Until one arrives after a restart, the sensor reads 99999 W and exports are capped by inverter_limit alone. That is the same as before this PR, so it's no regression, but the limit may not be applied straight after startup. It isn't restored from the cache.
  • A reported 0 W is treated as "no limit". _handle_mqtt_change already skips zero values (if val:), and I've kept that. Without a spec I can't tell whether 0 means "zero export" or "not configured". Treating it as zero export would stop all export planning if it actually means "not configured", which is worse than leaving things as they are.
  • maxSellPower is still not sent in battery commands. This PR only changes what Predbat plans, not how the inverter is commanded.
  • Not tested on a real Sigenergy plant.

Test plan

  • New tests publish_export_limit (an MQTT change message flows through to a 5000 W sensor), publish_export_limit_unknown (99999 fallback), and an export_limit assertion in automatic_config. All three failed before the change.
  • ./run_all --test sigenergy: 122 pass
  • ./run_all --quick: all pass
  • pre-commit on the changed files
  • Live check on a Sigenergy plant that reports a backfeed limit

🤖 Generated with Claude Code

…d limit

The Sigenergy Cloud component already parsed gridMaxBackfeedPowerW from the
openapi/change MQTT message but never used it, so Predbat planned exports up
to the inverter's AC limit even on a plant capped lower at the grid.

Publish it as sensor.<prefix>_sigenergy_<system>_export_limit (W) and wire
export_limit to it in automatic_config. Until the plant reports a limit the
sensor reads 99999 W (no limit), the same convention the gateway component
uses, so behaviour is unchanged for systems that never send one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@springfall2008
springfall2008 marked this pull request as ready for review October 21, 2026 19:28
@github-actions github-actions Bot added the BOT_REVIEW Trigger an autotriage label Oct 21, 2026

@springfall2008 springfall2008 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Reviewed against live data from a production Sigenergy plant with a 3.68 kW G98 export cap on a 12 kW inverter.

Live data point (partly ticks the "live check" box). The plant's only openapi/change message in 48 hours was:

{'antiReflux': 'on', 'batteryRatedChargePowerW': '13200.0', 'batteryRatedCapabilityWh': '27120.0', 'backupCutOffSOC%': '16.0', 'inverterMaxAbsorptionPowerW': '12000.0', 'peakShavingStatus': 'off', 'stormWatchStatus': 'off', 'batteryRatedDischargePowerW': '14400.0', 'inverterMaxActivePowerW': '12000.0', 'dischargeCutOffSOC%': '0.0', 'chargeCutOffSOC%': '100.0', 'peakShavingCutOffSOC%': '0.0', 'gridMaxBackfeedPowerW': '3680.0', 'inverterMaxApprentPowerVar': '12000.0'}

gridMaxBackfeedPowerW = 3680 matches the site's real cap, and live period messages show grid export pinned at 3.67–3.69 kW during exports. So the field is the right source. Three things before merge, the first a regression:

  1. set_arg overwrites a user's own export_limit (regression). See the inline comment. Today, a user who has set export_limit in apps.yaml gets the right plan. After this PR it is silently replaced by the sensor, which reads 99999 after every restart until a change message arrives. On the site above that would have been up to two days of no limit. gecloud.py already uses set_arg_auto("export_limit", ..., overwrite=False) for this reason. The SaaS uses exactly this kind of per-instance override as the workaround for capped Sigenergy sites, so they would regress on the next restart.

  2. The value is lost on restart. One change message in 48 hours is typical, because it only fires when the plant's settings change. A pod restarted at 20:17 had still received none at 20:30. Suggest persisting the last reported backfeed and antiReflux values in the component cache, e.g. a new SIGENERGY_CACHE_KEYS entry restored in load_cached_data(). Then the 99999 fallback only applies to a plant that has never reported.

  3. Use antiReflux. Sigenergy sends an explicit export-limiting flag next to the value ('antiReflux': 'on' above). Suggest:

    • antiReflux == 'off' → 99999 (no limit), whatever gridMaxBackfeedPowerW says.
    • antiReflux == 'on' → use gridMaxBackfeedPowerW, including 0. With limiting switched on, 0 most plausibly means zero export (unverified), which settles the PR's 0 W question. That needs _handle_mqtt_change to store 0 for this field instead of skipping it with if val:.
    • Field absent → current behaviour.

Minor: the _handle_mqtt_change docstring example could be replaced with the real payload above, which shows antiReflux.

Not covered by this PR, but worth knowing: the plan series on that site also showed 19–24 kW grid swings, beyond the 12 kW battery rate. A separate issue, not explained by the missing export limit.

Comment thread apps/predbat/sigenergy.py Outdated
Comment thread apps/predbat/sigenergy.py

@springfall2008 springfall2008 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Automated comment from the triage bot.

Reviewed head 4aa2c51: 2 findings, both inline, no blocking issues. The wiring itself is correct end to end — verified the new sensor flows through get_arg(required_unit='W') (inverter.py:802-803) into the export-rate caps in prediction (prediction.py:666) and dispatch (execute.py:390-391); that missing/0 → 99999 matches the house "no limit" convention (const.py:110, gateway.py:1419, solis.py:2447); that the three tests exercise real paths (MockSigenergyAPI subclasses SigenergyAPI) and would fail before the change; and the docs line is accurate. No kernel change needed — prediction untouched.

Non-anchor note: the PR's remaining open point (a reported 0 W treated as "no limit") is deliberate, disclosed in the body, and matches existing practice — solis maps a 0 register to 99999 too — so I agree with keeping it.

Comment thread apps/predbat/sigenergy.py Outdated
Comment thread apps/predbat/sigenergy.py Outdated
@springfall2008 springfall2008 removed the BOT_REVIEW Trigger an autotriage label Oct 21, 2026
…he the limit across restarts

PR #5681 review:
- automatic_config binds export_limit with set_arg_auto(overwrite=False), so an
  export_limit set in apps.yaml is no longer replaced by the sensor's 99999
  fallback.
- antiReflux "off" means no limit; "on" uses gridMaxBackfeedPowerW including
  0 W (zero export); without the flag the old behaviour is kept. The fields
  are merged per message into a new grid_export dict instead of systems, so a
  0 W backfeed is no longer dropped and a partial change message can't wipe
  them.
- grid_export is a new cache key restored by load_cached_data() and rewritten
  every poll, since the cache expires after a day and change messages can be
  further apart than that.
- The _handle_mqtt_change docstring example is now the live payload.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@springfall2008
springfall2008 merged commit 0eaafc6 into main Oct 21, 2026
2 checks passed
@springfall2008
springfall2008 deleted the feat/sigenergy-export-limit branch October 21, 2026 20:01
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.

1 participant