From 4e1dfb4a107c9a67036081d7073d2b9c7b4d1505 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 08:11:27 +0000 Subject: [PATCH 1/3] Initial plan From 0d24ba73fa1e51105507c58f28cfbb998b5fca04 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 08:26:34 +0000 Subject: [PATCH 2/3] Fix SolisCloud sticky reserve: clamp adjust_reserve at reserve_min, add warning log Agent-Logs-Url: https://github.com/springfall2008/batpred/sessions/e08373e6-d20f-4e46-8a22-9372b56b5bdb Co-authored-by: springfall2008 <48591903+springfall2008@users.noreply.github.com> --- apps/predbat/inverter.py | 16 +++++++++++++--- apps/predbat/tests/test_inverter.py | 13 +++++++++++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/apps/predbat/inverter.py b/apps/predbat/inverter.py index 23d49f7e2..f188830b4 100644 --- a/apps/predbat/inverter.py +++ b/apps/predbat/inverter.py @@ -476,6 +476,14 @@ def __init__(self, base, id=0, quiet=False, rest_postCommand=None, rest_getData= self.reserve_percent = self.reserve_min else: self.reserve_percent = self.reserve_percent_current + if self.reserve_percent_current > self.reserve_min and not quiet: + self.base.log( + "Warn: Inverter {} reserve is currently {}% which is higher than set_reserve_min of {}%. " + "The battery plan will be constrained to this reserve level until it is lowered. " + "If set_reserve_enable is on, Predbat will automatically reset it during the next execution cycle.".format( + self.id, dp0(self.reserve_percent_current), self.reserve_min + ) + ) self.reserve = dp3(self.soc_max * self.reserve_percent / 100.0) # Max inverter rate override @@ -1456,10 +1464,12 @@ def adjust_reserve(self, reserve): else: current_reserve = self.base.get_arg("reserve", index=self.id, default=0.0, required_unit="%") - # Round to integer and clamp to minimum + # Round to integer and clamp to minimum (use reserve_min not reserve_percent so that + # the reset call adjust_reserve(0) correctly resets to reserve_min rather than being + # clamped to a previously-elevated reserve_percent, which would leave a "sticky" reserve) reserve = int(reserve + 0.5) - if reserve < self.reserve_percent: - reserve = self.reserve_percent + if reserve < self.reserve_min: + reserve = self.reserve_min # Clamp reserve at max setting reserve = min(reserve, self.reserve_max) diff --git a/apps/predbat/tests/test_inverter.py b/apps/predbat/tests/test_inverter.py index 3e6ee62fa..e198809fa 100644 --- a/apps/predbat/tests/test_inverter.py +++ b/apps/predbat/tests/test_inverter.py @@ -220,7 +220,7 @@ def test_adjust_charge_window( return failed -def test_adjust_reserve(test_name, ha, inv, dummy_rest, prev_reserve, reserve, expect_reserve=None, reserve_min=4, reserve_max=100): +def test_adjust_reserve(test_name, ha, inv, dummy_rest, prev_reserve, reserve, expect_reserve=None, reserve_min=4, reserve_max=100, reserve_percent=None): """ Test inv.adjust_reserve(self, reserve): @@ -229,7 +229,12 @@ def test_adjust_reserve(test_name, ha, inv, dummy_rest, prev_reserve, reserve, e if expect_reserve is None: expect_reserve = reserve - inv.reserve_percent = reserve_min + # reserve_percent simulates the current planning floor (may differ from reserve_min for + # inverters where inv_has_reserve_soc is False, e.g. SolisCloud where the sensor can be "stuck" high) + if reserve_percent is None: + reserve_percent = reserve_min + inv.reserve_percent = reserve_percent + inv.reserve_min = reserve_min inv.reserve_max = reserve_max print("Test: {}".format(test_name)) @@ -1653,6 +1658,10 @@ def run_inverter_tests(my_predbat_dummy): failed |= test_adjust_reserve("adjust_reserve3", ha, inv, dummy_rest, 20, 100, reserve_max=100) failed |= test_adjust_reserve("adjust_reserve4", ha, inv, dummy_rest, 20, 100, 98, reserve_min=4, reserve_max=98) failed |= test_adjust_reserve("adjust_reserve5", ha, inv, dummy_rest, 50, 0, 0, reserve_min=0, reserve_max=100) + # Test SolisCloud "sticky reserve" bug fix: reserve_percent is elevated (80%) but reserve_min is low (4%). + # adjust_reserve(0) must clamp to reserve_min (4%), not to reserve_percent (80%). + failed |= test_adjust_reserve("adjust_reserve6_sticky_reserve", ha, inv, dummy_rest, 80, 0, 4, reserve_min=4, reserve_max=100, reserve_percent=80) + failed |= test_adjust_reserve("adjust_reserve7_sticky_reserve_custom_min", ha, inv, dummy_rest, 80, 0, 11, reserve_min=11, reserve_max=100, reserve_percent=80) if failed: return failed From 5caa936311b67b1df12bffd02f2175a610444c20 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 09:16:32 +0000 Subject: [PATCH 3/3] Simplify sticky reserve fix: use reserve_min as planning floor for non-managed inverters Agent-Logs-Url: https://github.com/springfall2008/batpred/sessions/afdc9cdf-fc4f-4025-b656-02c2eba79ced Co-authored-by: springfall2008 <48591903+springfall2008@users.noreply.github.com> --- apps/predbat/inverter.py | 18 ++++-------------- apps/predbat/tests/test_inverter.py | 4 ---- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/apps/predbat/inverter.py b/apps/predbat/inverter.py index f188830b4..7689fa5de 100644 --- a/apps/predbat/inverter.py +++ b/apps/predbat/inverter.py @@ -472,18 +472,10 @@ def __init__(self, base, id=0, quiet=False, rest_postCommand=None, rest_getData= self.reserve_min = battery_min_soc self.base.log("Reserve min: {}%, battery_min: {}%".format(self.reserve_min, dp0(battery_min_soc))) - if self.base.set_reserve_enable and self.inv_has_reserve_soc: + if (self.base.set_reserve_enable and self.inv_has_reserve_soc) or not self.inv_has_reserve_soc: self.reserve_percent = self.reserve_min else: self.reserve_percent = self.reserve_percent_current - if self.reserve_percent_current > self.reserve_min and not quiet: - self.base.log( - "Warn: Inverter {} reserve is currently {}% which is higher than set_reserve_min of {}%. " - "The battery plan will be constrained to this reserve level until it is lowered. " - "If set_reserve_enable is on, Predbat will automatically reset it during the next execution cycle.".format( - self.id, dp0(self.reserve_percent_current), self.reserve_min - ) - ) self.reserve = dp3(self.soc_max * self.reserve_percent / 100.0) # Max inverter rate override @@ -1464,12 +1456,10 @@ def adjust_reserve(self, reserve): else: current_reserve = self.base.get_arg("reserve", index=self.id, default=0.0, required_unit="%") - # Round to integer and clamp to minimum (use reserve_min not reserve_percent so that - # the reset call adjust_reserve(0) correctly resets to reserve_min rather than being - # clamped to a previously-elevated reserve_percent, which would leave a "sticky" reserve) + # Round to integer and clamp to minimum reserve = int(reserve + 0.5) - if reserve < self.reserve_min: - reserve = self.reserve_min + if reserve < self.reserve_percent: + reserve = self.reserve_percent # Clamp reserve at max setting reserve = min(reserve, self.reserve_max) diff --git a/apps/predbat/tests/test_inverter.py b/apps/predbat/tests/test_inverter.py index e198809fa..e6d1346e7 100644 --- a/apps/predbat/tests/test_inverter.py +++ b/apps/predbat/tests/test_inverter.py @@ -1658,10 +1658,6 @@ def run_inverter_tests(my_predbat_dummy): failed |= test_adjust_reserve("adjust_reserve3", ha, inv, dummy_rest, 20, 100, reserve_max=100) failed |= test_adjust_reserve("adjust_reserve4", ha, inv, dummy_rest, 20, 100, 98, reserve_min=4, reserve_max=98) failed |= test_adjust_reserve("adjust_reserve5", ha, inv, dummy_rest, 50, 0, 0, reserve_min=0, reserve_max=100) - # Test SolisCloud "sticky reserve" bug fix: reserve_percent is elevated (80%) but reserve_min is low (4%). - # adjust_reserve(0) must clamp to reserve_min (4%), not to reserve_percent (80%). - failed |= test_adjust_reserve("adjust_reserve6_sticky_reserve", ha, inv, dummy_rest, 80, 0, 4, reserve_min=4, reserve_max=100, reserve_percent=80) - failed |= test_adjust_reserve("adjust_reserve7_sticky_reserve_custom_min", ha, inv, dummy_rest, 80, 0, 11, reserve_min=11, reserve_max=100, reserve_percent=80) if failed: return failed