From 39774c0e024fc8f0ef114306142499b86943153c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 9 Dec 2021 13:30:20 -0800 Subject: [PATCH] Allow trigger reconnect from external bus (#10819) --- src/components/ha-sidebar.ts | 6 ++ src/external_app/external_auth.ts | 4 +- src/external_app/external_messaging.ts | 71 +++++++++++++++++--- src/state/external-mixin.ts | 15 +++++ src/state/hass-element.ts | 2 + src/state/themes-mixin.ts | 2 + test/external_app/external_messaging.spec.ts | 6 +- 7 files changed, 91 insertions(+), 15 deletions(-) create mode 100644 src/state/external-mixin.ts diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index f864e8a874..6b9008984f 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -43,6 +43,7 @@ import { PersistentNotification, subscribeNotifications, } from "../data/persistent_notification"; +import { getExternalConfig } from "../external_app/external_config"; import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive"; import { haStyleScrollbar } from "../resources/styles"; import type { HomeAssistant, PanelInfo, Route } from "../types"; @@ -266,6 +267,11 @@ class HaSidebar extends LitElement { subscribeNotifications(this.hass.connection, (notifications) => { this._notifications = notifications; }); + + // Temporary workaround for a bug in Android. Can be removed in Home Assistant 2022.2 + if (this.hass.auth.external) { + getExternalConfig(this.hass.auth.external); + } } protected updated(changedProps) { diff --git a/src/external_app/external_auth.ts b/src/external_app/external_auth.ts index 4c7c5f7f21..1917e4bec2 100644 --- a/src/external_app/external_auth.ts +++ b/src/external_app/external_auth.ts @@ -2,7 +2,7 @@ * Auth class that connects to a native app for authentication. */ import { Auth } from "home-assistant-js-websocket"; -import { ExternalMessaging, InternalMessage } from "./external_messaging"; +import { ExternalMessaging, EMMessage } from "./external_messaging"; const CALLBACK_SET_TOKEN = "externalAuthSetToken"; const CALLBACK_REVOKE_TOKEN = "externalAuthRevokeToken"; @@ -36,7 +36,7 @@ declare global { postMessage(payload: BasePayload); }; externalBus: { - postMessage(payload: InternalMessage); + postMessage(payload: EMMessage); }; }; }; diff --git a/src/external_app/external_messaging.ts b/src/external_app/external_messaging.ts index 1ad273d76c..f5640858fc 100644 --- a/src/external_app/external_messaging.ts +++ b/src/external_app/external_messaging.ts @@ -1,3 +1,4 @@ +import { Connection } from "home-assistant-js-websocket"; import { externalForwardConnectionEvents, externalForwardHaptics, @@ -7,39 +8,50 @@ const CALLBACK_EXTERNAL_BUS = "externalBus"; interface CommandInFlight { resolve: (data: any) => void; - reject: (err: ExternalError) => void; + reject: (err: EMError) => void; } -export interface InternalMessage { +export interface EMMessage { id?: number; type: string; payload?: unknown; } -interface ExternalError { +interface EMError { code: string; message: string; } -interface ExternalMessageResult { +interface EMMessageResultSuccess { id: number; type: "result"; success: true; result: unknown; } -interface ExternalMessageResultError { +interface EMMessageResultError { id: number; type: "result"; success: false; - error: ExternalError; + error: EMError; } -type ExternalMessage = ExternalMessageResult | ExternalMessageResultError; +interface EMExternalMessageRestart { + id: number; + type: "command"; + command: "restart"; +} + +type ExternalMessage = + | EMMessageResultSuccess + | EMMessageResultError + | EMExternalMessageRestart; export class ExternalMessaging { public commands: { [msgId: number]: CommandInFlight } = {}; + public connection?: Connection; + public cache: Record = {}; public msgId = 0; @@ -54,7 +66,7 @@ export class ExternalMessaging { * Send message to external app that expects a response. * @param msg message to send */ - public sendMessage(msg: InternalMessage): Promise { + public sendMessage(msg: EMMessage): Promise { const msgId = ++this.msgId; msg.id = msgId; @@ -69,7 +81,9 @@ export class ExternalMessaging { * Send message to external app without expecting a response. * @param msg message to send */ - public fireMessage(msg: InternalMessage) { + public fireMessage( + msg: EMMessage | EMMessageResultSuccess | EMMessageResultError + ) { if (!msg.id) { msg.id = ++this.msgId; } @@ -82,6 +96,43 @@ export class ExternalMessaging { console.log("Receiving message from external app", msg); } + if (msg.type === "command") { + if (!this.connection) { + // eslint-disable-next-line no-console + console.warn("Received command without having connection set", msg); + this.fireMessage({ + id: msg.id, + type: "result", + success: false, + error: { + code: "commands_not_init", + message: `Commands connection not set`, + }, + }); + } else if (msg.command === "restart") { + this.connection.socket.close(); + this.fireMessage({ + id: msg.id, + type: "result", + success: true, + result: null, + }); + } else { + // eslint-disable-next-line no-console + console.warn("Received unknown command", msg.command, msg); + this.fireMessage({ + id: msg.id, + type: "result", + success: false, + error: { + code: "unknown_command", + message: `Unknown command ${msg.command}`, + }, + }); + } + return; + } + const pendingCmd = this.commands[msg.id]; if (!pendingCmd) { @@ -99,7 +150,7 @@ export class ExternalMessaging { } } - protected _sendExternal(msg: InternalMessage) { + protected _sendExternal(msg: EMMessage) { if (__DEV__) { // eslint-disable-next-line no-console console.log("Sending message to external app", msg); diff --git a/src/state/external-mixin.ts b/src/state/external-mixin.ts new file mode 100644 index 0000000000..4bc48e5faf --- /dev/null +++ b/src/state/external-mixin.ts @@ -0,0 +1,15 @@ +import { Constructor } from "../types"; +import { HassBaseEl } from "./hass-base-mixin"; + +export const ExternalMixin = >( + superClass: T +) => + class extends superClass { + protected hassConnected() { + super.hassConnected(); + + if (this.hass!.auth.external) { + this.hass!.auth.external.connection = this.hass!.connection; + } + } + }; diff --git a/src/state/hass-element.ts b/src/state/hass-element.ts index 7a24238c23..1a8a6f3999 100644 --- a/src/state/hass-element.ts +++ b/src/state/hass-element.ts @@ -6,6 +6,7 @@ import DisconnectToastMixin from "./disconnect-toast-mixin"; import { hapticMixin } from "./haptic-mixin"; import { HassBaseEl } from "./hass-base-mixin"; import { loggingMixin } from "./logging-mixin"; +import { ExternalMixin } from "./external-mixin"; import MoreInfoMixin from "./more-info-mixin"; import NotificationMixin from "./notification-mixin"; import { panelTitleMixin } from "./panel-title-mixin"; @@ -31,4 +32,5 @@ export class HassElement extends ext(HassBaseEl, [ hapticMixin, panelTitleMixin, loggingMixin, + ExternalMixin, ]) {} diff --git a/src/state/themes-mixin.ts b/src/state/themes-mixin.ts index bb5c443a62..3595a54a59 100644 --- a/src/state/themes-mixin.ts +++ b/src/state/themes-mixin.ts @@ -131,5 +131,7 @@ export default >(superClass: T) => (themeMeta.getAttribute("default-content") as string); themeMeta.setAttribute("content", themeColor); } + + this.hass!.auth.external?.fireMessage({ type: "theme-update" }); } }; diff --git a/test/external_app/external_messaging.spec.ts b/test/external_app/external_messaging.spec.ts index 0903704572..6f3a3e2733 100644 --- a/test/external_app/external_messaging.spec.ts +++ b/test/external_app/external_messaging.spec.ts @@ -2,16 +2,16 @@ import { assert } from "chai"; import { ExternalMessaging, - InternalMessage, + EMMessage, } from "../../src/external_app/external_messaging"; // @ts-ignore global.__DEV__ = true; class MockExternalMessaging extends ExternalMessaging { - public mockSent: InternalMessage[] = []; + public mockSent: EMMessage[] = []; - protected _sendExternal(msg: InternalMessage) { + protected _sendExternal(msg: EMMessage) { this.mockSent.push(msg); } }