diff --git a/apps/predbat/inverter.py b/apps/predbat/inverter.py index 478f1ecc4..67aa9d8c2 100644 --- a/apps/predbat/inverter.py +++ b/apps/predbat/inverter.py @@ -1771,6 +1771,10 @@ def write_and_poll_option(self, name, entity_id, new_value, ignore_fail=False): old_value = self.base.get_state_wrapper(entity_id, refresh=True) + # Ensure new_value is a string for string operations (e.g. when an integer hour/minute is passed for a time entity) + if not isinstance(new_value, str): + new_value = str(new_value) + # If time format of the selector is %H:%M and we pass in %H:%M:%S then we need to strip the seconds if old_value and (":" in old_value) and (":" in new_value) and (len(old_value) == 5) and (len(new_value) == 8): new_value = new_value[:5] @@ -2147,8 +2151,17 @@ def adjust_force_export(self, force_export, new_start_time=None, new_end_time=No if self.inv_charge_time_format == "H M": # If the inverter uses hours and minutes then write to these entities too - self.write_and_poll_option("discharge_start_hour", self.base.get_arg("discharge_start_hour", indirect=False, index=self.id), int(new_start[:2])) - self.write_and_poll_option("discharge_start_minute", self.base.get_arg("discharge_start_minute", indirect=False, index=self.id), int(new_start[3:5])) + # If the entity is a time entity (e.g. for FB00 firmware), write the full time string instead of just the integer component + start_hour_id = self.base.get_arg("discharge_start_hour", indirect=False, index=self.id) + if start_hour_id and isinstance(start_hour_id, str) and start_hour_id.startswith("time."): + self.write_and_poll_option("discharge_start_hour", start_hour_id, new_start) + else: + self.write_and_poll_option("discharge_start_hour", start_hour_id, int(new_start[:2])) + start_minute_id = self.base.get_arg("discharge_start_minute", indirect=False, index=self.id) + if start_minute_id and isinstance(start_minute_id, str) and start_minute_id.startswith("time."): + self.write_and_poll_option("discharge_start_minute", start_minute_id, new_start) + else: + self.write_and_poll_option("discharge_start_minute", start_minute_id, int(new_start[3:5])) elif self.inv_charge_time_format == "H:M-H:M": # If the inverter uses hours and minutes then write to these entities too discharge_time = new_start + "-" + new_end @@ -2169,8 +2182,17 @@ def adjust_force_export(self, force_export, new_start_time=None, new_end_time=No # If the inverter uses hours and minutes then write to these entities too if self.inv_charge_time_format == "H M": - self.write_and_poll_option("discharge_end_hour", self.base.get_arg("discharge_end_hour", indirect=False, index=self.id), int(new_end[:2])) - self.write_and_poll_option("discharge_end_minute", self.base.get_arg("discharge_end_minute", indirect=False, index=self.id), int(new_end[3:5])) + # If the entity is a time entity (e.g. for FB00 firmware), write the full time string instead of just the integer component + end_hour_id = self.base.get_arg("discharge_end_hour", indirect=False, index=self.id) + if end_hour_id and isinstance(end_hour_id, str) and end_hour_id.startswith("time."): + self.write_and_poll_option("discharge_end_hour", end_hour_id, new_end) + else: + self.write_and_poll_option("discharge_end_hour", end_hour_id, int(new_end[:2])) + end_minute_id = self.base.get_arg("discharge_end_minute", indirect=False, index=self.id) + if end_minute_id and isinstance(end_minute_id, str) and end_minute_id.startswith("time."): + self.write_and_poll_option("discharge_end_minute", end_minute_id, new_end) + else: + self.write_and_poll_option("discharge_end_minute", end_minute_id, int(new_end[3:5])) elif self.inv_charge_time_format == "H:M-H:M": pass else: @@ -2587,8 +2609,17 @@ def adjust_charge_window(self, charge_start_time, charge_end_time, minutes_now): if self.inv_charge_time_format == "H M": # If the inverter uses hours and minutes then write to these entities too - self.write_and_poll_option("charge_start_hour", self.base.get_arg("charge_start_hour", indirect=False, index=self.id), int(new_start[:2])) - self.write_and_poll_option("charge_start_minute", self.base.get_arg("charge_start_minute", indirect=False, index=self.id), int(new_start[3:5])) + # If the entity is a time entity (e.g. for FB00 firmware), write the full time string instead of just the integer component + start_hour_id = self.base.get_arg("charge_start_hour", indirect=False, index=self.id) + if start_hour_id and isinstance(start_hour_id, str) and start_hour_id.startswith("time."): + self.write_and_poll_option("charge_start_hour", start_hour_id, new_start) + else: + self.write_and_poll_option("charge_start_hour", start_hour_id, int(new_start[:2])) + start_minute_id = self.base.get_arg("charge_start_minute", indirect=False, index=self.id) + if start_minute_id and isinstance(start_minute_id, str) and start_minute_id.startswith("time."): + self.write_and_poll_option("charge_start_minute", start_minute_id, new_start) + else: + self.write_and_poll_option("charge_start_minute", start_minute_id, int(new_start[3:5])) elif self.inv_charge_time_format == "H:M-H:M": # If the inverter uses hours and minutes then write to these entities too charge_time = new_start + "-" + new_end @@ -2606,8 +2637,17 @@ def adjust_charge_window(self, charge_start_time, charge_end_time, minutes_now): self.write_and_poll_option("charge_end_time", entity_id_end, new_end) if self.inv_charge_time_format == "H M": - self.write_and_poll_option("charge_end_hour", self.base.get_arg("charge_end_hour", indirect=False, index=self.id), int(new_end[:2])) - self.write_and_poll_option("charge_end_minute", self.base.get_arg("charge_end_minute", indirect=False, index=self.id), int(new_end[3:5])) + # If the entity is a time entity (e.g. for FB00 firmware), write the full time string instead of just the integer component + end_hour_id = self.base.get_arg("charge_end_hour", indirect=False, index=self.id) + if end_hour_id and isinstance(end_hour_id, str) and end_hour_id.startswith("time."): + self.write_and_poll_option("charge_end_hour", end_hour_id, new_end) + else: + self.write_and_poll_option("charge_end_hour", end_hour_id, int(new_end[:2])) + end_minute_id = self.base.get_arg("charge_end_minute", indirect=False, index=self.id) + if end_minute_id and isinstance(end_minute_id, str) and end_minute_id.startswith("time."): + self.write_and_poll_option("charge_end_minute", end_minute_id, new_end) + else: + self.write_and_poll_option("charge_end_minute", end_minute_id, int(new_end[3:5])) elif self.inv_charge_time_format == "H:M-H:M": pass else: diff --git a/apps/predbat/tests/test_infra.py b/apps/predbat/tests/test_infra.py index ee43d11f6..fd0773735 100644 --- a/apps/predbat/tests/test_infra.py +++ b/apps/predbat/tests/test_infra.py @@ -203,6 +203,12 @@ def call_service(self, service, **kwargs): print("Warn: Service for entity {} not a select".format(entity_id)) elif entity_id in self.dummy_items: self.dummy_items[entity_id] = kwargs.get("option", None) + elif service == "time/set_value": + entity_id = kwargs.get("entity_id", None) + if not entity_id.startswith("time."): + print("Warn: Service for entity {} not a time".format(entity_id)) + elif entity_id in self.dummy_items: + self.dummy_items[entity_id] = kwargs.get("time", None) return None def set_state(self, entity_id, state, attributes=None): diff --git a/apps/predbat/tests/test_inverter.py b/apps/predbat/tests/test_inverter.py index cbd36a65c..a1c04f21a 100644 --- a/apps/predbat/tests/test_inverter.py +++ b/apps/predbat/tests/test_inverter.py @@ -1393,6 +1393,94 @@ def test_discharge_window_none_value(test_name, my_predbat, dummy_items): return failed +def test_time_entity_hour_write(test_name, ha, inv, dummy_rest, direction, new_start, new_end): + """ + Test that when *_start_hour / *_end_hour args resolve to time.* entities the full + time string is written (not just the integer hour component) and that no TypeError + is raised. + + direction: "discharge" or "charge" + """ + failed = False + print("Test: {} direction={}".format(test_name, direction)) + + inv.rest_data = None + inv.inv_charge_time_format = "H M" + + new_start_ts = datetime.strptime(new_start, "%H:%M:%S") + new_end_ts = datetime.strptime(new_end, "%H:%M:%S") + + if direction == "discharge": + # Wire up time entities for discharge slot + ha.dummy_items["select.discharge_start_time"] = "00:00:00" + ha.dummy_items["select.discharge_end_time"] = "00:00:00" + ha.dummy_items["time.discharge_start_hour"] = "00:00:00" + ha.dummy_items["time.discharge_end_hour"] = "00:00:00" + ha.dummy_items["sensor.predbat_GE_0_scheduled_discharge_enable"] = "off" + ha.dummy_items["number.discharge_target_soc"] = inv.reserve_percent + ha.dummy_items["select.inverter_mode"] = "Eco" + ha.dummy_items["switch.inverter_button"] = "off" + + inv.base.args["discharge_start_time"] = "select.discharge_start_time" + inv.base.args["discharge_end_time"] = "select.discharge_end_time" + inv.base.args["discharge_start_hour"] = "time.discharge_start_hour" + inv.base.args["discharge_end_hour"] = "time.discharge_end_hour" + # No discharge_start_minute / discharge_end_minute – those args are absent + + try: + inv.adjust_force_export(True, new_start_ts, new_end_ts) + except TypeError as e: + print("ERROR: TypeError raised: {}".format(e)) + return True + + if ha.dummy_items.get("time.discharge_start_hour") != new_start: + print("ERROR: discharge_start_hour time entity should be {} got {}".format(new_start, ha.dummy_items.get("time.discharge_start_hour"))) + failed = True + if ha.dummy_items.get("time.discharge_end_hour") != new_end: + print("ERROR: discharge_end_hour time entity should be {} got {}".format(new_end, ha.dummy_items.get("time.discharge_end_hour"))) + failed = True + if ha.dummy_items.get("select.discharge_start_time") != new_start: + print("ERROR: discharge_start_time select entity should be {} got {}".format(new_start, ha.dummy_items.get("select.discharge_start_time"))) + failed = True + if ha.dummy_items.get("select.discharge_end_time") != new_end: + print("ERROR: discharge_end_time select entity should be {} got {}".format(new_end, ha.dummy_items.get("select.discharge_end_time"))) + failed = True + + else: # charge + ha.dummy_items["select.charge_start_time"] = "00:00:00" + ha.dummy_items["select.charge_end_time"] = "00:00:00" + ha.dummy_items["time.charge_start_hour"] = "00:00:00" + ha.dummy_items["time.charge_end_hour"] = "00:00:00" + ha.dummy_items["switch.scheduled_charge_enable"] = "off" + ha.dummy_items["switch.inverter_button"] = "off" + + inv.base.args["charge_start_time"] = "select.charge_start_time" + inv.base.args["charge_end_time"] = "select.charge_end_time" + inv.base.args["charge_start_hour"] = "time.charge_start_hour" + inv.base.args["charge_end_hour"] = "time.charge_end_hour" + + try: + inv.adjust_charge_window(new_start_ts, new_end_ts, inv.base.minutes_now) + except TypeError as e: + print("ERROR: TypeError raised: {}".format(e)) + return True + + if ha.dummy_items.get("time.charge_start_hour") != new_start: + print("ERROR: charge_start_hour time entity should be {} got {}".format(new_start, ha.dummy_items.get("time.charge_start_hour"))) + failed = True + if ha.dummy_items.get("time.charge_end_hour") != new_end: + print("ERROR: charge_end_hour time entity should be {} got {}".format(new_end, ha.dummy_items.get("time.charge_end_hour"))) + failed = True + if ha.dummy_items.get("select.charge_start_time") != new_start: + print("ERROR: charge_start_time select entity should be {} got {}".format(new_start, ha.dummy_items.get("select.charge_start_time"))) + failed = True + if ha.dummy_items.get("select.charge_end_time") != new_end: + print("ERROR: charge_end_time select entity should be {} got {}".format(new_end, ha.dummy_items.get("select.charge_end_time"))) + failed = True + + return failed + + def run_inverter_tests(my_predbat_dummy): """ Test the inverter functions @@ -2084,5 +2172,19 @@ def run_inverter_tests(my_predbat_dummy): if failed: return failed + # Tests for time.* entities used for discharge/charge hour config (GS_fb00 firmware) + failed |= test_time_entity_hour_write("time_entity_discharge_hour1", ha, inv, dummy_rest, "discharge", "22:30:00", "23:59:00") + if failed: + return failed + failed |= test_time_entity_hour_write("time_entity_discharge_hour2", ha, inv, dummy_rest, "discharge", "00:00:00", "06:30:00") + if failed: + return failed + failed |= test_time_entity_hour_write("time_entity_charge_hour1", ha, inv, dummy_rest, "charge", "01:00:00", "05:30:00") + if failed: + return failed + failed |= test_time_entity_hour_write("time_entity_charge_hour2", ha, inv, dummy_rest, "charge", "23:00:00", "23:59:00") + if failed: + return failed + failed |= test_inverter_self_test("self_test1", my_predbat) return failed