mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-08 18:39:40 +00:00
* 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
460 lines
11 KiB
TypeScript
460 lines
11 KiB
TypeScript
import type { NavigateOptions } from "../common/navigate";
|
|
import type { AutomationConfig } from "../data/automation";
|
|
|
|
const CALLBACK_EXTERNAL_BUS = "externalBus";
|
|
|
|
interface CommandInFlight {
|
|
resolve: (data: any) => void;
|
|
reject: (err: EMError) => void;
|
|
}
|
|
|
|
export interface EMMessage {
|
|
id?: number;
|
|
type: string;
|
|
}
|
|
|
|
interface EMError {
|
|
code: string;
|
|
message: string;
|
|
}
|
|
|
|
interface EMMessageResultSuccess {
|
|
id: number;
|
|
type: "result";
|
|
success: true;
|
|
result: unknown;
|
|
}
|
|
|
|
interface EMMessageResultError {
|
|
id: number;
|
|
type: "result";
|
|
success: false;
|
|
error: EMError;
|
|
}
|
|
|
|
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: {
|
|
title: string;
|
|
description: string;
|
|
alternative_option_label?: string;
|
|
};
|
|
}
|
|
|
|
interface EMOutgoingMessageBarCodeClose extends EMMessage {
|
|
type: "bar_code/close";
|
|
}
|
|
|
|
interface EMOutgoingMessageBarCodeNotify extends EMMessage {
|
|
type: "bar_code/notify";
|
|
payload: {
|
|
message: string;
|
|
};
|
|
}
|
|
|
|
interface EMOutgoingMessageMatterCommission extends EMMessage {
|
|
type: "matter/commission";
|
|
payload?: {
|
|
mac_extended_address: string | null;
|
|
extended_pan_id: string | null;
|
|
border_agent_id: string | null;
|
|
active_operational_dataset: string | null;
|
|
};
|
|
}
|
|
|
|
interface EMOutgoingMessageImportThreadCredentials extends EMMessage {
|
|
type: "thread/import_credentials";
|
|
}
|
|
|
|
interface EMOutgoingMessageWithAnswer {
|
|
"config/get": {
|
|
request: EMOutgoingMessageConfigGet;
|
|
response: ExternalConfig;
|
|
};
|
|
"entity/add_to/get_actions": {
|
|
request: EMOutgoingMessageEntityAddToGetActions;
|
|
response: ExternalEntityAddToActions;
|
|
};
|
|
}
|
|
|
|
interface EMOutgoingMessageExoplayerPlayHLS extends EMMessage {
|
|
type: "exoplayer/play_hls";
|
|
payload: {
|
|
url: string;
|
|
muted: boolean;
|
|
};
|
|
}
|
|
interface EMOutgoingMessageExoplayerResize extends EMMessage {
|
|
type: "exoplayer/resize";
|
|
payload: {
|
|
left: number;
|
|
top: number;
|
|
right: number;
|
|
bottom: number;
|
|
};
|
|
}
|
|
|
|
interface EMOutgoingMessageExoplayerStop extends EMMessage {
|
|
type: "exoplayer/stop";
|
|
}
|
|
|
|
interface EMOutgoingMessageThemeUpdate extends EMMessage {
|
|
type: "theme-update";
|
|
}
|
|
|
|
interface EMOutgoingMessageHaptic extends EMMessage {
|
|
type: "haptic";
|
|
payload: { hapticType: string };
|
|
}
|
|
|
|
interface EMOutgoingMessageConnectionStatus extends EMMessage {
|
|
type: "connection-status";
|
|
payload: { event: string };
|
|
}
|
|
|
|
interface EMOutgoingMessageAppConfiguration extends EMMessage {
|
|
type: "config_screen/show";
|
|
}
|
|
|
|
interface EMOutgoingMessageTagWrite extends EMMessage {
|
|
type: "tag/write";
|
|
payload: {
|
|
name: string | null;
|
|
tag: string;
|
|
};
|
|
}
|
|
|
|
interface EMOutgoingMessageSidebarShow extends EMMessage {
|
|
type: "sidebar/show";
|
|
}
|
|
|
|
interface EMOutgoingMessageAssistShow extends EMMessage {
|
|
type: "assist/show";
|
|
payload?: {
|
|
pipeline_id: "preferred" | "last_used" | string;
|
|
start_listening: boolean;
|
|
};
|
|
}
|
|
|
|
interface EMOutgoingMessageImprovScan extends EMMessage {
|
|
type: "improv/scan";
|
|
}
|
|
|
|
interface EMOutgoingMessageImprovConfigureDevice extends EMMessage {
|
|
type: "improv/configure_device";
|
|
payload: {
|
|
name: string;
|
|
};
|
|
}
|
|
|
|
interface EMOutgoingMessageThreadStoreInPlatformKeychain extends EMMessage {
|
|
type: "thread/store_in_platform_keychain";
|
|
payload: {
|
|
mac_extended_address: string | null;
|
|
border_agent_id: string | null;
|
|
active_operational_dataset: string;
|
|
extended_pan_id: string;
|
|
};
|
|
}
|
|
|
|
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
|
|
| EMOutgoingMessageAppConfiguration
|
|
| EMOutgoingMessageAssistShow
|
|
| EMOutgoingMessageBarCodeClose
|
|
| EMOutgoingMessageBarCodeNotify
|
|
| EMOutgoingMessageBarCodeScan
|
|
| EMOutgoingMessageConnectionStatus
|
|
| EMOutgoingMessageExoplayerPlayHLS
|
|
| EMOutgoingMessageExoplayerResize
|
|
| EMOutgoingMessageExoplayerStop
|
|
| EMOutgoingMessageHaptic
|
|
| EMOutgoingMessageImportThreadCredentials
|
|
| EMOutgoingMessageMatterCommission
|
|
| EMOutgoingMessageSidebarShow
|
|
| EMOutgoingMessageTagWrite
|
|
| EMOutgoingMessageThemeUpdate
|
|
| EMOutgoingMessageThreadStoreInPlatformKeychain
|
|
| EMOutgoingMessageImprovScan
|
|
| EMOutgoingMessageImprovConfigureDevice
|
|
| EMOutgoingMessageAddEntityTo;
|
|
|
|
export interface EMIncomingMessageRestart {
|
|
id: number;
|
|
type: "command";
|
|
command: "restart";
|
|
}
|
|
export interface EMIncomingMessageNavigate {
|
|
id: number;
|
|
type: "command";
|
|
command: "navigate";
|
|
payload: {
|
|
path: string;
|
|
options?: NavigateOptions;
|
|
};
|
|
}
|
|
|
|
export interface EMIncomingMessageShowNotifications {
|
|
id: number;
|
|
type: "command";
|
|
command: "notifications/show";
|
|
}
|
|
|
|
export interface EMIncomingMessageToggleSidebar {
|
|
id: number;
|
|
type: "command";
|
|
command: "sidebar/toggle";
|
|
}
|
|
|
|
export interface EMIncomingMessageShowSidebar {
|
|
id: number;
|
|
type: "command";
|
|
command: "sidebar/show";
|
|
}
|
|
|
|
export interface EMIncomingMessageShowAutomationEditor {
|
|
id: number;
|
|
type: "command";
|
|
command: "automation/editor/show";
|
|
payload?: {
|
|
config?: Partial<AutomationConfig>;
|
|
};
|
|
}
|
|
|
|
export interface EMIncomingMessageBarCodeScanResult {
|
|
id: number;
|
|
type: "command";
|
|
command: "bar_code/scan_result";
|
|
payload: {
|
|
// A string decoded from the barcode data.
|
|
rawValue: string;
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/Barcode_Detection_API#supported_barcode_formats
|
|
format:
|
|
| "aztec"
|
|
| "code_128"
|
|
| "code_39"
|
|
| "code_93"
|
|
| "codabar"
|
|
| "data_matrix"
|
|
| "ean_13"
|
|
| "ean_8"
|
|
| "itf"
|
|
| "pdf417"
|
|
| "qr_code"
|
|
| "upc_a"
|
|
| "upc_e"
|
|
| "unknown";
|
|
};
|
|
}
|
|
|
|
export interface EMIncomingMessageBarCodeScanAborted {
|
|
id: number;
|
|
type: "command";
|
|
command: "bar_code/aborted";
|
|
payload: {
|
|
reason: "canceled" | "alternative_options";
|
|
};
|
|
}
|
|
|
|
export interface ImprovDiscoveredDevice {
|
|
name: string;
|
|
}
|
|
|
|
export interface EMIncomingMessageImprovDeviceDiscovered extends EMMessage {
|
|
id: number;
|
|
type: "command";
|
|
command: "improv/discovered_device";
|
|
payload: ImprovDiscoveredDevice;
|
|
}
|
|
|
|
export interface EMIncomingMessageImprovDeviceSetupDone extends EMMessage {
|
|
id: number;
|
|
type: "command";
|
|
command: "improv/device_setup_done";
|
|
}
|
|
|
|
export type EMIncomingMessageCommands =
|
|
| EMIncomingMessageRestart
|
|
| EMIncomingMessageNavigate
|
|
| EMIncomingMessageShowNotifications
|
|
| EMIncomingMessageToggleSidebar
|
|
| EMIncomingMessageShowSidebar
|
|
| EMIncomingMessageShowAutomationEditor
|
|
| EMIncomingMessageBarCodeScanResult
|
|
| EMIncomingMessageBarCodeScanAborted
|
|
| EMIncomingMessageImprovDeviceDiscovered
|
|
| EMIncomingMessageImprovDeviceSetupDone;
|
|
|
|
type EMIncomingMessage =
|
|
| EMMessageResultSuccess
|
|
| EMMessageResultError
|
|
| EMIncomingMessageCommands;
|
|
|
|
type EMIncomingMessageHandler = (msg: EMIncomingMessageCommands) => boolean;
|
|
|
|
export interface ExternalConfig {
|
|
hasSettingsScreen?: boolean;
|
|
hasSidebar?: boolean;
|
|
canWriteTag?: boolean;
|
|
hasExoPlayer?: boolean;
|
|
canCommissionMatter?: boolean;
|
|
canImportThreadCredentials?: boolean;
|
|
canTransferThreadCredentialsToKeychain?: boolean;
|
|
hasAssist?: boolean;
|
|
hasBarCodeScanner?: number;
|
|
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 {
|
|
public config!: ExternalConfig;
|
|
|
|
public commands: Record<number, CommandInFlight> = {};
|
|
|
|
public msgId = 0;
|
|
|
|
private _commandHandler?: EMIncomingMessageHandler;
|
|
|
|
public async attach() {
|
|
window[CALLBACK_EXTERNAL_BUS] = (msg) => this.receiveMessage(msg);
|
|
window.addEventListener("connection-status", (ev) =>
|
|
this.fireMessage({
|
|
type: "connection-status",
|
|
payload: { event: ev.detail },
|
|
})
|
|
);
|
|
this.config = await this.sendMessage<"config/get">({
|
|
type: "config/get",
|
|
});
|
|
}
|
|
|
|
public addCommandHandler(handler: EMIncomingMessageHandler) {
|
|
this._commandHandler = handler;
|
|
}
|
|
|
|
/**
|
|
* Send message to external app that expects a response.
|
|
* @param msg message to send
|
|
*/
|
|
public sendMessage<T extends keyof EMOutgoingMessageWithAnswer>(
|
|
msg: EMOutgoingMessageWithAnswer[T]["request"]
|
|
): Promise<EMOutgoingMessageWithAnswer[T]["response"]> {
|
|
const msgId = ++this.msgId;
|
|
msg.id = msgId;
|
|
|
|
this._sendExternal(msg);
|
|
|
|
return new Promise<EMOutgoingMessageWithAnswer[T]["response"]>(
|
|
(resolve, reject) => {
|
|
this.commands[msgId] = { resolve, reject };
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Send message to external app without expecting a response.
|
|
* @param msg message to send
|
|
*/
|
|
public fireMessage(msg: EMOutgoingMessageWithoutAnswer) {
|
|
if (!msg.id) {
|
|
msg.id = ++this.msgId;
|
|
}
|
|
this._sendExternal(msg);
|
|
}
|
|
|
|
public receiveMessage(msg: EMIncomingMessage) {
|
|
if (__DEV__) {
|
|
// eslint-disable-next-line no-console
|
|
console.log("Receiving message from external app", msg);
|
|
}
|
|
|
|
if (msg.type === "command") {
|
|
if (!this._commandHandler || !this._commandHandler(msg)) {
|
|
let code: string;
|
|
let message: string;
|
|
if (this._commandHandler) {
|
|
code = "not_ready";
|
|
message = "Command handler not ready";
|
|
} else {
|
|
code = "unknown_command";
|
|
message = `Unknown command ${msg.command}`;
|
|
}
|
|
// eslint-disable-next-line no-console
|
|
console.warn(message, msg);
|
|
this.fireMessage({
|
|
id: msg.id,
|
|
type: "result",
|
|
success: false,
|
|
error: {
|
|
code,
|
|
message,
|
|
},
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
const pendingCmd = this.commands[msg.id];
|
|
|
|
if (!pendingCmd) {
|
|
// eslint-disable-next-line no-console
|
|
console.warn(`Received unknown msg ID`, msg.id);
|
|
return;
|
|
}
|
|
|
|
if (msg.type === "result") {
|
|
if (msg.success) {
|
|
pendingCmd.resolve(msg.result);
|
|
} else {
|
|
pendingCmd.reject(msg.error);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected _sendExternal(msg: EMMessage) {
|
|
if (__DEV__) {
|
|
// eslint-disable-next-line no-console
|
|
console.log("Sending message to external app", msg);
|
|
}
|
|
if (window.externalApp) {
|
|
window.externalApp.externalBus(JSON.stringify(msg));
|
|
} else {
|
|
window.webkit!.messageHandlers.externalBus.postMessage(msg);
|
|
}
|
|
}
|
|
}
|