From b518c3b8d27caae87839fac946b7e4cf34a9c17a Mon Sep 17 00:00:00 2001 From: Trefor Southwell <48591903+springfall2008@users.noreply.github.com> Date: Sat, 1 Feb 2025 20:00:56 +0000 Subject: [PATCH] Day of week filter (#1945) * Day of week filter https://github.com/springfall2008/batpred/issues/1926 * [pre-commit.ci lite] apply automatic fixes * Updated * [pre-commit.ci lite] apply automatic fixes * Update energy-rates.md * [pre-commit.ci lite] apply automatic fixes * Update energy-rates.md --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- apps/predbat/fetch.py | 45 ++++++++++++++++++++-------- apps/predbat/unit_test.py | 63 ++++++++++++++++++++++++++++++++++++--- docs/energy-rates.md | 13 ++++++++ 3 files changed, 105 insertions(+), 16 deletions(-) diff --git a/apps/predbat/fetch.py b/apps/predbat/fetch.py index 9f009a9d..be379847 100644 --- a/apps/predbat/fetch.py +++ b/apps/predbat/fetch.py @@ -1340,7 +1340,7 @@ def basic_rates(self, info, rtype, prev=None, rate_replicate={}): rates = prev.copy() else: # Set to zero - for minute in range(24 * 60): + for minute in range(48 * 60): rates[minute] = 0 manual_items = self.get_manual_api(rtype) @@ -1385,6 +1385,23 @@ def basic_rates(self, info, rtype, prev=None, rate_replicate={}): self.log("Warn: Bad date {} provided in energy rates".format(this_rate["date"])) self.record_status("Bad date {} provided in energy rates".format(this_rate["date"]), had_errors=True) continue + day_of_week = [] + if "day_of_week" in this_rate: + day = str(this_rate["day_of_week"]) + days = day.split(",") + for day in days: + try: + day = int(day) + except (ValueError, TypeError): + self.log("Warn: Bad day_of_week {} provided in energy rates, should be 0-6".format(day_of_week)) + self.record_status("Bad day_of_week {} provided in energy rates, should be 0-6".format(day_of_week), had_errors=True) + continue + if day < 1 or day > 7: + self.log("Warn: Bad day_of_week {} provided in energy rates, should be 0-6".format(day)) + self.record_status("Bad day_of_week {} provided in energy rates, should be 0-6".format(day), had_errors=True) + continue + # Store as Python day of week + day_of_week.append(day - 1) # Support increment to existing rates (for override) if "rate" in this_rate: @@ -1433,7 +1450,9 @@ def basic_rates(self, info, rtype, prev=None, rate_replicate={}): start_minutes += delta_minutes end_minutes += delta_minutes - self.log("Adding rate {}: {} => {} to {} @ {} date {} increment {}".format(rtype, this_rate, self.time_abs_str(start_minutes), self.time_abs_str(end_minutes), rate, date, rate_increment)) + self.log("Adding rate {}: {} => {} to {} @ {} date {} day_of_week {} increment {}".format(rtype, this_rate, self.time_abs_str(start_minutes), self.time_abs_str(end_minutes), rate, date, day_of_week, rate_increment)) + + day_of_week_midnight = self.midnight.weekday() # Store rates against range if end_minutes >= (-24 * 60) and start_minutes < max_minute: @@ -1443,16 +1462,18 @@ def basic_rates(self, info, rtype, prev=None, rate_replicate={}): minute_index = minute_mod # For incremental adjustments we have to loop over 24-hour periods while minute_index < max_minute: - if rate_increment: - rates[minute_index] = rates.get(minute_index, 0.0) + rate - rate_replicate[minute_index] = "increment" - else: - rates[minute_index] = rate - rate_replicate[minute_index] = "user" - if load_scaling is not None: - self.load_scaling_dynamic[minute_index] = load_scaling - if date or not prev: - break + current_day_of_week = (day_of_week_midnight + int(minute_index / (24 * 60))) % 7 + if not day_of_week or (current_day_of_week in day_of_week): + if rate_increment: + rates[minute_index] = rates.get(minute_index, 0.0) + rate + rate_replicate[minute_index] = "increment" + else: + rates[minute_index] = rate + rate_replicate[minute_index] = "user" + if load_scaling is not None: + self.load_scaling_dynamic[minute_index] = load_scaling + if date: + break minute_index += 24 * 60 if not date and not prev: rates[minute_mod + max_minute] = rate diff --git a/apps/predbat/unit_test.py b/apps/predbat/unit_test.py index 347e411b..ab15ba42 100644 --- a/apps/predbat/unit_test.py +++ b/apps/predbat/unit_test.py @@ -390,12 +390,14 @@ def run_nordpool_test(my_predbat): rate_import2, rate_export2 = future.futurerate_analysis(rates_agile, rates_agile_export) for key in rate_import: if rate_import[key] != rate_import2.get(key, None): - print("ERROR: Rate import data not the same") + print("ERROR: Rate import data not the same got {} vs {}".format(rate_import[key], rate_import2.get(key, None))) failed = True + break for key in rate_export: if rate_export[key] != rate_export2.get(key, None): - print("ERROR: Rate export data not the same") + print("ERROR: Rate export data not the same got {} vs {}".format(rate_export[key], rate_export2.get(key, None))) failed = True + break # Compute the minimum value in the hash, ignoring the keys min_import = min(rate_import.values()) @@ -1423,6 +1425,57 @@ def run_load_octopus_slot_test(testname, my_predbat, slots, expected_slots, cons return failed +def assert_rates(rates, start_minute, end_minute, expect_rate): + """ + Assert rates + """ + end_minute = min(end_minute, len(rates)) + for minute in range(start_minute, end_minute): + if rates[minute] != expect_rate: + print("ERROR: Rate at minute {} should be {} got {}".format(minute, expect_rate, rates[minute])) + return 1 + return 0 + + +def test_basic_rates(my_predbat): + """ + Test for basic rates function + + rates = basic_rates(self, info, rtype, prev=None, rate_replicate={}): + """ + failed = 0 + + old_midnight = my_predbat.midnight + my_predbat.midnight = datetime.strptime("2025-07-05T00:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + + print("Simple rate1") + simple_rate = [ + {"rate": 5}, + { + "rate": 10, + "start": "17:00:00", + "end": "19:00:00", + }, + ] + results = my_predbat.basic_rates(simple_rate, "import") + results, results_replicated = my_predbat.rate_replicate(results, is_import=True, is_gas=False) + + failed |= assert_rates(results, 0, 17 * 60, 5) + failed |= assert_rates(results, 17 * 60, 19 * 60, 10) + failed |= assert_rates(results, 19 * 60, 24 * 60 + 17 * 60, 5) + failed |= assert_rates(results, 24 * 60 + 17 * 60, 24 * 60 + 19 * 60, 10) + + simple_rate = [{"rate": 5}, {"rate": 10, "start": "17:00:00", "end": "19:00:00", "day_of_week": "7"}] + results = my_predbat.basic_rates(simple_rate, "import") + results, results_replicated = my_predbat.rate_replicate(results, is_import=True, is_gas=False) + + failed |= assert_rates(results, 0, 17 * 60 + 24 * 60, 5) + failed |= assert_rates(results, 24 * 60 + 17 * 60, 24 * 60 + 19 * 60, 10) + + my_predbat.midnight = old_midnight + return failed + + def run_load_octopus_slots_tests(my_predbat): """ Test for load octopus slots @@ -7769,8 +7822,12 @@ def main(): run_single_debug(args.debug_file, my_predbat, args.debug_file) sys.exit(0) + if not failed: + failed |= run_nordpool_test(my_predbat) if not failed: failed |= run_load_octopus_slots_tests(my_predbat) + if not failed: + failed |= test_basic_rates(my_predbat) if not failed: failed |= test_find_charge_rate(my_predbat) if not failed: @@ -7784,8 +7841,6 @@ def main(): failed = 1 if not failed: failed |= test_alert_feed(my_predbat) - if not failed: - failed |= run_nordpool_test(my_predbat) if not failed: failed |= run_inverter_tests() if not failed: diff --git a/docs/energy-rates.md b/docs/energy-rates.md index 54fca095..852896e0 100644 --- a/docs/energy-rates.md +++ b/docs/energy-rates.md @@ -217,6 +217,19 @@ rates_gas: **start** and **end** are in the time format of "HH:MM:SS" e.g. "12:30:00" and should be aligned to 30 minute slots normally. **rate** is in pence e.g. 4.2 +**day_of_week** Can also be used to control rates on specific days. You can specify one day or multiple days split by comma. +Note: Day 1 = Monday, 2 = Tuesday .... 7 = Sunday + +e.g: + +```yaml +rates_import: + - rate: 15 + day_of_week: "1,2,3,4,5" + - rate: 10 + day_of_week: "6,7" +``` + start and end can be omitted and Predbat will assume that you are on a single flat rate tariff. If there are any gaps in the 24-hour period then a zero rate will be assumed.