Repository navigation
Gateway MQTT: a missing max battery rate is replaced with a hard-coded 6000 W, which is then written to the inverter #5478
Description
Summary
When the Gateway (Hub) reports no maximum battery rate for an inverter, GatewayMQTT substitutes a hard-coded 6000 W. PredBat then plans on, and writes, a rate the inverter may not support. On a site with 3000 W inverters this produced set_charge_rate and set_discharge_rate commands of 6000 W.
What happened
A site with two GivEnergy AC-coupled battery inverters, each rated 3000 W, on one Hub.
For five minutes after the Hub restarted on new firmware, PredBat's view of one inverter changed from:
Inverter 1 with soc_max 9.52kWh ... battery rate raw 3000.0W, charge rate 3.0kW, discharge rate 3.0kW ... AC limit 3.0kW, export limit 6.0kW
to:
Inverter 1 with soc_max 9.52kWh ... battery rate raw 6000.0W, charge rate 6.0kW, discharge rate 6.0kW ... AC limit 6.0kW, export limit 100.0kW
The other inverter was unaffected. During those five minutes the inverter-balancing logic, restoring the inverter to its full rate, sent:
Inverter 1 current discharge rate is 3000W and new target is 6000W
GatewayMQTT: Published command: set_discharge_rate payload={"command": "set_discharge_rate", ..., "power_w": 6000, "serial": "<serial>"}
GatewayMQTT: set_discharge_rate for <serial> acknowledged by hub (...) applied={'power_w': 6000, 'slot': 1}
Inverter 1 write_and_poll_value: Wrote 6000 to discharge_rate, successfully now 6000.0
Two discharge-rate writes and one charge-rate write of 6000 W were sent and acknowledged. On the next cycle the reading returned to 3000 W and PredBat wrote 3000 W again. In the same window the reserve read as 0% and was rewritten.
Cause in the code
In apps/predbat/gateway.py on main:
battery_rate_max = bat.rate_max_w or 6000
...
inverter_rate_max = inv.inverter.rate_max_w or battery_rate_max
...
_raw_export_limit = control.export_limit_w
export_limit_publish = 99999 if _raw_export_limit == 0 else (0 if _raw_export_limit == 1 else _raw_export_limit)- A zero or unset
rate_max_wbecomes 6000 W. - The inverter limit falls back to that same 6000 W.
- A zero export limit is published as no limit.
So a status message with those fields unpopulated turns a 3 kW inverter with a 6 kW export limit into a 6 kW inverter with no export limit.
The trigger here is inferred from timing: the unpopulated values appeared within two minutes of the Hub reporting its new firmware version. I have not captured the raw status message.
Impact
- The plan is computed on double the real charge and discharge rate, with no export limit, for as long as the fields are missing.
- Rates above the inverter's rating are written to it. None was refused in this case, but the effect on the inverter is not known.
- It can recur on any Hub restart or telemetry gap, on any site whose inverters are not 6000 W.
Proposed behaviour
- Do not substitute a default for a missing rating. If
rate_max_wis zero or unset, keep the last good value for that inverter. - If there is no last good value (first telemetry after start-up), treat the inverter's ratings as not yet known and hold off control writes that depend on them, instead of assuming 6000 W.
- Apply the same rule to the inverter limit and the export limit: a missing value should not become a larger or unlimited one.
Related
- Gateway MQTT: an inverter unreadable at start-up is dropped until the next restart, even when listed in gateway_inverter_serial #5471: an inverter unreadable at start-up is dropped until the next restart. Same theme: missing Hub data should be treated as unavailable, not replaced with an assumption.
Tests to add
- Status with
rate_max_wpopulated, then a status with it zero: the publishedbattery_rate_maxandinverter_rate_maxstay at the last good value. - First status after start-up with
rate_max_wzero: no 6000 W value is published and no rate write above the later-reported rating is sent. - Export limit zero after a non-zero value: the limit does not become unlimited.
Activity
springfall2008 commentedon Oct 10, 2026
One clarification on the export limit. The code comment shows that
export_limit_w == 0is a deliberate sentinel for "undefined", published as unlimited. That mapping is fine for an inverter that has never reported a limit. The concern here is narrower: an inverter that had been reporting a 6 kW export limit was read as unlimited for five minutes when the field went to zero. Point 3 of the proposal should be read as: once a real limit has been seen for an inverter, a later "undefined" should keep the last good value.springfall2008 commentedon Oct 10, 2026
Automated first-pass triage — a maintainer will review before any action is taken.
Classification: bug. When the hub's status message leaves
battery.rate_max_w,inverter.rate_max_worcontrol.export_limit_wunpopulated, GatewayMQTT substitutes a hard-coded 6000 W and an unlimited export limit rather than keeping the inverter's last good values, and Predbat then plans and writes at those fabricated limits until the hub reports real ones again.Priority: medium.
Verified against main at v9.3.7-4-gd7d8c607 — all of the report's code pointers check out, every step of the chain:
gateway.py:1098—battery_rate_max = bat.rate_max_w or 6000;gateway.py:1115fallsinverter_rate_maxback to that same fabricated 6000, so both published limits become the assumption.gateway.py:1123-1124— a proto-0 (undefined)export_limit_wis published as 99999, read back byinverter.py:755-756as the 100.0 kW export limit in the quoted log line.gateway.py:1548(battery_rate_max),1527(inverter_limit) and1526(export_limit); consumed atinverter.py:622-623and643-655, wherebattery_rate_max_charge/discharge = min(inverter_limit_charge/discharge, battery_rate_max_raw)— both sides of thatmin()become 6000, so the plan's maximum rate doubles for as long as the fields are missing.execute.py:286-295) intoadjust_discharge_rate()(inverter.py:2246) — that is the quoted "current discharge rate is 3000W and new target is 6000W" line, acknowledged by the hub per the report._check_rate_caps()(gateway.py:1933) also sendsset_rate_capwith the fabricated 6000 W — it only skips caps of zero or less (gateway.py:1928) and the hub stores the cap (gateway.py:1943) — so the assumption is written to the hub's persisted rate cap too, until the next telemetry re-sends the real one.control.reserve_socpublishes literal 0 (gateway.py:1144), so Predbat re-writes its reserve against a wrong current value.The export-limit sentinel (proto 0 → unlimited) is deliberate per the comment at
gateway.py:1122, and the report's own clarification comment narrows the ask correctly: once a real limit has been seen for an inverter, a later "undefined" should keep the last good value, while never-reported can stay unlimited.For the fix shape, in-tree precedent exists:
merge_non_null()ingecloud.py:542keeps previous values when fresh fields are null, andgateway.pyhas no equivalent last-good cache for the rate/limit fields. Related: #5471 (same component, same theme of treating missing hub data as an assumption rather than unavailable — fix in flight on PR #5475).The
gatewaytest module passes against main; the three cases in the report's "Tests to add" are all new coverage fortest_gateway.py.fix(gateway): keep the last good ratings when the hub stops reporting…
fix(gateway): hold rate writes until an inverter has reported its max…
fix(gateway): keep the last good reserve, and store the export limit (#…
fix(gateway): keep configured inverters when the hub cannot read them (…