diff --git a/hassio/src/update-available/update-available-card.ts b/hassio/src/update-available/update-available-card.ts index a5a2bc81a2..27c3533b8e 100644 --- a/hassio/src/update-available/update-available-card.ts +++ b/hassio/src/update-available/update-available-card.ts @@ -45,7 +45,6 @@ import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box"; import "../../../src/layouts/hass-loading-screen"; import "../../../src/layouts/hass-subpage"; import "../../../src/layouts/hass-tabs-subpage"; -import { SUPERVISOR_UPDATE_NAMES } from "../../../src/panels/config/dashboard/ha-config-updates"; import { HomeAssistant, Route } from "../../../src/types"; import { addonArchIsSupported, extractChangelog } from "../util/addon"; @@ -55,6 +54,12 @@ declare global { } } +const SUPERVISOR_UPDATE_NAMES = { + core: "Home Assistant Core", + os: "Home Assistant Operating System", + supervisor: "Home Assistant Supervisor", +}; + type updateType = "os" | "supervisor" | "core" | "addon"; const changelogUrl = ( diff --git a/src/data/supervisor/root.ts b/src/data/supervisor/root.ts deleted file mode 100644 index 51fe449ecd..0000000000 --- a/src/data/supervisor/root.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { HomeAssistant } from "../../types"; - -interface SupervisorBaseAvailableUpdates { - panel_path?: string; - update_type?: string; - version_latest?: string; -} - -interface SupervisorAddonAvailableUpdates - extends SupervisorBaseAvailableUpdates { - update_type?: "addon"; - icon?: string; - name?: string; -} - -interface SupervisorCoreAvailableUpdates - extends SupervisorBaseAvailableUpdates { - update_type?: "core"; -} - -interface SupervisorOsAvailableUpdates extends SupervisorBaseAvailableUpdates { - update_type?: "os"; -} - -interface SupervisorSupervisorAvailableUpdates - extends SupervisorBaseAvailableUpdates { - update_type?: "supervisor"; -} - -export type SupervisorAvailableUpdates = - | SupervisorAddonAvailableUpdates - | SupervisorCoreAvailableUpdates - | SupervisorOsAvailableUpdates - | SupervisorSupervisorAvailableUpdates; - -export interface SupervisorAvailableUpdatesResponse { - available_updates: SupervisorAvailableUpdates[]; -} - -export const fetchSupervisorAvailableUpdates = async ( - hass: HomeAssistant -): Promise => - ( - await hass.callWS({ - type: "supervisor/api", - endpoint: "/available_updates", - method: "get", - }) - ).available_updates; - -export const refreshSupervisorAvailableUpdates = async ( - hass: HomeAssistant -): Promise => - hass.callWS({ - type: "supervisor/api", - endpoint: "/refresh_updates", - method: "post", - }); diff --git a/src/panels/config/dashboard/ha-config-dashboard.ts b/src/panels/config/dashboard/ha-config-dashboard.ts index 731a5c7079..3f0e3e4849 100644 --- a/src/panels/config/dashboard/ha-config-dashboard.ts +++ b/src/panels/config/dashboard/ha-config-dashboard.ts @@ -1,3 +1,5 @@ +import type { ActionDetail } from "@material/mwc-list"; +import "@material/mwc-list/mwc-list-item"; import { mdiCloudLock, mdiDotsVertical, @@ -5,10 +7,9 @@ import { mdiMagnify, mdiNewBox, } from "@mdi/js"; -import "@material/mwc-list/mwc-list-item"; -import type { ActionDetail } from "@material/mwc-list"; import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-toolbar/app-toolbar"; +import type { HassEntities } from "home-assistant-js-websocket"; import { css, CSSResultGroup, @@ -18,30 +19,29 @@ import { TemplateResult, } from "lit"; import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; -import "../../../components/ha-card"; -import "../../../components/ha-icon-next"; -import "../../../components/ha-icon-button"; -import "../../../components/ha-menu-button"; +import { computeStateDomain } from "../../../common/entity/compute_state_domain"; +import { caseInsensitiveStringCompare } from "../../../common/string/compare"; import "../../../components/ha-button-menu"; +import "../../../components/ha-card"; +import "../../../components/ha-icon-button"; +import "../../../components/ha-icon-next"; +import "../../../components/ha-menu-button"; import "../../../components/ha-svg-icon"; import { CloudStatus } from "../../../data/cloud"; -import { - refreshSupervisorAvailableUpdates, - SupervisorAvailableUpdates, -} from "../../../data/supervisor/root"; +import { updateCanInstall, UpdateEntity } from "../../../data/update"; +import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar"; import "../../../layouts/ha-app-layout"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; +import { documentationUrl } from "../../../util/documentation-url"; +import { showToast } from "../../../util/toast"; import "../ha-config-section"; import { configSections } from "../ha-panel-config"; import "./ha-config-navigation"; import "./ha-config-updates"; -import { fireEvent } from "../../../common/dom/fire_event"; -import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; -import { showToast } from "../../../util/toast"; -import { documentationUrl } from "../../../util/documentation-url"; const randomTip = (hass: HomeAssistant) => { const weighted: string[] = []; @@ -113,9 +113,6 @@ class HaConfigDashboard extends LitElement { @property() public cloudStatus?: CloudStatus; - // null means not available - @property() public supervisorUpdates?: SupervisorAvailableUpdates[] | null; - @property() public showAdvanced!: boolean; @state() private _tip?: string; @@ -123,6 +120,9 @@ class HaConfigDashboard extends LitElement { private _notifyUpdates = false; protected render(): TemplateResult { + const canInstallUpdates = this._filterUpdateEntitiesWithInstall( + this.hass.states + ); return html` @@ -160,50 +160,47 @@ class HaConfigDashboard extends LitElement { .isWide=${this.isWide} full-width > - ${this.supervisorUpdates === undefined - ? // Hide everything until updates loaded - html`` - : html`${this.supervisorUpdates?.length - ? html` - - ` - : ""} - - ${this.narrow && this.supervisorUpdates?.length - ? html`
- ${this.hass.localize("panel.config")} -
` - : ""} - ${this.cloudStatus && isComponentLoaded(this.hass, "cloud") - ? html` - - ` - : ""} + ${canInstallUpdates.length + ? html` + + ` + : ""} + + ${this.narrow && canInstallUpdates.length + ? html`
+ ${this.hass.localize("panel.config")} +
` + : ""} + ${this.cloudStatus && isComponentLoaded(this.hass, "cloud") + ? html` -
`} + ` + : ""} + +
Tip! @@ -221,11 +218,11 @@ class HaConfigDashboard extends LitElement { this._tip = randomTip(this.hass); } - if (!changedProps.has("supervisorUpdates") || !this._notifyUpdates) { + if (!changedProps.has("hass") || !this._notifyUpdates) { return; } this._notifyUpdates = false; - if (this.supervisorUpdates?.length) { + if (this._filterUpdateEntitiesWithInstall(this.hass.states).length) { showToast(this, { message: this.hass.localize( "ui.panel.config.updates.updates_refreshed" @@ -238,6 +235,44 @@ class HaConfigDashboard extends LitElement { } } + private _filterUpdateEntities = memoizeOne((entities: HassEntities) => + ( + Object.values(entities).filter( + (entity) => computeStateDomain(entity) === "update" + ) as UpdateEntity[] + ).sort((a, b) => { + if (a.attributes.title === "Home Assistant Core") { + return -3; + } + if (b.attributes.title === "Home Assistant Core") { + return 3; + } + if (a.attributes.title === "Home Assistant Operating System") { + return -2; + } + if (b.attributes.title === "Home Assistant Operating System") { + return 2; + } + if (a.attributes.title === "Home Assistant Supervisor") { + return -1; + } + if (b.attributes.title === "Home Assistant Supervisor") { + return 1; + } + return caseInsensitiveStringCompare( + a.attributes.title || a.attributes.friendly_name || "", + b.attributes.title || b.attributes.friendly_name || "" + ); + }) + ); + + private _filterUpdateEntitiesWithInstall = memoizeOne( + (entities: HassEntities) => + this._filterUpdateEntities(entities).filter((entity) => + updateCanInstall(entity) + ) + ); + private _showQuickBar(): void { showQuickBar(this, { commandMode: true, @@ -246,20 +281,24 @@ class HaConfigDashboard extends LitElement { } private async _handleMenuAction(ev: CustomEvent) { + const _entities = this._filterUpdateEntities(this.hass.states).map( + (entity) => entity.entity_id + ); switch (ev.detail.index) { case 0: - if (isComponentLoaded(this.hass, "hassio")) { + if (_entities.length) { this._notifyUpdates = true; - await refreshSupervisorAvailableUpdates(this.hass); - fireEvent(this, "ha-refresh-supervisor"); + await this.hass.callService("homeassistant", "update_entity", { + entity_id: _entities, + }); return; } showAlertDialog(this, { title: this.hass.localize( - "ui.panel.config.updates.check_unavailable.title" + "ui.panel.config.updates.no_update_entities.title" ), text: this.hass.localize( - "ui.panel.config.updates.check_unavailable.description" + "ui.panel.config.updates.no_update_entities.description" ), warning: true, }); diff --git a/src/panels/config/dashboard/ha-config-updates.ts b/src/panels/config/dashboard/ha-config-updates.ts index 8c11e4325e..cae7397e58 100644 --- a/src/panels/config/dashboard/ha-config-updates.ts +++ b/src/panels/config/dashboard/ha-config-updates.ts @@ -1,21 +1,14 @@ import "@material/mwc-button/mwc-button"; -import { mdiPackageVariant } from "@mdi/js"; import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-item-body"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../common/dom/fire_event"; +import "../../../components/entity/state-badge"; import "../../../components/ha-alert"; -import "../../../components/ha-logo-svg"; -import "../../../components/ha-svg-icon"; -import { SupervisorAvailableUpdates } from "../../../data/supervisor/root"; -import { HomeAssistant } from "../../../types"; import "../../../components/ha-icon-next"; - -export const SUPERVISOR_UPDATE_NAMES = { - core: "Home Assistant Core", - os: "Home Assistant Operating System", - supervisor: "Home Assistant Supervisor", -}; +import type { UpdateEntity } from "../../../data/update"; +import { HomeAssistant } from "../../../types"; @customElement("ha-config-updates") class HaConfigUpdates extends LitElement { @@ -24,62 +17,60 @@ class HaConfigUpdates extends LitElement { @property({ type: Boolean }) public narrow!: boolean; @property({ attribute: false }) - public supervisorUpdates?: SupervisorAvailableUpdates[] | null; + public updateEntities?: UpdateEntity[]; @state() private _showAll = false; protected render(): TemplateResult { - if (!this.supervisorUpdates?.length) { + if (!this.updateEntities?.length) { return html``; } const updates = - this._showAll || this.supervisorUpdates.length <= 3 - ? this.supervisorUpdates - : this.supervisorUpdates.slice(0, 2); + this._showAll || this.updateEntities.length <= 3 + ? this.updateEntities + : this.updateEntities.slice(0, 2); return html`
${this.hass.localize("ui.panel.config.updates.title", { - count: this.supervisorUpdates.length, + count: this.updateEntities.length, })}
${updates.map( - (update) => html` - - - - ${update.update_type === "addon" - ? update.icon - ? html`` - : html`` - : html``} - - - ${update.update_type === "addon" - ? update.name - : SUPERVISOR_UPDATE_NAMES[update.update_type!]} -
- ${this.hass.localize( - "ui.panel.config.updates.version_available", - { - version_available: update.version_latest, - } - )} -
-
- ${!this.narrow ? html`` : ""} -
-
+ (entity) => html` + + + + + + ${entity.attributes.title || entity.attributes.friendly_name} +
+ ${this.hass.localize( + "ui.panel.config.updates.version_available", + { + version_available: entity.attributes.latest_version, + } + )} +
+
+ ${!this.narrow ? html`` : ""} +
` )} - ${!this._showAll && this.supervisorUpdates.length >= 4 + ${!this._showAll && this.updateEntities.length >= 4 ? html` ` @@ -87,6 +78,12 @@ class HaConfigUpdates extends LitElement { `; } + private _openMoreInfo(ev: MouseEvent): void { + fireEvent(this, "hass-more-info", { + entityId: (ev.currentTarget as any).entity_id, + }); + } + private _showAllClicked() { this._showAll = true; } @@ -99,25 +96,11 @@ class HaConfigUpdates extends LitElement { padding: 16px; padding-bottom: 0; } - a { - text-decoration: none; - color: var(--primary-text-color); - } .icon { display: inline-flex; height: 100%; align-items: center; } - img, - ha-svg-icon, - ha-logo-svg { - --mdc-icon-size: 32px; - max-height: 32px; - width: 32px; - } - ha-logo-svg { - color: var(--secondary-text-color); - } ha-icon-next { color: var(--secondary-text-color); height: 24px; @@ -139,6 +122,9 @@ class HaConfigUpdates extends LitElement { outline: none; text-decoration: underline; } + paper-icon-item { + cursor: pointer; + } `, ]; } diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts index 850408f095..0ca0544fe7 100644 --- a/src/panels/config/ha-panel-config.ts +++ b/src/panels/config/ha-panel-config.ts @@ -27,10 +27,6 @@ import { customElement, property, state } from "lit/decorators"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { listenMediaQuery } from "../../common/dom/media_query"; import { CloudStatus, fetchCloudStatus } from "../../data/cloud"; -import { - fetchSupervisorAvailableUpdates, - SupervisorAvailableUpdates, -} from "../../data/supervisor/root"; import "../../layouts/hass-loading-screen"; import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page"; import { PageNavigation } from "../../layouts/hass-tabs-subpage"; @@ -397,8 +393,6 @@ class HaPanelConfig extends HassRouterPage { @state() private _cloudStatus?: CloudStatus; - @state() private _supervisorUpdates?: SupervisorAvailableUpdates[] | null; - private _listeners: Array<() => void> = []; public connectedCallback() { @@ -433,19 +427,7 @@ class HaPanelConfig extends HassRouterPage { } }); } - if (isComponentLoaded(this.hass, "hassio")) { - this._loadSupervisorUpdates(); - this.addEventListener("ha-refresh-supervisor", () => { - this._loadSupervisorUpdates(); - }); - this.addEventListener("connection-status", (ev) => { - if (ev.detail === "connected") { - this._loadSupervisorUpdates(); - } - }); - } else { - this._supervisorUpdates = null; - } + this.addEventListener("ha-refresh-cloud-status", () => this._updateCloudStatus() ); @@ -476,7 +458,6 @@ class HaPanelConfig extends HassRouterPage { isWide, narrow: this.narrow, cloudStatus: this._cloudStatus, - supervisorUpdates: this._supervisorUpdates, }); } else { el.route = this.routeTail; @@ -485,7 +466,6 @@ class HaPanelConfig extends HassRouterPage { el.isWide = isWide; el.narrow = this.narrow; el.cloudStatus = this._cloudStatus; - el.supervisorUpdates = this._supervisorUpdates; } } @@ -503,16 +483,6 @@ class HaPanelConfig extends HassRouterPage { setTimeout(() => this._updateCloudStatus(), 5000); } } - - private async _loadSupervisorUpdates(): Promise { - try { - this._supervisorUpdates = await fetchSupervisorAvailableUpdates( - this.hass - ); - } catch (err) { - this._supervisorUpdates = null; - } - } } declare global { diff --git a/src/translations/en.json b/src/translations/en.json index 8fe2de52a5..3ef90c50bd 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1079,9 +1079,9 @@ "learn_more": "Learn more" }, "updates": { - "check_unavailable": { + "no_update_entities": { "title": "Unable to check for updates", - "description": "You need to run the Home Assistant operating system to be able to check and install updates from the Home Assistant user interface." + "description": "You do not have any integrations that provide updates." }, "check_updates": "Check for updates", "no_new_updates": "No new updates found",