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>
This commit is contained in:
Erik Montnemery 2025-02-25 10:19:41 +01:00 committed by GitHub
parent c386abd49d
commit bf190a8a73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 289 additions and 96 deletions

View File

@ -74,6 +74,7 @@ from .core_config import async_process_ha_core_config
from .exceptions import HomeAssistantError
from .helpers import (
area_registry,
backup,
category_registry,
config_validation as cv,
device_registry,
@ -163,16 +164,6 @@ FRONTEND_INTEGRATIONS = {
# integrations can be removed and database migration status is
# visible in frontend
"frontend",
# Hassio is an after dependency of backup, after dependencies
# are not promoted from stage 2 to earlier stages, so we need to
# add it here. Hassio needs to be setup before backup, otherwise
# the backup integration will think we are a container/core install
# when using HAOS or Supervised install.
"hassio",
# Backup is an after dependency of frontend, after dependencies
# are not promoted from stage 2 to earlier stages, so we need to
# add it here.
"backup",
}
# Stage 0 is divided into substages. Each substage has a name, a set of integrations and a timeout.
# The substage containing recorder should have no timeout, as it could cancel a database migration.
@ -206,6 +197,8 @@ STAGE_1_INTEGRATIONS = {
"mqtt_eventstream",
# To provide account link implementations
"cloud",
# Ensure supervisor is available
"hassio",
}
DEFAULT_INTEGRATIONS = {
@ -905,6 +898,10 @@ async def _async_set_up_integrations(
if "recorder" in domains_to_setup:
recorder.async_initialize_recorder(hass)
# Initialize backup
if "backup" in domains_to_setup:
backup.async_initialize_backup(hass)
stage_0_and_1_domains: list[tuple[str, set[str], int | None]] = [
*(
(name, domain_group & domains_to_setup, timeout)

View File

@ -1,8 +1,8 @@
"""The Backup integration."""
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.backup import DATA_BACKUP
from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.typing import ConfigType
@ -32,6 +32,7 @@ from .manager import (
IdleEvent,
IncorrectPasswordError,
ManagerBackup,
ManagerStateEvent,
NewBackup,
RestoreBackupEvent,
RestoreBackupStage,
@ -63,12 +64,12 @@ __all__ = [
"IncorrectPasswordError",
"LocalBackupAgent",
"ManagerBackup",
"ManagerStateEvent",
"NewBackup",
"RestoreBackupEvent",
"RestoreBackupStage",
"RestoreBackupState",
"WrittenBackup",
"async_get_manager",
"suggested_filename",
"suggested_filename_from_name_date",
]
@ -91,7 +92,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
backup_manager = BackupManager(hass, reader_writer)
hass.data[DATA_MANAGER] = backup_manager
await backup_manager.async_setup()
try:
await backup_manager.async_setup()
except Exception as err:
hass.data[DATA_BACKUP].manager_ready.set_exception(err)
raise
else:
hass.data[DATA_BACKUP].manager_ready.set_result(None)
async_register_websocket_handlers(hass, with_hassio)
@ -122,15 +129,3 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async_register_http_views(hass)
return True
@callback
def async_get_manager(hass: HomeAssistant) -> BackupManager:
"""Get the backup manager instance.
Raises HomeAssistantError if the backup integration is not available.
"""
if DATA_MANAGER not in hass.data:
raise HomeAssistantError("Backup integration is not available")
return hass.data[DATA_MANAGER]

View File

@ -0,0 +1,38 @@
"""Websocket commands for the Backup integration."""
from typing import Any
import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.backup import async_subscribe_events
from .const import DATA_MANAGER
from .manager import ManagerStateEvent
@callback
def async_register_websocket_handlers(hass: HomeAssistant) -> None:
"""Register websocket commands."""
websocket_api.async_register_command(hass, handle_subscribe_events)
@websocket_api.require_admin
@websocket_api.websocket_command({vol.Required("type"): "backup/subscribe_events"})
@websocket_api.async_response
async def handle_subscribe_events(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Subscribe to backup events."""
def on_event(event: ManagerStateEvent) -> None:
connection.send_message(websocket_api.event_message(msg["id"], event))
if DATA_MANAGER in hass.data:
manager = hass.data[DATA_MANAGER]
on_event(manager.last_event)
connection.subscriptions[msg["id"]] = async_subscribe_events(hass, on_event)
connection.send_result(msg["id"])

View File

@ -33,6 +33,7 @@ from homeassistant.helpers import (
integration_platform,
issue_registry as ir,
)
from homeassistant.helpers.backup import DATA_BACKUP
from homeassistant.helpers.json import json_bytes
from homeassistant.util import dt as dt_util, json as json_util
@ -332,7 +333,9 @@ class BackupManager:
# Latest backup event and backup event subscribers
self.last_event: ManagerStateEvent = IdleEvent()
self.last_non_idle_event: ManagerStateEvent | None = None
self._backup_event_subscriptions: list[Callable[[ManagerStateEvent], None]] = []
self._backup_event_subscriptions = hass.data[
DATA_BACKUP
].backup_event_subscriptions
async def async_setup(self) -> None:
"""Set up the backup manager."""
@ -1279,19 +1282,6 @@ class BackupManager:
for subscription in self._backup_event_subscriptions:
subscription(event)
@callback
def async_subscribe_events(
self,
on_event: Callable[[ManagerStateEvent], None],
) -> Callable[[], None]:
"""Subscribe events."""
def remove_subscription() -> None:
self._backup_event_subscriptions.remove(on_event)
self._backup_event_subscriptions.append(on_event)
return remove_subscription
def _update_issue_backup_failed(self) -> None:
"""Update issue registry when a backup fails."""
ir.async_create_issue(

View File

@ -10,11 +10,7 @@ from homeassistant.helpers import config_validation as cv
from .config import Day, ScheduleRecurrence
from .const import DATA_MANAGER, LOGGER
from .manager import (
DecryptOnDowloadNotSupported,
IncorrectPasswordError,
ManagerStateEvent,
)
from .manager import DecryptOnDowloadNotSupported, IncorrectPasswordError
from .models import BackupNotFound, Folder
@ -34,7 +30,6 @@ def async_register_websocket_handlers(hass: HomeAssistant, with_hassio: bool) ->
websocket_api.async_register_command(hass, handle_create_with_automatic_settings)
websocket_api.async_register_command(hass, handle_delete)
websocket_api.async_register_command(hass, handle_restore)
websocket_api.async_register_command(hass, handle_subscribe_events)
websocket_api.async_register_command(hass, handle_config_info)
websocket_api.async_register_command(hass, handle_config_update)
@ -401,22 +396,3 @@ def handle_config_update(
changes.pop("type")
manager.config.update(**changes)
connection.send_result(msg["id"])
@websocket_api.require_admin
@websocket_api.websocket_command({vol.Required("type"): "backup/subscribe_events"})
@websocket_api.async_response
async def handle_subscribe_events(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Subscribe to backup events."""
def on_event(event: ManagerStateEvent) -> None:
connection.send_message(websocket_api.event_message(msg["id"], event))
manager = hass.data[DATA_MANAGER]
on_event(manager.last_event)
connection.subscriptions[msg["id"]] = manager.async_subscribe_events(on_event)
connection.send_result(msg["id"])

View File

@ -1,7 +1,6 @@
{
"domain": "frontend",
"name": "Home Assistant Frontend",
"after_dependencies": ["backup"],
"codeowners": ["@home-assistant/frontend"],
"dependencies": [
"api",

View File

@ -45,13 +45,13 @@ from homeassistant.components.backup import (
RestoreBackupStage,
RestoreBackupState,
WrittenBackup,
async_get_manager as async_get_backup_manager,
suggested_filename as suggested_backup_filename,
suggested_filename_from_name_date,
)
from homeassistant.const import __version__ as HAVERSION
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.backup import async_get_manager as async_get_backup_manager
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util import dt as dt_util
from homeassistant.util.enum import try_parse_enum
@ -751,7 +751,7 @@ async def backup_addon_before_update(
async def backup_core_before_update(hass: HomeAssistant) -> None:
"""Prepare for updating core."""
backup_manager = async_get_backup_manager(hass)
backup_manager = await async_get_backup_manager(hass)
client = get_supervisor_client(hass)
try:

View File

@ -1,7 +1,6 @@
{
"domain": "onboarding",
"name": "Home Assistant Onboarding",
"after_dependencies": ["backup"],
"codeowners": ["@home-assistant/core"],
"dependencies": ["auth", "http", "person"],
"documentation": "https://www.home-assistant.io/integrations/onboarding",

View File

@ -20,7 +20,6 @@ from homeassistant.components.backup import (
BackupManager,
Folder,
IncorrectPasswordError,
async_get_manager as async_get_backup_manager,
http as backup_http,
)
from homeassistant.components.http import KEY_HASS, KEY_HASS_REFRESH_TOKEN_ID
@ -29,6 +28,7 @@ from homeassistant.components.http.view import HomeAssistantView
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import area_registry as ar
from homeassistant.helpers.backup import async_get_manager as async_get_backup_manager
from homeassistant.helpers.system_info import async_get_system_info
from homeassistant.helpers.translation import async_get_translations
from homeassistant.setup import async_setup_component
@ -341,7 +341,7 @@ def with_backup_manager[_ViewT: BackupOnboardingView, **_P](
raise HTTPUnauthorized
try:
manager = async_get_backup_manager(request.app[KEY_HASS])
manager = await async_get_backup_manager(request.app[KEY_HASS])
except HomeAssistantError:
return self.json(
{"code": "backup_disabled"},

View File

@ -0,0 +1,70 @@
"""Helpers for the backup integration."""
from __future__ import annotations
import asyncio
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.util.hass_dict import HassKey
if TYPE_CHECKING:
from homeassistant.components.backup import BackupManager, ManagerStateEvent
DATA_BACKUP: HassKey[BackupData] = HassKey("backup_data")
DATA_MANAGER: HassKey[BackupManager] = HassKey("backup")
@dataclass(slots=True)
class BackupData:
"""Backup data stored in hass.data."""
backup_event_subscriptions: list[Callable[[ManagerStateEvent], None]] = field(
default_factory=list
)
manager_ready: asyncio.Future[None] = field(default_factory=asyncio.Future)
@callback
def async_initialize_backup(hass: HomeAssistant) -> None:
"""Initialize backup data.
This creates the BackupData instance stored in hass.data[DATA_BACKUP] and
registers the basic backup websocket API which is used by frontend to subscribe
to backup events.
"""
# pylint: disable-next=import-outside-toplevel
from homeassistant.components.backup import basic_websocket
hass.data[DATA_BACKUP] = BackupData()
basic_websocket.async_register_websocket_handlers(hass)
async def async_get_manager(hass: HomeAssistant) -> BackupManager:
"""Get the backup manager instance.
Raises HomeAssistantError if the backup integration is not available.
"""
if DATA_BACKUP not in hass.data:
raise HomeAssistantError("Backup integration is not available")
await hass.data[DATA_BACKUP].manager_ready
return hass.data[DATA_MANAGER]
@callback
def async_subscribe_events(
hass: HomeAssistant,
on_event: Callable[[ManagerStateEvent], None],
) -> Callable[[], None]:
"""Subscribe to backup events."""
backup_event_subscriptions = hass.data[DATA_BACKUP].backup_event_subscriptions
def remove_subscription() -> None:
backup_event_subscriptions.remove(on_event)
backup_event_subscriptions.append(on_event)
return remove_subscription

View File

@ -175,6 +175,10 @@ IGNORE_VIOLATIONS = {
"logbook",
# Temporary needed for migration until 2024.10
("conversation", "assist_pipeline"),
# The onboarding integration provides a limited backup API used during
# onboarding. The onboarding integration waits for the backup manager
# to be ready before calling any backup functionality.
("onboarding", "backup"),
}

View File

@ -19,6 +19,7 @@ from homeassistant.components.azure_storage.const import (
)
from homeassistant.components.backup import DOMAIN as BACKUP_DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers.backup import async_initialize_backup
from homeassistant.setup import async_setup_component
from . import setup_integration
@ -38,6 +39,7 @@ async def setup_backup_integration(
patch("homeassistant.components.backup.is_hassio", return_value=False),
patch("homeassistant.components.backup.store.STORE_DELAY_SAVE", 0),
):
async_initialize_backup(hass)
assert await async_setup_component(hass, BACKUP_DOMAIN, {})
await setup_integration(hass, mock_config_entry)

View File

@ -18,6 +18,7 @@ from homeassistant.components.backup import (
)
from homeassistant.components.backup.const import DATA_MANAGER
from homeassistant.core import HomeAssistant
from homeassistant.helpers.backup import async_initialize_backup
from homeassistant.setup import async_setup_component
from tests.common import mock_platform
@ -125,6 +126,7 @@ async def setup_backup_integration(
) -> dict[str, Mock]:
"""Set up the Backup integration."""
backups = backups or {}
async_initialize_backup(hass)
with (
patch("homeassistant.components.backup.is_hassio", return_value=with_hassio),
patch(

View File

@ -5768,3 +5768,20 @@
'type': 'event',
})
# ---
# name: test_subscribe_event_early
dict({
'event': dict({
'manager_state': 'idle',
}),
'id': 1,
'type': 'event',
})
# ---
# name: test_subscribe_event_early.1
dict({
'id': 1,
'result': None,
'success': True,
'type': 'result',
})
# ---

View File

@ -14,6 +14,7 @@ from syrupy import SnapshotAssertion
from homeassistant.components.backup import DOMAIN, AgentBackup
from homeassistant.core import HomeAssistant
from homeassistant.helpers.backup import async_initialize_backup
from homeassistant.setup import async_setup_component
from .common import (
@ -63,6 +64,7 @@ async def test_load_backups(
side_effect: Exception | None,
) -> None:
"""Test load backups."""
async_initialize_backup(hass)
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
client = await hass_ws_client(hass)
@ -82,6 +84,7 @@ async def test_upload(
hass_client: ClientSessionGenerator,
) -> None:
"""Test upload backup."""
async_initialize_backup(hass)
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
client = await hass_client()
@ -137,6 +140,7 @@ async def test_delete_backup(
unlink_path: Path | None,
) -> None:
"""Test delete backup."""
async_initialize_backup(hass)
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
client = await hass_ws_client(hass)

View File

@ -27,6 +27,8 @@ from homeassistant.components.backup.manager import (
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.backup import async_initialize_backup
from homeassistant.setup import async_setup_component
from .common import (
LOCAL_AGENT_ID,
@ -3264,6 +3266,29 @@ async def test_subscribe_event(
assert await client.receive_json() == snapshot
async def test_subscribe_event_early(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
snapshot: SnapshotAssertion,
) -> None:
"""Test subscribe event before backup integration has started."""
async_initialize_backup(hass)
await setup_backup_integration(hass, with_hassio=False)
client = await hass_ws_client(hass)
await client.send_json_auto_id({"type": "backup/subscribe_events"})
assert await client.receive_json() == snapshot
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
manager = hass.data[DATA_MANAGER]
manager.async_on_backup_event(
CreateBackupEvent(stage=None, state=CreateBackupState.IN_PROGRESS, reason=None)
)
assert await client.receive_json() == snapshot
@pytest.mark.parametrize(
("agent_id", "backup_id", "password"),
[

View File

@ -21,6 +21,7 @@ from homeassistant.components.cloud import DOMAIN
from homeassistant.components.cloud.backup import async_register_backup_agents_listener
from homeassistant.components.cloud.const import EVENT_CLOUD_EVENT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.backup import async_initialize_backup
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.setup import async_setup_component
from homeassistant.util.aiohttp import MockStreamReader
@ -44,7 +45,8 @@ async def setup_integration(
cloud: MagicMock,
cloud_logged_in: None,
) -> AsyncGenerator[None]:
"""Set up cloud integration."""
"""Set up cloud and backup integrations."""
async_initialize_backup(hass)
with (
patch("homeassistant.components.backup.is_hassio", return_value=False),
patch("homeassistant.components.backup.store.STORE_DELAY_SAVE", 0),

View File

@ -17,6 +17,7 @@ from homeassistant.components.backup import (
)
from homeassistant.components.google_drive import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers.backup import async_initialize_backup
from homeassistant.setup import async_setup_component
from .conftest import CONFIG_ENTRY_TITLE, TEST_AGENT_ID
@ -63,7 +64,8 @@ async def setup_integration(
config_entry: MockConfigEntry,
mock_api: MagicMock,
) -> None:
"""Set up Google Drive integration."""
"""Set up Google Drive and backup integrations."""
async_initialize_backup(hass)
config_entry.add_to_hass(hass)
assert await async_setup_component(hass, BACKUP_DOMAIN, {BACKUP_DOMAIN: {}})
mock_api.list_files = AsyncMock(

View File

@ -44,6 +44,7 @@ from homeassistant.components.backup import (
from homeassistant.components.hassio import DOMAIN
from homeassistant.components.hassio.backup import RESTORE_JOB_ID_ENV
from homeassistant.core import HomeAssistant
from homeassistant.helpers.backup import async_initialize_backup
from homeassistant.setup import async_setup_component
from .test_init import MOCK_ENVIRON
@ -320,6 +321,7 @@ async def setup_backup_integration(
hass: HomeAssistant, hassio_enabled: None, supervisor_client: AsyncMock
) -> None:
"""Set up Backup integration."""
async_initialize_backup(hass)
assert await async_setup_component(hass, BACKUP_DOMAIN, {BACKUP_DOMAIN: {}})
await hass.async_block_till_done()
@ -432,6 +434,7 @@ async def test_agent_info(
client = await hass_ws_client(hass)
supervisor_client.mounts.info.return_value = mounts
async_initialize_backup(hass)
assert await async_setup_component(hass, BACKUP_DOMAIN, {BACKUP_DOMAIN: {}})
await client.send_json_auto_id({"type": "backup/agents/info"})
@ -1287,6 +1290,7 @@ async def test_reader_writer_create_per_agent_encryption(
)
supervisor_client.jobs.get_job.return_value = TEST_JOB_NOT_DONE
supervisor_client.mounts.info.return_value = mounts
async_initialize_backup(hass)
assert await async_setup_component(hass, BACKUP_DOMAIN, {BACKUP_DOMAIN: {}})
for command in commands:
@ -2352,6 +2356,7 @@ async def test_restore_progress_after_restart(
supervisor_client.jobs.get_job.return_value = get_job_result
async_initialize_backup(hass)
with patch.dict(os.environ, MOCK_ENVIRON | {RESTORE_JOB_ID_ENV: TEST_JOB_ID}):
assert await async_setup_component(hass, BACKUP_DOMAIN, {BACKUP_DOMAIN: {}})
@ -2375,6 +2380,7 @@ async def test_restore_progress_after_restart_report_progress(
supervisor_client.jobs.get_job.return_value = TEST_JOB_NOT_DONE
async_initialize_backup(hass)
with patch.dict(os.environ, MOCK_ENVIRON | {RESTORE_JOB_ID_ENV: TEST_JOB_ID}):
assert await async_setup_component(hass, BACKUP_DOMAIN, {BACKUP_DOMAIN: {}})
@ -2457,6 +2463,7 @@ async def test_restore_progress_after_restart_unknown_job(
supervisor_client.jobs.get_job.side_effect = SupervisorError
async_initialize_backup(hass)
with patch.dict(os.environ, MOCK_ENVIRON | {RESTORE_JOB_ID_ENV: TEST_JOB_ID}):
assert await async_setup_component(hass, BACKUP_DOMAIN, {BACKUP_DOMAIN: {}})
@ -2556,6 +2563,7 @@ async def test_config_load_config_info(
hass_storage.update(storage_data)
async_initialize_backup(hass)
assert await async_setup_component(hass, BACKUP_DOMAIN, {BACKUP_DOMAIN: {}})
await hass.async_block_till_done()

View File

@ -18,6 +18,7 @@ from homeassistant.components.hassio.const import REQUEST_REFRESH_DELAY
from homeassistant.const import __version__ as HAVERSION
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.backup import async_initialize_backup
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
@ -235,6 +236,13 @@ async def test_update_addon(hass: HomeAssistant, update_addon: AsyncMock) -> Non
update_addon.assert_called_once_with("test", StoreAddonUpdate(backup=False))
async def setup_backup_integration(hass: HomeAssistant) -> None:
"""Set up the backup integration."""
async_initialize_backup(hass)
assert await async_setup_component(hass, "backup", {})
await hass.async_block_till_done()
@pytest.mark.parametrize(
("commands", "default_mount", "expected_kwargs"),
[
@ -318,8 +326,7 @@ async def test_update_addon_with_backup(
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
assert result
assert await async_setup_component(hass, "backup", {})
await hass.async_block_till_done()
await setup_backup_integration(hass)
client = await hass_ws_client(hass)
for command in commands:
@ -413,8 +420,7 @@ async def test_update_addon_with_backup_removes_old_backups(
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
assert result
assert await async_setup_component(hass, "backup", {})
await hass.async_block_till_done()
await setup_backup_integration(hass)
supervisor_client.mounts.info.return_value.default_backup_mount = None
with (
@ -588,8 +594,7 @@ async def test_update_core_with_backup(
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
assert result
assert await async_setup_component(hass, "backup", {})
await hass.async_block_till_done()
await setup_backup_integration(hass)
client = await hass_ws_client(hass)
for command in commands:
@ -691,8 +696,7 @@ async def test_update_addon_with_backup_and_error(
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
assert result
assert await async_setup_component(hass, "backup", {})
await hass.async_block_till_done()
await setup_backup_integration(hass)
supervisor_client.homeassistant.update.return_value = None
supervisor_client.mounts.info.return_value.default_backup_mount = None
@ -811,8 +815,7 @@ async def test_update_core_with_backup_and_error(
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
assert result
assert await async_setup_component(hass, "backup", {})
await hass.async_block_till_done()
await setup_backup_integration(hass)
supervisor_client.homeassistant.update.return_value = None
supervisor_client.mounts.info.return_value.default_backup_mount = None

View File

@ -26,6 +26,7 @@ from homeassistant.components.hassio.const import (
)
from homeassistant.const import __version__ as HAVERSION
from homeassistant.core import HomeAssistant
from homeassistant.helpers.backup import async_initialize_backup
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.setup import async_setup_component
@ -355,6 +356,13 @@ async def test_update_addon(
update_addon.assert_called_once_with("test", StoreAddonUpdate(backup=False))
async def setup_backup_integration(hass: HomeAssistant) -> None:
"""Set up the backup integration."""
async_initialize_backup(hass)
assert await async_setup_component(hass, "backup", {})
await hass.async_block_till_done()
@pytest.mark.parametrize(
("commands", "default_mount", "expected_kwargs"),
[
@ -438,8 +446,7 @@ async def test_update_addon_with_backup(
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
assert result
assert await async_setup_component(hass, "backup", {})
await hass.async_block_till_done()
await setup_backup_integration(hass)
client = await hass_ws_client(hass)
for command in commands:
@ -533,8 +540,7 @@ async def test_update_addon_with_backup_removes_old_backups(
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
assert result
assert await async_setup_component(hass, "backup", {})
await hass.async_block_till_done()
await setup_backup_integration(hass)
client = await hass_ws_client(hass)
supervisor_client.mounts.info.return_value.default_backup_mount = None
@ -686,8 +692,7 @@ async def test_update_core_with_backup(
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
assert result
assert await async_setup_component(hass, "backup", {})
await hass.async_block_till_done()
await setup_backup_integration(hass)
client = await hass_ws_client(hass)
for command in commands:
@ -766,8 +771,7 @@ async def test_update_addon_with_backup_and_error(
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
assert result
assert await async_setup_component(hass, "backup", {})
await hass.async_block_till_done()
await setup_backup_integration(hass)
supervisor_client.homeassistant.update.return_value = None
supervisor_client.mounts.info.return_value.default_backup_mount = None
@ -834,8 +838,7 @@ async def test_update_core_with_backup_and_error(
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
assert result
assert await async_setup_component(hass, "backup", {})
await hass.async_block_till_done()
await setup_backup_integration(hass)
supervisor_client.homeassistant.update.return_value = None
supervisor_client.mounts.info.return_value.default_backup_mount = None

View File

@ -15,6 +15,7 @@ from homeassistant.components.backup import (
from homeassistant.components.kitchen_sink import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import instance_id
from homeassistant.helpers.backup import async_initialize_backup
from homeassistant.setup import async_setup_component
from tests.typing import ClientSessionGenerator, WebSocketGenerator
@ -35,7 +36,8 @@ async def backup_only() -> AsyncGenerator[None]:
@pytest.fixture(autouse=True)
async def setup_integration(hass: HomeAssistant) -> AsyncGenerator[None]:
"""Set up Kitchen Sink integration."""
"""Set up Kitchen Sink and backup integrations."""
async_initialize_backup(hass)
with patch("homeassistant.components.backup.is_hassio", return_value=False):
assert await async_setup_component(hass, BACKUP_DOMAIN, {BACKUP_DOMAIN: {}})
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})

View File

@ -16,6 +16,7 @@ from homeassistant.components.onboarding import const, views
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import area_registry as ar
from homeassistant.helpers.backup import async_initialize_backup
from homeassistant.setup import async_setup_component
from . import mock_storage
@ -765,6 +766,7 @@ async def test_onboarding_backup_info(
mock_storage(hass_storage, {"done": []})
assert await async_setup_component(hass, "onboarding", {})
async_initialize_backup(hass)
assert await async_setup_component(hass, "backup", {})
await hass.async_block_till_done()
@ -881,6 +883,7 @@ async def test_onboarding_backup_restore(
mock_storage(hass_storage, {"done": []})
assert await async_setup_component(hass, "onboarding", {})
async_initialize_backup(hass)
assert await async_setup_component(hass, "backup", {})
await hass.async_block_till_done()
@ -977,6 +980,7 @@ async def test_onboarding_backup_restore_error(
mock_storage(hass_storage, {"done": []})
assert await async_setup_component(hass, "onboarding", {})
async_initialize_backup(hass)
assert await async_setup_component(hass, "backup", {})
await hass.async_block_till_done()
@ -1020,6 +1024,7 @@ async def test_onboarding_backup_restore_unexpected_error(
mock_storage(hass_storage, {"done": []})
assert await async_setup_component(hass, "onboarding", {})
async_initialize_backup(hass)
assert await async_setup_component(hass, "backup", {})
await hass.async_block_till_done()
@ -1045,6 +1050,7 @@ async def test_onboarding_backup_upload(
mock_storage(hass_storage, {"done": []})
assert await async_setup_component(hass, "onboarding", {})
async_initialize_backup(hass)
assert await async_setup_component(hass, "backup", {})
await hass.async_block_till_done()

View File

@ -21,6 +21,7 @@ from homeassistant.components.onedrive.backup import (
from homeassistant.components.onedrive.const import DATA_BACKUP_AGENT_LISTENERS, DOMAIN
from homeassistant.config_entries import SOURCE_REAUTH
from homeassistant.core import HomeAssistant
from homeassistant.helpers.backup import async_initialize_backup
from homeassistant.setup import async_setup_component
from . import setup_integration
@ -35,7 +36,8 @@ async def setup_backup_integration(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> AsyncGenerator[None]:
"""Set up onedrive integration."""
"""Set up onedrive and backup integrations."""
async_initialize_backup(hass)
with (
patch("homeassistant.components.backup.is_hassio", return_value=False),
patch("homeassistant.components.backup.store.STORE_DELAY_SAVE", 0),

View File

@ -28,6 +28,7 @@ from homeassistant.const import (
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.backup import async_initialize_backup
from homeassistant.setup import async_setup_component
from homeassistant.util.aiohttp import MockStreamReader
@ -164,7 +165,8 @@ async def setup_dsm_with_filestation(
hass: HomeAssistant,
mock_dsm_with_filestation: MagicMock,
):
"""Mock setup of synology dsm config entry."""
"""Mock setup of synology dsm config entry and backup integration."""
async_initialize_backup(hass)
with (
patch(
"homeassistant.components.synology_dsm.common.SynologyDSM",
@ -222,6 +224,7 @@ async def test_agents_not_loaded(
) -> None:
"""Test backup agent with no loaded config entry."""
with patch("homeassistant.components.backup.is_hassio", return_value=False):
async_initialize_backup(hass)
assert await async_setup_component(hass, BACKUP_DOMAIN, {BACKUP_DOMAIN: {}})
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
await hass.async_block_till_done()

View File

@ -13,6 +13,7 @@ from homeassistant.components.backup import DOMAIN as BACKUP_DOMAIN, AgentBackup
from homeassistant.components.webdav.backup import async_register_backup_agents_listener
from homeassistant.components.webdav.const import DATA_BACKUP_AGENT_LISTENERS, DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers.backup import async_initialize_backup
from homeassistant.setup import async_setup_component
from .const import BACKUP_METADATA, MOCK_LIST_WITH_INFOS
@ -30,6 +31,7 @@ async def setup_backup_integration(
patch("homeassistant.components.backup.is_hassio", return_value=False),
patch("homeassistant.components.backup.store.STORE_DELAY_SAVE", 0),
):
async_initialize_backup(hass)
assert await async_setup_component(hass, BACKUP_DOMAIN, {})
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)

View File

@ -0,0 +1,42 @@
"""The tests for the backup helpers."""
import asyncio
from unittest.mock import patch
import pytest
from homeassistant.components.backup import DOMAIN as BACKUP_DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import backup as backup_helper
from homeassistant.setup import async_setup_component
async def test_async_get_manager(hass: HomeAssistant) -> None:
"""Test async_get_manager."""
backup_helper.async_initialize_backup(hass)
task = asyncio.create_task(backup_helper.async_get_manager(hass))
assert await async_setup_component(hass, BACKUP_DOMAIN, {})
manager = await task
assert manager is hass.data[backup_helper.DATA_MANAGER]
async def test_async_get_manager_no_backup(hass: HomeAssistant) -> None:
"""Test async_get_manager when the backup integration is not enabled."""
with pytest.raises(HomeAssistantError, match="Backup integration is not available"):
await backup_helper.async_get_manager(hass)
async def test_async_get_manager_backup_failed_setup(hass: HomeAssistant) -> None:
"""Test test_async_get_manager when the backup integration can't be set up."""
backup_helper.async_initialize_backup(hass)
with patch(
"homeassistant.components.backup.manager.BackupManager.async_setup",
side_effect=Exception("Boom!"),
):
assert not await async_setup_component(hass, BACKUP_DOMAIN, {})
with (
pytest.raises(Exception, match="Boom!"),
):
await backup_helper.async_get_manager(hass)