Add time option for backup schedule (#23757)

Co-authored-by: Wendelin <w@pe8.at>
This commit is contained in:
Bram Kragten 2025-01-21 11:58:07 +01:00 committed by GitHub
parent e994e3565d
commit 7535d66373
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 621 additions and 259 deletions

View File

@ -337,6 +337,7 @@ export class HaBaseTimeInput extends LitElement {
}
.time-input-wrap {
display: flex;
flex: 1;
border-radius: var(--mdc-shape-small, 4px) var(--mdc-shape-small, 4px) 0 0;
overflow: hidden;
position: relative;
@ -345,6 +346,7 @@ export class HaBaseTimeInput extends LitElement {
}
ha-textfield {
width: 55px;
flex-grow: 1;
text-align: center;
--mdc-shape-small: 0;
--text-field-appearance: none;

View File

@ -17,6 +17,7 @@ export class HaMdListItem extends MdListItem {
}
md-item {
overflow: var(--md-item-overflow, hidden);
align-items: var(--md-item-align-items, center);
}
`,
];

View File

@ -11,22 +11,33 @@ import type { HomeAssistant } from "../types";
import { fileDownload } from "../util/file_download";
import { domainToName } from "./integration";
import type { FrontendLocaleData } from "./translation";
import checkValidDate from "../common/datetime/check_valid_date";
export const enum BackupScheduleState {
export const enum BackupScheduleRecurrence {
NEVER = "never",
DAILY = "daily",
MONDAY = "mon",
TUESDAY = "tue",
WEDNESDAY = "wed",
THURSDAY = "thu",
FRIDAY = "fri",
SATURDAY = "sat",
SUNDAY = "sun",
CUSTOM_DAYS = "custom_days",
}
export type BackupDay = "mon" | "tue" | "wed" | "thu" | "fri" | "sat" | "sun";
export const BACKUP_DAYS: BackupDay[] = [
"mon",
"tue",
"wed",
"thu",
"fri",
"sat",
"sun",
];
export const sortWeekdays = (weekdays) =>
weekdays.sort((a, b) => BACKUP_DAYS.indexOf(a) - BACKUP_DAYS.indexOf(b));
export interface BackupConfig {
last_attempted_automatic_backup: string | null;
last_completed_automatic_backup: string | null;
next_automatic_backup: string | null;
create_backup: {
agent_ids: string[];
include_addons: string[] | null;
@ -41,7 +52,9 @@ export interface BackupConfig {
days?: number | null;
};
schedule: {
state: BackupScheduleState;
recurrence: BackupScheduleRecurrence;
time?: string | null;
days: BackupDay[];
};
}
@ -59,7 +72,11 @@ export interface BackupMutableConfig {
copies?: number | null;
days?: number | null;
};
schedule?: BackupScheduleState;
schedule?: {
recurrence: BackupScheduleRecurrence;
time?: string | null;
days?: BackupDay[] | null;
};
}
export interface BackupAgent {
@ -337,9 +354,34 @@ export const downloadEmergencyKit = (
geneateEmergencyKitFileName(hass, appendFileName)
);
export const DEFAULT_OPTIMIZED_BACKUP_START_TIME = setMinutes(
setHours(new Date(), 4),
45
);
export const DEFAULT_OPTIMIZED_BACKUP_END_TIME = setMinutes(
setHours(new Date(), 5),
45
);
export const getFormattedBackupTime = memoizeOne(
(locale: FrontendLocaleData, config: HassConfig) => {
const date = setMinutes(setHours(new Date(), 4), 45);
(
locale: FrontendLocaleData,
config: HassConfig,
backupTime?: Date | string | null
) => {
if (checkValidDate(backupTime as Date)) {
return formatTime(backupTime as Date, locale, config);
}
if (typeof backupTime === "string" && backupTime) {
const splitted = backupTime.split(":");
const date = setMinutes(
setHours(new Date(), parseInt(splitted[0])),
parseInt(splitted[1])
);
return formatTime(date, locale, config);
}
return `${formatTime(DEFAULT_OPTIMIZED_BACKUP_START_TIME, locale, config)} - ${formatTime(DEFAULT_OPTIMIZED_BACKUP_END_TIME, locale, config)}`;
}
);

View File

@ -12,12 +12,22 @@ import type { HaMdSelect } from "../../../../../components/ha-md-select";
import "../../../../../components/ha-md-select-option";
import "../../../../../components/ha-md-textfield";
import "../../../../../components/ha-switch";
import type { BackupConfig } from "../../../../../data/backup";
import type { BackupConfig, BackupDay } from "../../../../../data/backup";
import {
BackupScheduleState,
getFormattedBackupTime,
BACKUP_DAYS,
BackupScheduleRecurrence,
DEFAULT_OPTIMIZED_BACKUP_END_TIME,
DEFAULT_OPTIMIZED_BACKUP_START_TIME,
sortWeekdays,
} from "../../../../../data/backup";
import type { HomeAssistant } from "../../../../../types";
import "../../../../../components/ha-time-input";
import "../../../../../components/ha-tip";
import "../../../../../components/ha-expansion-panel";
import "../../../../../components/ha-checkbox";
import "../../../../../components/ha-formfield";
import { formatTime } from "../../../../../common/datetime/format_time";
import { documentationUrl } from "../../../../../util/documentation-url";
export type BackupConfigSchedule = Pick<BackupConfig, "schedule" | "retention">;
@ -30,6 +40,11 @@ enum RetentionPreset {
CUSTOM = "custom",
}
enum BackupScheduleTime {
DEFAULT = "default",
CUSTOM = "custom",
}
interface RetentionData {
type: "copies" | "days";
value: number;
@ -44,15 +59,10 @@ const RETENTION_PRESETS: Record<
};
const SCHEDULE_OPTIONS = [
BackupScheduleState.DAILY,
BackupScheduleState.MONDAY,
BackupScheduleState.TUESDAY,
BackupScheduleState.WEDNESDAY,
BackupScheduleState.THURSDAY,
BackupScheduleState.FRIDAY,
BackupScheduleState.SATURDAY,
BackupScheduleState.SUNDAY,
] as const satisfies BackupScheduleState[];
BackupScheduleRecurrence.NEVER,
BackupScheduleRecurrence.DAILY,
BackupScheduleRecurrence.CUSTOM_DAYS,
] as const satisfies BackupScheduleRecurrence[];
const RETENTION_PRESETS_OPTIONS = [
RetentionPreset.COPIES_3,
@ -60,6 +70,11 @@ const RETENTION_PRESETS_OPTIONS = [
RetentionPreset.CUSTOM,
] as const satisfies RetentionPreset[];
const SCHEDULE_TIME_OPTIONS = [
BackupScheduleTime.DEFAULT,
BackupScheduleTime.CUSTOM,
] as const satisfies BackupScheduleTime[];
const computeRetentionPreset = (
data: RetentionData
): RetentionPreset | undefined => {
@ -72,8 +87,10 @@ const computeRetentionPreset = (
};
interface FormData {
enabled: boolean;
schedule: BackupScheduleState;
recurrence: BackupScheduleRecurrence;
time_option: BackupScheduleTime;
time?: string | null;
days: BackupDay[];
retention: {
type: "copies" | "days";
value: number;
@ -81,8 +98,9 @@ interface FormData {
}
const INITIAL_FORM_DATA: FormData = {
enabled: false,
schedule: BackupScheduleState.NEVER,
recurrence: BackupScheduleRecurrence.NEVER,
time_option: BackupScheduleTime.DEFAULT,
days: [],
retention: {
type: "copies",
value: 3,
@ -114,8 +132,15 @@ class HaBackupConfigSchedule extends LitElement {
const config = value;
return {
enabled: config.schedule.state !== BackupScheduleState.NEVER,
schedule: config.schedule.state,
recurrence: config.schedule.recurrence,
time_option: config.schedule.time
? BackupScheduleTime.CUSTOM
: BackupScheduleTime.DEFAULT,
time: config.schedule.time,
days:
config.schedule.recurrence === BackupScheduleRecurrence.CUSTOM_DAYS
? config.schedule.days
: [],
retention: {
type: config.retention.days != null ? "days" : "copies",
value: config.retention.days ?? config.retention.copies ?? 3,
@ -125,8 +150,14 @@ class HaBackupConfigSchedule extends LitElement {
private _setData(data: FormData) {
this.value = {
...this.value,
schedule: {
state: data.enabled ? data.schedule : BackupScheduleState.NEVER,
recurrence: data.recurrence,
time: data.time_option === BackupScheduleTime.CUSTOM ? data.time : null,
days:
data.recurrence === BackupScheduleRecurrence.CUSTOM_DAYS
? data.days
: [],
},
retention:
data.retention.type === "days"
@ -140,31 +171,14 @@ class HaBackupConfigSchedule extends LitElement {
protected render() {
const data = this._getData(this.value);
const time = getFormattedBackupTime(this.hass.locale, this.hass.config);
return html`
<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.schedule.use_automatic_backups"
)}
</span>
<ha-switch
slot="end"
@change=${this._enabledChanged}
.checked=${data.enabled}
></ha-switch>
</ha-md-list-item>
${data.enabled
? html`
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.schedule.schedule"
)}
</span>
)}</span
>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.schedule.schedule_description"
@ -174,15 +188,14 @@ class HaBackupConfigSchedule extends LitElement {
<ha-md-select
slot="end"
@change=${this._scheduleChanged}
.value=${data.schedule}
.value=${data.recurrence}
>
${SCHEDULE_OPTIONS.map(
(option) => html`
<ha-md-select-option .value=${option}>
<div slot="headline">
${this.hass.localize(
`ui.panel.config.backup.schedule.schedule_options.${option}`,
{ time }
`ui.panel.config.backup.schedule.schedule_options.${option}`
)}
</div>
</ha-md-select-option>
@ -190,11 +203,134 @@ class HaBackupConfigSchedule extends LitElement {
)}
</ha-md-select>
</ha-md-list-item>
${data.recurrence === BackupScheduleRecurrence.CUSTOM_DAYS
? html`<ha-expansion-panel
expanded
.header=${this.hass.localize(
"ui.panel.config.backup.schedule.custom_schedule"
)}
outlined
>
<ha-md-list-item class="days">
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.schedule.backup_every"
)}
</span>
<div slot="end">
${BACKUP_DAYS.map(
(day) => html`
<div>
<ha-formfield
.label=${this.hass.localize(`ui.panel.config.backup.overview.settings.weekdays.${day}`)}
>
<ha-checkbox
@change=${this._daysChanged}
.checked=${data.days.includes(day)}
.value=${day}
>
</ha-checkbox>
</span>
</ha-formfield>
</div>
`
)}
</div>
</ha-md-list-item>
</ha-expansion-panel>`
: nothing}
${data.recurrence === BackupScheduleRecurrence.DAILY ||
(data.recurrence === BackupScheduleRecurrence.CUSTOM_DAYS &&
data.days.length > 0)
? html`
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
`ui.panel.config.backup.schedule.retention`
"ui.panel.config.backup.schedule.time"
)}</span
>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.schedule.schedule_time_description"
)}
${data.time_option === BackupScheduleTime.DEFAULT
? this.hass.localize(
"ui.panel.config.backup.schedule.schedule_time_optimal_description",
{
time_range_start: formatTime(
DEFAULT_OPTIMIZED_BACKUP_START_TIME,
this.hass.locale,
this.hass.config
),
time_range_end: formatTime(
DEFAULT_OPTIMIZED_BACKUP_END_TIME,
this.hass.locale,
this.hass.config
),
}
)
: nothing}
</span>
<ha-md-select
slot="end"
@change=${this._scheduleTimeChanged}
.value=${data.time_option}
>
${SCHEDULE_TIME_OPTIONS.map(
(option) => html`
<ha-md-select-option .value=${option}>
<div slot="headline">
${this.hass.localize(
`ui.panel.config.backup.schedule.time_options.${option}`
)}
</div>
</ha-md-select-option>
`
)}
</ha-md-select>
</ha-md-list-item>
${data.time_option === BackupScheduleTime.CUSTOM
? html`<ha-expansion-panel
expanded
.header=${this.hass.localize(
"ui.panel.config.backup.schedule.custom_time"
)}
outlined
>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.schedule.custom_time_label"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.schedule.custom_time_description",
{
time: formatTime(
DEFAULT_OPTIMIZED_BACKUP_START_TIME,
this.hass.locale,
this.hass.config
),
}
)}
</span>
<ha-time-input
slot="end"
@value-changed=${this._timeChanged}
.value=${data.time ?? undefined}
.locale=${this.hass.locale}
>
</ha-time-input>
</ha-md-list-item>
</ha-expansion-panel>`
: nothing}
`
: nothing}
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(`ui.panel.config.backup.schedule.retention`)}
</span>
<span slot="supporting-text">
${this.hass.localize(
@ -204,7 +340,7 @@ class HaBackupConfigSchedule extends LitElement {
<ha-md-select
slot="end"
@change=${this._retentionPresetChanged}
.value=${this._retentionPreset}
.value=${this._retentionPreset ?? ""}
>
${RETENTION_PRESETS_OPTIONS.map(
(option) => html`
@ -219,17 +355,29 @@ class HaBackupConfigSchedule extends LitElement {
)}
</ha-md-select>
</ha-md-list-item>
${this._retentionPreset === RetentionPreset.CUSTOM
? html`
? 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}
.value=${data.retention.value.toString()}
id="value"
type="number"
.min=${MIN_VALUE}
.max=${MAX_VALUE}
.min=${MIN_VALUE.toString()}
.max=${MAX_VALUE.toString()}
step="1"
>
</ha-md-textfield>
@ -252,38 +400,87 @@ class HaBackupConfigSchedule extends LitElement {
)}
</ha-md-select-option>
</ha-md-select>
</ha-md-list-item>
`
: nothing}
`
</ha-md-list-item></ha-expansion-panel
> `
: nothing}
<ha-tip .hass=${this.hass}
>${this.hass.localize("ui.panel.config.backup.schedule.tip", {
backup_create: html`<a
href=${documentationUrl(
this.hass,
"/integrations/backup#example-backing-up-every-night-at-300-am"
)}
target="_blank"
rel="noopener noreferrer"
>backup.create</a
>`,
})}</ha-tip
>
</ha-md-list>
`;
}
private _enabledChanged(ev) {
ev.stopPropagation();
const target = ev.currentTarget as HaCheckbox;
const data = this._getData(this.value);
this._setData({
...data,
enabled: target.checked,
schedule: target.checked
? BackupScheduleState.DAILY
: BackupScheduleState.NEVER,
});
fireEvent(this, "value-changed", { value: this.value });
}
private _scheduleChanged(ev) {
ev.stopPropagation();
const target = ev.currentTarget as HaMdSelect;
const data = this._getData(this.value);
let days = [...data.days];
if (
target.value === BackupScheduleRecurrence.CUSTOM_DAYS &&
data.days.length === 0
) {
days = [...BACKUP_DAYS];
}
this._setData({
...data,
schedule: target.value as BackupScheduleState,
recurrence: target.value as BackupScheduleRecurrence,
days,
});
}
private _scheduleTimeChanged(ev) {
ev.stopPropagation();
const target = ev.currentTarget as HaMdSelect;
const data = this._getData(this.value);
this._setData({
...data,
time_option: target.value as BackupScheduleTime,
time: target.value === BackupScheduleTime.CUSTOM ? "04:45:00" : undefined,
});
}
private _timeChanged(ev) {
ev.stopPropagation();
const data = this._getData(this.value);
this._setData({
...data,
time: ev.detail.value,
});
}
private _daysChanged(ev) {
ev.stopPropagation();
const target = ev.currentTarget as HaCheckbox;
const value = target.value as BackupDay;
const data = this._getData(this.value);
const days = [...data.days];
if (target.checked && !data.days.includes(value)) {
days.push(value);
} else if (!target.checked && data.days.includes(value)) {
days.splice(days.indexOf(value), 1);
}
sortWeekdays(days);
this._setData({
...data,
days,
});
fireEvent(this, "value-changed", { value: this.value });
}
private _retentionPresetChanged(ev) {
@ -304,8 +501,6 @@ class HaBackupConfigSchedule extends LitElement {
retention: RETENTION_PRESETS[value],
});
}
fireEvent(this, "value-changed", { value: this.value });
}
private _retentionValueChanged(ev) {
@ -321,8 +516,6 @@ class HaBackupConfigSchedule extends LitElement {
value: clamped,
},
});
fireEvent(this, "value-changed", { value: this.value });
}
private _retentionTypeChanged(ev) {
@ -338,8 +531,6 @@ class HaBackupConfigSchedule extends LitElement {
type: value,
},
});
fireEvent(this, "value-changed", { value: this.value });
}
static styles = css`
@ -351,11 +542,13 @@ class HaBackupConfigSchedule extends LitElement {
ha-md-list-item {
--md-item-overflow: visible;
}
ha-md-select {
ha-md-select,
ha-time-input {
min-width: 210px;
}
@media all and (max-width: 450px) {
ha-md-select {
ha-md-select,
ha-time-input {
min-width: 160px;
}
}
@ -365,6 +558,21 @@ class HaBackupConfigSchedule extends LitElement {
ha-md-select#type {
min-width: 100px;
}
ha-expansion-panel {
--expansion-panel-summary-padding: 0 16px;
--expansion-panel-content-padding: 0 16px;
margin-bottom: 16px;
}
ha-tip {
text-align: unset;
margin: 16px 0;
}
ha-md-list-item.days {
--md-item-align-items: flex-start;
}
a {
color: var(--primary-color);
}
`;
}

View File

@ -11,7 +11,7 @@ import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-svg-icon";
import type { BackupConfig } from "../../../../../data/backup";
import {
BackupScheduleState,
BackupScheduleRecurrence,
computeBackupAgentName,
getFormattedBackupTime,
isLocalAgent,
@ -31,24 +31,87 @@ class HaBackupBackupsSummary extends LitElement {
private _scheduleDescription(config: BackupConfig): string {
const { copies, days } = config.retention;
const { state: schedule } = config.schedule;
const { recurrence } = config.schedule;
if (schedule === BackupScheduleState.NEVER) {
if (recurrence === BackupScheduleRecurrence.NEVER) {
return this.hass.localize(
"ui.panel.config.backup.overview.settings.schedule_never"
);
}
const time = getFormattedBackupTime(this.hass.locale, this.hass.config);
const scheduleText = this.hass.localize(
`ui.panel.config.backup.overview.settings.schedule_${schedule}`,
{ time }
const time: string | undefined | null =
this.config.schedule.time &&
getFormattedBackupTime(
this.hass.locale,
this.hass.config,
this.config.schedule.time
);
let scheduleText = this.hass.localize(
"ui.panel.config.backup.overview.settings.schedule_never"
);
const configDays = this.config.schedule.days;
if (
this.config.schedule.recurrence === BackupScheduleRecurrence.DAILY ||
(this.config.schedule.recurrence ===
BackupScheduleRecurrence.CUSTOM_DAYS &&
configDays.length === 7)
) {
scheduleText = this.hass.localize(
`ui.panel.config.backup.overview.settings.schedule_${!this.config.schedule.time ? "optimized_" : ""}daily`,
{
time,
}
);
} else if (
this.config.schedule.recurrence ===
BackupScheduleRecurrence.CUSTOM_DAYS &&
configDays.length !== 0
) {
if (
configDays.length === 2 &&
configDays.includes("sat") &&
configDays.includes("sun")
) {
scheduleText = this.hass.localize(
`ui.panel.config.backup.overview.settings.schedule_${!this.config.schedule.time ? "optimized_" : ""}weekend`,
{
time,
}
);
} else if (
configDays.length === 5 &&
!configDays.includes("sat") &&
!configDays.includes("sun")
) {
scheduleText = this.hass.localize(
`ui.panel.config.backup.overview.settings.schedule_${!this.config.schedule.time ? "optimized_" : ""}weekdays`,
{
time,
}
);
} else {
scheduleText = this.hass.localize(
`ui.panel.config.backup.overview.settings.schedule_${!this.config.schedule.time ? "optimized_" : ""}days`,
{
count: configDays.length,
days: configDays
.map((dayCode) =>
this.hass.localize(
`ui.panel.config.backup.overview.settings.${configDays.length > 2 ? "short_weekdays" : "weekdays"}.${dayCode}`
)
)
.join(", "),
time,
}
);
}
}
let copiesText = this.hass.localize(
`ui.panel.config.backup.overview.settings.schedule_copies_all`,
{ time }
`ui.panel.config.backup.overview.settings.schedule_copies_all`
);
if (copies) {
copiesText = this.hass.localize(

View File

@ -1,7 +1,7 @@
import { mdiBackupRestore, mdiCalendar } from "@mdi/js";
import { addHours, differenceInDays } from "date-fns";
import { addHours, differenceInDays, isToday, isTomorrow } from "date-fns";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { relativeTime } from "../../../../../common/datetime/relative_time";
@ -12,12 +12,13 @@ import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-svg-icon";
import type { BackupConfig, BackupContent } from "../../../../../data/backup";
import {
BackupScheduleState,
BackupScheduleRecurrence,
getFormattedBackupTime,
} from "../../../../../data/backup";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import "../ha-backup-summary-card";
import { formatDateWeekday } from "../../../../../common/datetime/format_date";
const OVERDUE_MARGIN_HOURS = 3;
@ -76,16 +77,6 @@ class HaBackupOverviewBackups extends LitElement {
const lastBackup = this._lastBackup(this.backups);
const backupTime = getFormattedBackupTime(
this.hass.locale,
this.hass.config
);
const nextBackupDescription = this.hass.localize(
`ui.panel.config.backup.overview.summary.next_backup_description.${this.config.schedule.state}`,
{ time: backupTime }
);
const lastAttemptDate = this.config.last_attempted_automatic_backup
? new Date(this.config.last_attempted_automatic_backup)
: new Date(0);
@ -94,6 +85,46 @@ class HaBackupOverviewBackups extends LitElement {
? new Date(this.config.last_completed_automatic_backup)
: new Date(0);
const nextAutomaticDate = this.config.next_automatic_backup
? new Date(this.config.next_automatic_backup)
: undefined;
const backupTime = getFormattedBackupTime(
this.hass.locale,
this.hass.config,
nextAutomaticDate || this.config.schedule.time
);
const nextBackupDescription =
this.config.schedule.recurrence === BackupScheduleRecurrence.NEVER ||
(this.config.schedule.recurrence ===
BackupScheduleRecurrence.CUSTOM_DAYS &&
this.config.schedule.days.length === 0)
? this.hass.localize(
`ui.panel.config.backup.overview.summary.no_automatic_backup`
)
: nextAutomaticDate
? this.hass.localize(
`ui.panel.config.backup.overview.summary.next_automatic_backup`,
{
day: isTomorrow(nextAutomaticDate)
? this.hass.localize(
"ui.panel.config.backup.overview.summary.tomorrow"
)
: isToday(nextAutomaticDate)
? this.hass.localize(
"ui.panel.config.backup.overview.summary.today"
)
: formatDateWeekday(
nextAutomaticDate,
this.hass.locale,
this.hass.config
),
time: backupTime,
}
)
: "";
// If last attempt is after last completed backup, show error
if (lastAttemptDate > lastCompletedDate) {
const lastUploadedBackup = this._lastUploadedBackup(this.backups);
@ -122,8 +153,13 @@ class HaBackupOverviewBackups extends LitElement {
)}
</span>
</ha-md-list-item>
${lastUploadedBackup || nextBackupDescription
? html`
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
<ha-svg-icon
slot="start"
.path=${mdiCalendar}
></ha-svg-icon>
<span slot="headline">
${lastUploadedBackup
? this.hass.localize(
@ -141,6 +177,8 @@ class HaBackupOverviewBackups extends LitElement {
: nextBackupDescription}
</span>
</ha-md-list-item>
`
: nothing}
</ha-md-list>
</ha-backup-summary-card>
`;
@ -164,10 +202,7 @@ class HaBackupOverviewBackups extends LitElement {
)}
</span>
</ha-md-list-item>
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
<span slot="headline">${nextBackupDescription}</span>
</ha-md-list-item>
${this._renderNextBackupDescription(nextBackupDescription)}
</ha-md-list>
</ha-backup-summary-card>
`;
@ -203,7 +238,9 @@ class HaBackupOverviewBackups extends LitElement {
)}
</span>
</ha-md-list-item>
<ha-md-list-item>
${lastUploadedBackup || nextBackupDescription
? html` <ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
<span slot="headline">
${lastUploadedBackup
@ -221,7 +258,8 @@ class HaBackupOverviewBackups extends LitElement {
)
: nextBackupDescription}
</span>
</ha-md-list-item>
</ha-md-list-item>`
: nothing}
</ha-md-list>
</ha-backup-summary-card>
`;
@ -248,51 +286,35 @@ class HaBackupOverviewBackups extends LitElement {
const isOverdue =
(numberOfDays >= 1 &&
this.config.schedule.state === BackupScheduleState.DAILY) ||
this.config.schedule.recurrence === BackupScheduleRecurrence.DAILY) ||
numberOfDays >= 7;
if (isOverdue) {
return html`
<ha-backup-summary-card
.heading=${this.hass.localize(
"ui.panel.config.backup.overview.summary.backup_too_old_heading",
`ui.panel.config.backup.overview.summary.${isOverdue ? "backup_too_old_heading" : "backup_success_heading"}`,
{ count: numberOfDays }
)}
status="warning"
.status=${isOverdue ? "warning" : "success"}
>
<ha-md-list>
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span slot="headline">${lastSuccessfulBackupDescription}</span>
</ha-md-list-item>
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
<span slot="headline">${nextBackupDescription}</span>
</ha-md-list-item>
${this._renderNextBackupDescription(nextBackupDescription)}
</ha-md-list>
</ha-backup-summary-card>
`;
}
return html`
<ha-backup-summary-card
.heading=${this.hass.localize(
"ui.panel.config.backup.overview.summary.backup_success_heading"
)}
status="success"
>
<ha-md-list>
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span slot="headline">${lastSuccessfulBackupDescription}</span>
</ha-md-list-item>
<ha-md-list-item>
private _renderNextBackupDescription(nextBackupDescription: string) {
return nextBackupDescription
? html` <ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
<span slot="headline">${nextBackupDescription}</span>
</ha-md-list-item>
</ha-md-list>
</ha-backup-summary-card>
`;
</ha-md-list-item>`
: nothing;
}
static get styles(): CSSResultGroup {

View File

@ -21,7 +21,7 @@ import type {
BackupMutableConfig,
} from "../../../../data/backup";
import {
BackupScheduleState,
BackupScheduleRecurrence,
CLOUD_AGENT,
CORE_LOCAL_AGENT,
downloadEmergencyKit,
@ -68,10 +68,13 @@ const RECOMMENDED_CONFIG: BackupConfig = {
days: null,
},
schedule: {
state: BackupScheduleState.DAILY,
recurrence: BackupScheduleRecurrence.DAILY,
time: null,
days: [],
},
last_attempted_automatic_backup: null,
last_completed_automatic_backup: null,
next_automatic_backup: null,
};
@customElement("ha-dialog-backup-onboarding")
@ -145,7 +148,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
include_database: this._config.create_backup.include_database,
agent_ids: this._config.create_backup.agent_ids,
},
schedule: this._config.schedule.state,
schedule: this._config.schedule,
retention: this._config.retention,
};

View File

@ -308,7 +308,7 @@ class HaConfigBackupSettings extends LitElement {
password: this._config!.create_backup.password,
},
retention: this._config!.retention,
schedule: this._config!.schedule.state,
schedule: this._config!.schedule,
});
fireEvent(this, "ha-refresh-backup-config");
}

View File

@ -2411,20 +2411,29 @@
"addons": "Add-ons"
},
"schedule": {
"use_automatic_backups": "Use automatic backups",
"schedule": "Schedule",
"backup_every": "Backup every",
"custom_schedule": "Custom schedule",
"time": "Time",
"custom_time": "Custom time",
"custom_time_label": "Backup at",
"custom_time_description": "The optimal time is after Home Assistant finished cleaning up the database. This is done at {time}.",
"schedule_description": "How often you want to create a backup.",
"schedule_time_description": "When the backup creation starts.",
"schedule_time_optimal_description": "By default Home Assistant picks the optimal time between {time_range_start} and {time_range_end}.",
"tip": "You can create your own custom backup automation with the {backup_create} action",
"schedule_options": {
"daily": "Daily at {time}",
"mon": "Monday at {time}",
"tue": "Tuesday at {time}",
"wed": "Wednesday at {time}",
"thu": "Thursday at {time}",
"fri": "Friday at {time}",
"sat": "Saturday at {time}",
"sun": "Sunday at {time}"
"never": "Never",
"daily": "Daily",
"custom_days": "Custom days"
},
"time_options": {
"default": "System optimal",
"custom": "Custom"
},
"retention": "Retention",
"custom_retention": "Custom retention",
"custom_retention_label": "Clean up every",
"retention_description": "Based on the maximum number of backups or how many days they should be kept.",
"retention_presets": {
"copies_3": "3 backups",
@ -2509,17 +2518,10 @@
}
},
"summary": {
"next_backup_description": {
"daily": "Next automatic backup tomorrow at {time}",
"mon": "Next automatic backup next Monday at {time}",
"tue": "Next automatic backup next Tuesday at {time}",
"wed": "Next automatic backup next Wednesday at {time}",
"thu": "Next automatic backup next Thursday at {time}",
"fri": "Next automatic backup next Friday at {time}",
"sat": "Next automatic backup next Saturday at {time}",
"sun": "Next automatic backup next Sunday at {time}",
"never": "No automatic backups scheduled"
},
"no_automatic_backup": "No automatic backups scheduled",
"next_automatic_backup": "Next automatic backup {day} at {time}",
"today": "today",
"tomorrow": "tomorrow",
"loading": "Loading backups...",
"last_backup_failed_heading": "Last automatic backup failed",
"last_backup_failed_description": "The last automatic backup triggered {relative_time} wasn't successful.",
@ -2545,13 +2547,13 @@
"schedule_copies_backups": "and keep {count} {count, plural,\n one {backup}\n other {backups}\n}",
"schedule_copies_days": "and keep {count} {count, plural,\n one {day}\n other {days}\n}",
"schedule_daily": "Daily at {time}",
"schedule_mon": "Weekly on Mondays at {time}",
"schedule_tue": "Weekly on Tuesdays at {time}",
"schedule_wed": "Weekly on Wednesdays at {time}",
"schedule_thu": "Weekly on Thursdays at {time}",
"schedule_fri": "Weekly on Fridays at {time}",
"schedule_sat": "Weekly on Saturdays at {time}",
"schedule_sun": "Weekly on Sundays at {time}",
"schedule_days": "Every {days} at {time}",
"schedule_weekdays": "Every weekday at {time}",
"schedule_optimized_weekdays": "Every weekday",
"schedule_weekend": "Every weekends at {time}",
"schedule_optimized_weekend": "Every weekends",
"schedule_optimized_daily": "Daily",
"schedule_optimized_days": "Every {days}",
"schedule_never": "Automatic backups are not scheduled",
"data": "Home Assistant data that is included",
"data_settings_history": "Settings and history",
@ -2564,7 +2566,26 @@
"locations_one": "Store in {name}",
"locations_many": "Store in {count} off-site {count, plural,\n one {location}\n other {locations}\n}",
"locations_local_only": "Local backup only",
"locations_none": "No locations configured"
"locations_none": "No locations configured",
"system_optimal_time": "system optimal time",
"weekdays": {
"mon": "[%key:ui::weekdays::monday%]",
"tue": "[%key:ui::weekdays::tuesday%]",
"wed": "[%key:ui::weekdays::wednesday%]",
"thu": "[%key:ui::weekdays::thursday%]",
"fri": "[%key:ui::weekdays::friday%]",
"sat": "[%key:ui::weekdays::saturday%]",
"sun": "[%key:ui::weekdays::sunday%]"
},
"short_weekdays": {
"mon": "Mo",
"tue": "Tu",
"wed": "We",
"thu": "Th",
"fri": "Fr",
"sat": "Sa",
"sun": "So"
}
}
},
"backups": {