mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-19 07:16:39 +00:00
Add support for update entities (#12059)
* Add support for update entities * Apply suggestions from code review Co-authored-by: Zack Barett <zackbarett@hey.com> * 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 <zackbarett@hey.com>
This commit is contained in:
parent
15e972c158
commit
bdde5268c6
3
gallery/src/pages/more-info/update.markdown
Normal file
3
gallery/src/pages/more-info/update.markdown
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Update
|
||||
---
|
140
gallery/src/pages/more-info/update.ts
Normal file
140
gallery/src/pages/more-info/update.ts
Normal file
@ -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`
|
||||
<demo-more-infos
|
||||
.hass=${this.hass}
|
||||
.entities=${ENTITIES.map((ent) => ent.entityId)}
|
||||
></demo-more-infos>
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -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",
|
||||
];
|
||||
|
||||
|
@ -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 &&
|
||||
|
@ -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) {
|
||||
|
36
src/data/update.ts
Normal file
36
src/data/update.ts
Normal file
@ -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;
|
212
src/dialogs/more-info/controls/more-info-update.ts
Normal file
212
src/dialogs/more-info/controls/more-info-update.ts
Normal file
@ -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`<mwc-linear-progress
|
||||
.progress=${this.stateObj.attributes.in_progress / 100}
|
||||
buffer=""
|
||||
></mwc-linear-progress>`
|
||||
: html`<mwc-linear-progress indeterminate></mwc-linear-progress>`
|
||||
: ""}
|
||||
${this.stateObj.attributes.title
|
||||
? html`<h3>${this.stateObj.attributes.title}</h3>`
|
||||
: ""}
|
||||
|
||||
<div class="row">
|
||||
<div class="key">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.current_version"
|
||||
)}
|
||||
</div>
|
||||
<div class="value">
|
||||
${this.stateObj.attributes.current_version ??
|
||||
this.hass.localize("state.default.unavailable")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="key">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.latest_version"
|
||||
)}
|
||||
</div>
|
||||
<div class="value">
|
||||
${this.stateObj.attributes.latest_version ??
|
||||
this.hass.localize("state.default.unavailable")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this.stateObj.attributes.release_url
|
||||
? html`<div class="row">
|
||||
<div class="key">
|
||||
<a
|
||||
href=${this.stateObj.attributes.release_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.release_announcement"
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
</div>`
|
||||
: ""}
|
||||
${this.stateObj.attributes.release_summary
|
||||
? html`<hr />
|
||||
<ha-markdown
|
||||
.content=${this.stateObj.attributes.release_summary}
|
||||
></ha-markdown> `
|
||||
: ""}
|
||||
${supportsFeature(this.stateObj, UPDATE_SUPPORT_BACKUP)
|
||||
? html`<hr />
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup"
|
||||
)}
|
||||
>
|
||||
<ha-checkbox
|
||||
checked
|
||||
.disabled=${updateIsInstalling(this.stateObj)}
|
||||
></ha-checkbox>
|
||||
</ha-formfield> `
|
||||
: ""}
|
||||
<hr />
|
||||
<div class="actions">
|
||||
<mwc-button
|
||||
@click=${this._handleSkip}
|
||||
.disabled=${skippedVersion ||
|
||||
this.stateObj.state === "off" ||
|
||||
updateIsInstalling(this.stateObj)}
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.more_info_control.update.skip")}
|
||||
</mwc-button>
|
||||
${supportsFeature(this.stateObj, UPDATE_SUPPORT_INSTALL)
|
||||
? html`
|
||||
<mwc-button
|
||||
@click=${this._handleInstall}
|
||||
.disabled=${(this.stateObj.state === "off" &&
|
||||
!skippedVersion) ||
|
||||
updateIsInstalling(this.stateObj)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.install"
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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<string, any> = {
|
||||
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;
|
||||
}
|
||||
}
|
@ -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"),
|
||||
|
@ -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"
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user