Allow to change encryption for each backup location (#23861)

* Add backup location settings page

* Add encryption settings

* Display unencrypted locations and backups

* Improve cloud detail page

* Fix encryption flag

* Fix restore backup

* Fix lint

* Fix translations

* Add warning

* Use agents in backup locations

* Feedback

* Use updated

* Improve encrypted/unencrypted status

* Improve code quality

* Remove hardcoded failed id

* Extend agent interface

* Use willupdate
This commit is contained in:
Paul Bottein 2025-01-29 14:23:47 +01:00 committed by GitHub
parent fcdcbbda05
commit a2f2d64f5c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 640 additions and 133 deletions

View File

@ -57,6 +57,7 @@ export interface BackupConfig {
time?: string | null;
days: BackupDay[];
};
agents: BackupAgentsConfig;
}
export interface BackupMutableConfig {
@ -78,6 +79,13 @@ export interface BackupMutableConfig {
time?: string | null;
days?: BackupDay[] | null;
};
agents?: BackupAgentsConfig;
}
export type BackupAgentsConfig = Record<string, BackupAgentConfig>;
export interface BackupAgentConfig {
protected: boolean;
}
export interface BackupAgent {
@ -85,13 +93,16 @@ export interface BackupAgent {
name: string;
}
export interface BackupContentAgent {
size: number;
protected: boolean;
}
export interface BackupContent {
backup_id: string;
date: string;
name: string;
protected: boolean;
size: number;
agent_ids?: string[];
agents: Record<string, BackupContentAgent>;
failed_agent_ids?: string[];
with_automatic_settings: boolean;
}
@ -305,6 +316,9 @@ export const computeBackupAgentName = (
return showName ? `${domainName}: ${name}` : domainName;
};
export const computeBackupSize = (backup: BackupContent) =>
Math.max(...Object.values(backup.agents).map((agent) => agent.size));
export const compareAgents = (a: string, b: string) => {
const isLocalA = isLocalAgent(a);
const isLocalB = isLocalAgent(b);

View File

@ -1,14 +1,18 @@
import { mdiHarddisk, mdiNas } from "@mdi/js";
import { mdiCog, mdiHarddisk, mdiNas } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { computeDomain } from "../../../../../common/entity/compute_domain";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-svg-icon";
import "../../../../../components/ha-switch";
import type { BackupAgent } from "../../../../../data/backup";
import type {
BackupAgent,
BackupAgentsConfig,
} from "../../../../../data/backup";
import {
CLOUD_AGENT,
computeBackupAgentName,
@ -18,6 +22,7 @@ import {
import type { CloudStatus } from "../../../../../data/cloud";
import type { HomeAssistant } from "../../../../../types";
import { brandsUrl } from "../../../../../util/brands-url";
import { navigate } from "../../../../../common/navigate";
const DEFAULT_AGENTS = [];
@ -29,6 +34,11 @@ class HaBackupConfigAgents extends LitElement {
@property({ attribute: false }) public agents: BackupAgent[] = [];
@property({ attribute: false }) public agentsConfig?: BackupAgentsConfig;
@property({ type: Boolean, attribute: "show-settings" }) public showSettings =
false;
@state() private value?: string[];
private _availableAgents = memoizeOne(
@ -53,6 +63,21 @@ class HaBackupConfigAgents extends LitElement {
"ui.panel.config.backup.agents.cloud_agent_description"
);
}
const encryptionTurnedOff =
this.agentsConfig?.[agentId]?.protected === false;
if (encryptionTurnedOff) {
return html`
<span class="dot warning"></span>
<span>
${this.hass.localize(
"ui.panel.config.backup.agents.encryption_turned_off"
)}
</span>
`;
}
if (isNetworkMountAgent(agentId)) {
return this.hass.localize(
"ui.panel.config.backup.agents.network_mount_agent_description"
@ -80,6 +105,7 @@ class HaBackupConfigAgents extends LitElement {
agentId === CLOUD_AGENT &&
this.cloudStatus.logged_in &&
!this.cloudStatus.active_subscription;
return html`
<ha-md-list-item>
${isLocalAgent(agentId)
@ -112,6 +138,16 @@ class HaBackupConfigAgents extends LitElement {
${description
? html`<div slot="supporting-text">${description}</div>`
: nothing}
${this.showSettings
? html`
<ha-icon-button
id=${agentId}
slot="end"
path=${mdiCog}
@click=${this._showAgentSettings}
></ha-icon-button>
`
: nothing}
<ha-switch
slot="end"
id=${agentId}
@ -133,6 +169,11 @@ class HaBackupConfigAgents extends LitElement {
`;
}
private _showAgentSettings(ev): void {
const agentId = ev.currentTarget.id;
navigate(`/config/backup/location/${agentId}`);
}
private _agentToggled(ev) {
ev.stopPropagation();
const value = ev.currentTarget.checked;
@ -180,6 +221,25 @@ class HaBackupConfigAgents extends LitElement {
--mdc-icon-size: 48px;
color: var(--primary-text-color);
}
ha-md-list-item [slot="supporting-text"] {
display: flex;
align-items: center;
flex-direction: row;
gap: 8px;
line-height: normal;
}
.dot {
display: block;
position: relative;
width: 8px;
height: 8px;
background-color: var(--disabled-color);
border-radius: 50%;
flex: none;
}
.dot.warning {
background-color: var(--warning-color);
}
`;
}

View File

@ -8,7 +8,10 @@ import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-next";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import type { BackupContent } from "../../../../../data/backup";
import {
computeBackupSize,
type BackupContent,
} from "../../../../../data/backup";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import { bytesToString } from "../../../../../util/bytes-to-string";
@ -22,7 +25,7 @@ const computeBackupStats = (backups: BackupContent[]): BackupStats =>
backups.reduce(
(stats, backup) => {
stats.count++;
stats.size += backup.size;
stats.size += computeBackupSize(backup);
return stats;
},
{ count: 0, size: 0 }

View File

@ -179,7 +179,8 @@ class HaBackupOverviewBackups extends LitElement {
now,
true
),
count: lastUploadedBackup.agent_ids?.length ?? 0,
count: Object.keys(lastUploadedBackup.agents)
.length,
}
)
: nextBackupDescription}
@ -265,7 +266,8 @@ class HaBackupOverviewBackups extends LitElement {
now,
true
),
count: lastUploadedBackup.agent_ids?.length ?? 0,
count: Object.keys(lastUploadedBackup.agents)
.length,
}
)
: nextBackupDescription}
@ -286,7 +288,7 @@ class HaBackupOverviewBackups extends LitElement {
now,
true
),
count: lastBackup.agent_ids?.length ?? 0,
count: Object.keys(lastBackup.agents).length,
}
);

View File

@ -72,6 +72,7 @@ const RECOMMENDED_CONFIG: BackupConfig = {
time: null,
days: [],
},
agents: {},
last_attempted_automatic_backup: null,
last_completed_automatic_backup: null,
next_automatic_backup: null,

View File

@ -78,7 +78,12 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
this._error = undefined;
this._state = undefined;
this._stage = undefined;
if (this._params.backup.protected) {
const agentIds = Object.keys(this._params.backup.agents);
const preferedAgent = getPreferredAgentForDownload(agentIds);
const isProtected = this._params.backup.agents[preferedAgent]?.protected;
if (isProtected) {
this._backupEncryptionKey = await this._fetchEncryptionKey();
if (!this._backupEncryptionKey) {
this._step = STEPS[1];
@ -322,9 +327,8 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
return;
}
const preferedAgent = getPreferredAgentForDownload(
this._params.backup.agent_ids!
);
const agentIds = Object.keys(this._params.backup.agents);
const preferedAgent = getPreferredAgentForDownload(agentIds);
const { addons, database_included, homeassistant_included, folders } =
this._params.selectedData;

View File

@ -39,7 +39,9 @@ import type {
BackupContent,
} from "../../../data/backup";
import {
compareAgents,
computeBackupAgentName,
computeBackupSize,
deleteBackup,
generateBackup,
generateBackupWithAutomaticSettings,
@ -68,6 +70,8 @@ import { downloadBackup } from "./helper/download_backup";
interface BackupRow extends DataTableRowData, BackupContent {
formatted_type: string;
size: number;
agent_ids: string[];
}
type BackupType = "automatic" | "manual";
@ -291,6 +295,8 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
const type = backup.with_automatic_settings ? "automatic" : "manual";
return {
...backup,
size: computeBackupSize(backup),
agent_ids: Object.keys(backup.agents).sort(compareAgents),
formatted_type: localize(`ui.panel.config.backup.type.${type}`),
};
});

View File

@ -23,12 +23,14 @@ import "../../../components/ha-md-list-item";
import type {
BackupAgent,
BackupConfig,
BackupContentAgent,
BackupContentExtended,
BackupData,
} from "../../../data/backup";
import {
compareAgents,
computeBackupAgentName,
computeBackupSize,
deleteBackup,
fetchBackupDetails,
isLocalAgent,
@ -45,21 +47,31 @@ import { fireEvent } from "../../../common/dom/fire_event";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import { downloadBackup } from "./helper/download_backup";
interface Agent {
interface Agent extends BackupContentAgent {
id: string;
success: boolean;
}
const computeAgents = (agent_ids: string[], failed_agent_ids: string[]) =>
[
...agent_ids.filter((id) => !failed_agent_ids.includes(id)),
...failed_agent_ids,
const computeAgents = (backup: BackupContentExtended) => {
const agentIds = Object.keys(backup.agents);
const failedAgentIds = backup.failed_agent_ids ?? [];
return [
...agentIds.filter((id) => !failedAgentIds.includes(id)),
...failedAgentIds,
]
.map<Agent>((id) => ({
id,
success: !failed_agent_ids.includes(id),
}))
.map<Agent>((id) => {
const agent: BackupContentAgent = backup.agents[id] ?? {
protected: false,
size: 0,
};
return {
...agent,
id: id,
success: !failedAgentIds.includes(id),
};
})
.sort((a, b) => compareAgents(a.id, b.id));
};
@customElement("ha-config-backup-details")
class HaConfigBackupDetails extends LitElement {
@ -156,7 +168,7 @@ class HaConfigBackupDetails extends LitElement {
)}
</span>
<span slot="supporting-text">
${bytesToString(this._backup.size)}
${bytesToString(computeBackupSize(this._backup))}
</span>
</ha-md-list-item>
<ha-md-list-item>
@ -173,22 +185,6 @@ class HaConfigBackupDetails extends LitElement {
)}
</span>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.details.summary.protection"
)}
</span>
<span slot="supporting-text">
${this._backup.protected
? this.hass.localize(
"ui.panel.config.backup.details.summary.protected_encrypted_aes_128"
)
: this.hass.localize(
"ui.panel.config.backup.details.summary.protected_not_encrypted"
)}
</span>
</ha-md-list-item>
</ha-md-list>
</div>
</ha-card>
@ -230,87 +226,108 @@ class HaConfigBackupDetails extends LitElement {
<ha-md-list>
${this._agents.map((agent) => {
const agentId = agent.id;
const success = agent.success;
const domain = computeDomain(agentId);
const name = computeBackupAgentName(
this.hass.localize,
agentId,
this.agents
);
const success = agent.success;
const failed = !agent.success;
const unencrypted = !agent.protected;
return html`
<ha-md-list-item>
${isLocalAgent(agentId)
? html`
<ha-svg-icon
.path=${mdiHarddisk}
slot="start"
>
</ha-svg-icon>
`
: isNetworkMountAgent(agentId)
${
isLocalAgent(agentId)
? html`
<ha-svg-icon
.path=${mdiNas}
.path=${mdiHarddisk}
slot="start"
></ha-svg-icon>
>
</ha-svg-icon>
`
: html`
<img
.src=${brandsUrl({
domain,
type: "icon",
useFallback: true,
darkOptimized:
this.hass.themes?.darkMode,
})}
crossorigin="anonymous"
referrerpolicy="no-referrer"
alt=""
slot="start"
/>
`}
: isNetworkMountAgent(agentId)
? html`
<ha-svg-icon
.path=${mdiNas}
slot="start"
></ha-svg-icon>
`
: html`
<img
.src=${brandsUrl({
domain,
type: "icon",
useFallback: true,
darkOptimized:
this.hass.themes?.darkMode,
})}
crossorigin="anonymous"
referrerpolicy="no-referrer"
alt=""
slot="start"
/>
`
}
<div slot="headline">${name}</div>
<div slot="supporting-text">
<span
class="dot ${success ? "success" : "error"}"
>
</span>
<span>
${success
? this.hass.localize(
"ui.panel.config.backup.details.locations.backup_stored"
)
: this.hass.localize(
"ui.panel.config.backup.details.locations.backup_failed"
)}
</span>
${
failed
? html`
<div slot="supporting-text">
<span class="dot error"></span>
<span>
${this.hass.localize(
"ui.panel.config.backup.details.locations.backup_failed"
)}
</span>
</div>
`
: unencrypted
? html`
<div slot="supporting-text">
<span class="dot warning"></span>
<span> Unencrypted </span>
</div>
`
: html`
<div slot="supporting-text">
<span class="dot success"></span>
<span> Encrypted </span>
</div>
`
}
</div>
${success
? html`<ha-button-menu
slot="end"
@action=${this._handleAgentAction}
.agent=${agentId}
fixed
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize(
"ui.common.menu"
)}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-list-item graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${mdiDownload}
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.backup.details.locations.download"
)}
</ha-list-item>
</ha-button-menu>`
: nothing}
${
success
? html`
<ha-button-menu
slot="end"
@action=${this._handleAgentAction}
.agent=${agentId}
fixed
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize(
"ui.common.menu"
)}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-list-item graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${mdiDownload}
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.backup.details.locations.download"
)}
</ha-list-item>
</ha-button-menu>
`
: nothing
}
</ha-md-list-item>
`;
})}
@ -354,10 +371,7 @@ class HaConfigBackupDetails extends LitElement {
try {
const response = await fetchBackupDetails(this.hass, this.backupId);
this._backup = response.backup;
this._agents = computeAgents(
response.backup.agent_ids || [],
response.backup.failed_agent_ids || []
);
this._agents = computeAgents(response.backup);
} catch (err: any) {
this._error =
err?.message ||
@ -479,6 +493,9 @@ class HaConfigBackupDetails extends LitElement {
.dot.success {
background-color: var(--success-color);
}
.dot.warning {
background-color: var(--warning-color);
}
.dot.error {
background-color: var(--error-color);
}

View File

@ -0,0 +1,369 @@
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-switch";
import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-circular-progress";
import "../../../components/ha-icon-button";
import "../../../components/ha-list-item";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import type {
BackupAgent,
BackupAgentConfig,
BackupConfig,
} from "../../../data/backup";
import {
CLOUD_AGENT,
computeBackupAgentName,
fetchBackupAgentsInfo,
updateBackupConfig,
} from "../../../data/backup";
import "../../../layouts/hass-subpage";
import type { HomeAssistant } from "../../../types";
import "./components/ha-backup-data-picker";
import { showConfirmationDialog } from "../../lovelace/custom-card-helpers";
import { fireEvent } from "../../../common/dom/fire_event";
@customElement("ha-config-backup-location")
class HaConfigBackupDetails extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: "agent-id" }) public agentId!: string;
@property({ attribute: false }) public config?: BackupConfig;
@property({ attribute: false }) public agents: BackupAgent[] = [];
@state() private _agent?: BackupAgent | null;
@state() private _error?: string;
protected willUpdate(changedProps: PropertyValues): void {
if (changedProps.has("agentId")) {
if (this.agentId) {
this._fetchAgent();
} else {
this._error = "Agent id not defined";
}
}
}
protected render() {
if (!this.hass) {
return nothing;
}
const encrypted = this._isEncryptionTurnedOn();
return html`
<hass-subpage
back-path="/config/backup/settings"
.hass=${this.hass}
.narrow=${this.narrow}
.header=${(this._agent &&
computeBackupAgentName(
this.hass.localize,
this.agentId,
this.agents
)) ||
this.hass.localize("ui.panel.config.backup.location.header")}
>
<div class="content">
${this._error &&
html`<ha-alert alert-type="error">${this._error}</ha-alert>`}
${this._agent === null
? html`
<ha-alert
alert-type="warning"
.title=${this.hass.localize(
"ui.panel.config.backup.location.not_found"
)}
>
${this.hass.localize(
"ui.panel.config.backup.location.not_found_description",
{ agentId: this.agentId }
)}
</ha-alert>
`
: !this.agentId
? html`<ha-circular-progress active></ha-circular-progress>`
: html`
${CLOUD_AGENT === this.agentId
? html`
<ha-card>
<div class="card-header">
${this.hass.localize(
"ui.panel.config.backup.location.configuration.title"
)}
</div>
<div class="card-content">
<p>
${this.hass.localize(
"ui.panel.config.backup.location.configuration.cloud_description"
)}
</p>
</div>
</ha-card>
`
: nothing}
<ha-card>
<div class="card-header">
${this.hass.localize(
"ui.panel.config.backup.location.encryption.title"
)}
</div>
<div class="card-content">
<p>
${this.hass.localize(
"ui.panel.config.backup.location.encryption.description"
)}
</p>
<ha-md-list>
${CLOUD_AGENT === this.agentId
? html`
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.location.encryption.location_encrypted"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.location.encryption.location_encrypted_cloud_description"
)}
</span>
<a
href="https://www.nabucasa.com/config/backups/"
target="_blank"
slot="end"
rel="noreferrer noopener"
>
<ha-button>
${this.hass.localize(
"ui.panel.config.backup.location.encryption.location_encrypted_cloud_learn_more"
)}
</ha-button>
</a>
</ha-md-list-item>
`
: encrypted
? html`
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.location.encryption.location_encrypted"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
`ui.panel.config.backup.location.encryption.location_encrypted_description`
)}
</span>
<ha-button
slot="end"
@click=${this._turnOffEncryption}
destructive
>
${this.hass.localize(
"ui.panel.config.backup.location.encryption.encryption_turn_off"
)}
</ha-button>
</ha-md-list-item>
`
: html`
<ha-alert
alert-type="warning"
.title=${this.hass.localize(
"ui.panel.config.backup.location.encryption.warning_encryption_turn_off"
)}
>
${this.hass.localize(
"ui.panel.config.backup.location.encryption.warning_encryption_turn_off_description"
)}
</ha-alert>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.location.encryption.location_unencrypted"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
`ui.panel.config.backup.location.encryption.location_unencrypted_description`
)}
</span>
<ha-button
slot="end"
@click=${this._turnOnEncryption}
>
${this.hass.localize(
"ui.panel.config.backup.location.encryption.encryption_turn_on"
)}
</ha-button>
</ha-md-list-item>
`}
</ha-md-list>
</div>
</ha-card>
`}
</div>
</hass-subpage>
`;
}
private _isEncryptionTurnedOn() {
const agentConfig = this.config?.agents[this.agentId] as
| BackupAgentConfig
| undefined;
if (!agentConfig) {
return true;
}
return agentConfig.protected;
}
private async _fetchAgent() {
try {
// Todo fetch agent details
const { agents } = await fetchBackupAgentsInfo(this.hass);
const agent = agents.find((a) => a.agent_id === this.agentId);
if (!agent) {
throw new Error("Agent not found");
}
this._agent = agent;
} catch (err: any) {
this._error =
err?.message ||
this.hass.localize("ui.panel.config.backup.details.error");
}
}
private async _updateAgentEncryption(value: boolean) {
const agentsConfig = {
...this.config?.agents,
[this.agentId]: {
...this.config?.agents[this.agentId],
protected: value,
},
};
await updateBackupConfig(this.hass, {
agents: agentsConfig,
});
fireEvent(this, "ha-refresh-backup-config");
}
private _turnOnEncryption() {
this._updateAgentEncryption(true);
}
private async _turnOffEncryption() {
const response = await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.backup.location.encryption.encryption_turn_off_confirm_title"
),
text: this.hass.localize(
"ui.panel.config.backup.location.encryption.encryption_turn_off_confirm_text"
),
confirmText: this.hass.localize(
"ui.panel.config.backup.location.encryption.encryption_turn_off_confirm_action"
),
dismissText: this.hass.localize("ui.common.cancel"),
destructive: true,
});
if (response) {
this._updateAgentEncryption(false);
}
}
static styles = css`
.content {
padding: 28px 20px 0;
max-width: 690px;
margin: 0 auto;
gap: 24px;
display: grid;
margin-bottom: 24px;
}
.card-content {
padding: 0 20px;
}
.card-actions {
display: flex;
justify-content: flex-end;
}
ha-md-list {
background: none;
padding: 0;
}
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-list-item-two-line-container-height: 64px;
}
ha-md-list-item img {
width: 48px;
}
ha-md-list-item ha-svg-icon[slot="start"] {
--mdc-icon-size: 48px;
color: var(--primary-text-color);
}
ha-md-list.summary ha-md-list-item {
--md-list-item-supporting-text-size: 1rem;
--md-list-item-label-text-size: 0.875rem;
--md-list-item-label-text-color: var(--secondary-text-color);
--md-list-item-supporting-text-color: var(--primary-text-color);
}
.warning {
color: var(--error-color);
}
.warning ha-svg-icon {
color: var(--error-color);
}
ha-button.danger {
--mdc-theme-primary: var(--error-color);
}
ha-backup-data-picker {
display: block;
}
ha-md-list-item [slot="supporting-text"] {
display: flex;
align-items: center;
flex-direction: row;
gap: 8px;
line-height: normal;
}
.dot {
display: block;
position: relative;
width: 8px;
height: 8px;
background-color: var(--disabled-color);
border-radius: 50%;
flex: none;
}
.dot.success {
background-color: var(--success-color);
}
.dot.error {
background-color: var(--error-color);
}
.card-header {
padding-bottom: 8px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-backup-location": HaConfigBackupDetails;
}
}

View File

@ -179,9 +179,11 @@ class HaConfigBackupSettings extends LitElement {
<ha-backup-config-agents
.hass=${this.hass}
.value=${this._config.create_backup.agent_ids}
.agentsConfig=${this._config.agents}
.cloudStatus=${this.cloudStatus}
.agents=${this.agents}
@value-changed=${this._agentsConfigChanged}
show-settings
></ha-backup-config-agents>
${!this._config.create_backup.agent_ids.length
? html`

View File

@ -88,11 +88,7 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
private async _fetchBackupInfo() {
const info = await fetchBackupInfo(this.hass);
this._backups = info.backups.map((backup) => ({
...backup,
agent_ids: backup.agent_ids?.sort(compareAgents),
failed_agent_ids: backup.failed_agent_ids?.sort(compareAgents),
}));
this._backups = info.backups;
}
private async _fetchBackupConfig() {
@ -124,6 +120,10 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
tag: "ha-config-backup-settings",
load: () => import("./ha-config-backup-settings"),
},
location: {
tag: "ha-config-backup-location",
load: () => import("./ha-config-backup-location"),
},
},
};
@ -138,11 +138,15 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
pageEl.agents = this._agents;
pageEl.fetching = this._fetching;
if (
(!changedProps || changedProps.has("route")) &&
this._currentPage === "details"
) {
pageEl.backupId = this.routeTail.path.substr(1);
if (!changedProps || changedProps.has("route")) {
switch (this._currentPage) {
case "details":
pageEl.backupId = this.routeTail.path.substr(1);
break;
case "location":
pageEl.agentId = this.routeTail.path.substr(1);
break;
}
}
}

View File

@ -42,11 +42,10 @@ const downloadEncryptedBackup = async (
confirmText: "Download encrypted",
})
) {
triggerDownload(
hass,
backup.backup_id,
agentId ?? getPreferredAgentForDownload(backup.agent_ids!)
);
const agentIds = Object.keys(backup.agents);
const preferedAgent = agentId ?? getPreferredAgentForDownload(agentIds);
triggerDownload(hass, backup.backup_id, preferedAgent);
}
};
@ -83,10 +82,11 @@ export const downloadBackup = async (
agentId?: string,
userProvided = false
): Promise<void> => {
const preferedAgent =
agentId ?? getPreferredAgentForDownload(backup.agent_ids!);
const agentIds = Object.keys(backup.agents);
const preferedAgent = agentId ?? getPreferredAgentForDownload(agentIds);
const isProtected = backup.agents[preferedAgent]?.protected;
if (backup.protected) {
if (isProtected) {
if (encryptionKey) {
try {
await canDecryptBackupOnDownload(

View File

@ -2404,6 +2404,7 @@
"cloud_agent_no_subcription": "You currently do not have an active Home Assistant Cloud subscription.",
"network_mount_agent_description": "Network storage",
"no_agents": "No locations configured",
"encryption_turned_off": "Encryption turned off",
"local_agent": "This system"
},
"data": {
@ -2650,10 +2651,7 @@
"summary": {
"title": "Backup",
"size": "Size",
"created": "Created",
"protection": "Protection",
"protected_encrypted_aes_128": "Encrypted AES-128",
"protected_not_encrypted": "Not encrypted"
"created": "Created"
},
"restore": {
"title": "Select what to restore",
@ -2661,10 +2659,37 @@
},
"locations": {
"title": "Locations",
"backup_stored": "Backup stored",
"backup_failed": "Backup failed",
"encryption_turned_off": "Encryption turned off",
"download": "Download from this location"
}
},
"location": {
"header": "Location",
"not_found": "Not found",
"not_found_description": "Location matching ''{backupId}'' not found",
"error": "Could not fetch location details",
"configuration": {
"title": "Configuration",
"cloud_description": "Home Assistant Cloud backup stores only one backup. The oldest backups are deleted."
},
"encryption": {
"title": "Encryption",
"description": "All your backups are encrypted by default to keep your data private and secure.",
"location_encrypted": "This location is encrypted",
"location_unencrypted": "This location is unencrypted",
"location_encrypted_description": "Your data private and secure by securing it with your encryption key.",
"location_encrypted_cloud_description": "Home Assistant Cloud is the privacy-focused cloud. This is why it will only accept encrypted backups and why we dont store your encryption key.",
"location_encrypted_cloud_learn_more": "Learn more",
"location_unencrypted_description": "Please keep your backups private and secure.",
"encryption_turn_on": "Turn on",
"encryption_turn_off": "Turn off",
"encryption_turn_off_confirm_title": "Turn encryption off?",
"encryption_turn_off_confirm_text": "All your next backups will not be encrypted for this location. Please keep your backups private and secure.",
"encryption_turn_off_confirm_action": "Turn encryption off",
"warning_encryption_turn_off": "Encryption turned off",
"warning_encryption_turn_off_description": "All your next backups will not be encrypted."
}
}
},
"tag": {