Lazy load more info content, split logbook and history (#6936)

This commit is contained in:
Bram Kragten 2020-09-12 19:39:54 +02:00 committed by GitHub
parent 8a998369d6
commit 99d0a0a6fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 391 additions and 190 deletions

View File

@ -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() {

View File

@ -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);

View File

@ -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", {

View File

@ -44,7 +44,6 @@ export const DOMAINS_WITH_MORE_INFO = [
"script",
"sun",
"timer",
"updater",
"vacuum",
"water_heater",
"weather",

View File

@ -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 = <T extends Function>(
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);
}
};
};

View File

@ -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 {
`
: ""}
</ha-header-bar>
${CONTROL_DOMAINS.includes(domain) &&
${DOMAINS_WITH_MORE_INFO.includes(domain) &&
this._computeShowHistoryComponent(entityId)
? html`
<mwc-tab-bar
@ -171,17 +212,23 @@ export class MoreInfoDialog extends LitElement {
.hass=${this.hass}
></state-card-content>
`}
<more-info-content
.stateObj=${stateObj}
.hass=${this.hass}
></more-info-content>
${CONTROL_DOMAINS.includes(domain) ||
${DOMAINS_WITH_MORE_INFO.includes(domain) ||
!this._computeShowHistoryComponent(entityId)
? ""
: html`<ha-more-info-history
.hass=${this.hass}
.entityId=${this._entityId}
></ha-more-info-history>`}
.hass=${this.hass}
.entityId=${this._entityId}
></ha-more-info-history>
<ha-more-info-logbook
.hass=${this.hass}
.entityId=${this._entityId}
></ha-more-info-logbook>`}
${this._moreInfoType
? dynamicElement(this._moreInfoType, {
hass: this.hass,
stateObj,
})
: ""}
${stateObj.attributes.restored
? html`
<p>
@ -210,6 +257,10 @@ export class MoreInfoDialog extends LitElement {
.hass=${this.hass}
.entityId=${this._entityId}
></ha-more-info-history>
<ha-more-info-logbook
.hass=${this.hass}
.entityId=${this._entityId}
></ha-more-info-logbook>
`
)}
</div>

View File

@ -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`<state-history-charts
@ -50,54 +40,35 @@ export class MoreInfoHistory extends LitElement {
.historyData=${this._stateHistory}
.isLoadingData=${!this._stateHistory}
></state-history-charts>`
: ""}
${isComponentLoaded(this.hass, "logbook")
? !this._entries
? html`
<ha-circular-progress
active
alt=${this.hass.localize("ui.common.loading")}
></ha-circular-progress>
`
: this._entries.length
? html`
<ha-logbook
class="ha-scrollbar"
narrow
no-icon
no-name
.hass=${this.hass}
.entries=${this._entries}
.userIdToName=${this._persons}
></ha-logbook>
`
: html`<div class="no-entries">
${this.hass.localize("ui.components.logbook.entries_not_found")}
</div>`
: ""} `;
}
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;
}
`,
];
}

View File

@ -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`
<ha-circular-progress
active
alt=${this.hass.localize("ui.common.loading")}
></ha-circular-progress>
`
: this._logbookEntries.length
? html`
<ha-logbook
class="ha-scrollbar"
narrow
no-icon
no-name
.hass=${this.hass}
.entries=${this._logbookEntries}
.userIdToName=${this._persons}
></ha-logbook>
`
: html`<div class="no-entries">
${this.hass.localize("ui.components.logbook.entries_not_found")}
</div>`
: ""}
`;
}
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;
}
}

View File

@ -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);