Skip to content

Sigenergy: stop Predbat and Axle fighting over the inverter, and make axle_control work - #4457

Merged
springfall2008 merged 1 commit into
mainfrom
fix/sigenergy-axle-control-contention
Aug 7, 2026
Merged

Sigenergy: stop Predbat and Axle fighting over the inverter, and make axle_control work#4457
springfall2008 merged 1 commit into
mainfrom
fix/sigenergy-axle-control-contention

Conversation

@mgazza

@mgazza mgazza commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

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:

  1. 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".

  2. Where Axle should own it, axle_control does not work at all. Fetch.fetch_config_options() raises set_read_only_axle for exactly this purpose, but it was only consumed in execute.py and output.py. sigenergy.py read the user-facing switch.predbat_set_read_only instead, 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_control work here

  • _axle_has_control() evaluates axle_control and the Axle event sensor live, via fetch_axle_active(), rather than reading the 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=True 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. It also removes an up-to-5-minute lag detecting event start and end.
  • 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

  • Reclaim VPP every minute instead of every 5. 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.
  • 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 one 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 "is it happening now".
  • Stop reporting a system in NBI as 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_control unset — 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_control is a global option: it gates self.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) and contention_marker_published_before_recovery (the marker being cleared before it was ever published). Full Sigenergy suite 78 pass / 0 fail; unit_test.py --quick passes with 0 failures; run_pre_commit clean.

One test-infra note: MockSigenergyAPI.get_arg() had to accept **kwargs, because fetch_axle_active() passes indirect=False and the mock's two-argument signature rejected it. The mock had drifted from ComponentBase.get_arg; this widens it rather than fully realigning it.

What this does NOT do

Two honest limits, raised in review:

  • The faster reclaim does not make Predbat and Axle coexist. With axle_control unset 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.
  • The stand-down is best-effort mutual exclusion, not a dispatch protocol. During an event the mode is left untouched and commands are suppressed, but a battery command issued just beforehand can carry a duration of up to 720 minutes (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. Enable axle_control selectively and watch the actual mode telemetry.

Worth stating plainly for context: load_axle_slot() adds pence_per_kwh to rate_export and 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 nor axle_control changes that.

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>

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

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 make axle_control effective 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_by marker; 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 **kwargs to match ComponentBase.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.

@springfall2008
springfall2008 requested a lite review from Copilot August 7, 2026 08:21
@springfall2008
springfall2008 merged commit bf4724a into main Aug 7, 2026
4 checks passed
@springfall2008
springfall2008 deleted the fix/sigenergy-axle-control-contention branch August 7, 2026 08:21

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 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.

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.

Sigenergy: Predbat and Axle contend for the inverter — VPP mode evicts Axle's NBI dispatch

3 participants