Supervisor backup update config (#24990)

This commit is contained in:
Wendelin 2025-04-10 11:55:20 +02:00 committed by GitHub
parent c8e46bd239
commit e3122e8e4d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 393 additions and 22 deletions

View File

@ -0,0 +1,21 @@
import type { HomeAssistant } from "../../types";
export interface SupervisorUpdateConfig {
add_on_backup_before_update: boolean;
add_on_backup_retain_copies?: number;
core_backup_before_update: boolean;
}
export const getSupervisorUpdateConfig = async (hass: HomeAssistant) =>
hass.callWS<SupervisorUpdateConfig>({
type: "hassio/update/config/info",
});
export const updateSupervisorUpdateConfig = async (
hass: HomeAssistant,
config: Partial<SupervisorUpdateConfig>
) =>
hass.callWS({
type: "hassio/update/config/update",
...config,
});

View File

@ -207,7 +207,7 @@ export const computeUpdateStateDisplay = (
return hass.formatEntityState(stateObj);
};
type UpdateType = "addon" | "home_assistant" | "generic";
export type UpdateType = "addon" | "home_assistant" | "generic";
export const getUpdateType = (
stateObj: UpdateEntity,

View File

@ -1,26 +1,27 @@
import "@material/mwc-linear-progress/mwc-linear-progress";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { BINARY_STATE_OFF } from "../../../common/const";
import { relativeTime } from "../../../common/datetime/relative_time";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-checkbox";
import "../../../components/ha-spinner";
import "../../../components/ha-faded";
import "../../../components/ha-formfield";
import "../../../components/ha-markdown";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import "../../../components/ha-spinner";
import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch";
import type { BackupConfig } from "../../../data/backup";
import { fetchBackupConfig } from "../../../data/backup";
import { isUnavailableState } from "../../../data/entity";
import type { EntitySources } from "../../../data/entity_sources";
import { fetchEntitySourcesWithCache } from "../../../data/entity_sources";
import type { UpdateEntity } from "../../../data/update";
import { getSupervisorUpdateConfig } from "../../../data/supervisor/update";
import type { UpdateEntity, UpdateType } from "../../../data/update";
import {
getUpdateType,
UpdateEntityFeature,
@ -44,11 +45,38 @@ class MoreInfoUpdate extends LitElement {
@state() private _backupConfig?: BackupConfig;
@state() private _createBackup = false;
@state() private _entitySources?: EntitySources;
private async _fetchBackupConfig() {
const { config } = await fetchBackupConfig(this.hass);
this._backupConfig = config;
try {
const { config } = await fetchBackupConfig(this.hass);
this._backupConfig = config;
} catch (err) {
// ignore error, because user will get a manual backup option
// eslint-disable-next-line no-console
console.error(err);
}
}
private async _fetchUpdateBackupConfig(type: UpdateType) {
try {
const config = await getSupervisorUpdateConfig(this.hass);
if (type === "home_assistant") {
this._createBackup = config.core_backup_before_update;
return;
}
if (type === "addon") {
this._createBackup = config.add_on_backup_before_update;
}
} catch (err) {
// ignore error, because user can still set the config
// eslint-disable-next-line no-console
console.error(err);
}
}
private async _fetchEntitySources() {
@ -256,7 +284,8 @@ class MoreInfoUpdate extends LitElement {
: nothing}
<ha-switch
slot="end"
id="create-backup"
.checked=${this._createBackup}
@change=${this._createBackupChanged}
.disabled=${updateIsInstalling(this.stateObj)}
></ha-switch>
</ha-md-list-item>
@ -319,6 +348,12 @@ class MoreInfoUpdate extends LitElement {
if (supportsFeature(this.stateObj!, UpdateEntityFeature.BACKUP)) {
this._fetchEntitySources().then(() => {
const type = getUpdateType(this.stateObj!, this._entitySources!);
if (
isComponentLoaded(this.hass, "hassio") &&
["home_assistant", "addon"].includes(type)
) {
this._fetchUpdateBackupConfig(type);
}
if (type === "home_assistant") {
this._fetchBackupConfig();
}
@ -347,13 +382,7 @@ class MoreInfoUpdate extends LitElement {
if (!supportsFeature(this.stateObj!, UpdateEntityFeature.BACKUP)) {
return false;
}
const createBackupSwitch = this.shadowRoot?.getElementById(
"create-backup"
) as HaSwitch;
if (createBackupSwitch) {
return createBackupSwitch.checked;
}
return false;
return this._createBackup;
}
private _handleInstall(): void {
@ -375,6 +404,10 @@ class MoreInfoUpdate extends LitElement {
this.hass.callService("update", "install", installData);
}
private _createBackupChanged(ev) {
this._createBackup = ev.target.checked;
}
private _handleSkip(): void {
if (this.stateObj!.attributes.auto_update) {
showAlertDialog(this, {

View File

@ -0,0 +1,132 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-md-select";
import type { HaMdSelect } from "../../../../../components/ha-md-select";
import "../../../../../components/ha-md-select-option";
import "../../../../../components/ha-md-textfield";
import type { HaMdTextfield } from "../../../../../components/ha-md-textfield";
import type { SupervisorUpdateConfig } from "../../../../../data/supervisor/update";
import type { HomeAssistant } from "../../../../../types";
const MIN_RETENTION_VALUE = 1;
@customElement("ha-backup-config-addon")
class HaBackupConfigAddon extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false })
public supervisorUpdateConfig?: SupervisorUpdateConfig;
protected render() {
return html`
<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
`ui.panel.config.backup.schedule.update_preference.label`
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
`ui.panel.config.backup.schedule.update_preference.supporting_text`
)}
</span>
<ha-md-select
slot="end"
@change=${this._updatePreferenceChanged}
.value=${this.supervisorUpdateConfig?.add_on_backup_before_update?.toString() ||
"false"}
>
<ha-md-select-option value="false">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.backup.schedule.update_preference.skip_backups"
)}
</div>
</ha-md-select-option>
<ha-md-select-option value="true">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.backup.schedule.update_preference.backup_before_update"
)}
</div>
</ha-md-select-option>
</ha-md-select>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(`ui.panel.config.backup.schedule.retention`)}
</span>
<span slot="supporting-text">
${this.hass.localize(
`ui.panel.config.backup.settings.addon_update_backup.retention_description`
)}
</span>
<ha-md-textfield
slot="end"
@change=${this._backupRetentionChanged}
.value=${this.supervisorUpdateConfig?.add_on_backup_retain_copies?.toString() ||
"1"}
type="number"
.min=${MIN_RETENTION_VALUE.toString()}
step="1"
>
</ha-md-textfield>
</ha-md-list-item>
</ha-md-list>
`;
}
private _updatePreferenceChanged(ev) {
ev.stopPropagation();
const target = ev.currentTarget as HaMdSelect;
const add_on_backup_before_update = target.value === "true";
fireEvent(this, "update-config-changed", {
value: {
add_on_backup_before_update,
},
});
}
private _backupRetentionChanged(ev) {
const target = ev.currentTarget as HaMdTextfield;
const add_on_backup_retain_copies = Number(target.value);
if (add_on_backup_retain_copies >= MIN_RETENTION_VALUE) {
fireEvent(this, "update-config-changed", {
value: {
add_on_backup_retain_copies,
},
});
}
}
static styles = css`
ha-md-list {
background: none;
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
}
ha-md-list-item {
--md-item-overflow: visible;
}
ha-md-select {
min-width: 210px;
}
@media all and (max-width: 450px) {
ha-md-select {
min-width: 160px;
width: 160px;
--md-filled-field-content-space: 0;
}
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-backup-config-addon": HaBackupConfigAddon;
}
}

View File

@ -2,9 +2,13 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { formatTime } from "../../../../../common/datetime/format_time";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { clamp } from "../../../../../common/number/clamp";
import "../../../../../components/ha-checkbox";
import type { HaCheckbox } from "../../../../../components/ha-checkbox";
import "../../../../../components/ha-expansion-panel";
import "../../../../../components/ha-formfield";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-md-select";
@ -12,6 +16,8 @@ import type { HaMdSelect } from "../../../../../components/ha-md-select";
import "../../../../../components/ha-md-select-option";
import "../../../../../components/ha-md-textfield";
import "../../../../../components/ha-switch";
import "../../../../../components/ha-time-input";
import "../../../../../components/ha-tip";
import type { BackupConfig, BackupDay } from "../../../../../data/backup";
import {
BACKUP_DAYS,
@ -20,13 +26,8 @@ import {
DEFAULT_OPTIMIZED_BACKUP_START_TIME,
sortWeekdays,
} from "../../../../../data/backup";
import type { SupervisorUpdateConfig } from "../../../../../data/supervisor/update";
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">;
@ -116,6 +117,11 @@ class HaBackupConfigSchedule extends LitElement {
@property({ attribute: false }) public value?: BackupConfigSchedule;
@property({ type: Boolean }) public supervisor = false;
@property({ attribute: false })
public supervisorUpdateConfig?: SupervisorUpdateConfig;
@state() private _retentionPreset?: RetentionPreset;
protected willUpdate(changedProperties: PropertyValues): void {
@ -333,6 +339,44 @@ class HaBackupConfigSchedule extends LitElement {
: nothing}
`
: nothing}
${this.supervisor
? html`
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
`ui.panel.config.backup.schedule.update_preference.label`
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
`ui.panel.config.backup.schedule.update_preference.supporting_text`
)}
</span>
<ha-md-select
slot="end"
@change=${this._updatePreferenceChanged}
.value=${this.supervisorUpdateConfig?.core_backup_before_update?.toString() ||
"false"}
>
<ha-md-select-option value="false">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.backup.schedule.update_preference.skip_backups"
)}
</div>
</ha-md-select-option>
<ha-md-select-option value="true">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.backup.schedule.update_preference.backup_before_update"
)}
</div>
</ha-md-select-option>
</ha-md-select>
</ha-md-list-item>
`
: nothing}
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(`ui.panel.config.backup.schedule.retention`)}
@ -488,6 +532,17 @@ class HaBackupConfigSchedule extends LitElement {
});
}
private _updatePreferenceChanged(ev) {
ev.stopPropagation();
const target = ev.currentTarget as HaMdSelect;
const core_backup_before_update = target.value === "true";
fireEvent(this, "update-config-changed", {
value: {
core_backup_before_update,
},
});
}
private _retentionPresetChanged(ev) {
ev.stopPropagation();
const target = ev.currentTarget as HaMdSelect;
@ -614,4 +669,10 @@ declare global {
interface HTMLElementTagNameMap {
"ha-backup-config-schedule": HaBackupConfigSchedule;
}
interface HASSDomEvents {
"update-config-changed": {
value: Partial<SupervisorUpdateConfig>;
};
}
}

View File

@ -7,20 +7,27 @@ import { fireEvent } from "../../../common/dom/fire_event";
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
import { debounce } from "../../../common/util/debounce";
import { nextRender } from "../../../common/util/render-status";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-next";
import "../../../components/ha-list-item";
import "../../../components/ha-alert";
import "../../../components/ha-password-field";
import "../../../components/ha-svg-icon";
import type { BackupAgent, BackupConfig } from "../../../data/backup";
import { updateBackupConfig } from "../../../data/backup";
import type { CloudStatus } from "../../../data/cloud";
import {
getSupervisorUpdateConfig,
updateSupervisorUpdateConfig,
type SupervisorUpdateConfig,
} from "../../../data/supervisor/update";
import "../../../layouts/hass-subpage";
import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import "./components/config/ha-backup-config-addon";
import "./components/config/ha-backup-config-agents";
import "./components/config/ha-backup-config-data";
import type { BackupConfigData } from "./components/config/ha-backup-config-data";
@ -28,7 +35,6 @@ import "./components/config/ha-backup-config-encryption-key";
import "./components/config/ha-backup-config-schedule";
import type { BackupConfigSchedule } from "./components/config/ha-backup-config-schedule";
import { showLocalBackupLocationDialog } from "./dialogs/show-dialog-local-backup-location";
import { documentationUrl } from "../../../util/documentation-url";
@customElement("ha-config-backup-settings")
class HaConfigBackupSettings extends LitElement {
@ -44,11 +50,19 @@ class HaConfigBackupSettings extends LitElement {
@state() private _config?: BackupConfig;
@state() private _supervisorUpdateConfig?: SupervisorUpdateConfig;
@state() private _supervisorUpdateConfigError?: string;
protected willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (changedProperties.has("config") && !this._config) {
this._config = this.config;
}
if (!this.hasUpdated && isComponentLoaded(this.hass, "hassio")) {
this._getSupervisorUpdateConfig();
}
}
public connectedCallback(): void {
@ -58,6 +72,21 @@ class HaConfigBackupSettings extends LitElement {
this._config = this.config;
}
private async _getSupervisorUpdateConfig() {
try {
this._supervisorUpdateConfig = await getSupervisorUpdateConfig(this.hass);
} catch (err: any) {
// eslint-disable-next-line no-console
console.error(err);
this._supervisorUpdateConfigError = this.hass.localize(
"ui.panel.config.backup.settings.addon_update_backup.error_load",
{
error: err?.message || err,
}
);
}
}
private async _scrollToSection() {
const hash = window.location.hash.substring(1);
if (
@ -145,9 +174,17 @@ class HaConfigBackupSettings extends LitElement {
"ui.panel.config.backup.settings.schedule.description"
)}
</p>
${this._supervisorUpdateConfigError
? html`<ha-alert alert-type="error">
${this._supervisorUpdateConfigError}
</ha-alert>`
: nothing}
<ha-backup-config-schedule
.hass=${this.hass}
.value=${this._config}
.supervisor=${supervisor}
.supervisorUpdateConfig=${this._supervisorUpdateConfig}
@update-config-changed=${this._supervisorUpdateConfigChanged}
@value-changed=${this._scheduleConfigChanged}
></ha-backup-config-schedule>
</div>
@ -230,6 +267,38 @@ class HaConfigBackupSettings extends LitElement {
: nothing}
</div>
</ha-card>
${supervisor
? html` <ha-card>
<div class="card-header">
${this.hass.localize(
"ui.panel.config.backup.settings.addon_update_backup.title"
)}
</div>
<div class="card-content">
<p>
${this.hass.localize(
"ui.panel.config.backup.settings.addon_update_backup.description"
)}
</p>
<p>
${this.hass.localize(
"ui.panel.config.backup.settings.addon_update_backup.local_only"
)}
</p>
${this._supervisorUpdateConfigError
? html`<ha-alert alert-type="error">
${this._supervisorUpdateConfigError}
</ha-alert>`
: nothing}
<ha-backup-config-addon
.hass=${this.hass}
.supervisorUpdateConfig=${this._supervisorUpdateConfig}
@update-config-changed=${this
._supervisorUpdateConfigChanged}
></ha-backup-config-addon>
</div>
</ha-card>`
: nothing}
<ha-card>
<div class="card-header">
${this.hass.localize(
@ -262,6 +331,15 @@ class HaConfigBackupSettings extends LitElement {
showLocalBackupLocationDialog(this, {});
}
private async _supervisorUpdateConfigChanged(ev) {
const config = ev.detail.value as SupervisorUpdateConfig;
this._supervisorUpdateConfig = {
...this._supervisorUpdateConfig!,
...config,
};
this._debounceSaveSupervisorUpdateConfig();
}
private _scheduleConfigChanged(ev) {
const value = ev.detail.value as BackupConfigSchedule;
this._config = {
@ -328,6 +406,32 @@ class HaConfigBackupSettings extends LitElement {
this._debounceSave();
}
private _debounceSaveSupervisorUpdateConfig = debounce(
() => this._saveSupervisorUpdateConfig(),
500
);
private async _saveSupervisorUpdateConfig() {
if (!this._supervisorUpdateConfig) {
return;
}
try {
await updateSupervisorUpdateConfig(
this.hass,
this._supervisorUpdateConfig
);
} catch (err: any) {
// eslint-disable-next-line no-console
console.error(err);
this._supervisorUpdateConfigError = this.hass.localize(
"ui.panel.config.backup.settings.addon_update_backup.error_save",
{
error: err?.message || err?.toString(),
}
);
}
}
private _debounceSave = debounce(() => this._save(), 500);
private async _save() {
@ -350,6 +454,12 @@ class HaConfigBackupSettings extends LitElement {
ha-card {
scroll-margin-top: 16px;
}
p {
color: var(--secondary-text-color);
}
p.error {
color: var(--error-color);
}
.content {
padding: 28px 20px 0;
max-width: 690px;

View File

@ -2520,6 +2520,12 @@
"retention_units": {
"copies": "backups",
"days": "days"
},
"update_preference": {
"label": "Backup preference when updating",
"supporting_text": "This can be adjusted each time",
"skip_backups": "Skip backups",
"backup_before_update": "Backup before update"
}
},
"encryption_key": {
@ -2699,6 +2705,14 @@
"encryption_key": {
"title": "Encryption key",
"description": "Keep this encryption key in a safe place, as you will need it to access your backup, allowing it to be restored. Download it as an emergency kit file and store it somewhere safe. Encryption keeps your backups private and secure."
},
"addon_update_backup": {
"title": "Add-on update backups",
"description": "Creates a backup of your add-on and its data. That way you can keep around the previous version of the add-on, so you can always roll back to it if needed.",
"local_only": "This backup is only saved on this system.",
"retention_description": "Prevent your system from filling up with old versions.",
"error_load": "Error loading supervisor update config: {error}",
"error_save": "Error saving supervisor update config: {error}"
}
},
"details": {