Multiple backup adjustments (#23361)

* Always show welcome screen during onboarding

* Fix content overflow in dialog

* Rename copies to backups, drop 7 days

* Remove including new and learn more button

* Some other fixes

* Allow changing default local location from new backup page

* Dont show addon version in settings and onboarding

* Add margin for ios

* Display old encryption key before change

* Refactor descriptipn
This commit is contained in:
Paul Bottein 2024-12-20 15:52:55 +01:00 committed by GitHub
parent c66f5e2d8a
commit fb228dc918
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 324 additions and 86 deletions

View File

@ -1,5 +1,6 @@
import { mdiBackupRestore, mdiFolder, mdiHarddisk, mdiPlayBox } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../common/config/is_component_loaded";
@ -173,6 +174,16 @@ class HaMountPicker extends LitElement {
fireEvent(this, "change");
}, 0);
}
static get styles(): CSSResultGroup {
return [
css`
ha-select {
width: 100%;
}
`,
];
}
}
declare global {

View File

@ -68,7 +68,8 @@ export type Selector =
| TTSVoiceSelector
| UiActionSelector
| UiColorSelector
| UiStateContentSelector;
| UiStateContentSelector
| BackupLocationSelector;
export interface ActionSelector {
// eslint-disable-next-line @typescript-eslint/ban-types

View File

@ -48,6 +48,13 @@ class HaBackupConfigAgents extends LitElement {
return this.value ?? DEFAULT_AGENTS;
}
private _description(agentId: string) {
if (agentId === CLOUD_AGENT) {
return "It stores one backup. The oldest backups are deleted.";
}
return "";
}
protected render() {
return html`
${this._agentIds.length > 0
@ -60,6 +67,7 @@ class HaBackupConfigAgents extends LitElement {
agentId,
this._agentIds
);
const description = this._description(agentId);
return html`
<ha-md-list-item>
${isLocalAgent(agentId)
@ -82,13 +90,8 @@ class HaBackupConfigAgents extends LitElement {
/>
`}
<div slot="headline">${name}</div>
${agentId === CLOUD_AGENT
? html`
<div slot="supporting-text">
It stores one backup. The oldest backups are
deleted.
</div>
`
${description
? html`<div slot="supporting-text">${description}</div>`
: nothing}
<ha-switch
slot="end"

View File

@ -60,6 +60,9 @@ class HaBackupConfigData extends LitElement {
@property({ type: Boolean, attribute: "force-home-assistant" })
public forceHomeAssistant = false;
@property({ attribute: "hide-addon-version", type: Boolean })
public hideAddonVersion = false;
@state() private value?: BackupConfigData;
@state() private _addons: BackupAddonItem[] = [];
@ -156,7 +159,7 @@ class HaBackupConfigData extends LitElement {
The bare minimum needed to restore your system.
</span>
${this.forceHomeAssistant
? html`<ha-button slot="end">Learn more</ha-button>`
? nothing
: html`
<ha-switch
id="homeassistant"
@ -253,7 +256,7 @@ class HaBackupConfigData extends LitElement {
.value=${data.addons_mode}
>
<ha-md-select-option value="all">
<div slot="headline">All, including new</div>
<div slot="headline">All</div>
</ha-md-select-option>
<ha-md-select-option value="none">
<div slot="headline">None</div>
@ -276,6 +279,7 @@ class HaBackupConfigData extends LitElement {
.value=${data.addons}
@value-changed=${this._addonsChanged}
.addons=${this._addons}
.hideVersion=${this.hideAddonVersion}
></ha-backup-addons-picker>
</ha-expansion-panel>
`

View File

@ -23,7 +23,6 @@ const MAX_VALUE = 50;
enum RetentionPreset {
COPIES_3 = "copies_3",
DAYS_7 = "days_7",
FOREVER = "forever",
CUSTOM = "custom",
}
@ -38,7 +37,6 @@ const RETENTION_PRESETS: Record<
RetentionData
> = {
copies_3: { type: "copies", value: 3 },
days_7: { type: "days", value: 7 },
forever: { type: "days", value: 0 },
};
@ -176,7 +174,7 @@ class HaBackupConfigSchedule extends LitElement {
</ha-md-select>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">Maximum copies</span>
<span slot="headline">Backups to keep</span>
<span slot="supporting-text">
The number of backups that are saved
</span>
@ -186,13 +184,10 @@ class HaBackupConfigSchedule extends LitElement {
.value=${this._retentionPreset}
>
<ha-md-select-option .value=${RetentionPreset.COPIES_3}>
<div slot="headline">Latest 3 copies</div>
</ha-md-select-option>
<ha-md-select-option .value=${RetentionPreset.DAYS_7}>
<div slot="headline">Keep 7 days</div>
<div slot="headline">3 backups</div>
</ha-md-select-option>
<ha-md-select-option .value=${RetentionPreset.FOREVER}>
<div slot="headline">Keep forever</div>
<div slot="headline">All backups</div>
</ha-md-select-option>
<ha-md-select-option .value=${RetentionPreset.CUSTOM}>
<div slot="headline">Custom</div>
@ -223,7 +218,7 @@ class HaBackupConfigSchedule extends LitElement {
<div slot="headline">days</div>
</ha-md-select-option>
<ha-md-select-option .value=${"copies"}>
<div slot="headline">copies</div>
<div slot="headline">backups</div>
</ha-md-select-option>
</ha-md-select>
</ha-md-list-item>

View File

@ -26,6 +26,9 @@ export class HaBackupAddonsPicker extends LitElement {
@property({ attribute: false }) public value?: string[];
@property({ attribute: "hide-version", type: Boolean })
public hideVersion = false;
protected render() {
return html`
<div class="items">
@ -35,7 +38,7 @@ export class HaBackupAddonsPicker extends LitElement {
<ha-backup-formfield-label
slot="label"
.label=${item.name}
.version=${item.version}
.version=${this.hideVersion ? undefined : item.version}
.iconPath=${item.iconPath || mdiPuzzle}
.imageUrl=${this.addons?.find((a) => a.slug === item.slug)?.icon
? `/api/hassio/addons/${item.slug}/icon`

View File

@ -43,7 +43,7 @@ class SupervisorFormfieldLabel extends LitElement {
margin-right: 4px;
margin-inline-end: 4px;
margin-inline-start: initial;
font-size: 16px;
font-size: 14px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0.5px;

View File

@ -102,8 +102,7 @@ class HaBackupOverviewBackups extends LitElement {
gap: 24px;
display: flex;
flex-direction: column;
margin-bottom: 24px;
margin-bottom: 72px;
margin-bottom: calc(72px + env(safe-area-inset-bottom));
}
.card-actions {
display: flex;

View File

@ -38,7 +38,7 @@ class HaBackupOverviewBackups extends LitElement {
Backups are essential to a reliable smart home. They protect your
setup against failures and allows you to quickly have a working
system again. It is recommended to create a daily backup and keep
copies of the last 3 days on two different locations. And one of
backups of the last 3 days on two different locations. And one of
them is off-site.
</p>
</div>

View File

@ -33,7 +33,7 @@ class HaBackupBackupsSummary extends LitElement {
let copiesText = "and keep all backups";
if (copies) {
copiesText = `and keep the latest ${copies} copie(s)`;
copiesText = `and keep the latest ${copies} backup(s)`;
} else if (days) {
copiesText = `and keep backups for ${days} day(s)`;
}
@ -69,7 +69,7 @@ class HaBackupBackupsSummary extends LitElement {
private _addonsDescription(config: BackupConfig): string {
if (config.create_backup.include_all_addons) {
return "All add-ons, including new";
return "All add-ons";
}
if (config.create_backup.include_addons?.length) {
return `${config.create_backup.include_addons.length} add-ons`;

View File

@ -82,8 +82,6 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
@state() private _step?: Step;
@state() private _steps: Step[] = [];
@state() private _params?: BackupOnboardingDialogParams;
@query("ha-md-dialog") private _dialog!: HaMdDialog;
@ -92,8 +90,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
public showDialog(params: BackupOnboardingDialogParams): void {
this._params = params;
this._steps = params.showIntro ? STEPS.concat() : STEPS.slice(1);
this._step = this._steps[0];
this._step = STEPS[0];
this._config = RECOMMENDED_CONFIG;
const agents: string[] = [];
@ -128,7 +125,6 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
}
this._opened = false;
this._step = undefined;
this._steps = [];
this._config = undefined;
this._params = undefined;
}
@ -170,19 +166,19 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
}
private _previousStep() {
const index = this._steps.indexOf(this._step!);
const index = STEPS.indexOf(this._step!);
if (index === 0) {
return;
}
this._step = this._steps[index - 1];
this._step = STEPS[index - 1];
}
private _nextStep() {
const index = this._steps.indexOf(this._step!);
if (index === this._steps.length - 1) {
const index = STEPS.indexOf(this._step!);
if (index === STEPS.length - 1) {
return;
}
this._step = this._steps[index + 1];
this._step = STEPS[index + 1];
}
protected render() {
@ -190,8 +186,8 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
return nothing;
}
const isLastStep = this._step === this._steps[this._steps.length - 1];
const isFirstStep = this._step === this._steps[0];
const isLastStep = this._step === STEPS[STEPS.length - 1];
const isFirstStep = this._step === STEPS[0];
return html`
<ha-md-dialog disable-cancel-action open @closed=${this.closeDialog}>
@ -224,7 +220,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
@click=${this._done}
.disabled=${!this._isStepValid()}
>
Save
Save and create backup
</ha-button>
`
: html`
@ -296,7 +292,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
Backups are essential to a reliable smart home. They protect your
setup against failures and allows you to quickly have a working
system again. It is recommended to create a daily backup and keep
copies of the last 3 days on two different locations. And one of
backups of the last 3 days on two different locations. And one of
them is off-site.
</p>
</div>
@ -331,7 +327,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
case "setup":
return html`
<p>
It is recommended to create a daily backup and keep copies of the
It is recommended to create a daily backup and keep backups of the
last 3 days on two different locations. And one of them is off-site.
</p>
<ha-md-list class="full">
@ -355,7 +351,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
return html`
<p>
Let Home Assistant take care of your backups by creating a scheduled
backup that also removes older copies.
backup that also removes older backups.
</p>
<ha-backup-config-schedule
.hass=${this.hass}
@ -374,6 +370,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
.value=${this._dataConfig(this._config)}
@value-changed=${this._dataChanged}
force-home-assistant
hide-addon-version
></ha-backup-config-data>
`;
case "locations":
@ -470,9 +467,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
ha-md-dialog {
width: 90vw;
max-width: 560px;
}
div[slot="content"] {
margin-top: -16px;
--dialog-content-padding: 8px 24px;
}
ha-md-list {
background: none;

View File

@ -141,6 +141,13 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
have access to all your current backups. All next backups will use
the new encryption key.
</p>
<div class="encryption-key">
<p>${this._params?.currentKey}</p>
<ha-icon-button
.path=${mdiContentCopy}
@click=${this._copyOldKeyToClipboard}
></ha-icon-button>
</div>
<ha-md-list>
<ha-md-list-item>
<span slot="headline">Download old emergency kit</span>
@ -202,6 +209,16 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
});
}
private _copyOldKeyToClipboard() {
if (!this._params?.currentKey) {
return;
}
copyToClipboard(this._params.currentKey);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
});
}
private _downloadOld() {
if (!this._params?.currentKey) {
return;
@ -240,9 +257,7 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
ha-md-dialog {
width: 90vw;
max-width: 560px;
}
div[slot="content"] {
margin-top: -16px;
--dialog-content-padding: 8px 24px;
}
ha-md-list {
background: none;

View File

@ -0,0 +1,151 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-button";
import { createCloseHeading } from "../../../../components/ha-dialog";
import "../../../../components/ha-form/ha-form";
import type {
HaFormSchema,
SchemaUnion,
} from "../../../../components/ha-form/types";
import { extractApiErrorMessage } from "../../../../data/hassio/common";
import { changeMountOptions } from "../../../../data/supervisor/mounts";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import type { LocalBackupLocationDialogParams } from "./show-dialog-local-backup-location";
const SCHEMA = [
{
name: "default_backup_mount",
required: true,
selector: { backup_location: {} },
},
] as const satisfies HaFormSchema[];
@customElement("dialog-local-backup-location")
class LocalBackupLocationDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _dialogParams?: LocalBackupLocationDialogParams;
@state() private _data?: { default_backup_mount: string | null };
@state() private _waiting?: boolean;
@state() private _error?: string;
public async showDialog(
dialogParams: LocalBackupLocationDialogParams
): Promise<void> {
this._dialogParams = dialogParams;
}
public closeDialog(): void {
this._data = undefined;
this._error = undefined;
this._waiting = undefined;
this._dialogParams = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._dialogParams) {
return nothing;
}
return html`
<ha-dialog
open
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this.hass.localize(
`ui.panel.config.backup.dialogs.local_backup_location.title`
)
)}
@closed=${this.closeDialog}
>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
<p>
${this.hass.localize(
`ui.panel.config.backup.dialogs.local_backup_location.description`
)}
</p>
<ha-form
.hass=${this.hass}
.data=${this._data}
.schema=${SCHEMA}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
dialogInitialFocus
></ha-form>
<ha-button
slot="secondaryAction"
@click=${this.closeDialog}
dialogInitialFocus
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
.disabled=${this._waiting || !this._data}
slot="primaryAction"
@click=${this._changeMount}
>
${this.hass.localize("ui.common.save")}
</ha-button>
</ha-dialog>
`;
}
private _computeLabelCallback = (
schema: SchemaUnion<typeof SCHEMA>
): string =>
this.hass.localize(
`ui.panel.config.backup.dialogs.local_backup_location.options.${schema.name}.name`
) || schema.name;
private _valueChanged(ev: CustomEvent) {
const newLocation = ev.detail.value.default_backup_mount;
this._data = {
default_backup_mount: newLocation === "/backup" ? null : newLocation,
};
}
private async _changeMount() {
if (!this._data) {
return;
}
this._error = undefined;
this._waiting = true;
try {
await changeMountOptions(this.hass, this._data);
} catch (err: any) {
this._error = extractApiErrorMessage(err);
this._waiting = false;
return;
}
this.closeDialog();
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-max-width: 500px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-local-backup-location": LocalBackupLocationDialog;
}
}

View File

@ -115,9 +115,6 @@ class DialogNewBackup extends LitElement implements HassDialog {
--dialog-content-padding: 0;
max-width: 500px;
}
div[slot="content"] {
margin-top: -16px;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-md-dialog {
max-width: none;

View File

@ -157,7 +157,11 @@ class DialogRestoreBackupEncryptionKey
</div>
<div slot="actions">
<ha-button @click=${this.closeDialog}>Cancel</ha-button>
<ha-button @click=${this._submit} .disabled=${!this._getKey()}>
<ha-button
@click=${this._submit}
class="danger"
.disabled=${!this._getKey()}
>
Restore
</ha-button>
</div>
@ -221,10 +225,14 @@ class DialogRestoreBackupEncryptionKey
ha-md-dialog {
max-width: 500px;
width: 100%;
--dialog-content-padding: 8px 24px;
}
.content p {
margin: 0 0 16px;
}
ha-button.danger {
--mdc-theme-primary: var(--error-color);
}
`,
];
}

View File

@ -193,9 +193,7 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
ha-md-dialog {
width: 90vw;
max-width: 500px;
}
div[slot="content"] {
margin-top: -16px;
--dialog-content-padding: 8px 24px;
}
ha-md-list {
background: none;

View File

@ -4,7 +4,6 @@ import type { CloudStatus } from "../../../../data/cloud";
export interface BackupOnboardingDialogParams {
submit?: (value: boolean) => void;
cancel?: () => void;
showIntro?: boolean;
cloudStatus: CloudStatus;
}

View File

@ -0,0 +1,14 @@
import { fireEvent } from "../../../../common/dom/fire_event";
export interface LocalBackupLocationDialogParams {}
export const showLocalBackupLocationDialog = (
element: HTMLElement,
dialogParams: LocalBackupLocationDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-local-backup-location",
dialogImport: () => import("./dialog-local-backup-location"),
dialogParams,
});
};

View File

@ -119,26 +119,6 @@ class HaConfigBackupDetails extends LitElement {
: !this._backup
? html`<ha-circular-progress active></ha-circular-progress>`
: html`
<ha-card header="Select what to restore">
<div class="card-content">
<ha-backup-data-picker
.hass=${this.hass}
.data=${this._backup}
.value=${this._selectedBackup}
@value-changed=${this._selectedBackupChanged}
.addonsInfo=${this._addonsInfo}
>
</ha-backup-data-picker>
</div>
<div class="card-actions">
<ha-button
@click=${this._restore}
.disabled=${this._isRestoreDisabled()}
>
Restore
</ha-button>
</div>
</ha-card>
<ha-card header="Backup">
<div class="card-content">
<ha-md-list>
@ -159,6 +139,27 @@ class HaConfigBackupDetails extends LitElement {
</ha-md-list>
</div>
</ha-card>
<ha-card header="Select what to restore">
<div class="card-content">
<ha-backup-data-picker
.hass=${this.hass}
.data=${this._backup}
.value=${this._selectedBackup}
@value-changed=${this._selectedBackupChanged}
.addonsInfo=${this._addonsInfo}
>
</ha-backup-data-picker>
</div>
<div class="card-actions">
<ha-button
@click=${this._restore}
.disabled=${this._isRestoreDisabled()}
class="danger"
>
Restore
</ha-button>
</div>
</ha-card>
<ha-card header="Locations">
<div class="card-content">
<ha-md-list>
@ -172,6 +173,8 @@ class HaConfigBackupDetails extends LitElement {
this._backup!.agent_ids!
);
const isLocal = isLocalAgent(agentId);
return html`
<ha-md-list-item>
${isLocalAgent(agentId)
@ -204,7 +207,11 @@ class HaConfigBackupDetails extends LitElement {
>
</span>
<span>
${success ? "Backup synced" : "Backup failed"}
${success
? isLocal
? "Backup created"
: "Backup uploaded"
: "Backup failed"}
</span>
</div>
${success
@ -390,6 +397,9 @@ class HaConfigBackupDetails extends LitElement {
.warning ha-svg-icon {
color: var(--error-color);
}
ha-button.danger {
--mdc-theme-primary: var(--error-color);
}
ha-backup-data-picker {
display: block;
}

View File

@ -1,7 +1,8 @@
import { mdiDotsVertical, mdiPlus, mdiUpload } from "@mdi/js";
import { mdiDotsVertical, mdiHarddisk, mdiPlus, mdiUpload } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { fireEvent } from "../../../common/dom/fire_event";
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
import "../../../components/ha-button";
@ -34,6 +35,7 @@ import "./components/overview/ha-backup-overview-settings";
import "./components/overview/ha-backup-overview-summary";
import { showBackupOnboardingDialog } from "./dialogs/show-dialog-backup_onboarding";
import { showGenerateBackupDialog } from "./dialogs/show-dialog-generate-backup";
import { showLocalBackupLocationDialog } from "./dialogs/show-dialog-local-backup-location";
import { showNewBackupDialog } from "./dialogs/show-dialog-new-backup";
import { showUploadBackupDialog } from "./dialogs/show-dialog-upload-backup";
@ -63,15 +65,22 @@ class HaConfigBackupOverview extends LitElement {
await showUploadBackupDialog(this, {});
}
private _handleOnboardingButtonClick(ev) {
ev.stopPropagation();
this._setupAutomaticBackup(false);
private async _changeLocalLocation(ev) {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
showLocalBackupLocationDialog(this, {});
}
private async _setupAutomaticBackup(showIntro: boolean) {
private _handleOnboardingButtonClick(ev) {
ev.stopPropagation();
this._setupAutomaticBackup();
}
private async _setupAutomaticBackup() {
const success = await showBackupOnboardingDialog(this, {
cloudStatus: this.cloudStatus,
showIntro: showIntro,
});
if (!success) {
return;
@ -84,7 +93,7 @@ class HaConfigBackupOverview extends LitElement {
private async _newBackup(): Promise<void> {
if (this._needsOnboarding) {
this._setupAutomaticBackup(true);
this._setupAutomaticBackup();
return;
}
@ -125,6 +134,8 @@ class HaConfigBackupOverview extends LitElement {
const backupInProgress =
"state" in this.manager && this.manager.state === "in_progress";
const isHassio = isComponentLoaded(this.hass, "hassio");
return html`
<hass-subpage
back-path="/config/system"
@ -139,6 +150,18 @@ class HaConfigBackupOverview extends LitElement {
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
${isHassio
? html`<ha-list-item
graphic="icon"
@request-selected=${this._changeLocalLocation}
>
<ha-svg-icon
slot="graphic"
.path=${mdiHarddisk}
></ha-svg-icon>
Change local location
</ha-list-item>`
: nothing}
<ha-list-item
graphic="icon"
@request-selected=${this._uploadBackup}

View File

@ -56,7 +56,7 @@ class HaConfigBackupSettings extends LitElement {
<div class="card-content">
<p>
Let Home Assistant take care of your backups by creating a
scheduled backup that also removes older copies.
scheduled backup that also removes older backups.
</p>
<ha-backup-config-schedule
.hass=${this.hass}
@ -73,6 +73,7 @@ class HaConfigBackupSettings extends LitElement {
.value=${this._dataConfig}
@value-changed=${this._dataConfigChanged}
force-home-assistant
hide-addon-version
></ha-backup-config-data>
</div>
</ha-card>

View File

@ -2209,6 +2209,17 @@
},
"picker": {
"search": "Search backups"
},
"dialogs": {
"local_backup_location": {
"title": "Change local backup location",
"description": "Change the default location where local backups are stored on your Home Assistant instance.",
"options": {
"default_backup_mount": {
"name": "Default location"
}
}
}
}
},
"tag": {