diff --git a/src/panels/config/core/ha-config-section-updates.ts b/src/panels/config/core/ha-config-section-updates.ts index 8b7574cd92..ac47231ff0 100644 --- a/src/panels/config/core/ha-config-section-updates.ts +++ b/src/panels/config/core/ha-config-section-updates.ts @@ -41,6 +41,8 @@ class HaConfigSectionUpdates extends LitElement { @property({ type: Boolean }) public narrow = false; + @state() private _searchParms = new URLSearchParams(window.location.search); + @state() private _showSkipped = false; @state() private _supervisorInfo?: HassioSupervisorInfo; @@ -65,7 +67,9 @@ class HaConfigSectionUpdates extends LitElement { return html` )[] { + return [ + subscribeRepairsIssueRegistry( + this.hass!.connection, + (repairs: { issues: RepairsIssue[] }) => { + // Filter to only active and non-ignored issues + this._repairsIssues = repairs.issues.filter( + (issue) => issue.active !== false && !issue.ignored + ); + } + ), + ]; + } + + public setConfig(config: RepairsCardConfig): void { + this._config = config; + } + + public getCardSize(): number { + return this._config?.vertical ? 2 : 1; + } + + public getGridOptions(): LovelaceGridOptions { + const columns = 6; + let min_columns = 6; + let rows = 1; + + if (this._config?.vertical) { + rows++; + min_columns = 3; + } + return { + columns, + rows, + min_columns, + min_rows: rows, + }; + } + + private async _handleAction(ev: ActionHandlerEvent) { + if (ev.detail.action === "tap" && !hasAction(this._config?.tap_action)) { + navigate("/config/repairs"); + return; + } + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } + + private get _hasCardAction() { + return ( + !this._config?.tap_action || + hasAction(this._config?.tap_action) || + hasAction(this._config?.hold_action) || + hasAction(this._config?.double_tap_action) + ); + } + + protected willUpdate(changedProps: PropertyValues): void { + super.willUpdate(changedProps); + + if (!this._config || !this.hass) { + return; + } + + // Update visibility based on admin status and repairs count + const shouldBeHidden = + !this.hass.user?.is_admin || + (this._config.hide_empty && this._repairsIssues.length === 0); + + if (shouldBeHidden !== this.hidden) { + this.style.display = shouldBeHidden ? "none" : ""; + this.toggleAttribute("hidden", shouldBeHidden); + fireEvent(this, "card-visibility-changed", { value: !shouldBeHidden }); + } + } + + protected render(): TemplateResult | typeof nothing { + if (!this._config || !this.hass || this.hidden) { + return nothing; + } + + const count = this._repairsIssues.length; + + const label = this.hass.localize("ui.card.repairs.title"); + const secondary = + count > 0 + ? this.hass.localize("ui.card.repairs.count_issues", { + count, + }) + : this.hass.localize("ui.card.repairs.no_issues"); + + return html` + + + + + + + `; + } + + static styles = [ + tileCardStyle, + css` + :host { + --tile-color: var(--warning-color); + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "hui-repairs-card": HuiRepairsCard; + } +} diff --git a/src/panels/lovelace/cards/hui-updates-card.ts b/src/panels/lovelace/cards/hui-updates-card.ts new file mode 100644 index 0000000000..6231dc5dae --- /dev/null +++ b/src/panels/lovelace/cards/hui-updates-card.ts @@ -0,0 +1,157 @@ +import { mdiPackageUp } from "@mdi/js"; +import type { PropertyValues, TemplateResult } from "lit"; +import { css, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { navigate } from "../../../common/navigate"; +import "../../../components/ha-card"; +import "../../../components/tile/ha-tile-container"; +import "../../../components/tile/ha-tile-icon"; +import "../../../components/tile/ha-tile-info"; +import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; +import { + filterUpdateEntities, + updateCanInstall, + type UpdateEntity, +} from "../../../data/update"; +import type { HomeAssistant } from "../../../types"; +import { handleAction } from "../common/handle-action"; +import { hasAction } from "../common/has-action"; +import type { LovelaceCard, LovelaceGridOptions } from "../types"; +import { tileCardStyle } from "./tile/tile-card-style"; +import type { UpdatesCardConfig } from "./types"; + +@customElement("hui-updates-card") +export class HuiUpdatesCard extends LitElement implements LovelaceCard { + public connectedWhileHidden = true; + + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() private _config?: UpdatesCardConfig; + + public setConfig(config: UpdatesCardConfig): void { + this._config = config; + } + + public getCardSize(): number { + return this._config?.vertical ? 2 : 1; + } + + public getGridOptions(): LovelaceGridOptions { + const columns = 6; + let min_columns = 6; + let rows = 1; + + if (this._config?.vertical) { + rows++; + min_columns = 3; + } + return { + columns, + rows, + min_columns, + min_rows: rows, + }; + } + + private _getUpdateEntities(): UpdateEntity[] { + if (!this.hass) { + return []; + } + return filterUpdateEntities( + this.hass.states, + this.hass.locale.language + ).filter((entity) => updateCanInstall(entity, false)); + } + + private async _handleAction(ev: ActionHandlerEvent) { + if (ev.detail.action === "tap" && !hasAction(this._config?.tap_action)) { + navigate("/config/updates"); + return; + } + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } + + private get _hasCardAction() { + return ( + !this._config?.tap_action || + hasAction(this._config?.tap_action) || + hasAction(this._config?.hold_action) || + hasAction(this._config?.double_tap_action) + ); + } + + protected willUpdate(changedProps: PropertyValues): void { + super.willUpdate(changedProps); + + if (!this._config || !this.hass) { + return; + } + + const updateEntities = this._getUpdateEntities(); + + // Update visibility based on admin status and updates count + const shouldBeHidden = + !this.hass.user?.is_admin || + (this._config.hide_empty && updateEntities.length === 0); + + if (shouldBeHidden !== this.hidden) { + this.style.display = shouldBeHidden ? "none" : ""; + this.toggleAttribute("hidden", shouldBeHidden); + fireEvent(this, "card-visibility-changed", { value: !shouldBeHidden }); + } + } + + protected render(): TemplateResult | typeof nothing { + if (!this._config || !this.hass || this.hidden) { + return nothing; + } + + const updateEntities = this._getUpdateEntities(); + const count = updateEntities.length; + + const label = this.hass.localize("ui.card.updates.title"); + const secondary = + count > 0 + ? this.hass.localize("ui.card.updates.count_updates", { + count, + }) + : this.hass.localize("ui.card.updates.no_updates"); + + return html` + + + + + + + `; + } + + static styles = [ + tileCardStyle, + css` + :host { + --tile-color: var(--info-color); + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "hui-updates-card": HuiUpdatesCard; + } +} diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index d6d05476ca..90c4d56079 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -682,3 +682,19 @@ export interface DiscoveredDevicesCardConfig extends LovelaceCardConfig { hold_action?: ActionConfig; double_tap_action?: ActionConfig; } + +export interface RepairsCardConfig extends LovelaceCardConfig { + hide_empty?: boolean; + vertical?: boolean; + tap_action?: ActionConfig; + hold_action?: ActionConfig; + double_tap_action?: ActionConfig; +} + +export interface UpdatesCardConfig extends LovelaceCardConfig { + hide_empty?: boolean; + vertical?: boolean; + tap_action?: ActionConfig; + hold_action?: ActionConfig; + double_tap_action?: ActionConfig; +} diff --git a/src/panels/lovelace/create-element/create-card-element.ts b/src/panels/lovelace/create-element/create-card-element.ts index ca5faa7395..95e71fcbe3 100644 --- a/src/panels/lovelace/create-element/create-card-element.ts +++ b/src/panels/lovelace/create-element/create-card-element.ts @@ -74,6 +74,8 @@ const LAZY_LOAD_TYPES = { error: () => import("../cards/hui-error-card"), "home-summary": () => import("../cards/hui-home-summary-card"), "discovered-devices": () => import("../cards/hui-discovered-devices-card"), + repairs: () => import("../cards/hui-repairs-card"), + updates: () => import("../cards/hui-updates-card"), gauge: () => import("../cards/hui-gauge-card"), "history-graph": () => import("../cards/hui-history-graph-card"), "horizontal-stack": () => import("../cards/hui-horizontal-stack-card"), diff --git a/src/panels/lovelace/strategies/home/home-overview-view-strategy.ts b/src/panels/lovelace/strategies/home/home-overview-view-strategy.ts index f2044cad62..4b9f6fab7a 100644 --- a/src/panels/lovelace/strategies/home/home-overview-view-strategy.ts +++ b/src/panels/lovelace/strategies/home/home-overview-view-strategy.ts @@ -23,7 +23,9 @@ import type { EmptyStateCardConfig, HomeSummaryCard, MarkdownCardConfig, + RepairsCardConfig, TileCardConfig, + UpdatesCardConfig, } from "../../cards/types"; import type { Condition } from "../../common/validate-condition"; import type { CommonControlSectionStrategyConfig } from "../usage_prediction/common-controls-section-strategy"; @@ -252,6 +254,24 @@ export class HomeOverviewViewStrategy extends ReactiveElement { // Build summary cards (used in both mobile section and sidebar) const summaryCards: LovelaceCardConfig[] = [ + // Repairs card - only visible to admins, hides when empty + { + type: "repairs", + hide_empty: true, + tap_action: { + action: "navigate", + navigation_path: "/config/repairs?historyBack=1", + }, + } satisfies RepairsCardConfig, + // Updates card - only visible to admins, hides when empty + { + type: "updates", + hide_empty: true, + tap_action: { + action: "navigate", + navigation_path: "/config/updates?historyBack=1", + }, + } satisfies UpdatesCardConfig, // Discovered devices card - only visible to admins, hides when empty { type: "discovered-devices", diff --git a/src/translations/en.json b/src/translations/en.json index 8f33dbaca2..14ce88c83f 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -220,6 +220,16 @@ "count_devices": "{count} {count, plural,\n one {device to add}\n other {devices to add}\n}", "no_devices": "No devices to add" }, + "repairs": { + "title": "Repairs", + "count_issues": "{count} {count, plural,\n one {issue}\n other {issues}\n}", + "no_issues": "No issues" + }, + "updates": { + "title": "Updates", + "count_updates": "{count} {count, plural,\n one {update available}\n other {updates available}\n}", + "no_updates": "Up to date" + }, "media_player": { "source": "Source", "sound_mode": "Sound mode",