From 99d0a0a6fdd1490aa4db4984339c0052f4ba6e4f Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 12 Sep 2020 19:39:54 +0200 Subject: [PATCH] Lazy load more info content, split logbook and history (#6936) --- gallery/src/components/demo-more-info.js | 2 +- gallery/src/components/more-info-content.ts | 73 ++++++++ gallery/src/demos/demo-more-info-light.ts | 2 +- src/common/const.ts | 1 - src/common/util/throttle.ts | 50 +++++ src/dialogs/more-info/ha-more-info-dialog.ts | 91 ++++++++-- src/dialogs/more-info/ha-more-info-history.ts | 118 +++--------- src/dialogs/more-info/ha-more-info-logbook.ts | 171 ++++++++++++++++++ src/dialogs/more-info/more-info-content.ts | 73 -------- 9 files changed, 391 insertions(+), 190 deletions(-) create mode 100644 gallery/src/components/more-info-content.ts create mode 100644 src/common/util/throttle.ts create mode 100644 src/dialogs/more-info/ha-more-info-logbook.ts delete mode 100644 src/dialogs/more-info/more-info-content.ts diff --git a/gallery/src/components/demo-more-info.js b/gallery/src/components/demo-more-info.js index 9def4e6095..9e4a34023f 100644 --- a/gallery/src/components/demo-more-info.js +++ b/gallery/src/components/demo-more-info.js @@ -2,8 +2,8 @@ import { html } from "@polymer/polymer/lib/utils/html-tag"; /* eslint-plugin-disable lit */ import { PolymerElement } from "@polymer/polymer/polymer-element"; import "../../../src/components/ha-card"; -import "../../../src/dialogs/more-info/more-info-content"; import "../../../src/state-summary/state-card-content"; +import "./more-info-content"; class DemoMoreInfo extends PolymerElement { static get template() { diff --git a/gallery/src/components/more-info-content.ts b/gallery/src/components/more-info-content.ts new file mode 100644 index 0000000000..6d7271155e --- /dev/null +++ b/gallery/src/components/more-info-content.ts @@ -0,0 +1,73 @@ +import { HassEntity } from "home-assistant-js-websocket"; +import { property, PropertyValues, UpdatingElement } from "lit-element"; +import dynamicContentUpdater from "../../../src/common/dom/dynamic_content_updater"; +import { stateMoreInfoType } from "../../../src/common/entity/state_more_info_type"; +import "../../../src/dialogs/more-info/controls/more-info-alarm_control_panel"; +import "../../../src/dialogs/more-info/controls/more-info-automation"; +import "../../../src/dialogs/more-info/controls/more-info-camera"; +import "../../../src/dialogs/more-info/controls/more-info-climate"; +import "../../../src/dialogs/more-info/controls/more-info-configurator"; +import "../../../src/dialogs/more-info/controls/more-info-counter"; +import "../../../src/dialogs/more-info/controls/more-info-cover"; +import "../../../src/dialogs/more-info/controls/more-info-default"; +import "../../../src/dialogs/more-info/controls/more-info-fan"; +import "../../../src/dialogs/more-info/controls/more-info-group"; +import "../../../src/dialogs/more-info/controls/more-info-humidifier"; +import "../../../src/dialogs/more-info/controls/more-info-input_datetime"; +import "../../../src/dialogs/more-info/controls/more-info-light"; +import "../../../src/dialogs/more-info/controls/more-info-lock"; +import "../../../src/dialogs/more-info/controls/more-info-media_player"; +import "../../../src/dialogs/more-info/controls/more-info-person"; +import "../../../src/dialogs/more-info/controls/more-info-script"; +import "../../../src/dialogs/more-info/controls/more-info-sun"; +import "../../../src/dialogs/more-info/controls/more-info-timer"; +import "../../../src/dialogs/more-info/controls/more-info-vacuum"; +import "../../../src/dialogs/more-info/controls/more-info-water_heater"; +import "../../../src/dialogs/more-info/controls/more-info-weather"; +import { HomeAssistant } from "../../../src/types"; + +class MoreInfoContent extends UpdatingElement { + @property({ attribute: false }) public hass?: HomeAssistant; + + @property() public stateObj?: HassEntity; + + private _detachedChild?: ChildNode; + + protected firstUpdated(): void { + this.style.position = "relative"; + this.style.display = "block"; + } + + // This is not a lit element, but an updating element, so we implement update + protected update(changedProps: PropertyValues): void { + super.update(changedProps); + const stateObj = this.stateObj; + const hass = this.hass; + + if (!stateObj || !hass) { + if (this.lastChild) { + this._detachedChild = this.lastChild; + // Detach child to prevent it from doing work. + this.removeChild(this.lastChild); + } + return; + } + + if (this._detachedChild) { + this.appendChild(this._detachedChild); + this._detachedChild = undefined; + } + + const moreInfoType = + stateObj.attributes && "custom_ui_more_info" in stateObj.attributes + ? stateObj.attributes.custom_ui_more_info + : "more-info-" + stateMoreInfoType(stateObj); + + dynamicContentUpdater(this, moreInfoType.toUpperCase(), { + hass, + stateObj, + }); + } +} + +customElements.define("more-info-content", MoreInfoContent); diff --git a/gallery/src/demos/demo-more-info-light.ts b/gallery/src/demos/demo-more-info-light.ts index 19677825d5..8c5ac611e9 100644 --- a/gallery/src/demos/demo-more-info-light.ts +++ b/gallery/src/demos/demo-more-info-light.ts @@ -3,10 +3,10 @@ import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; import "../../../src/components/ha-card"; import { SUPPORT_BRIGHTNESS } from "../../../src/data/light"; -import "../../../src/dialogs/more-info/more-info-content"; import { getEntity } from "../../../src/fake_data/entity"; import { provideHass } from "../../../src/fake_data/provide_hass"; import "../components/demo-more-infos"; +import "../components/more-info-content"; const ENTITIES = [ getEntity("light", "bed_light", "on", { diff --git a/src/common/const.ts b/src/common/const.ts index dc2bf20595..640c4fb13a 100644 --- a/src/common/const.ts +++ b/src/common/const.ts @@ -44,7 +44,6 @@ export const DOMAINS_WITH_MORE_INFO = [ "script", "sun", "timer", - "updater", "vacuum", "water_heater", "weather", diff --git a/src/common/util/throttle.ts b/src/common/util/throttle.ts new file mode 100644 index 0000000000..1cd98ea188 --- /dev/null +++ b/src/common/util/throttle.ts @@ -0,0 +1,50 @@ +// From: underscore.js https://github.com/jashkenas/underscore/blob/master/underscore.js + +// Returns a function, that, when invoked, will only be triggered at most once +// during a given window of time. Normally, the throttled function will run +// as much as it can, without ever going more than once per `wait` duration; +// but if you'd like to disable the execution on the leading edge, pass +// `false for leading`. To disable execution on the trailing edge, ditto. +export const throttle = ( + func: T, + wait: number, + leading = true, + trailing = true +): T => { + let timeout: number | undefined; + let previous = 0; + let context: any; + let args: any; + const later = () => { + previous = leading === false ? 0 : Date.now(); + timeout = undefined; + func.apply(context, args); + if (!timeout) { + context = null; + args = null; + } + }; + // @ts-ignore + return function (...argmnts) { + // @ts-ignore + // @typescript-eslint/no-this-alias + context = this; + args = argmnts; + + const now = Date.now(); + if (!previous && leading === false) { + previous = now; + } + const remaining = wait - (now - previous); + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout); + timeout = undefined; + } + previous = now; + func.apply(context, args); + } else if (!timeout && trailing !== false) { + timeout = window.setTimeout(later, remaining); + } + }; +}; diff --git a/src/dialogs/more-info/ha-more-info-dialog.ts b/src/dialogs/more-info/ha-more-info-dialog.ts index f5beadea90..bb217a732c 100644 --- a/src/dialogs/more-info/ha-more-info-dialog.ts +++ b/src/dialogs/more-info/ha-more-info-dialog.ts @@ -13,10 +13,15 @@ import { } from "lit-element"; import { cache } from "lit-html/directives/cache"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; -import { DOMAINS_MORE_INFO_NO_HISTORY } from "../../common/const"; +import { + DOMAINS_MORE_INFO_NO_HISTORY, + DOMAINS_WITH_MORE_INFO, +} from "../../common/const"; +import { dynamicElement } from "../../common/dom/dynamic-element-directive"; import { fireEvent } from "../../common/dom/fire_event"; import { computeDomain } from "../../common/entity/compute_domain"; import { computeStateName } from "../../common/entity/compute_state_name"; +import { stateMoreInfoType } from "../../common/entity/state_more_info_type"; import { navigate } from "../../common/navigate"; import "../../components/ha-dialog"; import "../../components/ha-header-bar"; @@ -30,21 +35,38 @@ import "../../state-summary/state-card-content"; import { HomeAssistant } from "../../types"; import { showConfirmationDialog } from "../generic/show-dialog-box"; import "./ha-more-info-history"; -import "./more-info-content"; +import "./ha-more-info-logbook"; const DOMAINS_NO_INFO = ["camera", "configurator"]; -const CONTROL_DOMAINS = [ - "light", - "media_player", - "vacuum", - "alarm_control_panel", - "climate", - "humidifier", - "weather", -]; const EDITABLE_DOMAINS_WITH_ID = ["scene", "automation"]; const EDITABLE_DOMAINS = ["script"]; +const MORE_INFO_CONTROL_IMPORT = { + alarm_control_panel: () => import("./controls/more-info-alarm_control_panel"), + automation: () => import("./controls/more-info-automation"), + camera: () => import("./controls/more-info-camera"), + climate: () => import("./controls/more-info-climate"), + configurator: () => import("./controls/more-info-configurator"), + counter: () => import("./controls/more-info-counter"), + cover: () => import("./controls/more-info-cover"), + fan: () => import("./controls/more-info-fan"), + group: () => import("./controls/more-info-group"), + humidifier: () => import("./controls/more-info-humidifier"), + input_datetime: () => import("./controls/more-info-input_datetime"), + light: () => import("./controls/more-info-light"), + lock: () => import("./controls/more-info-lock"), + media_player: () => import("./controls/more-info-media_player"), + person: () => import("./controls/more-info-person"), + script: () => import("./controls/more-info-script"), + sun: () => import("./controls/more-info-sun"), + timer: () => import("./controls/more-info-timer"), + vacuum: () => import("./controls/more-info-vacuum"), + water_heater: () => import("./controls/more-info-water_heater"), + weather: () => import("./controls/more-info-weather"), + hidden: () => {}, + default: () => import("./controls/more-info-default"), +}; + export interface MoreInfoDialogParams { entityId: string | null; } @@ -57,6 +79,8 @@ export class MoreInfoDialog extends LitElement { @internalProperty() private _entityId?: string | null; + @internalProperty() private _moreInfoType?: string; + @internalProperty() private _currTabIndex = 0; public showDialog(params: MoreInfoDialogParams) { @@ -73,6 +97,23 @@ export class MoreInfoDialog extends LitElement { fireEvent(this, "dialog-closed", { dialog: this.localName }); } + protected updated(changedProperties) { + if (!this.hass || !this._entityId || !changedProperties.has("_entityId")) { + return; + } + const stateObj = this.hass.states[this._entityId]; + if (!stateObj) { + return; + } + if (stateObj.attributes && "custom_ui_more_info" in stateObj.attributes) { + this._moreInfoType = stateObj.attributes.custom_ui_more_info; + } else { + const type = stateMoreInfoType(stateObj); + this._moreInfoType = `more-info-${type}`; + MORE_INFO_CONTROL_IMPORT[type](); + } + } + protected render() { if (!this._entityId) { return html``; @@ -137,7 +178,7 @@ export class MoreInfoDialog extends LitElement { ` : ""} - ${CONTROL_DOMAINS.includes(domain) && + ${DOMAINS_WITH_MORE_INFO.includes(domain) && this._computeShowHistoryComponent(entityId) ? html` `} - - ${CONTROL_DOMAINS.includes(domain) || + ${DOMAINS_WITH_MORE_INFO.includes(domain) || !this._computeShowHistoryComponent(entityId) ? "" : html``} + .hass=${this.hass} + .entityId=${this._entityId} + > + `} + ${this._moreInfoType + ? dynamicElement(this._moreInfoType, { + hass: this.hass, + stateObj, + }) + : ""} ${stateObj.attributes.restored ? html`

@@ -210,6 +257,10 @@ export class MoreInfoDialog extends LitElement { .hass=${this.hass} .entityId=${this._entityId} > + ` )} diff --git a/src/dialogs/more-info/ha-more-info-history.ts b/src/dialogs/more-info/ha-more-info-history.ts index 746bbb4c82..1dc70b68d4 100644 --- a/src/dialogs/more-info/ha-more-info-history.ts +++ b/src/dialogs/more-info/ha-more-info-history.ts @@ -9,14 +9,11 @@ import { TemplateResult, } from "lit-element"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; -import { computeStateDomain } from "../../common/entity/compute_state_domain"; -import "../../components/ha-circular-progress"; +import { throttle } from "../../common/util/throttle"; import "../../components/state-history-charts"; import { getRecentWithCache } from "../../data/cached-history"; import { HistoryResult } from "../../data/history"; -import { getLogbookData, LogbookEntry } from "../../data/logbook"; -import "../../panels/logbook/ha-logbook"; -import { haStyle, haStyleScrollbar } from "../../resources/styles"; +import { haStyle } from "../../resources/styles"; import { HomeAssistant } from "../../types"; @customElement("ha-more-info-history") @@ -27,21 +24,14 @@ export class MoreInfoHistory extends LitElement { @internalProperty() private _stateHistory?: HistoryResult; - @internalProperty() private _entries?: LogbookEntry[]; - - @internalProperty() private _persons = {}; - - private _historyRefreshInterval?: number; + private _throttleGetStateHistory = throttle(() => { + this._getStateHistory(); + }, 10000); protected render(): TemplateResult { if (!this.entityId) { return html``; } - const stateObj = this.hass.states[this.entityId]; - - if (!stateObj) { - return html``; - } return html`${isComponentLoaded(this.hass, "history") ? html`` - : ""} - ${isComponentLoaded(this.hass, "logbook") - ? !this._entries - ? html` - - ` - : this._entries.length - ? html` - - ` - : html`

- ${this.hass.localize("ui.components.logbook.entries_not_found")} -
` : ""} `; } - protected firstUpdated(): void { - this._fetchPersonNames(); - } - protected updated(changedProps: PropertyValues): void { super.updated(changedProps); - if (!this.entityId) { - clearInterval(this._historyRefreshInterval); - } if (changedProps.has("entityId")) { this._stateHistory = undefined; - this._entries = undefined; - this._getStateHistory(); - this._getLogBookData(); + if (!this.entityId) { + return; + } - clearInterval(this._historyRefreshInterval); - this._historyRefreshInterval = window.setInterval(() => { - this._getStateHistory(); - }, 60 * 1000); + this._throttleGetStateHistory(); + return; + } + + if (!this.entityId || !changedProps.has("hass")) { + return; + } + + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + + if ( + oldHass && + this.hass.states[this.entityId] !== oldHass?.states[this.entityId] + ) { + // wait for commit of data (we only account for the default setting of 1 sec) + setTimeout(this._throttleGetStateHistory, 1000); } } @@ -118,55 +89,14 @@ export class MoreInfoHistory extends LitElement { ); } - private async _getLogBookData() { - if (!isComponentLoaded(this.hass, "logbook")) { - return; - } - const yesterday = new Date(new Date().getTime() - 24 * 60 * 60 * 1000); - const now = new Date(); - this._entries = await getLogbookData( - this.hass, - yesterday.toISOString(), - now.toISOString(), - this.entityId, - true - ); - } - - private _fetchPersonNames() { - Object.values(this.hass.states).forEach((entity) => { - if ( - entity.attributes.user_id && - computeStateDomain(entity) === "person" - ) { - this._persons[entity.attributes.user_id] = - entity.attributes.friendly_name; - } - }); - } - static get styles() { return [ haStyle, - haStyleScrollbar, css` state-history-charts { display: block; margin-bottom: 16px; } - .no-entries { - text-align: center; - padding: 16px; - color: var(--secondary-text-color); - } - ha-logbook { - max-height: 250px; - overflow: auto; - } - ha-circular-progress { - display: flex; - justify-content: center; - } `, ]; } diff --git a/src/dialogs/more-info/ha-more-info-logbook.ts b/src/dialogs/more-info/ha-more-info-logbook.ts new file mode 100644 index 0000000000..a3258133f2 --- /dev/null +++ b/src/dialogs/more-info/ha-more-info-logbook.ts @@ -0,0 +1,171 @@ +import { + css, + customElement, + html, + internalProperty, + LitElement, + property, + PropertyValues, + TemplateResult, +} from "lit-element"; +import { isComponentLoaded } from "../../common/config/is_component_loaded"; +import { computeStateDomain } from "../../common/entity/compute_state_domain"; +import { throttle } from "../../common/util/throttle"; +import "../../components/ha-circular-progress"; +import "../../components/state-history-charts"; +import { getLogbookData, LogbookEntry } from "../../data/logbook"; +import "../../panels/logbook/ha-logbook"; +import { haStyle, haStyleScrollbar } from "../../resources/styles"; +import { HomeAssistant } from "../../types"; + +@customElement("ha-more-info-logbook") +export class MoreInfoLogbook extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public entityId!: string; + + @internalProperty() private _logbookEntries?: LogbookEntry[]; + + @internalProperty() private _persons = {}; + + private _lastLogbookDate?: Date; + + private _throttleGetLogbookEntries = throttle(() => { + this._getLogBookData(); + }, 10000); + + protected render(): TemplateResult { + if (!this.entityId) { + return html``; + } + const stateObj = this.hass.states[this.entityId]; + + if (!stateObj) { + return html``; + } + + return html` + ${isComponentLoaded(this.hass, "logbook") + ? !this._logbookEntries + ? html` + + ` + : this._logbookEntries.length + ? html` + + ` + : html`
+ ${this.hass.localize("ui.components.logbook.entries_not_found")} +
` + : ""} + `; + } + + protected firstUpdated(): void { + this._fetchPersonNames(); + } + + protected updated(changedProps: PropertyValues): void { + super.updated(changedProps); + + if (changedProps.has("entityId")) { + this._lastLogbookDate = undefined; + this._logbookEntries = undefined; + + if (!this.entityId) { + return; + } + + this._throttleGetLogbookEntries(); + return; + } + + if (!this.entityId || !changedProps.has("hass")) { + return; + } + + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + + if ( + oldHass && + this.hass.states[this.entityId] !== oldHass?.states[this.entityId] + ) { + // wait for commit of data (we only account for the default setting of 1 sec) + setTimeout(this._throttleGetLogbookEntries, 1000); + } + } + + private async _getLogBookData() { + if (!isComponentLoaded(this.hass, "logbook")) { + return; + } + const lastDate = + this._lastLogbookDate || + new Date(new Date().getTime() - 24 * 60 * 60 * 1000); + const now = new Date(); + const newEntries = await getLogbookData( + this.hass, + lastDate.toISOString(), + now.toISOString(), + this.entityId, + true + ); + this._logbookEntries = this._logbookEntries + ? [...newEntries, ...this._logbookEntries] + : newEntries; + this._lastLogbookDate = now; + } + + private _fetchPersonNames() { + Object.values(this.hass.states).forEach((entity) => { + if ( + entity.attributes.user_id && + computeStateDomain(entity) === "person" + ) { + this._persons[entity.attributes.user_id] = + entity.attributes.friendly_name; + } + }); + } + + static get styles() { + return [ + haStyle, + haStyleScrollbar, + css` + .no-entries { + text-align: center; + padding: 16px; + color: var(--secondary-text-color); + } + ha-logbook { + max-height: 250px; + overflow: auto; + display: block; + margin-top: 16px; + } + ha-circular-progress { + display: flex; + justify-content: center; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-more-info-logbook": MoreInfoLogbook; + } +} diff --git a/src/dialogs/more-info/more-info-content.ts b/src/dialogs/more-info/more-info-content.ts deleted file mode 100644 index 369d9e8d4f..0000000000 --- a/src/dialogs/more-info/more-info-content.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { HassEntity } from "home-assistant-js-websocket"; -import { property, PropertyValues, UpdatingElement } from "lit-element"; -import dynamicContentUpdater from "../../common/dom/dynamic_content_updater"; -import { stateMoreInfoType } from "../../common/entity/state_more_info_type"; -import { HomeAssistant } from "../../types"; -import "./controls/more-info-alarm_control_panel"; -import "./controls/more-info-automation"; -import "./controls/more-info-camera"; -import "./controls/more-info-climate"; -import "./controls/more-info-configurator"; -import "./controls/more-info-counter"; -import "./controls/more-info-cover"; -import "./controls/more-info-default"; -import "./controls/more-info-fan"; -import "./controls/more-info-group"; -import "./controls/more-info-humidifier"; -import "./controls/more-info-input_datetime"; -import "./controls/more-info-light"; -import "./controls/more-info-lock"; -import "./controls/more-info-media_player"; -import "./controls/more-info-person"; -import "./controls/more-info-script"; -import "./controls/more-info-sun"; -import "./controls/more-info-timer"; -import "./controls/more-info-vacuum"; -import "./controls/more-info-water_heater"; -import "./controls/more-info-weather"; - -class MoreInfoContent extends UpdatingElement { - @property({ attribute: false }) public hass?: HomeAssistant; - - @property() public stateObj?: HassEntity; - - private _detachedChild?: ChildNode; - - protected firstUpdated(): void { - this.style.position = "relative"; - this.style.display = "block"; - } - - // This is not a lit element, but an updating element, so we implement update - protected update(changedProps: PropertyValues): void { - super.update(changedProps); - const stateObj = this.stateObj; - const hass = this.hass; - - if (!stateObj || !hass) { - if (this.lastChild) { - this._detachedChild = this.lastChild; - // Detach child to prevent it from doing work. - this.removeChild(this.lastChild); - } - return; - } - - if (this._detachedChild) { - this.appendChild(this._detachedChild); - this._detachedChild = undefined; - } - - const moreInfoType = - stateObj.attributes && "custom_ui_more_info" in stateObj.attributes - ? stateObj.attributes.custom_ui_more_info - : "more-info-" + stateMoreInfoType(stateObj); - - dynamicContentUpdater(this, moreInfoType.toUpperCase(), { - hass, - stateObj, - }); - } -} - -customElements.define("more-info-content", MoreInfoContent);