mirror of
https://github.com/home-assistant/frontend.git
synced 2026-02-14 23:18:21 +00:00
* Update ts-eslint * Remove comments * Remove unused ts-ignore * Add undefined generic type instead of unknown * Remove unused undefined type * Fix type issues * Use undefined instead of void for subscribed return type
322 lines
9.0 KiB
TypeScript
322 lines
9.0 KiB
TypeScript
import type { PropertyValues } from "lit";
|
|
import { html } 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 type { WindowWithPreloads } from "../data/preloads";
|
|
import type { RecorderInfo } from "../data/recorder";
|
|
import { getRecorderInfo } from "../data/recorder";
|
|
import "../resources/custom-card-support";
|
|
import { HassElement } from "../state/hass-element";
|
|
import QuickBarMixin from "../state/quick-bar-mixin";
|
|
import type { HomeAssistant, Route } from "../types";
|
|
import { storeState } from "../util/ha-pref-storage";
|
|
import {
|
|
removeLaunchScreen,
|
|
renderLaunchScreenInfoBox,
|
|
} 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;
|
|
|
|
@state() private _databaseMigration?: boolean;
|
|
|
|
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>
|
|
`;
|
|
}
|
|
|
|
protected willUpdate(changedProps: PropertyValues<this>) {
|
|
super.willUpdate(changedProps);
|
|
if (
|
|
this._databaseMigration === undefined &&
|
|
changedProps.has("hass") &&
|
|
this.hass?.config &&
|
|
changedProps.get("hass")?.config !== this.hass?.config
|
|
) {
|
|
this.checkDataBaseMigration();
|
|
}
|
|
}
|
|
|
|
protected update(changedProps: PropertyValues<this>) {
|
|
if (
|
|
this.hass?.states &&
|
|
this.hass.config &&
|
|
this.hass.services &&
|
|
this._databaseMigration === false
|
|
) {
|
|
this.render = this.renderHass;
|
|
this.update = super.update;
|
|
removeLaunchScreen();
|
|
}
|
|
super.update(changedProps);
|
|
}
|
|
|
|
protected firstUpdated(changedProps: PropertyValues<this>) {
|
|
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
|
|
);
|
|
}
|
|
if (changedProps.has("_databaseMigration")) {
|
|
if (this.render !== this.renderHass) {
|
|
this._renderInitInfo(false);
|
|
} else if (this._databaseMigration) {
|
|
// we already removed the launch screen, so we refresh to add it again to show the migration screen
|
|
location.reload();
|
|
}
|
|
}
|
|
}
|
|
|
|
protected hassConnected() {
|
|
super.hassConnected();
|
|
// @ts-ignore
|
|
this._loadHassTranslations(this.hass!.language, "entity_component");
|
|
// @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 checkDataBaseMigration() {
|
|
if (__DEMO__) {
|
|
this._databaseMigration = false;
|
|
return;
|
|
}
|
|
|
|
let recorderInfoProm: Promise<RecorderInfo> | undefined;
|
|
const preloadWindow = window as WindowWithPreloads;
|
|
// On first load, we speed up loading page by having recorderInfoProm ready
|
|
if (preloadWindow.recorderInfoProm) {
|
|
recorderInfoProm = preloadWindow.recorderInfoProm;
|
|
preloadWindow.recorderInfoProm = undefined;
|
|
}
|
|
const info = await (
|
|
recorderInfoProm || getRecorderInfo(this.hass!.connection)
|
|
).catch((err) => {
|
|
// If the command failed with code unknown_command, recorder is not enabled,
|
|
// otherwise re-throw the error
|
|
if (err.code !== "unknown_command") throw err;
|
|
return { migration_in_progress: false, migration_is_live: false };
|
|
});
|
|
this._databaseMigration =
|
|
info.migration_in_progress && !info.migration_is_live;
|
|
if (this._databaseMigration) {
|
|
// check every 5 seconds if the migration is done
|
|
setTimeout(() => this.checkDataBaseMigration(), 5000);
|
|
}
|
|
}
|
|
|
|
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}
|
|
.migration=${this._databaseMigration}
|
|
></ha-init-page>`
|
|
);
|
|
}
|
|
}
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
"home-assistant": HomeAssistantAppEl;
|
|
}
|
|
}
|