diff --git a/hassio/src/update-available/update-available-card.ts b/hassio/src/update-available/update-available-card.ts index f61eb140f5..4f6c02ae2e 100644 --- a/hassio/src/update-available/update-available-card.ts +++ b/hassio/src/update-available/update-available-card.ts @@ -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` + ${createBackupTexts + ? html` +
+ + + + ${createBackupTexts.title} + + + ${createBackupTexts.description + ? html` + + ${createBackupTexts.description} + + ` + : nothing} + + + + ` + : nothing} ` : html` => { + 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 { - await hass.callApi>( - "POST", - `hassio/addons/${slug}/update` - ); + return; } + + await hass.callApi>( + "POST", + `hassio/addons/${slug}/update`, + { backup } + ); }; export const restartHassioAddon = async ( diff --git a/src/data/supervisor/core.ts b/src/data/supervisor/core.ts index 8eaf8b91d8..545824a835 100644 --- a/src/data/supervisor/core.ts +++ b/src/data/supervisor/core.ts @@ -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>("POST", "hassio/core/update"); + return; } + + await hass.callApi>("POST", "hassio/core/update", { + backup, + }); }; diff --git a/src/data/update.ts b/src/data/update.ts index 094ba32552..29f7ecf054 100644 --- a/src/data/update.ts +++ b/src/data/update.ts @@ -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"; +}; diff --git a/src/dialogs/more-info/controls/more-info-update.ts b/src/dialogs/more-info/controls/more-info-update.ts index 577aa63103..6d6619ab25 100644 --- a/src/dialogs/more-info/controls/more-info-update.ts +++ b/src/dialogs/more-info/controls/more-info-update.ts @@ -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`
@@ -133,6 +241,27 @@ class MoreInfoUpdate extends LitElement { : nothing}