diff --git a/homeassistant/components/backup/store.py b/homeassistant/components/backup/store.py index 9b4af823c77..8287080b5a2 100644 --- a/homeassistant/components/backup/store.py +++ b/homeassistant/components/backup/store.py @@ -16,7 +16,7 @@ if TYPE_CHECKING: STORE_DELAY_SAVE = 30 STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -STORAGE_VERSION_MINOR = 3 +STORAGE_VERSION_MINOR = 4 class StoredBackupData(TypedDict): @@ -60,6 +60,13 @@ class _BackupStore(Store[StoredBackupData]): else: data["config"]["schedule"]["days"] = [state] data["config"]["schedule"]["recurrence"] = "custom_days" + if old_minor_version < 4: + # Workaround for a bug in frontend which incorrectly set days to 0 + # instead of to None for unlimited retention. + if data["config"]["retention"]["copies"] == 0: + data["config"]["retention"]["copies"] = None + if data["config"]["retention"]["days"] == 0: + data["config"]["retention"]["days"] = None # Note: We allow reading data with major version 2. # Reject if major version is higher than 2. diff --git a/homeassistant/components/backup/websocket.py b/homeassistant/components/backup/websocket.py index b6d092e1913..8453046cabb 100644 --- a/homeassistant/components/backup/websocket.py +++ b/homeassistant/components/backup/websocket.py @@ -368,8 +368,10 @@ async def handle_config_info( ), vol.Optional("retention"): vol.Schema( { - vol.Optional("copies"): vol.Any(int, None), - vol.Optional("days"): vol.Any(int, None), + # Note: We can't use cv.positive_int because it allows 0 even + # though 0 is not positive. + vol.Optional("copies"): vol.Any(vol.All(int, vol.Range(min=1)), None), + vol.Optional("days"): vol.Any(vol.All(int, vol.Range(min=1)), None), }, ), vol.Optional("schedule"): vol.Schema( diff --git a/tests/components/backup/snapshots/test_store.ambr b/tests/components/backup/snapshots/test_store.ambr index 2fd81d6841a..04f88b84a97 100644 --- a/tests/components/backup/snapshots/test_store.ambr +++ b/tests/components/backup/snapshots/test_store.ambr @@ -39,7 +39,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- @@ -84,11 +84,100 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- # name: test_store_migration[store_data1] + dict({ + 'data': dict({ + 'backups': list([ + dict({ + 'backup_id': 'abc123', + 'failed_agent_ids': list([ + 'test.remote', + ]), + }), + ]), + 'config': dict({ + 'agents': 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({ + 'days': list([ + ]), + 'recurrence': 'never', + 'state': 'never', + 'time': None, + }), + }), + }), + 'key': 'backup', + 'minor_version': 4, + 'version': 1, + }) +# --- +# name: test_store_migration[store_data1].1 + dict({ + 'data': dict({ + 'backups': list([ + dict({ + 'backup_id': 'abc123', + 'failed_agent_ids': list([ + 'test.remote', + ]), + }), + ]), + 'config': dict({ + 'agents': 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': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'state': 'never', + 'time': None, + }), + }), + }), + 'key': 'backup', + 'minor_version': 4, + 'version': 1, + }) +# --- +# name: test_store_migration[store_data2] dict({ 'data': dict({ 'backups': list([ @@ -131,11 +220,11 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- -# name: test_store_migration[store_data1].1 +# name: test_store_migration[store_data2].1 dict({ 'data': dict({ 'backups': list([ @@ -179,7 +268,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- diff --git a/tests/components/backup/snapshots/test_websocket.ambr b/tests/components/backup/snapshots/test_websocket.ambr index 4452d191d5a..19a85de62ad 100644 --- a/tests/components/backup/snapshots/test_websocket.ambr +++ b/tests/components/backup/snapshots/test_websocket.ambr @@ -686,7 +686,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- @@ -800,7 +800,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- @@ -914,7 +914,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- @@ -1038,7 +1038,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- @@ -1205,7 +1205,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- @@ -1319,7 +1319,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- @@ -1435,7 +1435,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- @@ -1549,7 +1549,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- @@ -1667,7 +1667,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- @@ -1789,7 +1789,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- @@ -1903,7 +1903,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- @@ -2017,7 +2017,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- @@ -2131,7 +2131,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- @@ -2245,7 +2245,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- @@ -2323,6 +2323,154 @@ 'type': 'result', }) # --- +# name: test_config_update_errors[command10] + dict({ + 'id': 1, + 'result': dict({ + 'config': dict({ + 'agents': 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, + 'next_automatic_backup_additional': False, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_config_update_errors[command10].1 + dict({ + 'id': 3, + 'result': dict({ + 'config': dict({ + 'agents': 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, + 'next_automatic_backup_additional': False, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_config_update_errors[command11] + dict({ + 'id': 1, + 'result': dict({ + 'config': dict({ + 'agents': 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, + 'next_automatic_backup_additional': False, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_config_update_errors[command11].1 + dict({ + 'id': 3, + 'result': dict({ + 'config': dict({ + 'agents': 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, + 'next_automatic_backup_additional': False, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- # name: test_config_update_errors[command1] dict({ 'id': 1, diff --git a/tests/components/backup/test_store.py b/tests/components/backup/test_store.py index f05afbea9ec..eff53bda777 100644 --- a/tests/components/backup/test_store.py +++ b/tests/components/backup/test_store.py @@ -57,6 +57,38 @@ def mock_delay_save() -> Generator[None]: "key": DOMAIN, "version": 1, }, + { + "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": 0, + "days": 0, + }, + "schedule": { + "state": "never", + }, + }, + }, + "key": DOMAIN, + "version": 1, + }, { "data": { "backups": [ diff --git a/tests/components/backup/test_websocket.py b/tests/components/backup/test_websocket.py index 8632fb1e957..5e9d7f3c70a 100644 --- a/tests/components/backup/test_websocket.py +++ b/tests/components/backup/test_websocket.py @@ -1361,6 +1361,14 @@ async def test_config_update( "type": "backup/config/update", "agents": {"test-agent1": {"favorite": True}}, }, + { + "type": "backup/config/update", + "retention": {"copies": 0}, + }, + { + "type": "backup/config/update", + "retention": {"days": 0}, + }, ], ) async def test_config_update_errors( @@ -2158,7 +2166,7 @@ async def test_config_schedule_logic( { "type": "backup/config/update", "create_backup": {"agent_ids": ["test.test-agent"]}, - "retention": {"copies": 0, "days": None}, + "retention": {"copies": 1, "days": None}, "schedule": {"recurrence": "daily"}, }, { @@ -2232,7 +2240,7 @@ async def test_config_schedule_logic( { "type": "backup/config/update", "create_backup": {"agent_ids": ["test.test-agent"]}, - "retention": {"copies": 0, "days": None}, + "retention": {"copies": 1, "days": None}, "schedule": {"recurrence": "daily"}, }, { @@ -2301,7 +2309,7 @@ async def test_config_schedule_logic( { "type": "backup/config/update", "create_backup": {"agent_ids": ["test.test-agent"]}, - "retention": {"copies": 0, "days": None}, + "retention": {"copies": 1, "days": None}, "schedule": {"recurrence": "daily"}, }, { @@ -3019,7 +3027,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}, + "retention": {"copies": None, "days": 1}, "schedule": {"recurrence": "never"}, } ],