Skip to content

fix(solax): treat a null battery chargeDischargePower as 0 in the plant sum - #5894

Open
springfall2008 wants to merge 2 commits into
mainfrom
fix/solax-null-charge-discharge-power
Open

springfall2008 wants to merge 2 commits into
mainfrom
fix/solax-null-charge-discharge-power

Conversation

@springfall2008

@springfall2008 springfall2008 commented Oct 31, 2026 •

Copy link
Copy Markdown
Owner

Fixes #5893

Summary

get_charge_discharge_power_battery() summed each battery's chargeDischargePower with .get("chargeDischargePower", 0). That default only covers a missing key, so a battery pack that returns an explicit null raised TypeError: unsupported operand type(s) for +=: 'int' and 'NoneType' on every poll. The caller, publish_plant_info(), then left the SolaX component in error with no plant data reaching the plan.

The same pattern sits in the two helpers publish_plant_info() calls just before it, get_current_soc_battery_kwh() (batterySOC, batteryRemainings) and get_battery_temperature() (min() against None), so those are guarded too. In all three, an explicit null now behaves exactly as a missing field already did, so no existing result changes. Temperature is checked against None rather than with or, so a real 0.0 °C reading is still used.

The per-device path in publish_device_realtime_data() already guarded the power field.

Evidence

Realtime payload for the affected pack, from the site's log on 30 Oct 2026 (serial redacted):

'batterySOC': 97, 'batterySOH': 0, 'chargeDischargePower': None, 'batteryVoltage': 321.5,
'batteryTemperature': 16.0, 'totalDeviceDischarge': None, 'totalDeviceCharge': None, 'batteryRemainings': 14.8

Only chargeDischargePower is an observed failure. Null SOC, remainings and temperature have not been seen from SolaX; those guards cover the same latent class on the same call path. The other nulls in the payload are harmless: totalDeviceDischarge/totalDeviceCharge are not read anywhere, and batterySOH 0 is already mapped to 1.0.

Testing

  • Test 16 (power): two batteries, one at 1500 W and one None. Fails without the fix with the production TypeError, passes with it.
  • Test 17 (SOC/remainings) and Test 18 (temperature): both fail without the fix (+= on None, and '<' not supported between 'float' and 'NoneType'); with it, null fields give the same result as missing ones, and a null temperature is skipped.
  • ./run_all --test solax: passed.
  • ./run_all --quick: all tests passed (4 slow tests skipped), on each commit.
  • Pre-commit on the changed files: passed.
  • Not live tested: the site that hits this currently has a separate credentials problem, so its SolaX login fails before this code runs.

Not in this PR

Normalising the known-numeric fields where the response is stored (solax.py:1961) would protect every reader, including the further get_current_soc_battery_kwh() call sites. That is a wider change with no observed failure behind it, so it is left for a separate PR if wanted.

🤖 Generated with Claude Code

…nt sum

get_charge_discharge_power_battery() summed .get("chargeDischargePower", 0),
whose default only covers a missing key. A battery pack that returns an
explicit null raised TypeError on every poll, leaving the SolaX component
in error with no plant data reaching the plan.

Fixes #5893

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

@springfall2008 springfall2008 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated comment from the triage bot.

Review of head 2d09d20: 1 finding, inline. The fix itself is correct: or 0 behaves identically to the old default for a missing key and for 0/negative values, and only changes the crash case. Test 16 is red without the fix (the caught TypeError makes result16 a non-number, so failed is set) and green with it (1500 W), by reading; it sits inside test_helper_methods_main(), which run_solax_tests() already calls (test_solax.py:175). The per-device guard the body cites is real (solax.py:2569). No user-facing setting or value changed for healthy data, so no docs change is needed. The one finding is inline: the same null class is still unguarded on the sibling reads of the same dict, two of which the same caller reaches before this line.

Comment thread apps/predbat/solax.py
total_power = 0
for device_id in self.plant_batteries.get(plant_id, []):
total_power += self.realtime_device_data.get(device_id, {}).get("chargeDischargePower", 0)
total_power += self.realtime_device_data.get(device_id, {}).get("chargeDischargePower") or 0

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated comment from the triage bot.

The guard is right here, but the claim in the PR body that only the plant-level power sum is missing it is not quite true: the sibling reads of the same realtime_device_data dict on the same call path are still unguarded, and two of them run before this line does. publish_plant_info() calls get_current_soc_battery_kwh() at solax.py:2732 and get_battery_temperature() at solax.py:2736, so a null batterySOC, batteryRemainings or batteryTemperature in the same payload raises the same TypeError at solax.py:833-834 (current_soc += ...get("batterySOC", 0), battery_remainings += ...get("batteryRemainings", 0)) or solax.py:844 (min(None, temperature)), cancelling this cycle the same way.

What I verified vs. what I could not: the response is stored unfiltered (self.realtime_device_data[sn] = result[0], solax.py:1961), so an explicit null in any field lands as None, and get_current_soc_battery_kwh() has five further call sites (solax.py:818, 883, 3188, 3250, 3286) that spread the same crash class beyond the plant sensors. I could not confirm SolaX actually returns null for those other fields — the #5893 site's traceback reaching line 824 shows its SOC/temperature were numeric at that time — so this may never fire in practice; it is the same latent class rather than an observed failure.

A general fix would be to normalise the known-numeric fields where the response is stored (solax.py:1961) so every reader is safe, or failing that, apply this same guard at solax.py:833-834 and 844.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 11a9feb. The PR body was wrong: the SOC/remainings sum and the temperature min() share the same pattern and run first in publish_plant_info(). Both now treat an explicit null exactly like a missing field, so no existing result changes. Temperature uses an is not None check rather than or, so a real 0.0 °C reading still counts. Tests 17 and 18 fail without the change and pass with it.

The 30 Oct payload from the #5893 site confirms your caveat: only chargeDischargePower is null among the fields we read (batterySOC 97, batteryRemainings 14.8, batteryTemperature 16.0). The null totalDeviceDischarge/totalDeviceCharge are not read anywhere, and batterySOH 0 is already mapped to 1.0.

I have left out normalising at the store (solax.py:1961): it is a wider change with no observed failure behind it, so it is better as its own PR if wanted. That is noted in the PR body.

@springfall2008 springfall2008 removed the BOT_REVIEW Trigger an autotriage label Oct 31, 2026
…issing fields

The plant SOC sum and the battery temperature minimum read the same
realtime_device_data as the power sum, earlier in publish_plant_info(),
and also only defaulted a missing key. An explicit null now behaves
exactly as a missing field already did. Temperature is checked against
None rather than with `or`, so a real 0.0 reading is still used.

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

This branch has not been deployed

No deployments
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: plant battery power crashes when a battery pack reports null chargeDischargePower

1 participant