Skip to content

fix(solax): hold a lifetime counter that dips rather than read the recovery as energy - #5389

Merged
springfall2008 merged 3 commits into
mainfrom
fix/solax-counter-dip-5388
Oct 4, 2026
Merged

springfall2008 merged 3 commits into
mainfrom
fix/solax-counter-dip-5388

Conversation

@springfall2008

@springfall2008 springfall2008 commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

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

Part of #5388. It covers the counter dip (part 1 of the issue) and stops a plant with no PV source publishing battery discharge as PV. It does not read meter 2, so the issue should stay open.

Builds on #5357 (merged): the guard also covers the per-inverter yield added there.

Summary

SolaX can return a lower value for a plant lifetime total for a while and then the right one again. publish_plant_info() published the totals as received, so the recovery was counted as energy for that day: 181.3 kWh of PV on a 3.2 kWp array in the issue.

  • New hold_counter_dip(entity_id, value): a lifetime counter that reads lower than the last value published is held at that value, with a warning logged once per dip.
  • Applied to the five plant totals (total_yield, total_charged, total_discharged, total_imported, total_exported) and to each inverter's ..._<sn>_total_yield. total_load is built from the held values.
  • The last published value is seeded from the sensor itself after a restart. The last values and the time each dip started are also saved through the storage component (at once when a dip starts or ends, otherwise every 15 minutes), so the hold period does not start again on a restart, and a dip is still held when the sensors are gone, as after a Home Assistant restart.
  • A lower value that lasts SOLAX_COUNTER_DIP_HOLD_HOURS (24) is taken as a genuine reset and published.
  • An inverter read that returns 0.0 no longer overwrites its known yield. This replaces the separate last-known-yield dict from fix(solax): build PV yield from the inverters, not the plant total #5357: the inverter's own sensor, held on a dip, is now the record.
  • A plant where no inverter has PV inputs (mpptMap/pvMap) or a yield counter now publishes 0 for pv_yield instead of the plant total, and logs a warning once. On such a plant (a lone X1-AC) the plant total is the inverter AC output, so battery discharge was published as PV and counted twice in total_load. A plant whose inverters have PV inputs but report no yield still uses the plant total.

Testing

  • tools/triage_test.sh solax: fails without the fix, passes with it. With solax.py stashed the module fails at import (the new constant is missing), which proves little, so the new test_counter_dip_main was also run against the unfixed source with only the constant defined: it fails on the first case (Plant totals should be held at their previous values on a dip, got {'total_yield': 4260.4, ...}). With the fix all eight cases pass: dip held with the load, recovery published, reset accepted after the hold period, dip after a restart held against the sensor value, upward step published, an inverter reading 0.0 keeps its yield in the plant sum and in its own sensor, the no-PV-source warning logged once, and no warning for inverters with PV inputs.
  • Zero PV change: with the previous solax.py two cases fail (PV yield should be 0.0 ... got 19569.5 and ... got 4437.8); both pass with it, along with the load having no PV term and the plant figure still being used for inverters with PV inputs.
  • Review fixes: four new cases, failing before and passing after: a yield counter at 0.0 counts as a PV source, a reset older than the hold period is accepted after a restart, a dip is held against the stored value when the sensor is gone, and the state is only saved when needed.
  • ./run_all --quick and ./run_pre_commit: passed on the final commit.
  • Not run against a live plant. A dip cannot be produced on demand; the test uses the counter values from the issue.

Notes

  • The 24 hour hold is a judgement, not a measurement. The issue's evidence is daily, so how long the dip on 1 Oct lasted is not known beyond "less than a day". If a dip can outlast the hold, the lower value is accepted and the recovery is read as energy again. A genuine reset (a replaced device) freezes the affected sensor for 24 hours before the new value is taken.
  • Review feedback (Copilot, two findings, both fixed): the dip start time was memory only, so a system restarted daily would never accept a genuine reset; and plant_has_pv_source() ignored totalYield, so an inverter with a yield counter at 0.0 and no PV maps was classed as having no PV source. Such an inverter keeps the plant total, as before this PR.
  • The plant pv_yield sensor has no guard of its own. It is the sum of inverter yields, or the plant total, and both are now held, so it cannot dip from a counter glitch. Guarding it separately would freeze it for a day whenever its source changes, for example when a plant moves from the plant total to its inverters.
  • total_earnings is not guarded; nothing in Predbat reads it as energy.
  • Publishing 0 PV changes the trade-off the issue described, by maintainer decision. With no PV term, total_load on a site with third-party PV is low by whatever that PV supplies, and the cumulative figure falls while the site exports. Predbat's incrementing-sensor clean-up holds through a fall of under 1 kWh and treats a larger one as a reset, so no energy is invented. PV calibration has no production to compare with, and the plan adds the raw PV forecast to a load that already has the PV taken out, so it will over-estimate the solar available on that site. The warning says so; the real fix for such a site is a PV source from outside SolaX.
  • On upgrade, such a plant's pv_yield steps from the plant total to 0 and total_load drops by the same amount, once.
  • A hybrid with no panels reports a populated mpptMap of zeros, so it counts as having PV inputs. A plant made only of that would still use the plant total. Not seen on a live plant.
  • Meter 2 is not implemented, and nothing else in the API measures the third-party PV. The API text gives gridPowerM2 and the M2 energy fields as meter 2, positive for export. On the live plant they are flat: 12 hours of /openapi/v2/device/history_data through daylight show yield null and gridPowerM2 0.0 throughout, and the plant has no deviceType=3 meter device. Reading meter 2 needs a plant where it works.
  • GitNexus index was stale and does not contain the functions added in fix(solax): build PV yield from the inverters, not the plant total #5357, so callers were established by search: hold_counter_dip() is called from publish_plant_info(), publish_device_realtime_data() and get_inverter_last_yield(); earlier impact() runs rated publish_plant_info and publish_device_realtime_data LOW with one caller each (run).
  • Debug journal SolaX row updated with the finding.

🤖 Generated with Claude Code

…covery as energy

SolaX can return a lower value for a plant lifetime total for a while and
then the right one again. Published as received, the recovery was counted
as energy for that day: 181.3 kWh of PV on a 3.2 kWp array.

The five plant totals and each inverter's total yield are now held at the
last published value while they read lower, seeded from the sensor so a
dip straight after a restart is held too. A lower value that lasts 24
hours is taken as a genuine reset. The load is built from the held values.

A plant with no inverter that has PV inputs or a yield counter now logs a
warning once, as its PV figure is the inverter AC output.

Part of #5388.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@springfall2008 springfall2008 self-assigned this Oct 4, 2026
@springfall2008
springfall2008 marked this pull request as ready for review October 4, 2026 15:38
Copilot AI balanced review requested due to automatic review settings October 4, 2026 15:38

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

Reset timing is lost across restarts, and missing or zero-valued counters can be misclassified.

Review effort: Balanced
Findings: 1 Medium severity · 1 Low severity

Open (2)
What changed in this PR

Prevents transient SolaX lifetime-counter dips from inflating energy history and adds diagnostics for plants without a reliable PV source.

Changes:

  • Holds dipped plant and inverter counters for 24 hours.
  • Calculates load from held values and warns about missing PV sources.
  • Adds regression tests and debugging guidance.
File Description
apps/​predbat/​solax.py Implements counter-dip handling and PV-source warnings.
apps/​predbat/​tests/​test_solax.py Tests dip, recovery, reset, restart, and warning behavior.
tools/​debug-journal.md Documents the SolaX failure mode and mitigation.

💡 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
Comment thread apps/predbat/solax.py
CI and others added 2 commits October 4, 2026 17:09
… total

A plant whose only inverter has no PV inputs and no yield counter (a lone
AC-coupled battery inverter) fell back to the plant total yield for PV.
That figure is the inverter AC output, so battery discharge was published
as PV and counted twice in the load.

Such a plant now publishes 0 for the PV yield. The load has no PV term, so
on a site with PV on an inverter SolaX does not measure it is low by what
that PV supplies. A plant whose inverters have PV inputs but no yield of
their own still uses the plant total.

Part of #5388.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ield counter as a PV source

Review feedback on #5389.

The time a dip started was held in memory only, so a restart began the 24
hour hold again and a system restarted every day would never accept a
genuine counter reset. The dip start times and the last counter values are
now saved through the storage component: at once when a dip starts or
ends, otherwise every 15 minutes. The stored last value is used when the
sensor is gone, as after a Home Assistant restart.

plant_has_pv_source() ignored totalYield, so an inverter whose yield
counter reads 0.0 and which has no PV maps, such as a newly commissioned
one, was classed as having no PV source.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@springfall2008
springfall2008 merged commit fc3354f into main Oct 4, 2026
2 checks passed
@springfall2008
springfall2008 deleted the fix/solax-counter-dip-5388 branch October 4, 2026 16:24
springfall2008 added a commit that referenced this pull request Oct 5, 2026
…octopus row (GH#5390) (#5397)

* docs(debug-journal): fold in the 2026-10-04 queue slice (9 candidates); mark GH#5376 fixed by PR #5377 (v9.3.5) and GH#5366 by PR #5383; re-verify the SolaX/counter-dip entries after PR #5389; add the CID499 read/write units, marginal-band contamination, GivTCP verify-tolerance, minute-data dip asymmetry, Fox MaxSoc, car-export-blind, re:-first-match and tooltip-clamp entries

Co-Authored-By: Claude Code <noreply@anthropic.com>

* docs(debug-journal): map the IOG supervision bounce symptom into the octopus row (GH#5390)

Co-Authored-By: Claude Code <noreply@anthropic.com>

---------

Co-authored-by: CI <ci@example.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
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.

2 participants