Skip to content

Commit 3a51ad4

Browse files
Merge pull request #5265 from springfall2008/feat/soliscloud-freeze-hold
feat(solis): SolisCloud freeze charge and holds via the Battery Reserve
2 parents a282b75 + f15c4cf commit 3a51ad4

8 files changed

Lines changed: 471 additions & 107 deletions

File tree

‎apps/predbat/config.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2460,7 +2460,8 @@
24602460
"has_charge_enable_time": True,
24612461
"has_discharge_enable_time": True,
24622462
"has_target_soc": True,
2463-
"has_reserve_soc": False,
2463+
# The Battery Reserve SOC (CID 157), a floor while the component keeps the Battery Reserve bit on
2464+
"has_reserve_soc": True,
24642465
"has_timed_pause": False,
24652466
"charge_time_format": "HH:MM:SS",
24662467
"charge_time_entity_is_option": True,

‎apps/predbat/solis.py‎

Lines changed: 179 additions & 45 deletions
Large diffs are not rendered by default.

‎apps/predbat/tests/test_discovery_contract.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,9 @@ def test_agreement_names_each_disagreement():
120120

121121

122122
def test_dummied_settings_follow_the_row():
123-
"""SolisCloud has no reserve, and no cloud type has a GE mode entity."""
124-
assert "reserve" in dummied_settings("SolisCloud")
123+
"""GS has no reserve, SolisCloud now does (its Battery Reserve SOC), and no cloud type has a GE mode entity."""
124+
assert "reserve" in dummied_settings("GS")
125+
assert "reserve" not in dummied_settings("SolisCloud")
125126
assert "reserve" not in dummied_settings("SunsynkCloud")
126127
assert "inverter_mode" in dummied_settings("SunsynkCloud")
127128
assert "inverter_mode" not in dummied_settings("GE")

‎apps/predbat/tests/test_solis.py‎

Lines changed: 279 additions & 54 deletions
Large diffs are not rendered by default.

‎docs/components.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1212,6 +1212,9 @@ Integrates with Solis inverters for monitoring and controlling Solis battery sys
12121212
- Manually set `soc_max` in `apps.yaml` with your battery capacity in kWh (recommended), or
12131213
- Leave `soc_max` unset and allow Predbat to automatically detect battery size from historical charging data (requires several days of data)
12141214
- Supports both V1 (older firmware) and V2 (newer firmware) time window formats
1215+
- **Reserve and holds**: Predbat's reserve is the inverter's **Battery Reserve SOC**, and Predbat keeps the inverter's Battery Reserve switched on so that SOC is always honoured as a discharge floor. With **switch.predbat_set_reserve_enable** on, Predbat also changes it - `set_reserve_min` normally, and just above the current SoC to hold the battery while a car or iBoost charges and for freeze or hold charging. With it off, Predbat leaves the Battery Reserve SOC as you set it and plans against it
1216+
- **Manual configuration** (`solis_automatic: false`): bind `reserve` to `number.predbat_solis_<serial>_reserve_soc`, not the over-discharge SOC. Predbat warns at startup if `reserve` still points at `..._over_discharge_soc`, as it would otherwise raise that safety floor to hold the battery
1217+
- **Freeze charging**: inside a charge slot with the battery already at the slot's target SOC, Predbat sets the storage mode to `Self-Use - No Grid Charging`, so the slot holds the battery without importing from the grid while solar can still charge it. It keeps the hold through a dip of 1% below the target before charging again. On V2 firmware this mode has the same register value as `Self-Use - No Timed Charge/Discharge`, which is how the storage mode select shows it
12151218
- Automatic configuration available - sets up all required Predbat sensors automatically
12161219
- **PV-only inverters**: an inverter Solis Cloud reports as having no battery is never managed as a battery inverter and is never written to, but its generation is still included in `pv_today` and `pv_power` so the array total covers the whole roof. Its load and grid readings are left out, as those registers can overlap with the battery inverter's on a shared-CT installation - set `pv_today`/`pv_power`/`load_today` manually with `solis_cloud_pv_load_ignore: true` if you need something different
12171220
- **Inverter timezone must match Predbat's `timezone` setting**: charge/discharge slot times are written to the inverter as plain `HH:MM` values with no timezone attached. The inverter interprets these using its own configured timezone, not Predbat's. If your inverter's timezone is set to UTC (or anything other than Predbat's `timezone`, `Europe/London` by default), the resulting charge/discharge windows will be offset by the difference - for example, a full hour out whenever British Summer Time is in effect. Set the inverter's own timezone to match Predbat's `timezone` setting to avoid this.

‎docs/discovery-catalogue.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,8 @@ An inverter record holds enough to rebuild that inverter's definition - the per-
228228
has a reserve, a target SoC, charge/discharge enable switches, idle times or a timed pause follows
229229
from which settings `entities` binds to a writable (`rw`) entity; and protocol detail such as the time
230230
format or whether the charge rate is set in watts or amps is read from those entities' descriptors.
231-
A setting Predbat replaces with a placeholder for this inverter type - SolisCloud's `reserve`, say - is
232-
left out of `entities`. See the design in
231+
A setting Predbat replaces with a placeholder for this inverter type - the Solis Modbus (`GS`) type's
232+
`reserve`, say - is left out of `entities`. See the design in
233233
`docs/superpowers/specs/2026-09-24-discovery-inverter-record-vocabulary-design.md`.
234234

235235
### Writing a reporter

‎docs/superpowers/specs/2026-09-24-discovery-inverter-record-vocabulary-design.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ is the first to change behaviour.
6363
| D6 | `ratings` are keyed by Predbat setting name, in Predbat's units. A sensor binding for a rating lives in `entities` under the same key. |
6464
| D7 | A site-wide export limit is reported as each inverter's share of it. Predbat sums `export_limit` across inverters, so the shares add back up to the site figure. |
6565
| D8 | `functions` reports what the component believes the device is, whether probed or assumed. There is no marker for an assumed value. |
66-
| D9 | SolisCloud does not write the reserve (it often won't change); it presents the battery minimum SoC instead. Its record reports `battery_min_soc` and no `reserve` (section 3). |
66+
| D9 | SolisCloud does not write the reserve (it often won't change); it presents the battery minimum SoC instead. Its record reports `battery_min_soc` and no `reserve` (section 3). **Superseded:** `reserve` was then bound to the over-discharge SOC. It is now the Battery Reserve SOC (CID 157), a floor while the component keeps the Battery Reserve bit on, so SolisCloud has a reserve and its record reports both `reserve` (rw) and `battery_min_soc` (r). |
6767
| D10 | A record describes its own device, not the fleet. `automatic_config()` today binds some settings only when every inverter has them (GE Cloud: when any has them); the record states what this device has, and piece 3's coordinator applies whatever fleet rule it keeps. |
6868
| D11 | A record describes the device, not the user's opt-outs. Settings that stop a binding (`automatic_ignore_pv`, `givtcp_rest_power_ignore`) do not remove entities from the record; piece 3's coordinator applies them. |
6969
| D12 | A PV-only device's record carries its `pv_power` and `pv_today` as `access: r` entities - still with no `inverter_type` and no `capabilities` - so its generation is not lost when the coordinator configures from records (the GH#4922 regression). |
@@ -158,7 +158,7 @@ published, because its component publishes them itself before `build_discovery()
158158
A setting that `inverter.py` replaces with a dummy entity for this inverter type is left out of `entities`,
159159
because Predbat never reads or writes the component's binding for it. Today that means any setting whose
160160
presence flag is False in the type's row (section 2), and `inverter_mode` when both `has_ge_inverter_mode`
161-
and `has_ge_eco_toggle` are False. Solis's `reserve` is one such case (D9).
161+
and `has_ge_eco_toggle` are False. The Solis Modbus (`GS`) type's `reserve` is one such case; SolisCloud's was until D9 was superseded.
162162

163163
A rating with a sensor appears twice: the number in `ratings`, the binding in `entities` with
164164
`access: r`. The coordinator binds the entity where one exists and the rating's number otherwise.

‎tools/debug-journal.md‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)