From 208dcc1967059c040273f14a5a03f81ab00d5827 Mon Sep 17 00:00:00 2001 From: Trefor Southwell Date: Sun, 2 Nov 2025 15:59:02 +0000 Subject: [PATCH 1/9] Unit tests Increase token timeout for MCP --- apps/predbat/unit_test.py | 39 +++++++++++++++++++++++++++++++++++++-- apps/predbat/web_mcp.py | 2 +- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/apps/predbat/unit_test.py b/apps/predbat/unit_test.py index f113e0d0d..f304fe22a 100644 --- a/apps/predbat/unit_test.py +++ b/apps/predbat/unit_test.py @@ -3069,6 +3069,7 @@ def run_execute_test( assert_discharge_start_time_minutes=-1, assert_discharge_end_time_minutes=-1, inverter_charge_time_minutes_start=-1, + inverter_charge_time_minutes_end=-1, assert_charge_rate=None, assert_discharge_rate=None, assert_reserve=0, @@ -3093,6 +3094,7 @@ def run_execute_test( battery_temperature=20, ): print("Run scenario {}".format(name)) + my_predbat.log("Run scenario {}".format(name)) failed = False my_predbat.set_read_only = read_only my_predbat.car_charging_slots = [car_slot] @@ -3122,6 +3124,7 @@ def run_execute_test( my_predbat.set_reserve_enable = set_reserve_enable for inverter in my_predbat.inverters: inverter.charge_start_time_minutes = inverter_charge_time_minutes_start + inverter.charge_end_time_minutes = inverter_charge_time_minutes_end if soc_kw_array: inverter.soc_kw = soc_kw_array[inverter.id] else: @@ -3518,7 +3521,7 @@ def run_test_web_if(my_predbat): ha = my_predbat.ha_interface # Fetch page from 127.0.0.1:5052 - for page in ["/", "/dash", "/plan", "/config", "/apps", "/charts", "/compare", "/log", "/config", "/entity"]: + for page in ["/", "/dash", "/plan", "/config", "/apps", "/charts", "/compare", "/log", "/entity", "/components", "/browse"]: print("Fetch page {}".format(page)) address = "http://127.0.0.1:5052" + page res = requests.get(address) @@ -3567,6 +3570,8 @@ def run_execute_tests(my_predbat): charge_window_best5 = [{"start": my_predbat.minutes_now - 24 * 60, "end": my_predbat.minutes_now + 60, "average": 1}] charge_window_best6 = [{"start": my_predbat.minutes_now + 8 * 60, "end": my_predbat.minutes_now + 60 + 8 * 60, "average": 1}] charge_window_best7 = [{"start": my_predbat.minutes_now, "end": my_predbat.minutes_now + 23 * 60, "average": 1}] + charge_window_best7b = [{"start": 24*60-5, "end": my_predbat.minutes_now + 23 * 60, "average": 1}] + charge_window_best7c = [{"start": 0, "end": 11 * 60, "average": 1}] charge_window_best8 = [{"start": 0, "end": my_predbat.minutes_now + 12 * 60, "average": 1}] charge_window_best9 = [{"start": my_predbat.minutes_now + 60, "end": my_predbat.minutes_now + 90, "average": 1}] charge_window_best_short = [{"start": my_predbat.minutes_now, "end": my_predbat.minutes_now + 15, "average": 1}] @@ -4312,7 +4317,7 @@ def run_execute_tests(my_predbat): failed |= run_execute_test( my_predbat, - "charge_midnight2", + "charge_midnight2a", charge_window_best=charge_window_best7, charge_limit_best=charge_limit_best, assert_charge_time_enable=True, @@ -4321,7 +4326,37 @@ def run_execute_tests(my_predbat): assert_status="Charging", assert_charge_start_time_minutes=my_predbat.minutes_now, assert_charge_end_time_minutes=24 * 60 - 1, + inverter_charge_time_minutes_start = 30, ) + failed |= run_execute_test( + my_predbat, + "charge_midnight2b", + charge_window_best=charge_window_best7b, + charge_limit_best=charge_limit_best, + assert_charge_time_enable=True, + set_charge_window=True, + set_export_window=True, + assert_status="Charging", + assert_charge_start_time_minutes=24*60-30, + assert_charge_end_time_minutes=24 * 60 - 1, + inverter_charge_time_minutes_start = 30, + minutes_now=24*60-5, + ) + failed |= run_execute_test( + my_predbat, + "charge_midnight2c", + charge_window_best=charge_window_best7c, + charge_limit_best=charge_limit_best, + assert_charge_time_enable=True, + set_charge_window=True, + set_export_window=True, + assert_status="Charging", + assert_charge_start_time_minutes=0, + assert_charge_end_time_minutes=11*60, + inverter_charge_time_minutes_start=12*60, + minutes_now=0, + ) + for inverter in my_predbat.inverters: inverter.inv_can_span_midnight = True if failed: diff --git a/apps/predbat/web_mcp.py b/apps/predbat/web_mcp.py index edafa34a2..73bd61993 100644 --- a/apps/predbat/web_mcp.py +++ b/apps/predbat/web_mcp.py @@ -65,7 +65,7 @@ def __init__(self, enable, mcp_secret, mcp_port, base): self.jwt_secret = hashlib.sha256(f"jwt_signing_key_{self.mcp_secret}".encode()).hexdigest() self.jwt_algorithm = "HS256" - self.access_token_lifetime = timedelta(hours=1) + self.access_token_lifetime = timedelta(hours=24) self.authorization_code_lifetime = timedelta(minutes=10) # Authorization codes storage (code -> user/client info) From 847fec0a40d30fd1d06d9316e152afe8a1d6f535 Mon Sep 17 00:00:00 2001 From: Trefor Southwell Date: Sun, 2 Nov 2025 15:59:31 +0000 Subject: [PATCH 2/9] Bump version --- apps/predbat/predbat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py index 8845e964e..9d524f0c0 100644 --- a/apps/predbat/predbat.py +++ b/apps/predbat/predbat.py @@ -25,7 +25,7 @@ import requests import asyncio -THIS_VERSION = "v8.27.1" +THIS_VERSION = "v8.27.2" # fmt: off PREDBAT_FILES = ["predbat.py", "config.py", "prediction.py", "gecloud.py","utils.py", "inverter.py", "ha.py", "download.py", "unit_test.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"] From 70cccc13cdf38ba6a3d4be46270a25d4d7582f15 Mon Sep 17 00:00:00 2001 From: Trefor Southwell Date: Sun, 2 Nov 2025 16:15:28 +0000 Subject: [PATCH 3/9] Charge spanning midnight error --- apps/predbat/inverter.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/apps/predbat/inverter.py b/apps/predbat/inverter.py index e05800814..1bdbbec45 100644 --- a/apps/predbat/inverter.py +++ b/apps/predbat/inverter.py @@ -1985,8 +1985,9 @@ def adjust_force_export(self, force_export, new_start_time=None, new_end_time=No self.write_and_poll_switch("scheduled_discharge_enable", self.base.get_arg("scheduled_discharge_enable", indirect=False, index=self.id), True) self.log("Inverter {} Turning on scheduled export".format(self.id)) - if ((new_end != old_end) or (new_start != old_start) or (force_export != old_discharge_enable)) and self.inv_time_button_press: - self.press_and_poll_button() + if (new_end != old_end) or (new_start != old_start) or (force_export != old_discharge_enable): + if self.inv_time_button_press: + self.press_and_poll_button() # Force export, turn it on after we change the window if force_export: @@ -2053,6 +2054,10 @@ def disable_charge_window(self, notify=True): # Reset charge window to midnight if there is no charge enable time if not self.inv_has_charge_enable_time: self.adjust_charge_window(self.base.midnight_utc, self.base.midnight_utc, self.base.minutes_now) + else: + # Press button if needed + if self.inv_time_button_press: + self.press_and_poll_button() # Updated cached status to disabled self.charge_enable_time = False @@ -2306,6 +2311,11 @@ def adjust_charge_window(self, charge_start_time, charge_end_time, minutes_now): old_charge_schedule_enable = self.base.get_arg("scheduled_charge_enable", "on", index=self.id) else: self.log("Warn: Inverter {} unable read charge window as neither REST or discharge_start_time".format(self.id)) + old_charge_schedule_enable = "off" + + # Normalize old charge schedule enable + if old_charge_schedule_enable in ["off", "disable"]: + old_charge_schedule_enable = "off" # Apply clock skew charge_start_time += timedelta(seconds=self.base.inverter_clock_skew_start * 60) @@ -2379,7 +2389,7 @@ def adjust_charge_window(self, charge_start_time, charge_end_time, minutes_now): self.base.call_notify("Predbat: Inverter {} Charge window change to: {} - {} at {}".format(self.id, new_start, new_end, self.base.time_now_str())) self.base.log("Inverter {} Updated start and end charge window to {} - {} (old {} - {})".format(self.id, new_start, new_end, old_start, old_end)) - if old_charge_schedule_enable == "off" or old_charge_schedule_enable == "disable" or have_disabled: + if old_charge_schedule_enable == "off" or have_disabled: # Enable scheduled charge if not turned on if self.rest_data: self.rest_enableChargeSchedule(True) @@ -2391,20 +2401,21 @@ def adjust_charge_window(self, charge_start_time, charge_end_time, minutes_now): else: self.log("Warn: Inverter {} unable write charge window enable as neither REST or scheduled_charge_enable are set".format(self.id)) - if new_start != old_start or new_end != old_end or (self.inv_charge_time_format in ["H M", "H:M-H:M"]): - # For Solis inverters and fox we also have to press the update_charge_discharge button to send the times to the inverter - if self.inv_time_button_press: - self.press_and_poll_button() - # Only notify if it's a real change and not a temporary one - if (old_charge_schedule_enable == "off" or old_charge_schedule_enable == "disable") and self.base.set_inverter_notify: + if old_charge_schedule_enable == "off" and self.base.set_inverter_notify: self.base.call_notify("Predbat: Inverter {} Enabling scheduled charging at {}".format(self.id, self.base.time_now_str())) self.charge_enable_time = True - if old_charge_schedule_enable == "off" or old_charge_schedule_enable == "disable": + if old_charge_schedule_enable == "off": self.base.log("Inverter {} Turning on scheduled charge".format(self.id)) + if (new_start != old_start) or (new_end != old_end) or (old_charge_schedule_enable == "off"): + # For Solis inverters and fox we also have to press the update_charge_discharge button to send the times to the inverter + if self.inv_time_button_press: + self.press_and_poll_button() + + def press_and_poll_button(self): """ Press charge/discharge update button(s) for the inverter. From e47a34c3e1d45869cd66b46a871f1baf9e330b53 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sun, 2 Nov 2025 16:17:35 +0000 Subject: [PATCH 4/9] [pre-commit.ci lite] apply automatic fixes --- apps/predbat/inverter.py | 1 - apps/predbat/unit_test.py | 14 +++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/predbat/inverter.py b/apps/predbat/inverter.py index 1bdbbec45..9015ee230 100644 --- a/apps/predbat/inverter.py +++ b/apps/predbat/inverter.py @@ -2415,7 +2415,6 @@ def adjust_charge_window(self, charge_start_time, charge_end_time, minutes_now): if self.inv_time_button_press: self.press_and_poll_button() - def press_and_poll_button(self): """ Press charge/discharge update button(s) for the inverter. diff --git a/apps/predbat/unit_test.py b/apps/predbat/unit_test.py index f304fe22a..db6f763fa 100644 --- a/apps/predbat/unit_test.py +++ b/apps/predbat/unit_test.py @@ -3570,7 +3570,7 @@ def run_execute_tests(my_predbat): charge_window_best5 = [{"start": my_predbat.minutes_now - 24 * 60, "end": my_predbat.minutes_now + 60, "average": 1}] charge_window_best6 = [{"start": my_predbat.minutes_now + 8 * 60, "end": my_predbat.minutes_now + 60 + 8 * 60, "average": 1}] charge_window_best7 = [{"start": my_predbat.minutes_now, "end": my_predbat.minutes_now + 23 * 60, "average": 1}] - charge_window_best7b = [{"start": 24*60-5, "end": my_predbat.minutes_now + 23 * 60, "average": 1}] + charge_window_best7b = [{"start": 24 * 60 - 5, "end": my_predbat.minutes_now + 23 * 60, "average": 1}] charge_window_best7c = [{"start": 0, "end": 11 * 60, "average": 1}] charge_window_best8 = [{"start": 0, "end": my_predbat.minutes_now + 12 * 60, "average": 1}] charge_window_best9 = [{"start": my_predbat.minutes_now + 60, "end": my_predbat.minutes_now + 90, "average": 1}] @@ -4326,7 +4326,7 @@ def run_execute_tests(my_predbat): assert_status="Charging", assert_charge_start_time_minutes=my_predbat.minutes_now, assert_charge_end_time_minutes=24 * 60 - 1, - inverter_charge_time_minutes_start = 30, + inverter_charge_time_minutes_start=30, ) failed |= run_execute_test( my_predbat, @@ -4337,10 +4337,10 @@ def run_execute_tests(my_predbat): set_charge_window=True, set_export_window=True, assert_status="Charging", - assert_charge_start_time_minutes=24*60-30, + assert_charge_start_time_minutes=24 * 60 - 30, assert_charge_end_time_minutes=24 * 60 - 1, - inverter_charge_time_minutes_start = 30, - minutes_now=24*60-5, + inverter_charge_time_minutes_start=30, + minutes_now=24 * 60 - 5, ) failed |= run_execute_test( my_predbat, @@ -4352,8 +4352,8 @@ def run_execute_tests(my_predbat): set_export_window=True, assert_status="Charging", assert_charge_start_time_minutes=0, - assert_charge_end_time_minutes=11*60, - inverter_charge_time_minutes_start=12*60, + assert_charge_end_time_minutes=11 * 60, + inverter_charge_time_minutes_start=12 * 60, minutes_now=0, ) From c2dec8ca3e89e531e8305f5202da4affb540c7f1 Mon Sep 17 00:00:00 2001 From: Trefor Southwell Date: Sun, 2 Nov 2025 16:18:35 +0000 Subject: [PATCH 5/9] Move button push inside check --- apps/predbat/inverter.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/predbat/inverter.py b/apps/predbat/inverter.py index 1bdbbec45..cc90ed95a 100644 --- a/apps/predbat/inverter.py +++ b/apps/predbat/inverter.py @@ -2051,13 +2051,13 @@ def disable_charge_window(self, notify=True): self.base.log("Inverter {} Turning off scheduled charge".format(self.id)) - # Reset charge window to midnight if there is no charge enable time - if not self.inv_has_charge_enable_time: - self.adjust_charge_window(self.base.midnight_utc, self.base.midnight_utc, self.base.minutes_now) - else: - # Press button if needed - if self.inv_time_button_press: - self.press_and_poll_button() + # Reset charge window to midnight if there is no charge enable time + if not self.inv_has_charge_enable_time: + self.adjust_charge_window(self.base.midnight_utc, self.base.midnight_utc, self.base.minutes_now) + else: + # Press button if needed + if self.inv_time_button_press: + self.press_and_poll_button() # Updated cached status to disabled self.charge_enable_time = False From 7c1b5b2199df361cbd80e546550f8bec1c43b38a Mon Sep 17 00:00:00 2001 From: Trefor Southwell Date: Sun, 2 Nov 2025 16:45:59 +0000 Subject: [PATCH 6/9] Unit tests --- apps/predbat/unit_test.py | 58 +++++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/apps/predbat/unit_test.py b/apps/predbat/unit_test.py index db6f763fa..5479bade9 100644 --- a/apps/predbat/unit_test.py +++ b/apps/predbat/unit_test.py @@ -527,7 +527,7 @@ def get_commands(self): return commands -def test_adjust_charge_window(test_name, ha, inv, dummy_rest, prev_charge_start_time, prev_charge_end_time, prev_enable_charge, charge_start_time, charge_end_time, minutes_now, short=False): +def test_adjust_charge_window(test_name, ha, inv, dummy_rest, prev_charge_start_time, prev_charge_end_time, prev_enable_charge, charge_start_time, charge_end_time, minutes_now, short=False, has_inverter_time_button_press=False, expect_inverter_time_button_press=False): """ test: inv.adjust_charge_window(self, charge_start_time, charge_end_time, minutes_now): @@ -536,9 +536,11 @@ def test_adjust_charge_window(test_name, ha, inv, dummy_rest, prev_charge_start_ print("Test: {}".format(test_name)) inv.rest_data = None + inv.has_inverter_time_button_press = has_inverter_time_button_press ha.dummy_items["select.charge_start_time"] = prev_charge_start_time[:5] if short else prev_charge_start_time ha.dummy_items["select.charge_end_time"] = prev_charge_end_time[:5] if short else prev_charge_end_time ha.dummy_items["switch.scheduled_charge_enable"] = "on" if prev_enable_charge else "off" + ha.dummy_items['switch.inverter_button'] = 'off' charge_start_time_tm = datetime.strptime(charge_start_time, "%H:%M:%S") charge_end_time_tm = datetime.strptime(charge_end_time, "%H:%M:%S") @@ -560,6 +562,8 @@ def test_adjust_charge_window(test_name, ha, inv, dummy_rest, prev_charge_start_ if ha.get_state("switch.scheduled_charge_enable") != "on": print("ERROR: Charge enable should be on got {}".format(ha.get_state("switch.scheduled_charge_enable"))) failed = True + if ha.get_state('switch.inverter_button') != ('on' if expect_inverter_time_button_press else 'off'): + print("ERROR: Inverter time button press should be {} got {}".format('on' if expect_inverter_time_button_press else 'off', ha.get_state('switch.inverter_button'))) # REST Mode inv.rest_api = "dummy" @@ -632,7 +636,7 @@ def test_adjust_reserve(test_name, ha, inv, dummy_rest, prev_reserve, reserve, e return failed -def test_adjust_force_export(test_name, ha, inv, dummy_rest, prev_start, prev_end, prev_force_export, prev_discharge_target, new_start, new_end, new_force_export): +def test_adjust_force_export(test_name, ha, inv, dummy_rest, prev_start, prev_end, prev_force_export, prev_discharge_target, new_start, new_end, new_force_export, has_inv_time_button_press=False, expect_inv_time_button_press=False): """ Test inv.adjust_reserve(self, reserve): @@ -657,6 +661,7 @@ def test_adjust_force_export(test_name, ha, inv, dummy_rest, prev_start, prev_en inv.inv_has_charge_enable_time = False inv.ge_inverter_mode = True inv.rest_v3 = True + inv.inv_time_button_press = has_inv_time_button_press if inv.ge_inverter_mode and not new_force_export: expect_start = prev_start @@ -672,6 +677,7 @@ def test_adjust_force_export(test_name, ha, inv, dummy_rest, prev_start, prev_en ha.dummy_items["sensor.predbat_GE_0_scheduled_discharge_enable"] = prev_force_export ha.dummy_items["number.discharge_target_soc"] = prev_discharge_target ha.dummy_items["select.inverter_mode"] = prev_mode + ha.dummy_items['switch.inverter_button'] = 'off' new_start_timestamp = datetime.strptime(new_start, "%H:%M:%S") new_end_timestamp = datetime.strptime(new_end, "%H:%M:%S") @@ -692,6 +698,9 @@ def test_adjust_force_export(test_name, ha, inv, dummy_rest, prev_start, prev_en if ha.get_state("select.inverter_mode") != new_mode: print("ERROR: Inverter mode should be {} got {}".format(new_mode, ha.get_state("select.inverter_mode"))) failed = True + if ha.get_state('switch.inverter_button') != ('on' if expect_inv_time_button_press else 'off'): + print("ERROR: Inverter button press should be {} got {}".format('on' if expect_inv_time_button_press else 'off', ha.get_state('switch.inverter_button'))) + failed = True print("Test: {} - REST".format(test_name)) # REST Mode @@ -1192,6 +1201,7 @@ def test_inverter_update( dummy_items["number.discharge_target_soc"] = 4 dummy_items["sensor.battery_capacity"] = expect_battery_capacity dummy_items["sensor.predbat_GE_0_scheduled_discharge_enable"] = "on" if expect_discharge_enable else "off" + dummy_items['switch.inverter_button'] = 'off' print("sensor.predbat_GE_0_scheduled_discharge_enable = {}".format(dummy_items["sensor.predbat_GE_0_scheduled_discharge_enable"])) if not has_discharge_enable_time: dummy_items["switch.scheduled_discharge_enable"] = "n/a" @@ -2060,11 +2070,13 @@ def run_inverter_tests(): "select.discharge_end_time": "04:44:00", "sensor.predbat_GE_0_scheduled_discharge_enable": "off", "number.discharge_target_soc": 4, + "switch.inverter_button": False, } my_predbat.ha_interface.dummy_items = dummy_items my_predbat.args["auto_restart"] = [{"service": "switch/turn_on", "entity_id": "switch.restart"}] my_predbat.args["givtcp_rest"] = None my_predbat.args["inverter_type"] = ["GE"] + my_predbat.args["schedule_write_button"] = "switch.inverter_button" for entity_id in dummy_items.keys(): arg_name = entity_id.split(".")[1] my_predbat.args[arg_name] = entity_id @@ -2252,19 +2264,28 @@ def run_inverter_tests(): inv.update_status(my_predbat.minutes_now) my_predbat.inv = inv - failed |= test_adjust_force_export("adjust_force_export1", ha, inv, dummy_rest, "00:00:00", "00:00:00", False, 4, "11:00:00", "11:30:00", False) + failed |= test_adjust_force_export("adjust_force_export1", ha, inv, dummy_rest, "00:00:00", "00:00:00", False, 4, "11:00:00", "11:30:00", False, has_inv_time_button_press=True, expect_inv_time_button_press=True) + if failed: + return failed + failed |= test_adjust_force_export("adjust_force_export2", ha, inv, dummy_rest, "00:00:00", "00:00:00", False, 4, "11:00:00", "11:30:00", True, has_inv_time_button_press=True, expect_inv_time_button_press=True) + if failed: + return failed + failed |= test_adjust_force_export("adjust_force_export3", ha, inv, dummy_rest, "00:00:00", "00:00:00", False, 10, "11:00:00", "11:30:00", True, has_inv_time_button_press=True, expect_inv_time_button_press=True) + if failed: + return failed + failed |= test_adjust_force_export("adjust_force_export4", ha, inv, dummy_rest, "00:11:00", "01:12:12", True, 10, "11:00:00", "11:30:00", True, has_inv_time_button_press=True, expect_inv_time_button_press=True) if failed: return failed - failed |= test_adjust_force_export("adjust_force_export2", ha, inv, dummy_rest, "00:00:00", "00:00:00", False, 4, "11:00:00", "11:30:00", True) + failed |= test_adjust_force_export("adjust_force_export5", ha, inv, dummy_rest, "00:11:00", "01:12:12", True, 4, "11:00:00", "11:30:00", False, has_inv_time_button_press=True, expect_inv_time_button_press=True) if failed: return failed - failed |= test_adjust_force_export("adjust_force_export3", ha, inv, dummy_rest, "00:00:00", "00:00:00", False, 10, "11:00:00", "11:30:00", True) + failed |= test_adjust_force_export("adjust_force_export6", ha, inv, dummy_rest, "11:00:00", "11:30:00", True, 4, "11:00:00", "11:30:00", True, has_inv_time_button_press=True, expect_inv_time_button_press=False) if failed: return failed - failed |= test_adjust_force_export("adjust_force_export4", ha, inv, dummy_rest, "00:11:00", "01:12:12", True, 10, "11:00:00", "11:30:00", True) + failed |= test_adjust_force_export("adjust_force_export7", ha, inv, dummy_rest, "11:00:00", "11:30:00", True, 4, "11:00:00", "11:30:00", False, has_inv_time_button_press=True, expect_inv_time_button_press=True) if failed: return failed - failed |= test_adjust_force_export("adjust_force_export5", ha, inv, dummy_rest, "00:11:00", "01:12:12", True, 4, "11:00:00", "11:30:00", False) + failed |= test_adjust_force_export("adjust_force_export8", ha, inv, dummy_rest, "11:00:00", "11:30:00", False, 4, "11:00:00", "11:30:00", True, has_inv_time_button_press=True, expect_inv_time_button_press=True) if failed: return failed @@ -2310,12 +2331,14 @@ def run_inverter_tests(): if failed: return failed - failed |= test_adjust_charge_window("adjust_charge_window1", ha, inv, dummy_rest, "00:00:00", "00:00:00", False, "00:00:00", "00:00:00", my_predbat.minutes_now) - failed |= test_adjust_charge_window("adjust_charge_window2", ha, inv, dummy_rest, "00:00:00", "00:00:00", False, "00:00:00", "23:00:00", my_predbat.minutes_now) - failed |= test_adjust_charge_window("adjust_charge_window3", ha, inv, dummy_rest, "00:00:00", "00:00:00", True, "00:00:00", "23:00:00", my_predbat.minutes_now) - failed |= test_adjust_charge_window("adjust_charge_window4", ha, inv, dummy_rest, "00:00:00", "00:00:00", False, "01:12:00", "23:12:00", my_predbat.minutes_now) - failed |= test_adjust_charge_window("adjust_charge_window5", ha, inv, dummy_rest, "00:00:00", "00:00:00", True, "01:12:00", "23:12:00", my_predbat.minutes_now) - failed |= test_adjust_charge_window("adjust_charge_window6", ha, inv, dummy_rest, "00:00:00", "00:00:00", True, "01:12:00", "23:12:00", my_predbat.minutes_now, short=True) + failed |= test_adjust_charge_window("adjust_charge_window1", ha, inv, dummy_rest, "00:00:00", "00:00:00", False, "00:00:00", "00:00:00", my_predbat.minutes_now, has_inverter_time_button_press=True, expect_inverter_time_button_press=False) + failed |= test_adjust_charge_window("adjust_charge_window2", ha, inv, dummy_rest, "00:00:00", "00:00:00", False, "00:00:00", "23:00:00", my_predbat.minutes_now, has_inverter_time_button_press=True, expect_inverter_time_button_press=True) + failed |= test_adjust_charge_window("adjust_charge_window3", ha, inv, dummy_rest, "00:00:00", "00:00:00", True, "00:00:00", "23:00:00", my_predbat.minutes_now, has_inverter_time_button_press=True, expect_inverter_time_button_press=True) + failed |= test_adjust_charge_window("adjust_charge_window4", ha, inv, dummy_rest, "00:00:00", "00:00:00", False, "01:12:00", "23:12:00", my_predbat.minutes_now, has_inverter_time_button_press=True, expect_inverter_time_button_press=True) + failed |= test_adjust_charge_window("adjust_charge_window5", ha, inv, dummy_rest, "00:00:00", "00:00:00", True, "01:12:00", "23:12:00", my_predbat.minutes_now, has_inverter_time_button_press=True, expect_inverter_time_button_press=True) + failed |= test_adjust_charge_window("adjust_charge_window6", ha, inv, dummy_rest, "00:00:00", "00:00:00", True, "01:12:00", "23:12:00", my_predbat.minutes_now, short=True, has_inverter_time_button_press=True, expect_inverter_time_button_press=True) + failed |= test_adjust_charge_window("adjust_charge_window7", ha, inv, dummy_rest, "00:11:00", "00:12:00", True, "00:11:00", "00:12:00", my_predbat.minutes_now, short=True, has_inverter_time_button_press=True, expect_inverter_time_button_press=False) + failed |= test_adjust_charge_window("adjust_charge_window7", ha, inv, dummy_rest, "00:11:00", "00:12:00", False, "00:11:00", "00:12:00", my_predbat.minutes_now, short=True, has_inverter_time_button_press=True, expect_inverter_time_button_press=True) if failed: return failed @@ -3092,6 +3115,7 @@ def run_execute_test( reserve_max=100, car_soc=0, battery_temperature=20, + assert_button_push=False, ): print("Run scenario {}".format(name)) my_predbat.log("Run scenario {}".format(name)) @@ -9816,6 +9840,10 @@ def main(): if not failed: failed |= run_model_tests(my_predbat) + if not failed: + failed |= run_inverter_tests() + if not failed: + failed |= run_execute_tests(my_predbat) if not failed: failed |= test_previous_days_modal_filter(my_predbat) @@ -9852,16 +9880,12 @@ def main(): # failed = 1 if not failed: failed |= test_alert_feed(my_predbat) - if not failed: - failed |= run_inverter_tests() if not failed: failed |= run_iboost_smart_tests(my_predbat) if not failed: failed |= run_car_charging_smart_tests(my_predbat) if not failed: failed |= run_intersect_window_tests(my_predbat) - if not failed: - failed |= run_execute_tests(my_predbat) if not failed: failed |= run_inverter_multi_tests(my_predbat) if not failed: From 0cb4499b1c9038ea405bec0676c98dfaefeaedb0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sun, 2 Nov 2025 16:47:20 +0000 Subject: [PATCH 7/9] [pre-commit.ci lite] apply automatic fixes --- apps/predbat/unit_test.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/predbat/unit_test.py b/apps/predbat/unit_test.py index 5479bade9..33a556822 100644 --- a/apps/predbat/unit_test.py +++ b/apps/predbat/unit_test.py @@ -527,7 +527,9 @@ def get_commands(self): return commands -def test_adjust_charge_window(test_name, ha, inv, dummy_rest, prev_charge_start_time, prev_charge_end_time, prev_enable_charge, charge_start_time, charge_end_time, minutes_now, short=False, has_inverter_time_button_press=False, expect_inverter_time_button_press=False): +def test_adjust_charge_window( + test_name, ha, inv, dummy_rest, prev_charge_start_time, prev_charge_end_time, prev_enable_charge, charge_start_time, charge_end_time, minutes_now, short=False, has_inverter_time_button_press=False, expect_inverter_time_button_press=False +): """ test: inv.adjust_charge_window(self, charge_start_time, charge_end_time, minutes_now): @@ -540,7 +542,7 @@ def test_adjust_charge_window(test_name, ha, inv, dummy_rest, prev_charge_start_ ha.dummy_items["select.charge_start_time"] = prev_charge_start_time[:5] if short else prev_charge_start_time ha.dummy_items["select.charge_end_time"] = prev_charge_end_time[:5] if short else prev_charge_end_time ha.dummy_items["switch.scheduled_charge_enable"] = "on" if prev_enable_charge else "off" - ha.dummy_items['switch.inverter_button'] = 'off' + ha.dummy_items["switch.inverter_button"] = "off" charge_start_time_tm = datetime.strptime(charge_start_time, "%H:%M:%S") charge_end_time_tm = datetime.strptime(charge_end_time, "%H:%M:%S") @@ -562,8 +564,8 @@ def test_adjust_charge_window(test_name, ha, inv, dummy_rest, prev_charge_start_ if ha.get_state("switch.scheduled_charge_enable") != "on": print("ERROR: Charge enable should be on got {}".format(ha.get_state("switch.scheduled_charge_enable"))) failed = True - if ha.get_state('switch.inverter_button') != ('on' if expect_inverter_time_button_press else 'off'): - print("ERROR: Inverter time button press should be {} got {}".format('on' if expect_inverter_time_button_press else 'off', ha.get_state('switch.inverter_button'))) + if ha.get_state("switch.inverter_button") != ("on" if expect_inverter_time_button_press else "off"): + print("ERROR: Inverter time button press should be {} got {}".format("on" if expect_inverter_time_button_press else "off", ha.get_state("switch.inverter_button"))) # REST Mode inv.rest_api = "dummy" @@ -677,7 +679,7 @@ def test_adjust_force_export(test_name, ha, inv, dummy_rest, prev_start, prev_en ha.dummy_items["sensor.predbat_GE_0_scheduled_discharge_enable"] = prev_force_export ha.dummy_items["number.discharge_target_soc"] = prev_discharge_target ha.dummy_items["select.inverter_mode"] = prev_mode - ha.dummy_items['switch.inverter_button'] = 'off' + ha.dummy_items["switch.inverter_button"] = "off" new_start_timestamp = datetime.strptime(new_start, "%H:%M:%S") new_end_timestamp = datetime.strptime(new_end, "%H:%M:%S") @@ -698,8 +700,8 @@ def test_adjust_force_export(test_name, ha, inv, dummy_rest, prev_start, prev_en if ha.get_state("select.inverter_mode") != new_mode: print("ERROR: Inverter mode should be {} got {}".format(new_mode, ha.get_state("select.inverter_mode"))) failed = True - if ha.get_state('switch.inverter_button') != ('on' if expect_inv_time_button_press else 'off'): - print("ERROR: Inverter button press should be {} got {}".format('on' if expect_inv_time_button_press else 'off', ha.get_state('switch.inverter_button'))) + if ha.get_state("switch.inverter_button") != ("on" if expect_inv_time_button_press else "off"): + print("ERROR: Inverter button press should be {} got {}".format("on" if expect_inv_time_button_press else "off", ha.get_state("switch.inverter_button"))) failed = True print("Test: {} - REST".format(test_name)) @@ -1201,7 +1203,7 @@ def test_inverter_update( dummy_items["number.discharge_target_soc"] = 4 dummy_items["sensor.battery_capacity"] = expect_battery_capacity dummy_items["sensor.predbat_GE_0_scheduled_discharge_enable"] = "on" if expect_discharge_enable else "off" - dummy_items['switch.inverter_button'] = 'off' + dummy_items["switch.inverter_button"] = "off" print("sensor.predbat_GE_0_scheduled_discharge_enable = {}".format(dummy_items["sensor.predbat_GE_0_scheduled_discharge_enable"])) if not has_discharge_enable_time: dummy_items["switch.scheduled_discharge_enable"] = "n/a" From 1cd252a9916c7decee5393dc0c0e56d8c02a98d0 Mon Sep 17 00:00:00 2001 From: Trefor Southwell Date: Sun, 2 Nov 2025 18:14:43 +0000 Subject: [PATCH 8/9] Fixes for charge disable --- apps/predbat/inverter.py | 4 +- apps/predbat/unit_test.py | 82 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/apps/predbat/inverter.py b/apps/predbat/inverter.py index 3993c2917..e05a53c77 100644 --- a/apps/predbat/inverter.py +++ b/apps/predbat/inverter.py @@ -2389,8 +2389,8 @@ def adjust_charge_window(self, charge_start_time, charge_end_time, minutes_now): self.base.call_notify("Predbat: Inverter {} Charge window change to: {} - {} at {}".format(self.id, new_start, new_end, self.base.time_now_str())) self.base.log("Inverter {} Updated start and end charge window to {} - {} (old {} - {})".format(self.id, new_start, new_end, old_start, old_end)) - if old_charge_schedule_enable == "off" or have_disabled: - # Enable scheduled charge if not turned on + if (old_charge_schedule_enable == "off" or have_disabled) and (new_start != new_end): + # Enable scheduled charge if not turned on, unless the start and end are the same (disabled) if self.rest_data: self.rest_enableChargeSchedule(True) elif "scheduled_charge_enable" in self.base.args: diff --git a/apps/predbat/unit_test.py b/apps/predbat/unit_test.py index 33a556822..6a1c9c670 100644 --- a/apps/predbat/unit_test.py +++ b/apps/predbat/unit_test.py @@ -527,6 +527,69 @@ def get_commands(self): return commands +def test_disable_charge_window( + test_name, ha, inv, dummy_rest, prev_charge_start_time, prev_charge_end_time, prev_enable_charge, minutes_now, has_charge_enable_time=True, has_inverter_time_button_press=False, expect_inverter_time_button_press=False +): + """ + Test the disable charge window function + """ + failed = False + print("Test: {} has_charge_enable_time={} has_inverter_time_button_press={} prev_enable_charge={}".format(test_name, has_charge_enable_time, has_inverter_time_button_press, prev_enable_charge)) + + inv.rest_data = None + inv.inv_time_button_press = has_inverter_time_button_press + inv.inv_has_charge_enable_time = has_charge_enable_time + ha.dummy_items["select.charge_start_time"] = prev_charge_start_time + ha.dummy_items["select.charge_end_time"] = prev_charge_end_time + ha.dummy_items["switch.scheduled_charge_enable"] = "on" if prev_enable_charge else "off" + ha.dummy_items["switch.inverter_button"] = "off" + + inv.disable_charge_window() + + if not has_charge_enable_time and ha.get_state("select.charge_start_time") != "00:00:00": + print("ERROR: Charge start time should be 00:00:00 got {}".format(ha.get_state("select.charge_start_time"))) + failed = True + if not has_charge_enable_time and ha.get_state("select.charge_end_time") != "00:00:00": + print("ERROR: Charge end time should be 00:00:00 got {}".format(ha.get_state("select.charge_end_time"))) + failed = True + if ha.get_state("switch.scheduled_charge_enable") != "off": + print("ERROR: Charge enable should be off got {}".format(ha.get_state("switch.scheduled_charge_enable"))) + failed = True + if ha.get_state("switch.inverter_button") != ("on" if expect_inverter_time_button_press else "off"): + print("ERROR: Inverter time button press should be {} got {}".format("on" if expect_inverter_time_button_press else "off", ha.get_state("switch.inverter_button"))) + + # REST Mode + inv.rest_api = "dummy" + inv.rest_data = {} + inv.rest_data["Timeslots"] = {} + inv.rest_data["Timeslots"]["Charge_start_time_slot_1"] = prev_charge_start_time + inv.rest_data["Timeslots"]["Charge_end_time_slot_1"] = prev_charge_end_time + inv.rest_data["Control"] = {} + inv.rest_data["Control"]["Enable_Charge_Schedule"] = "enable" if prev_enable_charge else "disabled" + dummy_rest.rest_data = copy.deepcopy(inv.rest_data) + dummy_rest.rest_data["Timeslots"]["Charge_start_time_slot_1"] = "00:00:00" + dummy_rest.rest_data["Timeslots"]["Charge_end_time_slot_1"] = "00:00:00" + dummy_rest.rest_data["Control"]["Enable_Charge_Schedule"] = "disabled" + + print("REST Mode") + inv.disable_charge_window() + print("After disable charge window") + rest_command = dummy_rest.get_commands() + charge_start_time = "00:00:00" + charge_end_time = "00:00:00" + if (prev_charge_start_time != charge_start_time or prev_charge_end_time != charge_end_time) and not has_charge_enable_time: + expect_data = [["dummy/setChargeSlot1", {"start": charge_start_time[0:5], "finish": charge_end_time[0:5]}]] + else: + expect_data = [] + if prev_enable_charge and has_charge_enable_time: + expect_data.append(["dummy/enableChargeSchedule", {"state": "disable"}]) + + if json.dumps(expect_data) != json.dumps(rest_command): + print("ERROR: Rest command should be {} got {}".format(expect_data, rest_command)) + failed = True + return failed + + def test_adjust_charge_window( test_name, ha, inv, dummy_rest, prev_charge_start_time, prev_charge_end_time, prev_enable_charge, charge_start_time, charge_end_time, minutes_now, short=False, has_inverter_time_button_press=False, expect_inverter_time_button_press=False ): @@ -574,11 +637,11 @@ def test_adjust_charge_window( inv.rest_data["Timeslots"]["Charge_start_time_slot_1"] = prev_charge_start_time inv.rest_data["Timeslots"]["Charge_end_time_slot_1"] = prev_charge_end_time inv.rest_data["Control"] = {} - inv.rest_data["Control"]["Enable_Charge_Schedule"] = "on" if prev_enable_charge else "off" + inv.rest_data["Control"]["Enable_Charge_Schedule"] = "enable" if prev_enable_charge else "disable" dummy_rest.rest_data = copy.deepcopy(inv.rest_data) dummy_rest.rest_data["Timeslots"]["Charge_start_time_slot_1"] = charge_start_time dummy_rest.rest_data["Timeslots"]["Charge_end_time_slot_1"] = charge_end_time - dummy_rest.rest_data["Control"]["Enable_Charge_Schedule"] = True + dummy_rest.rest_data["Control"]["Enable_Charge_Schedule"] = "enable" inv.adjust_charge_window(charge_start_time_tm, charge_end_time_tm, minutes_now) rest_command = dummy_rest.get_commands() @@ -2333,7 +2396,7 @@ def run_inverter_tests(): if failed: return failed - failed |= test_adjust_charge_window("adjust_charge_window1", ha, inv, dummy_rest, "00:00:00", "00:00:00", False, "00:00:00", "00:00:00", my_predbat.minutes_now, has_inverter_time_button_press=True, expect_inverter_time_button_press=False) + failed |= test_adjust_charge_window("adjust_charge_window1", ha, inv, dummy_rest, "00:00:00", "00:00:00", True, "00:00:00", "00:00:00", my_predbat.minutes_now, has_inverter_time_button_press=True, expect_inverter_time_button_press=False) failed |= test_adjust_charge_window("adjust_charge_window2", ha, inv, dummy_rest, "00:00:00", "00:00:00", False, "00:00:00", "23:00:00", my_predbat.minutes_now, has_inverter_time_button_press=True, expect_inverter_time_button_press=True) failed |= test_adjust_charge_window("adjust_charge_window3", ha, inv, dummy_rest, "00:00:00", "00:00:00", True, "00:00:00", "23:00:00", my_predbat.minutes_now, has_inverter_time_button_press=True, expect_inverter_time_button_press=True) failed |= test_adjust_charge_window("adjust_charge_window4", ha, inv, dummy_rest, "00:00:00", "00:00:00", False, "01:12:00", "23:12:00", my_predbat.minutes_now, has_inverter_time_button_press=True, expect_inverter_time_button_press=True) @@ -2343,6 +2406,19 @@ def run_inverter_tests(): failed |= test_adjust_charge_window("adjust_charge_window7", ha, inv, dummy_rest, "00:11:00", "00:12:00", False, "00:11:00", "00:12:00", my_predbat.minutes_now, short=True, has_inverter_time_button_press=True, expect_inverter_time_button_press=True) if failed: return failed + + failed |= test_disable_charge_window("disable_charge_window1", ha, inv, dummy_rest, "01:12:00", "23:12:00", True, my_predbat.minutes_now, has_charge_enable_time=True, has_inverter_time_button_press=True, expect_inverter_time_button_press=True) + if failed: + return failed + failed |= test_disable_charge_window("disable_charge_window2", ha, inv, dummy_rest, "01:12:00", "23:12:00", False, my_predbat.minutes_now, has_charge_enable_time=True, has_inverter_time_button_press=True, expect_inverter_time_button_press=False) + if failed: + return failed + failed |= test_disable_charge_window("disable_charge_window3", ha, inv, dummy_rest, "00:00:00", "00:00:00", True, my_predbat.minutes_now, has_charge_enable_time=True, has_inverter_time_button_press=True, expect_inverter_time_button_press=True) + if failed: + return failed + failed |= test_disable_charge_window("disable_charge_window4", ha, inv, dummy_rest, "00:00:00", "00:00:00", False, my_predbat.minutes_now, has_charge_enable_time=True, has_inverter_time_button_press=True, expect_inverter_time_button_press=False) + if failed: + return failed failed |= test_call_service_template("test_service_simple1", my_predbat, inv, service_name="test_service", domain="charge", data={"test": "data"}, extra_data={"extra": "data"}) failed |= test_call_service_template("test_service_simple2", my_predbat, inv, service_name="test_service", domain="charge", data={"test": "data"}, extra_data={"extra": "data"}, clear=False, repeat=True) From 689c86e6e2de0a6b41bdb2b9388749f3a3cd0516 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sun, 2 Nov 2025 18:16:18 +0000 Subject: [PATCH 9/9] [pre-commit.ci lite] apply automatic fixes --- apps/predbat/unit_test.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/predbat/unit_test.py b/apps/predbat/unit_test.py index 6a1c9c670..17c7559d1 100644 --- a/apps/predbat/unit_test.py +++ b/apps/predbat/unit_test.py @@ -527,9 +527,7 @@ def get_commands(self): return commands -def test_disable_charge_window( - test_name, ha, inv, dummy_rest, prev_charge_start_time, prev_charge_end_time, prev_enable_charge, minutes_now, has_charge_enable_time=True, has_inverter_time_button_press=False, expect_inverter_time_button_press=False -): +def test_disable_charge_window(test_name, ha, inv, dummy_rest, prev_charge_start_time, prev_charge_end_time, prev_enable_charge, minutes_now, has_charge_enable_time=True, has_inverter_time_button_press=False, expect_inverter_time_button_press=False): """ Test the disable charge window function """ @@ -2406,7 +2404,7 @@ def run_inverter_tests(): failed |= test_adjust_charge_window("adjust_charge_window7", ha, inv, dummy_rest, "00:11:00", "00:12:00", False, "00:11:00", "00:12:00", my_predbat.minutes_now, short=True, has_inverter_time_button_press=True, expect_inverter_time_button_press=True) if failed: return failed - + failed |= test_disable_charge_window("disable_charge_window1", ha, inv, dummy_rest, "01:12:00", "23:12:00", True, my_predbat.minutes_now, has_charge_enable_time=True, has_inverter_time_button_press=True, expect_inverter_time_button_press=True) if failed: return failed