From 90c234ffad32ecf8cf0d45a1d57547255099a4da Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 17 May 2022 08:53:22 -0700 Subject: [PATCH] Refactor logbook data fetch logic into reusable class (#12701) --- src/common/util/throttle.ts | 8 +- src/components/entity/ha-entity-picker.ts | 2 +- src/components/trace/ha-trace-logbook.ts | 6 +- src/components/trace/ha-trace-path-details.ts | 6 +- src/dialogs/more-info/ha-more-info-logbook.ts | 194 +---- src/panels/logbook/ha-logbook-renderer.ts | 532 ++++++++++++++ src/panels/logbook/ha-logbook.ts | 664 ++++++------------ src/panels/logbook/ha-panel-logbook.ts | 261 +++---- src/panels/lovelace/cards/hui-logbook-card.ts | 214 +----- 9 files changed, 883 insertions(+), 1004 deletions(-) create mode 100644 src/panels/logbook/ha-logbook-renderer.ts diff --git a/src/common/util/throttle.ts b/src/common/util/throttle.ts index 2860f66be5..02327a5789 100644 --- a/src/common/util/throttle.ts +++ b/src/common/util/throttle.ts @@ -13,7 +13,7 @@ export const throttle = ( ) => { let timeout: number | undefined; let previous = 0; - return (...args: T): void => { + const throttledFunc = (...args: T): void => { const later = () => { previous = leading === false ? 0 : Date.now(); timeout = undefined; @@ -35,4 +35,10 @@ export const throttle = ( timeout = window.setTimeout(later, remaining); } }; + throttledFunc.cancel = () => { + clearTimeout(timeout); + timeout = undefined; + previous = 0; + }; + return throttledFunc; }; diff --git a/src/components/entity/ha-entity-picker.ts b/src/components/entity/ha-entity-picker.ts index 7f9c8d3d80..3b6c3e6304 100644 --- a/src/components/entity/ha-entity-picker.ts +++ b/src/components/entity/ha-entity-picker.ts @@ -20,7 +20,7 @@ interface HassEntityWithCachedName extends HassEntity { friendly_name: string; } -export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; +export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean; // eslint-disable-next-line lit/prefer-static-styles const rowRenderer: ComboBoxLitRenderer = (item) => diff --git a/src/components/trace/ha-trace-logbook.ts b/src/components/trace/ha-trace-logbook.ts index 2f4da4de66..7b0118b3cf 100644 --- a/src/components/trace/ha-trace-logbook.ts +++ b/src/components/trace/ha-trace-logbook.ts @@ -3,7 +3,7 @@ import { customElement, property } from "lit/decorators"; import { LogbookEntry } from "../../data/logbook"; import { HomeAssistant } from "../../types"; import "./hat-logbook-note"; -import "../../panels/logbook/ha-logbook"; +import "../../panels/logbook/ha-logbook-renderer"; import { TraceExtended } from "../../data/trace"; @customElement("ha-trace-logbook") @@ -19,12 +19,12 @@ export class HaTraceLogbook extends LitElement { protected render(): TemplateResult { return this.logbookEntries.length ? html` - + > ` : html`
diff --git a/src/components/trace/ha-trace-path-details.ts b/src/components/trace/ha-trace-path-details.ts index ebf77a2ab6..dcf97366bd 100644 --- a/src/components/trace/ha-trace-path-details.ts +++ b/src/components/trace/ha-trace-path-details.ts @@ -13,7 +13,7 @@ import { getDataFromPath, TraceExtended, } from "../../data/trace"; -import "../../panels/logbook/ha-logbook"; +import "../../panels/logbook/ha-logbook-renderer"; import { traceTabStyles } from "./trace-tab-styles"; import { HomeAssistant } from "../../types"; import type { NodeInfo } from "./hat-script-graph"; @@ -224,12 +224,12 @@ export class HaTracePathDetails extends LitElement { return entries.length ? html` - + > ` : html`
diff --git a/src/dialogs/more-info/ha-more-info-logbook.ts b/src/dialogs/more-info/ha-more-info-logbook.ts index e577ff7cb6..15712055c0 100644 --- a/src/dialogs/more-info/ha-more-info-logbook.ts +++ b/src/dialogs/more-info/ha-more-info-logbook.ts @@ -1,17 +1,10 @@ import { startOfYesterday } from "date-fns"; import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { customElement, property } from "lit/decorators"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { fireEvent } from "../../common/dom/fire_event"; -import { computeStateDomain } from "../../common/entity/compute_state_domain"; -import { throttle } from "../../common/util/throttle"; -import "../../components/ha-circular-progress"; -import { getLogbookData, LogbookEntry } from "../../data/logbook"; -import { loadTraceContexts, TraceContexts } from "../../data/trace"; -import { fetchUsers } from "../../data/user"; import "../../panels/logbook/ha-logbook"; -import { haStyle } from "../../resources/styles"; -import { HomeAssistant } from "../../types"; +import type { HomeAssistant } from "../../types"; @customElement("ha-more-info-logbook") export class MoreInfoLogbook extends LitElement { @@ -19,26 +12,12 @@ export class MoreInfoLogbook extends LitElement { @property() public entityId!: string; - @state() private _logbookEntries?: LogbookEntry[]; - - @state() private _traceContexts?: TraceContexts; - - @state() private _userIdToName = {}; - - private _lastLogbookDate?: Date; - - private _fetchUserPromise?: Promise; - - private _error?: string; - private _showMoreHref = ""; - private _throttleGetLogbookEntries = throttle(() => { - this._getLogBookData(); - }, 10000); + private _time = { recent: 86400 }; protected render(): TemplateResult { - if (!this.entityId) { + if (!isComponentLoaded(this.hass, "logbook") || !this.entityId) { return html``; } const stateObj = this.hass.states[this.entityId]; @@ -48,149 +27,34 @@ export class MoreInfoLogbook extends LitElement { } return html` - ${isComponentLoaded(this.hass, "logbook") - ? this._error - ? html`
- ${`${this.hass.localize( - "ui.components.logbook.retrieval_error" - )}: ${this._error}`} -
` - : !this._logbookEntries - ? html` - - ` - : this._logbookEntries.length - ? html` -
-
- ${this.hass.localize("ui.dialogs.more_info_control.logbook")} -
- ${this.hass.localize( - "ui.dialogs.more_info_control.show_more" - )} -
- - ` - : html`
- ${this.hass.localize("ui.components.logbook.entries_not_found")} -
` - : ""} +
+
+ ${this.hass.localize("ui.dialogs.more_info_control.logbook")} +
+ ${this.hass.localize("ui.dialogs.more_info_control.show_more")} +
+ `; } - protected firstUpdated(): void { - this._fetchUserPromise = this._fetchUserNames(); - } - - protected updated(changedProps: PropertyValues): void { - super.updated(changedProps); - - if (changedProps.has("entityId")) { - this._lastLogbookDate = undefined; - this._logbookEntries = undefined; - - if (!this.entityId) { - return; - } + protected willUpdate(changedProps: PropertyValues): void { + super.willUpdate(changedProps); + if (changedProps.has("entityId") && this.entityId) { this._showMoreHref = `/logbook?entity_id=${ this.entityId }&start_date=${startOfYesterday().toISOString()}`; - - 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(); - let newEntries; - let traceContexts; - - try { - [newEntries, traceContexts] = await Promise.all([ - getLogbookData( - this.hass, - lastDate.toISOString(), - now.toISOString(), - this.entityId - ), - this.hass.user?.is_admin ? loadTraceContexts(this.hass) : {}, - this._fetchUserPromise, - ]); - } catch (err: any) { - this._error = err.message; - } - - this._logbookEntries = this._logbookEntries - ? [...newEntries, ...this._logbookEntries] - : newEntries; - this._lastLogbookDate = now; - this._traceContexts = traceContexts; - } - - private async _fetchUserNames() { - const userIdToName = {}; - - // Start loading users - const userProm = this.hass.user?.is_admin && fetchUsers(this.hass); - - // Process persons - Object.values(this.hass.states).forEach((entity) => { - if ( - entity.attributes.user_id && - computeStateDomain(entity) === "person" - ) { - this._userIdToName[entity.attributes.user_id] = - entity.attributes.friendly_name; - } - }); - - // Process users - if (userProm) { - const users = await userProm; - for (const user of users) { - if (!(user.id in userIdToName)) { - userIdToName[user.id] = user.name; - } - } - } - - this._userIdToName = userIdToName; } private _close(): void { @@ -199,13 +63,7 @@ export class MoreInfoLogbook extends LitElement { static get styles() { return [ - haStyle, css` - .no-entries { - text-align: center; - padding: 16px; - color: var(--secondary-text-color); - } ha-logbook { --logbook-max-height: 250px; } @@ -214,10 +72,6 @@ export class MoreInfoLogbook extends LitElement { --logbook-max-height: unset; } } - ha-circular-progress { - display: flex; - justify-content: center; - } .header { display: flex; flex-direction: row; diff --git a/src/panels/logbook/ha-logbook-renderer.ts b/src/panels/logbook/ha-logbook-renderer.ts new file mode 100644 index 0000000000..4514499dac --- /dev/null +++ b/src/panels/logbook/ha-logbook-renderer.ts @@ -0,0 +1,532 @@ +import "@lit-labs/virtualizer"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; +import { customElement, eventOptions, property } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; +import { DOMAINS_WITH_DYNAMIC_PICTURE } from "../../common/const"; +import { formatDate } from "../../common/datetime/format_date"; +import { formatTimeWithSeconds } from "../../common/datetime/format_time"; +import { restoreScroll } from "../../common/decorators/restore-scroll"; +import { fireEvent } from "../../common/dom/fire_event"; +import { computeDomain } from "../../common/entity/compute_domain"; +import { domainIcon } from "../../common/entity/domain_icon"; +import { computeRTL, emitRTLDirection } from "../../common/util/compute_rtl"; +import "../../components/entity/state-badge"; +import "../../components/ha-circular-progress"; +import "../../components/ha-relative-time"; +import { LogbookEntry } from "../../data/logbook"; +import { TraceContexts } from "../../data/trace"; +import { + haStyle, + haStyleScrollbar, + buttonLinkStyle, +} from "../../resources/styles"; +import { HomeAssistant } from "../../types"; + +const EVENT_LOCALIZE_MAP = { + script_started: "from_script", +}; + +@customElement("ha-logbook-renderer") +class HaLogbookRenderer extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public userIdToName = {}; + + @property({ attribute: false }) + public traceContexts: TraceContexts = {}; + + @property({ attribute: false }) public entries: LogbookEntry[] = []; + + @property({ type: Boolean, attribute: "narrow" }) + public narrow = false; + + @property({ attribute: "rtl", type: Boolean }) + private _rtl = false; + + @property({ type: Boolean, attribute: "virtualize", reflect: true }) + public virtualize = false; + + @property({ type: Boolean, attribute: "no-icon" }) + public noIcon = false; + + @property({ type: Boolean, attribute: "no-name" }) + public noName = false; + + @property({ type: Boolean, attribute: "relative-time" }) + public relativeTime = false; + + // @ts-ignore + @restoreScroll(".container") private _savedScrollPos?: number; + + protected shouldUpdate(changedProps: PropertyValues) { + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + const languageChanged = + oldHass === undefined || oldHass.locale !== this.hass.locale; + + return ( + changedProps.has("entries") || + changedProps.has("traceContexts") || + languageChanged + ); + } + + protected updated(_changedProps: PropertyValues) { + const oldHass = _changedProps.get("hass") as HomeAssistant | undefined; + + if (oldHass === undefined || oldHass.language !== this.hass.language) { + this._rtl = computeRTL(this.hass); + } + } + + protected render(): TemplateResult { + if (!this.entries?.length) { + return html` +
+ ${this.hass.localize("ui.components.logbook.entries_not_found")} +
+ `; + } + + return html` +
+ ${this.virtualize + ? html` + ` + : this.entries.map((item, index) => + this._renderLogbookItem(item, index) + )} +
+ `; + } + + private _renderLogbookItem = ( + item: LogbookEntry, + index: number + ): TemplateResult => { + if (!item || index === undefined) { + return html``; + } + + const seenEntityIds: string[] = []; + const previous = this.entries[index - 1]; + const stateObj = item.entity_id + ? this.hass.states[item.entity_id] + : undefined; + const item_username = + item.context_user_id && this.userIdToName[item.context_user_id]; + const domain = item.entity_id + ? computeDomain(item.entity_id) + : // Domain is there if there is no entity ID. + item.domain!; + + return html` +
+ ${index === 0 || + (item?.when && + previous?.when && + new Date(item.when * 1000).toDateString() !== + new Date(previous.when * 1000).toDateString()) + ? html` +

+ ${formatDate(new Date(item.when * 1000), this.hass.locale)} +

+ ` + : html``} + +
+
+ ${!this.noIcon + ? // We do not want to use dynamic entity pictures (e.g., from media player) for the log book rendering, + // as they would present a false state in the log (played media right now vs actual historic data). + html` + + ` + : ""} +
+
+ ${!this.noName // Used for more-info panel (single entity case) + ? this._renderEntity(item.entity_id, item.name) + : ""} + ${item.message + ? html`${this._formatMessageWithPossibleEntity( + item.message, + seenEntityIds, + item.entity_id + )}` + : item.source + ? html` ${this._formatMessageWithPossibleEntity( + item.source, + seenEntityIds, + undefined, + "ui.components.logbook.by" + )}` + : ""} + ${item_username + ? ` ${this.hass.localize( + "ui.components.logbook.by_user" + )} ${item_username}` + : ``} + ${item.context_event_type + ? this._formatEventBy(item, seenEntityIds) + : ""} + ${item.context_message + ? html` ${this._formatMessageWithPossibleEntity( + item.context_message, + seenEntityIds, + item.context_entity_id, + "ui.components.logbook.for" + )}` + : ""} + ${item.context_entity_id && + !seenEntityIds.includes(item.context_entity_id) + ? // Another entity such as an automation or script + html` ${this.hass.localize("ui.components.logbook.for")} + ${this._renderEntity( + item.context_entity_id, + item.context_entity_id_name + )}` + : ""} +
+
+ ${formatTimeWithSeconds( + new Date(item.when * 1000), + this.hass.locale + )} + - + + ${["script", "automation"].includes(item.domain!) && + item.context_id! in this.traceContexts + ? html` + - + ${this.hass.localize( + "ui.components.logbook.show_trace" + )} + ` + : ""} +
+
+
+
+
+ `; + }; + + @eventOptions({ passive: true }) + private _saveScrollPos(e: Event) { + this._savedScrollPos = (e.target as HTMLDivElement).scrollTop; + } + + private _formatEventBy(item: LogbookEntry, seenEntities: string[]) { + if (item.context_event_type === "call_service") { + return `${this.hass.localize("ui.components.logbook.from_service")} ${ + item.context_domain + }.${item.context_service}`; + } + if (item.context_event_type === "automation_triggered") { + if (seenEntities.includes(item.context_entity_id!)) { + return ""; + } + seenEntities.push(item.context_entity_id!); + return html`${this.hass.localize("ui.components.logbook.from_automation")} + ${this._renderEntity(item.context_entity_id, item.context_name)}`; + } + if (item.context_name) { + return `${this.hass.localize("ui.components.logbook.from")} ${ + item.context_name + }`; + } + if (item.context_event_type === "state_changed") { + return ""; + } + if (item.context_event_type! in EVENT_LOCALIZE_MAP) { + return `${this.hass.localize( + `ui.components.logbook.${EVENT_LOCALIZE_MAP[item.context_event_type!]}` + )}`; + } + return `${this.hass.localize( + "ui.components.logbook.from" + )} ${this.hass.localize("ui.components.logbook.event")} ${ + item.context_event_type + }`; + } + + private _renderEntity( + entityId: string | undefined, + entityName: string | undefined + ) { + const hasState = entityId && entityId in this.hass.states; + const displayName = + entityName || + (hasState + ? this.hass.states[entityId].attributes.friendly_name || entityId + : entityId); + if (!hasState) { + return displayName; + } + return html``; + } + + private _formatMessageWithPossibleEntity( + message: string, + seenEntities: string[], + possibleEntity?: string, + localizePrefix?: string + ) { + // + // As we are looking at a log(book), we are doing entity_id + // "highlighting"/"colorizing". The goal is to make it easy for + // the user to access the entity that caused the event. + // + // If there is an entity_id in the message that is also in the + // state machine, we search the message for the entity_id and + // replace it with _renderEntity + // + if (message.indexOf(".") !== -1) { + const messageParts = message.split(" "); + for (let i = 0, size = messageParts.length; i < size; i++) { + if (messageParts[i] in this.hass.states) { + const entityId = messageParts[i]; + if (seenEntities.includes(entityId)) { + return ""; + } + seenEntities.push(entityId); + const messageEnd = messageParts.splice(i); + messageEnd.shift(); // remove the entity + return html` ${messageParts.join(" ")} + ${this._renderEntity( + entityId, + this.hass.states[entityId].attributes.friendly_name + )} + ${messageEnd.join(" ")}`; + } + } + } + // + // When we have a message has a specific entity_id attached to + // it, and the entity_id is not in the message, we look + // for the friendly name of the entity and replace that with + // _renderEntity if its there so the user can quickly get to + // that entity. + // + if (possibleEntity && possibleEntity in this.hass.states) { + const possibleEntityName = + this.hass.states[possibleEntity].attributes.friendly_name; + if (possibleEntityName && message.endsWith(possibleEntityName)) { + if (seenEntities.includes(possibleEntity)) { + return ""; + } + seenEntities.push(possibleEntity); + message = message.substring( + 0, + message.length - possibleEntityName.length + ); + return html` ${localizePrefix ? this.hass.localize(localizePrefix) : ""} + ${message} ${this._renderEntity(possibleEntity, possibleEntityName)}`; + } + } + return message; + } + + private _entityClicked(ev: Event) { + const entityId = (ev.currentTarget as any).entityId; + if (!entityId) { + return; + } + + ev.preventDefault(); + ev.stopPropagation(); + fireEvent(this, "hass-more-info", { + entityId: entityId, + }); + } + + private _close(): void { + setTimeout(() => fireEvent(this, "closed"), 500); + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + haStyleScrollbar, + buttonLinkStyle, + css` + :host([virtualize]) { + display: block; + height: 100%; + } + + .rtl { + direction: ltr; + } + + .entry-container { + width: 100%; + } + + .entry { + display: flex; + width: 100%; + line-height: 2em; + padding: 8px 16px; + box-sizing: border-box; + border-top: 1px solid var(--divider-color); + } + + .entry.no-entity, + .no-name .entry { + cursor: default; + } + + .entry:hover { + background-color: rgba(var(--rgb-primary-text-color), 0.04); + } + + .narrow:not(.no-icon) .time { + margin-left: 32px; + } + + .message-relative_time { + display: flex; + flex-direction: column; + } + + .secondary { + font-size: 12px; + line-height: 1.7; + } + + .secondary a { + color: var(--secondary-text-color); + } + + .date { + margin: 8px 0; + padding: 0 16px; + } + + .narrow .date { + padding: 0 8px; + } + + .rtl .date { + direction: rtl; + } + + .icon-message { + display: flex; + align-items: center; + } + + .no-entries { + text-align: center; + color: var(--secondary-text-color); + } + + state-badge { + margin-right: 16px; + flex-shrink: 0; + color: var(--state-icon-color); + } + + .message { + color: var(--primary-text-color); + } + + .no-name .message:first-letter { + text-transform: capitalize; + } + + a { + color: var(--primary-color); + } + + .container { + max-height: var(--logbook-max-height); + } + + .container, + lit-virtualizer { + height: 100%; + } + + lit-virtualizer { + contain: size layout !important; + } + + .narrow .entry { + line-height: 1.5; + padding: 8px; + } + + .narrow .icon-message state-badge { + margin-left: 0; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-logbook-renderer": HaLogbookRenderer; + } +} diff --git a/src/panels/logbook/ha-logbook.ts b/src/panels/logbook/ha-logbook.ts index 680eb38603..c797249032 100644 --- a/src/panels/logbook/ha-logbook.ts +++ b/src/panels/logbook/ha-logbook.ts @@ -1,55 +1,31 @@ -import "@lit-labs/virtualizer"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { customElement, eventOptions, property } from "lit/decorators"; -import { classMap } from "lit/directives/class-map"; -import { DOMAINS_WITH_DYNAMIC_PICTURE } from "../../common/const"; -import { formatDate } from "../../common/datetime/format_date"; -import { formatTimeWithSeconds } from "../../common/datetime/format_time"; -import { restoreScroll } from "../../common/decorators/restore-scroll"; -import { fireEvent } from "../../common/dom/fire_event"; -import { computeDomain } from "../../common/entity/compute_domain"; -import { domainIcon } from "../../common/entity/domain_icon"; -import { computeRTL, emitRTLDirection } from "../../common/util/compute_rtl"; -import "../../components/entity/state-badge"; +import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { isComponentLoaded } from "../../common/config/is_component_loaded"; +import { ensureArray } from "../../common/ensure-array"; +import { computeStateDomain } from "../../common/entity/compute_state_domain"; +import { throttle } from "../../common/util/throttle"; import "../../components/ha-circular-progress"; -import "../../components/ha-relative-time"; -import { LogbookEntry } from "../../data/logbook"; -import { TraceContexts } from "../../data/trace"; import { - haStyle, - haStyleScrollbar, - buttonLinkStyle, -} from "../../resources/styles"; + clearLogbookCache, + getLogbookData, + LogbookEntry, +} from "../../data/logbook"; +import { loadTraceContexts, TraceContexts } from "../../data/trace"; +import { fetchUsers } from "../../data/user"; import { HomeAssistant } from "../../types"; - -const EVENT_LOCALIZE_MAP = { - script_started: "from_script", -}; +import "./ha-logbook-renderer"; @customElement("ha-logbook") -class HaLogbook extends LitElement { +export class HaLogbook extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public userIdToName = {}; + @property() public time!: { range: [Date, Date] } | { recent: number }; - @property({ attribute: false }) - public traceContexts: TraceContexts = {}; - - @property({ attribute: false }) public entries: LogbookEntry[] = []; + @property() public entityId?: string | string[]; @property({ type: Boolean, attribute: "narrow" }) public narrow = false; - @property({ attribute: "rtl", type: Boolean }) - private _rtl = false; - @property({ type: Boolean, attribute: "virtualize", reflect: true }) public virtualize = false; @@ -62,463 +38,227 @@ class HaLogbook extends LitElement { @property({ type: Boolean, attribute: "relative-time" }) public relativeTime = false; - // @ts-ignore - @restoreScroll(".container") private _savedScrollPos?: number; + @property({ type: Boolean }) public showMoreLink = true; - protected shouldUpdate(changedProps: PropertyValues) { - const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - const languageChanged = - oldHass === undefined || oldHass.locale !== this.hass.locale; + @state() private _logbookEntries?: LogbookEntry[]; - return ( - changedProps.has("entries") || - changedProps.has("traceContexts") || - languageChanged - ); - } + @state() private _traceContexts?: TraceContexts; - protected updated(_changedProps: PropertyValues) { - const oldHass = _changedProps.get("hass") as HomeAssistant | undefined; + @state() private _userIdToName = {}; - if (oldHass === undefined || oldHass.language !== this.hass.language) { - this._rtl = computeRTL(this.hass); - } - } + @state() private _error?: string; + + private _lastLogbookDate?: Date; + + private _renderId = 1; + + private _throttleGetLogbookEntries = throttle( + () => this._getLogBookData(), + 10000 + ); protected render(): TemplateResult { - if (!this.entries?.length) { + if (!isComponentLoaded(this.hass, "logbook")) { + return html``; + } + + if (this._error) { + return html`
+ ${`${this.hass.localize("ui.components.logbook.retrieval_error")}: ${ + this._error + }`} +
`; + } + + if (this._logbookEntries === undefined) { return html` -
- ${this.hass.localize("ui.components.logbook.entries_not_found")} +
+
`; } + if (this._logbookEntries.length === 0) { + return html`
+ ${this.hass.localize("ui.components.logbook.entries_not_found")} +
`; + } + return html` -
- ${this.virtualize - ? html` - ` - : this.entries.map((item, index) => - this._renderLogbookItem(item, index) - )} -
+ `; } - private _renderLogbookItem = ( - item: LogbookEntry, - index: number - ): TemplateResult => { - if (!item || index === undefined) { - return html``; - } - - const seenEntityIds: string[] = []; - const previous = this.entries[index - 1]; - const stateObj = item.entity_id - ? this.hass.states[item.entity_id] - : undefined; - const item_username = - item.context_user_id && this.userIdToName[item.context_user_id]; - const domain = item.entity_id - ? computeDomain(item.entity_id) - : // Domain is there if there is no entity ID. - item.domain!; - - return html` -
- ${index === 0 || - (item?.when && - previous?.when && - new Date(item.when * 1000).toDateString() !== - new Date(previous.when * 1000).toDateString()) - ? html` -

- ${formatDate(new Date(item.when * 1000), this.hass.locale)} -

- ` - : html``} - -
-
- ${!this.noIcon - ? // We do not want to use dynamic entity pictures (e.g., from media player) for the log book rendering, - // as they would present a false state in the log (played media right now vs actual historic data). - html` - - ` - : ""} -
-
- ${!this.noName // Used for more-info panel (single entity case) - ? this._renderEntity(item.entity_id, item.name) - : ""} - ${item.message - ? html`${this._formatMessageWithPossibleEntity( - item.message, - seenEntityIds, - item.entity_id - )}` - : item.source - ? html` ${this._formatMessageWithPossibleEntity( - item.source, - seenEntityIds, - undefined, - "ui.components.logbook.by" - )}` - : ""} - ${item_username - ? ` ${this.hass.localize( - "ui.components.logbook.by_user" - )} ${item_username}` - : ``} - ${item.context_event_type - ? this._formatEventBy(item, seenEntityIds) - : ""} - ${item.context_message - ? html` ${this._formatMessageWithPossibleEntity( - item.context_message, - seenEntityIds, - item.context_entity_id, - "ui.components.logbook.for" - )}` - : ""} - ${item.context_entity_id && - !seenEntityIds.includes(item.context_entity_id) - ? // Another entity such as an automation or script - html` ${this.hass.localize("ui.components.logbook.for")} - ${this._renderEntity( - item.context_entity_id, - item.context_entity_id_name - )}` - : ""} -
-
- ${formatTimeWithSeconds( - new Date(item.when * 1000), - this.hass.locale - )} - - - - ${["script", "automation"].includes(item.domain!) && - item.context_id! in this.traceContexts - ? html` - - - ${this.hass.localize( - "ui.components.logbook.show_trace" - )} - ` - : ""} -
-
-
-
-
- `; - }; - - @eventOptions({ passive: true }) - private _saveScrollPos(e: Event) { - this._savedScrollPos = (e.target as HTMLDivElement).scrollTop; - } - - private _formatEventBy(item: LogbookEntry, seenEntities: string[]) { - if (item.context_event_type === "call_service") { - return `${this.hass.localize("ui.components.logbook.from_service")} ${ - item.context_domain - }.${item.context_service}`; - } - if (item.context_event_type === "automation_triggered") { - if (seenEntities.includes(item.context_entity_id!)) { - return ""; - } - seenEntities.push(item.context_entity_id!); - return html`${this.hass.localize("ui.components.logbook.from_automation")} - ${this._renderEntity(item.context_entity_id, item.context_name)}`; - } - if (item.context_name) { - return `${this.hass.localize("ui.components.logbook.from")} ${ - item.context_name - }`; - } - if (item.context_event_type === "state_changed") { - return ""; - } - if (item.context_event_type! in EVENT_LOCALIZE_MAP) { - return `${this.hass.localize( - `ui.components.logbook.${EVENT_LOCALIZE_MAP[item.context_event_type!]}` - )}`; - } - return `${this.hass.localize( - "ui.components.logbook.from" - )} ${this.hass.localize("ui.components.logbook.event")} ${ - item.context_event_type - }`; - } - - private _renderEntity( - entityId: string | undefined, - entityName: string | undefined - ) { - const hasState = entityId && entityId in this.hass.states; - const displayName = - entityName || - (hasState - ? this.hass.states[entityId].attributes.friendly_name || entityId - : entityId); - if (!hasState) { - return displayName; - } - return html``; - } - - private _formatMessageWithPossibleEntity( - message: string, - seenEntities: string[], - possibleEntity?: string, - localizePrefix?: string - ) { - // - // As we are looking at a log(book), we are doing entity_id - // "highlighting"/"colorizing". The goal is to make it easy for - // the user to access the entity that caused the event. - // - // If there is an entity_id in the message that is also in the - // state machine, we search the message for the entity_id and - // replace it with _renderEntity - // - if (message.indexOf(".") !== -1) { - const messageParts = message.split(" "); - for (let i = 0, size = messageParts.length; i < size; i++) { - if (messageParts[i] in this.hass.states) { - const entityId = messageParts[i]; - if (seenEntities.includes(entityId)) { - return ""; - } - seenEntities.push(entityId); - const messageEnd = messageParts.splice(i); - messageEnd.shift(); // remove the entity - return html` ${messageParts.join(" ")} - ${this._renderEntity( - entityId, - this.hass.states[entityId].attributes.friendly_name - )} - ${messageEnd.join(" ")}`; - } - } - } - // - // When we have a message has a specific entity_id attached to - // it, and the entity_id is not in the message, we look - // for the friendly name of the entity and replace that with - // _renderEntity if its there so the user can quickly get to - // that entity. - // - if (possibleEntity && possibleEntity in this.hass.states) { - const possibleEntityName = - this.hass.states[possibleEntity].attributes.friendly_name; - if (possibleEntityName && message.endsWith(possibleEntityName)) { - if (seenEntities.includes(possibleEntity)) { - return ""; - } - seenEntities.push(possibleEntity); - message = message.substring( - 0, - message.length - possibleEntityName.length - ); - return html` ${localizePrefix ? this.hass.localize(localizePrefix) : ""} - ${message} ${this._renderEntity(possibleEntity, possibleEntityName)}`; - } - } - return message; - } - - private _entityClicked(ev: Event) { - const entityId = (ev.currentTarget as any).entityId; - if (!entityId) { + public async refresh(force = false) { + if (!force && this._logbookEntries === undefined) { return; } - ev.preventDefault(); - ev.stopPropagation(); - fireEvent(this, "hass-more-info", { - entityId: entityId, - }); + this._throttleGetLogbookEntries.cancel(); + this._updateTraceContexts.cancel(); + this._updateUsers.cancel(); + + if ("range" in this.time) { + clearLogbookCache( + this.time.range[0].toISOString(), + this.time.range[1].toISOString() + ); + } + + this._lastLogbookDate = undefined; + this._logbookEntries = undefined; + this._error = undefined; + this._throttleGetLogbookEntries(); } - private _close(): void { - setTimeout(() => fireEvent(this, "closed"), 500); + protected updated(changedProps: PropertyValues): void { + super.updated(changedProps); + + if (changedProps.has("time") || changedProps.has("entityId")) { + this.refresh(true); + return; + } + + // We only need to fetch again if we track recent entries for an entity + if ( + !("recent" in this.time) || + !changedProps.has("hass") || + !this.entityId + ) { + return; + } + + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + + // Refresh data if we know the entity has changed. + if ( + !oldHass || + ensureArray(this.entityId).some( + (entityId) => this.hass.states[entityId] !== oldHass?.states[entityId] + ) + ) { + // wait for commit of data (we only account for the default setting of 1 sec) + setTimeout(this._throttleGetLogbookEntries, 1000); + } } - static get styles(): CSSResultGroup { + private async _getLogBookData() { + this._renderId += 1; + const renderId = this._renderId; + let startTime: Date; + let endTime: Date; + let appendData = false; + + if ("range" in this.time) { + [startTime, endTime] = this.time.range; + } else { + // Recent data + appendData = true; + startTime = + this._lastLogbookDate || + new Date(new Date().getTime() - 24 * 60 * 60 * 1000); + endTime = new Date(); + } + + this._updateUsers(); + if (this.hass.user?.is_admin) { + this._updateTraceContexts(); + } + + let newEntries: LogbookEntry[]; + + try { + newEntries = await getLogbookData( + this.hass, + startTime.toISOString(), + endTime.toISOString(), + this.entityId ? ensureArray(this.entityId).toString() : undefined + ); + } catch (err: any) { + if (renderId === this._renderId) { + this._error = err.message; + } + return; + } + + // New render happening. + if (renderId !== this._renderId) { + return; + } + + this._logbookEntries = + appendData && this._logbookEntries + ? newEntries.concat(...this._logbookEntries) + : newEntries; + this._lastLogbookDate = endTime; + } + + private _updateTraceContexts = throttle(async () => { + this._traceContexts = await loadTraceContexts(this.hass); + }, 60000); + + private _updateUsers = throttle(async () => { + const userIdToName = {}; + + // Start loading users + const userProm = this.hass.user?.is_admin && fetchUsers(this.hass); + + // Process persons + for (const entity of Object.values(this.hass.states)) { + if ( + entity.attributes.user_id && + computeStateDomain(entity) === "person" + ) { + userIdToName[entity.attributes.user_id] = + entity.attributes.friendly_name; + } + } + + // Process users + if (userProm) { + const users = await userProm; + for (const user of users) { + if (!(user.id in userIdToName)) { + userIdToName[user.id] = user.name; + } + } + } + + this._userIdToName = userIdToName; + }, 60000); + + static get styles() { return [ - haStyle, - haStyleScrollbar, - buttonLinkStyle, css` :host([virtualize]) { display: block; height: 100%; } - .rtl { - direction: ltr; - } - - .entry-container { - width: 100%; - } - - .entry { - display: flex; - width: 100%; - line-height: 2em; - padding: 8px 16px; - box-sizing: border-box; - border-top: 1px solid var(--divider-color); - } - - .entry.no-entity, - .no-name .entry { - cursor: default; - } - - .entry:hover { - background-color: rgba(var(--rgb-primary-text-color), 0.04); - } - - .narrow:not(.no-icon) .time { - margin-left: 32px; - } - - .message-relative_time { - display: flex; - flex-direction: column; - } - - .secondary { - font-size: 12px; - line-height: 1.7; - } - - .secondary a { - color: var(--secondary-text-color); - } - - .date { - margin: 8px 0; - padding: 0 16px; - } - - .narrow .date { - padding: 0 8px; - } - - .rtl .date { - direction: rtl; - } - - .icon-message { - display: flex; - align-items: center; - } - .no-entries { text-align: center; + padding: 16px; color: var(--secondary-text-color); } - state-badge { - margin-right: 16px; - flex-shrink: 0; - color: var(--state-icon-color); - } - - .message { - color: var(--primary-text-color); - } - - .no-name .message:first-letter { - text-transform: capitalize; - } - - a { - color: var(--primary-color); - } - - .container { - max-height: var(--logbook-max-height); - } - - .container, - lit-virtualizer { + .progress-wrapper { + display: flex; + justify-content: center; height: 100%; - } - - lit-virtualizer { - contain: size layout !important; - } - - .narrow .entry { - line-height: 1.5; - padding: 8px; - } - - .narrow .icon-message state-badge { - margin-left: 0; + align-items: center; } `, ]; diff --git a/src/panels/logbook/ha-panel-logbook.ts b/src/panels/logbook/ha-panel-logbook.ts index feac004259..864532d5f2 100644 --- a/src/panels/logbook/ha-panel-logbook.ts +++ b/src/panels/logbook/ha-panel-logbook.ts @@ -12,28 +12,19 @@ import { } from "date-fns"; import { css, html, LitElement, PropertyValues } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { navigate } from "../../common/navigate"; import { createSearchParam, - extractSearchParam, + extractSearchParamsObject, } from "../../common/url/search-params"; import { computeRTL } from "../../common/util/compute_rtl"; import "../../components/entity/ha-entity-picker"; -import "../../components/ha-circular-progress"; +import type { HaEntityPickerEntityFilterFunc } from "../../components/entity/ha-entity-picker"; import "../../components/ha-date-range-picker"; import type { DateRangePickerRanges } from "../../components/ha-date-range-picker"; import "../../components/ha-icon-button"; import "../../components/ha-menu-button"; -import { - clearLogbookCache, - getLogbookData, - LogbookEntry, -} from "../../data/logbook"; -import { loadTraceContexts, TraceContexts } from "../../data/trace"; -import { fetchUsers } from "../../data/user"; -import { showAlertDialog } from "../../dialogs/generic/show-dialog-box"; import "../../layouts/ha-app-layout"; import { haStyle } from "../../resources/styles"; import { HomeAssistant } from "../../types"; @@ -45,36 +36,24 @@ export class HaPanelLogbook extends LitElement { @property({ reflect: true, type: Boolean }) narrow!: boolean; - @property() _startDate: Date; + @state() _time: { range: [Date, Date] }; - @property() _endDate: Date; - - @property() _entityId = ""; - - @property() _isLoading = false; - - @property() _entries: LogbookEntry[] = []; + @state() _entityId = ""; @property({ reflect: true, type: Boolean }) rtl = false; @state() private _ranges?: DateRangePickerRanges; - private _fetchUserPromise?: Promise; - - @state() private _userIdToName = {}; - - @state() private _traceContexts: TraceContexts = {}; - public constructor() { super(); const start = new Date(); start.setHours(start.getHours() - 2, 0, 0, 0); - this._startDate = start; const end = new Date(); end.setHours(end.getHours() + 1, 0, 0, 0); - this._endDate = end; + + this._time = { range: [start, end] }; } protected render() { @@ -91,19 +70,15 @@ export class HaPanelLogbook extends LitElement { @click=${this._refreshLogbook} .path=${mdiRefresh} .label=${this.hass!.localize("ui.common.refresh")} - .disabled=${this._isLoading} > - ${this._isLoading ? html`` : ""} -
@@ -114,38 +89,27 @@ export class HaPanelLogbook extends LitElement { .label=${this.hass.localize( "ui.components.entity.entity-picker.entity" )} - .disabled=${this._isLoading} + .entityFilter=${this._entityFilter} @change=${this._entityPicked} >
- ${this._isLoading - ? html` -
- -
- ` - : html` - - `} + `; } - protected firstUpdated(changedProps: PropertyValues) { - super.firstUpdated(changedProps); - this.hass.loadBackendTranslation("title"); + protected willUpdate(changedProps: PropertyValues) { + super.willUpdate(changedProps); - this._fetchUserPromise = this._fetchUserNames(); + if (this.hasUpdated) { + return; + } const today = new Date(); const weekStart = startOfWeek(today); @@ -164,151 +128,125 @@ export class HaPanelLogbook extends LitElement { [addDays(weekStart, -7), addDays(weekEnd, -7)], }; - this._entityId = extractSearchParam("entity_id") ?? ""; - - const startDate = extractSearchParam("start_date"); - if (startDate) { - this._startDate = new Date(startDate); - } - const endDate = extractSearchParam("end_date"); - if (endDate) { - this._endDate = new Date(endDate); - } + this._applyURLParams(); } - protected updated(changedProps: PropertyValues) { - if ( - changedProps.has("_startDate") || - changedProps.has("_endDate") || - changedProps.has("_entityId") - ) { - this._getData(); - } + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + this.hass.loadBackendTranslation("title"); + } + public connectedCallback(): void { + super.connectedCallback(); + window.addEventListener("location-changed", this._locationChanged); + } + + public disconnectedCallback(): void { + super.disconnectedCallback(); + window.removeEventListener("location-changed", this._locationChanged); + } + + private _locationChanged = () => { + this._applyURLParams(); + }; + + protected updated(changedProps: PropertyValues) { if (changedProps.has("hass")) { const oldHass = changedProps.get("hass") as HomeAssistant | undefined; if (!oldHass || oldHass.language !== this.hass.language) { this.rtl = computeRTL(this.hass); } } + + this._applyURLParams(); } - private async _fetchUserNames() { - const userIdToName = {}; + private _applyURLParams() { + const searchParams = new URLSearchParams(location.search); - // Start loading users - const userProm = this.hass.user?.is_admin && fetchUsers(this.hass); - - // Process persons - Object.values(this.hass.states).forEach((entity) => { - if ( - entity.attributes.user_id && - computeStateDomain(entity) === "person" - ) { - this._userIdToName[entity.attributes.user_id] = - entity.attributes.friendly_name; - } - }); - - // Process users - if (userProm) { - const users = await userProm; - for (const user of users) { - if (!(user.id in userIdToName)) { - userIdToName[user.id] = user.name; - } - } + if (searchParams.has("entity_id")) { + this._entityId = searchParams.get("entity_id") ?? ""; } - this._userIdToName = userIdToName; + const startDateStr = searchParams.get("start_date"); + const endDateStr = searchParams.get("end_date"); + + if (startDateStr || endDateStr) { + const startDate = startDateStr + ? new Date(startDateStr) + : this._time.range[0]; + const endDate = endDateStr ? new Date(endDateStr) : this._time.range[1]; + + // Only set if date has changed. + if ( + startDate.getTime() !== this._time.range[0].getTime() || + endDate.getTime() !== this._time.range[1].getTime() + ) { + this._time = { + range: [ + startDateStr ? new Date(startDateStr) : this._time.range[0], + endDateStr ? new Date(endDateStr) : this._time.range[1], + ], + }; + } + } } private _dateRangeChanged(ev) { - this._startDate = ev.detail.startDate; + const startDate = ev.detail.startDate; const endDate = ev.detail.endDate; if (endDate.getHours() === 0 && endDate.getMinutes() === 0) { endDate.setDate(endDate.getDate() + 1); endDate.setMilliseconds(endDate.getMilliseconds() - 1); } - this._endDate = endDate; - - this._updatePath(); + this._time = { range: [startDate, endDate] }; + this._updatePath({ + start_date: this._time.range[0].toISOString(), + end_date: this._time.range[1].toISOString(), + }); } private _entityPicked(ev) { this._entityId = ev.target.value; - - this._updatePath(); + this._updatePath({ entity_id: this._entityId }); } - private _updatePath() { - const params: Record = {}; - - if (this._entityId) { - params.entity_id = this._entityId; + private _updatePath(update: Record) { + const params = extractSearchParamsObject(); + for (const [key, value] of Object.entries(update)) { + if (value === undefined) { + delete params[key]; + } else { + params[key] = value; + } } - - if (this._startDate) { - params.start_date = this._startDate.toISOString(); - } - - if (this._endDate) { - params.end_date = this._endDate.toISOString(); - } - navigate(`/logbook?${createSearchParam(params)}`, { replace: true }); } private _refreshLogbook() { - this._entries = []; - clearLogbookCache( - this._startDate.toISOString(), - this._endDate.toISOString() - ); - this._getData(); + this.shadowRoot!.querySelector("ha-logbook")?.refresh(); } - private async _getData() { - this._isLoading = true; - let entries; - let traceContexts; - - try { - [entries, traceContexts] = await Promise.all([ - getLogbookData( - this.hass, - this._startDate.toISOString(), - this._endDate.toISOString(), - this._entityId - ), - isComponentLoaded(this.hass, "trace") && this.hass.user?.is_admin - ? loadTraceContexts(this.hass) - : {}, - this._fetchUserPromise, - ]); - } catch (err: any) { - showAlertDialog(this, { - title: this.hass.localize("ui.components.logbook.retrieval_error"), - text: err.message, - }); + private _entityFilter: HaEntityPickerEntityFilterFunc = (entity) => { + if (computeStateDomain(entity) !== "sensor") { + return true; } - this._entries = entries; - this._traceContexts = traceContexts; - this._isLoading = false; - } + return ( + entity.attributes.unit_of_measurement === undefined && + entity.attributes.state_class === undefined + ); + }; static get styles() { return [ haStyle, css` - ha-logbook, - .progress-wrapper { + ha-logbook { height: calc(100vh - 136px); } - :host([narrow]) ha-logbook, - :host([narrow]) .progress-wrapper { + :host([narrow]) ha-logbook { height: calc(100vh - 198px); } @@ -321,17 +259,6 @@ export class HaPanelLogbook extends LitElement { margin-right: 0; } - .progress-wrapper { - position: relative; - } - - ha-circular-progress { - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - } - .filters { display: flex; align-items: flex-end; diff --git a/src/panels/lovelace/cards/hui-logbook-card.ts b/src/panels/lovelace/cards/hui-logbook-card.ts index 3adf6f40e8..963644fdf8 100644 --- a/src/panels/lovelace/cards/hui-logbook-card.ts +++ b/src/panels/lovelace/cards/hui-logbook-card.ts @@ -9,15 +9,11 @@ import { import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; -import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; -import { throttle } from "../../../common/util/throttle"; import "../../../components/ha-card"; -import "../../../components/ha-circular-progress"; -import { fetchUsers } from "../../../data/user"; -import { getLogbookData, LogbookEntry } from "../../../data/logbook"; import type { HomeAssistant } from "../../../types"; import "../../logbook/ha-logbook"; +import type { HaLogbook } from "../../logbook/ha-logbook"; import { findEntities } from "../common/find-entities"; import { processConfigEntities } from "../common/process-config-entities"; import "../components/hui-warning"; @@ -56,21 +52,9 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard { @state() private _config?: LogbookCardConfig; - @state() private _logbookEntries?: LogbookEntry[]; + @state() private _time?: HaLogbook["time"]; - @state() private _configEntities?: EntityConfig[]; - - @state() private _userIdToName = {}; - - private _lastLogbookDate?: Date; - - private _fetchUserPromise?: Promise; - - private _error?: string; - - private _throttleGetLogbookEntries = throttle(() => { - this._getLogBookData(); - }, 10000); + @state() private _entityId?: string[]; public getCardSize(): number { return 9 + (this._config?.title ? 1 : 0); @@ -81,45 +65,16 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard { throw new Error("Entities must be specified"); } - this._configEntities = processConfigEntities(config.entities); - this._config = { hours_to_show: 24, ...config, }; - } - - protected shouldUpdate(changedProps: PropertyValues): boolean { - if ( - changedProps.has("_config") || - changedProps.has("_persons") || - changedProps.has("_logbookEntries") - ) { - return true; - } - - const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - - if ( - !this._configEntities || - !oldHass || - oldHass.themes !== this.hass!.themes || - oldHass.locale !== this.hass!.locale - ) { - return true; - } - - for (const entity of this._configEntities) { - if (oldHass.states[entity.entity] !== this.hass!.states[entity.entity]) { - return true; - } - } - - return false; - } - - protected firstUpdated(): void { - this._fetchUserPromise = this._fetchUserNames(); + this._time = { + recent: this._config!.hours_to_show! * 60 * 60 * 1000, + }; + this._entityId = processConfigEntities(config.entities).map( + (entity) => entity.entity + ); } protected updated(changedProperties: PropertyValues) { @@ -139,33 +94,6 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard { ) { applyThemesOnElement(this, this.hass.themes, this._config.theme); } - - if ( - configChanged && - (oldConfig?.entities !== this._config.entities || - oldConfig?.hours_to_show !== this._config!.hours_to_show) - ) { - this._logbookEntries = undefined; - this._lastLogbookDate = undefined; - - if (!this._configEntities) { - return; - } - - this._throttleGetLogbookEntries(); - return; - } - - if ( - oldHass && - this._configEntities!.some( - (entity) => - oldHass.states[entity.entity] !== this.hass!.states[entity.entity] - ) - ) { - // wait for commit of data (we only account for the default setting of 1 sec) - setTimeout(this._throttleGetLogbookEntries, 1000); - } } protected render(): TemplateResult { @@ -189,116 +117,19 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard { class=${classMap({ "no-header": !this._config!.title })} >
- ${this._error - ? html` -
- ${`${this.hass.localize( - "ui.components.logbook.retrieval_error" - )}: ${this._error}`} -
- ` - : !this._logbookEntries - ? html` - - ` - : this._logbookEntries.length - ? html` - - ` - : html` -
- ${this.hass.localize( - "ui.components.logbook.entries_not_found" - )} -
- `} +
`; } - private async _getLogBookData() { - if ( - !this.hass || - !this._config || - !isComponentLoaded(this.hass, "logbook") - ) { - return; - } - - const hoursToShowDate = new Date( - new Date().getTime() - this._config!.hours_to_show! * 60 * 60 * 1000 - ); - const lastDate = this._lastLogbookDate || hoursToShowDate; - const now = new Date(); - let newEntries: LogbookEntry[]; - - try { - [newEntries] = await Promise.all([ - getLogbookData( - this.hass, - lastDate.toISOString(), - now.toISOString(), - this._configEntities!.map((entity) => entity.entity).toString() - ), - this._fetchUserPromise, - ]); - } catch (err: any) { - this._error = err.message; - return; - } - - const logbookEntries = this._logbookEntries - ? [...newEntries, ...this._logbookEntries] - : newEntries; - - this._logbookEntries = logbookEntries.filter( - (logEntry) => new Date(logEntry.when * 1000) > hoursToShowDate - ); - - this._lastLogbookDate = now; - } - - private async _fetchUserNames() { - const userIdToName = {}; - - // Start loading users - const userProm = this.hass.user?.is_admin && fetchUsers(this.hass); - - // Process persons - Object.values(this.hass!.states).forEach((entity) => { - if ( - entity.attributes.user_id && - computeStateDomain(entity) === "person" - ) { - this._userIdToName[entity.attributes.user_id] = - entity.attributes.friendly_name; - } - }); - - // Process users - if (userProm) { - const users = await userProm; - for (const user of users) { - if (!(user.id in userIdToName)) { - userIdToName[user.id] = user.name; - } - } - } - - this._userIdToName = userIdToName; - } - static get styles(): CSSResultGroup { return [ css` @@ -317,21 +148,10 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard { padding-top: 16px; } - .no-entries { - text-align: center; - padding: 16px; - color: var(--secondary-text-color); - } - ha-logbook { height: 385px; display: block; } - - ha-circular-progress { - display: flex; - justify-content: center; - } `, ]; }