Sigenergy: stop Predbat and Axle fighting over the inverter, and make axle_control work - #4457
Merged
Merged
Conversation
A Sigenergy accepts one controller at a time, and VPP mode (Predbat) and the NorthBound Interface (Axle's dispatch channel) are mutually exclusive. Today Predbat wins that contest silently, on a 5 minute timer, and the axle_control option that is supposed to let Axle win does not work here at all. Observed on a live system on 2026-08-06: Axle took the inverter into Northbound Integration at 19:35:18 and it was back in VPP at 19:40:12 — exactly one SIGENERGY_POLL_INTERVAL — while the log only said "controls skipped until onboard is approved". Make axle_control actually work on Sigenergy: - _axle_has_control() evaluates axle_control and the Axle event sensor LIVE rather than reading Fetch's cached set_read_only_axle. That flag is only refreshed by the 5 minute prediction loop and is still False from reset() when this phase-1 component makes its first run — precisely the case that matters, a restart during a live event, where the cached flag would have had Predbat reclaim VPP and kill the dispatch. - during an event the operating mode is left exactly as it is. If Axle has moved the system to NBI it stays there; if the event has started but Axle has not switched yet, do not drop to MSC either — that hands control to the owner's app rather than to Axle. - battery commands are suppressed for the same window. - VPP is reclaimed when the event ends, and that is logged. And where Predbat does keep ownership, make it deliberate rather than a race: - reclaim VPP every minute instead of every 5. This also lets an event start or end be picked up promptly now that ownership is evaluated here. set_operating_ mode is an MQTT publish that only fires when the mode is wrong, so it costs nothing against the REST rate limit. - log the reclaim naming the controller being displaced. - record last_contended_by on the status sensor, published on the same minute cadence and never cleared. Contention is usually shorter than a publish interval, so a marker reset on recovery would almost never be seen; "has this happened" is the useful signal, and in_vpp already answers "right now". - stop reporting a system in NBI as pending_approval. The SaaS UI renders that as an amber "approve this in the Sigenergy app" banner, so every Axle event told the user to approve something that needed no approval. Behaviour with axle_control unset is unchanged except for the faster reclaim, the clearer logs and the status fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 7, 2026
springfall2008
marked this pull request as ready for review
August 7, 2026 08:06
Contributor
There was a problem hiding this comment.
Pull request overview
This PR updates the Sigenergy integration to handle single-controller contention explicitly: Predbat now either (a) deliberately re-asserts VPP ownership on a 60s cadence when it should own the inverter, or (b) stands down cleanly during live Axle events when axle_control is enabled, leaving operating mode untouched and suppressing battery commands.
Changes:
- Add live Axle-event evaluation (
fetch_axle_active) to makeaxle_controleffective immediately (including across restarts) and suppress controls during events. - Reclaim VPP ownership every minute (instead of every poll interval) and log/record contention via a persistent
last_contended_bymarker; treat NBI contention as “active” (not “pending_approval”). - Expand Sigenergy unit tests with regression coverage, and update the Sigenergy mock
get_arg()signature to accept**kwargsto matchComponentBase.get_arg.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| apps/predbat/sigenergy.py | Implements minute-cadence VPP reclaim, Axle stand-down (axle_control) via live sensor evaluation, improved contention/status reporting, and control suppression during events. |
| apps/predbat/tests/test_sigenergy.py | Adds regression tests for contention/marker publication and Axle stand-down behavior; updates test mock get_arg() to accept broader kwargs. |
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Suppressed comments (2)
apps/predbat/sigenergy.py:2231
- When a system is offboarded, _manage_vpp_registration() returns before clearing _axle_standoff_logged. If the offboard switch is enabled mid-Axle event, the stand-down flag can remain set and later produce a misleading “Axle VPP event ended — resuming control…” log when offboard is turned off (even though the event may have ended earlier). Clearing the flag on the offboard path avoids stale state/logging.
if is_offboard:
return False
apps/predbat/sigenergy.py:2212
- The docstring says an active Axle event under axle_control “takes priority over every case above”, but the implementation returns early for is_offboard before checking Axle control. Either move the Axle check above the offboard return, or clarify in the docstring that offboard still short-circuits all mode management.
An active Axle event under the ``axle_control`` option takes priority over every
case above: Predbat stands down and leaves the operating mode untouched so Axle can
drive the battery through the NorthBound Interface.
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.
Supersedes #4455 and #4456, which were opened as competing alternatives. A review showed they were not actually alternatives — see #4454 for the reasoning — so this is the two of them combined, with the review findings fixed.
Fixes #4454.
The problem
A Sigenergy accepts one controller at a time. VPP mode (Predbat) and the NorthBound Interface (Axle's dispatch channel) are mutually exclusive. Two things follow, and both are broken:
Where Predbat owns the inverter, it wins by accident.
_manage_vpp_registration()ran on the 5-minute poll and silently switched the system back to VPP, so an Axle dispatch could hold it for up to 5 minutes before being displaced, logged only as "controls skipped until onboard is approved".Where Axle should own it,
axle_controldoes not work at all.Fetch.fetch_config_options()raisesset_read_only_axlefor exactly this purpose, but it was only consumed inexecute.pyandoutput.py.sigenergy.pyread the user-facingswitch.predbat_set_read_onlyinstead, so it never learned Axle had the floor.Observed live on 2026-08-06: Axle took the inverter into Northbound Integration at 19:35:18; it was back in VPP at 19:40:12 — exactly one
SIGENERGY_POLL_INTERVAL.Make
axle_controlwork here_axle_has_control()evaluatesaxle_controland the Axle event sensor live, viafetch_axle_active(), rather than reading the cachedset_read_only_axle. That flag is only refreshed by the 5-minute prediction loop and is stillFalsefromreset()when this phase-1 component makes itsfirst=Truerun — precisely the case that matters, a restart during a live event, where the cached flag would have had Predbat reclaim VPP and kill the dispatch. It also removes an up-to-5-minute lag detecting event start and end.And where Predbat does keep ownership, make it deliberate
set_operating_mode()is an MQTT publish that only fires when the mode is actually wrong, so this costs nothing against the REST rate limit.last_contended_byon the status sensor, published on the same minute cadence and never cleared. Contention is usually shorter than one publish interval, so a marker reset on recovery would almost never be seen; "has this happened" is the useful signal, andin_vppalready answers "is it happening now".pending_approval. Downstream UIs render that as an amber "approve this in the Sigenergy app" banner, so every Axle event told an already-onboarded user to approve something that needed no approval.Compatibility
With
axle_controlunset — which is the current state of every system we operate — behaviour is unchanged except for the faster reclaim, the clearer logs, and the status fix. Nothing here silently changes who drives the battery.Note that
axle_controlis a global option: it gatesself.set_read_only, which covers the whole inverter-write path for any inverter type, not just Sigenergy. Enabling it is a separate, wider decision than merging this.Tests
11 tests in
tests/test_sigenergy.py, including two explicit regression tests for the review findings:axle_standoff_survives_restart(the stale-flag case) andcontention_marker_published_before_recovery(the marker being cleared before it was ever published). Full Sigenergy suite 78 pass / 0 fail;unit_test.py --quickpasses with 0 failures;run_pre_commitclean.One test-infra note:
MockSigenergyAPI.get_arg()had to accept**kwargs, becausefetch_axle_active()passesindirect=Falseand the mock's two-argument signature rejected it. The mock had drifted fromComponentBase.get_arg; this widens it rather than fully realigning it.What this does NOT do
Two honest limits, raised in review:
axle_controlunset it converts a five-minute contest into a one-minute contest and makes Predbat win deterministically. That is the intended ownership policy, not a resolution of the conflict.apply_controls()), and nothing revokes it. If Axle moves the system to NBI that command stops applying; if Axle does not, it can remain active with no acknowledgement from either side that control changed hands. Enableaxle_controlselectively and watch the actual mode telemetry.Worth stating plainly for context:
load_axle_slot()addspence_per_kwhtorate_exportand adjusts load scaling. An Axle session is a price signal, not a required delivery — the optimiser may still decline to export on SOC, reserve or economics. Neither this PR noraxle_controlchanges that.