From f03cd9c239c00e8ca3496b0e4723b015122c461f Mon Sep 17 00:00:00 2001 From: Timothy <6560631+TimoPtr@users.noreply.github.com> Date: Thu, 6 Nov 2025 09:25:05 +0100 Subject: [PATCH] Add Add entity to feature for external_app (#26346) * Add Add entity to feature for external_app * Update icon from plus to plusboxmultiple * Apply suggestion on the name * Add missing shouldHandleRequestSelectedEvent that caused duplicate * WIP * Rework the logic to match the agreed design * Rename property * Apply PR comments * Apply prettier * Merge MessageWithAnswer * Apply PR comments --- src/dialogs/more-info/ha-more-info-add-to.ts | 152 +++++++++++++++++++ src/dialogs/more-info/ha-more-info-dialog.ts | 38 ++++- src/external_app/external_messaging.ts | 35 ++++- src/translations/en.json | 5 + 4 files changed, 227 insertions(+), 3 deletions(-) create mode 100644 src/dialogs/more-info/ha-more-info-add-to.ts diff --git a/src/dialogs/more-info/ha-more-info-add-to.ts b/src/dialogs/more-info/ha-more-info-add-to.ts new file mode 100644 index 0000000000..262b2f8ab8 --- /dev/null +++ b/src/dialogs/more-info/ha-more-info-add-to.ts @@ -0,0 +1,152 @@ +import { LitElement, css, html, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import "../../components/ha-alert"; +import "../../components/ha-icon"; +import "../../components/ha-list-item"; +import "../../components/ha-spinner"; +import type { + ExternalEntityAddToActions, + ExternalEntityAddToAction, +} from "../../external_app/external_messaging"; +import { showToast } from "../../util/toast"; + +import type { HomeAssistant } from "../../types"; + +@customElement("ha-more-info-add-to") +export class HaMoreInfoAddTo extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public entityId!: string; + + @state() private _externalActions?: ExternalEntityAddToActions = { + actions: [], + }; + + @state() private _loading = true; + + private async _loadExternalActions() { + if (this.hass.auth.external?.config.hasEntityAddTo) { + this._externalActions = + await this.hass.auth.external?.sendMessage<"entity/add_to/get_actions">( + { + type: "entity/add_to/get_actions", + payload: { entity_id: this.entityId }, + } + ); + } + } + + private async _actionSelected(ev: CustomEvent) { + const action = (ev.currentTarget as any) + .action as ExternalEntityAddToAction; + if (!action.enabled) { + return; + } + + try { + await this.hass.auth.external!.fireMessage({ + type: "entity/add_to", + payload: { + entity_id: this.entityId, + app_payload: action.app_payload, + }, + }); + } catch (err: any) { + showToast(this, { + message: this.hass.localize( + "ui.dialogs.more_info_control.add_to.action_failed", + { + error: err.message || err, + } + ), + }); + } + } + + protected async firstUpdated() { + await this._loadExternalActions(); + this._loading = false; + } + + protected render() { + if (this._loading) { + return html` +
+ +
+ `; + } + + if (!this._externalActions?.actions.length) { + return html` + + ${this.hass.localize( + "ui.dialogs.more_info_control.add_to.no_actions" + )} + + `; + } + + return html` +
+ ${this._externalActions.actions.map( + (action) => html` + + ${action.name} + ${action.details + ? html`${action.details}` + : nothing} + + + ` + )} +
+ `; + } + + static styles = css` + :host { + display: block; + padding: var(--ha-space-2) var(--ha-space-6) var(--ha-space-6) + var(--ha-space-6); + } + + .loading { + display: flex; + justify-content: center; + align-items: center; + padding: var(--ha-space-8); + } + + .actions-list { + display: flex; + flex-direction: column; + } + + ha-list-item { + cursor: pointer; + } + + ha-list-item[disabled] { + cursor: not-allowed; + opacity: 0.5; + } + + ha-icon { + display: flex; + align-items: center; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-more-info-add-to": HaMoreInfoAddTo; + } +} diff --git a/src/dialogs/more-info/ha-more-info-dialog.ts b/src/dialogs/more-info/ha-more-info-dialog.ts index 3442fff685..403e1581fb 100644 --- a/src/dialogs/more-info/ha-more-info-dialog.ts +++ b/src/dialogs/more-info/ha-more-info-dialog.ts @@ -8,6 +8,7 @@ import { mdiPencil, mdiPencilOff, mdiPencilOutline, + mdiPlusBoxMultipleOutline, mdiTransitConnectionVariant, } from "@mdi/js"; import type { HassEntity } from "home-assistant-js-websocket"; @@ -60,6 +61,7 @@ import { computeShowLogBookComponent, } from "./const"; import "./controls/more-info-default"; +import "./ha-more-info-add-to"; import "./ha-more-info-history-and-logbook"; import "./ha-more-info-info"; import "./ha-more-info-settings"; @@ -73,7 +75,7 @@ export interface MoreInfoDialogParams { data?: Record; } -type View = "info" | "history" | "settings" | "related"; +type View = "info" | "history" | "settings" | "related" | "add_to"; interface ChildView { viewTag: string; @@ -194,6 +196,10 @@ export class MoreInfoDialog extends LitElement { ); } + private _shouldShowAddEntityTo(): boolean { + return !!this.hass.auth.external?.config.hasEntityAddTo; + } + private _getDeviceId(): string | null { const entity = this.hass.entities[this._entityId!] as | EntityRegistryEntry @@ -295,6 +301,11 @@ export class MoreInfoDialog extends LitElement { this._setView("related"); } + private _goToAddEntityTo(ev) { + if (!shouldHandleRequestSelectedEvent(ev)) return; + this._setView("add_to"); + } + private _breadcrumbClick(ev: Event) { ev.stopPropagation(); this._setView("related"); @@ -521,6 +532,22 @@ export class MoreInfoDialog extends LitElement { .path=${mdiInformationOutline} > + ${this._shouldShowAddEntityTo() + ? html` + + ${this.hass.localize( + "ui.dialogs.more_info_control.add_entity_to" + )} + + + ` + : nothing} ` : nothing} @@ -613,7 +640,14 @@ export class MoreInfoDialog extends LitElement { : "entity"} > ` - : nothing + : this._currView === "add_to" + ? html` + + ` + : nothing )} ` diff --git a/src/external_app/external_messaging.ts b/src/external_app/external_messaging.ts index 1ece06d9b8..8a91faeb1c 100644 --- a/src/external_app/external_messaging.ts +++ b/src/external_app/external_messaging.ts @@ -36,6 +36,13 @@ interface EMOutgoingMessageConfigGet extends EMMessage { type: "config/get"; } +interface EMOutgoingMessageEntityAddToGetActions extends EMMessage { + type: "entity/add_to/get_actions"; + payload: { + entity_id: string; + }; +} + interface EMOutgoingMessageBarCodeScan extends EMMessage { type: "bar_code/scan"; payload: { @@ -75,6 +82,10 @@ interface EMOutgoingMessageWithAnswer { request: EMOutgoingMessageConfigGet; response: ExternalConfig; }; + "entity/add_to/get_actions": { + request: EMOutgoingMessageEntityAddToGetActions; + response: ExternalEntityAddToActions; + }; } interface EMOutgoingMessageExoplayerPlayHLS extends EMMessage { @@ -157,6 +168,14 @@ interface EMOutgoingMessageThreadStoreInPlatformKeychain extends EMMessage { }; } +interface EMOutgoingMessageAddEntityTo extends EMMessage { + type: "entity/add_to"; + payload: { + entity_id: string; + app_payload: string; // Opaque string received from get_actions + }; +} + type EMOutgoingMessageWithoutAnswer = | EMMessageResultError | EMMessageResultSuccess @@ -177,7 +196,8 @@ type EMOutgoingMessageWithoutAnswer = | EMOutgoingMessageThemeUpdate | EMOutgoingMessageThreadStoreInPlatformKeychain | EMOutgoingMessageImprovScan - | EMOutgoingMessageImprovConfigureDevice; + | EMOutgoingMessageImprovConfigureDevice + | EMOutgoingMessageAddEntityTo; export interface EMIncomingMessageRestart { id: number; @@ -305,6 +325,19 @@ export interface ExternalConfig { canSetupImprov?: boolean; downloadFileSupported?: boolean; appVersion?: string; + hasEntityAddTo?: boolean; // Supports "Add to" from more-info dialog, with action coming from external app +} + +export interface ExternalEntityAddToAction { + enabled: boolean; + name: string; // Translated name of the action to be displayed in the UI + details?: string; // Optional translated details of the action to be displayed in the UI + mdi_icon: string; // MDI icon name to be displayed in the UI (e.g., "mdi:car") + app_payload: string; // Opaque string to be sent back when the action is selected +} + +export interface ExternalEntityAddToActions { + actions: ExternalEntityAddToAction[]; } export class ExternalMessaging { diff --git a/src/translations/en.json b/src/translations/en.json index ca0877753e..dddeae42ad 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1434,6 +1434,7 @@ "back_to_info": "Back to info", "info": "Information", "related": "Related", + "add_entity_to": "Add to", "history": "History", "aggregate": "5-minute aggregated", "logbook": "Activity", @@ -1450,6 +1451,10 @@ "last_action": "Last action", "last_triggered": "Last triggered" }, + "add_to": { + "no_actions": "No actions available", + "action_failed": "Failed to perform the action {error}" + }, "sun": { "azimuth": "Azimuth", "elevation": "Elevation",