Make the time for automated backups configurable (#135825)

* Make the time for automated backups configurable

* Store time as a string, use None to indicate default time

* Don't add jitter if the time is set by user

* Include time of next automatic backup in response to backup/info

* Update tests

* Rename recurrence to state

* Include scheduled backup time in backup/config/info response

* Address review comments

* Update cloud test

* Add test for store migration

* Address review comments
This commit is contained in:
Erik Montnemery 2025-01-20 12:57:46 +01:00 committed by GitHub
parent 8020bec47b
commit 43da828a51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 629 additions and 122 deletions

View File

@ -5,6 +5,7 @@ from __future__ import annotations
import asyncio import asyncio
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass, field, replace from dataclasses import dataclass, field, replace
import datetime as dt
from datetime import datetime, timedelta from datetime import datetime, timedelta
from enum import StrEnum from enum import StrEnum
import random import random
@ -23,11 +24,13 @@ from .models import BackupManagerError, Folder
if TYPE_CHECKING: if TYPE_CHECKING:
from .manager import BackupManager, ManagerBackup from .manager import BackupManager, ManagerBackup
# The time of the automatic backup event should be compatible with CRON_PATTERN_DAILY = "{m} {h} * * *"
# the time of the recorder's nightly job which runs at 04:12. CRON_PATTERN_WEEKLY = "{m} {h} * * {d}"
# Run the backup at 04:45.
CRON_PATTERN_DAILY = "45 4 * * *" # The default time for automatic backups to run is at 04:45.
CRON_PATTERN_WEEKLY = "45 4 * * {}" # This time is chosen to be compatible with the time of the recorder's
# nightly job which runs at 04:12.
DEFAULT_BACKUP_TIME = dt.time(4, 45)
# Randomize the start time of the backup by up to 60 minutes to avoid # Randomize the start time of the backup by up to 60 minutes to avoid
# all backups running at the same time. # all backups running at the same time.
@ -74,6 +77,11 @@ class BackupConfigData:
else: else:
last_completed = None last_completed = None
if time_str := data["schedule"]["time"]:
time = dt_util.parse_time(time_str)
else:
time = None
return cls( return cls(
create_backup=CreateBackupConfig( create_backup=CreateBackupConfig(
agent_ids=data["create_backup"]["agent_ids"], agent_ids=data["create_backup"]["agent_ids"],
@ -90,7 +98,9 @@ class BackupConfigData:
copies=retention["copies"], copies=retention["copies"],
days=retention["days"], days=retention["days"],
), ),
schedule=BackupSchedule(state=ScheduleState(data["schedule"]["state"])), schedule=BackupSchedule(
state=ScheduleState(data["schedule"]["state"]), time=time
),
) )
def to_dict(self) -> StoredBackupConfig: def to_dict(self) -> StoredBackupConfig:
@ -137,7 +147,7 @@ class BackupConfig:
*, *,
create_backup: CreateBackupParametersDict | UndefinedType = UNDEFINED, create_backup: CreateBackupParametersDict | UndefinedType = UNDEFINED,
retention: RetentionParametersDict | UndefinedType = UNDEFINED, retention: RetentionParametersDict | UndefinedType = UNDEFINED,
schedule: ScheduleState | UndefinedType = UNDEFINED, schedule: ScheduleParametersDict | UndefinedType = UNDEFINED,
) -> None: ) -> None:
"""Update config.""" """Update config."""
if create_backup is not UNDEFINED: if create_backup is not UNDEFINED:
@ -148,7 +158,7 @@ class BackupConfig:
self.data.retention = new_retention self.data.retention = new_retention
self.data.retention.apply(self._manager) self.data.retention.apply(self._manager)
if schedule is not UNDEFINED: if schedule is not UNDEFINED:
new_schedule = BackupSchedule(state=schedule) new_schedule = BackupSchedule(**schedule)
if new_schedule.to_dict() != self.data.schedule.to_dict(): if new_schedule.to_dict() != self.data.schedule.to_dict():
self.data.schedule = new_schedule self.data.schedule = new_schedule
self.data.schedule.apply(self._manager) self.data.schedule.apply(self._manager)
@ -243,10 +253,18 @@ class StoredBackupSchedule(TypedDict):
"""Represent the stored backup schedule configuration.""" """Represent the stored backup schedule configuration."""
state: ScheduleState state: ScheduleState
time: str | None
class ScheduleParametersDict(TypedDict, total=False):
"""Represent parameters for backup schedule."""
state: ScheduleState
time: dt.time | None
class ScheduleState(StrEnum): class ScheduleState(StrEnum):
"""Represent the schedule state.""" """Represent the schedule recurrence."""
NEVER = "never" NEVER = "never"
DAILY = "daily" DAILY = "daily"
@ -264,7 +282,9 @@ class BackupSchedule:
"""Represent the backup schedule.""" """Represent the backup schedule."""
state: ScheduleState = ScheduleState.NEVER state: ScheduleState = ScheduleState.NEVER
time: dt.time | None = None
cron_event: CronSim | None = field(init=False, default=None) cron_event: CronSim | None = field(init=False, default=None)
next_automatic_backup: datetime | None = field(init=False, default=None)
@callback @callback
def apply( def apply(
@ -279,11 +299,17 @@ class BackupSchedule:
self._unschedule_next(manager) self._unschedule_next(manager)
return return
time = self.time if self.time is not None else DEFAULT_BACKUP_TIME
if self.state is ScheduleState.DAILY: if self.state is ScheduleState.DAILY:
self._schedule_next(CRON_PATTERN_DAILY, manager) self._schedule_next(
CRON_PATTERN_DAILY.format(m=time.minute, h=time.hour),
manager,
)
else: else:
self._schedule_next( self._schedule_next(
CRON_PATTERN_WEEKLY.format(self.state.value), CRON_PATTERN_WEEKLY.format(
m=time.minute, h=time.hour, d=self.state.value
),
manager, manager,
) )
@ -304,7 +330,10 @@ class BackupSchedule:
if next_time < now: if next_time < now:
# schedule a backup at next daily time once # schedule a backup at next daily time once
# if we missed the last scheduled backup # if we missed the last scheduled backup
cron_event = CronSim(CRON_PATTERN_DAILY, now) time = self.time if self.time is not None else DEFAULT_BACKUP_TIME
cron_event = CronSim(
CRON_PATTERN_DAILY.format(m=time.minute, h=time.hour), now
)
next_time = next(cron_event) next_time = next(cron_event)
# reseed the cron event attribute # reseed the cron event attribute
# add a day to the next time to avoid scheduling at the same time again # add a day to the next time to avoid scheduling at the same time again
@ -334,19 +363,27 @@ class BackupSchedule:
except Exception: # noqa: BLE001 except Exception: # noqa: BLE001
LOGGER.exception("Unexpected error creating automatic backup") LOGGER.exception("Unexpected error creating automatic backup")
if self.time is None:
# randomize the start time of the backup by up to 60 minutes if the time is
# not set to avoid all backups running at the same time
next_time += timedelta(seconds=random.randint(0, BACKUP_START_TIME_JITTER)) next_time += timedelta(seconds=random.randint(0, BACKUP_START_TIME_JITTER))
LOGGER.debug("Scheduling next automatic backup at %s", next_time) LOGGER.debug("Scheduling next automatic backup at %s", next_time)
self.next_automatic_backup = next_time
manager.remove_next_backup_event = async_track_point_in_time( manager.remove_next_backup_event = async_track_point_in_time(
manager.hass, _create_backup, next_time manager.hass, _create_backup, next_time
) )
def to_dict(self) -> StoredBackupSchedule: def to_dict(self) -> StoredBackupSchedule:
"""Convert backup schedule to a dict.""" """Convert backup schedule to a dict."""
return StoredBackupSchedule(state=self.state) return StoredBackupSchedule(
state=self.state,
time=self.time.isoformat() if self.time else None,
)
@callback @callback
def _unschedule_next(self, manager: BackupManager) -> None: def _unschedule_next(self, manager: BackupManager) -> None:
"""Unschedule the next backup.""" """Unschedule the next backup."""
self.next_automatic_backup = None
if (remove_next_event := manager.remove_next_backup_event) is not None: if (remove_next_event := manager.remove_next_backup_event) is not None:
remove_next_event() remove_next_event()
manager.remove_next_backup_event = None manager.remove_next_backup_event = None

View File

@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, TypedDict from typing import TYPE_CHECKING, Any, TypedDict
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.storage import Store from homeassistant.helpers.storage import Store
@ -16,6 +16,7 @@ if TYPE_CHECKING:
STORE_DELAY_SAVE = 30 STORE_DELAY_SAVE = 30
STORAGE_KEY = DOMAIN STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1 STORAGE_VERSION = 1
STORAGE_VERSION_MINOR = 2
class StoredBackupData(TypedDict): class StoredBackupData(TypedDict):
@ -25,14 +26,44 @@ class StoredBackupData(TypedDict):
config: StoredBackupConfig config: StoredBackupConfig
class _BackupStore(Store[StoredBackupData]):
"""Class to help storing backup data."""
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize storage class."""
super().__init__(
hass,
STORAGE_VERSION,
STORAGE_KEY,
minor_version=STORAGE_VERSION_MINOR,
)
async def _async_migrate_func(
self,
old_major_version: int,
old_minor_version: int,
old_data: dict[str, Any],
) -> dict[str, Any]:
"""Migrate to the new version."""
data = old_data
if old_major_version == 1:
if old_minor_version < 2:
# Version 1.2 adds configurable backup time
data["config"]["schedule"]["time"] = None
if old_major_version > 1:
raise NotImplementedError
return data
class BackupStore: class BackupStore:
"""Store backup config.""" """Store backup config."""
def __init__(self, hass: HomeAssistant, manager: BackupManager) -> None: def __init__(self, hass: HomeAssistant, manager: BackupManager) -> None:
"""Initialize the backup manager.""" """Initialize the backup store."""
self._hass = hass self._hass = hass
self._manager = manager self._manager = manager
self._store: Store[StoredBackupData] = Store(hass, STORAGE_VERSION, STORAGE_KEY) self._store = _BackupStore(hass)
async def load(self) -> StoredBackupData | None: async def load(self) -> StoredBackupData | None:
"""Load the store.""" """Load the store."""

View File

@ -6,6 +6,7 @@ import voluptuous as vol
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from .config import ScheduleState from .config import ScheduleState
from .const import DATA_MANAGER, LOGGER from .const import DATA_MANAGER, LOGGER
@ -59,6 +60,7 @@ async def handle_info(
"backups": [backup.as_frontend_json() for backup in backups.values()], "backups": [backup.as_frontend_json() for backup in backups.values()],
"last_attempted_automatic_backup": manager.config.data.last_attempted_automatic_backup, "last_attempted_automatic_backup": manager.config.data.last_attempted_automatic_backup,
"last_completed_automatic_backup": manager.config.data.last_completed_automatic_backup, "last_completed_automatic_backup": manager.config.data.last_completed_automatic_backup,
"next_automatic_backup": manager.config.data.schedule.next_automatic_backup,
}, },
) )
@ -321,7 +323,10 @@ async def handle_config_info(
connection.send_result( connection.send_result(
msg["id"], msg["id"],
{ {
"config": manager.config.data.to_dict(), "config": manager.config.data.to_dict()
| {
"next_automatic_backup": manager.config.data.schedule.next_automatic_backup
},
}, },
) )
@ -351,7 +356,12 @@ async def handle_config_info(
vol.Optional("days"): vol.Any(int, None), vol.Optional("days"): vol.Any(int, None),
}, },
), ),
vol.Optional("schedule"): vol.All(str, vol.Coerce(ScheduleState)), vol.Optional("schedule"): vol.Schema(
{
vol.Optional("state"): vol.All(str, vol.Coerce(ScheduleState)),
vol.Optional("time"): vol.Any(cv.time, None),
}
),
} }
) )
@websocket_api.async_response @websocket_api.async_response

View File

@ -83,6 +83,7 @@
]), ]),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
}), }),
'success': True, 'success': True,
'type': 'result', 'type': 'result',
@ -112,6 +113,7 @@
]), ]),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
}), }),
'success': True, 'success': True,
'type': 'result', 'type': 'result',
@ -141,6 +143,7 @@
]), ]),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
}), }),
'success': True, 'success': True,
'type': 'result', 'type': 'result',
@ -170,6 +173,7 @@
]), ]),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
}), }),
'success': True, 'success': True,
'type': 'result', 'type': 'result',
@ -199,6 +203,7 @@
]), ]),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
}), }),
'success': True, 'success': True,
'type': 'result', 'type': 'result',

View File

@ -0,0 +1,40 @@
# serializer version: 1
# name: test_store_migration
dict({
'data': dict({
'backups': list([
dict({
'backup_id': 'abc123',
'failed_agent_ids': list([
'test.remote',
]),
}),
]),
'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,
'retention': dict({
'copies': None,
'days': None,
}),
'schedule': dict({
'state': 'never',
'time': None,
}),
}),
}),
'key': 'backup',
'minor_version': 2,
'version': 1,
})
# ---

View File

@ -244,12 +244,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': None, 'days': None,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'never', 'state': 'never',
'time': None,
}), }),
}), }),
}), }),
@ -279,12 +281,14 @@
}), }),
'last_attempted_automatic_backup': '2024-10-26T04:45:00+01:00', 'last_attempted_automatic_backup': '2024-10-26T04:45:00+01:00',
'last_completed_automatic_backup': '2024-10-26T04:45:00+01:00', 'last_completed_automatic_backup': '2024-10-26T04:45:00+01:00',
'next_automatic_backup': '2024-11-14T04:55:00+01:00',
'retention': dict({ 'retention': dict({
'copies': 3, 'copies': 3,
'days': 7, 'days': 7,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'daily', 'state': 'daily',
'time': None,
}), }),
}), }),
}), }),
@ -310,12 +314,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
'retention': dict({ 'retention': dict({
'copies': 3, 'copies': 3,
'days': None, 'days': None,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'never', 'state': 'never',
'time': None,
}), }),
}), }),
}), }),
@ -341,12 +347,14 @@
}), }),
'last_attempted_automatic_backup': '2024-10-27T04:45:00+01:00', 'last_attempted_automatic_backup': '2024-10-27T04:45:00+01:00',
'last_completed_automatic_backup': '2024-10-26T04:45:00+01:00', 'last_completed_automatic_backup': '2024-10-26T04:45:00+01:00',
'next_automatic_backup': None,
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': 7, 'days': 7,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'never', 'state': 'never',
'time': None,
}), }),
}), }),
}), }),
@ -372,12 +380,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': '2024-11-18T04:55:00+01:00',
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': None, 'days': None,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'mon', 'state': 'mon',
'time': None,
}), }),
}), }),
}), }),
@ -403,12 +413,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': '2024-11-16T04:55:00+01:00',
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': None, 'days': None,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'sat', 'state': 'sat',
'time': None,
}), }),
}), }),
}), }),
@ -433,12 +445,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': None, 'days': None,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'never', 'state': 'never',
'time': None,
}), }),
}), }),
}), }),
@ -464,12 +478,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': 7, 'days': 7,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'never', 'state': 'never',
'time': None,
}), }),
}), }),
}), }),
@ -502,11 +518,12 @@
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'never', 'state': 'never',
'time': None,
}), }),
}), }),
}), }),
'key': 'backup', 'key': 'backup',
'minor_version': 1, 'minor_version': 2,
'version': 1, 'version': 1,
}) })
# --- # ---
@ -527,12 +544,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': None, 'days': None,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'never', 'state': 'never',
'time': None,
}), }),
}), }),
}), }),
@ -558,12 +577,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': '2024-11-14T04:55:00+01:00',
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': 7, 'days': 7,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'daily', 'state': 'daily',
'time': None,
}), }),
}), }),
}), }),
@ -596,11 +617,12 @@
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'daily', 'state': 'daily',
'time': None,
}), }),
}), }),
}), }),
'key': 'backup', 'key': 'backup',
'minor_version': 1, 'minor_version': 2,
'version': 1, 'version': 1,
}) })
# --- # ---
@ -621,12 +643,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': None, 'days': None,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'never', 'state': 'never',
'time': None,
}), }),
}), }),
}), }),
@ -652,12 +676,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': '2024-11-14T06:00:00+01:00',
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': None, 'days': None,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'daily', 'state': 'daily',
'time': '06:00:00',
}), }),
}), }),
}), }),
@ -690,11 +716,12 @@
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'daily', 'state': 'daily',
'time': '06:00:00',
}), }),
}), }),
}), }),
'key': 'backup', 'key': 'backup',
'minor_version': 1, 'minor_version': 2,
'version': 1, 'version': 1,
}) })
# --- # ---
@ -715,12 +742,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': None, 'days': None,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'never', 'state': 'never',
'time': None,
}), }),
}), }),
}), }),
@ -746,12 +775,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': '2024-11-18T04:55:00+01:00',
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': None, 'days': None,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'mon', 'state': 'mon',
'time': None,
}), }),
}), }),
}), }),
@ -784,11 +815,12 @@
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'mon', 'state': 'mon',
'time': None,
}), }),
}), }),
}), }),
'key': 'backup', 'key': 'backup',
'minor_version': 1, 'minor_version': 2,
'version': 1, 'version': 1,
}) })
# --- # ---
@ -809,12 +841,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': None, 'days': None,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'never', 'state': 'never',
'time': None,
}), }),
}), }),
}), }),
@ -840,12 +874,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': None, 'days': None,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'never', 'state': 'never',
'time': None,
}), }),
}), }),
}), }),
@ -878,11 +914,12 @@
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'never', 'state': 'never',
'time': None,
}), }),
}), }),
}), }),
'key': 'backup', 'key': 'backup',
'minor_version': 1, 'minor_version': 2,
'version': 1, 'version': 1,
}) })
# --- # ---
@ -903,12 +940,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': None, 'days': None,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'never', 'state': 'never',
'time': None,
}), }),
}), }),
}), }),
@ -938,12 +977,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': '2024-11-14T04:55:00+01:00',
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': None, 'days': None,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'daily', 'state': 'daily',
'time': None,
}), }),
}), }),
}), }),
@ -980,11 +1021,12 @@
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'daily', 'state': 'daily',
'time': None,
}), }),
}), }),
}), }),
'key': 'backup', 'key': 'backup',
'minor_version': 1, 'minor_version': 2,
'version': 1, 'version': 1,
}) })
# --- # ---
@ -1005,12 +1047,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': None, 'days': None,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'never', 'state': 'never',
'time': None,
}), }),
}), }),
}), }),
@ -1036,12 +1080,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': '2024-11-14T04:55:00+01:00',
'retention': dict({ 'retention': dict({
'copies': 3, 'copies': 3,
'days': 7, 'days': 7,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'daily', 'state': 'daily',
'time': None,
}), }),
}), }),
}), }),
@ -1074,11 +1120,12 @@
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'daily', 'state': 'daily',
'time': None,
}), }),
}), }),
}), }),
'key': 'backup', 'key': 'backup',
'minor_version': 1, 'minor_version': 2,
'version': 1, 'version': 1,
}) })
# --- # ---
@ -1099,12 +1146,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': None, 'days': None,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'never', 'state': 'never',
'time': None,
}), }),
}), }),
}), }),
@ -1130,12 +1179,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': '2024-11-14T04:55:00+01:00',
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': None, 'days': None,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'daily', 'state': 'daily',
'time': None,
}), }),
}), }),
}), }),
@ -1168,11 +1219,12 @@
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'daily', 'state': 'daily',
'time': None,
}), }),
}), }),
}), }),
'key': 'backup', 'key': 'backup',
'minor_version': 1, 'minor_version': 2,
'version': 1, 'version': 1,
}) })
# --- # ---
@ -1193,12 +1245,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': None, 'days': None,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'never', 'state': 'never',
'time': None,
}), }),
}), }),
}), }),
@ -1224,12 +1278,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': '2024-11-14T04:55:00+01:00',
'retention': dict({ 'retention': dict({
'copies': 3, 'copies': 3,
'days': None, 'days': None,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'daily', 'state': 'daily',
'time': None,
}), }),
}), }),
}), }),
@ -1262,11 +1318,12 @@
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'daily', 'state': 'daily',
'time': None,
}), }),
}), }),
}), }),
'key': 'backup', 'key': 'backup',
'minor_version': 1, 'minor_version': 2,
'version': 1, 'version': 1,
}) })
# --- # ---
@ -1287,12 +1344,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': None, 'days': None,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'never', 'state': 'never',
'time': None,
}), }),
}), }),
}), }),
@ -1318,12 +1377,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': '2024-11-14T04:55:00+01:00',
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': 7, 'days': 7,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'daily', 'state': 'daily',
'time': None,
}), }),
}), }),
}), }),
@ -1356,11 +1417,12 @@
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'daily', 'state': 'daily',
'time': None,
}), }),
}), }),
}), }),
'key': 'backup', 'key': 'backup',
'minor_version': 1, 'minor_version': 2,
'version': 1, 'version': 1,
}) })
# --- # ---
@ -1381,12 +1443,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': None, 'days': None,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'never', 'state': 'never',
'time': None,
}), }),
}), }),
}), }),
@ -1412,12 +1476,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': '2024-11-14T04:55:00+01:00',
'retention': dict({ 'retention': dict({
'copies': 3, 'copies': 3,
'days': None, 'days': None,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'daily', 'state': 'daily',
'time': None,
}), }),
}), }),
}), }),
@ -1450,11 +1516,12 @@
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'daily', 'state': 'daily',
'time': None,
}), }),
}), }),
}), }),
'key': 'backup', 'key': 'backup',
'minor_version': 1, 'minor_version': 2,
'version': 1, 'version': 1,
}) })
# --- # ---
@ -1475,12 +1542,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': None, 'days': None,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'never', 'state': 'never',
'time': None,
}), }),
}), }),
}), }),
@ -1505,12 +1574,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': None, 'days': None,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'never', 'state': 'never',
'time': None,
}), }),
}), }),
}), }),
@ -1535,12 +1606,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': None, 'days': None,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'never', 'state': 'never',
'time': None,
}), }),
}), }),
}), }),
@ -1565,12 +1638,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': None, 'days': None,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'never', 'state': 'never',
'time': None,
}), }),
}), }),
}), }),
@ -1595,12 +1670,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': None, 'days': None,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'never', 'state': 'never',
'time': None,
}), }),
}), }),
}), }),
@ -1625,12 +1702,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': None, 'days': None,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'never', 'state': 'never',
'time': None,
}), }),
}), }),
}), }),
@ -1655,12 +1734,14 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': None, 'days': None,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'never', 'state': 'never',
'time': None,
}), }),
}), }),
}), }),
@ -1685,12 +1766,142 @@
}), }),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
'retention': dict({ 'retention': dict({
'copies': None, 'copies': None,
'days': None, 'days': None,
}), }),
'schedule': dict({ 'schedule': dict({
'state': 'never', 'state': 'never',
'time': None,
}),
}),
}),
'success': True,
'type': 'result',
})
# ---
# name: test_config_update_errors[command4]
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({
'state': 'never',
'time': None,
}),
}),
}),
'success': True,
'type': 'result',
})
# ---
# name: test_config_update_errors[command4].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({
'state': 'never',
'time': None,
}),
}),
}),
'success': True,
'type': 'result',
})
# ---
# name: test_config_update_errors[command5]
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({
'state': 'never',
'time': None,
}),
}),
}),
'success': True,
'type': 'result',
})
# ---
# name: test_config_update_errors[command5].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({
'state': 'never',
'time': None,
}), }),
}), }),
}), }),
@ -1708,6 +1919,7 @@
]), ]),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
}), }),
'success': True, 'success': True,
'type': 'result', 'type': 'result',
@ -1734,6 +1946,7 @@
]), ]),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
}), }),
'success': True, 'success': True,
'type': 'result', 'type': 'result',
@ -1776,6 +1989,7 @@
]), ]),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
}), }),
'success': True, 'success': True,
'type': 'result', 'type': 'result',
@ -1802,6 +2016,7 @@
]), ]),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
}), }),
'success': True, 'success': True,
'type': 'result', 'type': 'result',
@ -1844,6 +2059,7 @@
]), ]),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
}), }),
'success': True, 'success': True,
'type': 'result', 'type': 'result',
@ -1897,6 +2113,7 @@
]), ]),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
}), }),
'success': True, 'success': True,
'type': 'result', 'type': 'result',
@ -1934,6 +2151,7 @@
]), ]),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
}), }),
'success': True, 'success': True,
'type': 'result', 'type': 'result',
@ -1982,6 +2200,7 @@
]), ]),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
}), }),
'success': True, 'success': True,
'type': 'result', 'type': 'result',
@ -2025,6 +2244,7 @@
]), ]),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
}), }),
'success': True, 'success': True,
'type': 'result', 'type': 'result',
@ -2078,6 +2298,7 @@
]), ]),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
}), }),
'success': True, 'success': True,
'type': 'result', 'type': 'result',
@ -2132,6 +2353,7 @@
]), ]),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
}), }),
'success': True, 'success': True,
'type': 'result', 'type': 'result',
@ -2187,6 +2409,7 @@
]), ]),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
}), }),
'success': True, 'success': True,
'type': 'result', 'type': 'result',
@ -2240,6 +2463,7 @@
]), ]),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
}), }),
'success': True, 'success': True,
'type': 'result', 'type': 'result',
@ -2293,6 +2517,7 @@
]), ]),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
}), }),
'success': True, 'success': True,
'type': 'result', 'type': 'result',
@ -2346,6 +2571,7 @@
]), ]),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
}), }),
'success': True, 'success': True,
'type': 'result', 'type': 'result',
@ -2400,6 +2626,7 @@
]), ]),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
}), }),
'success': True, 'success': True,
'type': 'result', 'type': 'result',
@ -2844,6 +3071,7 @@
]), ]),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
}), }),
'success': True, 'success': True,
'type': 'result', 'type': 'result',
@ -2886,6 +3114,7 @@
]), ]),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
}), }),
'success': True, 'success': True,
'type': 'result', 'type': 'result',
@ -2929,6 +3158,7 @@
]), ]),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
}), }),
'success': True, 'success': True,
'type': 'result', 'type': 'result',
@ -2993,6 +3223,7 @@
]), ]),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
}), }),
'success': True, 'success': True,
'type': 'result', 'type': 'result',
@ -3036,6 +3267,7 @@
]), ]),
'last_attempted_automatic_backup': None, 'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None, 'last_completed_automatic_backup': None,
'next_automatic_backup': None,
}), }),
'success': True, 'success': True,
'type': 'result', 'type': 'result',

View File

@ -274,6 +274,7 @@ async def test_async_initiate_backup(
"agent_errors": {}, "agent_errors": {},
"last_attempted_automatic_backup": None, "last_attempted_automatic_backup": None,
"last_completed_automatic_backup": None, "last_completed_automatic_backup": None,
"next_automatic_backup": None,
} }
await ws_client.send_json_auto_id({"type": "backup/subscribe_events"}) await ws_client.send_json_auto_id({"type": "backup/subscribe_events"})
@ -519,6 +520,7 @@ async def test_async_initiate_backup_with_agent_error(
"agent_errors": {}, "agent_errors": {},
"last_attempted_automatic_backup": None, "last_attempted_automatic_backup": None,
"last_completed_automatic_backup": None, "last_completed_automatic_backup": None,
"next_automatic_backup": None,
} }
await ws_client.send_json_auto_id( await ws_client.send_json_auto_id(
@ -613,6 +615,7 @@ async def test_async_initiate_backup_with_agent_error(
"agent_errors": {}, "agent_errors": {},
"last_attempted_automatic_backup": None, "last_attempted_automatic_backup": None,
"last_completed_automatic_backup": None, "last_completed_automatic_backup": None,
"next_automatic_backup": None,
} }
await hass.async_block_till_done() await hass.async_block_till_done()
@ -880,6 +883,7 @@ async def test_async_initiate_backup_non_agent_upload_error(
"agent_errors": {}, "agent_errors": {},
"last_attempted_automatic_backup": None, "last_attempted_automatic_backup": None,
"last_completed_automatic_backup": None, "last_completed_automatic_backup": None,
"next_automatic_backup": None,
} }
await ws_client.send_json_auto_id({"type": "backup/subscribe_events"}) await ws_client.send_json_auto_id({"type": "backup/subscribe_events"})
@ -990,6 +994,7 @@ async def test_async_initiate_backup_with_task_error(
"agent_errors": {}, "agent_errors": {},
"last_attempted_automatic_backup": None, "last_attempted_automatic_backup": None,
"last_completed_automatic_backup": None, "last_completed_automatic_backup": None,
"next_automatic_backup": None,
} }
await ws_client.send_json_auto_id({"type": "backup/subscribe_events"}) await ws_client.send_json_auto_id({"type": "backup/subscribe_events"})
@ -1094,6 +1099,7 @@ async def test_initiate_backup_file_error(
"agent_errors": {}, "agent_errors": {},
"last_attempted_automatic_backup": None, "last_attempted_automatic_backup": None,
"last_completed_automatic_backup": None, "last_completed_automatic_backup": None,
"next_automatic_backup": None,
} }
await ws_client.send_json_auto_id({"type": "backup/subscribe_events"}) await ws_client.send_json_auto_id({"type": "backup/subscribe_events"})
@ -1614,6 +1620,7 @@ async def test_receive_backup_agent_error(
"agent_errors": {}, "agent_errors": {},
"last_attempted_automatic_backup": None, "last_attempted_automatic_backup": None,
"last_completed_automatic_backup": None, "last_completed_automatic_backup": None,
"next_automatic_backup": None,
} }
await ws_client.send_json_auto_id( await ws_client.send_json_auto_id(
@ -1691,6 +1698,7 @@ async def test_receive_backup_agent_error(
"agent_errors": {}, "agent_errors": {},
"last_attempted_automatic_backup": None, "last_attempted_automatic_backup": None,
"last_completed_automatic_backup": None, "last_completed_automatic_backup": None,
"next_automatic_backup": None,
} }
await hass.async_block_till_done() await hass.async_block_till_done()
@ -1751,6 +1759,7 @@ async def test_receive_backup_non_agent_upload_error(
"agent_errors": {}, "agent_errors": {},
"last_attempted_automatic_backup": None, "last_attempted_automatic_backup": None,
"last_completed_automatic_backup": None, "last_completed_automatic_backup": None,
"next_automatic_backup": None,
} }
await ws_client.send_json_auto_id({"type": "backup/subscribe_events"}) await ws_client.send_json_auto_id({"type": "backup/subscribe_events"})
@ -1871,6 +1880,7 @@ async def test_receive_backup_file_write_error(
"agent_errors": {}, "agent_errors": {},
"last_attempted_automatic_backup": None, "last_attempted_automatic_backup": None,
"last_completed_automatic_backup": None, "last_completed_automatic_backup": None,
"next_automatic_backup": None,
} }
await ws_client.send_json_auto_id({"type": "backup/subscribe_events"}) await ws_client.send_json_auto_id({"type": "backup/subscribe_events"})
@ -1979,6 +1989,7 @@ async def test_receive_backup_read_tar_error(
"agent_errors": {}, "agent_errors": {},
"last_attempted_automatic_backup": None, "last_attempted_automatic_backup": None,
"last_completed_automatic_backup": None, "last_completed_automatic_backup": None,
"next_automatic_backup": None,
} }
await ws_client.send_json_auto_id({"type": "backup/subscribe_events"}) await ws_client.send_json_auto_id({"type": "backup/subscribe_events"})
@ -2146,6 +2157,7 @@ async def test_receive_backup_file_read_error(
"agent_errors": {}, "agent_errors": {},
"last_attempted_automatic_backup": None, "last_attempted_automatic_backup": None,
"last_completed_automatic_backup": None, "last_completed_automatic_backup": None,
"next_automatic_backup": None,
} }
await ws_client.send_json_auto_id({"type": "backup/subscribe_events"}) await ws_client.send_json_auto_id({"type": "backup/subscribe_events"})

View File

@ -0,0 +1,54 @@
"""Tests for the Backup integration."""
from typing import Any
from syrupy import SnapshotAssertion
from homeassistant.components.backup.const import DOMAIN
from homeassistant.core import HomeAssistant
from .common import setup_backup_integration
async def test_store_migration(
hass: HomeAssistant,
hass_storage: dict[str, Any],
snapshot: SnapshotAssertion,
) -> None:
"""Test migrating the backup store."""
hass_storage[DOMAIN] = {
"data": {
"backups": [
{
"backup_id": "abc123",
"failed_agent_ids": ["test.remote"],
}
],
"config": {
"create_backup": {
"agent_ids": [],
"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": {
"copies": None,
"days": None,
},
"schedule": {
"state": "never",
},
},
},
"key": DOMAIN,
"version": 1,
}
await setup_backup_integration(hass)
await hass.async_block_till_done()
assert hass_storage[DOMAIN] == snapshot

View File

@ -14,6 +14,7 @@ from homeassistant.components.backup import (
BackupAgentPlatformProtocol, BackupAgentPlatformProtocol,
BackupReaderWriterError, BackupReaderWriterError,
Folder, Folder,
store,
) )
from homeassistant.components.backup.agent import BackupAgentUnreachableError from homeassistant.components.backup.agent import BackupAgentUnreachableError
from homeassistant.components.backup.const import DATA_MANAGER, DOMAIN from homeassistant.components.backup.const import DATA_MANAGER, DOMAIN
@ -70,9 +71,7 @@ DEFAULT_STORAGE_DATA: dict[str, Any] = {
"copies": None, "copies": None,
"days": None, "days": None,
}, },
"schedule": { "schedule": {"state": "never", "time": None},
"state": "never",
},
}, },
} }
@ -305,7 +304,8 @@ async def test_delete_with_errors(
hass_storage[DOMAIN] = { hass_storage[DOMAIN] = {
"data": storage_data, "data": storage_data,
"key": DOMAIN, "key": DOMAIN,
"version": 1, "version": store.STORAGE_VERSION,
"minor_version": store.STORAGE_VERSION_MINOR,
} }
await setup_backup_integration( await setup_backup_integration(
hass, with_hassio=False, backups={LOCAL_AGENT_ID: [TEST_BACKUP_ABC123]} hass, with_hassio=False, backups={LOCAL_AGENT_ID: [TEST_BACKUP_ABC123]}
@ -924,11 +924,12 @@ async def test_agents_info(
"retention": {"copies": 3, "days": 7}, "retention": {"copies": 3, "days": 7},
"last_attempted_automatic_backup": "2024-10-26T04:45:00+01:00", "last_attempted_automatic_backup": "2024-10-26T04:45:00+01:00",
"last_completed_automatic_backup": "2024-10-26T04:45:00+01:00", "last_completed_automatic_backup": "2024-10-26T04:45:00+01:00",
"schedule": {"state": "daily"}, "schedule": {"state": "daily", "time": None},
}, },
}, },
"key": DOMAIN, "key": DOMAIN,
"version": 1, "version": store.STORAGE_VERSION,
"minor_version": store.STORAGE_VERSION_MINOR,
}, },
}, },
{ {
@ -948,11 +949,12 @@ async def test_agents_info(
"retention": {"copies": 3, "days": None}, "retention": {"copies": 3, "days": None},
"last_attempted_automatic_backup": None, "last_attempted_automatic_backup": None,
"last_completed_automatic_backup": None, "last_completed_automatic_backup": None,
"schedule": {"state": "never"}, "schedule": {"state": "never", "time": None},
}, },
}, },
"key": DOMAIN, "key": DOMAIN,
"version": 1, "version": store.STORAGE_VERSION,
"minor_version": store.STORAGE_VERSION_MINOR,
}, },
}, },
{ {
@ -972,11 +974,12 @@ async def test_agents_info(
"retention": {"copies": None, "days": 7}, "retention": {"copies": None, "days": 7},
"last_attempted_automatic_backup": "2024-10-27T04:45:00+01:00", "last_attempted_automatic_backup": "2024-10-27T04:45:00+01:00",
"last_completed_automatic_backup": "2024-10-26T04:45:00+01:00", "last_completed_automatic_backup": "2024-10-26T04:45:00+01:00",
"schedule": {"state": "never"}, "schedule": {"state": "never", "time": None},
}, },
}, },
"key": DOMAIN, "key": DOMAIN,
"version": 1, "version": store.STORAGE_VERSION,
"minor_version": store.STORAGE_VERSION_MINOR,
}, },
}, },
{ {
@ -996,11 +999,12 @@ async def test_agents_info(
"retention": {"copies": None, "days": None}, "retention": {"copies": None, "days": None},
"last_attempted_automatic_backup": None, "last_attempted_automatic_backup": None,
"last_completed_automatic_backup": None, "last_completed_automatic_backup": None,
"schedule": {"state": "mon"}, "schedule": {"state": "mon", "time": None},
}, },
}, },
"key": DOMAIN, "key": DOMAIN,
"version": 1, "version": store.STORAGE_VERSION,
"minor_version": store.STORAGE_VERSION_MINOR,
}, },
}, },
{ {
@ -1020,30 +1024,35 @@ async def test_agents_info(
"retention": {"copies": None, "days": None}, "retention": {"copies": None, "days": None},
"last_attempted_automatic_backup": None, "last_attempted_automatic_backup": None,
"last_completed_automatic_backup": None, "last_completed_automatic_backup": None,
"schedule": {"state": "sat"}, "schedule": {"state": "sat", "time": None},
}, },
}, },
"key": DOMAIN, "key": DOMAIN,
"version": 1, "version": store.STORAGE_VERSION,
"minor_version": store.STORAGE_VERSION_MINOR,
}, },
}, },
], ],
) )
@patch("homeassistant.components.backup.config.random.randint", Mock(return_value=600))
async def test_config_info( async def test_config_info(
hass: HomeAssistant, hass: HomeAssistant,
hass_ws_client: WebSocketGenerator, hass_ws_client: WebSocketGenerator,
freezer: FrozenDateTimeFactory,
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
hass_storage: dict[str, Any], hass_storage: dict[str, Any],
storage_data: dict[str, Any] | None, storage_data: dict[str, Any] | None,
) -> None: ) -> None:
"""Test getting backup config info.""" """Test getting backup config info."""
client = await hass_ws_client(hass)
await hass.config.async_set_time_zone("Europe/Amsterdam")
freezer.move_to("2024-11-13T12:01:00+01:00")
hass_storage.update(storage_data) hass_storage.update(storage_data)
await setup_backup_integration(hass) await setup_backup_integration(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
client = await hass_ws_client(hass)
await client.send_json_auto_id({"type": "backup/config/info"}) await client.send_json_auto_id({"type": "backup/config/info"})
assert await client.receive_json() == snapshot assert await client.receive_json() == snapshot
@ -1060,17 +1069,17 @@ async def test_config_info(
{ {
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]}, "create_backup": {"agent_ids": ["test-agent"]},
"schedule": "daily", "schedule": {"state": "daily", "time": "06:00"},
}, },
{ {
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]}, "create_backup": {"agent_ids": ["test-agent"]},
"schedule": "mon", "schedule": {"state": "mon"},
}, },
{ {
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]}, "create_backup": {"agent_ids": ["test-agent"]},
"schedule": "never", "schedule": {"state": "never"},
}, },
{ {
"type": "backup/config/update", "type": "backup/config/update",
@ -1081,59 +1090,63 @@ async def test_config_info(
"name": "test-name", "name": "test-name",
"password": "test-password", "password": "test-password",
}, },
"schedule": "daily", "schedule": {"state": "daily"},
}, },
{ {
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]}, "create_backup": {"agent_ids": ["test-agent"]},
"retention": {"copies": 3, "days": 7}, "retention": {"copies": 3, "days": 7},
"schedule": "daily", "schedule": {"state": "daily"},
}, },
{ {
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]}, "create_backup": {"agent_ids": ["test-agent"]},
"retention": {"copies": None, "days": None}, "retention": {"copies": None, "days": None},
"schedule": "daily", "schedule": {"state": "daily"},
}, },
{ {
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]}, "create_backup": {"agent_ids": ["test-agent"]},
"retention": {"copies": 3, "days": None}, "retention": {"copies": 3, "days": None},
"schedule": "daily", "schedule": {"state": "daily"},
}, },
{ {
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]}, "create_backup": {"agent_ids": ["test-agent"]},
"retention": {"copies": None, "days": 7}, "retention": {"copies": None, "days": 7},
"schedule": "daily", "schedule": {"state": "daily"},
}, },
{ {
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]}, "create_backup": {"agent_ids": ["test-agent"]},
"retention": {"copies": 3}, "retention": {"copies": 3},
"schedule": "daily", "schedule": {"state": "daily"},
}, },
{ {
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]}, "create_backup": {"agent_ids": ["test-agent"]},
"retention": {"days": 7}, "retention": {"days": 7},
"schedule": "daily", "schedule": {"state": "daily"},
}, },
], ],
) )
@patch("homeassistant.components.backup.config.random.randint", Mock(return_value=600))
async def test_config_update( async def test_config_update(
hass: HomeAssistant, hass: HomeAssistant,
hass_ws_client: WebSocketGenerator, hass_ws_client: WebSocketGenerator,
freezer: FrozenDateTimeFactory,
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
command: dict[str, Any], command: dict[str, Any],
hass_storage: dict[str, Any], hass_storage: dict[str, Any],
) -> None: ) -> None:
"""Test updating the backup config.""" """Test updating the backup config."""
client = await hass_ws_client(hass)
await hass.config.async_set_time_zone("Europe/Amsterdam")
freezer.move_to("2024-11-13T12:01:00+01:00")
await setup_backup_integration(hass) await setup_backup_integration(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
client = await hass_ws_client(hass)
await client.send_json_auto_id({"type": "backup/config/info"}) await client.send_json_auto_id({"type": "backup/config/info"})
assert await client.receive_json() == snapshot assert await client.receive_json() == snapshot
@ -1146,6 +1159,11 @@ async def test_config_update(
assert await client.receive_json() == snapshot assert await client.receive_json() == snapshot
await hass.async_block_till_done() await hass.async_block_till_done()
# Trigger store write
freezer.tick(60)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass_storage[DOMAIN] == snapshot assert hass_storage[DOMAIN] == snapshot
@ -1156,7 +1174,17 @@ async def test_config_update(
{ {
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]}, "create_backup": {"agent_ids": ["test-agent"]},
"schedule": "someday", "schedule": "blah",
},
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]},
"schedule": {"state": "someday"},
},
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]},
"schedule": {"time": "early"},
}, },
{ {
"type": "backup/config/update", "type": "backup/config/update",
@ -1205,6 +1233,7 @@ async def test_config_update_errors(
"time_2", "time_2",
"attempted_backup_time", "attempted_backup_time",
"completed_backup_time", "completed_backup_time",
"scheduled_backup_time",
"backup_calls_1", "backup_calls_1",
"backup_calls_2", "backup_calls_2",
"call_args", "call_args",
@ -1215,10 +1244,11 @@ async def test_config_update_errors(
# No config update # No config update
[], [],
"2024-11-11T04:45:00+01:00", "2024-11-11T04:45:00+01:00",
"2024-11-12T04:45:00+01:00", "2024-11-12T04:55:00+01:00",
"2024-11-13T04:45:00+01:00", "2024-11-13T04:55:00+01:00",
"2024-11-12T04:45:00+01:00", "2024-11-12T04:55:00+01:00",
"2024-11-12T04:45:00+01:00", "2024-11-12T04:55:00+01:00",
"2024-11-12T04:55:00+01:00",
1, 1,
2, 2,
BACKUP_CALL, BACKUP_CALL,
@ -1230,14 +1260,15 @@ async def test_config_update_errors(
{ {
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]}, "create_backup": {"agent_ids": ["test.test-agent"]},
"schedule": "daily", "schedule": {"state": "daily"},
} }
], ],
"2024-11-11T04:45:00+01:00", "2024-11-11T04:45:00+01:00",
"2024-11-12T04:45:00+01:00", "2024-11-12T04:55:00+01:00",
"2024-11-13T04:45:00+01:00", "2024-11-13T04:55:00+01:00",
"2024-11-12T04:45:00+01:00", "2024-11-12T04:55:00+01:00",
"2024-11-12T04:45:00+01:00", "2024-11-12T04:55:00+01:00",
"2024-11-12T04:55:00+01:00",
1, 1,
2, 2,
BACKUP_CALL, BACKUP_CALL,
@ -1248,14 +1279,15 @@ async def test_config_update_errors(
{ {
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]}, "create_backup": {"agent_ids": ["test.test-agent"]},
"schedule": "mon", "schedule": {"state": "mon"},
} }
], ],
"2024-11-11T04:45:00+01:00", "2024-11-11T04:45:00+01:00",
"2024-11-18T04:45:00+01:00", "2024-11-18T04:55:00+01:00",
"2024-11-25T04:45:00+01:00", "2024-11-25T04:55:00+01:00",
"2024-11-18T04:45:00+01:00", "2024-11-18T04:55:00+01:00",
"2024-11-18T04:45:00+01:00", "2024-11-18T04:55:00+01:00",
"2024-11-18T04:55:00+01:00",
1, 1,
2, 2,
BACKUP_CALL, BACKUP_CALL,
@ -1266,7 +1298,45 @@ async def test_config_update_errors(
{ {
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]}, "create_backup": {"agent_ids": ["test.test-agent"]},
"schedule": "never", "schedule": {"state": "mon", "time": "03:45"},
}
],
"2024-11-11T03:45:00+01:00",
"2024-11-18T03:45:00+01:00",
"2024-11-25T03:45:00+01:00",
"2024-11-18T03:45:00+01:00",
"2024-11-18T03:45:00+01:00",
"2024-11-18T03:45:00+01:00",
1,
2,
BACKUP_CALL,
None,
),
(
[
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]},
"schedule": {"state": "daily", "time": "03:45"},
}
],
"2024-11-11T03:45:00+01:00",
"2024-11-12T03:45:00+01:00",
"2024-11-13T03:45:00+01:00",
"2024-11-12T03:45:00+01:00",
"2024-11-12T03:45:00+01:00",
"2024-11-12T03:45:00+01:00",
1,
2,
BACKUP_CALL,
None,
),
(
[
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]},
"schedule": {"state": "never"},
} }
], ],
"2024-11-11T04:45:00+01:00", "2024-11-11T04:45:00+01:00",
@ -1274,6 +1344,7 @@ async def test_config_update_errors(
"2034-11-11T13:00:00+01:00", "2034-11-11T13:00:00+01:00",
"2024-11-11T04:45:00+01:00", "2024-11-11T04:45:00+01:00",
"2024-11-11T04:45:00+01:00", "2024-11-11T04:45:00+01:00",
None,
0, 0,
0, 0,
None, None,
@ -1284,14 +1355,15 @@ async def test_config_update_errors(
{ {
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]}, "create_backup": {"agent_ids": ["test.test-agent"]},
"schedule": "daily", "schedule": {"state": "daily"},
} }
], ],
"2024-10-26T04:45:00+01:00", "2024-10-26T04:45:00+01:00",
"2024-11-12T04:45:00+01:00", "2024-11-12T04:55:00+01:00",
"2024-11-13T04:45:00+01:00", "2024-11-13T04:55:00+01:00",
"2024-11-12T04:45:00+01:00", "2024-11-12T04:55:00+01:00",
"2024-11-12T04:45:00+01:00", "2024-11-12T04:55:00+01:00",
"2024-11-12T04:55:00+01:00",
1, 1,
2, 2,
BACKUP_CALL, BACKUP_CALL,
@ -1302,14 +1374,15 @@ async def test_config_update_errors(
{ {
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]}, "create_backup": {"agent_ids": ["test.test-agent"]},
"schedule": "mon", "schedule": {"state": "mon"},
} }
], ],
"2024-10-26T04:45:00+01:00", "2024-10-26T04:45:00+01:00",
"2024-11-12T04:45:00+01:00", "2024-11-12T04:55:00+01:00",
"2024-11-13T04:45:00+01:00", "2024-11-13T04:55:00+01:00",
"2024-11-12T04:45:00+01:00", # missed event uses daily schedule once "2024-11-12T04:55:00+01:00", # missed event uses daily schedule once
"2024-11-12T04:45:00+01:00", # missed event uses daily schedule once "2024-11-12T04:55:00+01:00", # missed event uses daily schedule once
"2024-11-12T04:55:00+01:00",
1, 1,
1, 1,
BACKUP_CALL, BACKUP_CALL,
@ -1320,7 +1393,7 @@ async def test_config_update_errors(
{ {
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]}, "create_backup": {"agent_ids": ["test.test-agent"]},
"schedule": "never", "schedule": {"state": "never"},
} }
], ],
"2024-10-26T04:45:00+01:00", "2024-10-26T04:45:00+01:00",
@ -1328,6 +1401,7 @@ async def test_config_update_errors(
"2034-11-12T12:00:00+01:00", "2034-11-12T12:00:00+01:00",
"2024-10-26T04:45:00+01:00", "2024-10-26T04:45:00+01:00",
"2024-10-26T04:45:00+01:00", "2024-10-26T04:45:00+01:00",
None,
0, 0,
0, 0,
None, None,
@ -1338,14 +1412,15 @@ async def test_config_update_errors(
{ {
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]}, "create_backup": {"agent_ids": ["test.test-agent"]},
"schedule": "daily", "schedule": {"state": "daily"},
} }
], ],
"2024-11-11T04:45:00+01:00", "2024-11-11T04:45:00+01:00",
"2024-11-12T04:45:00+01:00", "2024-11-12T04:55:00+01:00",
"2024-11-13T04:45:00+01:00", "2024-11-13T04:55:00+01:00",
"2024-11-12T04:45:00+01:00", # attempted to create backup but failed "2024-11-12T04:55:00+01:00", # attempted to create backup but failed
"2024-11-11T04:45:00+01:00", "2024-11-11T04:45:00+01:00",
"2024-11-12T04:55:00+01:00",
1, 1,
2, 2,
BACKUP_CALL, BACKUP_CALL,
@ -1356,14 +1431,15 @@ async def test_config_update_errors(
{ {
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]}, "create_backup": {"agent_ids": ["test.test-agent"]},
"schedule": "daily", "schedule": {"state": "daily"},
} }
], ],
"2024-11-11T04:45:00+01:00", "2024-11-11T04:45:00+01:00",
"2024-11-12T04:45:00+01:00", "2024-11-12T04:55:00+01:00",
"2024-11-13T04:45:00+01:00", "2024-11-13T04:55:00+01:00",
"2024-11-12T04:45:00+01:00", # attempted to create backup but failed "2024-11-12T04:55:00+01:00", # attempted to create backup but failed
"2024-11-11T04:45:00+01:00", "2024-11-11T04:45:00+01:00",
"2024-11-12T04:55:00+01:00",
1, 1,
2, 2,
BACKUP_CALL, BACKUP_CALL,
@ -1371,7 +1447,7 @@ async def test_config_update_errors(
), ),
], ],
) )
@patch("homeassistant.components.backup.config.BACKUP_START_TIME_JITTER", 0) @patch("homeassistant.components.backup.config.random.randint", Mock(return_value=600))
async def test_config_schedule_logic( async def test_config_schedule_logic(
hass: HomeAssistant, hass: HomeAssistant,
hass_ws_client: WebSocketGenerator, hass_ws_client: WebSocketGenerator,
@ -1384,6 +1460,7 @@ async def test_config_schedule_logic(
time_2: str, time_2: str,
attempted_backup_time: str, attempted_backup_time: str,
completed_backup_time: str, completed_backup_time: str,
scheduled_backup_time: str,
backup_calls_1: int, backup_calls_1: int,
backup_calls_2: int, backup_calls_2: int,
call_args: Any, call_args: Any,
@ -1406,13 +1483,14 @@ async def test_config_schedule_logic(
"retention": {"copies": None, "days": None}, "retention": {"copies": None, "days": None},
"last_attempted_automatic_backup": last_completed_automatic_backup, "last_attempted_automatic_backup": last_completed_automatic_backup,
"last_completed_automatic_backup": last_completed_automatic_backup, "last_completed_automatic_backup": last_completed_automatic_backup,
"schedule": {"state": "daily"}, "schedule": {"state": "daily", "time": None},
}, },
} }
hass_storage[DOMAIN] = { hass_storage[DOMAIN] = {
"data": storage_data, "data": storage_data,
"key": DOMAIN, "key": DOMAIN,
"version": 1, "version": store.STORAGE_VERSION,
"minor_version": store.STORAGE_VERSION_MINOR,
} }
create_backup.side_effect = create_backup_side_effect create_backup.side_effect = create_backup_side_effect
await hass.config.async_set_time_zone("Europe/Amsterdam") await hass.config.async_set_time_zone("Europe/Amsterdam")
@ -1426,6 +1504,10 @@ async def test_config_schedule_logic(
result = await client.receive_json() result = await client.receive_json()
assert result["success"] assert result["success"]
await client.send_json_auto_id({"type": "backup/info"})
result = await client.receive_json()
assert result["result"]["next_automatic_backup"] == scheduled_backup_time
freezer.move_to(time_1) freezer.move_to(time_1)
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -1471,7 +1553,7 @@ async def test_config_schedule_logic(
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]}, "create_backup": {"agent_ids": ["test.test-agent"]},
"retention": {"copies": None, "days": None}, "retention": {"copies": None, "days": None},
"schedule": "daily", "schedule": {"state": "daily"},
}, },
{ {
"backup-1": MagicMock( "backup-1": MagicMock(
@ -1510,7 +1592,7 @@ async def test_config_schedule_logic(
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]}, "create_backup": {"agent_ids": ["test.test-agent"]},
"retention": {"copies": 3, "days": None}, "retention": {"copies": 3, "days": None},
"schedule": "daily", "schedule": {"state": "daily"},
}, },
{ {
"backup-1": MagicMock( "backup-1": MagicMock(
@ -1549,7 +1631,7 @@ async def test_config_schedule_logic(
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]}, "create_backup": {"agent_ids": ["test.test-agent"]},
"retention": {"copies": 3, "days": None}, "retention": {"copies": 3, "days": None},
"schedule": "daily", "schedule": {"state": "daily"},
}, },
{ {
"backup-1": MagicMock( "backup-1": MagicMock(
@ -1578,7 +1660,7 @@ async def test_config_schedule_logic(
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]}, "create_backup": {"agent_ids": ["test.test-agent"]},
"retention": {"copies": 3, "days": None}, "retention": {"copies": 3, "days": None},
"schedule": "daily", "schedule": {"state": "daily"},
}, },
{ {
"backup-1": MagicMock( "backup-1": MagicMock(
@ -1622,7 +1704,7 @@ async def test_config_schedule_logic(
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]}, "create_backup": {"agent_ids": ["test.test-agent"]},
"retention": {"copies": 2, "days": None}, "retention": {"copies": 2, "days": None},
"schedule": "daily", "schedule": {"state": "daily"},
}, },
{ {
"backup-1": MagicMock( "backup-1": MagicMock(
@ -1666,7 +1748,7 @@ async def test_config_schedule_logic(
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]}, "create_backup": {"agent_ids": ["test.test-agent"]},
"retention": {"copies": 2, "days": None}, "retention": {"copies": 2, "days": None},
"schedule": "daily", "schedule": {"state": "daily"},
}, },
{ {
"backup-1": MagicMock( "backup-1": MagicMock(
@ -1705,7 +1787,7 @@ async def test_config_schedule_logic(
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]}, "create_backup": {"agent_ids": ["test.test-agent"]},
"retention": {"copies": 2, "days": None}, "retention": {"copies": 2, "days": None},
"schedule": "daily", "schedule": {"state": "daily"},
}, },
{ {
"backup-1": MagicMock( "backup-1": MagicMock(
@ -1744,7 +1826,7 @@ async def test_config_schedule_logic(
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]}, "create_backup": {"agent_ids": ["test.test-agent"]},
"retention": {"copies": 0, "days": None}, "retention": {"copies": 0, "days": None},
"schedule": "daily", "schedule": {"state": "daily"},
}, },
{ {
"backup-1": MagicMock( "backup-1": MagicMock(
@ -1788,7 +1870,7 @@ async def test_config_schedule_logic(
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]}, "create_backup": {"agent_ids": ["test.test-agent"]},
"retention": {"copies": 0, "days": None}, "retention": {"copies": 0, "days": None},
"schedule": "daily", "schedule": {"state": "daily"},
}, },
{ {
"backup-1": MagicMock( "backup-1": MagicMock(
@ -1852,13 +1934,14 @@ async def test_config_retention_copies_logic(
"retention": {"copies": None, "days": None}, "retention": {"copies": None, "days": None},
"last_attempted_automatic_backup": None, "last_attempted_automatic_backup": None,
"last_completed_automatic_backup": last_backup_time, "last_completed_automatic_backup": last_backup_time,
"schedule": {"state": "daily"}, "schedule": {"state": "daily", "time": None},
}, },
} }
hass_storage[DOMAIN] = { hass_storage[DOMAIN] = {
"data": storage_data, "data": storage_data,
"key": DOMAIN, "key": DOMAIN,
"version": 1, "version": store.STORAGE_VERSION,
"minor_version": store.STORAGE_VERSION_MINOR,
} }
get_backups.return_value = (backups, get_backups_agent_errors) get_backups.return_value = (backups, get_backups_agent_errors)
delete_backup.return_value = delete_backup_agent_errors delete_backup.return_value = delete_backup_agent_errors
@ -1922,7 +2005,7 @@ async def test_config_retention_copies_logic(
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]}, "create_backup": {"agent_ids": ["test.test-agent"]},
"retention": {"copies": None, "days": None}, "retention": {"copies": None, "days": None},
"schedule": "never", "schedule": {"state": "never"},
}, },
{ {
"backup-1": MagicMock( "backup-1": MagicMock(
@ -1958,7 +2041,7 @@ async def test_config_retention_copies_logic(
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]}, "create_backup": {"agent_ids": ["test.test-agent"]},
"retention": {"copies": 3, "days": None}, "retention": {"copies": 3, "days": None},
"schedule": "never", "schedule": {"state": "never"},
}, },
{ {
"backup-1": MagicMock( "backup-1": MagicMock(
@ -1994,7 +2077,7 @@ async def test_config_retention_copies_logic(
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]}, "create_backup": {"agent_ids": ["test.test-agent"]},
"retention": {"copies": 3, "days": None}, "retention": {"copies": 3, "days": None},
"schedule": "never", "schedule": {"state": "never"},
}, },
{ {
"backup-1": MagicMock( "backup-1": MagicMock(
@ -2035,7 +2118,7 @@ async def test_config_retention_copies_logic(
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]}, "create_backup": {"agent_ids": ["test.test-agent"]},
"retention": {"copies": 2, "days": None}, "retention": {"copies": 2, "days": None},
"schedule": "never", "schedule": {"state": "never"},
}, },
{ {
"backup-1": MagicMock( "backup-1": MagicMock(
@ -2109,13 +2192,14 @@ async def test_config_retention_copies_logic_manual_backup(
"retention": {"copies": None, "days": None}, "retention": {"copies": None, "days": None},
"last_attempted_automatic_backup": None, "last_attempted_automatic_backup": None,
"last_completed_automatic_backup": None, "last_completed_automatic_backup": None,
"schedule": {"state": "daily"}, "schedule": {"state": "daily", "time": None},
}, },
} }
hass_storage[DOMAIN] = { hass_storage[DOMAIN] = {
"data": storage_data, "data": storage_data,
"key": DOMAIN, "key": DOMAIN,
"version": 1, "version": store.STORAGE_VERSION,
"minor_version": store.STORAGE_VERSION_MINOR,
} }
get_backups.return_value = (backups, get_backups_agent_errors) get_backups.return_value = (backups, get_backups_agent_errors)
delete_backup.return_value = delete_backup_agent_errors delete_backup.return_value = delete_backup_agent_errors
@ -2236,7 +2320,7 @@ async def test_config_retention_copies_logic_manual_backup(
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]}, "create_backup": {"agent_ids": ["test-agent"]},
"retention": {"copies": None, "days": 2}, "retention": {"copies": None, "days": 2},
"schedule": "never", "schedule": {"state": "never"},
} }
], ],
{ {
@ -2272,7 +2356,7 @@ async def test_config_retention_copies_logic_manual_backup(
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]}, "create_backup": {"agent_ids": ["test-agent"]},
"retention": {"copies": None, "days": 2}, "retention": {"copies": None, "days": 2},
"schedule": "never", "schedule": {"state": "never"},
} }
], ],
{ {
@ -2308,7 +2392,7 @@ async def test_config_retention_copies_logic_manual_backup(
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]}, "create_backup": {"agent_ids": ["test-agent"]},
"retention": {"copies": None, "days": 3}, "retention": {"copies": None, "days": 3},
"schedule": "never", "schedule": {"state": "never"},
} }
], ],
{ {
@ -2344,7 +2428,7 @@ async def test_config_retention_copies_logic_manual_backup(
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]}, "create_backup": {"agent_ids": ["test-agent"]},
"retention": {"copies": None, "days": 2}, "retention": {"copies": None, "days": 2},
"schedule": "never", "schedule": {"state": "never"},
} }
], ],
{ {
@ -2385,7 +2469,7 @@ async def test_config_retention_copies_logic_manual_backup(
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]}, "create_backup": {"agent_ids": ["test-agent"]},
"retention": {"copies": None, "days": 2}, "retention": {"copies": None, "days": 2},
"schedule": "never", "schedule": {"state": "never"},
} }
], ],
{ {
@ -2421,7 +2505,7 @@ async def test_config_retention_copies_logic_manual_backup(
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]}, "create_backup": {"agent_ids": ["test-agent"]},
"retention": {"copies": None, "days": 2}, "retention": {"copies": None, "days": 2},
"schedule": "never", "schedule": {"state": "never"},
} }
], ],
{ {
@ -2457,7 +2541,7 @@ async def test_config_retention_copies_logic_manual_backup(
"type": "backup/config/update", "type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]}, "create_backup": {"agent_ids": ["test-agent"]},
"retention": {"copies": None, "days": 0}, "retention": {"copies": None, "days": 0},
"schedule": "never", "schedule": {"state": "never"},
} }
], ],
{ {
@ -2529,13 +2613,14 @@ async def test_config_retention_days_logic(
"retention": {"copies": None, "days": stored_retained_days}, "retention": {"copies": None, "days": stored_retained_days},
"last_attempted_automatic_backup": None, "last_attempted_automatic_backup": None,
"last_completed_automatic_backup": last_backup_time, "last_completed_automatic_backup": last_backup_time,
"schedule": {"state": "never"}, "schedule": {"state": "never", "time": None},
}, },
} }
hass_storage[DOMAIN] = { hass_storage[DOMAIN] = {
"data": storage_data, "data": storage_data,
"key": DOMAIN, "key": DOMAIN,
"version": 1, "version": store.STORAGE_VERSION,
"minor_version": store.STORAGE_VERSION_MINOR,
} }
get_backups.return_value = (backups, get_backups_agent_errors) get_backups.return_value = (backups, get_backups_agent_errors)
delete_backup.return_value = delete_backup_agent_errors delete_backup.return_value = delete_backup_agent_errors

View File

@ -204,6 +204,7 @@ async def test_agents_list_backups_fail_cloud(
"backups": [], "backups": [],
"last_attempted_automatic_backup": None, "last_attempted_automatic_backup": None,
"last_completed_automatic_backup": None, "last_completed_automatic_backup": None,
"next_automatic_backup": None,
} }