Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions apps/predbat/inverter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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((1 - self.battery_scaling) * 100, 2),
"unit_of_measurement": "kWh",
"device_class": "energy",
"state_class": "measurement",
Expand All @@ -658,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%}".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 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,
Expand All @@ -667,6 +672,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((1 - self.battery_scaling) * 100, 2),
"unit_of_measurement": "kWh",
"device_class": "energy",
"state_class": "measurement",
Expand Down Expand Up @@ -700,7 +706,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:
Expand Down
73 changes: 73 additions & 0 deletions apps/predbat/tests/test_find_battery_size.py
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,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()
Expand All @@ -799,6 +800,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
Expand Down Expand Up @@ -960,6 +968,66 @@ 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.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):
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 5% 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 test_find_battery_size_soc_kw_unavailable(my_predbat):
"""
Test find_battery_size when soc_kw history contains 'unavailable' entries.
Expand Down Expand Up @@ -1033,6 +1101,7 @@ def patched_get_history(entity_id, now=None, days=30):

# Restore soc_percent for subsequent tests
setup_predbat(my_predbat)

return failed


Expand Down Expand Up @@ -1133,5 +1202,9 @@ def run_find_battery_size_tests(my_predbat):
if failed:
return failed

failed |= test_find_battery_size_with_scaling(my_predbat)
if failed:
Comment thread
springfall2008 marked this conversation as resolved.
return failed

print("**** find_battery_size tests completed ****")
return failed
Loading