mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-18 23:06:40 +00:00
Reintroduce backup switch when updating core and addons (#23814)
This commit is contained in:
parent
bc21877008
commit
5453da75ea
@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@ -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 {
|
||||
await hass.callApi<HassioResponse<void>>(
|
||||
"POST",
|
||||
`hassio/addons/${slug}/update`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await hass.callApi<HassioResponse<void>>(
|
||||
"POST",
|
||||
`hassio/addons/${slug}/update`,
|
||||
{ backup }
|
||||
);
|
||||
};
|
||||
|
||||
export const restartHassioAddon = async (
|
||||
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
@ -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";
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user