Skip to content

Commit bf190a8

Browse files
emontnemerybdraco
andauthored
Add backup helper (#139199)
* Add backup helper * Add hassio to stage 1 * Apply same changes to newly merged `webdav` and `azure_storage` to fix inflight conflict * Address comments, add tests --------- Co-authored-by: J. Nick Koston <nick@koston.org>
1 parent c386abd commit bf190a8

File tree

27 files changed

+289
-96
lines changed

27 files changed

+289
-96
lines changed

homeassistant/bootstrap.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
from .exceptions import HomeAssistantError
7575
from .helpers import (
7676
area_registry,
77+
backup,
7778
category_registry,
7879
config_validation as cv,
7980
device_registry,
@@ -163,16 +164,6 @@
163164
# integrations can be removed and database migration status is
164165
# visible in frontend
165166
"frontend",
166-
# Hassio is an after dependency of backup, after dependencies
167-
# are not promoted from stage 2 to earlier stages, so we need to
168-
# add it here. Hassio needs to be setup before backup, otherwise
169-
# the backup integration will think we are a container/core install
170-
# when using HAOS or Supervised install.
171-
"hassio",
172-
# Backup is an after dependency of frontend, after dependencies
173-
# are not promoted from stage 2 to earlier stages, so we need to
174-
# add it here.
175-
"backup",
176167
}
177168
# Stage 0 is divided into substages. Each substage has a name, a set of integrations and a timeout.
178169
# The substage containing recorder should have no timeout, as it could cancel a database migration.
@@ -206,6 +197,8 @@
206197
"mqtt_eventstream",
207198
# To provide account link implementations
208199
"cloud",
200+
# Ensure supervisor is available
201+
"hassio",
209202
}
210203

211204
DEFAULT_INTEGRATIONS = {
@@ -905,6 +898,10 @@ async def _async_set_up_integrations(
905898
if "recorder" in domains_to_setup:
906899
recorder.async_initialize_recorder(hass)
907900

901+
# Initialize backup
902+
if "backup" in domains_to_setup:
903+
backup.async_initialize_backup(hass)
904+
908905
stage_0_and_1_domains: list[tuple[str, set[str], int | None]] = [
909906
*(
910907
(name, domain_group & domains_to_setup, timeout)

homeassistant/components/backup/__init__.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""The Backup integration."""
22

3-
from homeassistant.core import HomeAssistant, ServiceCall, callback
4-
from homeassistant.exceptions import HomeAssistantError
3+
from homeassistant.core import HomeAssistant, ServiceCall
54
from homeassistant.helpers import config_validation as cv
5+
from homeassistant.helpers.backup import DATA_BACKUP
66
from homeassistant.helpers.hassio import is_hassio
77
from homeassistant.helpers.typing import ConfigType
88

@@ -32,6 +32,7 @@
3232
IdleEvent,
3333
IncorrectPasswordError,
3434
ManagerBackup,
35+
ManagerStateEvent,
3536
NewBackup,
3637
RestoreBackupEvent,
3738
RestoreBackupStage,
@@ -63,12 +64,12 @@
6364
"IncorrectPasswordError",
6465
"LocalBackupAgent",
6566
"ManagerBackup",
67+
"ManagerStateEvent",
6668
"NewBackup",
6769
"RestoreBackupEvent",
6870
"RestoreBackupStage",
6971
"RestoreBackupState",
7072
"WrittenBackup",
71-
"async_get_manager",
7273
"suggested_filename",
7374
"suggested_filename_from_name_date",
7475
]
@@ -91,7 +92,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
9192

9293
backup_manager = BackupManager(hass, reader_writer)
9394
hass.data[DATA_MANAGER] = backup_manager
94-
await backup_manager.async_setup()
95+
try:
96+
await backup_manager.async_setup()
97+
except Exception as err:
98+
hass.data[DATA_BACKUP].manager_ready.set_exception(err)
99+
raise
100+
else:
101+
hass.data[DATA_BACKUP].manager_ready.set_result(None)
95102

96103
async_register_websocket_handlers(hass, with_hassio)
97104

@@ -122,15 +129,3 @@ async def async_handle_create_automatic_service(call: ServiceCall) -> None:
122129
async_register_http_views(hass)
123130

124131
return True
125-
126-
127-
@callback
128-
def async_get_manager(hass: HomeAssistant) -> BackupManager:
129-
"""Get the backup manager instance.
130-
131-
Raises HomeAssistantError if the backup integration is not available.
132-
"""
133-
if DATA_MANAGER not in hass.data:
134-
raise HomeAssistantError("Backup integration is not available")
135-
136-
return hass.data[DATA_MANAGER]
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Websocket commands for the Backup integration."""
2+
3+
from typing import Any
4+
5+
import voluptuous as vol
6+
7+
from homeassistant.components import websocket_api
8+
from homeassistant.core import HomeAssistant, callback
9+
from homeassistant.helpers.backup import async_subscribe_events
10+
11+
from .const import DATA_MANAGER
12+
from .manager import ManagerStateEvent
13+
14+
15+
@callback
16+
def async_register_websocket_handlers(hass: HomeAssistant) -> None:
17+
"""Register websocket commands."""
18+
websocket_api.async_register_command(hass, handle_subscribe_events)
19+
20+
21+
@websocket_api.require_admin
22+
@websocket_api.websocket_command({vol.Required("type"): "backup/subscribe_events"})
23+
@websocket_api.async_response
24+
async def handle_subscribe_events(
25+
hass: HomeAssistant,
26+
connection: websocket_api.ActiveConnection,
27+
msg: dict[str, Any],
28+
) -> None:
29+
"""Subscribe to backup events."""
30+
31+
def on_event(event: ManagerStateEvent) -> None:
32+
connection.send_message(websocket_api.event_message(msg["id"], event))
33+
34+
if DATA_MANAGER in hass.data:
35+
manager = hass.data[DATA_MANAGER]
36+
on_event(manager.last_event)
37+
connection.subscriptions[msg["id"]] = async_subscribe_events(hass, on_event)
38+
connection.send_result(msg["id"])

homeassistant/components/backup/manager.py

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
integration_platform,
3434
issue_registry as ir,
3535
)
36+
from homeassistant.helpers.backup import DATA_BACKUP
3637
from homeassistant.helpers.json import json_bytes
3738
from homeassistant.util import dt as dt_util, json as json_util
3839

@@ -332,7 +333,9 @@ def __init__(self, hass: HomeAssistant, reader_writer: BackupReaderWriter) -> No
332333
# Latest backup event and backup event subscribers
333334
self.last_event: ManagerStateEvent = IdleEvent()
334335
self.last_non_idle_event: ManagerStateEvent | None = None
335-
self._backup_event_subscriptions: list[Callable[[ManagerStateEvent], None]] = []
336+
self._backup_event_subscriptions = hass.data[
337+
DATA_BACKUP
338+
].backup_event_subscriptions
336339

337340
async def async_setup(self) -> None:
338341
"""Set up the backup manager."""
@@ -1279,19 +1282,6 @@ def async_on_backup_event(
12791282
for subscription in self._backup_event_subscriptions:
12801283
subscription(event)
12811284

1282-
@callback
1283-
def async_subscribe_events(
1284-
self,
1285-
on_event: Callable[[ManagerStateEvent], None],
1286-
) -> Callable[[], None]:
1287-
"""Subscribe events."""
1288-
1289-
def remove_subscription() -> None:
1290-
self._backup_event_subscriptions.remove(on_event)
1291-
1292-
self._backup_event_subscriptions.append(on_event)
1293-
return remove_subscription
1294-
12951285
def _update_issue_backup_failed(self) -> None:
12961286
"""Update issue registry when a backup fails."""
12971287
ir.async_create_issue(

homeassistant/components/backup/websocket.py

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,7 @@
1010

1111
from .config import Day, ScheduleRecurrence
1212
from .const import DATA_MANAGER, LOGGER
13-
from .manager import (
14-
DecryptOnDowloadNotSupported,
15-
IncorrectPasswordError,
16-
ManagerStateEvent,
17-
)
13+
from .manager import DecryptOnDowloadNotSupported, IncorrectPasswordError
1814
from .models import BackupNotFound, Folder
1915

2016

@@ -34,7 +30,6 @@ def async_register_websocket_handlers(hass: HomeAssistant, with_hassio: bool) ->
3430
websocket_api.async_register_command(hass, handle_create_with_automatic_settings)
3531
websocket_api.async_register_command(hass, handle_delete)
3632
websocket_api.async_register_command(hass, handle_restore)
37-
websocket_api.async_register_command(hass, handle_subscribe_events)
3833

3934
websocket_api.async_register_command(hass, handle_config_info)
4035
websocket_api.async_register_command(hass, handle_config_update)
@@ -401,22 +396,3 @@ def handle_config_update(
401396
changes.pop("type")
402397
manager.config.update(**changes)
403398
connection.send_result(msg["id"])
404-
405-
406-
@websocket_api.require_admin
407-
@websocket_api.websocket_command({vol.Required("type"): "backup/subscribe_events"})
408-
@websocket_api.async_response
409-
async def handle_subscribe_events(
410-
hass: HomeAssistant,
411-
connection: websocket_api.ActiveConnection,
412-
msg: dict[str, Any],
413-
) -> None:
414-
"""Subscribe to backup events."""
415-
416-
def on_event(event: ManagerStateEvent) -> None:
417-
connection.send_message(websocket_api.event_message(msg["id"], event))
418-
419-
manager = hass.data[DATA_MANAGER]
420-
on_event(manager.last_event)
421-
connection.subscriptions[msg["id"]] = manager.async_subscribe_events(on_event)
422-
connection.send_result(msg["id"])

homeassistant/components/frontend/manifest.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
{
22
"domain": "frontend",
33
"name": "Home Assistant Frontend",
4-
"after_dependencies": ["backup"],
54
"codeowners": ["@home-assistant/frontend"],
65
"dependencies": [
76
"api",

homeassistant/components/hassio/backup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,13 @@
4545
RestoreBackupStage,
4646
RestoreBackupState,
4747
WrittenBackup,
48-
async_get_manager as async_get_backup_manager,
4948
suggested_filename as suggested_backup_filename,
5049
suggested_filename_from_name_date,
5150
)
5251
from homeassistant.const import __version__ as HAVERSION
5352
from homeassistant.core import HomeAssistant, callback
5453
from homeassistant.exceptions import HomeAssistantError
54+
from homeassistant.helpers.backup import async_get_manager as async_get_backup_manager
5555
from homeassistant.helpers.dispatcher import async_dispatcher_connect
5656
from homeassistant.util import dt as dt_util
5757
from homeassistant.util.enum import try_parse_enum
@@ -751,7 +751,7 @@ def addon_update_backup_filter(
751751

752752
async def backup_core_before_update(hass: HomeAssistant) -> None:
753753
"""Prepare for updating core."""
754-
backup_manager = async_get_backup_manager(hass)
754+
backup_manager = await async_get_backup_manager(hass)
755755
client = get_supervisor_client(hass)
756756

757757
try:

homeassistant/components/onboarding/manifest.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
{
22
"domain": "onboarding",
33
"name": "Home Assistant Onboarding",
4-
"after_dependencies": ["backup"],
54
"codeowners": ["@home-assistant/core"],
65
"dependencies": ["auth", "http", "person"],
76
"documentation": "https://www.home-assistant.io/integrations/onboarding",

homeassistant/components/onboarding/views.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
BackupManager,
2121
Folder,
2222
IncorrectPasswordError,
23-
async_get_manager as async_get_backup_manager,
2423
http as backup_http,
2524
)
2625
from homeassistant.components.http import KEY_HASS, KEY_HASS_REFRESH_TOKEN_ID
@@ -29,6 +28,7 @@
2928
from homeassistant.core import HomeAssistant, callback
3029
from homeassistant.exceptions import HomeAssistantError
3130
from homeassistant.helpers import area_registry as ar
31+
from homeassistant.helpers.backup import async_get_manager as async_get_backup_manager
3232
from homeassistant.helpers.system_info import async_get_system_info
3333
from homeassistant.helpers.translation import async_get_translations
3434
from homeassistant.setup import async_setup_component
@@ -341,7 +341,7 @@ async def with_backup(
341341
raise HTTPUnauthorized
342342

343343
try:
344-
manager = async_get_backup_manager(request.app[KEY_HASS])
344+
manager = await async_get_backup_manager(request.app[KEY_HASS])
345345
except HomeAssistantError:
346346
return self.json(
347347
{"code": "backup_disabled"},

homeassistant/helpers/backup.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""Helpers for the backup integration."""
2+
3+
from __future__ import annotations
4+
5+
import asyncio
6+
from collections.abc import Callable
7+
from dataclasses import dataclass, field
8+
from typing import TYPE_CHECKING
9+
10+
from homeassistant.core import HomeAssistant, callback
11+
from homeassistant.exceptions import HomeAssistantError
12+
from homeassistant.util.hass_dict import HassKey
13+
14+
if TYPE_CHECKING:
15+
from homeassistant.components.backup import BackupManager, ManagerStateEvent
16+
17+
DATA_BACKUP: HassKey[BackupData] = HassKey("backup_data")
18+
DATA_MANAGER: HassKey[BackupManager] = HassKey("backup")
19+
20+
21+
@dataclass(slots=True)
22+
class BackupData:
23+
"""Backup data stored in hass.data."""
24+
25+
backup_event_subscriptions: list[Callable[[ManagerStateEvent], None]] = field(
26+
default_factory=list
27+
)
28+
manager_ready: asyncio.Future[None] = field(default_factory=asyncio.Future)
29+
30+
31+
@callback
32+
def async_initialize_backup(hass: HomeAssistant) -> None:
33+
"""Initialize backup data.
34+
35+
This creates the BackupData instance stored in hass.data[DATA_BACKUP] and
36+
registers the basic backup websocket API which is used by frontend to subscribe
37+
to backup events.
38+
"""
39+
# pylint: disable-next=import-outside-toplevel
40+
from homeassistant.components.backup import basic_websocket
41+
42+
hass.data[DATA_BACKUP] = BackupData()
43+
basic_websocket.async_register_websocket_handlers(hass)
44+
45+
46+
async def async_get_manager(hass: HomeAssistant) -> BackupManager:
47+
"""Get the backup manager instance.
48+
49+
Raises HomeAssistantError if the backup integration is not available.
50+
"""
51+
if DATA_BACKUP not in hass.data:
52+
raise HomeAssistantError("Backup integration is not available")
53+
54+
await hass.data[DATA_BACKUP].manager_ready
55+
return hass.data[DATA_MANAGER]
56+
57+
58+
@callback
59+
def async_subscribe_events(
60+
hass: HomeAssistant,
61+
on_event: Callable[[ManagerStateEvent], None],
62+
) -> Callable[[], None]:
63+
"""Subscribe to backup events."""
64+
backup_event_subscriptions = hass.data[DATA_BACKUP].backup_event_subscriptions
65+
66+
def remove_subscription() -> None:
67+
backup_event_subscriptions.remove(on_event)
68+
69+
backup_event_subscriptions.append(on_event)
70+
return remove_subscription

0 commit comments

Comments
 (0)