Skip to content

fix(solax): build PV yield from the inverters, not the plant total - #5357

Merged
springfall2008 merged 4 commits into
mainfrom
fix/solax-pv-yield-from-devices-5356
Oct 4, 2026
Merged

springfall2008 merged 4 commits into
mainfrom
fix/solax-pv-yield-from-devices-5356

Conversation

@springfall2008

@springfall2008 springfall2008 commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

This is an automated draft PR generated from issue #5356 — a maintainer should review it before merging.

Fixes #5356

Summary

On a SolaX Cloud plant with a PV-only inverter plus a separate AC-coupled battery inverter, the plant realtime totalYield includes the battery inverter's AC output. pv_today pointed at that figure, so battery discharge was counted as PV, counted a second time in total_load, and scaled the PV forecast up through calibration.

  • New plant sensor sensor.<prefix>_solax_<plant>_pv_yield, the sum of each inverter's device-level totalYield (get_plant_pv_yield()). automatic_config() now points pv_today at it.
  • total_load uses the same figure in place of the plant totalYield.
  • Each inverter contributes its last known totalYield, kept per inverter and updated only when a read returns a number. A lifetime counter does not go backwards, so a failed read cannot make the sum dip, and total_load keeps being published. After a restart the last known value is seeded from the inverter's own ..._<sn>_total_yield sensor, so an inverter that no longer answers stays in the sum. An inverter that reads fine but has no totalYield counts as 0. Both figures are held only when an inverter that onlineStatus says should be answering failed its read and has never given a yield; an offline inverter that never reported counts as 0.
  • If no inverter reports a yield of its own, the plant figure is used as before. Once a plant has been built from its inverters it never drops back to the plant figure; a zero sum is held instead, because the two differ by thousands of kWh.
  • ..._total_yield still publishes the raw plant figure, so existing dashboards keep their meaning.
  • pv_power and grid_power were taken from the first inverter listed for the plant, so on a mixed plant they were one device's reading and depended on the order the API returned the inverters in. New plant sensors sensor.<prefix>_solax_<plant>_pv_power and ..._grid_power are summed over every inverter (the sums load_power already used), and automatic_config() points the two args at them. Both are held when an inverter that should be answering was not read on the cycle; an offline inverter counts as 0 W. The per-inverter sensors are unchanged.
  • The standalone CLI (python3 solax.py --client-id ... --client-secret ...) now prints the plant yield, each inverter's yield and AC output counters, and the figure that would be published, for checking a live plant.

Testing

  • ./run_all --test solax: fails without the fix, passes with it. With solax.py stashed the module fails on the new pv_today assertion in test_automatic_config_main (got ['sensor.predbat_solax_plant1_total_yield']); the run stops at that first failure, so the new test_plant_pv_yield_main was not separately reached in the red run. With the fix all six of its cases pass: inverter sum used for PV and load, plant total unchanged, held on a failed inverter read, republished on the next good read, held for an unread inverter, fallback to the plant figure, and no fallback once the inverters have reported.
  • ./run_all --quick: all tests passed.
  • ./run_pre_commit: passed.
  • ./run_all --test solax for the power change: fails without it (Expected plant pv_power 2000W summed over both inverters, got None), passes with it. Covers the sum over two inverters, the hold on a failed inverter read, the republish on the next good read, and the pv_power/grid_power args.
  • Last-known-yield change: with the previous solax.py the rewritten test_plant_pv_yield_main fails four cases (load not republished on a failed read, a lone inverter with no totalYield, an offline inverter that never reported, the seed from the sensor); all pass with it. ./run_all --quick passes on the final commit. Pre-commit was run on the changed files only for the second and third commits.
  • Live test of the yield change, 4 Oct, on the plant from the issue (X1-Boost PV-only plus AC-coupled X1-Hybrid-G4, read-only CLI run): pv_today points at ..._pv_yield, which published 10282.2 kWh, the X1-Boost's own totalYield; ..._total_yield still published the plant's 19596.81; total_load matched the formula with the inverter figure. The daily relationship held again: 9.2 + 5.3 = plant dailyYield 14.5.
  • Live runs of the final code, 4 Oct, three plants (read-only CLI):
    • PV-only inverter plus AC-coupled hybrid (the plant from the issue): PV yield is the PV inverter's own figure, load matches the formula, plant pv_power/grid_power published.
    • Two DC-coupled hybrids, each with panels: PV yield is the sum of the two device yields, and plant pv_power is the sum of both (the first inverter alone was 4698 W of 5280 W).
    • A single X1-AC, which returns no totalYield: no longer held, falls back to the plant figure. See the note below, that figure is wrong on this plant.
  • Not yet live tested: a night-time read, the seed from the inverter sensor across a real restart (a CLI run has no sensor state to seed from), and a two-inverter plant with a non-zero grid reading (both inverters read 0 W in every run, so it is not shown that summing grid power across them does not double count).
  • Review fix (raw plant ID): two new cases fail before it and pass after. ./run_all --quick passes on the rebased branch.

Notes

  • The fix relies on the daily relationship verified in the issue (PV inverter yield + battery inverter dailyACOutput = plant dailyYield, exact on three days). The lifetime totals did not decompose the same way, and this PR makes no assumption about how SolaX builds them.
  • DC-coupled hybrids: on the live plant of two hybrids with panels, device dailyYield (23.3 and 2.6) and dailyACOutput (24.1 and 9.5) are separate counters, and the plant dailyYield (33.6) is exactly the sum of the two AC outputs. So the plant figure is inverter AC output there too, and the old wiring over-read PV on that plant as well (33.6 against 25.9). Device yield was not checked against an independent PV measurement.
  • Still wrong: a plant whose only inverter is an X1-AC. It has no totalYield and no MPPT or PV map, so the PV yield falls back to the plant figure, and on the live plant that figure is battery discharge (plant dailyYield = inverter dailyACOutput = plant dailyDischarged, one day's sample). This is the pre-PR behaviour, not a regression. Publishing 0 for an inverter with no PV inputs would fix it; left for a maintainer decision.
  • Plant ID lookup (review feedback): plant_inverters is keyed by the plant ID as SolaX gives it and the entity names use a lowercased form. The yield and plant power lookups now use the raw ID. Numeric plant IDs, which is every plant seen, were unaffected.
  • Two existing fixtures (test_publish_plant_info_main, test_realtime_publish_gating_main) gained an inverter realtime read, since the load is now only published once the inverters have been read.
  • Switch-over: ..._pv_yield is a new entity starting at a different absolute value, so the day of the upgrade has a partial day of PV. Inflated PV history stays in the calibration window for several days on affected sites.
  • The "has reported from inverters" flag is in memory only. A restart whose first cycle reads a zero yield from every inverter would publish the plant figure once. Not handled; it needs the API to return a wrong zero at exactly that moment.
  • An inverter that is offline and has never reported counts as 0, so on a fresh install where the PV inverter is offline for the first cycles (and has no sensor to seed from) the plant figure is published until it answers, then the value steps down to the inverter sum. One-off, first run only.
  • state_class on the new sensor is measurement, matching the other plant totals rather than fixing them all here.
  • GitNexus impact() on publish_plant_info and automatic_config: LOW risk, one direct caller each (run).
  • Debug journal SolaX row updated with the finding.

🤖 Generated with Claude Code

@springfall2008 springfall2008 self-assigned this Oct 2, 2026
@springfall2008
springfall2008 marked this pull request as ready for review October 4, 2026 14:31
Copilot AI balanced review requested due to automatic review settings October 4, 2026 14:31

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

The normalized plant ID causes inverter lookup to fail for non-normalized SolaX IDs, silently restoring the incorrect plant-total calculation.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
What changed in this PR

Fixes SolaX PV/load accounting by deriving PV yield from inverter-level counters instead of the plant aggregate.

Changes:

  • Adds inverter-summed PV yield with fallback and failure gating.
  • Updates automatic configuration, diagnostics, documentation, and tests.
  • Records the SolaX behavior in the debug journal.
File Description
apps/​predbat/​solax.py Implements corrected PV-yield aggregation.
apps/​predbat/​tests/​test_solax.py Adds aggregation and failure-path tests.
docs/​apps-yaml.md Documents the new sensor.
tools/​debug-journal.md Records the diagnosed behavior.

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

Comment thread apps/predbat/solax.py Outdated
CI and others added 4 commits October 4, 2026 15:52
On a SolaX Cloud plant with a PV-only inverter and a separate AC-coupled
battery inverter, the plant realtime totalYield includes the battery
inverter's AC output. pv_today pointed at it, so battery discharge was
counted as PV, counted again in total_load, and inflated PV calibration.

Publish a new plant sensor, <plant>_pv_yield, summed from each inverter's
totalYield, use it for pv_today and in the total_load sum, and hold both
when an inverter was not read so the cumulative counter never dips. The
plant figure is still used where no inverter reports a yield of its own.

Fixes #5356

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
automatic_config() pointed pv_power and grid_power at the first inverter
listed for the plant. On a plant with a PV-only inverter and a separate
battery inverter that is one device's reading, and which one depends on the
order the API returns them in.

Publish plant level pv_power and grid_power sensors from the sums already
built for load_power, and point the args at those. Both are held when an
inverter in the plant was not read on the cycle.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…he plant

get_plant_pv_yield() held the whole plant whenever any inverter was missing,
which treated three different cases alike. An inverter that failed a read
after being read before now contributes its last known totalYield, a
lifetime counter does not go backwards so the sum cannot dip. An inverter
that reads fine but has no totalYield counts as 0 rather than holding the
plant for ever. The value is held only when an inverter that onlineStatus
says should be answering failed its read and has never given a yield.

The last known yield is seeded from the inverter's own total yield sensor,
so an inverter that no longer answers stays in the sum across a restart.

The plant pv_power and grid_power follow the same narrowing: an offline
inverter counts as 0W rather than holding them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s it

plant_inverters is keyed by the raw plant ID while the entity names use a
lowercased form with spaces replaced. get_plant_pv_yield() and the plant
power hold were handed the entity form, so a plant ID the tidying changes
found no inverters: the PV yield fell back to the plant total and the plant
powers were published with an inverter unread. Numeric plant IDs, which is
every plant seen so far, were unaffected.

Review feedback on #5357. Also records the live DC-coupled and X1-AC
findings in the debug journal.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@springfall2008
springfall2008 force-pushed the fix/solax-pv-yield-from-devices-5356 branch from 97ca706 to 5eee944 Compare October 4, 2026 14:55
@springfall2008
springfall2008 merged commit 74c9c13 into main Oct 4, 2026
2 checks passed
@springfall2008
springfall2008 deleted the fix/solax-pv-yield-from-devices-5356 branch October 4, 2026 15:00
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.

SolaX Cloud: battery discharge counted as PV (and twice in load) on plants with an AC-coupled battery inverter

2 participants