Repository navigation
feat(gateway): send each inverter's charge/discharge rate cap to the hub (set_rate_cap) - #5387
Merged
Merged
Conversation
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>
Contributor
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Zero ceilings can become unrestricted rates, and failed cap-only publications suppress subsequent retries.
Review effort: Balanced
Findings: 1
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.
…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>
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>
_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>
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.


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
set_rate_cap {"serial", "charge_cap_w", "discharge_cap_w"}, one per inverter. The hub stores it. No protobuf or plan change._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:_inverter_slot_serials), and each inverter's cap is sent under the serial of its slot.battery_rate_max_charge/battery_rate_max_discharge, which is already limited byinverter_limit_charge/inverter_limit_dischargeandbattery_rate_max.inverter_typeis notGWMQTT, and one whose limits PredBat has not read yet (a cap of 0 would mean "no cap" to the hub).Compatibility
Review focus
inverter_reset, the command is fire-and-forget: a refusal from the hub is not retried until one of the re-send triggers above.ExecutionPlan; that is reverted, andgateway_status.proto/gateway_status_pb2.pyare identical to main.Testing
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 gatewaypasses.unit_test.py --quickpassed before the last change (the hub-offline gate); only the gateway suite was re-run after it.🤖 Generated with Claude Code