Repository navigation
fix(gateway): keep configured inverters when the hub cannot read them (#5471) - #5475
Merged
springfall2008 merged 10 commits intoOct 11, 2026
Merged
Conversation
…#5471) An inverter the hub could not read when Predbat started was left out of auto-config and stayed out until the next restart, because every serial in the status message was recorded as configured and so never counted as new. With gateway_inverter_serial set, the list now decides the site: - what the hub reports each listed inverter to be is kept in a discovery record and saved through the Storage component, updated only while the hub is reading that inverter; - a listed inverter with a record keeps its slot when the hub cannot read it, including across a restart; one never read yet is left out with a warning until it is; - records for serials no longer in the list are not used; - a listed inverter with no battery is configured as PV only: no slot, no writes, its generation added to pv_power and pv_today; - a configured inverter the hub is not reading logs an error and marks the run as having had errors until it returns. With no serial list nothing is stored and selection is as before, except that an inverter unusable at start-up is now registered when it becomes usable. An inverter with no readings keeps the entity values it last reported instead of being published as zero. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Initial unread inverters can still be omitted, save failures lose durability, and first-frame entities may be missing.
3 open findings
What changed in this PR
Adds durable Gateway inverter discovery so configured inverters remain bound during telemetry outages.
Changes:
- Persists inverter discovery metadata and retains stale entity values.
- Supports PV-only listed inverters and dynamic reconfiguration.
- Adds extensive Gateway regression tests.
| File | Description |
|---|---|
apps/predbat/gateway.py |
Implements discovery persistence, binding, offline handling, and PV-only support. |
apps/predbat/tests/test_gateway.py |
Adds coverage for discovery, outages, persistence, and reconfiguration. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…nverters at once Review feedback on #5475: - Storage.save() reports a failed write by returning False, which was ignored after the dirty flag had been cleared, so one failed write lost the record. The save now stays pending and is retried after five minutes. - An inverter that is only published once bound (one the hub does not flag primary, or a PV-only one) had no entities on the status that bound it, because entities were injected before auto-config ran. They are now injected again after a successful auto-config, before the API is declared started. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… unread Follow-ups from review of #5475, all consequences of keeping an inverter configured while the hub cannot read it: - shared CT: grid and load power and today's import, export and load live on the first inverter's entities; while that one is unread they are filled from the next inverter on the clamp; - an unread inverter's power readings go to zero; its state of charge, size and settings stay as last reported; - an unread battery inverter behind a Gateway or Plant EMS is reported as an error, not only one with a slot of its own; - a stored discovery record that cannot be read is dropped with a warning instead of stopping auto-config on every status; - gateway_integrate_power: midnight still resets the day while an inverter is away, and a PV-only inverter's generation counts towards the load; - a stored PV-only inverter missing from the status gets its PV entities; - a slot with no battery size known is left unbound for soc_max again, and is re-bound when the hub first reports a size; - after auto-config only the units the first pass skipped are published. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… them (#5478) A status with an inverter's ratings unpopulated, seen for a few minutes after a hub restart, was published as a 6000 W battery and inverter rate with no export limit. A 3 kW inverter was planned on, and written to, at 6 kW. A zero battery rate, inverter rate or export limit now keeps the last real value the hub reported for that inverter. With gateway_inverter_serial set the two rates are also kept in the stored discovery record, so they survive a restart. An inverter that has never reported a rating still gets the 6000 W default and no export limit, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…imum rate (#5478) Until an inverter has reported its maximum rate, the rate Predbat works to is the 6000 W default. The plan still needs a figure, but it is only an assumption, so the gateway no longer acts on it: a charge or discharge rate write above zero for that inverter is not sent, and nor is its set_rate_cap. Predbat reads the unchanged rate back and tries again, so the write goes through once the rating is known. A rate of zero is always sent, so a charge or discharge can still be stopped. A Gateway or Plant EMS is not held: it has no battery rating of its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…5478) - A reserve the hub leaves out keeps the last real value it reported, like the ratings. Read as 0% it was rewritten by Predbat for nothing. A 0% that Predbat itself writes is real, so the next 0 from the hub is then published as 0. - The export limit is added to the stored discovery record, so with gateway_inverter_serial set the last good limit survives a restart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With no gateway_inverter_serial every inverter the hub reads now has its record stored as well, and its last known battery size, rates and export limit are used when the hub leaves them out, including after a restart. What the list changes is unchanged: without one, auto-config uses the inverters it finds, and an inverter that is in the records but not in the status, or not being read, is not held on to. A battery size the hub leaves out is now published from the record in the same way as the ratings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…st-authoritative-5471
…verter The integrated PV tracks were a positional list built when integration began. When auto-config re-ran and changed the slots (a PV-only inverter gaining a battery, or an inverter becoming usable later) the tracks were paired with the wrong counters or dropped. They are now keyed by serial, for slot and PV-only inverters alike. With gateway_inverter_serial set and a discovery record held, a status with an empty inverter list is no longer ignored: the stored inverters are bound at start-up, and once running their powers are zeroed and the unread error is raised. It does not count towards the hub being alive. Without a list or a record it is ignored as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…reserve until a write of 0 is applied Integration runs before auto-config, so an inverter that gained its slot on a status had its PV baseline taken from the next one, losing that interval. The status is now taken as the baseline for the new slots straight after auto-config, with nothing added. The last good reserve was forgotten before the write of 0% went out. It is now forgotten only once the write has been sent, and put back if the hub refuses it, so a refused or failed write no longer reads back as a reserve of 0. Co-Authored-By: Claude Opus 5.5 <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.


Fixes #5471. Fixes #5478.
The problem
On a Hub site, a battery inverter the Hub could not read when Predbat started was left out of auto-config and stayed out until the next restart. Predbat planned and controlled on fewer inverters than the site has and reported no error (one site ran on half its capacity for about 37 hours).
automatic_config()only took inverters that wereprimarywith battery data present, then recorded every serial in the status message as configured._needs_reconfigure()only re-runs for a serial it has not seen, so the dropped inverter never triggered a re-run.What this changes
With
gateway_inverter_serialset: the list decides the sitegateway/discovery_<device id>, JSON), loaded inrun()before the MQTT listener starts, and saved when one changes. A save the storage refuses stays pending and is retried after five minutes.num_invertersno longer shrinks.pv_powerandpv_todayentities are appended after the bound inverters' so its generation is counted (the same patternsolis.pyuses).Errorwhen that starts and marks each run as having had errors until it returns.The coordinator rule is unchanged: a Plant EMS, or a Gateway with two or more battery AIOs, is still the single control target, so a list naming a Gateway and its AIOs behaves as it does today.
With no serial list
Selection uses the same primary-and-battery filter as before: auto-config uses the inverters it finds, and inverters can come and go. The discovery records are still stored for every inverter the Hub reads, but only for their last known battery size, rates and export limit. An inverter that is in the records but not in the status, or not being read, is not held on to. One change to selection: an inverter that was unusable at start-up is now registered on the first status in which it becomes usable, without a restart. It does not re-run auto-config on every status while the inverter stays unusable, and removals are still ignored.
All gateway sites: stale, not zero
An inverter with no readings at all keeps the state of charge, battery size and settings it last reported, as an inverter behind an offline cloud API does. Previously the empty message was published, which read as a 0% battery. Its power readings (battery, PV, grid, load, inverter) do go to zero: a power is a rate, and a stale one would read as an inverter still discharging hours after it dropped out. An inverter with no entities yet is published once with defaults (and its stored battery size, if known) so the core has something to read.
Entities are also published again straight after a successful auto-config, before the API is declared started. An inverter that is only published once bound (one the Hub does not flag
primary, or a PV-only one) would otherwise have no entities until the next status message.Keeping the site's readings sound while an inverter is unread
gateway_shared_ctthe grid and load power and today's import, export and load are read from the first inverter's entities only. While that inverter is unread they are filled from the next inverter on the clamp, which reports the same readings.gateway_integrate_power. Integration still stops while an inverter with a slot is missing or disconnected, as before, but midnight now resets the day's figures in that state too. A PV-only inverter's generation is added to the load. Each inverter's generation is tracked by serial, so auto-config re-running and changing the slots (a PV-only inverter gaining a battery, an inverter becoming usable later) neither loses it nor mixes it with another inverter's. After auto-config runs, the status it ran on is taken as the baseline for the new slots, so an inverter is counted from the status it joined on.soc_max(automatic sizing), as on main, and is re-bound on the first status that reports a size.Ratings the Hub stops reporting (#5478)
A status with an inverter's ratings unpopulated, seen for a few minutes after a Hub restart, was published as a 6000 W battery and inverter rate with no export limit. A 3 kW inverter was planned on, and written to, at 6 kW.
set_rate_cap. Predbat reads the unchanged rate back and tries again, so the write goes through once the rating arrives. A rate of zero is always sent, so a charge or discharge can still be stopped. A Gateway or Plant EMS is not held. The execution plan still carries the fleet rate from the core, which includes the default: that relies on the Hub, which cannot write to an inverter it has not discovered and clamps the plan power to the real rating of one it has.Behaviour changes to check in review
A Hub that never reports
rate_max_wfor a battery inverter can no longer have its charge or discharge rate raised above zero. Before, such a site ran on the 6000 W default. If any firmware in the field never reports the rating, this needs a decision before merge.With a serial list, the Hub's
primaryflag no longer decides whether a listed battery inverter is used."Discovered" means the Hub has reported readings for the inverter at least once, not merely listed its serial. Readings are needed to tell a battery inverter from a PV-only one.
A PV-only inverter contributes PV only. Its load, grid, import and export readings are not used.
The error is a log line plus the run's error flag. The text of
predbat.statusis not changed.Not covered
not_polledis not known. If it sends some readings with an empty battery block, the battery values are still published as zero._load_discovery()/_save_discovery()with a fake storage, not throughrun()or the real Storage backend.Testing
TestGatewayUnitControlBindingandTestIntegratePower(test_gateway.py). The first 18 cover: never-read inverter left out then bound; stored inverter bound when unread or absent at start-up; changed list beating stored records; records saved, updated and left alone while offline; last known values stored and used without a list, while inverters still come and go; offline error raised and cleared; stale entity values; PV-only configuration and its later promotion to a battery inverter; no re-run of auto-config per status; a failed save retried after a wait; a newly bound inverter published on the same status. The rest cover the section above: shared-CT readings carried by the next inverter, powers zeroed, coordinator-site errors, unreadable stored records, PV-only entities after a restart, battery size re-binding, the midnight reset and PV-only generation undergateway_integrate_power. The last ten cover Gateway MQTT: a missing max battery rate is replaced with a hard-coded 6000 W, which is then written to the inverter #5478: the reserve kept and a written 0% honoured, a stored export limit (including a limit of zero) after a restart, ratings kept when the Hub stops reporting them, a zero export limit kept, defaults when never reported, stored ratings after a restart, rate writes and rate caps held on an assumed rating, and coordinators not held. Five more cover the second review round: PV tracked across a promotion to a slot and for an inverter that joins later, and a status naming no inverter at start-up, once running, and with no list or record. Four more cover the third round: the integration rebased after auto-config (directly and through_process_telemetry()), and the reserve kept when a write of 0% fails or is refused.test_incomplete_battery_telemetry_waits_then_excludes_pv_only_unit) is unchanged and passes../run_all --test gatewaypasses,./run_all --quickpasses, and pre-commit passes on both changed files.🤖 Generated with Claude Code