diff --git a/src/data/supervisor/update.ts b/src/data/supervisor/update.ts new file mode 100644 index 0000000000..fc53b4399a --- /dev/null +++ b/src/data/supervisor/update.ts @@ -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({ + type: "hassio/update/config/info", + }); + +export const updateSupervisorUpdateConfig = async ( + hass: HomeAssistant, + config: Partial +) => + hass.callWS({ + type: "hassio/update/config/update", + ...config, + }); diff --git a/src/data/update.ts b/src/data/update.ts index 29f7ecf054..e80fb2f285 100644 --- a/src/data/update.ts +++ b/src/data/update.ts @@ -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, diff --git a/src/dialogs/more-info/controls/more-info-update.ts b/src/dialogs/more-info/controls/more-info-update.ts index 206789552e..d4e0503604 100644 --- a/src/dialogs/more-info/controls/more-info-update.ts +++ b/src/dialogs/more-info/controls/more-info-update.ts @@ -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} @@ -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, { diff --git a/src/panels/config/backup/components/config/ha-backup-config-addon.ts b/src/panels/config/backup/components/config/ha-backup-config-addon.ts new file mode 100644 index 0000000000..c9ba165579 --- /dev/null +++ b/src/panels/config/backup/components/config/ha-backup-config-addon.ts @@ -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` + + + + ${this.hass.localize( + `ui.panel.config.backup.schedule.update_preference.label` + )} + + + ${this.hass.localize( + `ui.panel.config.backup.schedule.update_preference.supporting_text` + )} + + + +
+ ${this.hass.localize( + "ui.panel.config.backup.schedule.update_preference.skip_backups" + )} +
+
+ +
+ ${this.hass.localize( + "ui.panel.config.backup.schedule.update_preference.backup_before_update" + )} +
+
+
+
+ + + ${this.hass.localize(`ui.panel.config.backup.schedule.retention`)} + + + ${this.hass.localize( + `ui.panel.config.backup.settings.addon_update_backup.retention_description` + )} + + + + +
+ `; + } + + 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; + } +} 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 c19d334487..a0f7e9db2a 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 @@ -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; @@ -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` + + + ${this.hass.localize( + `ui.panel.config.backup.schedule.update_preference.label` + )} + + + ${this.hass.localize( + `ui.panel.config.backup.schedule.update_preference.supporting_text` + )} + + + +
+ ${this.hass.localize( + "ui.panel.config.backup.schedule.update_preference.skip_backups" + )} +
+
+ +
+ ${this.hass.localize( + "ui.panel.config.backup.schedule.update_preference.backup_before_update" + )} +
+
+
+
+ ` + : nothing} + ${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; + }; + } } diff --git a/src/panels/config/backup/ha-config-backup-settings.ts b/src/panels/config/backup/ha-config-backup-settings.ts index e841b872aa..42e39fdbad 100644 --- a/src/panels/config/backup/ha-config-backup-settings.ts +++ b/src/panels/config/backup/ha-config-backup-settings.ts @@ -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" )}

+ ${this._supervisorUpdateConfigError + ? html` + ${this._supervisorUpdateConfigError} + ` + : nothing} @@ -230,6 +267,38 @@ class HaConfigBackupSettings extends LitElement { : nothing} + ${supervisor + ? html` +
+ ${this.hass.localize( + "ui.panel.config.backup.settings.addon_update_backup.title" + )} +
+
+

+ ${this.hass.localize( + "ui.panel.config.backup.settings.addon_update_backup.description" + )} +

+

+ ${this.hass.localize( + "ui.panel.config.backup.settings.addon_update_backup.local_only" + )} +

+ ${this._supervisorUpdateConfigError + ? html` + ${this._supervisorUpdateConfigError} + ` + : nothing} + +
+
` + : nothing}
${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; diff --git a/src/translations/en.json b/src/translations/en.json index 9ddbb2dfb0..d476ec3501 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -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": {