diff --git a/hassio/src/addon-view/hassio-addon-info.js b/hassio/src/addon-view/hassio-addon-info.js index 25f283a693..4ec0a959a9 100644 --- a/hassio/src/addon-view/hassio-addon-info.js +++ b/hassio/src/addon-view/hassio-addon-info.js @@ -110,7 +110,7 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) { display: block; } .state div { - width: 150px; + width: 180px; display: inline-block; } paper-toggle-button { @@ -311,7 +311,7 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) { id="apparmor" icon="hassio:shield" label="apparmor" - description="[[addon.apparmor]]" + description="" >
@@ -522,6 +531,11 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) { this.set("addon.protected", !this.addon.protected); } + panelToggled() { + const data = { ingress_panel: !this.addon.ingress_panel }; + this.hass.callApi("POST", `hassio/addons/${this.addonSlug}/options`, data); + } + showMoreInfo(e) { const id = e.target.getAttribute("id"); showHassioMarkdownDialog(this, { diff --git a/setup.py b/setup.py index 21bae2f874..bbb3e0c5cd 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20190419.0", + version="20190424.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/home-assistant-polymer", author="The Home Assistant Authors", diff --git a/src/components/entity/ha-entity-toggle.ts b/src/components/entity/ha-entity-toggle.ts index 2b0c40c3c5..2818cb5ac0 100644 --- a/src/components/entity/ha-entity-toggle.ts +++ b/src/components/entity/ha-entity-toggle.ts @@ -14,6 +14,7 @@ import { } from "lit-element"; import { HomeAssistant } from "../../types"; import { HassEntity } from "home-assistant-js-websocket"; +import { forwardHaptic } from "../../util/haptics"; const isOn = (stateObj?: HassEntity) => stateObj !== undefined && !STATES_OFF.includes(stateObj.state); @@ -89,6 +90,7 @@ class HaEntityToggle extends LitElement { if (!this.hass || !this.stateObj) { return; } + forwardHaptic(this, "light"); const stateDomain = computeStateDomain(this.stateObj); let serviceDomain; let service; diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 50428786ad..480e2a27eb 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -18,6 +18,10 @@ import isComponentLoaded from "../common/config/is_component_loaded"; import { HomeAssistant, PanelInfo } from "../types"; import { fireEvent } from "../common/dom/fire_event"; import { DEFAULT_PANEL } from "../common/const"; +import { + getExternalConfig, + ExternalConfig, +} from "../external_app/external_config"; const computeUrl = (urlPath) => `/${urlPath}`; @@ -69,6 +73,7 @@ class HaSidebar extends LitElement { @property() public hass?: HomeAssistant; @property() public _defaultPage?: string = localStorage.defaultPage || DEFAULT_PANEL; + @property() private _externalConfig?: ExternalConfig; protected render() { const hass = this.hass; @@ -117,6 +122,27 @@ class HaSidebar extends LitElement { ` )} + ${this._externalConfig && this._externalConfig.hasSettingsScreen + ? html` + + + + ${hass.localize( + "ui.sidebar.external_app_configuration" + )} + + + ` + : ""} ${!hass.user ? html` @@ -193,6 +219,9 @@ class HaSidebar extends LitElement { } protected shouldUpdate(changedProps: PropertyValues): boolean { + if (changedProps.has("_externalConfig")) { + return true; + } if (!this.hass || !changedProps.has("hass")) { return false; } @@ -210,10 +239,26 @@ class HaSidebar extends LitElement { ); } + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + if (this.hass && this.hass.auth.external) { + getExternalConfig(this.hass.auth.external).then((conf) => { + this._externalConfig = conf; + }); + } + } + private _handleLogOut() { fireEvent(this, "hass-logout"); } + private _handleExternalAppConfiguration(ev: Event) { + ev.preventDefault(); + this.hass!.auth.external!.fireMessage({ + type: "config_screen/show", + }); + } + static get styles(): CSSResult { return css` :host { @@ -259,7 +304,7 @@ class HaSidebar extends LitElement { --paper-item-min-height: 40px; } - a ha-icon { + ha-icon[slot="item-icon"] { color: var(--sidebar-icon-color); } diff --git a/src/entrypoints/core.ts b/src/entrypoints/core.ts index b4deef57d1..92a535d078 100644 --- a/src/entrypoints/core.ts +++ b/src/entrypoints/core.ts @@ -26,7 +26,7 @@ const isExternal = location.search.includes("external_auth=1"); const authProm = isExternal ? () => - import(/* webpackChunkName: "external_auth" */ "../common/auth/external_auth").then( + import(/* webpackChunkName: "external_auth" */ "../external_app/external_auth").then( (mod) => new mod.default(hassUrl) ) : () => diff --git a/src/common/auth/external_auth.ts b/src/external_app/external_auth.ts similarity index 85% rename from src/common/auth/external_auth.ts rename to src/external_app/external_auth.ts index cbb4e573aa..e463f26e68 100644 --- a/src/common/auth/external_auth.ts +++ b/src/external_app/external_auth.ts @@ -2,6 +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"; const CALLBACK_SET_TOKEN = "externalAuthSetToken"; const CALLBACK_REVOKE_TOKEN = "externalAuthRevokeToken"; @@ -20,6 +21,7 @@ declare global { externalApp?: { getExternalAuth(payload: string); revokeExternalAuth(payload: string); + externalBus(payload: string); }; webkit?: { messageHandlers: { @@ -29,6 +31,9 @@ declare global { revokeExternalAuth: { postMessage(payload: BasePayload); }; + externalBus: { + postMessage(payload: InternalMessage); + }; }; }; } @@ -41,6 +46,8 @@ if (!window.externalApp && !window.webkit) { } export default class ExternalAuth extends Auth { + public external = new ExternalMessaging(); + constructor(hassUrl) { super({ hassUrl, @@ -51,19 +58,10 @@ export default class ExternalAuth extends Auth { // This will trigger connection to do a refresh right away expires: 0, }); + this.external.attach(); } public async refreshAccessToken() { - const responseProm = new Promise( - (resolve, reject) => { - window[CALLBACK_SET_TOKEN] = (success, data) => - success ? resolve(data) : reject(data); - } - ); - - // Allow promise to set resolve on window object. - await 0; - const callbackPayload = { callback: CALLBACK_SET_TOKEN }; if (window.externalApp) { @@ -74,21 +72,18 @@ export default class ExternalAuth extends Auth { ); } - const tokens = await responseProm; + const tokens = await new Promise( + (resolve, reject) => { + window[CALLBACK_SET_TOKEN] = (success, data) => + success ? resolve(data) : reject(data); + } + ); this.data.access_token = tokens.access_token; this.data.expires = tokens.expires_in * 1000 + Date.now(); } public async revoke() { - const responseProm = new Promise((resolve, reject) => { - window[CALLBACK_REVOKE_TOKEN] = (success, data) => - success ? resolve(data) : reject(data); - }); - - // Allow promise to set resolve on window object. - await 0; - const callbackPayload = { callback: CALLBACK_REVOKE_TOKEN }; if (window.externalApp) { @@ -99,6 +94,9 @@ export default class ExternalAuth extends Auth { ); } - await responseProm; + await new Promise((resolve, reject) => { + window[CALLBACK_REVOKE_TOKEN] = (success, data) => + success ? resolve(data) : reject(data); + }); } } diff --git a/src/external_app/external_config.ts b/src/external_app/external_config.ts new file mode 100644 index 0000000000..7651b1307b --- /dev/null +++ b/src/external_app/external_config.ts @@ -0,0 +1,16 @@ +import { ExternalMessaging } from "./external_messaging"; + +export interface ExternalConfig { + hasSettingsScreen: boolean; +} + +export const getExternalConfig = ( + bus: ExternalMessaging +): Promise => { + if (!bus.cache.cfg) { + bus.cache.cfg = bus.sendMessage({ + type: "config/get", + }); + } + return bus.cache.cfg; +}; diff --git a/src/external_app/external_events_forwarder.ts b/src/external_app/external_events_forwarder.ts new file mode 100644 index 0000000000..9f146cfebc --- /dev/null +++ b/src/external_app/external_events_forwarder.ts @@ -0,0 +1,15 @@ +import { ExternalMessaging } from "./external_messaging"; + +export const externalForwardConnectionEvents = (bus: ExternalMessaging) => { + document.addEventListener("connection-status", (ev) => + bus.fireMessage({ + type: "connection-status", + payload: { event: ev.detail }, + }) + ); +}; + +export const externalForwardHaptics = (bus: ExternalMessaging) => + document.addEventListener("haptic", (ev) => + bus.fireMessage({ type: "haptic", payload: { hapticType: ev.detail } }) + ); diff --git a/src/external_app/external_messaging.ts b/src/external_app/external_messaging.ts new file mode 100644 index 0000000000..7d2cbc1b34 --- /dev/null +++ b/src/external_app/external_messaging.ts @@ -0,0 +1,111 @@ +import { + externalForwardConnectionEvents, + externalForwardHaptics, +} from "./external_events_forwarder"; + +const CALLBACK_EXTERNAL_BUS = "externalBus"; + +interface CommandInFlight { + resolve: (data: any) => void; + reject: (err: ExternalError) => void; +} + +export interface InternalMessage { + id?: number; + type: string; + payload?: unknown; +} + +interface ExternalError { + code: string; + message: string; +} + +interface ExternalMessageResult { + id: number; + type: "result"; + success: true; + result: unknown; +} + +interface ExternalMessageResultError { + id: number; + type: "result"; + success: false; + error: ExternalError; +} + +type ExternalMessage = ExternalMessageResult | ExternalMessageResultError; + +export class ExternalMessaging { + public commands: { [msgId: number]: CommandInFlight } = {}; + public cache: { [key: string]: any } = {}; + public msgId = 0; + + public attach() { + externalForwardConnectionEvents(this); + externalForwardHaptics(this); + window[CALLBACK_EXTERNAL_BUS] = (msg) => this.receiveMessage(msg); + } + + /** + * Send message to external app that expects a response. + * @param msg message to send + */ + public sendMessage(msg: InternalMessage): Promise { + const msgId = ++this.msgId; + msg.id = msgId; + + this.fireMessage(msg); + + return new Promise((resolve, reject) => { + this.commands[msgId] = { resolve, reject }; + }); + } + + /** + * Send message to external app without expecting a response. + * @param msg message to send + */ + public fireMessage(msg: InternalMessage) { + if (!msg.id) { + msg.id = ++this.msgId; + } + this._sendExternal(msg); + } + + public receiveMessage(msg: ExternalMessage) { + if (__DEV__) { + // tslint:disable-next-line: no-console + console.log("Receiving message from external app", msg); + } + + const pendingCmd = this.commands[msg.id]; + + if (!pendingCmd) { + // tslint: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: InternalMessage) { + if (__DEV__) { + // tslint: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); + } + } +} diff --git a/src/layouts/app/connection-mixin.js b/src/layouts/app/connection-mixin.js index 6eb72d47b4..a3f5fbe993 100644 --- a/src/layouts/app/connection-mixin.js +++ b/src/layouts/app/connection-mixin.js @@ -16,6 +16,8 @@ import { getLocalLanguage } from "../../util/hass-translation"; import { fetchWithAuth } from "../../util/fetch-with-auth"; import hassCallApi from "../../util/hass-call-api"; import { subscribePanels } from "../../data/ws-panels"; +import { forwardHaptic } from "../../util/haptics"; +import { fireEvent } from "../../common/dom/fire_event"; export default (superClass) => class extends EventsMixin(LocalizeMixin(superClass)) { @@ -75,6 +77,7 @@ export default (superClass) => err ); } + forwardHaptic(this, "error"); const message = this.hass.localize( "ui.notification_toast.service_call_failed", @@ -126,11 +129,16 @@ export default (superClass) => const conn = this.hass.connection; + fireEvent(document, "connection-status", "connected"); + conn.addEventListener("ready", () => this.hassReconnected()); conn.addEventListener("disconnected", () => this.hassDisconnected()); // If we reconnect after losing connection and auth is no longer valid. conn.addEventListener("reconnect-error", (_conn, err) => { - if (err === ERR_INVALID_AUTH) location.reload(); + if (err === ERR_INVALID_AUTH) { + fireEvent(document, "connection-status", "auth-invalid"); + location.reload(); + } }); subscribeEntities(conn, (states) => this._updateHass({ states })); @@ -142,10 +150,12 @@ export default (superClass) => hassReconnected() { super.hassReconnected(); this._updateHass({ connected: true }); + fireEvent(document, "connection-status", "connected"); } hassDisconnected() { super.hassDisconnected(); this._updateHass({ connected: false }); + fireEvent(document, "connection-status", "disconnected"); } }; diff --git a/src/panels/config/integrations/ha-config-entries-dashboard.js b/src/panels/config/integrations/ha-config-entries-dashboard.js index 0f46a2eeca..c2720962df 100644 --- a/src/panels/config/integrations/ha-config-entries-dashboard.js +++ b/src/panels/config/integrations/ha-config-entries-dashboard.js @@ -64,7 +64,7 @@ class HaConfigManagerDashboard extends LocalizeMixin(