diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py index 74323619a..b48a3d5fc 100644 --- a/apps/predbat/predbat.py +++ b/apps/predbat/predbat.py @@ -27,10 +27,10 @@ import requests import asyncio -THIS_VERSION = "v8.30.8" +THIS_VERSION = "v8.30.9" # fmt: off -PREDBAT_FILES = ["predbat.py", "hass.py", "config.py", "prediction.py", "gecloud.py", "utils.py", "inverter.py", "ha.py", "download.py", "web.py", "web_helper.py", "predheat.py", "futurerate.py", "octopus.py", "solcast.py", "execute.py", "plan.py", "fetch.py", "output.py", "userinterface.py", "energydataservice.py", "alertfeed.py", "compare.py", "db_manager.py", "db_engine.py", "plugin_system.py", "ohme.py", "components.py", "fox.py", "carbon.py", "web_mcp.py", "component_base.py", "axle.py"] +PREDBAT_FILES = ["predbat.py", "hass.py", "config.py", "prediction.py", "gecloud.py", "utils.py", "inverter.py", "ha.py", "download.py", "web.py", "web_helper.py", "predheat.py", "futurerate.py", "octopus.py", "solcast.py", "execute.py", "plan.py", "fetch.py", "output.py", "userinterface.py", "energydataservice.py", "alertfeed.py", "compare.py", "db_manager.py", "db_engine.py", "plugin_system.py", "ohme.py", "components.py", "fox.py", "carbon.py", "web_mcp.py", "component_base.py", "axle.py", "unit_test.py"] # fmt: on from download import predbat_update_move, predbat_update_download, check_install diff --git a/apps/predbat/tests/test_find_charge_rate.py b/apps/predbat/tests/test_find_charge_rate.py index 5ff202c14..da50efeeb 100644 --- a/apps/predbat/tests/test_find_charge_rate.py +++ b/apps/predbat/tests/test_find_charge_rate.py @@ -64,3 +64,118 @@ def test_find_charge_rate(my_predbat): print("**** ERROR: Best real rate should be 1225 ****") failed = 1 return failed + + +def test_find_charge_rate_string_temperature(my_predbat): + """ + Test find_charge_rate with string temperature indices in the curve + This tests backward compatibility with configs that may have string keys + Uses extreme temperature curve values to ensure they actually affect the result + """ + failed = 0 + + current_charge_rate = 952 + soc = 5.0 # Lower SOC so more charge is needed + soc_max = 9.52 + log_to = print # my_predbat.log + minutes_now = my_predbat.minutes_now + window = {"start": minutes_now - 60, "end": minutes_now + 50} + target_soc = soc_max + # Use a flat charge curve so temperature becomes the only limiting factor + battery_charge_power_curve = {100: 1.0, 99: 1.0, 98: 1.0, 97: 1.0, 96: 1.0, 95: 1.0, 94: 1.0, 93: 1.0, 92: 1.0, 91: 1.0, 90: 1.0} + set_charge_low_power = True + charge_low_power_margin = my_predbat.charge_low_power_margin + battery_rate_min = 0 + battery_rate_max_scaling = 1 + battery_loss = 0.96 + battery_temperature = 17.0 + # Temperature curve with STRING keys - these are scale factors that reduce charging rate + # Using 1.0 (no reduction) for most temps, and 0.5 (50% reduction) at 17 degrees + battery_temperature_curve = {"19": 1.0, "18": 1.0, "17": 0.5, "16": 1.0, "15": 1.0, "14": 1.0, "13": 1.0, "12": 1.0, "11": 1.0, "10": 1.0, "9": 1.0, "8": 1.0, "7": 1.0, "6": 1.0, "5": 1.0, "4": 1.0, "3": 1.0, "2": 1.0, "1": 1.0, "0": 1.0} + max_rate = 6000 # High enough that temperature becomes the limiting factor + + best_rate, best_rate_real = find_charge_rate( + minutes_now, + soc, + window, + target_soc, + max_rate / MINUTE_WATT, + soc_max, + battery_charge_power_curve, + set_charge_low_power, + charge_low_power_margin, + battery_rate_min / MINUTE_WATT, + battery_rate_max_scaling, + battery_loss, + log_to, + battery_temperature=battery_temperature, + battery_temperature_curve=battery_temperature_curve, + current_charge_rate=current_charge_rate / MINUTE_WATT, + ) + print("String temp test - Best_rate {} Best_rate_real {}".format(best_rate * MINUTE_WATT, best_rate_real * MINUTE_WATT)) + # With temp scale factor 0.5 at 17 degrees and high max_rate, temperature should be the limiting factor + # Temperature cap: 9.52 * 0.5 / 60 = 0.0793 kW/min = 4760W + # Best rate should be capped by temperature to ~4760W + if best_rate * MINUTE_WATT != 6000: + print("**** ERROR: Best rate should be 6000 (with string temp curve) ****") + failed = 1 + # Best_rate_real should be temperature-limited to ~4760W + expected_temp_limited = int(9.52 * 0.5 / 60 * MINUTE_WATT) # Should be ~4760W + if abs(best_rate_real * MINUTE_WATT - expected_temp_limited) > 100: # Allow 100W tolerance + print("**** ERROR: Best real rate {} should be temp-limited to ~{}W ****".format(best_rate_real * MINUTE_WATT, expected_temp_limited)) + failed = 1 + return failed + + +def test_find_charge_rate_string_charge_curve(my_predbat): + """ + Test find_charge_rate with string charge power curve indices + This tests backward compatibility with configs that may have string keys in charge curve + """ + failed = 0 + + current_charge_rate = 952 + soc = 9.04 + soc_max = 9.52 + log_to = print # my_predbat.log + minutes_now = my_predbat.minutes_now + window = {"start": minutes_now - 60, "end": minutes_now + 50} + target_soc = soc_max + # Battery charge power curve with STRING keys instead of integers + battery_charge_power_curve = {"100": 0.15, "99": 0.15, "98": 0.23, "97": 0.3, "96": 0.42, "95": 0.49, "94": 0.55, "93": 0.69, "92": 0.79, "91": 0.89, "90": 0.96} + set_charge_low_power = True + charge_low_power_margin = my_predbat.charge_low_power_margin + battery_rate_min = 0 + battery_rate_max_scaling = 1 + battery_loss = 0.96 + battery_temperature = 17.0 + battery_temperature_curve = {19: 0.33, 18: 0.33, 17: 0.33, 16: 0.33, 15: 0.33, 14: 0.33, 13: 0.33, 12: 0.33, 11: 0.33, 10: 0.25, 9: 0.25, 8: 0.25, 7: 0.25, 6: 0.25, 5: 0.25, 4: 0.25, 3: 0.25, 2: 0.25, 1: 0.15, 0: 0.00} + max_rate = 2500 + + best_rate, best_rate_real = find_charge_rate( + minutes_now, + soc, + window, + target_soc, + max_rate / MINUTE_WATT, + soc_max, + battery_charge_power_curve, + set_charge_low_power, + charge_low_power_margin, + battery_rate_min / MINUTE_WATT, + battery_rate_max_scaling, + battery_loss, + log_to, + battery_temperature=battery_temperature, + battery_temperature_curve=battery_temperature_curve, + current_charge_rate=current_charge_rate / MINUTE_WATT, + ) + print("String charge curve test - Best_rate {} Best_rate_real {}".format(best_rate * MINUTE_WATT, best_rate_real * MINUTE_WATT)) + # Should get the same results as with integer keys + if best_rate * MINUTE_WATT != 2500: + print("**** ERROR: Best rate should be 2500 (with string charge curve) ****") + failed = 1 + if best_rate_real * MINUTE_WATT != 1225: + print("**** ERROR: Best real rate should be 1225 (with string charge curve) ****") + failed = 1 + return failed diff --git a/apps/predbat/unit_test.py b/apps/predbat/unit_test.py index 421cd1b7c..59fb0d6fd 100644 --- a/apps/predbat/unit_test.py +++ b/apps/predbat/unit_test.py @@ -33,7 +33,7 @@ from tests.test_nordpool import run_nordpool_test from tests.test_car_charging_smart import run_car_charging_smart_tests from tests.test_plugin_startup import test_plugin_startup_order -from tests.test_optimise_levels import run_optimise_levels +from tests.test_optimise_levels import run_optimise_levels_tests from tests.test_energydataservice import test_energydataservice from tests.test_iboost import run_iboost_smart_tests from tests.test_alert_feed import test_alert_feed @@ -123,7 +123,7 @@ from tests.test_hainterface_websocket import run_hainterface_websocket_tests from tests.test_web_if import run_test_web_if from tests.test_window import run_window_sort_tests, run_intersect_window_tests -from tests.test_find_charge_rate import test_find_charge_rate +from tests.test_find_charge_rate import test_find_charge_rate, test_find_charge_rate_string_temperature, test_find_charge_rate_string_charge_curve from tests.test_manual_api import run_test_manual_api from tests.test_manual_soc import run_test_manual_soc from tests.test_manual_times import run_test_manual_times @@ -405,6 +405,8 @@ def main(): ("octopus_slots", run_load_octopus_slots_tests, "Load Octopus slots tests", False), ("rate_add_io_slots", run_rate_add_io_slots_tests, "Rate add IO slots tests", False), ("find_charge_rate", test_find_charge_rate, "Find charge rate tests", False), + ("find_charge_rate_string_temp", test_find_charge_rate_string_temperature, "Find charge rate string temperature", False), + ("find_charge_rate_string_curve", test_find_charge_rate_string_charge_curve, "Find charge rate string charge curve", False), ("find_charge_curve", run_find_charge_curve_tests, "Find charge curve tests", False), ("energydataservice", test_energydataservice, "Energy data service tests", False), ("saving_session", test_saving_session, "Saving session tests", False), @@ -617,7 +619,7 @@ def main(): ("ohme_switch_max_charge_off", test_ohme_switch_event_handler_max_charge_off, "Ohme switch_event_handler max_charge off", False), ("ohme_switch_approve_charge", test_ohme_switch_event_handler_approve_charge, "Ohme switch_event_handler approve_charge", False), ("ohme_switch_approve_wrong_status", test_ohme_switch_event_handler_approve_charge_wrong_status, "Ohme switch_event_handler approve wrong status", False), - ("optimise_levels", run_optimise_levels, "Optimise levels tests", True), + ("optimise_levels", run_optimise_levels_tests, "Optimise levels tests", True), ("optimise_windows", run_optimise_all_windows_tests, "Optimise all windows tests", True), ("debug_cases", run_debug_cases, "Debug case file tests", True), ("download_octopus_rates", test_octopus_download_rates_wrapper, "Test download octopus rates", False), diff --git a/apps/predbat/utils.py b/apps/predbat/utils.py index 2145b2d30..9e64c7463 100644 --- a/apps/predbat/utils.py +++ b/apps/predbat/utils.py @@ -831,36 +831,6 @@ def str2time(str): return tdata -def get_curve_value(curve, key, default=1.0): - """ - Get a value from a battery power curve dictionary. - Supports both integer and string keys for compatibility with YAML configurations. - - Args: - curve: Dictionary containing the power curve (e.g., battery_charge_power_curve) - key: Integer SOC percentage to look up - default: Default value if key not found (default: 1.0) - - Returns: - The curve value at the given SOC percentage, or default if not found - - Note: - This function handles both integer keys (100, 99, 98) and string keys ("100", "99", "98") - to support YAML configurations that require string keys for encryption (e.g., SOPS). - """ - # Try integer key first (most common case) - if key in curve: - return curve[key] - - # Try string key for YAML configs with string-based keys - str_key = str(key) - if str_key in curve: - return curve[str_key] - - # Return default if neither found - return default - - def calc_percent_limit(charge_limit, soc_max): """ Calculate a charge limit in percent @@ -974,18 +944,35 @@ def get_discharge_rate_curve(soc, discharge_rate_setting, soc_max, battery_rate_ return max(min(discharge_rate_setting, max_discharge_rate), battery_rate_min) +""" +Get value from curve with integer or string index +""" + + +def get_curve_value(curve, index, default=1.0): + if index in curve: + return curve[index] + elif str(index) in curve: + return curve[str(index)] + else: + return default + + def find_battery_temperature_cap(battery_temperature, battery_temperature_curve, soc_max, max_rate): """ Find the battery temperature cap """ battery_temperature_idx = min(battery_temperature, 20) battery_temperature_idx = max(battery_temperature_idx, -20) - if battery_temperature_idx in battery_temperature_curve: - battery_temperature_adjust = battery_temperature_curve[battery_temperature_idx] - elif battery_temperature_idx > 0: - battery_temperature_adjust = battery_temperature_curve.get(20, 1.0) - else: - battery_temperature_adjust = battery_temperature_curve.get(0, 1.0) + battery_temperature_idx = int(battery_temperature_idx) # Convert to int for proper key matching + # Try to get the temperature adjustment from the curve (handles both int and string keys) + battery_temperature_adjust = get_curve_value(battery_temperature_curve, battery_temperature_idx, None) + if battery_temperature_adjust is None: + # If not found, try fallback values + if battery_temperature_idx > 0: + battery_temperature_adjust = get_curve_value(battery_temperature_curve, 20, 1.0) + else: + battery_temperature_adjust = get_curve_value(battery_temperature_curve, 0, 1.0) battery_temperature_rate_cap = soc_max * battery_temperature_adjust / 60.0 return min(battery_temperature_rate_cap, max_rate)