Skip to content

Commit f03cd9c

Browse files
authored
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
1 parent 19a4e37 commit f03cd9c

File tree

4 files changed

+227
-3
lines changed

4 files changed

+227
-3
lines changed
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { LitElement, css, html, nothing } from "lit";
2+
import { customElement, property, state } from "lit/decorators";
3+
import "../../components/ha-alert";
4+
import "../../components/ha-icon";
5+
import "../../components/ha-list-item";
6+
import "../../components/ha-spinner";
7+
import type {
8+
ExternalEntityAddToActions,
9+
ExternalEntityAddToAction,
10+
} from "../../external_app/external_messaging";
11+
import { showToast } from "../../util/toast";
12+
13+
import type { HomeAssistant } from "../../types";
14+
15+
@customElement("ha-more-info-add-to")
16+
export class HaMoreInfoAddTo extends LitElement {
17+
@property({ attribute: false }) public hass!: HomeAssistant;
18+
19+
@property({ attribute: false }) public entityId!: string;
20+
21+
@state() private _externalActions?: ExternalEntityAddToActions = {
22+
actions: [],
23+
};
24+
25+
@state() private _loading = true;
26+
27+
private async _loadExternalActions() {
28+
if (this.hass.auth.external?.config.hasEntityAddTo) {
29+
this._externalActions =
30+
await this.hass.auth.external?.sendMessage<"entity/add_to/get_actions">(
31+
{
32+
type: "entity/add_to/get_actions",
33+
payload: { entity_id: this.entityId },
34+
}
35+
);
36+
}
37+
}
38+
39+
private async _actionSelected(ev: CustomEvent) {
40+
const action = (ev.currentTarget as any)
41+
.action as ExternalEntityAddToAction;
42+
if (!action.enabled) {
43+
return;
44+
}
45+
46+
try {
47+
await this.hass.auth.external!.fireMessage({
48+
type: "entity/add_to",
49+
payload: {
50+
entity_id: this.entityId,
51+
app_payload: action.app_payload,
52+
},
53+
});
54+
} catch (err: any) {
55+
showToast(this, {
56+
message: this.hass.localize(
57+
"ui.dialogs.more_info_control.add_to.action_failed",
58+
{
59+
error: err.message || err,
60+
}
61+
),
62+
});
63+
}
64+
}
65+
66+
protected async firstUpdated() {
67+
await this._loadExternalActions();
68+
this._loading = false;
69+
}
70+
71+
protected render() {
72+
if (this._loading) {
73+
return html`
74+
<div class="loading">
75+
<ha-spinner></ha-spinner>
76+
</div>
77+
`;
78+
}
79+
80+
if (!this._externalActions?.actions.length) {
81+
return html`
82+
<ha-alert alert-type="info">
83+
${this.hass.localize(
84+
"ui.dialogs.more_info_control.add_to.no_actions"
85+
)}
86+
</ha-alert>
87+
`;
88+
}
89+
90+
return html`
91+
<div class="actions-list">
92+
${this._externalActions.actions.map(
93+
(action) => html`
94+
<ha-list-item
95+
graphic="icon"
96+
.disabled=${!action.enabled}
97+
.action=${action}
98+
.twoline=${!!action.details}
99+
@click=${this._actionSelected}
100+
>
101+
<span>${action.name}</span>
102+
${action.details
103+
? html`<span slot="secondary">${action.details}</span>`
104+
: nothing}
105+
<ha-icon slot="graphic" .icon=${action.mdi_icon}></ha-icon>
106+
</ha-list-item>
107+
`
108+
)}
109+
</div>
110+
`;
111+
}
112+
113+
static styles = css`
114+
:host {
115+
display: block;
116+
padding: var(--ha-space-2) var(--ha-space-6) var(--ha-space-6)
117+
var(--ha-space-6);
118+
}
119+
120+
.loading {
121+
display: flex;
122+
justify-content: center;
123+
align-items: center;
124+
padding: var(--ha-space-8);
125+
}
126+
127+
.actions-list {
128+
display: flex;
129+
flex-direction: column;
130+
}
131+
132+
ha-list-item {
133+
cursor: pointer;
134+
}
135+
136+
ha-list-item[disabled] {
137+
cursor: not-allowed;
138+
opacity: 0.5;
139+
}
140+
141+
ha-icon {
142+
display: flex;
143+
align-items: center;
144+
}
145+
`;
146+
}
147+
148+
declare global {
149+
interface HTMLElementTagNameMap {
150+
"ha-more-info-add-to": HaMoreInfoAddTo;
151+
}
152+
}

src/dialogs/more-info/ha-more-info-dialog.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
mdiPencil,
99
mdiPencilOff,
1010
mdiPencilOutline,
11+
mdiPlusBoxMultipleOutline,
1112
mdiTransitConnectionVariant,
1213
} from "@mdi/js";
1314
import type { HassEntity } from "home-assistant-js-websocket";
@@ -60,6 +61,7 @@ import {
6061
computeShowLogBookComponent,
6162
} from "./const";
6263
import "./controls/more-info-default";
64+
import "./ha-more-info-add-to";
6365
import "./ha-more-info-history-and-logbook";
6466
import "./ha-more-info-info";
6567
import "./ha-more-info-settings";
@@ -73,7 +75,7 @@ export interface MoreInfoDialogParams {
7375
data?: Record<string, any>;
7476
}
7577

76-
type View = "info" | "history" | "settings" | "related";
78+
type View = "info" | "history" | "settings" | "related" | "add_to";
7779

7880
interface ChildView {
7981
viewTag: string;
@@ -194,6 +196,10 @@ export class MoreInfoDialog extends LitElement {
194196
);
195197
}
196198

199+
private _shouldShowAddEntityTo(): boolean {
200+
return !!this.hass.auth.external?.config.hasEntityAddTo;
201+
}
202+
197203
private _getDeviceId(): string | null {
198204
const entity = this.hass.entities[this._entityId!] as
199205
| EntityRegistryEntry
@@ -295,6 +301,11 @@ export class MoreInfoDialog extends LitElement {
295301
this._setView("related");
296302
}
297303

304+
private _goToAddEntityTo(ev) {
305+
if (!shouldHandleRequestSelectedEvent(ev)) return;
306+
this._setView("add_to");
307+
}
308+
298309
private _breadcrumbClick(ev: Event) {
299310
ev.stopPropagation();
300311
this._setView("related");
@@ -521,6 +532,22 @@ export class MoreInfoDialog extends LitElement {
521532
.path=${mdiInformationOutline}
522533
></ha-svg-icon>
523534
</ha-list-item>
535+
${this._shouldShowAddEntityTo()
536+
? html`
537+
<ha-list-item
538+
graphic="icon"
539+
@request-selected=${this._goToAddEntityTo}
540+
>
541+
${this.hass.localize(
542+
"ui.dialogs.more_info_control.add_entity_to"
543+
)}
544+
<ha-svg-icon
545+
slot="graphic"
546+
.path=${mdiPlusBoxMultipleOutline}
547+
></ha-svg-icon>
548+
</ha-list-item>
549+
`
550+
: nothing}
524551
</ha-button-menu>
525552
`
526553
: nothing}
@@ -613,7 +640,14 @@ export class MoreInfoDialog extends LitElement {
613640
: "entity"}
614641
></ha-related-items>
615642
`
616-
: nothing
643+
: this._currView === "add_to"
644+
? html`
645+
<ha-more-info-add-to
646+
.hass=${this.hass}
647+
.entityId=${entityId}
648+
></ha-more-info-add-to>
649+
`
650+
: nothing
617651
)}
618652
</div>
619653
`

src/external_app/external_messaging.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ interface EMOutgoingMessageConfigGet extends EMMessage {
3636
type: "config/get";
3737
}
3838

39+
interface EMOutgoingMessageEntityAddToGetActions extends EMMessage {
40+
type: "entity/add_to/get_actions";
41+
payload: {
42+
entity_id: string;
43+
};
44+
}
45+
3946
interface EMOutgoingMessageBarCodeScan extends EMMessage {
4047
type: "bar_code/scan";
4148
payload: {
@@ -75,6 +82,10 @@ interface EMOutgoingMessageWithAnswer {
7582
request: EMOutgoingMessageConfigGet;
7683
response: ExternalConfig;
7784
};
85+
"entity/add_to/get_actions": {
86+
request: EMOutgoingMessageEntityAddToGetActions;
87+
response: ExternalEntityAddToActions;
88+
};
7889
}
7990

8091
interface EMOutgoingMessageExoplayerPlayHLS extends EMMessage {
@@ -157,6 +168,14 @@ interface EMOutgoingMessageThreadStoreInPlatformKeychain extends EMMessage {
157168
};
158169
}
159170

171+
interface EMOutgoingMessageAddEntityTo extends EMMessage {
172+
type: "entity/add_to";
173+
payload: {
174+
entity_id: string;
175+
app_payload: string; // Opaque string received from get_actions
176+
};
177+
}
178+
160179
type EMOutgoingMessageWithoutAnswer =
161180
| EMMessageResultError
162181
| EMMessageResultSuccess
@@ -177,7 +196,8 @@ type EMOutgoingMessageWithoutAnswer =
177196
| EMOutgoingMessageThemeUpdate
178197
| EMOutgoingMessageThreadStoreInPlatformKeychain
179198
| EMOutgoingMessageImprovScan
180-
| EMOutgoingMessageImprovConfigureDevice;
199+
| EMOutgoingMessageImprovConfigureDevice
200+
| EMOutgoingMessageAddEntityTo;
181201

182202
export interface EMIncomingMessageRestart {
183203
id: number;
@@ -305,6 +325,19 @@ export interface ExternalConfig {
305325
canSetupImprov?: boolean;
306326
downloadFileSupported?: boolean;
307327
appVersion?: string;
328+
hasEntityAddTo?: boolean; // Supports "Add to" from more-info dialog, with action coming from external app
329+
}
330+
331+
export interface ExternalEntityAddToAction {
332+
enabled: boolean;
333+
name: string; // Translated name of the action to be displayed in the UI
334+
details?: string; // Optional translated details of the action to be displayed in the UI
335+
mdi_icon: string; // MDI icon name to be displayed in the UI (e.g., "mdi:car")
336+
app_payload: string; // Opaque string to be sent back when the action is selected
337+
}
338+
339+
export interface ExternalEntityAddToActions {
340+
actions: ExternalEntityAddToAction[];
308341
}
309342

310343
export class ExternalMessaging {

src/translations/en.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1434,6 +1434,7 @@
14341434
"back_to_info": "Back to info",
14351435
"info": "Information",
14361436
"related": "Related",
1437+
"add_entity_to": "Add to",
14371438
"history": "History",
14381439
"aggregate": "5-minute aggregated",
14391440
"logbook": "Activity",
@@ -1450,6 +1451,10 @@
14501451
"last_action": "Last action",
14511452
"last_triggered": "Last triggered"
14521453
},
1454+
"add_to": {
1455+
"no_actions": "No actions available",
1456+
"action_failed": "Failed to perform the action {error}"
1457+
},
14531458
"sun": {
14541459
"azimuth": "Azimuth",
14551460
"elevation": "Elevation",

0 commit comments

Comments
 (0)