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",