Backup retention by location (#25144)

This commit is contained in:
Wendelin 2025-04-28 09:24:34 +02:00 committed by GitHub
parent 672fbc6007
commit 1b79869c87
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 388 additions and 272 deletions

View File

@ -2,6 +2,7 @@ import { memoize } from "@fullcalendar/core/internal";
import { setHours, setMinutes } from "date-fns"; import { setHours, setMinutes } from "date-fns";
import type { HassConfig } from "home-assistant-js-websocket"; import type { HassConfig } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import checkValidDate from "../common/datetime/check_valid_date";
import { import {
formatDateTime, formatDateTime,
formatDateTimeNumeric, formatDateTimeNumeric,
@ -10,11 +11,10 @@ import { formatTime } from "../common/datetime/format_time";
import type { LocalizeFunc } from "../common/translations/localize"; import type { LocalizeFunc } from "../common/translations/localize";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import { fileDownload } from "../util/file_download"; import { fileDownload } from "../util/file_download";
import { handleFetchPromise } from "../util/hass-call-api";
import type { BackupManagerState, ManagerStateEvent } from "./backup_manager";
import { domainToName } from "./integration"; import { domainToName } from "./integration";
import type { FrontendLocaleData } from "./translation"; import type { FrontendLocaleData } from "./translation";
import type { BackupManagerState, ManagerStateEvent } from "./backup_manager";
import checkValidDate from "../common/datetime/check_valid_date";
import { handleFetchPromise } from "../util/hass-call-api";
export const enum BackupScheduleRecurrence { export const enum BackupScheduleRecurrence {
NEVER = "never", NEVER = "never",
@ -37,6 +37,11 @@ export const BACKUP_DAYS: BackupDay[] = [
export const sortWeekdays = (weekdays) => export const sortWeekdays = (weekdays) =>
weekdays.sort((a, b) => BACKUP_DAYS.indexOf(a) - BACKUP_DAYS.indexOf(b)); weekdays.sort((a, b) => BACKUP_DAYS.indexOf(a) - BACKUP_DAYS.indexOf(b));
export interface Retention {
copies?: number | null;
days?: number | null;
}
export interface BackupConfig { export interface BackupConfig {
automatic_backups_configured: boolean; automatic_backups_configured: boolean;
last_attempted_automatic_backup: string | null; last_attempted_automatic_backup: string | null;
@ -52,10 +57,7 @@ export interface BackupConfig {
name: string | null; name: string | null;
password: string | null; password: string | null;
}; };
retention: { retention: Retention;
copies?: number | null;
days?: number | null;
};
schedule: { schedule: {
recurrence: BackupScheduleRecurrence; recurrence: BackupScheduleRecurrence;
time?: string | null; time?: string | null;
@ -75,10 +77,7 @@ export interface BackupMutableConfig {
name?: string | null; name?: string | null;
password?: string | null; password?: string | null;
}; };
retention?: { retention?: Retention;
copies?: number | null;
days?: number | null;
};
schedule?: { schedule?: {
recurrence: BackupScheduleRecurrence; recurrence: BackupScheduleRecurrence;
time?: string | null; time?: string | null;
@ -90,7 +89,8 @@ export interface BackupMutableConfig {
export type BackupAgentsConfig = Record<string, BackupAgentConfig>; export type BackupAgentsConfig = Record<string, BackupAgentConfig>;
export interface BackupAgentConfig { export interface BackupAgentConfig {
protected: boolean; protected?: boolean;
retention?: Retention | null;
} }
export interface BackupAgent { export interface BackupAgent {

View File

@ -0,0 +1,274 @@
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { clamp } from "../../../../../common/number/clamp";
import "../../../../../components/ha-expansion-panel";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-md-select";
import type { HaMdSelect } from "../../../../../components/ha-md-select";
import "../../../../../components/ha-md-select-option";
import "../../../../../components/ha-md-textfield";
import type { BackupConfig, Retention } from "../../../../../data/backup";
import type { HomeAssistant } from "../../../../../types";
export type BackupConfigSchedule = Pick<BackupConfig, "schedule" | "retention">;
const MIN_VALUE = 1;
const MAX_VALUE = 9999; // because of input width
export enum RetentionPreset {
GLOBAL = "global",
COPIES_3 = "copies_3",
FOREVER = "forever",
CUSTOM = "custom",
}
const PRESET_MAP: Record<
Exclude<RetentionPreset, RetentionPreset.CUSTOM>,
Retention | null
> = {
copies_3: { copies: 3, days: null },
forever: { copies: null, days: null },
global: null,
};
export interface RetentionData {
type: "copies" | "days" | "forever";
value: number;
}
@customElement("ha-backup-config-retention")
class HaBackupConfigRetention extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public retention?: Retention | null;
@property() public headline?: string;
@property({ type: Boolean, attribute: "location-specific" })
public locationSpecific = false;
@state() private _preset: RetentionPreset = RetentionPreset.COPIES_3;
@state() private _type: "copies" | "days" = "copies";
@state() private _value = 3;
private presetOptions = [
RetentionPreset.COPIES_3,
RetentionPreset.FOREVER,
RetentionPreset.CUSTOM,
];
public willUpdate(properties: PropertyValues) {
super.willUpdate(properties);
if (!this.hasUpdated) {
if (!this.retention) {
this._preset = RetentionPreset.GLOBAL;
} else if (
this.retention?.days === null &&
this.retention?.copies === null
) {
this._preset = RetentionPreset.FOREVER;
} else {
this._value = this.retention.copies || this.retention.days || 3;
if (
this.retention.days ||
this.locationSpecific ||
this.retention.copies !== 3
) {
this._preset = RetentionPreset.CUSTOM;
this._type = this.retention?.copies ? "copies" : "days";
}
}
if (this.locationSpecific) {
this.presetOptions = [
RetentionPreset.GLOBAL,
RetentionPreset.FOREVER,
RetentionPreset.CUSTOM,
];
}
}
}
protected render() {
return html`
<ha-md-list-item>
<span slot="headline">
${this.headline ??
this.hass.localize(`ui.panel.config.backup.schedule.retention`)}
</span>
<span slot="supporting-text">
${this.hass.localize(
`ui.panel.config.backup.schedule.retention_description`
)}
</span>
<ha-md-select
slot="end"
@change=${this._retentionPresetChanged}
.value=${this._preset}
>
${this.presetOptions.map(
(option) => html`
<ha-md-select-option .value=${option}>
<div slot="headline">
${this.hass.localize(
`ui.panel.config.backup.schedule.retention_presets.${option}`
)}
</div>
</ha-md-select-option>
`
)}
</ha-md-select>
</ha-md-list-item>
${this._preset === RetentionPreset.CUSTOM
? html`<ha-expansion-panel
expanded
.header=${this.hass.localize(
"ui.panel.config.backup.schedule.custom_retention"
)}
outlined
>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.schedule.custom_retention_label"
)}
</span>
<ha-md-textfield
slot="end"
@change=${this._retentionValueChanged}
.value=${this._value.toString()}
id="value"
type="number"
.min=${MIN_VALUE.toString()}
.max=${MAX_VALUE.toString()}
step="1"
>
</ha-md-textfield>
<ha-md-select
slot="end"
@change=${this._retentionTypeChanged}
.value=${this._type}
id="type"
>
<ha-md-select-option value="days">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.backup.schedule.retention_units.days"
)}
</div>
</ha-md-select-option>
<ha-md-select-option value="copies">
${this.hass.localize(
"ui.panel.config.backup.schedule.retention_units.copies"
)}
</ha-md-select-option>
</ha-md-select>
</ha-md-list-item></ha-expansion-panel
> `
: nothing}
`;
}
private _retentionPresetChanged(ev) {
ev.stopPropagation();
const target = ev.currentTarget as HaMdSelect;
let value = target.value as RetentionPreset;
if (
value === RetentionPreset.CUSTOM &&
(this.locationSpecific || this._preset === RetentionPreset.FOREVER)
) {
this._preset = value;
// custom needs to have a type of days or copies, set it to default copies 3
value = RetentionPreset.COPIES_3;
} else {
this._preset = value;
}
if (this.locationSpecific || value !== RetentionPreset.CUSTOM) {
const retention = PRESET_MAP[value];
fireEvent(this, "value-changed", {
value: retention,
});
}
}
private _retentionValueChanged(ev) {
ev.stopPropagation();
const target = ev.currentTarget as HaMdSelect;
const value = parseInt(target.value);
const clamped = clamp(value, MIN_VALUE, MAX_VALUE);
target.value = clamped.toString();
fireEvent(this, "value-changed", {
value: {
copies: this._type === "copies" ? clamped : null,
days: this._type === "days" ? clamped : null,
},
});
}
private _retentionTypeChanged(ev) {
ev.stopPropagation();
const target = ev.currentTarget as HaMdSelect;
const type = target.value as "copies" | "days";
fireEvent(this, "value-changed", {
value: {
copies: type === "copies" ? this._value : null,
days: type === "days" ? this._value : null,
},
});
}
static styles = css`
ha-md-list-item {
--md-item-overflow: visible;
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
}
@media all and (max-width: 450px) {
ha-md-select {
min-width: 160px;
width: 160px;
--md-filled-field-content-space: 0;
}
}
ha-md-textfield#value {
min-width: 70px;
}
ha-md-select#type {
min-width: 100px;
}
@media all and (max-width: 450px) {
ha-md-textfield#value {
min-width: 60px;
margin: 0 -8px;
}
ha-md-select#type {
min-width: 120px;
width: 120px;
}
}
ha-expansion-panel {
--expansion-panel-summary-padding: 0 16px;
--expansion-panel-content-padding: 0 16px;
margin-bottom: 16px;
}
ha-md-list-item.days {
--md-item-align-items: flex-start;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-backup-config-retention": HaBackupConfigRetention;
}
}

View File

@ -1,10 +1,8 @@
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { formatTime } from "../../../../../common/datetime/format_time"; import { formatTime } from "../../../../../common/datetime/format_time";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import { clamp } from "../../../../../common/number/clamp";
import "../../../../../components/ha-checkbox"; import "../../../../../components/ha-checkbox";
import type { HaCheckbox } from "../../../../../components/ha-checkbox"; import type { HaCheckbox } from "../../../../../components/ha-checkbox";
import "../../../../../components/ha-expansion-panel"; import "../../../../../components/ha-expansion-panel";
@ -15,10 +13,13 @@ import "../../../../../components/ha-md-select";
import type { HaMdSelect } from "../../../../../components/ha-md-select"; import type { HaMdSelect } from "../../../../../components/ha-md-select";
import "../../../../../components/ha-md-select-option"; import "../../../../../components/ha-md-select-option";
import "../../../../../components/ha-md-textfield"; import "../../../../../components/ha-md-textfield";
import "../../../../../components/ha-switch";
import "../../../../../components/ha-time-input"; import "../../../../../components/ha-time-input";
import "../../../../../components/ha-tip"; import "../../../../../components/ha-tip";
import type { BackupConfig, BackupDay } from "../../../../../data/backup"; import type {
BackupConfig,
BackupDay,
Retention,
} from "../../../../../data/backup";
import { import {
BACKUP_DAYS, BACKUP_DAYS,
BackupScheduleRecurrence, BackupScheduleRecurrence,
@ -29,76 +30,32 @@ import {
import type { SupervisorUpdateConfig } from "../../../../../data/supervisor/update"; import type { SupervisorUpdateConfig } from "../../../../../data/supervisor/update";
import type { HomeAssistant } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";
import { documentationUrl } from "../../../../../util/documentation-url"; import { documentationUrl } from "../../../../../util/documentation-url";
import "./ha-backup-config-retention";
export type BackupConfigSchedule = Pick<BackupConfig, "schedule" | "retention">; export type BackupConfigSchedule = Pick<BackupConfig, "schedule" | "retention">;
const MIN_VALUE = 1;
const MAX_VALUE = 50;
enum RetentionPreset {
COPIES_3 = "copies_3",
FOREVER = "forever",
CUSTOM = "custom",
}
enum BackupScheduleTime { enum BackupScheduleTime {
DEFAULT = "default", DEFAULT = "default",
CUSTOM = "custom", CUSTOM = "custom",
} }
interface RetentionData {
type: "copies" | "days" | "forever";
value: number;
}
const RETENTION_PRESETS: Record<
Exclude<RetentionPreset, RetentionPreset.CUSTOM>,
RetentionData
> = {
copies_3: { type: "copies", value: 3 },
forever: { type: "forever", value: 0 },
};
const SCHEDULE_OPTIONS = [ const SCHEDULE_OPTIONS = [
BackupScheduleRecurrence.NEVER, BackupScheduleRecurrence.NEVER,
BackupScheduleRecurrence.DAILY, BackupScheduleRecurrence.DAILY,
BackupScheduleRecurrence.CUSTOM_DAYS, BackupScheduleRecurrence.CUSTOM_DAYS,
] as const satisfies BackupScheduleRecurrence[]; ] as const satisfies BackupScheduleRecurrence[];
const RETENTION_PRESETS_OPTIONS = [
RetentionPreset.COPIES_3,
RetentionPreset.FOREVER,
RetentionPreset.CUSTOM,
] as const satisfies RetentionPreset[];
const SCHEDULE_TIME_OPTIONS = [ const SCHEDULE_TIME_OPTIONS = [
BackupScheduleTime.DEFAULT, BackupScheduleTime.DEFAULT,
BackupScheduleTime.CUSTOM, BackupScheduleTime.CUSTOM,
] as const satisfies BackupScheduleTime[]; ] as const satisfies BackupScheduleTime[];
const computeRetentionPreset = (
data: RetentionData
): RetentionPreset | undefined => {
for (const [key, value] of Object.entries(RETENTION_PRESETS)) {
if (
value.type === data.type &&
(value.type === RetentionPreset.FOREVER || value.value === data.value)
) {
return key as RetentionPreset;
}
}
return RetentionPreset.CUSTOM;
};
interface FormData { interface FormData {
recurrence: BackupScheduleRecurrence; recurrence: BackupScheduleRecurrence;
time_option: BackupScheduleTime; time_option: BackupScheduleTime;
time?: string | null; time?: string | null;
days: BackupDay[]; days: BackupDay[];
retention: { retention: Retention;
type: "copies" | "days" | "forever";
value: number;
};
} }
const INITIAL_FORM_DATA: FormData = { const INITIAL_FORM_DATA: FormData = {
@ -106,8 +63,7 @@ const INITIAL_FORM_DATA: FormData = {
time_option: BackupScheduleTime.DEFAULT, time_option: BackupScheduleTime.DEFAULT,
days: [], days: [],
retention: { retention: {
type: "copies", copies: 3,
value: 3,
}, },
}; };
@ -122,17 +78,6 @@ class HaBackupConfigSchedule extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
public supervisorUpdateConfig?: SupervisorUpdateConfig; public supervisorUpdateConfig?: SupervisorUpdateConfig;
@state() private _retentionPreset?: RetentionPreset;
protected willUpdate(changedProperties: PropertyValues): void {
if (changedProperties.has("value")) {
if (this._retentionPreset !== RetentionPreset.CUSTOM) {
const data = this._getData(this.value);
this._retentionPreset = computeRetentionPreset(data.retention);
}
}
}
private _getData = memoizeOne((value?: BackupConfigSchedule): FormData => { private _getData = memoizeOne((value?: BackupConfigSchedule): FormData => {
if (!value) { if (!value) {
return INITIAL_FORM_DATA; return INITIAL_FORM_DATA;
@ -150,15 +95,7 @@ class HaBackupConfigSchedule extends LitElement {
config.schedule.recurrence === BackupScheduleRecurrence.CUSTOM_DAYS config.schedule.recurrence === BackupScheduleRecurrence.CUSTOM_DAYS
? config.schedule.days ? config.schedule.days
: [], : [],
retention: { retention: config.retention,
type:
config.retention.days === null && config.retention.copies === null
? "forever"
: config.retention.days != null
? "days"
: "copies",
value: config.retention.days ?? config.retention.copies ?? 3,
},
}; };
}); });
@ -173,12 +110,7 @@ class HaBackupConfigSchedule extends LitElement {
? data.days ? data.days
: [], : [],
}, },
retention: retention: data.retention,
data.retention.type === "forever"
? { days: null, copies: null }
: data.retention.type === "days"
? { days: data.retention.value, copies: null }
: { copies: data.retention.value, days: null },
}; };
fireEvent(this, "value-changed", { value: this.value }); fireEvent(this, "value-changed", { value: this.value });
@ -377,81 +309,11 @@ class HaBackupConfigSchedule extends LitElement {
` `
: nothing} : nothing}
<ha-md-list-item> <ha-backup-config-retention
<span slot="headline"> .hass=${this.hass}
${this.hass.localize(`ui.panel.config.backup.schedule.retention`)} .retention=${data.retention}
</span> @value-changed=${this._retentionChanged}
<span slot="supporting-text"> ></ha-backup-config-retention>
${this.hass.localize(
`ui.panel.config.backup.schedule.retention_description`
)}
</span>
<ha-md-select
slot="end"
@change=${this._retentionPresetChanged}
.value=${this._retentionPreset ?? ""}
>
${RETENTION_PRESETS_OPTIONS.map(
(option) => html`
<ha-md-select-option .value=${option}>
<div slot="headline">
${this.hass.localize(
`ui.panel.config.backup.schedule.retention_presets.${option}`
)}
</div>
</ha-md-select-option>
`
)}
</ha-md-select>
</ha-md-list-item>
${this._retentionPreset === RetentionPreset.CUSTOM
? html`<ha-expansion-panel
expanded
.header=${this.hass.localize(
"ui.panel.config.backup.schedule.custom_retention"
)}
outlined
>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.schedule.custom_retention_label"
)}
</span>
<ha-md-textfield
slot="end"
@change=${this._retentionValueChanged}
.value=${data.retention.value.toString()}
id="value"
type="number"
.min=${MIN_VALUE.toString()}
.max=${MAX_VALUE.toString()}
step="1"
>
</ha-md-textfield>
<ha-md-select
slot="end"
@change=${this._retentionTypeChanged}
.value=${data.retention.type}
id="type"
>
<ha-md-select-option value="days">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.backup.schedule.retention_units.days"
)}
</div>
</ha-md-select-option>
<ha-md-select-option value="copies">
${this.hass.localize(
"ui.panel.config.backup.schedule.retention_units.copies"
)}
</ha-md-select-option>
</ha-md-select>
</ha-md-list-item></ha-expansion-panel
> `
: nothing}
<ha-tip .hass=${this.hass} <ha-tip .hass=${this.hass}
>${this.hass.localize("ui.panel.config.backup.schedule.tip", { >${this.hass.localize("ui.panel.config.backup.schedule.tip", {
backup_create: html`<a backup_create: html`<a
@ -543,65 +405,18 @@ class HaBackupConfigSchedule extends LitElement {
}); });
} }
private _retentionPresetChanged(ev) { private _retentionChanged(ev: CustomEvent<{ value: Retention }>) {
ev.stopPropagation(); ev.stopPropagation();
const target = ev.currentTarget as HaMdSelect; const retention = ev.detail.value;
let value = target.value as RetentionPreset;
// custom needs to have a type of days or copies, set it to default copies 3
if (
value === RetentionPreset.CUSTOM &&
this._retentionPreset === RetentionPreset.FOREVER
) {
this._retentionPreset = value;
value = RetentionPreset.COPIES_3;
} else {
this._retentionPreset = value;
}
if (value !== RetentionPreset.CUSTOM) {
const data = this._getData(this.value); const data = this._getData(this.value);
const retention = RETENTION_PRESETS[value];
// Ensure we have at least 1 in default value because user can't select 0 const newData = {
if (value !== RetentionPreset.FOREVER) {
retention.value = Math.max(retention.value, 1);
}
this._setData({
...data, ...data,
retention, retention,
}); };
}
}
private _retentionValueChanged(ev) { this._setData(newData);
ev.stopPropagation();
const target = ev.currentTarget as HaMdSelect;
const value = parseInt(target.value);
const clamped = clamp(value, MIN_VALUE, MAX_VALUE);
const data = this._getData(this.value);
target.value = clamped.toString();
this._setData({
...data,
retention: {
...data.retention,
value: clamped,
},
});
}
private _retentionTypeChanged(ev) {
ev.stopPropagation();
const target = ev.currentTarget as HaMdSelect;
const value = target.value as "copies" | "days";
const data = this._getData(this.value);
this._setData({
...data,
retention: {
...data.retention,
type: value,
},
});
} }
static styles = css` static styles = css`
@ -631,25 +446,7 @@ class HaBackupConfigSchedule extends LitElement {
width: 145px; width: 145px;
} }
} }
ha-md-textfield#value {
min-width: 70px;
}
ha-md-select#type {
min-width: 100px;
}
@media all and (max-width: 450px) {
ha-md-textfield#value {
min-width: 60px;
margin: 0 -8px;
}
ha-md-select#type {
min-width: 120px;
width: 120px;
}
}
ha-expansion-panel { ha-expansion-panel {
--expansion-panel-summary-padding: 0 16px;
--expansion-panel-content-padding: 0 16px;
margin-bottom: 16px; margin-bottom: 16px;
} }
ha-tip { ha-tip {

View File

@ -1,32 +1,35 @@
import type { PropertyValues } from "lit"; import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert"; import "../../../components/ha-alert";
import "../../../components/ha-button"; import "../../../components/ha-button";
import "../../../components/ha-switch";
import "../../../components/ha-button-menu"; import "../../../components/ha-button-menu";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-fade-in"; import "../../../components/ha-fade-in";
import "../../../components/ha-spinner";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-md-list"; import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item"; import "../../../components/ha-md-list-item";
import "../../../components/ha-spinner";
import "../../../components/ha-switch";
import type { import type {
BackupAgent, BackupAgent,
BackupAgentConfig, BackupAgentConfig,
BackupConfig, BackupConfig,
Retention,
} from "../../../data/backup"; } from "../../../data/backup";
import { import {
CLOUD_AGENT, CLOUD_AGENT,
computeBackupAgentName, computeBackupAgentName,
fetchBackupAgentsInfo, fetchBackupAgentsInfo,
isLocalAgent,
updateBackupConfig, updateBackupConfig,
} from "../../../data/backup"; } from "../../../data/backup";
import "../../../layouts/hass-subpage"; import "../../../layouts/hass-subpage";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import "./components/ha-backup-data-picker";
import { showConfirmationDialog } from "../../lovelace/custom-card-helpers"; import { showConfirmationDialog } from "../../lovelace/custom-card-helpers";
import { fireEvent } from "../../../common/dom/fire_event"; import "./components/config/ha-backup-config-retention";
import "./components/ha-backup-data-picker";
@customElement("ha-config-backup-location") @customElement("ha-config-backup-location")
class HaConfigBackupDetails extends LitElement { class HaConfigBackupDetails extends LitElement {
@ -61,18 +64,21 @@ class HaConfigBackupDetails extends LitElement {
const encrypted = this._isEncryptionTurnedOn(); const encrypted = this._isEncryptionTurnedOn();
return html` const agentName =
<hass-subpage (this._agent &&
back-path="/config/backup/settings"
.hass=${this.hass}
.narrow=${this.narrow}
.header=${(this._agent &&
computeBackupAgentName( computeBackupAgentName(
this.hass.localize, this.hass.localize,
this.agentId, this.agentId,
this.agents this.agents
)) || )) ||
this.hass.localize("ui.panel.config.backup.location.header")} this.hass.localize("ui.panel.config.backup.location.header");
return html`
<hass-subpage
back-path="/config/backup/settings"
.hass=${this.hass}
.narrow=${this.narrow}
.header=${agentName}
> >
<div class="content"> <div class="content">
${this._error && ${this._error &&
@ -96,14 +102,14 @@ class HaConfigBackupDetails extends LitElement {
><ha-spinner></ha-spinner ><ha-spinner></ha-spinner
></ha-fade-in>` ></ha-fade-in>`
: html` : html`
${CLOUD_AGENT === this.agentId
? html`
<ha-card> <ha-card>
<div class="card-header"> <div class="card-header">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.backup.location.configuration.title" "ui.panel.config.backup.location.configuration.title"
)} )}
</div> </div>
${CLOUD_AGENT === this.agentId
? html`
<div class="card-content"> <div class="card-content">
<p> <p>
${this.hass.localize( ${this.hass.localize(
@ -111,9 +117,21 @@ class HaConfigBackupDetails extends LitElement {
)} )}
</p> </p>
</div> </div>
</ha-card>
` `
: this.config?.agents[this.agentId]
? html`<ha-backup-config-retention
location-specific
.headline=${this.hass.localize(
`ui.panel.config.backup.location.retention_for_${isLocalAgent(this.agentId) ? "this_system" : "location"}`,
{ location: agentName }
)}
.hass=${this.hass}
.retention=${this.config?.agents[this.agentId]
?.retention}
@value-changed=${this._retentionChanged}
></ha-backup-config-retention>`
: nothing} : nothing}
</ha-card>
<ha-card> <ha-card>
<div class="card-header"> <div class="card-header">
${this.hass.localize( ${this.hass.localize(
@ -247,18 +265,37 @@ class HaConfigBackupDetails extends LitElement {
} }
} }
private async _updateAgentEncryption(value: boolean) { private async _updateAgentConfig(config: Partial<BackupAgentConfig>) {
const agentsConfig = { try {
...this.config?.agents, const agents = this.config?.agents || {};
[this.agentId]: { agents[this.agentId] = {
...this.config?.agents[this.agentId], ...(agents[this.agentId] || {}),
protected: value, ...config,
},
}; };
await updateBackupConfig(this.hass, { await updateBackupConfig(this.hass, {
agents: agentsConfig, agents,
}); });
fireEvent(this, "ha-refresh-backup-config"); fireEvent(this, "ha-refresh-backup-config");
} catch (err: any) {
this._error = this.hass.localize(
"ui.panel.config.backup.location.save_error",
{ error: err.message }
);
}
}
private _retentionChanged(ev: CustomEvent<{ value: Retention }>) {
const retention = ev.detail.value;
this._updateAgentConfig({
retention,
});
}
private async _updateAgentEncryption(value: boolean) {
this._updateAgentConfig({
protected: value,
});
} }
private _turnOnEncryption() { private _turnOnEncryption() {
@ -363,6 +400,10 @@ class HaConfigBackupDetails extends LitElement {
ha-spinner { ha-spinner {
margin: 24px auto; margin: 24px auto;
} }
ha-backup-config-retention {
display: block;
padding: 16px;
}
`; `;
} }

View File

@ -2533,6 +2533,7 @@
"custom_retention_label": "Keep only", "custom_retention_label": "Keep only",
"retention_description": "Based on the maximum number of backups or how many days they should be kept.", "retention_description": "Based on the maximum number of backups or how many days they should be kept.",
"retention_presets": { "retention_presets": {
"global": "Use global settings",
"copies_3": "3 backups", "copies_3": "3 backups",
"forever": "Forever", "forever": "Forever",
"custom": "Custom" "custom": "Custom"
@ -2762,6 +2763,9 @@
}, },
"location": { "location": {
"header": "Location", "header": "Location",
"save_error": "Error saving configuration: {error}",
"retention_for_this_system": "Retention for this system",
"retention_for_location": "Retention for {location}",
"not_found": "Not found", "not_found": "Not found",
"not_found_description": "Location matching ''{backupId}'' not found", "not_found_description": "Location matching ''{backupId}'' not found",
"error": "Could not fetch location details", "error": "Could not fetch location details",