From fb4df00e3c48df29413251a130a5ac7b614ad65c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 21 Jan 2025 08:27:41 +0100 Subject: [PATCH] Add support for custom weekly backup schedule (#136079) * Add support for custom weekly backup schedule * Rename the new flag to custom_days * Make the store change backwards compatible * Improve comments --- homeassistant/components/backup/config.py | 55 +- homeassistant/components/backup/store.py | 8 +- homeassistant/components/backup/websocket.py | 17 +- .../backup/snapshots/test_store.ambr | 3 + .../backup/snapshots/test_websocket.ambr | 664 +++++++++++++++--- tests/components/backup/test_websocket.py | 244 +++++-- 6 files changed, 831 insertions(+), 160 deletions(-) diff --git a/homeassistant/components/backup/config.py b/homeassistant/components/backup/config.py index 997813eca21..da6a2b85ccb 100644 --- a/homeassistant/components/backup/config.py +++ b/homeassistant/components/backup/config.py @@ -81,6 +81,7 @@ class BackupConfigData: time = dt_util.parse_time(time_str) else: time = None + days = [Day(day) for day in data["schedule"]["days"]] return cls( create_backup=CreateBackupConfig( @@ -99,7 +100,10 @@ class BackupConfigData: days=retention["days"], ), schedule=BackupSchedule( - state=ScheduleState(data["schedule"]["state"]), time=time + days=days, + recurrence=ScheduleRecurrence(data["schedule"]["recurrence"]), + state=ScheduleState(data["schedule"]["state"]), + time=time, ), ) @@ -252,6 +256,8 @@ class RetentionParametersDict(TypedDict, total=False): class StoredBackupSchedule(TypedDict): """Represent the stored backup schedule configuration.""" + days: list[Day] + recurrence: ScheduleRecurrence state: ScheduleState time: str | None @@ -259,13 +265,38 @@ class StoredBackupSchedule(TypedDict): class ScheduleParametersDict(TypedDict, total=False): """Represent parameters for backup schedule.""" + days: list[Day] + recurrence: ScheduleRecurrence state: ScheduleState time: dt.time | None -class ScheduleState(StrEnum): +class Day(StrEnum): + """Represent the day(s) in a custom schedule recurrence.""" + + MONDAY = "mon" + TUESDAY = "tue" + WEDNESDAY = "wed" + THURSDAY = "thu" + FRIDAY = "fri" + SATURDAY = "sat" + SUNDAY = "sun" + + +class ScheduleRecurrence(StrEnum): """Represent the schedule recurrence.""" + NEVER = "never" + DAILY = "daily" + CUSTOM_DAYS = "custom_days" + + +class ScheduleState(StrEnum): + """Represent the schedule recurrence. + + This is deprecated and can be remove in HA Core 2025.8. + """ + NEVER = "never" DAILY = "daily" MONDAY = "mon" @@ -281,6 +312,10 @@ class ScheduleState(StrEnum): class BackupSchedule: """Represent the backup schedule.""" + days: list[Day] = field(default_factory=list) + recurrence: ScheduleRecurrence = ScheduleRecurrence.NEVER + # Although no longer used, state is kept for backwards compatibility. + # It can be removed in HA Core 2025.8. state: ScheduleState = ScheduleState.NEVER time: dt.time | None = None cron_event: CronSim | None = field(init=False, default=None) @@ -293,22 +328,26 @@ class BackupSchedule: ) -> None: """Apply a new schedule. - There are only three possible state types: never, daily, or weekly. + There are only three possible recurrence types: never, daily, or custom_days """ - if self.state is ScheduleState.NEVER: + if self.recurrence is ScheduleRecurrence.NEVER or ( + self.recurrence is ScheduleRecurrence.CUSTOM_DAYS and not self.days + ): self._unschedule_next(manager) return time = self.time if self.time is not None else DEFAULT_BACKUP_TIME - if self.state is ScheduleState.DAILY: + if self.recurrence is ScheduleRecurrence.DAILY: self._schedule_next( CRON_PATTERN_DAILY.format(m=time.minute, h=time.hour), manager, ) - else: + else: # ScheduleRecurrence.CUSTOM_DAYS self._schedule_next( CRON_PATTERN_WEEKLY.format( - m=time.minute, h=time.hour, d=self.state.value + m=time.minute, + h=time.hour, + d=",".join(day.value for day in self.days), ), manager, ) @@ -376,6 +415,8 @@ class BackupSchedule: def to_dict(self) -> StoredBackupSchedule: """Convert backup schedule to a dict.""" return StoredBackupSchedule( + days=self.days, + recurrence=self.recurrence, state=self.state, time=self.time.isoformat() if self.time else None, ) diff --git a/homeassistant/components/backup/store.py b/homeassistant/components/backup/store.py index 205bdf80375..b8241bb771d 100644 --- a/homeassistant/components/backup/store.py +++ b/homeassistant/components/backup/store.py @@ -48,8 +48,14 @@ class _BackupStore(Store[StoredBackupData]): data = old_data if old_major_version == 1: if old_minor_version < 2: - # Version 1.2 adds configurable backup time + # Version 1.2 adds configurable backup time and custom days data["config"]["schedule"]["time"] = None + if (state := data["config"]["schedule"]["state"]) in ("daily", "never"): + data["config"]["schedule"]["days"] = [] + data["config"]["schedule"]["recurrence"] = state + else: + data["config"]["schedule"]["days"] = [state] + data["config"]["schedule"]["recurrence"] = "custom_days" if old_major_version > 1: raise NotImplementedError diff --git a/homeassistant/components/backup/websocket.py b/homeassistant/components/backup/websocket.py index 235d53952c1..672dd5ebb13 100644 --- a/homeassistant/components/backup/websocket.py +++ b/homeassistant/components/backup/websocket.py @@ -8,7 +8,7 @@ from homeassistant.components import websocket_api from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv -from .config import ScheduleState +from .config import Day, ScheduleRecurrence from .const import DATA_MANAGER, LOGGER from .manager import ( DecryptOnDowloadNotSupported, @@ -320,13 +320,17 @@ async def handle_config_info( ) -> None: """Send the stored backup config.""" manager = hass.data[DATA_MANAGER] + config = manager.config.data.to_dict() + # Remove state from schedule, it's not needed in the frontend + # mypy doesn't like deleting from TypedDict, ignore it + del config["schedule"]["state"] # type: ignore[misc] connection.send_result( msg["id"], { - "config": manager.config.data.to_dict() + "config": config | { "next_automatic_backup": manager.config.data.schedule.next_automatic_backup - }, + } }, ) @@ -358,7 +362,12 @@ async def handle_config_info( ), vol.Optional("schedule"): vol.Schema( { - vol.Optional("state"): vol.All(str, vol.Coerce(ScheduleState)), + vol.Optional("days"): vol.Any( + vol.All([vol.Coerce(Day)], vol.Unique()), + ), + vol.Optional("recurrence"): vol.All( + str, vol.Coerce(ScheduleRecurrence) + ), vol.Optional("time"): vol.Any(cv.time, None), } ), diff --git a/tests/components/backup/snapshots/test_store.ambr b/tests/components/backup/snapshots/test_store.ambr index fb5d0c276b5..a66bbe43b85 100644 --- a/tests/components/backup/snapshots/test_store.ambr +++ b/tests/components/backup/snapshots/test_store.ambr @@ -28,6 +28,9 @@ 'days': None, }), 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', 'state': 'never', 'time': None, }), diff --git a/tests/components/backup/snapshots/test_websocket.ambr b/tests/components/backup/snapshots/test_websocket.ambr index 8b0ab1317c3..2c88dc50577 100644 --- a/tests/components/backup/snapshots/test_websocket.ambr +++ b/tests/components/backup/snapshots/test_websocket.ambr @@ -250,7 +250,9 @@ 'days': None, }), 'schedule': dict({ - 'state': 'never', + 'days': list([ + ]), + 'recurrence': 'never', 'time': None, }), }), @@ -287,7 +289,16 @@ 'days': 7, }), 'schedule': dict({ - 'state': 'daily', + 'days': list([ + 'mon', + 'tue', + 'wed', + 'thu', + 'fri', + 'sat', + 'sun', + ]), + 'recurrence': 'custom_days', 'time': None, }), }), @@ -320,7 +331,9 @@ 'days': None, }), 'schedule': dict({ - 'state': 'never', + 'days': list([ + ]), + 'recurrence': 'never', 'time': None, }), }), @@ -353,7 +366,9 @@ 'days': 7, }), 'schedule': dict({ - 'state': 'never', + 'days': list([ + ]), + 'recurrence': 'never', 'time': None, }), }), @@ -386,7 +401,10 @@ 'days': None, }), 'schedule': dict({ - 'state': 'mon', + 'days': list([ + 'mon', + ]), + 'recurrence': 'custom_days', 'time': None, }), }), @@ -413,13 +431,52 @@ }), 'last_attempted_automatic_backup': None, 'last_completed_automatic_backup': None, - 'next_automatic_backup': '2024-11-16T04:55:00+01:00', + 'next_automatic_backup': None, 'retention': dict({ 'copies': None, 'days': None, }), 'schedule': dict({ - 'state': 'sat', + 'days': list([ + ]), + 'recurrence': 'never', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_config_info[storage_data6] + dict({ + 'id': 1, + 'result': dict({ + 'config': dict({ + 'create_backup': dict({ + 'agent_ids': list([ + 'test-agent', + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': False, + 'include_folders': None, + 'name': None, + 'password': None, + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'next_automatic_backup': '2024-11-17T04:55:00+01:00', + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + 'mon', + 'sun', + ]), + 'recurrence': 'custom_days', 'time': None, }), }), @@ -451,7 +508,9 @@ 'days': None, }), 'schedule': dict({ - 'state': 'never', + 'days': list([ + ]), + 'recurrence': 'never', 'time': None, }), }), @@ -484,7 +543,9 @@ 'days': 7, }), 'schedule': dict({ - 'state': 'never', + 'days': list([ + ]), + 'recurrence': 'never', 'time': None, }), }), @@ -517,6 +578,9 @@ 'days': 7, }), 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', 'state': 'never', 'time': None, }), @@ -550,7 +614,9 @@ 'days': None, }), 'schedule': dict({ - 'state': 'never', + 'days': list([ + ]), + 'recurrence': 'never', 'time': None, }), }), @@ -579,11 +645,13 @@ 'last_completed_automatic_backup': None, 'next_automatic_backup': '2024-11-14T04:55:00+01:00', 'retention': dict({ - 'copies': None, - 'days': 7, + 'copies': 3, + 'days': None, }), 'schedule': dict({ - 'state': 'daily', + 'days': list([ + ]), + 'recurrence': 'daily', 'time': None, }), }), @@ -611,12 +679,121 @@ }), 'last_attempted_automatic_backup': None, 'last_completed_automatic_backup': None, + 'retention': dict({ + 'copies': 3, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'daily', + 'state': 'never', + 'time': None, + }), + }), + }), + 'key': 'backup', + 'minor_version': 2, + 'version': 1, + }) +# --- +# name: test_config_update[command11] + dict({ + 'id': 1, + 'result': dict({ + 'config': dict({ + 'create_backup': dict({ + 'agent_ids': list([ + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': True, + 'include_folders': None, + 'name': None, + 'password': None, + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'next_automatic_backup': None, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_config_update[command11].1 + dict({ + 'id': 3, + 'result': dict({ + 'config': dict({ + 'create_backup': dict({ + 'agent_ids': list([ + 'test-agent', + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': True, + 'include_folders': None, + 'name': None, + 'password': None, + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'next_automatic_backup': '2024-11-14T04:55:00+01:00', 'retention': dict({ 'copies': None, 'days': 7, }), 'schedule': dict({ - 'state': 'daily', + 'days': list([ + ]), + 'recurrence': 'daily', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_config_update[command11].2 + dict({ + 'data': dict({ + 'backups': list([ + ]), + 'config': dict({ + 'create_backup': dict({ + 'agent_ids': list([ + 'test-agent', + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': True, + 'include_folders': None, + 'name': None, + 'password': None, + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'retention': dict({ + 'copies': None, + 'days': 7, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'daily', + 'state': 'never', 'time': None, }), }), @@ -649,7 +826,9 @@ 'days': None, }), 'schedule': dict({ - 'state': 'never', + 'days': list([ + ]), + 'recurrence': 'never', 'time': None, }), }), @@ -682,7 +861,9 @@ 'days': None, }), 'schedule': dict({ - 'state': 'daily', + 'days': list([ + ]), + 'recurrence': 'daily', 'time': '06:00:00', }), }), @@ -715,7 +896,10 @@ 'days': None, }), 'schedule': dict({ - 'state': 'daily', + 'days': list([ + ]), + 'recurrence': 'daily', + 'state': 'never', 'time': '06:00:00', }), }), @@ -748,7 +932,9 @@ 'days': None, }), 'schedule': dict({ - 'state': 'never', + 'days': list([ + ]), + 'recurrence': 'never', 'time': None, }), }), @@ -781,7 +967,10 @@ 'days': None, }), 'schedule': dict({ - 'state': 'mon', + 'days': list([ + 'mon', + ]), + 'recurrence': 'custom_days', 'time': None, }), }), @@ -814,7 +1003,11 @@ 'days': None, }), 'schedule': dict({ - 'state': 'mon', + 'days': list([ + 'mon', + ]), + 'recurrence': 'custom_days', + 'state': 'never', 'time': None, }), }), @@ -847,7 +1040,9 @@ 'days': None, }), 'schedule': dict({ - 'state': 'never', + 'days': list([ + ]), + 'recurrence': 'never', 'time': None, }), }), @@ -880,7 +1075,9 @@ 'days': None, }), 'schedule': dict({ - 'state': 'never', + 'days': list([ + ]), + 'recurrence': 'never', 'time': None, }), }), @@ -913,6 +1110,9 @@ 'days': None, }), 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', 'state': 'never', 'time': None, }), @@ -946,7 +1146,9 @@ 'days': None, }), 'schedule': dict({ - 'state': 'never', + 'days': list([ + ]), + 'recurrence': 'never', 'time': None, }), }), @@ -964,26 +1166,26 @@ 'agent_ids': list([ 'test-agent', ]), - 'include_addons': list([ - 'test-addon', - ]), + 'include_addons': None, 'include_all_addons': False, 'include_database': True, - 'include_folders': list([ - 'media', - ]), - 'name': 'test-name', - 'password': 'test-password', + 'include_folders': None, + 'name': None, + 'password': None, }), 'last_attempted_automatic_backup': None, 'last_completed_automatic_backup': None, - 'next_automatic_backup': '2024-11-14T04:55:00+01:00', + 'next_automatic_backup': '2024-11-17T04:55:00+01:00', 'retention': dict({ 'copies': None, 'days': None, }), 'schedule': dict({ - 'state': 'daily', + 'days': list([ + 'mon', + 'sun', + ]), + 'recurrence': 'custom_days', 'time': None, }), }), @@ -1002,16 +1204,12 @@ 'agent_ids': list([ 'test-agent', ]), - 'include_addons': list([ - 'test-addon', - ]), + 'include_addons': None, 'include_all_addons': False, 'include_database': True, - 'include_folders': list([ - 'media', - ]), - 'name': 'test-name', - 'password': 'test-password', + 'include_folders': None, + 'name': None, + 'password': None, }), 'last_attempted_automatic_backup': None, 'last_completed_automatic_backup': None, @@ -1020,7 +1218,12 @@ 'days': None, }), 'schedule': dict({ - 'state': 'daily', + 'days': list([ + 'mon', + 'sun', + ]), + 'recurrence': 'custom_days', + 'state': 'never', 'time': None, }), }), @@ -1053,7 +1256,9 @@ 'days': None, }), 'schedule': dict({ - 'state': 'never', + 'days': list([ + ]), + 'recurrence': 'never', 'time': None, }), }), @@ -1071,22 +1276,28 @@ 'agent_ids': list([ 'test-agent', ]), - 'include_addons': None, + 'include_addons': list([ + 'test-addon', + ]), 'include_all_addons': False, 'include_database': True, - 'include_folders': None, - 'name': None, - 'password': None, + 'include_folders': list([ + 'media', + ]), + 'name': 'test-name', + 'password': 'test-password', }), 'last_attempted_automatic_backup': None, 'last_completed_automatic_backup': None, 'next_automatic_backup': '2024-11-14T04:55:00+01:00', 'retention': dict({ - 'copies': 3, - 'days': 7, + 'copies': None, + 'days': None, }), 'schedule': dict({ - 'state': 'daily', + 'days': list([ + ]), + 'recurrence': 'daily', 'time': None, }), }), @@ -1105,21 +1316,28 @@ 'agent_ids': list([ 'test-agent', ]), - 'include_addons': None, + 'include_addons': list([ + 'test-addon', + ]), 'include_all_addons': False, 'include_database': True, - 'include_folders': None, - 'name': None, - 'password': None, + 'include_folders': list([ + 'media', + ]), + 'name': 'test-name', + 'password': 'test-password', }), 'last_attempted_automatic_backup': None, 'last_completed_automatic_backup': None, 'retention': dict({ - 'copies': 3, - 'days': 7, + 'copies': None, + 'days': None, }), 'schedule': dict({ - 'state': 'daily', + 'days': list([ + ]), + 'recurrence': 'daily', + 'state': 'never', 'time': None, }), }), @@ -1152,7 +1370,9 @@ 'days': None, }), 'schedule': dict({ - 'state': 'never', + 'days': list([ + ]), + 'recurrence': 'never', 'time': None, }), }), @@ -1181,11 +1401,13 @@ 'last_completed_automatic_backup': None, 'next_automatic_backup': '2024-11-14T04:55:00+01:00', 'retention': dict({ - 'copies': None, - 'days': None, + 'copies': 3, + 'days': 7, }), 'schedule': dict({ - 'state': 'daily', + 'days': list([ + ]), + 'recurrence': 'daily', 'time': None, }), }), @@ -1214,11 +1436,14 @@ 'last_attempted_automatic_backup': None, 'last_completed_automatic_backup': None, 'retention': dict({ - 'copies': None, - 'days': None, + 'copies': 3, + 'days': 7, }), 'schedule': dict({ - 'state': 'daily', + 'days': list([ + ]), + 'recurrence': 'daily', + 'state': 'never', 'time': None, }), }), @@ -1251,7 +1476,9 @@ 'days': None, }), 'schedule': dict({ - 'state': 'never', + 'days': list([ + ]), + 'recurrence': 'never', 'time': None, }), }), @@ -1280,11 +1507,13 @@ 'last_completed_automatic_backup': None, 'next_automatic_backup': '2024-11-14T04:55:00+01:00', 'retention': dict({ - 'copies': 3, + 'copies': None, 'days': None, }), 'schedule': dict({ - 'state': 'daily', + 'days': list([ + ]), + 'recurrence': 'daily', 'time': None, }), }), @@ -1313,11 +1542,14 @@ 'last_attempted_automatic_backup': None, 'last_completed_automatic_backup': None, 'retention': dict({ - 'copies': 3, + 'copies': None, 'days': None, }), 'schedule': dict({ - 'state': 'daily', + 'days': list([ + ]), + 'recurrence': 'daily', + 'state': 'never', 'time': None, }), }), @@ -1350,7 +1582,9 @@ 'days': None, }), 'schedule': dict({ - 'state': 'never', + 'days': list([ + ]), + 'recurrence': 'never', 'time': None, }), }), @@ -1379,11 +1613,13 @@ 'last_completed_automatic_backup': None, 'next_automatic_backup': '2024-11-14T04:55:00+01:00', 'retention': dict({ - 'copies': None, - 'days': 7, + 'copies': 3, + 'days': None, }), 'schedule': dict({ - 'state': 'daily', + 'days': list([ + ]), + 'recurrence': 'daily', 'time': None, }), }), @@ -1412,11 +1648,14 @@ 'last_attempted_automatic_backup': None, 'last_completed_automatic_backup': None, 'retention': dict({ - 'copies': None, - 'days': 7, + 'copies': 3, + 'days': None, }), 'schedule': dict({ - 'state': 'daily', + 'days': list([ + ]), + 'recurrence': 'daily', + 'state': 'never', 'time': None, }), }), @@ -1449,7 +1688,9 @@ 'days': None, }), 'schedule': dict({ - 'state': 'never', + 'days': list([ + ]), + 'recurrence': 'never', 'time': None, }), }), @@ -1478,11 +1719,13 @@ 'last_completed_automatic_backup': None, 'next_automatic_backup': '2024-11-14T04:55:00+01:00', 'retention': dict({ - 'copies': 3, - 'days': None, + 'copies': None, + 'days': 7, }), 'schedule': dict({ - 'state': 'daily', + 'days': list([ + ]), + 'recurrence': 'daily', 'time': None, }), }), @@ -1511,11 +1754,14 @@ 'last_attempted_automatic_backup': None, 'last_completed_automatic_backup': None, 'retention': dict({ - 'copies': 3, - 'days': None, + 'copies': None, + 'days': 7, }), 'schedule': dict({ - 'state': 'daily', + 'days': list([ + ]), + 'recurrence': 'daily', + 'state': 'never', 'time': None, }), }), @@ -1548,7 +1794,9 @@ 'days': None, }), 'schedule': dict({ - 'state': 'never', + 'days': list([ + ]), + 'recurrence': 'never', 'time': None, }), }), @@ -1580,7 +1828,9 @@ 'days': None, }), 'schedule': dict({ - 'state': 'never', + 'days': list([ + ]), + 'recurrence': 'never', 'time': None, }), }), @@ -1612,7 +1862,9 @@ 'days': None, }), 'schedule': dict({ - 'state': 'never', + 'days': list([ + ]), + 'recurrence': 'never', 'time': None, }), }), @@ -1644,7 +1896,9 @@ 'days': None, }), 'schedule': dict({ - 'state': 'never', + 'days': list([ + ]), + 'recurrence': 'never', 'time': None, }), }), @@ -1676,7 +1930,9 @@ 'days': None, }), 'schedule': dict({ - 'state': 'never', + 'days': list([ + ]), + 'recurrence': 'never', 'time': None, }), }), @@ -1708,7 +1964,9 @@ 'days': None, }), 'schedule': dict({ - 'state': 'never', + 'days': list([ + ]), + 'recurrence': 'never', 'time': None, }), }), @@ -1740,7 +1998,9 @@ 'days': None, }), 'schedule': dict({ - 'state': 'never', + 'days': list([ + ]), + 'recurrence': 'never', 'time': None, }), }), @@ -1772,7 +2032,9 @@ 'days': None, }), 'schedule': dict({ - 'state': 'never', + 'days': list([ + ]), + 'recurrence': 'never', 'time': None, }), }), @@ -1804,7 +2066,9 @@ 'days': None, }), 'schedule': dict({ - 'state': 'never', + 'days': list([ + ]), + 'recurrence': 'never', 'time': None, }), }), @@ -1836,7 +2100,9 @@ 'days': None, }), 'schedule': dict({ - 'state': 'never', + 'days': list([ + ]), + 'recurrence': 'never', 'time': None, }), }), @@ -1868,7 +2134,9 @@ 'days': None, }), 'schedule': dict({ - 'state': 'never', + 'days': list([ + ]), + 'recurrence': 'never', 'time': None, }), }), @@ -1900,7 +2168,213 @@ 'days': None, }), 'schedule': dict({ - 'state': 'never', + 'days': list([ + ]), + 'recurrence': 'never', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_config_update_errors[command6] + dict({ + 'id': 1, + 'result': dict({ + 'config': dict({ + 'create_backup': dict({ + 'agent_ids': list([ + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': True, + 'include_folders': None, + 'name': None, + 'password': None, + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'next_automatic_backup': None, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_config_update_errors[command6].1 + dict({ + 'id': 3, + 'result': dict({ + 'config': dict({ + 'create_backup': dict({ + 'agent_ids': list([ + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': True, + 'include_folders': None, + 'name': None, + 'password': None, + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'next_automatic_backup': None, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_config_update_errors[command7] + dict({ + 'id': 1, + 'result': dict({ + 'config': dict({ + 'create_backup': dict({ + 'agent_ids': list([ + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': True, + 'include_folders': None, + 'name': None, + 'password': None, + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'next_automatic_backup': None, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_config_update_errors[command7].1 + dict({ + 'id': 3, + 'result': dict({ + 'config': dict({ + 'create_backup': dict({ + 'agent_ids': list([ + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': True, + 'include_folders': None, + 'name': None, + 'password': None, + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'next_automatic_backup': None, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_config_update_errors[command8] + dict({ + 'id': 1, + 'result': dict({ + 'config': dict({ + 'create_backup': dict({ + 'agent_ids': list([ + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': True, + 'include_folders': None, + 'name': None, + 'password': None, + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'next_automatic_backup': None, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_config_update_errors[command8].1 + dict({ + 'id': 3, + 'result': dict({ + 'config': dict({ + 'create_backup': dict({ + 'agent_ids': list([ + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': True, + 'include_folders': None, + 'name': None, + 'password': None, + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'next_automatic_backup': None, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', 'time': None, }), }), diff --git a/tests/components/backup/test_websocket.py b/tests/components/backup/test_websocket.py index 29ce4dc485e..44a470053a5 100644 --- a/tests/components/backup/test_websocket.py +++ b/tests/components/backup/test_websocket.py @@ -71,9 +71,10 @@ DEFAULT_STORAGE_DATA: dict[str, Any] = { "copies": None, "days": None, }, - "schedule": {"state": "never", "time": None}, + "schedule": {"days": [], "recurrence": "never", "state": "never", "time": None}, }, } +DAILY = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] @pytest.fixture @@ -924,7 +925,12 @@ async def test_agents_info( "retention": {"copies": 3, "days": 7}, "last_attempted_automatic_backup": "2024-10-26T04:45:00+01:00", "last_completed_automatic_backup": "2024-10-26T04:45:00+01:00", - "schedule": {"state": "daily", "time": None}, + "schedule": { + "days": DAILY, + "recurrence": "custom_days", + "state": "never", + "time": None, + }, }, }, "key": DOMAIN, @@ -949,7 +955,12 @@ async def test_agents_info( "retention": {"copies": 3, "days": None}, "last_attempted_automatic_backup": None, "last_completed_automatic_backup": None, - "schedule": {"state": "never", "time": None}, + "schedule": { + "days": [], + "recurrence": "never", + "state": "never", + "time": None, + }, }, }, "key": DOMAIN, @@ -974,7 +985,12 @@ async def test_agents_info( "retention": {"copies": None, "days": 7}, "last_attempted_automatic_backup": "2024-10-27T04:45:00+01:00", "last_completed_automatic_backup": "2024-10-26T04:45:00+01:00", - "schedule": {"state": "never", "time": None}, + "schedule": { + "days": [], + "recurrence": "never", + "state": "never", + "time": None, + }, }, }, "key": DOMAIN, @@ -999,7 +1015,12 @@ async def test_agents_info( "retention": {"copies": None, "days": None}, "last_attempted_automatic_backup": None, "last_completed_automatic_backup": None, - "schedule": {"state": "mon", "time": None}, + "schedule": { + "days": ["mon"], + "recurrence": "custom_days", + "state": "never", + "time": None, + }, }, }, "key": DOMAIN, @@ -1024,7 +1045,42 @@ async def test_agents_info( "retention": {"copies": None, "days": None}, "last_attempted_automatic_backup": None, "last_completed_automatic_backup": None, - "schedule": {"state": "sat", "time": None}, + "schedule": { + "days": [], + "recurrence": "never", + "state": "never", + "time": None, + }, + }, + }, + "key": DOMAIN, + "version": store.STORAGE_VERSION, + "minor_version": store.STORAGE_VERSION_MINOR, + }, + }, + { + "backup": { + "data": { + "backups": [], + "config": { + "create_backup": { + "agent_ids": ["test-agent"], + "include_addons": None, + "include_all_addons": False, + "include_database": False, + "include_folders": None, + "name": None, + "password": None, + }, + "retention": {"copies": None, "days": None}, + "last_attempted_automatic_backup": None, + "last_completed_automatic_backup": None, + "schedule": { + "days": ["mon", "sun"], + "recurrence": "custom_days", + "state": "never", + "time": None, + }, }, }, "key": DOMAIN, @@ -1069,17 +1125,22 @@ async def test_config_info( { "type": "backup/config/update", "create_backup": {"agent_ids": ["test-agent"]}, - "schedule": {"state": "daily", "time": "06:00"}, + "schedule": {"recurrence": "daily", "time": "06:00"}, }, { "type": "backup/config/update", "create_backup": {"agent_ids": ["test-agent"]}, - "schedule": {"state": "mon"}, + "schedule": {"days": ["mon"], "recurrence": "custom_days"}, }, { "type": "backup/config/update", "create_backup": {"agent_ids": ["test-agent"]}, - "schedule": {"state": "never"}, + "schedule": {"recurrence": "never"}, + }, + { + "type": "backup/config/update", + "create_backup": {"agent_ids": ["test-agent"]}, + "schedule": {"days": ["mon", "sun"], "recurrence": "custom_days"}, }, { "type": "backup/config/update", @@ -1090,43 +1151,43 @@ async def test_config_info( "name": "test-name", "password": "test-password", }, - "schedule": {"state": "daily"}, + "schedule": {"recurrence": "daily"}, }, { "type": "backup/config/update", "create_backup": {"agent_ids": ["test-agent"]}, "retention": {"copies": 3, "days": 7}, - "schedule": {"state": "daily"}, + "schedule": {"recurrence": "daily"}, }, { "type": "backup/config/update", "create_backup": {"agent_ids": ["test-agent"]}, "retention": {"copies": None, "days": None}, - "schedule": {"state": "daily"}, + "schedule": {"recurrence": "daily"}, }, { "type": "backup/config/update", "create_backup": {"agent_ids": ["test-agent"]}, "retention": {"copies": 3, "days": None}, - "schedule": {"state": "daily"}, + "schedule": {"recurrence": "daily"}, }, { "type": "backup/config/update", "create_backup": {"agent_ids": ["test-agent"]}, "retention": {"copies": None, "days": 7}, - "schedule": {"state": "daily"}, + "schedule": {"recurrence": "daily"}, }, { "type": "backup/config/update", "create_backup": {"agent_ids": ["test-agent"]}, "retention": {"copies": 3}, - "schedule": {"state": "daily"}, + "schedule": {"recurrence": "daily"}, }, { "type": "backup/config/update", "create_backup": {"agent_ids": ["test-agent"]}, "retention": {"days": 7}, - "schedule": {"state": "daily"}, + "schedule": {"recurrence": "daily"}, }, ], ) @@ -1174,17 +1235,32 @@ async def test_config_update( { "type": "backup/config/update", "create_backup": {"agent_ids": ["test-agent"]}, - "schedule": "blah", + "recurrence": "blah", }, { "type": "backup/config/update", "create_backup": {"agent_ids": ["test-agent"]}, - "schedule": {"state": "someday"}, + "recurrence": "never", }, { "type": "backup/config/update", "create_backup": {"agent_ids": ["test-agent"]}, - "schedule": {"time": "early"}, + "recurrence": {"state": "someday"}, + }, + { + "type": "backup/config/update", + "create_backup": {"agent_ids": ["test-agent"]}, + "recurrence": {"time": "early"}, + }, + { + "type": "backup/config/update", + "create_backup": {"agent_ids": ["test-agent"]}, + "recurrence": {"days": "mon"}, + }, + { + "type": "backup/config/update", + "create_backup": {"agent_ids": ["test-agent"]}, + "recurrence": {"days": ["fun"]}, }, { "type": "backup/config/update", @@ -1260,7 +1336,7 @@ async def test_config_update_errors( { "type": "backup/config/update", "create_backup": {"agent_ids": ["test.test-agent"]}, - "schedule": {"state": "daily"}, + "schedule": {"recurrence": "daily"}, } ], "2024-11-11T04:45:00+01:00", @@ -1279,7 +1355,7 @@ async def test_config_update_errors( { "type": "backup/config/update", "create_backup": {"agent_ids": ["test.test-agent"]}, - "schedule": {"state": "mon"}, + "schedule": {"days": ["mon"], "recurrence": "custom_days"}, } ], "2024-11-11T04:45:00+01:00", @@ -1298,7 +1374,11 @@ async def test_config_update_errors( { "type": "backup/config/update", "create_backup": {"agent_ids": ["test.test-agent"]}, - "schedule": {"state": "mon", "time": "03:45"}, + "schedule": { + "days": ["mon"], + "recurrence": "custom_days", + "time": "03:45", + }, } ], "2024-11-11T03:45:00+01:00", @@ -1317,7 +1397,7 @@ async def test_config_update_errors( { "type": "backup/config/update", "create_backup": {"agent_ids": ["test.test-agent"]}, - "schedule": {"state": "daily", "time": "03:45"}, + "schedule": {"recurrence": "daily", "time": "03:45"}, } ], "2024-11-11T03:45:00+01:00", @@ -1336,7 +1416,26 @@ async def test_config_update_errors( { "type": "backup/config/update", "create_backup": {"agent_ids": ["test.test-agent"]}, - "schedule": {"state": "never"}, + "schedule": {"days": ["wed", "fri"], "recurrence": "custom_days"}, + } + ], + "2024-11-11T04:45:00+01:00", + "2024-11-13T04:55:00+01:00", + "2024-11-15T04:55:00+01:00", + "2024-11-13T04:55:00+01:00", + "2024-11-13T04:55:00+01:00", + "2024-11-13T04:55:00+01:00", + 1, + 2, + BACKUP_CALL, + None, + ), + ( + [ + { + "type": "backup/config/update", + "create_backup": {"agent_ids": ["test.test-agent"]}, + "schedule": {"recurrence": "never"}, } ], "2024-11-11T04:45:00+01:00", @@ -1355,7 +1454,26 @@ async def test_config_update_errors( { "type": "backup/config/update", "create_backup": {"agent_ids": ["test.test-agent"]}, - "schedule": {"state": "daily"}, + "schedule": {"days": [], "recurrence": "custom_days"}, + } + ], + "2024-11-11T04:45:00+01:00", + "2034-11-11T12:00:00+01:00", # ten years later and still no backups + "2034-11-11T13:00:00+01:00", + "2024-11-11T04:45:00+01:00", + "2024-11-11T04:45:00+01:00", + None, + 0, + 0, + None, + None, + ), + ( + [ + { + "type": "backup/config/update", + "create_backup": {"agent_ids": ["test.test-agent"]}, + "schedule": {"recurrence": "daily"}, } ], "2024-10-26T04:45:00+01:00", @@ -1374,7 +1492,7 @@ async def test_config_update_errors( { "type": "backup/config/update", "create_backup": {"agent_ids": ["test.test-agent"]}, - "schedule": {"state": "mon"}, + "schedule": {"days": ["mon"], "recurrence": "custom_days"}, } ], "2024-10-26T04:45:00+01:00", @@ -1393,7 +1511,7 @@ async def test_config_update_errors( { "type": "backup/config/update", "create_backup": {"agent_ids": ["test.test-agent"]}, - "schedule": {"state": "never"}, + "schedule": {"recurrence": "never"}, } ], "2024-10-26T04:45:00+01:00", @@ -1412,7 +1530,7 @@ async def test_config_update_errors( { "type": "backup/config/update", "create_backup": {"agent_ids": ["test.test-agent"]}, - "schedule": {"state": "daily"}, + "schedule": {"recurrence": "daily"}, } ], "2024-11-11T04:45:00+01:00", @@ -1431,7 +1549,7 @@ async def test_config_update_errors( { "type": "backup/config/update", "create_backup": {"agent_ids": ["test.test-agent"]}, - "schedule": {"state": "daily"}, + "schedule": {"recurrence": "daily"}, } ], "2024-11-11T04:45:00+01:00", @@ -1483,7 +1601,12 @@ async def test_config_schedule_logic( "retention": {"copies": None, "days": None}, "last_attempted_automatic_backup": last_completed_automatic_backup, "last_completed_automatic_backup": last_completed_automatic_backup, - "schedule": {"state": "daily", "time": None}, + "schedule": { + "days": [], + "recurrence": "daily", + "state": "never", + "time": None, + }, }, } hass_storage[DOMAIN] = { @@ -1553,7 +1676,7 @@ async def test_config_schedule_logic( "type": "backup/config/update", "create_backup": {"agent_ids": ["test.test-agent"]}, "retention": {"copies": None, "days": None}, - "schedule": {"state": "daily"}, + "schedule": {"recurrence": "daily"}, }, { "backup-1": MagicMock( @@ -1592,7 +1715,7 @@ async def test_config_schedule_logic( "type": "backup/config/update", "create_backup": {"agent_ids": ["test.test-agent"]}, "retention": {"copies": 3, "days": None}, - "schedule": {"state": "daily"}, + "schedule": {"recurrence": "daily"}, }, { "backup-1": MagicMock( @@ -1631,7 +1754,7 @@ async def test_config_schedule_logic( "type": "backup/config/update", "create_backup": {"agent_ids": ["test.test-agent"]}, "retention": {"copies": 3, "days": None}, - "schedule": {"state": "daily"}, + "schedule": {"recurrence": "daily"}, }, { "backup-1": MagicMock( @@ -1660,7 +1783,7 @@ async def test_config_schedule_logic( "type": "backup/config/update", "create_backup": {"agent_ids": ["test.test-agent"]}, "retention": {"copies": 3, "days": None}, - "schedule": {"state": "daily"}, + "schedule": {"recurrence": "daily"}, }, { "backup-1": MagicMock( @@ -1704,7 +1827,7 @@ async def test_config_schedule_logic( "type": "backup/config/update", "create_backup": {"agent_ids": ["test.test-agent"]}, "retention": {"copies": 2, "days": None}, - "schedule": {"state": "daily"}, + "schedule": {"recurrence": "daily"}, }, { "backup-1": MagicMock( @@ -1748,7 +1871,7 @@ async def test_config_schedule_logic( "type": "backup/config/update", "create_backup": {"agent_ids": ["test.test-agent"]}, "retention": {"copies": 2, "days": None}, - "schedule": {"state": "daily"}, + "schedule": {"recurrence": "daily"}, }, { "backup-1": MagicMock( @@ -1787,7 +1910,7 @@ async def test_config_schedule_logic( "type": "backup/config/update", "create_backup": {"agent_ids": ["test.test-agent"]}, "retention": {"copies": 2, "days": None}, - "schedule": {"state": "daily"}, + "schedule": {"recurrence": "daily"}, }, { "backup-1": MagicMock( @@ -1826,7 +1949,7 @@ async def test_config_schedule_logic( "type": "backup/config/update", "create_backup": {"agent_ids": ["test.test-agent"]}, "retention": {"copies": 0, "days": None}, - "schedule": {"state": "daily"}, + "schedule": {"recurrence": "daily"}, }, { "backup-1": MagicMock( @@ -1870,7 +1993,7 @@ async def test_config_schedule_logic( "type": "backup/config/update", "create_backup": {"agent_ids": ["test.test-agent"]}, "retention": {"copies": 0, "days": None}, - "schedule": {"state": "daily"}, + "schedule": {"recurrence": "daily"}, }, { "backup-1": MagicMock( @@ -1934,7 +2057,12 @@ async def test_config_retention_copies_logic( "retention": {"copies": None, "days": None}, "last_attempted_automatic_backup": None, "last_completed_automatic_backup": last_backup_time, - "schedule": {"state": "daily", "time": None}, + "schedule": { + "days": [], + "recurrence": "daily", + "state": "never", + "time": None, + }, }, } hass_storage[DOMAIN] = { @@ -2005,7 +2133,7 @@ async def test_config_retention_copies_logic( "type": "backup/config/update", "create_backup": {"agent_ids": ["test.test-agent"]}, "retention": {"copies": None, "days": None}, - "schedule": {"state": "never"}, + "schedule": {"recurrence": "never"}, }, { "backup-1": MagicMock( @@ -2041,7 +2169,7 @@ async def test_config_retention_copies_logic( "type": "backup/config/update", "create_backup": {"agent_ids": ["test.test-agent"]}, "retention": {"copies": 3, "days": None}, - "schedule": {"state": "never"}, + "schedule": {"recurrence": "never"}, }, { "backup-1": MagicMock( @@ -2077,7 +2205,7 @@ async def test_config_retention_copies_logic( "type": "backup/config/update", "create_backup": {"agent_ids": ["test.test-agent"]}, "retention": {"copies": 3, "days": None}, - "schedule": {"state": "never"}, + "schedule": {"recurrence": "never"}, }, { "backup-1": MagicMock( @@ -2118,7 +2246,7 @@ async def test_config_retention_copies_logic( "type": "backup/config/update", "create_backup": {"agent_ids": ["test.test-agent"]}, "retention": {"copies": 2, "days": None}, - "schedule": {"state": "never"}, + "schedule": {"recurrence": "never"}, }, { "backup-1": MagicMock( @@ -2192,7 +2320,12 @@ async def test_config_retention_copies_logic_manual_backup( "retention": {"copies": None, "days": None}, "last_attempted_automatic_backup": None, "last_completed_automatic_backup": None, - "schedule": {"state": "daily", "time": None}, + "schedule": { + "days": [], + "recurrence": "daily", + "state": "never", + "time": None, + }, }, } hass_storage[DOMAIN] = { @@ -2320,7 +2453,7 @@ async def test_config_retention_copies_logic_manual_backup( "type": "backup/config/update", "create_backup": {"agent_ids": ["test-agent"]}, "retention": {"copies": None, "days": 2}, - "schedule": {"state": "never"}, + "schedule": {"recurrence": "never"}, } ], { @@ -2356,7 +2489,7 @@ async def test_config_retention_copies_logic_manual_backup( "type": "backup/config/update", "create_backup": {"agent_ids": ["test-agent"]}, "retention": {"copies": None, "days": 2}, - "schedule": {"state": "never"}, + "schedule": {"recurrence": "never"}, } ], { @@ -2392,7 +2525,7 @@ async def test_config_retention_copies_logic_manual_backup( "type": "backup/config/update", "create_backup": {"agent_ids": ["test-agent"]}, "retention": {"copies": None, "days": 3}, - "schedule": {"state": "never"}, + "schedule": {"recurrence": "never"}, } ], { @@ -2428,7 +2561,7 @@ async def test_config_retention_copies_logic_manual_backup( "type": "backup/config/update", "create_backup": {"agent_ids": ["test-agent"]}, "retention": {"copies": None, "days": 2}, - "schedule": {"state": "never"}, + "schedule": {"recurrence": "never"}, } ], { @@ -2469,7 +2602,7 @@ async def test_config_retention_copies_logic_manual_backup( "type": "backup/config/update", "create_backup": {"agent_ids": ["test-agent"]}, "retention": {"copies": None, "days": 2}, - "schedule": {"state": "never"}, + "schedule": {"recurrence": "never"}, } ], { @@ -2505,7 +2638,7 @@ async def test_config_retention_copies_logic_manual_backup( "type": "backup/config/update", "create_backup": {"agent_ids": ["test-agent"]}, "retention": {"copies": None, "days": 2}, - "schedule": {"state": "never"}, + "schedule": {"recurrence": "never"}, } ], { @@ -2541,7 +2674,7 @@ async def test_config_retention_copies_logic_manual_backup( "type": "backup/config/update", "create_backup": {"agent_ids": ["test-agent"]}, "retention": {"copies": None, "days": 0}, - "schedule": {"state": "never"}, + "schedule": {"recurrence": "never"}, } ], { @@ -2613,7 +2746,12 @@ async def test_config_retention_days_logic( "retention": {"copies": None, "days": stored_retained_days}, "last_attempted_automatic_backup": None, "last_completed_automatic_backup": last_backup_time, - "schedule": {"state": "never", "time": None}, + "schedule": { + "days": [], + "recurrence": "never", + "state": "never", + "time": None, + }, }, } hass_storage[DOMAIN] = {