mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-22 16:56:35 +00:00
Add time option for backup schedule (#23757)
Co-authored-by: Wendelin <w@pe8.at>
This commit is contained in:
parent
e994e3565d
commit
7535d66373
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@ -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)}`;
|
||||
}
|
||||
);
|
||||
|
@ -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);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user