Skip to content

feat(gateway): send each inverter's charge/discharge rate cap to the hub (set_rate_cap) - #5387

Merged
springfall2008 merged 6 commits into
mainfrom
feat/gateway-plan-rate-caps
Oct 4, 2026
Merged

springfall2008 merged 6 commits into
mainfrom
feat/gateway-plan-rate-caps

Conversation

@springfall2008

@springfall2008 springfall2008 commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

PredBat side of Predictive-Cloud-Ltd/predbat-gateway#424. The hub side is Predictive-Cloud-Ltd/predbat-gateway#428; the two can merge in either order.

Problem

When the PredBat Hub loses the cloud it runs the cached plan itself. Inside a window it uses the window's power_w, which PredBat has already capped. Outside any window it has to pick a rate with nothing to go on, and it used the inverter's rated power. On a site capped at 3.6 kW discharge that left the inverter set to 6 kW, and a stored window then ran at 6 kW while the hub's link to the inverter was down.

Change

  • New command: set_rate_cap {"serial", "charge_cap_w", "discharge_cap_w"}, one per inverter. The hub stores it. No protobuf or plan change.
  • When it is sent: _check_rate_caps() runs on each cycle and is gated like _check_inverter_resets(). Each inverter's cap is sent once, and again only when:
    • the cap changes (the user edits a limit);
    • read-only mode is switched off;
    • the hub comes back online. Commands are not retained, so nothing is sent to an offline hub, and a hub that returns may be on new firmware or have lost its stored caps.
  • Which inverter is which: auto-config already binds PredBat inverter slot N to a hub inverter, sorted by serial. It now records that list (_inverter_slot_serials), and each inverter's cap is sent under the serial of its slot.
  • Values: each inverter's own battery_rate_max_charge / battery_rate_max_discharge, which is already limited by inverter_limit_charge / inverter_limit_discharge and battery_rate_max.
  • Not sent: an inverter whose inverter_type is not GWMQTT, and one whose limits PredBat has not read yet (a cap of 0 would mean "no cap" to the hub).

Compatibility

  • Hub firmware without the command refuses it and nothing changes.
  • Hub firmware with the command, talking to a PredBat without this change, receives no cap and behaves as it does today.

Review focus

  • Like inverter_reset, the command is fire-and-forget: a refusal from the hub is not retried until one of the re-send triggers above.
  • The first commits on this branch carried the caps in ExecutionPlan; that is reverted, and gateway_status.proto / gateway_status_pb2.py are identical to main.

Testing

  • 12 new cases in tests/test_gateway.py: command format, a cap per serial, slot id rather than list order, non-gateway inverters skipped, sent once, re-sent on change, read-only, hub offline then back, not alive, before auto-config, limits not read yet, and auto-config recording slot serials.
  • unit_test.py --test gateway passes. unit_test.py --quick passed before the last change (the hub-offline gate); only the gateway suite was re-run after it.
  • pre-commit passes on the changed files.
  • Not run against a hub.

🤖 Generated with Claude Code

When the hub runs the cached plan itself (autonomous mode) it has to pick a
charge and discharge rate outside any window. It had no copy of PredBat's
limits, so it used the inverter's rated power - 6 kW on a site capped at
3.6 kW (predbat-gateway#424).

ExecutionPlan gains charge_cap_w / discharge_cap_w (fields 5 and 6). The
gateway component fills them with the smallest per-inverter rate among the
inverters it drives (inverter_type GWMQTT), after inverter_limit_charge /
inverter_limit_discharge and battery_rate_max. The hub applies one cap pair
to every inverter, so the summed site rate the plan hook receives would be
too loose on a multi-inverter site.

A change in the caps alone now counts as a plan change, so it is published
with a new version, and the periodic re-publish carries the caps too.

Hub firmware without the fields ignores them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 4, 2026 14:10

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.

Copilot review overview

🟡 Changes recommended

Zero ceilings can become unrestricted rates, and failed cap-only publications suppress subsequent retries.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity

Open (2)
What changed in this PR

Adds per-inverter rate caps to PredBat’s gateway plans so the hub can respect site limits during autonomous operation.

Changes:

  • Extends the plan schema with charge and discharge caps.
  • Derives caps from gateway-controlled inverters and includes them in change detection and re-publishing.
  • Adds serialization and publishing regression tests.
File Description
apps/​predbat/​tests/​test_gateway.py Tests cap selection, serialization, and publishing.
apps/​predbat/​gateway.py Calculates, publishes, and logs rate caps.
apps/​predbat/​gateway_status.proto Defines plan-level cap fields.
apps/​predbat/​gateway_status_pb2.py Updates generated protobuf bindings.
Files not reviewed (1)
  • apps/predbat/gateway_status_pb2.py: Generated file

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread apps/predbat/gateway.py Outdated
Comment thread apps/predbat/gateway.py
…plan

Two inverters on one hub can have different limits, so a single cap pair
could not say which was which. The plan now carries `repeated
InverterRateCap rate_caps = 5` ({serial, charge_cap_w, discharge_cap_w}) in
place of the two plan-level fields.

Auto-config records the serial bound to each PredBat inverter slot
(_inverter_slot_serials). Each GWMQTT inverter's battery_rate_max_charge /
discharge is sent under the serial of its slot. An inverter of another type,
or a plan executed before auto-config, sends no cap and the hub uses rated
power.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@springfall2008 springfall2008 changed the title feat(gateway): send the site's charge/discharge rate caps with the plan feat(gateway): send each inverter's charge/discharge rate cap with the plan Oct 4, 2026
pre-commit-ci-lite Bot and others added 3 commits October 4, 2026 14:19
Replaces the plan-carried caps. gateway_status.proto and the plan builder are
back to what is on main; the caps were the same data in every plan.

_check_rate_caps() runs on each cycle, gated like _check_inverter_resets(),
and sends `set_rate_cap {serial, charge_cap_w, discharge_cap_w}` for each
GWMQTT inverter. It is sent once, and again only when the cap changes,
read-only mode is switched off, or the hub comes back online (commands are
not retained, so nothing is sent to an offline hub).

The serial comes from auto-config, which now records the serial bound to
each PredBat inverter slot (_inverter_slot_serials).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@springfall2008 springfall2008 changed the title feat(gateway): send each inverter's charge/discharge rate cap with the plan feat(gateway): send each inverter's charge/discharge rate cap to the hub (set_rate_cap) Oct 4, 2026
@springfall2008
springfall2008 requested a balanced review from Copilot October 4, 2026 15:00

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.

Copilot review overview

🟡 Changes recommended

Offline/online transitions between housekeeping calls can prevent required cap resends after a hub restart.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
Resolved since last review (2)

Comment thread apps/predbat/gateway.py
_check_rate_caps() only noticed the hub was offline if a run() cycle landed
while it was. A reboot after a firmware update is quicker than the gap
between cycles, so the offline state was never seen and unchanged caps were
not sent again - leaving a hub that had just gained the command, or lost its
stored caps, without them.

The online-message handler now clears what was sent on any online/offline
transition.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@springfall2008 springfall2008 self-assigned this Oct 4, 2026
@springfall2008
springfall2008 merged commit 2e386aa into main Oct 4, 2026
2 checks passed
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