Skip to content

feat(solis): SolisCloud freeze charge and holds via the Battery Reserve - #5265

Merged
springfall2008 merged 2 commits into
mainfrom
feat/soliscloud-freeze-hold
Sep 27, 2026
Merged

springfall2008 merged 2 commits into
mainfrom
feat/soliscloud-freeze-hold

Conversation

@springfall2008

@springfall2008 springfall2008 commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Summary

This is the SolisCloud counterpart to #5258 (Solis FB00 over Modbus). It fixes two things that didn't behave as the planner models them on SolisCloud: freeze charging, and holding the battery for a car or iBoost.

1. Freeze charge

SolisCloud has no reserve, so Predbat freezes by keeping the charge window, with the target set to the current SoC and a discharge rate of 0. The component didn't treat that as a hold:

  • V2 firmware: it wrote Self-Use with grid charging on, so the inverter topped the battery up from the grid.
  • V1 firmware: at the target it zeroed the charge current, so the 0 A rule chose Feed-in priority and exported PV during the freeze.

Fix: is_holding_in_charge_slot() detects a charge slot that is open now with the battery at or above its target. In that case the component writes Self-Use - No Grid Charging (new ENUM_SELF_USE_HOLD) before the 0 A → Feed-in rule is considered:

  • 3 on V1, where bit 1 is the slot enable. The existing "No Timed Charge/Discharge" (1) would switch the slots off.
  • 1 on V2.

The slot stops the battery discharging, grid charging is off, and PV can still charge it. The same rule applies when a normal charge reaches its target.

2. Holds (car/iBoost, freeze or hold on reserve)

These did nothing on SolisCloud: has_reserve_soc was False, execute.py turned reserve functions off, and reserve was bound to the over-discharge SOC.

  • reserve is now bound to the Battery Reserve SOC (CID 157), and the row declares has_reserve_soc: True. battery_min_soc stays the over-discharge SOC (CID 158).
  • Solis only treats the Battery Reserve SOC as a discharge floor while Battery Reserve (CID 636 bit 4) is on; see the app screenshot in Solis Seminar Episode 61. set_storage_mode_if_needed() now always sets bit 4, so the reserve is always honoured. set_reserve_enable only decides whether Predbat changes it. With it off, the user manages the reserve, Predbat plans against it, and the inverter enforces it (changed in review round 1).
  • Reserve writes show in the cache and entity at once (show_soc_limit_now()) and are written from the event queue. Non-slot events are only handled at the next run(), far later than Predbat's 2 s write check.
  • Outside a slot the component already writes a No-Grid-Charging mode. So when a hold raises the reserve to SoC + 1, the inverter can't import from the grid to reach it.
  • A freeze export now becomes 80 (Feed-in + Reserve, no grid charging) with reserve management on, so the reserve floor holds there too.

Other

  • The discovery record now reports reserve (rw) as well as battery_min_soc (r). Spec D9 ("SolisCloud does not write the reserve") is marked superseded; it dated from reserve being bound to the over-discharge SOC.
  • Docs are updated (Solis component notes, discovery catalogue), plus the debug-journal Solis row.

For review

  • Behaviour change for all SolisCloud users: the inverter's Battery Reserve switch is turned on, so the Battery Reserve SOC becomes a real floor. With set_reserve_enable on, Predbat sets it (set_reserve_min normally, SoC + 1 during a hold). With it off, the value already on the inverter applies, and Predbat plans against it.
  • Hold hysteresis: the charge-slot hold starts at the target and holds until the SoC is more than 1% below it (SOLIS_HOLD_HYSTERESIS).
  • Manual setups: a startup warning fires if reserve still points at the over-discharge SOC.
  • Self-Use - No Grid Charging is a new option in the component's storage-mode select.
  • is_holding_in_charge_slot() compares the inverter's reported SoC with the slot's integer target. If the reported SoC is a fraction below the target (for example 61.6 against 62), the first cycle of a freeze may still top up that fraction before the hold starts.

Live test (SolisCloud)

  • Freeze charge slot: storage mode Self-Use - No Grid Charging (1 on V2, 3 on V1). No import, no discharge, and PV still charges the battery.
  • Car charging outside a slot: Battery Reserve on, Battery Reserve SOC at SoC + 1, and the battery holding at about 0 W.
  • After the hold: the Battery Reserve SOC returns to set_reserve_min, and the battery discharges normally.

Testing

  • ./run_all --test solis: all pass. New tests:

    • test_write_time_windows_hold_in_charge_slot: V1/V2, at, above and below target, including 0 A.
    • test_compute_solis_mode_value_hold_and_reserve.
    • test_storage_mode_sets_battery_reserve_with_set_reserve_enable.

    The hold test failed before the change (Self-Use where the hold mode was expected). The Battery Reserve tests fail when bit 4 isn't applied (mutation-checked). The catalogue and automatic_config tests now expect reserve → reserve_soc.

  • ./run_all --test discovery_contract and the other discovery tests: pass. The dummied-reserve example is now GS.

  • ./run_all --quick: all passed.

  • pre-commit: clean.

🤖 Generated with Claude Code

Freeze charge: SolisCloud has no reserve, so Predbat freezes by keeping the
charge window with the target at the current SoC. The component treated that
as a normal charge on V2 (Self-Use, grid charging on - it topped up from the
grid) and, on V1, zeroed the charge current at target so the 0A rule chose
Feed-in priority, exporting PV during the freeze.

- is_holding_in_charge_slot(): a charge slot open now with the battery at or
  above its target is only holding. Write 'Self-Use - No Grid Charging'
  (ENUM_SELF_USE_HOLD: 3 on V1, where bit 1 is the slot enable; 1 on V2)
  ahead of the 0A -> Feed-in rule

Holds (car/iBoost, freeze or hold on reserve) did nothing on SolisCloud:
has_reserve_soc was False and reserve was bound to the over-discharge SOC.

- Bind reserve to the Battery Reserve SOC (CID 157) and declare
  has_reserve_soc, keeping battery_min_soc on the over-discharge SOC
- set_storage_mode_if_needed() sets the Battery Reserve bit (CID 636 bit 4)
  while set_reserve_enable is on, so that SOC is a real discharge floor; it
  is left alone otherwise, as enabling it unmanaged would enforce whatever
  reserve SOC the inverter already held. Outside slots the component already
  writes a No-Grid-Charging mode, so a raised reserve is never imported to
- Discovery record reports reserve (rw) as well as battery_min_soc; spec D9
  marked superseded
- Docs (Solis component notes, discovery catalogue) and debug journal

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@springfall2008 springfall2008 added the BOT_REVIEW Trigger an autotriage label Sep 27, 2026
Comment thread apps/predbat/config.py
Comment thread apps/predbat/solis.py
Comment thread apps/predbat/solis.py
Comment thread apps/predbat/config.py
Comment thread apps/predbat/solis.py Outdated
Comment thread apps/predbat/solis.py Outdated
Comment thread apps/predbat/solis.py
Comment thread apps/predbat/solis.py
Comment thread apps/predbat/solis.py
Comment thread apps/predbat/solis.py
Comment thread apps/predbat/tests/test_solis.py
Comment thread apps/predbat/solis.py
@springfall2008 springfall2008 removed the BOT_REVIEW Trigger an autotriage label Sep 27, 2026
…s at once, hold hysteresis

- Always set the Battery Reserve bit. set_reserve_enable only decides whether
  Predbat changes the Battery Reserve SOC; with it off inverter.py plans
  against the live reserve, so the inverter must enforce it too. Gating the
  bit left the plan flooring at a register the inverter ignored, a reserve
  pinned after switching off mid-hold, and a mixed-fleet hole
- show_soc_limit_now(): a battery SOC limit change updates the cache and its
  entity at once; the write still happens from the event queue. Non-slot
  events wait for the next run(), far beyond Predbat's 2s write verify
- 1% hysteresis on the charge-slot hold (SOLIS_HOLD_HYSTERESIS) so the mode
  does not flip across the target boundary, without stopping a charge short
- V1 zeroes the slot current from the same hold check; one
  set_hold_storage_mode() for both firmware branches; SOLIS_SOC_LIMITS shared
  by publishing, the immediate update and the write handler
- Warn at startup when reserve still points at the over-discharge SOC
- Tests: hold hysteresis, hold values with the reserve bit (19 V1 / 17 V2),
  hold ending with the slot window, immediate SOC limit, the warning; existing
  storage-mode value tests now carry bit 4
- Docs: reserve always honoured, manual reserve binding, V2 hold label

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@springfall2008
springfall2008 merged commit 3a51ad4 into main Sep 27, 2026
2 checks passed
@springfall2008
springfall2008 deleted the feat/soliscloud-freeze-hold branch September 27, 2026 15:33
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.

1 participant