Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 7 additions & 10 deletions homeassistant/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
from .exceptions import HomeAssistantError
from .helpers import (
area_registry,
backup,
category_registry,
config_validation as cv,
device_registry,
Expand Down Expand Up @@ -163,16 +164,6 @@
# 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.
Expand Down Expand Up @@ -206,6 +197,8 @@
"mqtt_eventstream",
# To provide account link implementations
"cloud",
# Ensure supervisor is available
"hassio",
}

DEFAULT_INTEGRATIONS = {
Expand Down Expand Up @@ -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)
Expand Down
27 changes: 11 additions & 16 deletions homeassistant/components/backup/__init__.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -32,6 +32,7 @@
IdleEvent,
IncorrectPasswordError,
ManagerBackup,
ManagerStateEvent,
NewBackup,
RestoreBackupEvent,
RestoreBackupStage,
Expand Down Expand Up @@ -63,12 +64,12 @@
"IncorrectPasswordError",
"LocalBackupAgent",
"ManagerBackup",
"ManagerStateEvent",
"NewBackup",
"RestoreBackupEvent",
"RestoreBackupStage",
"RestoreBackupState",
"WrittenBackup",
"async_get_manager",
"suggested_filename",
"suggested_filename_from_name_date",
]
Expand All @@ -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)

Expand Down Expand Up @@ -122,15 +129,3 @@ async def async_handle_create_automatic_service(call: ServiceCall) -> None:
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]
38 changes: 38 additions & 0 deletions homeassistant/components/backup/basic_websocket.py
Original file line number Diff line number Diff line change
@@ -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"])
18 changes: 4 additions & 14 deletions homeassistant/components/backup/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
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

Expand Down Expand Up @@ -332,7 +333,9 @@ def __init__(self, hass: HomeAssistant, reader_writer: BackupReaderWriter) -> No
# 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."""
Expand Down Expand Up @@ -1279,19 +1282,6 @@ def async_on_backup_event(
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(
Expand Down
26 changes: 1 addition & 25 deletions homeassistant/components/backup/websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,7 @@

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


Expand All @@ -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)
Expand Down Expand Up @@ -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"])
1 change: 0 additions & 1 deletion homeassistant/components/frontend/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"domain": "frontend",
"name": "Home Assistant Frontend",
"after_dependencies": ["backup"],
"codeowners": ["@home-assistant/frontend"],
"dependencies": [
"api",
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/hassio/backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,13 @@
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
Expand Down Expand Up @@ -751,7 +751,7 @@ def addon_update_backup_filter(

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:
Expand Down
1 change: 0 additions & 1 deletion homeassistant/components/onboarding/manifest.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/onboarding/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
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
Expand All @@ -29,6 +28,7 @@
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
Expand Down Expand Up @@ -341,7 +341,7 @@ async def with_backup(
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"},
Expand Down
70 changes: 70 additions & 0 deletions homeassistant/helpers/backup.py
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions script/hassfest/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,10 @@ def visit_Attribute(self, node: ast.Attribute) -> None:
"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"),
}


Expand Down
2 changes: 2 additions & 0 deletions tests/components/azure_storage/test_backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
)
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
Expand All @@ -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)

Expand Down
Loading