diff --git a/apps/predbat/inverter.py b/apps/predbat/inverter.py index 5fcd15c9..942f99cd 100644 --- a/apps/predbat/inverter.py +++ b/apps/predbat/inverter.py @@ -1933,7 +1933,7 @@ def call_service_template(self, service, data, domain="charge", extra_data={}): else: # Record the last service called self.base.last_service_hash[hash_index] = this_service_hash - self.log("Inverter {} Calling service {} domain {} with data {}".format(self.id, service, domain, data)) + self.log("Inverter {} Calling service {} domain {} with data {} extra_data {}".format(self.id, service, domain, data, extra_data)) if not isinstance(service_list, list): service_list = [service_list] diff --git a/apps/predbat/unit_test.py b/apps/predbat/unit_test.py index 2b666637..b6997115 100644 --- a/apps/predbat/unit_test.py +++ b/apps/predbat/unit_test.py @@ -124,6 +124,7 @@ def get_history(self, entity_id, now=None, days=30): class TestInverter: def __init__(self): + self.id = 0 pass @@ -997,7 +998,6 @@ def call_service_template(self, service, data, domain="charge", extra_data={}) ha.service_store_enable = False return failed - def run_car_charging_smart_test(test_name, my_predbat, battery_size=10.0, limit=8.0, soc=0, rate=10.0, loss=1.0, max_price=99, smart=True, plan_time="00:00:00", expect_cost=0, expect_kwh=0): """ Run a car charging smart test @@ -1017,24 +1017,28 @@ def run_car_charging_smart_test(test_name, my_predbat, battery_size=10.0, limit= my_predbat.car_charging_plan_time = [plan_time] my_predbat.num_cars = 1 - slots = my_predbat.plan_car_charging(0, my_predbat.low_rates) + my_predbat.car_charging_slots[0] = my_predbat.plan_car_charging(0, my_predbat.low_rates) total_kwh = 0 total_cost = 0 - for slot in slots: + for slot in my_predbat.car_charging_slots[0]: total_kwh += slot["kwh"] total_cost += slot["cost"] if total_kwh != expect_kwh: print("ERROR: Car charging total kwh should be {} got {}".format(expect_kwh, total_kwh)) failed = True print(slots) + total_pd = my_predbat.car_charge_slot_kwh(my_predbat.minutes_now, my_predbat.minutes_now + my_predbat.forecast_minutes) + if total_pd != expect_kwh: + print("ERROR: Car charging total calculated with car_charge_slot_kwh should be {} got {}".format(expect_kwh, total_pd)) + failed = True + print(slots) if total_cost != expect_cost: print("ERROR: Car charging total cost should be {} got {}".format(expect_cost, total_cost)) failed = True print(slots) - + return failed - def run_car_charging_smart_tests(my_predbat): """ Test car charging smart @@ -1052,13 +1056,13 @@ def run_car_charging_smart_tests(my_predbat): failed |= run_car_charging_smart_test("smart2", my_predbat, battery_size=12.0, limit=10.0, soc=0, rate=10.0, loss=1.0, max_price=99, smart=False, expect_cost=150, expect_kwh=10) failed |= run_car_charging_smart_test("smart3", my_predbat, battery_size=12.0, limit=10.0, soc=2, rate=10.0, loss=1.0, max_price=99, smart=True, expect_cost=80, expect_kwh=8) failed |= run_car_charging_smart_test("smart4", my_predbat, battery_size=12.0, limit=10.0, soc=2, rate=10.0, loss=0.5, max_price=99, smart=True, expect_cost=160, expect_kwh=16) - failed |= run_car_charging_smart_test("smart5", my_predbat, battery_size=100.0, limit=100.0, soc=0, rate=1.0, loss=1, max_price=99, smart=True, expect_cost=12 * 15, expect_kwh=12, plan_time="00:00:00") - failed |= run_car_charging_smart_test("smart6", my_predbat, battery_size=100.0, limit=100.0, soc=0, rate=1.0, loss=1, max_price=99, smart=True, expect_cost=14 * 15, expect_kwh=14, plan_time="02:00:00") - failed |= run_car_charging_smart_test("smart7", my_predbat, battery_size=100.0, limit=100.0, soc=0, rate=1.0, loss=1, max_price=10, smart=True, expect_cost=7 * 10, expect_kwh=7, plan_time="02:00:00") + failed |= run_car_charging_smart_test("smart5", my_predbat, battery_size=100.0, limit=100.0, soc=0, rate=1.0, loss=1, max_price=99, smart=True, expect_cost=12*15, expect_kwh=12, plan_time="00:00:00") + failed |= run_car_charging_smart_test("smart6", my_predbat, battery_size=100.0, limit=100.0, soc=0, rate=1.0, loss=1, max_price=99, smart=True, expect_cost=14*15, expect_kwh=14, plan_time="02:00:00") + failed |= run_car_charging_smart_test("smart7", my_predbat, battery_size=100.0, limit=100.0, soc=0, rate=1.0, loss=1, max_price=10, smart=True, expect_cost=7*10, expect_kwh=7, plan_time="02:00:00") + failed |= run_car_charging_smart_test("smart8", my_predbat, battery_size=100.0, limit=100.0, soc=0, rate=1.0, loss=1, max_price=10, smart=False, expect_cost=7*10, expect_kwh=7, plan_time="02:00:00") return failed - def run_inverter_tests(): """ Test the inverter functions @@ -1187,6 +1191,8 @@ def run_inverter_tests(): failed |= test_call_service_template("test_service_simple4", my_predbat, inv, service_name="test_service", domain="charge", data={"test": "data"}, extra_data={"extra": "data"}, clear=False, repeat=True) failed |= test_call_service_template("test_service_simple5", my_predbat, inv, service_name="test_service", domain="charge", data={"test": "data"}, extra_data={"extra": "data2"}, clear=False, repeat=False) + failed |= test_call_service_template("test_service_simple6", my_predbat, inv, service_name="test_service", domain="charge", data={"test": "data"}, extra_data={"extra_dummy": "data2"}, clear=False, repeat=False) + failed |= test_call_service_template( "test_service_complex1", my_predbat, @@ -1202,7 +1208,23 @@ def run_inverter_tests(): failed |= test_call_service_template( "test_service_complex2", my_predbat, inv, service_name="complex_service", domain="charge", data={"test": "data"}, extra_data={"extra": "extra_data"}, service_template={"service": "funny", "dummy": "22", "extra": "{extra}"}, clear=False, repeat=True ) + + my_predbat.args["extra"] = "42" + failed |= test_call_service_template( + "test_service_complex3", + my_predbat, + inv, + service_name="complex_service", + domain="charge", + data={"test": "data"}, + extra_data={"extra": "extra_data"}, + service_template={"service": "funny", "dummy": "22", "extra": "{extra}"}, + expected_result=[["funny", {"dummy": "22", "extra": "extra_data"}]], + clear=True, + ) + inv.soc_percent = 49 + failed |= test_call_adjust_charge_immediate("charge_immediate1", my_predbat, ha, inv, dummy_items, 100, clear=True, stop_discharge=True) failed |= test_call_adjust_charge_immediate("charge_immediate2", my_predbat, ha, inv, dummy_items, 0) failed |= test_call_adjust_charge_immediate("charge_immediate3", my_predbat, ha, inv, dummy_items, 0, repeat=True) @@ -1534,6 +1556,7 @@ def adjust_charge_window(self, charge_start_time, charge_end_time, minutes_now): self.charge_start_time_minutes = (charge_start_time - self.midnight_utc).total_seconds() / 60 self.charge_end_time_minutes = (charge_end_time - self.midnight_utc).total_seconds() / 60 self.charge_time_enable = True + #print("Charge start_time {} charge_end_time {}".format(self.charge_start_time_minutes, self.charge_end_time_minutes)) def adjust_charge_immediate(self, target_soc, freeze=False): self.immediate_charge_soc_target = target_soc @@ -1551,6 +1574,7 @@ def adjust_force_export(self, force_export, new_start_time=None, new_end_time=No if new_end_time is not None: delta = new_end_time - self.midnight_utc self.discharge_end_time_minutes = delta.total_seconds() / 60 + #print("Force export {} start_time {} end_time {}".format(self.force_export, self.discharge_start_time_minutes, self.discharge_end_time_minutes)) def adjust_idle_time(self, charge_start=None, charge_end=None, discharge_start=None, discharge_end=None): self.idle_charge_start = charge_start @@ -1619,6 +1643,8 @@ def run_execute_test( has_charge_enable_time=True, inverter_hybrid=False, battery_max_rate=1000, + minutes_now = 12 * 60, + update_plan=False, ): print("Run scenario {}".format(name)) my_predbat.soc_kw = soc_kw @@ -1631,6 +1657,12 @@ def run_execute_test( my_predbat.inverter_hybrid = inverter_hybrid my_predbat.set_charge_low_power = set_charge_low_power my_predbat.charge_low_power_margin = charge_low_power_margin + my_predbat.minutes_now = minutes_now + + charge_window_best = charge_window_best.copy() + charge_limit_best = charge_limit_best.copy() + export_window_best = export_window_best.copy() + export_limits_best = export_limits_best.copy() if assert_immediate_soc_target is None: assert_immediate_soc_target = assert_soc_target @@ -1669,6 +1701,14 @@ def run_execute_test( my_predbat.set_discharge_during_charge = set_discharge_during_charge my_predbat.car_charging_from_battery = False + # Shift on plan? + if update_plan: + my_predbat.plan_last_updated = my_predbat.now_utc + my_predbat.args['threads'] = 0 + my_predbat.calculate_plan(recompute=False) + + print("charge_window_best {} charge_limit_best {} export_window_best {} export_limits_best {}".format(charge_window_best, charge_limit_best, export_window_best, export_limits_best)) + status, status_extra = my_predbat.execute_plan() for inverter in my_predbat.inverters: @@ -1731,6 +1771,7 @@ def run_execute_test( print("ERROR: Inverter {} Immediate export SOC freeze should be True got {}".format(inverter.id, inverter.immediate_discharge_soc_freeze)) failed = True + my_predbat.minutes_now = 12 * 60 return failed @@ -1754,9 +1795,9 @@ def run_single_debug(test_name, my_predbat, debug_file, expected_file=None): print("Combined export slots {} min_improvement_export {} set_export_freeze_only {}".format(my_predbat.combine_export_slots, my_predbat.metric_min_improvement_export, my_predbat.set_export_freeze_only)) if not expected_file: pass - # my_predbat.combine_export_slots = False + #my_predbat.combine_export_slots = False # my_predbat.best_soc_keep = 1.0 - # my_predbat.metric_min_improvement_export = 5 + #my_predbat.metric_min_improvement_export = 5 if re_do_rates: # Set rate thresholds @@ -1846,7 +1887,12 @@ def run_single_debug(test_name, my_predbat, debug_file, expected_file=None): print("Wrote plan to {}".format(filename)) # Expected - actual_data = {"charge_limit_best": my_predbat.charge_limit_best, "charge_window_best": my_predbat.charge_window_best, "export_window_best": my_predbat.export_window_best, "export_limits_best": my_predbat.export_limits_best} + actual_data = { + "charge_limit_best": my_predbat.charge_limit_best, + "charge_window_best": my_predbat.charge_window_best, + "export_window_best": my_predbat.export_window_best, + "export_limits_best": my_predbat.export_limits_best + } actual_json = json.dumps(actual_data) if expected_file: print("Compare with {}".format(expected_file)) @@ -1865,7 +1911,6 @@ def run_single_debug(test_name, my_predbat, debug_file, expected_file=None): print("Wrote plan json to {}".format(filename)) return failed - def run_execute_tests(my_predbat): print("**** Running execute tests ****\n") reset_inverter(my_predbat) @@ -1880,6 +1925,7 @@ def run_execute_tests(my_predbat): 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}] + charge_limit_best0 = [10] charge_limit_best = [10, 10] charge_limit_best2 = [5] charge_limit_best_frz = [1] @@ -1905,6 +1951,7 @@ def run_execute_tests(my_predbat): failed |= run_execute_test(my_predbat, "no_charge", charge_window_best=charge_window_best, charge_limit_best=charge_limit_best) failed |= run_execute_test(my_predbat, "no_charge2", set_charge_window=True, set_export_window=True, set_discharge_during_charge=False) + failed |= run_execute_test(my_predbat, "no_charge3", set_charge_window=True, set_export_window=True, set_discharge_during_charge=False, has_timed_pause=False) failed |= run_execute_test(my_predbat, "no_charge_future", set_charge_window=True, set_export_window=True, charge_window_best=charge_window_best4, charge_limit_best=charge_limit_best) failed |= run_execute_test(my_predbat, "no_charge_future_hybrid", set_charge_window=True, set_export_window=True, charge_window_best=charge_window_best4, charge_limit_best=charge_limit_best, inverter_hybrid=True) failed |= run_execute_test( @@ -2295,7 +2342,7 @@ def run_execute_tests(my_predbat): ) failed |= run_execute_test( my_predbat, - "charge23", + "charge2d", charge_window_best=charge_window_best, charge_limit_best=charge_limit_best2, assert_charge_time_enable=True, @@ -2307,6 +2354,26 @@ def run_execute_tests(my_predbat): assert_charge_end_time_minutes=my_predbat.minutes_now + 60, assert_soc_target=50, ) + failed |= run_execute_test( + my_predbat, + "charge2e", + charge_window_best=charge_window_best, + charge_limit_best=charge_limit_best2, + set_charge_window=True, + set_export_window=True, + assert_status="Charging", + soc_kw=0, + assert_charge_start_time_minutes=-1, + assert_charge_end_time_minutes=my_predbat.minutes_now + 60, + assert_discharge_rate=0, + assert_pause_discharge=False, + assert_reserve=0, + assert_immediate_soc_target=50, + assert_charge_time_enable=True, + assert_soc_target=50, + has_timed_pause=False, + set_discharge_during_charge=False, + ) failed |= run_execute_test( my_predbat, "charge3", @@ -2879,19 +2946,42 @@ def run_execute_tests(my_predbat): failed |= run_execute_test( my_predbat, - "discharge_charge", + "discharge_charge1", export_window_best=export_window_best, export_limits_best=export_limits_best, - charge_limit_best=charge_limit_best, + charge_limit_best=charge_limit_best0, charge_window_best=charge_window_best9, assert_force_export=True, set_charge_window=True, set_export_window=True, - soc_kw=10, + soc_kw=9, assert_status="Exporting", assert_immediate_soc_target=0, assert_discharge_start_time_minutes=my_predbat.minutes_now, assert_discharge_end_time_minutes=my_predbat.minutes_now + 60 + 1, + minutes_now = 775, + ) + if failed: + return failed + + failed |= run_execute_test( + my_predbat, + "discharge_charge2", + export_window_best=export_window_best, + export_limits_best=export_limits_best, + charge_limit_best=charge_limit_best0, + charge_window_best=charge_window_best9, + assert_force_export=False, + set_charge_window=True, + set_export_window=True, + soc_kw=9, + assert_status="Charging", + assert_immediate_soc_target=100, + assert_charge_start_time_minutes=-1, + assert_charge_end_time_minutes=my_predbat.minutes_now + 90, + assert_charge_time_enable=True, + minutes_now = 780, + update_plan=True, ) if failed: return failed diff --git a/apps/predbat/userinterface.py b/apps/predbat/userinterface.py index 8a8786a5..ba45ccdb 100644 --- a/apps/predbat/userinterface.py +++ b/apps/predbat/userinterface.py @@ -98,7 +98,11 @@ def resolve_arg(self, arg, value, default=None, indirect=True, combine=False, at if isinstance(value, str) and "{" in value: try: if extra_args: - value = value.format(**self.args, **extra_args) + # Remove duplicates or format will fail + arg_hash = {} + arg_hash.update(self.args) + arg_hash.update(extra_args) + value = value.format(**arg_hash) else: value = value.format(**self.args) except KeyError: