Reintroduce backup switch when updating core and addons (#23814)

This commit is contained in:
Paul Bottein 2025-01-27 08:25:28 +01:00 committed by GitHub
parent bc21877008
commit 5453da75ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 346 additions and 22 deletions

View File

@ -9,6 +9,7 @@ import {
} from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-alert";
@ -18,7 +19,11 @@ import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-faded";
import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-markdown";
import "../../../src/components/ha-md-list";
import "../../../src/components/ha-md-list-item";
import "../../../src/components/ha-svg-icon";
import "../../../src/components/ha-switch";
import type { HaSwitch } from "../../../src/components/ha-switch";
import type { HassioAddonDetails } from "../../../src/data/hassio/addon";
import {
fetchHassioAddonChangelog,
@ -121,6 +126,8 @@ class UpdateAvailableCard extends LitElement {
const changelog = changelogUrl(this._updateType, this._version_latest);
const createBackupTexts = this._computeCreateBackupTexts();
return html`
<ha-card
outlined
@ -160,6 +167,30 @@ class UpdateAvailableCard extends LitElement {
)}
</p>
</div>
${createBackupTexts
? html`
<hr />
<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${createBackupTexts.title}
</span>
${createBackupTexts.description
? html`
<span slot="supporting-text">
${createBackupTexts.description}
</span>
`
: nothing}
<ha-switch
slot="end"
id="create-backup"
></ha-switch>
</ha-md-list-item>
</ha-md-list>
`
: nothing}
`
: html`<ha-circular-progress
aria-label="Updating"
@ -227,6 +258,48 @@ class UpdateAvailableCard extends LitElement {
}
}
private _computeCreateBackupTexts():
| { title: string; description?: string }
| undefined {
// Addon backup
if (
this._updateType === "addon" &&
atLeastVersion(this.hass.config.version, 2025, 2, 0)
) {
const version = this._version;
return {
title: this.supervisor.localize("update_available.create_backup.addon"),
description: this.supervisor.localize(
"update_available.create_backup.addon_description",
{ version: version }
),
};
}
// Old behavior
if (this._updateType && ["core", "addon"].includes(this._updateType)) {
return {
title: this.supervisor.localize(
"update_available.create_backup.generic"
),
};
}
return undefined;
}
get _shouldCreateBackup(): boolean {
if (this._updateType && !["core", "addon"].includes(this._updateType)) {
return false;
}
const createBackupSwitch = this.shadowRoot?.getElementById(
"create-backup"
) as HaSwitch;
if (createBackupSwitch) {
return createBackupSwitch.checked;
}
return true;
}
get _version(): string {
return this._updateType
? this._updateType === "addon"
@ -341,14 +414,22 @@ class UpdateAvailableCard extends LitElement {
}
private async _update() {
if (this._shouldCreateBackup && this.supervisor.info.state === "freeze") {
this._error = this.supervisor.localize("backup.backup_already_running");
return;
}
this._error = undefined;
this._updating = true;
try {
if (this._updateType === "addon") {
await updateHassioAddon(this.hass, this.addonSlug!);
await updateHassioAddon(
this.hass,
this.addonSlug!,
this._shouldCreateBackup
);
} else if (this._updateType === "core") {
await updateCore(this.hass);
await updateCore(this.hass, this._shouldCreateBackup);
} else if (this._updateType === "os") {
await updateOS(this.hass);
} else if (this._updateType === "supervisor") {
@ -403,6 +484,17 @@ class UpdateAvailableCard extends LitElement {
border-bottom: none;
margin: 16px 0 0 0;
}
ha-md-list {
padding: 0;
margin-bottom: -16px;
}
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-item-overflow: visible;
}
`,
];
}

View File

@ -313,21 +313,34 @@ export const installHassioAddon = async (
export const updateHassioAddon = async (
hass: HomeAssistant,
slug: string
slug: string,
backup: boolean
): Promise<void> => {
if (atLeastVersion(hass.config.version, 2025, 2, 0)) {
await hass.callWS({
type: "hassio/update/addon",
addon: slug,
backup: backup,
});
return;
}
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: `/store/addons/${slug}/update`,
method: "post",
timeout: null,
data: { backup },
});
} else {
return;
}
await hass.callApi<HassioResponse<void>>(
"POST",
`hassio/addons/${slug}/update`
`hassio/addons/${slug}/update`,
{ backup }
);
}
};
export const restartHassioAddon = async (

View File

@ -6,15 +6,27 @@ export const restartCore = async (hass: HomeAssistant) => {
await hass.callService("homeassistant", "restart");
};
export const updateCore = async (hass: HomeAssistant) => {
export const updateCore = async (hass: HomeAssistant, backup: boolean) => {
if (atLeastVersion(hass.config.version, 2025, 2, 0)) {
await hass.callWS({
type: "hassio/update/core",
backup: backup,
});
return;
}
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: "/core/update",
method: "post",
timeout: null,
data: { backup },
});
} else {
await hass.callApi<HassioResponse<void>>("POST", "hassio/core/update");
return;
}
await hass.callApi<HassioResponse<void>>("POST", "hassio/core/update", {
backup,
});
};

View File

@ -13,6 +13,7 @@ import { caseInsensitiveStringCompare } from "../common/string/compare";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../types";
import { showToast } from "../util/toast";
import type { EntitySources } from "./entity_sources";
export enum UpdateEntityFeature {
INSTALL = 1,
@ -60,6 +61,10 @@ export const updateReleaseNotes = (hass: HomeAssistant, entityId: string) =>
entity_id: entityId,
});
const HOME_ASSISTANT_CORE_TITLE = "Home Assistant Core";
const HOME_ASSISTANT_SUPERVISOR_TITLE = "Home Assistant Supervisor";
const HOME_ASSISTANT_OS_TITLE = "Home Assistant Operating System";
export const filterUpdateEntities = (
entities: HassEntities,
language?: string
@ -69,22 +74,22 @@ export const filterUpdateEntities = (
(entity) => computeStateDomain(entity) === "update"
) as UpdateEntity[]
).sort((a, b) => {
if (a.attributes.title === "Home Assistant Core") {
if (a.attributes.title === HOME_ASSISTANT_CORE_TITLE) {
return -3;
}
if (b.attributes.title === "Home Assistant Core") {
if (b.attributes.title === HOME_ASSISTANT_CORE_TITLE) {
return 3;
}
if (a.attributes.title === "Home Assistant Operating System") {
if (a.attributes.title === HOME_ASSISTANT_OS_TITLE) {
return -2;
}
if (b.attributes.title === "Home Assistant Operating System") {
if (b.attributes.title === HOME_ASSISTANT_OS_TITLE) {
return 2;
}
if (a.attributes.title === "Home Assistant Supervisor") {
if (a.attributes.title === HOME_ASSISTANT_SUPERVISOR_TITLE) {
return -1;
}
if (b.attributes.title === "Home Assistant Supervisor") {
if (b.attributes.title === HOME_ASSISTANT_SUPERVISOR_TITLE) {
return 1;
}
return caseInsensitiveStringCompare(
@ -201,3 +206,32 @@ export const computeUpdateStateDisplay = (
return hass.formatEntityState(stateObj);
};
type UpdateType = "addon" | "home_assistant" | "generic";
export const getUpdateType = (
stateObj: UpdateEntity,
entitySources: EntitySources
): UpdateType => {
const entity_id = stateObj.entity_id;
const domain = entitySources[entity_id]?.domain;
if (domain !== "hassio") {
return "generic";
}
const title = stateObj.attributes.title || "";
if (title === HOME_ASSISTANT_CORE_TITLE) {
return "home_assistant";
}
if (
![
HOME_ASSISTANT_CORE_TITLE,
HOME_ASSISTANT_SUPERVISOR_TITLE,
HOME_ASSISTANT_OS_TITLE,
].includes(title)
) {
return "addon";
}
return "generic";
};

View File

@ -2,6 +2,7 @@ import "@material/mwc-linear-progress/mwc-linear-progress";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
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";
@ -10,10 +11,18 @@ import "../../../components/ha-circular-progress";
import "../../../components/ha-faded";
import "../../../components/ha-formfield";
import "../../../components/ha-markdown";
import "../../../components/ha-settings-row";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
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 {
getUpdateType,
UpdateEntityFeature,
updateIsInstalling,
updateReleaseNotes,
@ -33,6 +42,103 @@ class MoreInfoUpdate extends LitElement {
@state() private _markdownLoading = true;
@state() private _backupConfig?: BackupConfig;
@state() private _entitySources?: EntitySources;
private async _fetchBackupConfig() {
const { config } = await fetchBackupConfig(this.hass);
this._backupConfig = config;
}
private async _fetchEntitySources() {
this._entitySources = await fetchEntitySourcesWithCache(this.hass);
}
private _computeCreateBackupTexts():
| { title: string; description?: string }
| undefined {
if (
!this.stateObj ||
!supportsFeature(this.stateObj, UpdateEntityFeature.BACKUP)
) {
return undefined;
}
const updateType = this._entitySources
? getUpdateType(this.stateObj, this._entitySources)
: "generic";
// Automatic or manual for Home Assistant update
if (updateType === "home_assistant") {
const isBackupConfigValid =
!!this._backupConfig &&
!!this._backupConfig.create_backup.password &&
this._backupConfig.create_backup.agent_ids.length > 0;
if (!isBackupConfigValid) {
return {
title: this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup.manual"
),
description: this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup.manual_description"
),
};
}
const lastAutomaticBackupDate = this._backupConfig
?.last_completed_automatic_backup
? new Date(this._backupConfig?.last_completed_automatic_backup)
: null;
const now = new Date();
return {
title: this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup.automatic"
),
description: lastAutomaticBackupDate
? this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup.automatic_description_last",
{
relative_time: relativeTime(
lastAutomaticBackupDate,
this.hass.locale,
now,
true
),
}
)
: this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup.automatic_description_none"
),
};
}
// Addon backup
if (updateType === "addon") {
const version = this.stateObj.attributes.installed_version;
return {
title: this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup.addon"
),
description: version
? this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup.addon_description",
{ version: version }
)
: undefined,
};
}
// Fallback to generic UI
return {
title: this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup.generic"
),
};
}
protected render() {
if (
!this.hass ||
@ -47,6 +153,8 @@ class MoreInfoUpdate extends LitElement {
this.stateObj.attributes.skipped_version ===
this.stateObj.attributes.latest_version;
const createBackupTexts = this._computeCreateBackupTexts();
return html`
<div class="content">
<div class="summary">
@ -133,6 +241,27 @@ class MoreInfoUpdate extends LitElement {
: nothing}
</div>
<div class="footer">
${createBackupTexts
? html`
<ha-md-list>
<ha-md-list-item>
<span slot="headline">${createBackupTexts.title}</span>
${createBackupTexts.description
? html`
<span slot="supporting-text">
${createBackupTexts.description}
</span>
`
: nothing}
<ha-switch
slot="end"
id="create-backup"
.disabled=${updateIsInstalling(this.stateObj)}
></ha-switch>
</ha-md-list-item>
</ha-md-list>
`
: nothing}
<div class="actions">
${this.stateObj.state === BINARY_STATE_OFF &&
this.stateObj.attributes.skipped_version
@ -186,6 +315,14 @@ class MoreInfoUpdate extends LitElement {
if (supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES)) {
this._fetchReleaseNotes();
}
if (supportsFeature(this.stateObj!, UpdateEntityFeature.BACKUP)) {
this._fetchEntitySources().then(() => {
const type = getUpdateType(this.stateObj!, this._entitySources!);
if (type === "home_assistant") {
this._fetchBackupConfig();
}
});
}
}
private async _markdownLoaded() {
@ -205,11 +342,28 @@ class MoreInfoUpdate extends LitElement {
}
}
get _shouldCreateBackup(): boolean {
if (!supportsFeature(this.stateObj!, UpdateEntityFeature.BACKUP)) {
return false;
}
const createBackupSwitch = this.shadowRoot?.getElementById(
"create-backup"
) as HaSwitch;
if (createBackupSwitch) {
return createBackupSwitch.checked;
}
return false;
}
private _handleInstall(): void {
const installData: Record<string, any> = {
entity_id: this.stateObj!.entity_id,
};
if (this._shouldCreateBackup) {
installData.backup = true;
}
if (
supportsFeature(this.stateObj!, UpdateEntityFeature.SPECIFIC_VERSION) &&
this.stateObj!.attributes.latest_version
@ -289,14 +443,18 @@ class MoreInfoUpdate extends LitElement {
z-index: 10;
}
ha-settings-row {
ha-md-list {
width: 100%;
padding: 0 24px;
box-sizing: border-box;
margin-bottom: -16px;
margin-top: -4px;
}
ha-md-list-item {
--md-list-item-leading-space: 24px;
--md-list-item-trailing-space: 24px;
}
.actions {
width: 100%;
display: flex;

View File

@ -1276,7 +1276,17 @@
"install": "Install",
"update": "Update",
"auto_update_enabled_title": "Can not skip version",
"auto_update_enabled_text": "Automatic updates for this item have been enabled; skipping it is, therefore, unavailable. You can either install this update now or wait for Home Assistant to do it automatically."
"auto_update_enabled_text": "Automatic updates for this item have been enabled; skipping it is, therefore, unavailable. You can either install this update now or wait for Home Assistant to do it automatically.",
"create_backup": {
"automatic": "Automatic backup before update",
"automatic_description_last": "Last automatic backup {relative_time}.",
"automatic_description_none": "No automatic backup yet.",
"manual": "Create manual backup before update",
"manual_description": "Includes Home Assistant settings and history.",
"addon": "Keep backup of the last version",
"addon_description": "For easily restoring to version {version}",
"generic": "Create backup"
}
},
"updater": {
"title": "Update instructions"
@ -8478,7 +8488,12 @@
"open_release_notes": "Open release notes",
"description": "You have {version} installed. Press update to update to version {newest_version}",
"updating": "Updating {name} to version {version}",
"no_update": "No update available for {name}"
"no_update": "No update available for {name}",
"create_backup": {
"addon": "[%key:ui::dialogs::more_info_control::update::create_backup::addon%]",
"addon_description": "[%key:ui::dialogs::more_info_control::update::create_backup::addon_description%]",
"generic": "[%key:ui::dialogs::more_info_control::update::create_backup::generic%]"
}
},
"confirm": {
"restart": {