ha-frontend/src/layouts/home-assistant.ts

259 lines
6.8 KiB
TypeScript

import { html, PropertyValues } from "lit";
import { customElement, state } from "lit/decorators";
import { isNavigationClick } from "../common/dom/is-navigation-click";
import { navigate } from "../common/navigate";
import { getStorageDefaultPanelUrlPath } from "../data/panel";
import "../resources/custom-card-support";
import { HassElement } from "../state/hass-element";
import QuickBarMixin from "../state/quick-bar-mixin";
import { HomeAssistant, Route } from "../types";
import { storeState } from "../util/ha-pref-storage";
import {
renderLaunchScreenInfoBox,
removeLaunchScreen,
} from "../util/launch-screen";
import {
registerServiceWorker,
supportsServiceWorker,
} from "../util/register-service-worker";
import "./ha-init-page";
import "./home-assistant-main";
const useHash = __DEMO__;
const curPath = () =>
useHash ? location.hash.substring(1) : location.pathname;
const panelUrl = (path: string) => {
const dividerPos = path.indexOf("/", 1);
return dividerPos === -1 ? path.substring(1) : path.substring(1, dividerPos);
};
@customElement("home-assistant")
export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
@state() private _route: Route;
private _panelUrl: string;
private _haVersion?: string;
private _hiddenTimeout?: number;
private _visiblePromiseResolve?: () => void;
constructor() {
super();
const path = curPath();
if (["", "/"].includes(path)) {
navigate(`/${getStorageDefaultPanelUrlPath()}${location.search}`, {
replace: true,
});
}
this._route = {
prefix: "",
path,
};
this._panelUrl = panelUrl(path);
}
protected renderHass() {
return html`
<home-assistant-main
.hass=${this.hass}
.route=${this._route}
></home-assistant-main>
`;
}
update(changedProps) {
if (this.hass?.states && this.hass.config && this.hass.services) {
this.render = this.renderHass;
this.update = super.update;
removeLaunchScreen();
}
super.update(changedProps);
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._initializeHass();
setTimeout(() => registerServiceWorker(this), 1000);
this.addEventListener("hass-suspend-when-hidden", (ev) => {
this._updateHass({ suspendWhenHidden: ev.detail.suspend });
storeState(this.hass!);
});
// Navigation
const updateRoute = (path = curPath()) => {
if (this._route && path === this._route.path) {
return;
}
this._route = {
prefix: "",
path: path,
};
this._panelUrl = panelUrl(path);
this.panelUrlChanged(this._panelUrl!);
this._updateHass({ panelUrl: this._panelUrl });
};
window.addEventListener("location-changed", () => updateRoute());
// Handle history changes
if (useHash) {
window.addEventListener("hashchange", () => updateRoute());
} else {
window.addEventListener("popstate", () => updateRoute());
}
// Handle clicking on links
window.addEventListener("click", (ev) => {
const href = isNavigationClick(ev);
if (href) {
navigate(href);
}
});
// Render launch screen info box (loading data / error message)
// if Home Assistant is not loaded yet.
if (this.render !== this.renderHass) {
this._renderInitInfo(false);
}
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (changedProps.has("hass")) {
this.hassChanged(
this.hass!,
changedProps.get("hass") as HomeAssistant | undefined
);
}
}
protected hassConnected() {
super.hassConnected();
// @ts-ignore
this._loadHassTranslations(this.hass!.language, "state");
// @ts-ignore
this._loadHassTranslations(this.hass!.language, "entity");
document.addEventListener(
"visibilitychange",
() => this._checkVisibility(),
false
);
document.addEventListener("freeze", () => this._suspendApp());
document.addEventListener("resume", () => this._checkVisibility());
}
protected hassReconnected() {
super.hassReconnected();
// If backend has been upgraded, make sure we update frontend
if (this.hass!.connection.haVersion !== this._haVersion) {
if (supportsServiceWorker()) {
navigator.serviceWorker.getRegistration().then((registration) => {
if (registration) {
registration.update();
} else {
// @ts-ignore Firefox supports forceGet
location.reload(true);
}
});
} else {
// @ts-ignore Firefox supports forceGet
location.reload(true);
}
}
}
protected async _initializeHass() {
try {
let result;
if (window.hassConnection) {
result = await window.hassConnection;
} else {
// In the edge case that core.ts loads before app.ts
result = await new Promise((resolve) => {
window.hassConnectionReady = resolve;
});
}
const { auth, conn } = result;
this._haVersion = conn.haVersion;
this.initializeHass(auth, conn);
} catch (err: any) {
this._renderInitInfo(true);
}
}
protected _checkVisibility() {
if (document.hidden) {
// If the document is hidden, we will prevent reconnects until we are visible again
this._onHidden();
} else {
this._onVisible();
}
}
private _onHidden() {
if (this._visiblePromiseResolve) {
return;
}
this.hass!.connection.suspendReconnectUntil(
new Promise((resolve) => {
this._visiblePromiseResolve = resolve;
})
);
if (this.hass!.suspendWhenHidden !== false) {
// We close the connection to Home Assistant after being hidden for 5 minutes
this._hiddenTimeout = window.setTimeout(() => {
this._hiddenTimeout = undefined;
// setTimeout can be delayed in the background and only fire
// when we switch to the tab or app again (Hey Android!)
if (document.hidden) {
this._suspendApp();
}
}, 300000);
}
window.addEventListener("focus", () => this._onVisible(), { once: true });
}
private _suspendApp() {
if (!this.hass!.connection.connected) {
return;
}
window.stop();
this.hass!.connection.suspend();
}
private _onVisible() {
// Clear timer to close the connection
if (this._hiddenTimeout) {
clearTimeout(this._hiddenTimeout);
this._hiddenTimeout = undefined;
}
// Unsuspend the reconnect
if (this._visiblePromiseResolve) {
this._visiblePromiseResolve();
this._visiblePromiseResolve = undefined;
}
}
private _renderInitInfo(error: boolean) {
renderLaunchScreenInfoBox(
html`<ha-init-page .error=${error}></ha-init-page>`
);
}
}
declare global {
interface HTMLElementTagNameMap {
"home-assistant": HomeAssistantAppEl;
}
}