diff --git a/apps/predbat/fox.py b/apps/predbat/fox.py index 108c5d928..9f6b624c9 100644 --- a/apps/predbat/fox.py +++ b/apps/predbat/fox.py @@ -16,6 +16,7 @@ import asyncio from datetime import datetime, timedelta, timezone +import os import time import hashlib from predbat_metrics import record_api_call @@ -134,13 +135,21 @@ def sort_schedule_by_start_time(schedule): return schedule -def validate_schedule(new_schedule, reserve, fdPwr_max): +def pad_schedule(schedule, target_count, reserve, fdPwr_max): + """Pad schedule to target_count with disabled SelfUse zero-time entries.""" + disabled_entry = {"enable": 0, "startHour": 0, "startMinute": 0, "endHour": 0, "endMinute": 0, "workMode": "SelfUse", "fdSoc": reserve, "maxSoc": 100, "fdPwr": fdPwr_max, "minSocOnGrid": reserve} + while target_count > 0 and len(schedule) < target_count: + schedule.append(disabled_entry.copy()) + return schedule + + +def validate_schedule(new_schedule, reserve, fdPwr_max, target_count=0): # Sort schedule by start time, closest to midnight first new_schedule = sort_schedule_by_start_time(new_schedule) if not new_schedule: # No schedule entries so disable - new_schedule = [{"enable": 1, "startHour": 0, "startMinute": 0, "endHour": 23, "endMinute": 59, "workMode": "SelfUse", "fdSoc": reserve, "maxSoc": 100, "fdPwr": fdPwr_max, "minSocOnGrid": reserve}] - return new_schedule + result = [{"enable": 1, "startHour": 0, "startMinute": 0, "endHour": 23, "endMinute": 59, "workMode": "SelfUse", "fdSoc": reserve, "maxSoc": 100, "fdPwr": fdPwr_max, "minSocOnGrid": reserve}] + return pad_schedule(result, target_count, reserve, fdPwr_max) # Process all schedule entries result_schedule = [] @@ -203,7 +212,8 @@ def validate_schedule(new_schedule, reserve, fdPwr_max): demand_start_minute += 1 result_schedule.append({"enable": 1, "startHour": demand_start_hour, "startMinute": demand_start_minute, "endHour": 23, "endMinute": 59, "workMode": "SelfUse", "fdSoc": reserve, "maxSoc": 100, "fdPwr": fdPwr_max, "minSocOnGrid": reserve}) - return result_schedule + # Pad to target_count with disabled SelfUse entries if the device originally had more slots + return pad_schedule(result_schedule, target_count, reserve, fdPwr_max) class FoxAPI(ComponentBase, OAuthMixin): @@ -224,10 +234,10 @@ def initialize(self, key, automatic, inverter_sn=None, auth_method=None, token_e self.device_production_year = {} self.device_battery_charging_time = {} self.device_scheduler = {} - self.device_current_schedule = {} self.local_schedule = {} self.fdpwr_max = {} self.fdsoc_min = {} + self.device_scheduler_count = {} # Rate limiting tracking self.requests_today = 0 self.rate_limit_errors_today = 0 @@ -710,8 +720,6 @@ async def compute_schedule(self, deviceSN): "minSocOnGrid": reserve, "maxSoc": 100, } - self.device_current_schedule[deviceSN] = battery_slots - # Sort the groups so that group 0 is the first charge slot and group 1 is the first discharge slot # For multiple slots pick the enabled one first charge_group = {} @@ -808,7 +816,7 @@ async def set_scheduler_enabled(self, deviceSN, enabled): # Do change enable if not already modified if self.device_scheduler.get(deviceSN, {}).get("enable", None) == enabled_value: self.log("Fox: Debug: Scheduler for {} already set to enabled {}".format(deviceSN, enabled)) - return + return False self.log("Fox: Debug: Setting scheduler enabled={} was {} for {}".format(enabled, self.device_scheduler.get(deviceSN, {}).get("enable", None), deviceSN)) @@ -818,6 +826,8 @@ async def set_scheduler_enabled(self, deviceSN, enabled): if deviceSN not in self.device_scheduler: self.device_scheduler[deviceSN] = {} self.device_scheduler[deviceSN]["enable"] = enabled_value + return True + return False async def set_scheduler(self, deviceSN, groups): """ @@ -829,7 +839,7 @@ async def set_scheduler(self, deviceSN, groups): if not groups: if current_enable: # Disable scheduler if enabled and no groups - await self.set_scheduler_enabled(deviceSN, False) + return await self.set_scheduler_enabled(deviceSN, False) else: # Compare old and new schedule to see if it needs setting same = schedules_are_equal(datetime.now(), current_groups, groups) @@ -841,6 +851,8 @@ async def set_scheduler(self, deviceSN, groups): self.device_scheduler[deviceSN] = {} self.device_scheduler[deviceSN]["enable"] = True self.device_scheduler[deviceSN]["groups"] = groups + return True + return False async def publish_schedule_settings_ha(self, deviceSN): """ @@ -1029,7 +1041,8 @@ async def get_scheduler(self, deviceSN, checkBattery=True): # Min SOC On grid can change as Predbat writes reserve so this must be the real min self.fdsoc_min[deviceSN] = result.get("properties", {}).get("fdsoc", {}).get("range", {}).get("min", 10) - self.log("Fox: Fetched schedule got {} fdPwr max {} fdSoc min {}".format(result, self.fdpwr_max[deviceSN], self.fdsoc_min[deviceSN])) + self.device_scheduler_count[deviceSN] = len(result.get("groups", [])) + self.log("Fox: Fetched schedule got {} fdPwr max {} fdSoc min {} groups {}".format(result, self.fdpwr_max[deviceSN], self.fdsoc_min[deviceSN], self.device_scheduler_count[deviceSN])) self.device_scheduler[deviceSN] = result return result return {} @@ -1514,7 +1527,7 @@ async def write_battery_schedule_event(self, entity_id, value): entity_id = entity_id.replace(f"number.{self.prefix}_fox_", "") sn = entity_id.split("_")[0] serial = None - for s in self.device_current_schedule: + for s in self.device_detail: if s.lower() == sn.lower(): serial = s break @@ -1619,12 +1632,10 @@ async def apply_battery_schedule(self, serial): new_schedule.append( {"enable": 1, "startHour": start_hour, "startMinute": start_minute, "endHour": end_hour, "endMinute": end_minute, "workMode": "ForceDischarge", "fdSoc": max(soc, reserve), "maxSoc": reserve, "fdPwr": power, "minSocOnGrid": reserve} ) - new_schedule = validate_schedule(new_schedule, reserve, fdPwr_max) + new_schedule = validate_schedule(new_schedule, reserve, fdPwr_max, self.device_scheduler_count.get(serial, 0)) self.log("Fox: New schedule for {}: {}".format(serial, new_schedule)) - result = await self.set_scheduler(serial, new_schedule) - if result is not None: - self.device_current_schedule[serial] = new_schedule - await self.publish_data() + await self.set_scheduler(serial, new_schedule) + await self.publish_data() async def automatic_config(self): """ @@ -1754,20 +1765,90 @@ def set_arg(self, key, value): print(f"Set arg {key} = {value} (state={state})") -async def test_fox_api(sn, api_key, token_hash): # pragma: no cover +async def test_write_schedule(sn, api_key, token_hash, token_expires, supabase_url, supabase_key, user_id): # pragma: no cover + """ + Write a hardcoded test schedule to the Fox API and read it back to verify + """ + if supabase_url: + os.environ["SUPABASE_URL"] = supabase_url + if supabase_key: + os.environ["SUPABASE_KEY"] = supabase_key + + schedule = [ + {"endHour": 20, "fdPwr": 7000, "minSocOnGrid": 10, "workMode": "ForceDischarge", "fdSoc": 10, "enable": 1, "startHour": 20, "maxSoc": 100, "startMinute": 16, "endMinute": 30}, # 20:16 - 20:30 + ] + + mock_base = MockBase() + if user_id: + mock_base.args["user_id"] = user_id + + arg_dict = {"key": api_key or "", "automatic": False} + if token_hash or supabase_url: + arg_dict["auth_method"] = "oauth" + arg_dict["token_hash"] = token_hash + arg_dict["token_expires_at"] = token_expires + fox_api = FoxAPI(mock_base, **arg_dict) + + # Discover devices if no SN provided + devices = await fox_api.get_device_list() + if not devices: + print("No devices found") + return + serial = sn if sn else devices[0].get("deviceSN") + print(f"Using device SN: {serial}") + + # Fetch device detail so hasBattery check passes + await fox_api.get_device_detail(serial) + + # Initial read back to check connectivity + read_back = await fox_api.get_scheduler(serial, checkBattery=False) + + # Write the schedule + print(f"Writing schedule:\n{json.dumps(schedule, indent=2)}") + schedule = validate_schedule(schedule, 10, 7000, fox_api.device_scheduler_count.get(serial, 0)) + write_ok = await fox_api.set_scheduler(serial, schedule) + print(f"Write result: {write_ok}") + + # Read back and print + print("Reading back schedule...") + read_back = await fox_api.get_scheduler(serial, checkBattery=False) + read_back_groups = read_back.get("groups", []) + print(f"Read back schedule:\n{json.dumps(read_back, indent=2)}") + + # Compare written schedule against read-back groups + from datetime import datetime as _dt + + match = schedules_are_equal(_dt.now(), schedule, read_back_groups) + print(f"Schedule match: {match}") + if not match: + print("WARNING: Written schedule does not match read-back schedule") + + +async def test_fox_api(sn, api_key, token_hash, token_expires, supabase_url, supabase_key, user_id): # pragma: no cover """ Run a test """ - print(f"Testing Fox API with key: {api_key[:10]}...") + # Set supabase env vars before constructing FoxAPI so OAuthMixin can find them + if supabase_url: + os.environ["SUPABASE_URL"] = supabase_url + if supabase_key: + os.environ["SUPABASE_KEY"] = supabase_key + + if api_key: + print(f"Testing Fox API with api-key: {api_key[:10]}...") + else: + print("Testing Fox API with OAuth token-hash...") # Create a mock base object mock_base = MockBase() + if user_id: + mock_base.args["user_id"] = user_id - # Create FoxAPI instance with a lambda that returns the API key - arg_dict = {} - arg_dict = {"key": api_key, "automatic": True, "token_hash": token_hash} - if token_hash: + arg_dict = {"key": api_key or "", "automatic": True} + if token_hash or supabase_url: arg_dict["auth_method"] = "oauth" + arg_dict["token_hash"] = token_hash + arg_dict["token_expires_at"] = token_expires fox_api = FoxAPI(mock_base, **arg_dict) # Call run() once @@ -1782,16 +1863,29 @@ def main(): # pragma: no cover """ parser = argparse.ArgumentParser(description="Test Fox API") parser.add_argument("--serial", action="store", default=None, help="Fox API serial number") - parser.add_argument("--api-key", required=True, help="Fox API key") - parser.add_argument("--token-hash", action="store", help="Fox API token hash") + auth_group = parser.add_mutually_exclusive_group(required=True) + auth_group.add_argument("--api-key", help="Fox API key") + auth_group.add_argument("--token-hash", action="store", help="Fox API OAuth token hash") + parser.add_argument("--token-expires", action="store", help="Fox API OAuth token expiry timestamp") + parser.add_argument("--supabase-url", action="store", help="Supabase URL for OAuth token refresh") + parser.add_argument("--supabase-key", action="store", help="Supabase anon key for OAuth token refresh") + parser.add_argument("--user-id", action="store", help="Supabase user ID for OAuth token refresh") + parser.add_argument("--write-schedule", action="store_true", help="Write a test schedule and read it back instead of running a full test") args = parser.parse_args() - key = args.api_key serial = args.serial + api_key = args.api_key token_hash = args.token_hash + token_expires = args.token_expires + supabase_url = args.supabase_url + supabase_key = args.supabase_key + user_id = args.user_id # Run the test - asyncio.run(test_fox_api(serial, key, token_hash)) + if args.write_schedule: + asyncio.run(test_write_schedule(serial, api_key, token_hash, token_expires, supabase_url, supabase_key, user_id)) + else: + asyncio.run(test_fox_api(serial, api_key, token_hash, token_expires, supabase_url, supabase_key, user_id)) if __name__ == "__main__": diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py index f3f93e863..d0f9d7dcf 100644 --- a/apps/predbat/predbat.py +++ b/apps/predbat/predbat.py @@ -36,7 +36,7 @@ import requests import asyncio -THIS_VERSION = "v8.37.4" +THIS_VERSION = "v8.37.5" from download import predbat_update_move, predbat_update_download, check_install, resolve_predbat_repository, DEFAULT_PREDBAT_REPOSITORY from const import MINUTE_WATT diff --git a/apps/predbat/tests/test_fox_api.py b/apps/predbat/tests/test_fox_api.py index 8a0e6112f..c34b79aae 100644 --- a/apps/predbat/tests/test_fox_api.py +++ b/apps/predbat/tests/test_fox_api.py @@ -22,11 +22,11 @@ class MockFoxAPI: def __init__(self): self.device_battery_charging_time = {} self.device_scheduler = {} - self.device_settings = {} self.local_schedule = {} - self.device_current_schedule = {} + self.device_settings = {} self.fdpwr_max = {} self.fdsoc_min = {} + self.device_scheduler_count = {} self.inverter_sn_filter = [] def getMinSocOnGrid(self, deviceSN): @@ -56,10 +56,10 @@ def __init__(self): self.device_production_year = {} self.device_battery_charging_time = {} self.device_scheduler = {} - self.device_current_schedule = {} self.local_schedule = {} self.fdpwr_max = {} self.fdsoc_min = {} + self.device_scheduler_count = {} self.local_tz = pytz.timezone("Europe/London") self.inverter_sn_filter = [] @@ -646,6 +646,86 @@ def test_validate_schedule_discharge_ending_at_midnight(my_predbat): return False +def test_validate_schedule_padding_normal(my_predbat): + """ + Test validate_schedule pads to target_count with disabled SelfUse entries when result is shorter + """ + print(" - test_validate_schedule_padding_normal") + + # Single ForceCharge window 02:30 - 05:30 with target_count=8 + new_schedule = [{"enable": 1, "startHour": 2, "startMinute": 30, "endHour": 5, "endMinute": 30, "workMode": "ForceCharge", "fdSoc": 100, "maxSoc": 100, "fdPwr": 8000, "minSocOnGrid": 10}] + reserve = 10 + fdPwr_max = 8000 + + result = validate_schedule(new_schedule, reserve, fdPwr_max, target_count=8) + + # Should be padded to 8 entries + assert len(result) == 8, f"Expected 8 entries after padding, got {len(result)}" + + # All padding entries should be disabled SelfUse with zero start/end + for entry in result[3:]: + assert entry["enable"] == 0, f"Padding entry should be disabled: {entry}" + assert entry["workMode"] == "SelfUse", f"Padding entry should be SelfUse: {entry}" + assert entry["startHour"] == 0 + assert entry["startMinute"] == 0 + assert entry["endHour"] == 0 + assert entry["endMinute"] == 0 + + return False + + +def test_validate_schedule_padding_empty(my_predbat): + """ + Test validate_schedule pads even when input is empty (early-return path) + """ + print(" - test_validate_schedule_padding_empty") + + reserve = 15 + fdPwr_max = 7000 + + result = validate_schedule([], reserve, fdPwr_max, target_count=8) + + # Should be padded to 8 entries + assert len(result) == 8, f"Expected 8 entries after padding, got {len(result)}" + + # First entry should be the default full-day SelfUse + assert result[0]["enable"] == 1 + assert result[0]["workMode"] == "SelfUse" + assert result[0]["startHour"] == 0 + assert result[0]["endHour"] == 23 + assert result[0]["endMinute"] == 59 + + # Remaining entries should be disabled padding + for entry in result[1:]: + assert entry["enable"] == 0, f"Padding entry should be disabled: {entry}" + assert entry["workMode"] == "SelfUse" + assert entry["startHour"] == 0 + assert entry["startMinute"] == 0 + assert entry["endHour"] == 0 + assert entry["endMinute"] == 0 + + return False + + +def test_validate_schedule_padding_no_target(my_predbat): + """ + Test validate_schedule does not pad when target_count=0 (default) + """ + print(" - test_validate_schedule_padding_no_target") + + new_schedule = [{"enable": 1, "startHour": 2, "startMinute": 0, "endHour": 5, "endMinute": 0, "workMode": "ForceCharge", "fdSoc": 100, "maxSoc": 100, "fdPwr": 8000, "minSocOnGrid": 10}] + reserve = 10 + fdPwr_max = 8000 + + result = validate_schedule(new_schedule, reserve, fdPwr_max) + + # No padding: all entries should be enabled (natural gap-fill entries) + for entry in result: + assert entry["enable"] == 1, f"Unexpected disabled entry without target_count: {entry}" + + return False + + def test_compute_schedule_scheduler_enabled_charge(my_predbat): """ Test compute_schedule with scheduler enabled and a charge window @@ -1905,6 +1985,7 @@ def test_api_get_scheduler(my_predbat): assert fox.fdpwr_max[deviceSN] == 8000 assert fox.fdsoc_min[deviceSN] == 10 + assert fox.device_scheduler_count[deviceSN] == 2 return False @@ -3809,7 +3890,6 @@ def test_write_battery_schedule_event_reserve(my_predbat): fox.device_settings[deviceSN] = {"MinSocOnGrid": {"value": 10}} fox.fdpwr_max[deviceSN] = 8000 fox.fdsoc_min[deviceSN] = 10 - fox.device_current_schedule[deviceSN] = [] fox.local_schedule[deviceSN] = {"reserve": 10} fox.device_scheduler[deviceSN] = {"enable": False, "groups": []} @@ -3835,7 +3915,6 @@ def test_write_battery_schedule_event_charge_enable(my_predbat): fox.device_settings[deviceSN] = {"MinSocOnGrid": {"value": 10}} fox.fdpwr_max[deviceSN] = 8000 fox.fdsoc_min[deviceSN] = 10 - fox.device_current_schedule[deviceSN] = [] fox.local_schedule[deviceSN] = {"reserve": 10, "charge": {"enable": 0}} run_async(fox.write_battery_schedule_event("switch.predbat_fox_test123456_battery_schedule_charge_enable", "turn_on")) @@ -3860,7 +3939,6 @@ def test_write_battery_schedule_event_time_change(my_predbat): fox.device_settings[deviceSN] = {"MinSocOnGrid": {"value": 10}} fox.fdpwr_max[deviceSN] = 8000 fox.fdsoc_min[deviceSN] = 10 - fox.device_current_schedule[deviceSN] = [] fox.local_schedule[deviceSN] = {"reserve": 10, "charge": {"start_time": "00:00:00", "end_time": "00:00:00"}} run_async(fox.write_battery_schedule_event("select.predbat_fox_test123456_battery_schedule_charge_start_time", "02:30:00")) @@ -3885,7 +3963,6 @@ def test_write_battery_schedule_event_soc_change(my_predbat): fox.device_settings[deviceSN] = {"MinSocOnGrid": {"value": 10}} fox.fdpwr_max[deviceSN] = 8000 fox.fdsoc_min[deviceSN] = 10 - fox.device_current_schedule[deviceSN] = [] fox.local_schedule[deviceSN] = {"reserve": 10, "discharge": {"soc": 10}} run_async(fox.write_battery_schedule_event("number.predbat_fox_test123456_battery_schedule_discharge_soc", "20")) @@ -3910,7 +3987,6 @@ def test_write_battery_schedule_event_power_change(my_predbat): fox.device_settings[deviceSN] = {"MinSocOnGrid": {"value": 10}} fox.fdpwr_max[deviceSN] = 8000 fox.fdsoc_min[deviceSN] = 10 - fox.device_current_schedule[deviceSN] = [] fox.local_schedule[deviceSN] = {"reserve": 10, "discharge": {"power": 8000}} run_async(fox.write_battery_schedule_event("number.predbat_fox_test123456_battery_schedule_discharge_power", "5000")) @@ -3923,14 +3999,13 @@ def test_write_battery_schedule_event_power_change(my_predbat): def test_write_battery_schedule_event_unknown_serial(my_predbat): """ - Test write_battery_schedule_event with unknown serial number (not in device_current_schedule) + Test write_battery_schedule_event with unknown serial number (not in device_detail) """ print(" - test_write_battery_schedule_event_unknown_serial") fox = MockFoxAPIWithRequests() - # Setup empty device_current_schedule - fox.device_current_schedule = {} + # device_detail is empty by default (no known devices) # Try to write with unknown serial run_async(fox.write_battery_schedule_event("number.predbat_fox_unknown123_battery_schedule_reserve", "20")) @@ -3955,7 +4030,6 @@ def test_write_battery_schedule_event_reserve_invalid_value(my_predbat): fox.device_settings[deviceSN] = {"MinSocOnGrid": {"value": 10}} fox.fdpwr_max[deviceSN] = 8000 fox.fdsoc_min[deviceSN] = 15 - fox.device_current_schedule[deviceSN] = [] fox.local_schedule[deviceSN] = {"reserve": 20} fox.device_scheduler[deviceSN] = {"enable": False, "groups": []} @@ -3982,7 +4056,6 @@ def test_write_battery_schedule_event_unknown_direction(my_predbat): fox.device_settings[deviceSN] = {"MinSocOnGrid": {"value": 10}} fox.fdpwr_max[deviceSN] = 8000 fox.fdsoc_min[deviceSN] = 10 - fox.device_current_schedule[deviceSN] = [] fox.local_schedule[deviceSN] = {"reserve": 10} # Entity ID without _charge_ or _discharge_ in it (not reserve either) @@ -4010,7 +4083,6 @@ def test_write_battery_schedule_event_initialize_direction_dict(my_predbat): fox.device_settings[deviceSN] = {"MinSocOnGrid": {"value": 10}} fox.fdpwr_max[deviceSN] = 8000 fox.fdsoc_min[deviceSN] = 10 - fox.device_current_schedule[deviceSN] = [] # local_schedule exists but no charge/discharge dict fox.local_schedule[deviceSN] = {"reserve": 10} @@ -4037,7 +4109,6 @@ def test_write_battery_schedule_event_soc_invalid_value(my_predbat): fox.device_settings[deviceSN] = {"MinSocOnGrid": {"value": 10}} fox.fdpwr_max[deviceSN] = 8000 fox.fdsoc_min[deviceSN] = 10 - fox.device_current_schedule[deviceSN] = [] fox.local_schedule[deviceSN] = {"reserve": 10, "charge": {"soc": 80}, "discharge": {"soc": 20}} # Test charge with invalid value - should default to 100 @@ -4065,7 +4136,6 @@ def test_write_battery_schedule_event_power_invalid_value(my_predbat): fox.device_settings[deviceSN] = {"MinSocOnGrid": {"value": 10}} fox.fdpwr_max[deviceSN] = 8000 fox.fdsoc_min[deviceSN] = 10 - fox.device_current_schedule[deviceSN] = [] fox.local_schedule[deviceSN] = {"reserve": 10, "charge": {"power": 5000}} # Try to set power to invalid value - should default to fdpwr_max (8000) @@ -4089,7 +4159,6 @@ def test_write_battery_schedule_event_start_time_invalid(my_predbat): fox.device_settings[deviceSN] = {"MinSocOnGrid": {"value": 10}} fox.fdpwr_max[deviceSN] = 8000 fox.fdsoc_min[deviceSN] = 10 - fox.device_current_schedule[deviceSN] = [] fox.local_schedule[deviceSN] = {"reserve": 10, "charge": {"start_time": "02:30:00"}} # Try to set start_time to invalid value - should default to "00:00:00" @@ -4113,7 +4182,6 @@ def test_write_battery_schedule_event_end_time_invalid(my_predbat): fox.device_settings[deviceSN] = {"MinSocOnGrid": {"value": 10}} fox.fdpwr_max[deviceSN] = 8000 fox.fdsoc_min[deviceSN] = 10 - fox.device_current_schedule[deviceSN] = [] fox.local_schedule[deviceSN] = {"reserve": 10, "discharge": {"end_time": "19:00:00"}} # Try to set end_time to invalid value - should default to "00:00:00" @@ -4137,7 +4205,6 @@ def test_write_battery_schedule_event_write_trigger(my_predbat): fox.device_settings[deviceSN] = {"MinSocOnGrid": {"value": 10}} fox.fdpwr_max[deviceSN] = 8000 fox.fdsoc_min[deviceSN] = 10 - fox.device_current_schedule[deviceSN] = [] fox.local_schedule[deviceSN] = { "reserve": 10, "charge": {"enable": 1, "start_time": "02:30:00", "end_time": "05:30:00", "soc": 100, "power": 8000}, @@ -4182,7 +4249,6 @@ def test_write_battery_schedule_event_unknown_attribute(my_predbat): fox.device_settings[deviceSN] = {"MinSocOnGrid": {"value": 10}} fox.fdpwr_max[deviceSN] = 8000 fox.fdsoc_min[deviceSN] = 10 - fox.device_current_schedule[deviceSN] = [] fox.local_schedule[deviceSN] = {"reserve": 10, "charge": {}} # Try with unknown attribute @@ -4208,7 +4274,6 @@ def test_write_battery_schedule_event_initialize_local_schedule(my_predbat): fox.device_settings[deviceSN] = {"MinSocOnGrid": {"value": 10}} fox.fdpwr_max[deviceSN] = 8000 fox.fdsoc_min[deviceSN] = 15 - fox.device_current_schedule[deviceSN] = [] # local_schedule doesn't have deviceSN key fox.local_schedule = {} fox.device_scheduler[deviceSN] = {"enable": False, "groups": []} @@ -4258,7 +4323,6 @@ def test_select_event_battery_schedule(my_predbat): fox.device_settings[deviceSN] = {"MinSocOnGrid": {"value": 10}} fox.fdpwr_max[deviceSN] = 8000 fox.fdsoc_min[deviceSN] = 10 - fox.device_current_schedule[deviceSN] = [] fox.local_schedule[deviceSN] = {"reserve": 10, "charge": {"start_time": "00:00:00"}} run_async(fox.select_event("select.predbat_fox_test123456_battery_schedule_charge_start_time", "02:30:00")) @@ -4304,7 +4368,6 @@ def test_number_event_battery_schedule(my_predbat): fox.device_settings[deviceSN] = {"MinSocOnGrid": {"value": 10}} fox.fdpwr_max[deviceSN] = 8000 fox.fdsoc_min[deviceSN] = 10 - fox.device_current_schedule[deviceSN] = [] fox.local_schedule[deviceSN] = {"reserve": 10, "charge": {"soc": 80}} run_async(fox.number_event("number.predbat_fox_test123456_battery_schedule_charge_soc", "100")) @@ -4329,7 +4392,6 @@ def test_switch_event_battery_schedule(my_predbat): fox.device_settings[deviceSN] = {"MinSocOnGrid": {"value": 10}} fox.fdpwr_max[deviceSN] = 8000 fox.fdsoc_min[deviceSN] = 10 - fox.device_current_schedule[deviceSN] = [] fox.local_schedule[deviceSN] = {"reserve": 10, "charge": {"enable": 0}} run_async(fox.switch_event("switch.predbat_fox_test123456_battery_schedule_charge_enable", "turn_on")) @@ -4354,7 +4416,6 @@ def test_switch_event_toggle(my_predbat): fox.device_settings[deviceSN] = {"MinSocOnGrid": {"value": 10}} fox.fdpwr_max[deviceSN] = 8000 fox.fdsoc_min[deviceSN] = 10 - fox.device_current_schedule[deviceSN] = [] fox.local_schedule[deviceSN] = {"reserve": 10, "discharge": {"enable": 1}} run_async(fox.switch_event("switch.predbat_fox_test123456_battery_schedule_discharge_enable", "turn_off")) @@ -5380,6 +5441,9 @@ def run_fox_api_tests(my_predbat): failed |= test_validate_schedule_multiple_windows(my_predbat) failed |= test_validate_schedule_both_charge_and_discharge(my_predbat) failed |= test_validate_schedule_discharge_ending_at_midnight(my_predbat) + failed |= test_validate_schedule_padding_normal(my_predbat) + failed |= test_validate_schedule_padding_empty(my_predbat) + failed |= test_validate_schedule_padding_no_target(my_predbat) if failed: return failed diff --git a/apps/predbat/tests/test_fox_oauth.py b/apps/predbat/tests/test_fox_oauth.py index 32e6ef404..0c1099683 100644 --- a/apps/predbat/tests/test_fox_oauth.py +++ b/apps/predbat/tests/test_fox_oauth.py @@ -34,7 +34,6 @@ def __init__(self, auth_method="api_key", key="test-api-key", token_expires_at=N self.device_production_year = {} self.device_battery_charging_time = {} self.device_scheduler = {} - self.device_current_schedule = {} self.local_schedule = {} self.fdpwr_max = {} self.fdsoc_min = {}