From 7971494b8b8d225b84028ec750a4721b3637e093 Mon Sep 17 00:00:00 2001 From: Trefor Southwell Date: Thu, 21 May 2026 21:17:45 +0100 Subject: [PATCH 1/6] Fix battery size --- apps/predbat/inverter.py | 6 +- apps/predbat/tests/test_find_battery_size.py | 68 +++++++++++++++++++- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/apps/predbat/inverter.py b/apps/predbat/inverter.py index baf3fd7a9..aadee09fa 100644 --- a/apps/predbat/inverter.py +++ b/apps/predbat/inverter.py @@ -372,8 +372,8 @@ def __init__(self, base, id=0, quiet=False, rest_postCommand=None, rest_getData= ivtime = idetails["Invertor_Time"] else: self.battery_temperature = self.base.get_arg("battery_temperature", default=20, index=self.id, required_unit="°C") - self.soc_max = self.base.get_arg("soc_max", default=0.0, index=self.id) * self.battery_scaling - self.nominal_capacity = self.soc_max + self.nominal_capacity = self.base.get_arg("soc_max", default=0.0, index=self.id) + self.soc_max = self.nominal_capacity * self.battery_scaling if self.inverter_type in ["GE", "GEC", "GEE"]: self.battery_rate_max_raw = self.base.get_arg("charge_rate", attribute="max", index=self.id, default=2600.0, required_unit="W") @@ -724,7 +724,7 @@ def find_battery_size(self, nominal_capacity=0): clean_increment=False, smoothing=False, divide_by=1.0, - scale=self.battery_scaling, + scale=1.0, required_unit="%", ) battery_power, _ = minute_data( diff --git a/apps/predbat/tests/test_find_battery_size.py b/apps/predbat/tests/test_find_battery_size.py index 394e8c45e..ff60d0149 100644 --- a/apps/predbat/tests/test_find_battery_size.py +++ b/apps/predbat/tests/test_find_battery_size.py @@ -953,6 +953,68 @@ def test_update_soc_max_calculated_sensor_mixed_none(my_predbat): return failed +def test_find_battery_size_with_scaling(my_predbat): + """ + Test find_battery_size with battery_scaling < 1.0 (80% depth-of-discharge). + + A 10 kWh nominal battery with battery_scaling=0.8 has a usable capacity of 8 kWh. + The SoC sensor reports 0-100% of that 8 kWh usable range. + find_battery_size must return ~8 kWh, not ~10 kWh. + + This regression test catches the bug where battery_scaling was applied to the SoC + percentage values inside find_battery_size, causing the nominal capacity to be + returned instead of the usable (scaled) capacity. + """ + print("*** Running test: find_battery_size_with_scaling ***") + failed = False + + nominal_kwh = 10.0 + battery_scaling = 0.8 + usable_kwh = nominal_kwh * battery_scaling # 8 kWh + + inv = Inverter(my_predbat, 0) + inv.battery_rate_max_charge = 2600 / 60000 + inv.battery_rate_max_discharge = 2600 / 60000 + inv.soc_max = usable_kwh + inv.nominal_capacity = nominal_kwh + inv.battery_scaling = battery_scaling + + setup_predbat(my_predbat) + + # History data reflects 0-100% SoC of the usable 8 kWh range + create_test_history_data(my_predbat, num_days=2, battery_size_kwh=usable_kwh) + + try: + estimated_size = inv.find_battery_size(nominal_kwh) + if estimated_size is None: + print("ERROR: find_battery_size returned None; expected ~{} kWh".format(usable_kwh)) + failed = True + else: + print("Estimated battery size: {:.2f} kWh (expected: {:.2f} kWh usable, nominal {:.2f} kWh)".format(estimated_size, usable_kwh, nominal_kwh)) + tolerance = 0.20 + lower_bound = usable_kwh * (1 - tolerance) + upper_bound = usable_kwh * (1 + tolerance) + if not (lower_bound <= estimated_size <= upper_bound): + print( + "ERROR: Estimated size {:.2f} kWh is outside usable range [{:.2f}, {:.2f}] kWh. " + "If this is ~{:.2f} kWh the battery_scaling bug is present (SoC percent was scaled " + "inside find_battery_size, returning nominal instead of usable capacity).".format( + estimated_size, lower_bound, upper_bound, nominal_kwh + ) + ) + failed = True + else: + print("SUCCESS: find_battery_size returned usable capacity {:.2f} kWh (within 20% of {:.2f} kWh)".format(estimated_size, usable_kwh)) + except Exception as e: + print("ERROR: find_battery_size raised exception: {}".format(e)) + import traceback + + traceback.print_exc() + failed = True + + return failed + + def run_find_battery_size_tests(my_predbat): """ Run all find_battery_size tests @@ -1029,7 +1091,7 @@ def run_find_battery_size_tests(my_predbat): failed |= test_battery_size_tracking_no_nominal(my_predbat) if failed: return failed - + failed |= test_battery_size_tracking_none_stored_on_failure(my_predbat) if failed: return failed @@ -1043,6 +1105,10 @@ def run_find_battery_size_tests(my_predbat): return failed failed |= test_update_soc_max_calculated_sensor_mixed_none(my_predbat) + if failed: + return failed + + failed |= test_find_battery_size_with_scaling(my_predbat) if failed: return failed From 22a9ee259de5c68e67a466fbd6b04ba86767728e Mon Sep 17 00:00:00 2001 From: Trefor Southwell Date: Fri, 22 May 2026 08:10:15 +0100 Subject: [PATCH 2/6] Test updates and print --- apps/predbat/inverter.py | 4 +++- apps/predbat/tests/test_find_battery_size.py | 12 ++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/apps/predbat/inverter.py b/apps/predbat/inverter.py index aadee09fa..e32299562 100644 --- a/apps/predbat/inverter.py +++ b/apps/predbat/inverter.py @@ -641,6 +641,7 @@ def update_soc_max_calculated_sensor(self, found_size, nominal_capacity=0): "history": history, "nominal_capacity": round(nominal_capacity, 3), "degradation_percent": None, + "configured_degradation": round(self.battery_scaling * 100, 2), "unit_of_measurement": "kWh", "device_class": "energy", "state_class": "measurement", @@ -658,7 +659,7 @@ def update_soc_max_calculated_sensor(self, found_size, nominal_capacity=0): found_size_str = "{:.2f} kWh".format(found_size) if found_size is not None else "None" degradation = (self.nominal_capacity - trimmed_mean) / self.nominal_capacity if self.nominal_capacity > 0 else 0 - self.log("Inverter {} battery size tracking: found_size {}, history {}, trimmed_mean {:.2f} kWh, degradation {:.2%}".format(self.id, found_size_str, history, trimmed_mean, degradation)) + self.log("Inverter {} battery size tracking: found_size {}, history {}, trimmed_mean {:.2f} kWh, degradation {:.2%} configured degradation {:.2f}".format(self.id, found_size_str, history, trimmed_mean, degradation, self.battery_scaling)) self.base.dashboard_item( sensor_name, @@ -667,6 +668,7 @@ def update_soc_max_calculated_sensor(self, found_size, nominal_capacity=0): "history": history, "nominal_capacity": round(nominal_capacity, 3), "degradation_percent": round(degradation * 100, 2), + "configured_degradation": round(self.battery_scaling * 100, 2), "unit_of_measurement": "kWh", "device_class": "energy", "state_class": "measurement", diff --git a/apps/predbat/tests/test_find_battery_size.py b/apps/predbat/tests/test_find_battery_size.py index ff60d0149..886c50d13 100644 --- a/apps/predbat/tests/test_find_battery_size.py +++ b/apps/predbat/tests/test_find_battery_size.py @@ -776,6 +776,7 @@ def test_battery_size_tracking_none_stored_on_failure(my_predbat): # Mock find_battery_size to always fail inv.find_battery_size = lambda _nc=0: None + original_soc_max = inv.soc_max # 10.0 try: inv.battery_size_tracking() @@ -792,6 +793,13 @@ def test_battery_size_tracking_none_stored_on_failure(my_predbat): failed = True else: print("SUCCESS: None correctly stored in history to prevent re-calculation") + + # soc_max must remain unchanged when find_battery_size fails and battery_scaling_auto is off + if inv.soc_max != original_soc_max: + print("ERROR: soc_max changed from {:.3f} to {:.3f} when it should remain unchanged after a failed find_battery_size".format(original_soc_max, inv.soc_max)) + failed = True + else: + print("SUCCESS: soc_max remains {:.3f} kWh after failed find_battery_size".format(inv.soc_max)) except Exception as e: print("ERROR: test raised exception: {}".format(e)) import traceback @@ -991,7 +999,7 @@ def test_find_battery_size_with_scaling(my_predbat): failed = True else: print("Estimated battery size: {:.2f} kWh (expected: {:.2f} kWh usable, nominal {:.2f} kWh)".format(estimated_size, usable_kwh, nominal_kwh)) - tolerance = 0.20 + tolerance = 0.05 # 5% tolerance since scaling should be applied correctly lower_bound = usable_kwh * (1 - tolerance) upper_bound = usable_kwh * (1 + tolerance) if not (lower_bound <= estimated_size <= upper_bound): @@ -1004,7 +1012,7 @@ def test_find_battery_size_with_scaling(my_predbat): ) failed = True else: - print("SUCCESS: find_battery_size returned usable capacity {:.2f} kWh (within 20% of {:.2f} kWh)".format(estimated_size, usable_kwh)) + print("SUCCESS: find_battery_size returned usable capacity {:.2f} kWh (within 5% of {:.2f} kWh)".format(estimated_size, usable_kwh)) except Exception as e: print("ERROR: find_battery_size raised exception: {}".format(e)) import traceback From 972ad66851d97d80086e6ced289efc373e70867b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 07:35:45 +0000 Subject: [PATCH 3/6] [pre-commit.ci lite] apply automatic fixes --- apps/predbat/tests/test_find_battery_size.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/predbat/tests/test_find_battery_size.py b/apps/predbat/tests/test_find_battery_size.py index 886c50d13..8587b131b 100644 --- a/apps/predbat/tests/test_find_battery_size.py +++ b/apps/predbat/tests/test_find_battery_size.py @@ -1006,9 +1006,7 @@ def test_find_battery_size_with_scaling(my_predbat): print( "ERROR: Estimated size {:.2f} kWh is outside usable range [{:.2f}, {:.2f}] kWh. " "If this is ~{:.2f} kWh the battery_scaling bug is present (SoC percent was scaled " - "inside find_battery_size, returning nominal instead of usable capacity).".format( - estimated_size, lower_bound, upper_bound, nominal_kwh - ) + "inside find_battery_size, returning nominal instead of usable capacity).".format(estimated_size, lower_bound, upper_bound, nominal_kwh) ) failed = True else: @@ -1099,7 +1097,7 @@ def run_find_battery_size_tests(my_predbat): failed |= test_battery_size_tracking_no_nominal(my_predbat) if failed: return failed - + failed |= test_battery_size_tracking_none_stored_on_failure(my_predbat) if failed: return failed @@ -1114,8 +1112,8 @@ def run_find_battery_size_tests(my_predbat): failed |= test_update_soc_max_calculated_sensor_mixed_none(my_predbat) if failed: - return failed - + return failed + failed |= test_find_battery_size_with_scaling(my_predbat) if failed: return failed From dacbcfa1b18586bc7d17e47fe25f1bba9dd4c26a Mon Sep 17 00:00:00 2001 From: Trefor Southwell Date: Fri, 22 May 2026 14:59:08 +0100 Subject: [PATCH 4/6] Fix merge issue --- apps/predbat/inverter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/predbat/inverter.py b/apps/predbat/inverter.py index 501a328b7..d0c0351a9 100644 --- a/apps/predbat/inverter.py +++ b/apps/predbat/inverter.py @@ -702,7 +702,7 @@ def find_battery_size(self, nominal_capacity=0): clean_increment=False, smoothing=False, divide_by=1.0, - scale=self.battery_scaling, + scale=1.0, required_unit="%", ) else: From 50e431109f659c312fc8d9deef1cdf2b53e4e0b1 Mon Sep 17 00:00:00 2001 From: Trefor Southwell Date: Fri, 22 May 2026 15:08:32 +0100 Subject: [PATCH 5/6] Review feedback --- apps/predbat/inverter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/predbat/inverter.py b/apps/predbat/inverter.py index d0c0351a9..ae2ec91dd 100644 --- a/apps/predbat/inverter.py +++ b/apps/predbat/inverter.py @@ -641,7 +641,7 @@ def update_soc_max_calculated_sensor(self, found_size, nominal_capacity=0): "history": history, "nominal_capacity": round(nominal_capacity, 3), "degradation_percent": None, - "configured_degradation": round(self.battery_scaling * 100, 2), + "configured_degradation": round((1 - self.battery_scaling) * 100, 2), "unit_of_measurement": "kWh", "device_class": "energy", "state_class": "measurement", @@ -659,7 +659,7 @@ def update_soc_max_calculated_sensor(self, found_size, nominal_capacity=0): found_size_str = "{:.2f} kWh".format(found_size) if found_size is not None else "None" degradation = (self.nominal_capacity - trimmed_mean) / self.nominal_capacity if self.nominal_capacity > 0 else 0 - self.log("Inverter {} battery size tracking: found_size {}, history {}, trimmed_mean {:.2f} kWh, degradation {:.2%} configured degradation {:.2f}".format(self.id, found_size_str, history, trimmed_mean, degradation, self.battery_scaling)) + self.log("Inverter {} battery size tracking: found_size {}, history {}, trimmed_mean {:.2f} kWh, degradation {:.2%}, configured battery_scaling {:.0f}% (configured degradation {:.0f}%)".format(self.id, found_size_str, history, trimmed_mean, degradation, self.battery_scaling * 100, (1 - self.battery_scaling) * 100)) self.base.dashboard_item( sensor_name, @@ -668,7 +668,7 @@ def update_soc_max_calculated_sensor(self, found_size, nominal_capacity=0): "history": history, "nominal_capacity": round(nominal_capacity, 3), "degradation_percent": round(degradation * 100, 2), - "configured_degradation": round(self.battery_scaling * 100, 2), + "configured_degradation": round((1 - self.battery_scaling) * 100, 2), "unit_of_measurement": "kWh", "device_class": "energy", "state_class": "measurement", From ab2a99c7408b71a1708578d60519e593f69d2224 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 14:10:46 +0000 Subject: [PATCH 6/6] [pre-commit.ci lite] apply automatic fixes --- apps/predbat/inverter.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/predbat/inverter.py b/apps/predbat/inverter.py index ae2ec91dd..acdded6e1 100644 --- a/apps/predbat/inverter.py +++ b/apps/predbat/inverter.py @@ -659,7 +659,11 @@ def update_soc_max_calculated_sensor(self, found_size, nominal_capacity=0): found_size_str = "{:.2f} kWh".format(found_size) if found_size is not None else "None" degradation = (self.nominal_capacity - trimmed_mean) / self.nominal_capacity if self.nominal_capacity > 0 else 0 - self.log("Inverter {} battery size tracking: found_size {}, history {}, trimmed_mean {:.2f} kWh, degradation {:.2%}, configured battery_scaling {:.0f}% (configured degradation {:.0f}%)".format(self.id, found_size_str, history, trimmed_mean, degradation, self.battery_scaling * 100, (1 - self.battery_scaling) * 100)) + self.log( + "Inverter {} battery size tracking: found_size {}, history {}, trimmed_mean {:.2f} kWh, degradation {:.2%}, configured battery_scaling {:.0f}% (configured degradation {:.0f}%)".format( + self.id, found_size_str, history, trimmed_mean, degradation, self.battery_scaling * 100, (1 - self.battery_scaling) * 100 + ) + ) self.base.dashboard_item( sensor_name,