diff --git a/src/components/ha-base-time-input.ts b/src/components/ha-base-time-input.ts index c3e3fcc985..2936e8c96f 100644 --- a/src/components/ha-base-time-input.ts +++ b/src/components/ha-base-time-input.ts @@ -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; diff --git a/src/components/ha-md-list-item.ts b/src/components/ha-md-list-item.ts index 982b9359c8..0920557f9a 100644 --- a/src/components/ha-md-list-item.ts +++ b/src/components/ha-md-list-item.ts @@ -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); } `, ]; diff --git a/src/data/backup.ts b/src/data/backup.ts index 9b470505a0..ca1c6a836f 100644 --- a/src/data/backup.ts +++ b/src/data/backup.ts @@ -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); - return formatTime(date, locale, config); + ( + 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)}`; } ); diff --git a/src/panels/config/backup/components/config/ha-backup-config-schedule.ts b/src/panels/config/backup/components/config/ha-backup-config-schedule.ts index ca5bb2cf18..958ae2f094 100644 --- a/src/panels/config/backup/components/config/ha-backup-config-schedule.ts +++ b/src/panels/config/backup/components/config/ha-backup-config-schedule.ts @@ -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; @@ -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,49 +171,118 @@ class HaBackupConfigSchedule extends LitElement { protected render() { const data = this._getData(this.value); - const time = getFormattedBackupTime(this.hass.locale, this.hass.config); - return html` ${this.hass.localize( - "ui.panel.config.backup.schedule.use_automatic_backups" + "ui.panel.config.backup.schedule.schedule" + )} + + ${this.hass.localize( + "ui.panel.config.backup.schedule.schedule_description" )} - + @change=${this._scheduleChanged} + .value=${data.recurrence} + > + ${SCHEDULE_OPTIONS.map( + (option) => html` + +
+ ${this.hass.localize( + `ui.panel.config.backup.schedule.schedule_options.${option}` + )} +
+
+ ` + )} +
- ${data.enabled + ${data.recurrence === BackupScheduleRecurrence.CUSTOM_DAYS + ? html` + + + ${this.hass.localize( + "ui.panel.config.backup.schedule.backup_every" + )} + +
+ ${BACKUP_DAYS.map( + (day) => html` +
+ + + + + +
+ ` + )} +
+
+
` + : nothing} + ${data.recurrence === BackupScheduleRecurrence.DAILY || + (data.recurrence === BackupScheduleRecurrence.CUSTOM_DAYS && + data.days.length > 0) ? html` ${this.hass.localize( - "ui.panel.config.backup.schedule.schedule" - )} - + "ui.panel.config.backup.schedule.time" + )} ${this.hass.localize( - "ui.panel.config.backup.schedule.schedule_description" + "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} - ${SCHEDULE_OPTIONS.map( + ${SCHEDULE_TIME_OPTIONS.map( (option) => html`
${this.hass.localize( - `ui.panel.config.backup.schedule.schedule_options.${option}`, - { time } + `ui.panel.config.backup.schedule.time_options.${option}` )}
@@ -190,100 +290,197 @@ class HaBackupConfigSchedule extends LitElement { )}
- - - ${this.hass.localize( - `ui.panel.config.backup.schedule.retention` - )} - - - ${this.hass.localize( - `ui.panel.config.backup.schedule.retention_description` - )} - - - ${RETENTION_PRESETS_OPTIONS.map( - (option) => html` - -
- ${this.hass.localize( - `ui.panel.config.backup.schedule.retention_presets.${option}` - )} -
-
- ` - )} -
-
- ${this._retentionPreset === RetentionPreset.CUSTOM - ? html` + ${data.time_option === BackupScheduleTime.CUSTOM + ? html` - + ${this.hass.localize( + "ui.panel.config.backup.schedule.custom_time_label" + )} + + + ${this.hass.localize( + "ui.panel.config.backup.schedule.custom_time_description", + { + time: formatTime( + DEFAULT_OPTIMIZED_BACKUP_START_TIME, + this.hass.locale, + this.hass.config + ), + } + )} + + - - - -
- ${this.hass.localize( - "ui.panel.config.backup.schedule.retention_units.days" - )} -
-
- - ${this.hass.localize( - "ui.panel.config.backup.schedule.retention_units.copies" - )} - -
+
- ` +
` : nothing} ` : nothing} + + + ${this.hass.localize(`ui.panel.config.backup.schedule.retention`)} + + + ${this.hass.localize( + `ui.panel.config.backup.schedule.retention_description` + )} + + + ${RETENTION_PRESETS_OPTIONS.map( + (option) => html` + +
+ ${this.hass.localize( + `ui.panel.config.backup.schedule.retention_presets.${option}` + )} +
+
+ ` + )} +
+
+ + ${this._retentionPreset === RetentionPreset.CUSTOM + ? html` + + + ${this.hass.localize( + "ui.panel.config.backup.schedule.custom_retention_label" + )} + + + + + +
+ ${this.hass.localize( + "ui.panel.config.backup.schedule.retention_units.days" + )} +
+
+ + ${this.hass.localize( + "ui.panel.config.backup.schedule.retention_units.copies" + )} + +
+
` + : nothing} + ${this.hass.localize("ui.panel.config.backup.schedule.tip", { + backup_create: html`backup.create`, + })}
`; } - 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); + } `; } diff --git a/src/panels/config/backup/components/overview/ha-backup-overview-settings.ts b/src/panels/config/backup/components/overview/ha-backup-overview-settings.ts index 97d13ca84c..499c8313eb 100644 --- a/src/panels/config/backup/components/overview/ha-backup-overview-settings.ts +++ b/src/panels/config/backup/components/overview/ha-backup-overview-settings.ts @@ -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 time: string | undefined | null = + this.config.schedule.time && + getFormattedBackupTime( + this.hass.locale, + this.hass.config, + this.config.schedule.time + ); - const scheduleText = this.hass.localize( - `ui.panel.config.backup.overview.settings.schedule_${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( diff --git a/src/panels/config/backup/components/overview/ha-backup-overview-summary.ts b/src/panels/config/backup/components/overview/ha-backup-overview-summary.ts index df04e12519..20811753d6 100644 --- a/src/panels/config/backup/components/overview/ha-backup-overview-summary.ts +++ b/src/panels/config/backup/components/overview/ha-backup-overview-summary.ts @@ -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,25 +153,32 @@ class HaBackupOverviewBackups extends LitElement { )} - - - - ${lastUploadedBackup - ? this.hass.localize( - "ui.panel.config.backup.overview.summary.last_successful_backup_description", - { - relative_time: relativeTime( - new Date(lastUploadedBackup.date), - this.hass.locale, - now, - true - ), - count: lastUploadedBackup.agent_ids?.length ?? 0, - } - ) - : nextBackupDescription} - - + ${lastUploadedBackup || nextBackupDescription + ? html` + + + + ${lastUploadedBackup + ? this.hass.localize( + "ui.panel.config.backup.overview.summary.last_successful_backup_description", + { + relative_time: relativeTime( + new Date(lastUploadedBackup.date), + this.hass.locale, + now, + true + ), + count: lastUploadedBackup.agent_ids?.length ?? 0, + } + ) + : nextBackupDescription} + + + ` + : nothing} `; @@ -164,10 +202,7 @@ class HaBackupOverviewBackups extends LitElement { )} - - - ${nextBackupDescription} - + ${this._renderNextBackupDescription(nextBackupDescription)} `; @@ -203,25 +238,28 @@ class HaBackupOverviewBackups extends LitElement { )} - - - - ${lastUploadedBackup - ? this.hass.localize( - "ui.panel.config.backup.overview.summary.last_successful_backup_description", - { - relative_time: relativeTime( - new Date(lastUploadedBackup.date), - this.hass.locale, - now, - true - ), - count: lastUploadedBackup.agent_ids?.length ?? 0, - } - ) - : nextBackupDescription} - - + + ${lastUploadedBackup || nextBackupDescription + ? html` + + + ${lastUploadedBackup + ? this.hass.localize( + "ui.panel.config.backup.overview.summary.last_successful_backup_description", + { + relative_time: relativeTime( + new Date(lastUploadedBackup.date), + this.hass.locale, + now, + true + ), + count: lastUploadedBackup.agent_ids?.length ?? 0, + } + ) + : nextBackupDescription} + + ` + : nothing} `; @@ -248,53 +286,37 @@ 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` - - - - - ${lastSuccessfulBackupDescription} - - - - ${nextBackupDescription} - - - - `; - } - return html` ${lastSuccessfulBackupDescription} - - - ${nextBackupDescription} - + ${this._renderNextBackupDescription(nextBackupDescription)} `; } + private _renderNextBackupDescription(nextBackupDescription: string) { + return nextBackupDescription + ? html` + + ${nextBackupDescription} + ` + : nothing; + } + static get styles(): CSSResultGroup { return [ haStyle, diff --git a/src/panels/config/backup/dialogs/dialog-backup-onboarding.ts b/src/panels/config/backup/dialogs/dialog-backup-onboarding.ts index 68f73df823..4bec74e156 100644 --- a/src/panels/config/backup/dialogs/dialog-backup-onboarding.ts +++ b/src/panels/config/backup/dialogs/dialog-backup-onboarding.ts @@ -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, }; diff --git a/src/panels/config/backup/ha-config-backup-settings.ts b/src/panels/config/backup/ha-config-backup-settings.ts index 4bed661988..958bf11663 100644 --- a/src/panels/config/backup/ha-config-backup-settings.ts +++ b/src/panels/config/backup/ha-config-backup-settings.ts @@ -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"); } diff --git a/src/translations/en.json b/src/translations/en.json index c4187f68e3..09b9865df6 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -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": {