From bdde5268c640044d124d5c63bf9abf7a372e0d24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 22 Mar 2022 18:23:54 +0100 Subject: [PATCH] Add support for update entities (#12059) * Add support for update entities * Apply suggestions from code review Co-authored-by: Zack Barett * Add to gallery * implement xx% * Adjustments for skipped * Add progress bar * Add UPDATE_SUPPORT_INSTALL * Allow skipping without install support * Add version to service call if supported * Adjust changelog link * Use Installing * adjustments * Use unavailable Co-authored-by: Zack Barett --- gallery/src/pages/more-info/update.markdown | 3 + gallery/src/pages/more-info/update.ts | 140 ++++++++++++ src/common/const.ts | 2 + src/common/entity/compute_state_display.ts | 28 +++ src/common/entity/domain_icon.ts | 10 + src/data/update.ts | 36 +++ .../more-info/controls/more-info-update.ts | 212 ++++++++++++++++++ .../more-info/state_more_info_control.ts | 1 + src/translations/en.json | 13 ++ 9 files changed, 445 insertions(+) create mode 100644 gallery/src/pages/more-info/update.markdown create mode 100644 gallery/src/pages/more-info/update.ts create mode 100644 src/data/update.ts create mode 100644 src/dialogs/more-info/controls/more-info-update.ts diff --git a/gallery/src/pages/more-info/update.markdown b/gallery/src/pages/more-info/update.markdown new file mode 100644 index 0000000000..e7540412e3 --- /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 0000000000..1488e318dc --- /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 f11e7d7380..ee9f94000c 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 3838218ea4..e78b889457 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 40b0106216..af89f00d16 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 0000000000..22891a92ac --- /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 0000000000..de1dbe1f10 --- /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 02abe5e731..3df5e85b4b 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 7b597b64c7..bb75c9224e 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -228,6 +228,11 @@ "service": { "run": "Run" }, + "update": { + "installing": "Installing", + "installing_with_progress": "Installing ({progress}%)", + "up_to_date": "Up-to-date" + }, "timer": { "actions": { "start": "start", @@ -719,6 +724,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" },