Skip to content

Gateway MQTT: a missing max battery rate is replaced with a hard-coded 6000 W, which is then written to the inverter #5478

Closed

Description

@springfall2008

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_w becomes 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

  1. Do not substitute a default for a missing rating. If rate_max_w is zero or unset, keep the last good value for that inverter.
  2. 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.
  3. Apply the same rule to the inverter limit and the export limit: a missing value should not become a larger or unlimited one.

Related

Tests to add

  • Status with rate_max_w populated, then a status with it zero: the published battery_rate_max and inverter_rate_max stay at the last good value.
  • First status after start-up with rate_max_w zero: 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

  1. springfall2008 commented on Oct 10, 2026

    @springfall2008
    OwnerAuthor

    One clarification on the export limit. The code comment shows that export_limit_w == 0 is 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.

  2. springfall2008 commented on Oct 10, 2026

    @springfall2008
    OwnerAuthor

    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_w or control.export_limit_w unpopulated, 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:1115 falls inverter_rate_max back to that same fabricated 6000, so both published limits become the assumption.
    • gateway.py:1123-1124 — a proto-0 (undefined) export_limit_w is published as 99999, read back by inverter.py:755-756 as the 100.0 kW export limit in the quoted log line.
    • The entities wire into Predbat's args at gateway.py:1548 (battery_rate_max), 1527 (inverter_limit) and 1526 (export_limit); consumed at inverter.py:622-623 and 643-655, where battery_rate_max_charge/discharge = min(inverter_limit_charge/discharge, battery_rate_max_raw) — both sides of that min() become 6000, so the plan's maximum rate doubles for as long as the fields are missing.
    • The write itself goes through the executor's reset/balancer path (execute.py:286-295) into adjust_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.
    • One addition to the impact list: during the same window _check_rate_caps() (gateway.py:1933) also sends set_rate_cap with 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.
    • The reserve rewrite in the same window fits the same class: an unpopulated control.reserve_soc publishes 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() in gecloud.py:542 keeps previous values when fresh fields are null, and gateway.py has 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 gateway test module passes against main; the three cases in the report's "Tests to add" are all new coverage for test_gateway.py.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions