diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 9cfc1c95d8b..e25bfbe358c 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -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) diff --git a/homeassistant/components/backup/__init__.py b/homeassistant/components/backup/__init__.py index a5159086945..d9d1c3cc2fe 100644 --- a/homeassistant/components/backup/__init__.py +++ b/homeassistant/components/backup/__init__.py @@ -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] diff --git a/homeassistant/components/backup/basic_websocket.py b/homeassistant/components/backup/basic_websocket.py new file mode 100644 index 00000000000..614dc23a927 --- /dev/null +++ b/homeassistant/components/backup/basic_websocket.py @@ -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"]) diff --git a/homeassistant/components/backup/manager.py b/homeassistant/components/backup/manager.py index 0f79cd79e0c..3bf31618b24 100644 --- a/homeassistant/components/backup/manager.py +++ b/homeassistant/components/backup/manager.py @@ -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( diff --git a/homeassistant/components/backup/websocket.py b/homeassistant/components/backup/websocket.py index 5084f904ec6..8b5f35287dd 100644 --- a/homeassistant/components/backup/websocket.py +++ b/homeassistant/components/backup/websocket.py @@ -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"]) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 499e1fbddb2..b13b33685d5 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -1,7 +1,6 @@ { "domain": "frontend", "name": "Home Assistant Frontend", - "after_dependencies": ["backup"], "codeowners": ["@home-assistant/frontend"], "dependencies": [ "api", diff --git a/homeassistant/components/hassio/backup.py b/homeassistant/components/hassio/backup.py index e7d169c142c..fe69b9e08e5 100644 --- a/homeassistant/components/hassio/backup.py +++ b/homeassistant/components/hassio/backup.py @@ -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: diff --git a/homeassistant/components/onboarding/manifest.json b/homeassistant/components/onboarding/manifest.json index 3634894cd00..a4cf814eb2a 100644 --- a/homeassistant/components/onboarding/manifest.json +++ b/homeassistant/components/onboarding/manifest.json @@ -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", diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index b392c6b57b0..a590588c009 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -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"}, diff --git a/homeassistant/helpers/backup.py b/homeassistant/helpers/backup.py new file mode 100644 index 00000000000..4ab302749a1 --- /dev/null +++ b/homeassistant/helpers/backup.py @@ -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 diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index d29571eaa83..368c2f762b8 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -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"), } diff --git a/tests/components/azure_storage/test_backup.py b/tests/components/azure_storage/test_backup.py index 4dc1de0a26e..7c5912a4981 100644 --- a/tests/components/azure_storage/test_backup.py +++ b/tests/components/azure_storage/test_backup.py @@ -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) diff --git a/tests/components/backup/common.py b/tests/components/backup/common.py index b21698bf365..e41da5c1bad 100644 --- a/tests/components/backup/common.py +++ b/tests/components/backup/common.py @@ -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( diff --git a/tests/components/backup/snapshots/test_websocket.ambr b/tests/components/backup/snapshots/test_websocket.ambr index c100a87e8cc..17e3ca8b176 100644 --- a/tests/components/backup/snapshots/test_websocket.ambr +++ b/tests/components/backup/snapshots/test_websocket.ambr @@ -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', + }) +# --- diff --git a/tests/components/backup/test_backup.py b/tests/components/backup/test_backup.py index 38b61ce65ea..c9d797f4e30 100644 --- a/tests/components/backup/test_backup.py +++ b/tests/components/backup/test_backup.py @@ -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) diff --git a/tests/components/backup/test_websocket.py b/tests/components/backup/test_websocket.py index 6605674a679..9b2241882c4 100644 --- a/tests/components/backup/test_websocket.py +++ b/tests/components/backup/test_websocket.py @@ -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"), [ diff --git a/tests/components/cloud/test_backup.py b/tests/components/cloud/test_backup.py index 18793cc00bb..5220d3eccd5 100644 --- a/tests/components/cloud/test_backup.py +++ b/tests/components/cloud/test_backup.py @@ -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), diff --git a/tests/components/google_drive/test_backup.py b/tests/components/google_drive/test_backup.py index 70431e2049f..2da397def5b 100644 --- a/tests/components/google_drive/test_backup.py +++ b/tests/components/google_drive/test_backup.py @@ -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( diff --git a/tests/components/hassio/test_backup.py b/tests/components/hassio/test_backup.py index c7f400cef5c..6e4fe4dd428 100644 --- a/tests/components/hassio/test_backup.py +++ b/tests/components/hassio/test_backup.py @@ -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() diff --git a/tests/components/hassio/test_update.py b/tests/components/hassio/test_update.py index 83af302e1ce..a3718454538 100644 --- a/tests/components/hassio/test_update.py +++ b/tests/components/hassio/test_update.py @@ -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 diff --git a/tests/components/hassio/test_websocket_api.py b/tests/components/hassio/test_websocket_api.py index e752b53ae7a..b695cc1794a 100644 --- a/tests/components/hassio/test_websocket_api.py +++ b/tests/components/hassio/test_websocket_api.py @@ -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 diff --git a/tests/components/kitchen_sink/test_backup.py b/tests/components/kitchen_sink/test_backup.py index 7c693abcda8..933979ee913 100644 --- a/tests/components/kitchen_sink/test_backup.py +++ b/tests/components/kitchen_sink/test_backup.py @@ -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: {}}) diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 08d21a13331..b7189bda6cc 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -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() diff --git a/tests/components/onedrive/test_backup.py b/tests/components/onedrive/test_backup.py index c307e5190c1..a81eb03a51c 100644 --- a/tests/components/onedrive/test_backup.py +++ b/tests/components/onedrive/test_backup.py @@ -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), diff --git a/tests/components/synology_dsm/test_backup.py b/tests/components/synology_dsm/test_backup.py index 8e98f4dffa9..24cfe29f52b 100644 --- a/tests/components/synology_dsm/test_backup.py +++ b/tests/components/synology_dsm/test_backup.py @@ -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() diff --git a/tests/components/webdav/test_backup.py b/tests/components/webdav/test_backup.py index b02fb2e9628..2219e92f700 100644 --- a/tests/components/webdav/test_backup.py +++ b/tests/components/webdav/test_backup.py @@ -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) diff --git a/tests/helpers/test_backup.py b/tests/helpers/test_backup.py new file mode 100644 index 00000000000..10ff5cb855f --- /dev/null +++ b/tests/helpers/test_backup.py @@ -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)