diff --git a/gallery/src/pages/more-info/update.markdown b/gallery/src/pages/more-info/update.markdown
new file mode 100644
index 000000000000..e7540412e389
--- /dev/null
+++ b/gallery/src/pages/more-info/update.markdown
@@ -0,0 +1,3 @@
+---
+title: Update
+---
diff --git a/gallery/src/pages/more-info/update.ts b/gallery/src/pages/more-info/update.ts
new file mode 100644
index 000000000000..1488e318dcbc
--- /dev/null
+++ b/gallery/src/pages/more-info/update.ts
@@ -0,0 +1,140 @@
+import { html, LitElement, PropertyValues, TemplateResult } from "lit";
+import { customElement, property, query } from "lit/decorators";
+import "../../../../src/components/ha-card";
+import {
+ UPDATE_SUPPORT_BACKUP,
+ UPDATE_SUPPORT_PROGRESS,
+ UPDATE_SUPPORT_INSTALL,
+} from "../../../../src/data/update";
+import "../../../../src/dialogs/more-info/more-info-content";
+import { getEntity } from "../../../../src/fake_data/entity";
+import {
+ MockHomeAssistant,
+ provideHass,
+} from "../../../../src/fake_data/provide_hass";
+import "../../components/demo-more-infos";
+
+const base_attributes = {
+ title: "Awesome",
+ current_version: "1.2.2",
+ latest_version: "1.2.3",
+ release_url: "https://home-assistant.io",
+ supported_features: UPDATE_SUPPORT_INSTALL,
+ skipped_version: null,
+ in_progress: false,
+ release_summary:
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. In nec metus aliquet, porta mi ut, ultrices odio. Etiam egestas orci tellus, non semper metus blandit tincidunt. Praesent elementum turpis vel tempor pharetra. Sed quis cursus diam. Proin sem justo.",
+};
+
+const ENTITIES = [
+ getEntity("update", "update1", "on", {
+ ...base_attributes,
+ friendly_name: "Update",
+ }),
+ getEntity("update", "update2", "on", {
+ ...base_attributes,
+ title: null,
+ friendly_name: "Update without title",
+ }),
+ getEntity("update", "update3", "on", {
+ ...base_attributes,
+ release_url: null,
+ friendly_name: "Update without release_url",
+ }),
+ getEntity("update", "update4", "on", {
+ ...base_attributes,
+ release_summary: null,
+ friendly_name: "Update without release_summary",
+ }),
+ getEntity("update", "update5", "off", {
+ ...base_attributes,
+ current_version: "1.2.3",
+ friendly_name: "No update",
+ }),
+ getEntity("update", "update6", "off", {
+ ...base_attributes,
+ skipped_version: "1.2.3",
+ friendly_name: "Skipped version",
+ }),
+ getEntity("update", "update7", "on", {
+ ...base_attributes,
+ supported_features:
+ base_attributes.supported_features + UPDATE_SUPPORT_BACKUP,
+ friendly_name: "With backup support",
+ }),
+ getEntity("update", "update8", "on", {
+ ...base_attributes,
+ in_progress: true,
+ friendly_name: "With true in_progress",
+ }),
+ getEntity("update", "update9", "on", {
+ ...base_attributes,
+ in_progress: 25,
+ supported_features:
+ base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
+ friendly_name: "With 25 in_progress",
+ }),
+ getEntity("update", "update10", "on", {
+ ...base_attributes,
+ in_progress: 50,
+ supported_features:
+ base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
+ friendly_name: "With 50 in_progress",
+ }),
+ getEntity("update", "update11", "on", {
+ ...base_attributes,
+ in_progress: 75,
+ supported_features:
+ base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
+ friendly_name: "With 75 in_progress",
+ }),
+ getEntity("update", "update12", "unavailable", {
+ ...base_attributes,
+ in_progress: 50,
+ friendly_name: "Unavailable",
+ }),
+ getEntity("update", "update13", "on", {
+ ...base_attributes,
+ supported_features: 0,
+ friendly_name: "No install support",
+ }),
+ getEntity("update", "update14", "off", {
+ ...base_attributes,
+ current_version: null,
+ friendly_name: "Update without current_version",
+ }),
+ getEntity("update", "update15", "off", {
+ ...base_attributes,
+ latest_version: null,
+ friendly_name: "Update without latest_version",
+ }),
+];
+
+@customElement("demo-more-info-update")
+class DemoMoreInfoUpdate extends LitElement {
+ @property() public hass!: MockHomeAssistant;
+
+ @query("demo-more-infos") private _demoRoot!: HTMLElement;
+
+ protected render(): TemplateResult {
+ return html`
+ ent.entityId)}
+ >
+ `;
+ }
+
+ protected firstUpdated(changedProperties: PropertyValues) {
+ super.firstUpdated(changedProperties);
+ const hass = provideHass(this._demoRoot);
+ hass.updateTranslations(null, "en");
+ hass.addEntities(ENTITIES);
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "demo-more-info-update": DemoMoreInfoUpdate;
+ }
+}
diff --git a/src/common/const.ts b/src/common/const.ts
index f11e7d738038..ee9f94000cee 100644
--- a/src/common/const.ts
+++ b/src/common/const.ts
@@ -187,6 +187,7 @@ export const DOMAINS_WITH_MORE_INFO = [
"scene",
"sun",
"timer",
+ "update",
"vacuum",
"water_heater",
"weather",
@@ -200,6 +201,7 @@ export const DOMAINS_HIDE_DEFAULT_MORE_INFO = [
"input_text",
"number",
"scene",
+ "update",
"select",
];
diff --git a/src/common/entity/compute_state_display.ts b/src/common/entity/compute_state_display.ts
index 3838218ea490..e78b88945712 100644
--- a/src/common/entity/compute_state_display.ts
+++ b/src/common/entity/compute_state_display.ts
@@ -1,12 +1,18 @@
import { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { FrontendLocaleData } from "../../data/translation";
+import {
+ updateIsInstalling,
+ UpdateEntity,
+ UPDATE_SUPPORT_PROGRESS,
+} from "../../data/update";
import { formatDate } from "../datetime/format_date";
import { formatDateTime } from "../datetime/format_date_time";
import { formatTime } from "../datetime/format_time";
import { formatNumber, isNumericState } from "../number/format_number";
import { LocalizeFunc } from "../translations/localize";
import { computeStateDomain } from "./compute_state_domain";
+import { supportsFeature } from "./supports-feature";
export const computeStateDisplay = (
localize: LocalizeFunc,
@@ -130,6 +136,28 @@ export const computeStateDisplay = (
}
}
+ if (domain === "update") {
+ // When updating, and entity does not support % show "Installing"
+ // When updating, and entity does support % show "Installing (xx%)"
+ // When update available, show the version
+ // When the latest version is skipped, show the latest version
+ // When update is not available, show "Up-to-date"
+ // When update is not available and there is no latest_version show "Unavailable"
+ return compareState === "on"
+ ? updateIsInstalling(stateObj as UpdateEntity)
+ ? supportsFeature(stateObj, UPDATE_SUPPORT_PROGRESS)
+ ? localize("ui.card.update.installing_with_progress", {
+ progress: stateObj.attributes.in_progress,
+ })
+ : localize("ui.card.update.installing")
+ : stateObj.attributes.latest_version
+ : stateObj.attributes.skipped_version ===
+ stateObj.attributes.latest_version
+ ? stateObj.attributes.latest_version ??
+ localize("state.default.unavailable")
+ : localize("ui.card.update.up_to_date");
+ }
+
return (
// Return device class translation
(stateObj.attributes.device_class &&
diff --git a/src/common/entity/domain_icon.ts b/src/common/entity/domain_icon.ts
index b64d93f8a04b..51045d2d97f5 100644
--- a/src/common/entity/domain_icon.ts
+++ b/src/common/entity/domain_icon.ts
@@ -26,8 +26,11 @@ import {
mdiCheckCircleOutline,
mdiCloseCircleOutline,
mdiWeatherNight,
+ mdiPackage,
+ mdiPackageDown,
} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
+import { updateIsInstalling, UpdateEntity } from "../../data/update";
/**
* Return the icon to be used for a domain.
*
@@ -133,6 +136,13 @@ export const domainIcon = (
return stateObj?.state === "above_horizon"
? FIXED_DOMAIN_ICONS[domain]
: mdiWeatherNight;
+
+ case "update":
+ return compareState === "on"
+ ? updateIsInstalling(stateObj as UpdateEntity)
+ ? mdiPackageDown
+ : mdiPackageUp
+ : mdiPackage;
}
if (domain in FIXED_DOMAIN_ICONS) {
diff --git a/src/data/update.ts b/src/data/update.ts
new file mode 100644
index 000000000000..22891a92acb0
--- /dev/null
+++ b/src/data/update.ts
@@ -0,0 +1,36 @@
+import type {
+ HassEntityAttributeBase,
+ HassEntityBase,
+} from "home-assistant-js-websocket";
+import { supportsFeature } from "../common/entity/supports-feature";
+
+export const UPDATE_SUPPORT_INSTALL = 1;
+export const UPDATE_SUPPORT_SPECIFIC_VERSION = 2;
+export const UPDATE_SUPPORT_PROGRESS = 4;
+export const UPDATE_SUPPORT_BACKUP = 8;
+
+interface UpdateEntityAttributes extends HassEntityAttributeBase {
+ current_version: string | null;
+ in_progress: boolean | number;
+ latest_version: string | null;
+ release_summary: string | null;
+ release_url: string | null;
+ skipped_version: string | null;
+ title: string | null;
+}
+
+export interface UpdateEntity extends HassEntityBase {
+ attributes: UpdateEntityAttributes;
+}
+
+export const updateUsesProgress = (entity: UpdateEntity): boolean =>
+ supportsFeature(entity, UPDATE_SUPPORT_PROGRESS) &&
+ typeof entity.attributes.in_progress === "number";
+
+export const updateCanInstall = (entity: UpdateEntity): boolean =>
+ supportsFeature(entity, UPDATE_SUPPORT_INSTALL) &&
+ entity.attributes.latest_version !== entity.attributes.current_version &&
+ entity.attributes.latest_version !== entity.attributes.skipped_version;
+
+export const updateIsInstalling = (entity: UpdateEntity): boolean =>
+ updateUsesProgress(entity) || !!entity.attributes.in_progress;
diff --git a/src/dialogs/more-info/controls/more-info-update.ts b/src/dialogs/more-info/controls/more-info-update.ts
new file mode 100644
index 000000000000..de1dbe1f1007
--- /dev/null
+++ b/src/dialogs/more-info/controls/more-info-update.ts
@@ -0,0 +1,212 @@
+import "@material/mwc-button/mwc-button";
+import "@material/mwc-linear-progress/mwc-linear-progress";
+import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
+import { customElement, property } from "lit/decorators";
+import { supportsFeature } from "../../../common/entity/supports-feature";
+import "../../../components/ha-checkbox";
+import "../../../components/ha-formfield";
+import "../../../components/ha-markdown";
+import { UNAVAILABLE_STATES } from "../../../data/entity";
+import {
+ updateIsInstalling,
+ UpdateEntity,
+ UPDATE_SUPPORT_BACKUP,
+ UPDATE_SUPPORT_INSTALL,
+ UPDATE_SUPPORT_PROGRESS,
+ UPDATE_SUPPORT_SPECIFIC_VERSION,
+} from "../../../data/update";
+import type { HomeAssistant } from "../../../types";
+
+@customElement("more-info-update")
+class MoreInfoUpdate extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property({ attribute: false }) public stateObj?: UpdateEntity;
+
+ protected render(): TemplateResult {
+ if (
+ !this.hass ||
+ !this.stateObj ||
+ UNAVAILABLE_STATES.includes(this.stateObj.state)
+ ) {
+ return html``;
+ }
+
+ const skippedVersion =
+ this.stateObj.attributes.latest_version &&
+ this.stateObj.attributes.skipped_version ===
+ this.stateObj.attributes.latest_version;
+
+ return html`
+ ${this.stateObj.attributes.in_progress
+ ? supportsFeature(this.stateObj, UPDATE_SUPPORT_PROGRESS) &&
+ typeof this.stateObj.attributes.in_progress === "number"
+ ? html``
+ : html``
+ : ""}
+ ${this.stateObj.attributes.title
+ ? html`
${this.stateObj.attributes.title}
`
+ : ""}
+
+
+
+ ${this.hass.localize(
+ "ui.dialogs.more_info_control.update.current_version"
+ )}
+
+
+ ${this.stateObj.attributes.current_version ??
+ this.hass.localize("state.default.unavailable")}
+
+
+
+
+ ${this.hass.localize(
+ "ui.dialogs.more_info_control.update.latest_version"
+ )}
+
+
+ ${this.stateObj.attributes.latest_version ??
+ this.hass.localize("state.default.unavailable")}
+
+
+
+ ${this.stateObj.attributes.release_url
+ ? html``
+ : ""}
+ ${this.stateObj.attributes.release_summary
+ ? html`
+ `
+ : ""}
+ ${supportsFeature(this.stateObj, UPDATE_SUPPORT_BACKUP)
+ ? html`
+
+
+ `
+ : ""}
+
+
+
+ ${this.hass.localize("ui.dialogs.more_info_control.update.skip")}
+
+ ${supportsFeature(this.stateObj, UPDATE_SUPPORT_INSTALL)
+ ? html`
+
+ ${this.hass.localize(
+ "ui.dialogs.more_info_control.update.install"
+ )}
+
+ `
+ : ""}
+
+ `;
+ }
+
+ get _shouldCreateBackup(): boolean | null {
+ if (!supportsFeature(this.stateObj!, UPDATE_SUPPORT_BACKUP)) {
+ return null;
+ }
+ const checkbox = this.shadowRoot?.querySelector("ha-checkbox");
+ if (checkbox) {
+ return checkbox.checked;
+ }
+ return true;
+ }
+
+ private _handleInstall(): void {
+ const installData: Record = {
+ entity_id: this.stateObj!.entity_id,
+ };
+
+ if (this._shouldCreateBackup) {
+ installData.backup = true;
+ }
+
+ if (
+ supportsFeature(this.stateObj!, UPDATE_SUPPORT_SPECIFIC_VERSION) &&
+ this.stateObj!.attributes.latest_version
+ ) {
+ installData.version = this.stateObj!.attributes.latest_version;
+ }
+
+ this.hass.callService("update", "install", installData);
+ }
+
+ private _handleSkip(): void {
+ this.hass.callService("update", "skip", {
+ entity_id: this.stateObj!.entity_id,
+ });
+ }
+
+ static get styles(): CSSResultGroup {
+ return css`
+ hr {
+ border-color: var(--divider-color);
+ border-bottom: none;
+ margin: 16px 0;
+ }
+ ha-expansion-panel {
+ margin: 16px 0;
+ }
+ .row {
+ margin: 0;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ }
+ .actions {
+ margin: 8px 0 0;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ }
+
+ .actions mwc-button {
+ margin: 0 4px 4px;
+ }
+ a {
+ color: var(--primary-color);
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "more-info-update": MoreInfoUpdate;
+ }
+}
diff --git a/src/dialogs/more-info/state_more_info_control.ts b/src/dialogs/more-info/state_more_info_control.ts
index 02abe5e73188..3df5e85b4be7 100644
--- a/src/dialogs/more-info/state_more_info_control.ts
+++ b/src/dialogs/more-info/state_more_info_control.ts
@@ -25,6 +25,7 @@ const LAZY_LOADED_MORE_INFO_CONTROL = {
script: () => import("./controls/more-info-script"),
sun: () => import("./controls/more-info-sun"),
timer: () => import("./controls/more-info-timer"),
+ update: () => import("./controls/more-info-update"),
vacuum: () => import("./controls/more-info-vacuum"),
water_heater: () => import("./controls/more-info-water_heater"),
weather: () => import("./controls/more-info-weather"),
diff --git a/src/translations/en.json b/src/translations/en.json
index 19be33d2e5eb..8b824b0be54e 100755
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -223,6 +223,11 @@
"service": {
"run": "Run"
},
+ "update": {
+ "installing": "Installing",
+ "installing_with_progress": "Installing ({progress}%)",
+ "up_to_date": "Up-to-date"
+ },
"timer": {
"actions": {
"start": "start",
@@ -713,6 +718,14 @@
"rising": "Rising",
"setting": "Setting"
},
+ "update": {
+ "current_version": "Current version",
+ "latest_version": "Latest version",
+ "release_announcement": "Read release announcement",
+ "skip": "Skip",
+ "install": "Install",
+ "create_backup": "Create backup before updating"
+ },
"updater": {
"title": "Update Instructions"
},