mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-23 17:26:42 +00:00
Refactor logbook data fetch logic into reusable class (#12701)
This commit is contained in:
parent
dd3a3ec586
commit
90c234ffad
@ -13,7 +13,7 @@ export const throttle = <T extends any[]>(
|
|||||||
) => {
|
) => {
|
||||||
let timeout: number | undefined;
|
let timeout: number | undefined;
|
||||||
let previous = 0;
|
let previous = 0;
|
||||||
return (...args: T): void => {
|
const throttledFunc = (...args: T): void => {
|
||||||
const later = () => {
|
const later = () => {
|
||||||
previous = leading === false ? 0 : Date.now();
|
previous = leading === false ? 0 : Date.now();
|
||||||
timeout = undefined;
|
timeout = undefined;
|
||||||
@ -35,4 +35,10 @@ export const throttle = <T extends any[]>(
|
|||||||
timeout = window.setTimeout(later, remaining);
|
timeout = window.setTimeout(later, remaining);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
throttledFunc.cancel = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = undefined;
|
||||||
|
previous = 0;
|
||||||
|
};
|
||||||
|
return throttledFunc;
|
||||||
};
|
};
|
||||||
|
@ -20,7 +20,7 @@ interface HassEntityWithCachedName extends HassEntity {
|
|||||||
friendly_name: string;
|
friendly_name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
|
export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean;
|
||||||
|
|
||||||
// eslint-disable-next-line lit/prefer-static-styles
|
// eslint-disable-next-line lit/prefer-static-styles
|
||||||
const rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (item) =>
|
const rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (item) =>
|
||||||
|
@ -3,7 +3,7 @@ import { customElement, property } from "lit/decorators";
|
|||||||
import { LogbookEntry } from "../../data/logbook";
|
import { LogbookEntry } from "../../data/logbook";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import "./hat-logbook-note";
|
import "./hat-logbook-note";
|
||||||
import "../../panels/logbook/ha-logbook";
|
import "../../panels/logbook/ha-logbook-renderer";
|
||||||
import { TraceExtended } from "../../data/trace";
|
import { TraceExtended } from "../../data/trace";
|
||||||
|
|
||||||
@customElement("ha-trace-logbook")
|
@customElement("ha-trace-logbook")
|
||||||
@ -19,12 +19,12 @@ export class HaTraceLogbook extends LitElement {
|
|||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return this.logbookEntries.length
|
return this.logbookEntries.length
|
||||||
? html`
|
? html`
|
||||||
<ha-logbook
|
<ha-logbook-renderer
|
||||||
relative-time
|
relative-time
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.entries=${this.logbookEntries}
|
.entries=${this.logbookEntries}
|
||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
></ha-logbook>
|
></ha-logbook-renderer>
|
||||||
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
|
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
|
||||||
`
|
`
|
||||||
: html`<div class="padded-box">
|
: html`<div class="padded-box">
|
||||||
|
@ -13,7 +13,7 @@ import {
|
|||||||
getDataFromPath,
|
getDataFromPath,
|
||||||
TraceExtended,
|
TraceExtended,
|
||||||
} from "../../data/trace";
|
} from "../../data/trace";
|
||||||
import "../../panels/logbook/ha-logbook";
|
import "../../panels/logbook/ha-logbook-renderer";
|
||||||
import { traceTabStyles } from "./trace-tab-styles";
|
import { traceTabStyles } from "./trace-tab-styles";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import type { NodeInfo } from "./hat-script-graph";
|
import type { NodeInfo } from "./hat-script-graph";
|
||||||
@ -224,12 +224,12 @@ export class HaTracePathDetails extends LitElement {
|
|||||||
|
|
||||||
return entries.length
|
return entries.length
|
||||||
? html`
|
? html`
|
||||||
<ha-logbook
|
<ha-logbook-renderer
|
||||||
relative-time
|
relative-time
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.entries=${entries}
|
.entries=${entries}
|
||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
></ha-logbook>
|
></ha-logbook-renderer>
|
||||||
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
|
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
|
||||||
`
|
`
|
||||||
: html`<div class="padded-box">
|
: html`<div class="padded-box">
|
||||||
|
@ -1,17 +1,10 @@
|
|||||||
import { startOfYesterday } from "date-fns";
|
import { startOfYesterday } from "date-fns";
|
||||||
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
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 { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
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 "../../panels/logbook/ha-logbook";
|
||||||
import { haStyle } from "../../resources/styles";
|
import type { HomeAssistant } from "../../types";
|
||||||
import { HomeAssistant } from "../../types";
|
|
||||||
|
|
||||||
@customElement("ha-more-info-logbook")
|
@customElement("ha-more-info-logbook")
|
||||||
export class MoreInfoLogbook extends LitElement {
|
export class MoreInfoLogbook extends LitElement {
|
||||||
@ -19,26 +12,12 @@ export class MoreInfoLogbook extends LitElement {
|
|||||||
|
|
||||||
@property() public entityId!: string;
|
@property() public entityId!: string;
|
||||||
|
|
||||||
@state() private _logbookEntries?: LogbookEntry[];
|
|
||||||
|
|
||||||
@state() private _traceContexts?: TraceContexts;
|
|
||||||
|
|
||||||
@state() private _userIdToName = {};
|
|
||||||
|
|
||||||
private _lastLogbookDate?: Date;
|
|
||||||
|
|
||||||
private _fetchUserPromise?: Promise<void>;
|
|
||||||
|
|
||||||
private _error?: string;
|
|
||||||
|
|
||||||
private _showMoreHref = "";
|
private _showMoreHref = "";
|
||||||
|
|
||||||
private _throttleGetLogbookEntries = throttle(() => {
|
private _time = { recent: 86400 };
|
||||||
this._getLogBookData();
|
|
||||||
}, 10000);
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (!this.entityId) {
|
if (!isComponentLoaded(this.hass, "logbook") || !this.entityId) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
const stateObj = this.hass.states[this.entityId];
|
const stateObj = this.hass.states[this.entityId];
|
||||||
@ -48,149 +27,34 @@ export class MoreInfoLogbook extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${isComponentLoaded(this.hass, "logbook")
|
<div class="header">
|
||||||
? this._error
|
<div class="title">
|
||||||
? html`<div class="no-entries">
|
${this.hass.localize("ui.dialogs.more_info_control.logbook")}
|
||||||
${`${this.hass.localize(
|
</div>
|
||||||
"ui.components.logbook.retrieval_error"
|
<a href=${this._showMoreHref} @click=${this._close}
|
||||||
)}: ${this._error}`}
|
>${this.hass.localize("ui.dialogs.more_info_control.show_more")}</a
|
||||||
</div>`
|
>
|
||||||
: !this._logbookEntries
|
</div>
|
||||||
? html`
|
<ha-logbook
|
||||||
<ha-circular-progress
|
.hass=${this.hass}
|
||||||
active
|
.time=${this._time}
|
||||||
alt=${this.hass.localize("ui.common.loading")}
|
.entityId=${this.entityId}
|
||||||
></ha-circular-progress>
|
narrow
|
||||||
`
|
no-icon
|
||||||
: this._logbookEntries.length
|
no-name
|
||||||
? html`
|
relative-time
|
||||||
<div class="header">
|
></ha-logbook>
|
||||||
<div class="title">
|
|
||||||
${this.hass.localize("ui.dialogs.more_info_control.logbook")}
|
|
||||||
</div>
|
|
||||||
<a href=${this._showMoreHref} @click=${this._close}
|
|
||||||
>${this.hass.localize(
|
|
||||||
"ui.dialogs.more_info_control.show_more"
|
|
||||||
)}</a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<ha-logbook
|
|
||||||
narrow
|
|
||||||
no-icon
|
|
||||||
no-name
|
|
||||||
relative-time
|
|
||||||
.hass=${this.hass}
|
|
||||||
.entries=${this._logbookEntries}
|
|
||||||
.traceContexts=${this._traceContexts}
|
|
||||||
.userIdToName=${this._userIdToName}
|
|
||||||
></ha-logbook>
|
|
||||||
`
|
|
||||||
: html`<div class="no-entries">
|
|
||||||
${this.hass.localize("ui.components.logbook.entries_not_found")}
|
|
||||||
</div>`
|
|
||||||
: ""}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected firstUpdated(): void {
|
protected willUpdate(changedProps: PropertyValues): void {
|
||||||
this._fetchUserPromise = this._fetchUserNames();
|
super.willUpdate(changedProps);
|
||||||
}
|
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues): void {
|
|
||||||
super.updated(changedProps);
|
|
||||||
|
|
||||||
if (changedProps.has("entityId")) {
|
|
||||||
this._lastLogbookDate = undefined;
|
|
||||||
this._logbookEntries = undefined;
|
|
||||||
|
|
||||||
if (!this.entityId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (changedProps.has("entityId") && this.entityId) {
|
||||||
this._showMoreHref = `/logbook?entity_id=${
|
this._showMoreHref = `/logbook?entity_id=${
|
||||||
this.entityId
|
this.entityId
|
||||||
}&start_date=${startOfYesterday().toISOString()}`;
|
}&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 {
|
private _close(): void {
|
||||||
@ -199,13 +63,7 @@ export class MoreInfoLogbook extends LitElement {
|
|||||||
|
|
||||||
static get styles() {
|
static get styles() {
|
||||||
return [
|
return [
|
||||||
haStyle,
|
|
||||||
css`
|
css`
|
||||||
.no-entries {
|
|
||||||
text-align: center;
|
|
||||||
padding: 16px;
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
}
|
|
||||||
ha-logbook {
|
ha-logbook {
|
||||||
--logbook-max-height: 250px;
|
--logbook-max-height: 250px;
|
||||||
}
|
}
|
||||||
@ -214,10 +72,6 @@ export class MoreInfoLogbook extends LitElement {
|
|||||||
--logbook-max-height: unset;
|
--logbook-max-height: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ha-circular-progress {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
532
src/panels/logbook/ha-logbook-renderer.ts
Normal file
532
src/panels/logbook/ha-logbook-renderer.ts
Normal file
@ -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<this>) {
|
||||||
|
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`
|
||||||
|
<div class="container no-entries" .dir=${emitRTLDirection(this._rtl)}>
|
||||||
|
${this.hass.localize("ui.components.logbook.entries_not_found")}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="container ha-scrollbar ${classMap({
|
||||||
|
narrow: this.narrow,
|
||||||
|
rtl: this._rtl,
|
||||||
|
"no-name": this.noName,
|
||||||
|
"no-icon": this.noIcon,
|
||||||
|
})}"
|
||||||
|
@scroll=${this._saveScrollPos}
|
||||||
|
>
|
||||||
|
${this.virtualize
|
||||||
|
? html`<lit-virtualizer
|
||||||
|
scroller
|
||||||
|
class="ha-scrollbar"
|
||||||
|
.items=${this.entries}
|
||||||
|
.renderItem=${this._renderLogbookItem}
|
||||||
|
>
|
||||||
|
</lit-virtualizer>`
|
||||||
|
: this.entries.map((item, index) =>
|
||||||
|
this._renderLogbookItem(item, index)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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`
|
||||||
|
<div class="entry-container">
|
||||||
|
${index === 0 ||
|
||||||
|
(item?.when &&
|
||||||
|
previous?.when &&
|
||||||
|
new Date(item.when * 1000).toDateString() !==
|
||||||
|
new Date(previous.when * 1000).toDateString())
|
||||||
|
? html`
|
||||||
|
<h4 class="date">
|
||||||
|
${formatDate(new Date(item.when * 1000), this.hass.locale)}
|
||||||
|
</h4>
|
||||||
|
`
|
||||||
|
: html``}
|
||||||
|
|
||||||
|
<div class="entry ${classMap({ "no-entity": !item.entity_id })}">
|
||||||
|
<div class="icon-message">
|
||||||
|
${!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`
|
||||||
|
<state-badge
|
||||||
|
.hass=${this.hass}
|
||||||
|
.overrideIcon=${item.icon ||
|
||||||
|
(item.domain && !stateObj
|
||||||
|
? domainIcon(item.domain!)
|
||||||
|
: undefined)}
|
||||||
|
.overrideImage=${DOMAINS_WITH_DYNAMIC_PICTURE.has(domain)
|
||||||
|
? ""
|
||||||
|
: stateObj?.attributes.entity_picture_local ||
|
||||||
|
stateObj?.attributes.entity_picture}
|
||||||
|
.stateObj=${stateObj}
|
||||||
|
.stateColor=${false}
|
||||||
|
></state-badge>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
<div class="message-relative_time">
|
||||||
|
<div class="message">
|
||||||
|
${!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
|
||||||
|
)}`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
<div class="secondary">
|
||||||
|
<span
|
||||||
|
>${formatTimeWithSeconds(
|
||||||
|
new Date(item.when * 1000),
|
||||||
|
this.hass.locale
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
|
-
|
||||||
|
<ha-relative-time
|
||||||
|
.hass=${this.hass}
|
||||||
|
.datetime=${item.when * 1000}
|
||||||
|
capitalize
|
||||||
|
></ha-relative-time>
|
||||||
|
${["script", "automation"].includes(item.domain!) &&
|
||||||
|
item.context_id! in this.traceContexts
|
||||||
|
? html`
|
||||||
|
-
|
||||||
|
<a
|
||||||
|
href=${`/config/${
|
||||||
|
this.traceContexts[item.context_id!].domain
|
||||||
|
}/trace/${
|
||||||
|
this.traceContexts[item.context_id!].domain ===
|
||||||
|
"script"
|
||||||
|
? `script.${
|
||||||
|
this.traceContexts[item.context_id!].item_id
|
||||||
|
}`
|
||||||
|
: this.traceContexts[item.context_id!].item_id
|
||||||
|
}?run_id=${
|
||||||
|
this.traceContexts[item.context_id!].run_id
|
||||||
|
}`}
|
||||||
|
@click=${this._close}
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.components.logbook.show_trace"
|
||||||
|
)}</a
|
||||||
|
>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
@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`<button
|
||||||
|
class="link"
|
||||||
|
@click=${this._entityClicked}
|
||||||
|
.entityId=${entityId}
|
||||||
|
>
|
||||||
|
${displayName}
|
||||||
|
</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,55 +1,31 @@
|
|||||||
import "@lit-labs/virtualizer";
|
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||||
import {
|
import { customElement, property, state } from "lit/decorators";
|
||||||
css,
|
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||||
CSSResultGroup,
|
import { ensureArray } from "../../common/ensure-array";
|
||||||
html,
|
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||||
LitElement,
|
import { throttle } from "../../common/util/throttle";
|
||||||
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-circular-progress";
|
||||||
import "../../components/ha-relative-time";
|
|
||||||
import { LogbookEntry } from "../../data/logbook";
|
|
||||||
import { TraceContexts } from "../../data/trace";
|
|
||||||
import {
|
import {
|
||||||
haStyle,
|
clearLogbookCache,
|
||||||
haStyleScrollbar,
|
getLogbookData,
|
||||||
buttonLinkStyle,
|
LogbookEntry,
|
||||||
} from "../../resources/styles";
|
} from "../../data/logbook";
|
||||||
|
import { loadTraceContexts, TraceContexts } from "../../data/trace";
|
||||||
|
import { fetchUsers } from "../../data/user";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
|
import "./ha-logbook-renderer";
|
||||||
const EVENT_LOCALIZE_MAP = {
|
|
||||||
script_started: "from_script",
|
|
||||||
};
|
|
||||||
|
|
||||||
@customElement("ha-logbook")
|
@customElement("ha-logbook")
|
||||||
class HaLogbook extends LitElement {
|
export class HaLogbook extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false }) public userIdToName = {};
|
@property() public time!: { range: [Date, Date] } | { recent: number };
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property() public entityId?: string | string[];
|
||||||
public traceContexts: TraceContexts = {};
|
|
||||||
|
|
||||||
@property({ attribute: false }) public entries: LogbookEntry[] = [];
|
|
||||||
|
|
||||||
@property({ type: Boolean, attribute: "narrow" })
|
@property({ type: Boolean, attribute: "narrow" })
|
||||||
public narrow = false;
|
public narrow = false;
|
||||||
|
|
||||||
@property({ attribute: "rtl", type: Boolean })
|
|
||||||
private _rtl = false;
|
|
||||||
|
|
||||||
@property({ type: Boolean, attribute: "virtualize", reflect: true })
|
@property({ type: Boolean, attribute: "virtualize", reflect: true })
|
||||||
public virtualize = false;
|
public virtualize = false;
|
||||||
|
|
||||||
@ -62,463 +38,227 @@ class HaLogbook extends LitElement {
|
|||||||
@property({ type: Boolean, attribute: "relative-time" })
|
@property({ type: Boolean, attribute: "relative-time" })
|
||||||
public relativeTime = false;
|
public relativeTime = false;
|
||||||
|
|
||||||
// @ts-ignore
|
@property({ type: Boolean }) public showMoreLink = true;
|
||||||
@restoreScroll(".container") private _savedScrollPos?: number;
|
|
||||||
|
|
||||||
protected shouldUpdate(changedProps: PropertyValues<this>) {
|
@state() private _logbookEntries?: LogbookEntry[];
|
||||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
|
||||||
const languageChanged =
|
|
||||||
oldHass === undefined || oldHass.locale !== this.hass.locale;
|
|
||||||
|
|
||||||
return (
|
@state() private _traceContexts?: TraceContexts;
|
||||||
changedProps.has("entries") ||
|
|
||||||
changedProps.has("traceContexts") ||
|
|
||||||
languageChanged
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected updated(_changedProps: PropertyValues) {
|
@state() private _userIdToName = {};
|
||||||
const oldHass = _changedProps.get("hass") as HomeAssistant | undefined;
|
|
||||||
|
|
||||||
if (oldHass === undefined || oldHass.language !== this.hass.language) {
|
@state() private _error?: string;
|
||||||
this._rtl = computeRTL(this.hass);
|
|
||||||
}
|
private _lastLogbookDate?: Date;
|
||||||
}
|
|
||||||
|
private _renderId = 1;
|
||||||
|
|
||||||
|
private _throttleGetLogbookEntries = throttle(
|
||||||
|
() => this._getLogBookData(),
|
||||||
|
10000
|
||||||
|
);
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (!this.entries?.length) {
|
if (!isComponentLoaded(this.hass, "logbook")) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._error) {
|
||||||
|
return html`<div class="no-entries">
|
||||||
|
${`${this.hass.localize("ui.components.logbook.retrieval_error")}: ${
|
||||||
|
this._error
|
||||||
|
}`}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._logbookEntries === undefined) {
|
||||||
return html`
|
return html`
|
||||||
<div class="container no-entries" .dir=${emitRTLDirection(this._rtl)}>
|
<div class="progress-wrapper">
|
||||||
${this.hass.localize("ui.components.logbook.entries_not_found")}
|
<ha-circular-progress
|
||||||
|
active
|
||||||
|
alt=${this.hass.localize("ui.common.loading")}
|
||||||
|
></ha-circular-progress>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this._logbookEntries.length === 0) {
|
||||||
|
return html`<div class="no-entries">
|
||||||
|
${this.hass.localize("ui.components.logbook.entries_not_found")}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div
|
<ha-logbook-renderer
|
||||||
class="container ha-scrollbar ${classMap({
|
.hass=${this.hass}
|
||||||
narrow: this.narrow,
|
.narrow=${this.narrow}
|
||||||
rtl: this._rtl,
|
.virtualize=${this.virtualize}
|
||||||
"no-name": this.noName,
|
.noIcon=${this.noIcon}
|
||||||
"no-icon": this.noIcon,
|
.noName=${this.noName}
|
||||||
})}"
|
.relativeTime=${this.relativeTime}
|
||||||
@scroll=${this._saveScrollPos}
|
.entries=${this._logbookEntries}
|
||||||
>
|
.traceContexts=${this._traceContexts}
|
||||||
${this.virtualize
|
.userIdToName=${this._userIdToName}
|
||||||
? html`<lit-virtualizer
|
></ha-logbook-renderer>
|
||||||
scroller
|
|
||||||
class="ha-scrollbar"
|
|
||||||
.items=${this.entries}
|
|
||||||
.renderItem=${this._renderLogbookItem}
|
|
||||||
>
|
|
||||||
</lit-virtualizer>`
|
|
||||||
: this.entries.map((item, index) =>
|
|
||||||
this._renderLogbookItem(item, index)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderLogbookItem = (
|
public async refresh(force = false) {
|
||||||
item: LogbookEntry,
|
if (!force && this._logbookEntries === undefined) {
|
||||||
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`
|
|
||||||
<div class="entry-container">
|
|
||||||
${index === 0 ||
|
|
||||||
(item?.when &&
|
|
||||||
previous?.when &&
|
|
||||||
new Date(item.when * 1000).toDateString() !==
|
|
||||||
new Date(previous.when * 1000).toDateString())
|
|
||||||
? html`
|
|
||||||
<h4 class="date">
|
|
||||||
${formatDate(new Date(item.when * 1000), this.hass.locale)}
|
|
||||||
</h4>
|
|
||||||
`
|
|
||||||
: html``}
|
|
||||||
|
|
||||||
<div class="entry ${classMap({ "no-entity": !item.entity_id })}">
|
|
||||||
<div class="icon-message">
|
|
||||||
${!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`
|
|
||||||
<state-badge
|
|
||||||
.hass=${this.hass}
|
|
||||||
.overrideIcon=${item.icon ||
|
|
||||||
(item.domain && !stateObj
|
|
||||||
? domainIcon(item.domain!)
|
|
||||||
: undefined)}
|
|
||||||
.overrideImage=${DOMAINS_WITH_DYNAMIC_PICTURE.has(domain)
|
|
||||||
? ""
|
|
||||||
: stateObj?.attributes.entity_picture_local ||
|
|
||||||
stateObj?.attributes.entity_picture}
|
|
||||||
.stateObj=${stateObj}
|
|
||||||
.stateColor=${false}
|
|
||||||
></state-badge>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
<div class="message-relative_time">
|
|
||||||
<div class="message">
|
|
||||||
${!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
|
|
||||||
)}`
|
|
||||||
: ""}
|
|
||||||
</div>
|
|
||||||
<div class="secondary">
|
|
||||||
<span
|
|
||||||
>${formatTimeWithSeconds(
|
|
||||||
new Date(item.when * 1000),
|
|
||||||
this.hass.locale
|
|
||||||
)}</span
|
|
||||||
>
|
|
||||||
-
|
|
||||||
<ha-relative-time
|
|
||||||
.hass=${this.hass}
|
|
||||||
.datetime=${item.when * 1000}
|
|
||||||
capitalize
|
|
||||||
></ha-relative-time>
|
|
||||||
${["script", "automation"].includes(item.domain!) &&
|
|
||||||
item.context_id! in this.traceContexts
|
|
||||||
? html`
|
|
||||||
-
|
|
||||||
<a
|
|
||||||
href=${`/config/${
|
|
||||||
this.traceContexts[item.context_id!].domain
|
|
||||||
}/trace/${
|
|
||||||
this.traceContexts[item.context_id!].domain ===
|
|
||||||
"script"
|
|
||||||
? `script.${
|
|
||||||
this.traceContexts[item.context_id!].item_id
|
|
||||||
}`
|
|
||||||
: this.traceContexts[item.context_id!].item_id
|
|
||||||
}?run_id=${
|
|
||||||
this.traceContexts[item.context_id!].run_id
|
|
||||||
}`}
|
|
||||||
@click=${this._close}
|
|
||||||
>${this.hass.localize(
|
|
||||||
"ui.components.logbook.show_trace"
|
|
||||||
)}</a
|
|
||||||
>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
};
|
|
||||||
|
|
||||||
@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`<button
|
|
||||||
class="link"
|
|
||||||
@click=${this._entityClicked}
|
|
||||||
.entityId=${entityId}
|
|
||||||
>
|
|
||||||
${displayName}
|
|
||||||
</button>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ev.preventDefault();
|
this._throttleGetLogbookEntries.cancel();
|
||||||
ev.stopPropagation();
|
this._updateTraceContexts.cancel();
|
||||||
fireEvent(this, "hass-more-info", {
|
this._updateUsers.cancel();
|
||||||
entityId: entityId,
|
|
||||||
});
|
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 {
|
protected updated(changedProps: PropertyValues): void {
|
||||||
setTimeout(() => fireEvent(this, "closed"), 500);
|
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 [
|
return [
|
||||||
haStyle,
|
|
||||||
haStyleScrollbar,
|
|
||||||
buttonLinkStyle,
|
|
||||||
css`
|
css`
|
||||||
:host([virtualize]) {
|
:host([virtualize]) {
|
||||||
display: block;
|
display: block;
|
||||||
height: 100%;
|
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 {
|
.no-entries {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
padding: 16px;
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
state-badge {
|
.progress-wrapper {
|
||||||
margin-right: 16px;
|
display: flex;
|
||||||
flex-shrink: 0;
|
justify-content: center;
|
||||||
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%;
|
height: 100%;
|
||||||
}
|
align-items: center;
|
||||||
|
|
||||||
lit-virtualizer {
|
|
||||||
contain: size layout !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.narrow .entry {
|
|
||||||
line-height: 1.5;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.narrow .icon-message state-badge {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
@ -12,28 +12,19 @@ import {
|
|||||||
} from "date-fns";
|
} from "date-fns";
|
||||||
import { css, html, LitElement, PropertyValues } from "lit";
|
import { css, html, LitElement, PropertyValues } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
|
||||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||||
import { navigate } from "../../common/navigate";
|
import { navigate } from "../../common/navigate";
|
||||||
import {
|
import {
|
||||||
createSearchParam,
|
createSearchParam,
|
||||||
extractSearchParam,
|
extractSearchParamsObject,
|
||||||
} from "../../common/url/search-params";
|
} from "../../common/url/search-params";
|
||||||
import { computeRTL } from "../../common/util/compute_rtl";
|
import { computeRTL } from "../../common/util/compute_rtl";
|
||||||
import "../../components/entity/ha-entity-picker";
|
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 "../../components/ha-date-range-picker";
|
||||||
import type { DateRangePickerRanges } from "../../components/ha-date-range-picker";
|
import type { DateRangePickerRanges } from "../../components/ha-date-range-picker";
|
||||||
import "../../components/ha-icon-button";
|
import "../../components/ha-icon-button";
|
||||||
import "../../components/ha-menu-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 "../../layouts/ha-app-layout";
|
||||||
import { haStyle } from "../../resources/styles";
|
import { haStyle } from "../../resources/styles";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
@ -45,36 +36,24 @@ export class HaPanelLogbook extends LitElement {
|
|||||||
|
|
||||||
@property({ reflect: true, type: Boolean }) narrow!: boolean;
|
@property({ reflect: true, type: Boolean }) narrow!: boolean;
|
||||||
|
|
||||||
@property() _startDate: Date;
|
@state() _time: { range: [Date, Date] };
|
||||||
|
|
||||||
@property() _endDate: Date;
|
@state() _entityId = "";
|
||||||
|
|
||||||
@property() _entityId = "";
|
|
||||||
|
|
||||||
@property() _isLoading = false;
|
|
||||||
|
|
||||||
@property() _entries: LogbookEntry[] = [];
|
|
||||||
|
|
||||||
@property({ reflect: true, type: Boolean }) rtl = false;
|
@property({ reflect: true, type: Boolean }) rtl = false;
|
||||||
|
|
||||||
@state() private _ranges?: DateRangePickerRanges;
|
@state() private _ranges?: DateRangePickerRanges;
|
||||||
|
|
||||||
private _fetchUserPromise?: Promise<void>;
|
|
||||||
|
|
||||||
@state() private _userIdToName = {};
|
|
||||||
|
|
||||||
@state() private _traceContexts: TraceContexts = {};
|
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
const start = new Date();
|
const start = new Date();
|
||||||
start.setHours(start.getHours() - 2, 0, 0, 0);
|
start.setHours(start.getHours() - 2, 0, 0, 0);
|
||||||
this._startDate = start;
|
|
||||||
|
|
||||||
const end = new Date();
|
const end = new Date();
|
||||||
end.setHours(end.getHours() + 1, 0, 0, 0);
|
end.setHours(end.getHours() + 1, 0, 0, 0);
|
||||||
this._endDate = end;
|
|
||||||
|
this._time = { range: [start, end] };
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
@ -91,19 +70,15 @@ export class HaPanelLogbook extends LitElement {
|
|||||||
@click=${this._refreshLogbook}
|
@click=${this._refreshLogbook}
|
||||||
.path=${mdiRefresh}
|
.path=${mdiRefresh}
|
||||||
.label=${this.hass!.localize("ui.common.refresh")}
|
.label=${this.hass!.localize("ui.common.refresh")}
|
||||||
.disabled=${this._isLoading}
|
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
</app-toolbar>
|
</app-toolbar>
|
||||||
</app-header>
|
</app-header>
|
||||||
|
|
||||||
${this._isLoading ? html`` : ""}
|
|
||||||
|
|
||||||
<div class="filters">
|
<div class="filters">
|
||||||
<ha-date-range-picker
|
<ha-date-range-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
?disabled=${this._isLoading}
|
.startDate=${this._time.range[0]}
|
||||||
.startDate=${this._startDate}
|
.endDate=${this._time.range[1]}
|
||||||
.endDate=${this._endDate}
|
|
||||||
.ranges=${this._ranges}
|
.ranges=${this._ranges}
|
||||||
@change=${this._dateRangeChanged}
|
@change=${this._dateRangeChanged}
|
||||||
></ha-date-range-picker>
|
></ha-date-range-picker>
|
||||||
@ -114,38 +89,27 @@ export class HaPanelLogbook extends LitElement {
|
|||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
"ui.components.entity.entity-picker.entity"
|
"ui.components.entity.entity-picker.entity"
|
||||||
)}
|
)}
|
||||||
.disabled=${this._isLoading}
|
.entityFilter=${this._entityFilter}
|
||||||
@change=${this._entityPicked}
|
@change=${this._entityPicked}
|
||||||
></ha-entity-picker>
|
></ha-entity-picker>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${this._isLoading
|
<ha-logbook
|
||||||
? html`
|
.hass=${this.hass}
|
||||||
<div class="progress-wrapper">
|
.time=${this._time}
|
||||||
<ha-circular-progress
|
.entityId=${this._entityId}
|
||||||
active
|
virtualize
|
||||||
alt=${this.hass.localize("ui.common.loading")}
|
></ha-logbook>
|
||||||
></ha-circular-progress>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<ha-logbook
|
|
||||||
.hass=${this.hass}
|
|
||||||
.entries=${this._entries}
|
|
||||||
.userIdToName=${this._userIdToName}
|
|
||||||
.traceContexts=${this._traceContexts}
|
|
||||||
virtualize
|
|
||||||
></ha-logbook>
|
|
||||||
`}
|
|
||||||
</ha-app-layout>
|
</ha-app-layout>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected firstUpdated(changedProps: PropertyValues) {
|
protected willUpdate(changedProps: PropertyValues) {
|
||||||
super.firstUpdated(changedProps);
|
super.willUpdate(changedProps);
|
||||||
this.hass.loadBackendTranslation("title");
|
|
||||||
|
|
||||||
this._fetchUserPromise = this._fetchUserNames();
|
if (this.hasUpdated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const weekStart = startOfWeek(today);
|
const weekStart = startOfWeek(today);
|
||||||
@ -164,151 +128,125 @@ export class HaPanelLogbook extends LitElement {
|
|||||||
[addDays(weekStart, -7), addDays(weekEnd, -7)],
|
[addDays(weekStart, -7), addDays(weekEnd, -7)],
|
||||||
};
|
};
|
||||||
|
|
||||||
this._entityId = extractSearchParam("entity_id") ?? "";
|
this._applyURLParams();
|
||||||
|
|
||||||
const startDate = extractSearchParam("start_date");
|
|
||||||
if (startDate) {
|
|
||||||
this._startDate = new Date(startDate);
|
|
||||||
}
|
|
||||||
const endDate = extractSearchParam("end_date");
|
|
||||||
if (endDate) {
|
|
||||||
this._endDate = new Date(endDate);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues<this>) {
|
protected firstUpdated(changedProps: PropertyValues) {
|
||||||
if (
|
super.firstUpdated(changedProps);
|
||||||
changedProps.has("_startDate") ||
|
this.hass.loadBackendTranslation("title");
|
||||||
changedProps.has("_endDate") ||
|
}
|
||||||
changedProps.has("_entityId")
|
|
||||||
) {
|
|
||||||
this._getData();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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<this>) {
|
||||||
if (changedProps.has("hass")) {
|
if (changedProps.has("hass")) {
|
||||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||||
if (!oldHass || oldHass.language !== this.hass.language) {
|
if (!oldHass || oldHass.language !== this.hass.language) {
|
||||||
this.rtl = computeRTL(this.hass);
|
this.rtl = computeRTL(this.hass);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._applyURLParams();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _fetchUserNames() {
|
private _applyURLParams() {
|
||||||
const userIdToName = {};
|
const searchParams = new URLSearchParams(location.search);
|
||||||
|
|
||||||
// Start loading users
|
if (searchParams.has("entity_id")) {
|
||||||
const userProm = this.hass.user?.is_admin && fetchUsers(this.hass);
|
this._entityId = searchParams.get("entity_id") ?? "";
|
||||||
|
|
||||||
// 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;
|
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) {
|
private _dateRangeChanged(ev) {
|
||||||
this._startDate = ev.detail.startDate;
|
const startDate = ev.detail.startDate;
|
||||||
const endDate = ev.detail.endDate;
|
const endDate = ev.detail.endDate;
|
||||||
if (endDate.getHours() === 0 && endDate.getMinutes() === 0) {
|
if (endDate.getHours() === 0 && endDate.getMinutes() === 0) {
|
||||||
endDate.setDate(endDate.getDate() + 1);
|
endDate.setDate(endDate.getDate() + 1);
|
||||||
endDate.setMilliseconds(endDate.getMilliseconds() - 1);
|
endDate.setMilliseconds(endDate.getMilliseconds() - 1);
|
||||||
}
|
}
|
||||||
this._endDate = endDate;
|
this._time = { range: [startDate, endDate] };
|
||||||
|
this._updatePath({
|
||||||
this._updatePath();
|
start_date: this._time.range[0].toISOString(),
|
||||||
|
end_date: this._time.range[1].toISOString(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _entityPicked(ev) {
|
private _entityPicked(ev) {
|
||||||
this._entityId = ev.target.value;
|
this._entityId = ev.target.value;
|
||||||
|
this._updatePath({ entity_id: this._entityId });
|
||||||
this._updatePath();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _updatePath() {
|
private _updatePath(update: Record<string, string>) {
|
||||||
const params: Record<string, string> = {};
|
const params = extractSearchParamsObject();
|
||||||
|
for (const [key, value] of Object.entries(update)) {
|
||||||
if (this._entityId) {
|
if (value === undefined) {
|
||||||
params.entity_id = this._entityId;
|
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 });
|
navigate(`/logbook?${createSearchParam(params)}`, { replace: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _refreshLogbook() {
|
private _refreshLogbook() {
|
||||||
this._entries = [];
|
this.shadowRoot!.querySelector("ha-logbook")?.refresh();
|
||||||
clearLogbookCache(
|
|
||||||
this._startDate.toISOString(),
|
|
||||||
this._endDate.toISOString()
|
|
||||||
);
|
|
||||||
this._getData();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _getData() {
|
private _entityFilter: HaEntityPickerEntityFilterFunc = (entity) => {
|
||||||
this._isLoading = true;
|
if (computeStateDomain(entity) !== "sensor") {
|
||||||
let entries;
|
return true;
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._entries = entries;
|
return (
|
||||||
this._traceContexts = traceContexts;
|
entity.attributes.unit_of_measurement === undefined &&
|
||||||
this._isLoading = false;
|
entity.attributes.state_class === undefined
|
||||||
}
|
);
|
||||||
|
};
|
||||||
|
|
||||||
static get styles() {
|
static get styles() {
|
||||||
return [
|
return [
|
||||||
haStyle,
|
haStyle,
|
||||||
css`
|
css`
|
||||||
ha-logbook,
|
ha-logbook {
|
||||||
.progress-wrapper {
|
|
||||||
height: calc(100vh - 136px);
|
height: calc(100vh - 136px);
|
||||||
}
|
}
|
||||||
|
|
||||||
:host([narrow]) ha-logbook,
|
:host([narrow]) ha-logbook {
|
||||||
:host([narrow]) .progress-wrapper {
|
|
||||||
height: calc(100vh - 198px);
|
height: calc(100vh - 198px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -321,17 +259,6 @@ export class HaPanelLogbook extends LitElement {
|
|||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-wrapper {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-circular-progress {
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.filters {
|
.filters {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
|
@ -9,15 +9,11 @@ import {
|
|||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
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 { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||||
import { throttle } from "../../../common/util/throttle";
|
|
||||||
import "../../../components/ha-card";
|
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 type { HomeAssistant } from "../../../types";
|
||||||
import "../../logbook/ha-logbook";
|
import "../../logbook/ha-logbook";
|
||||||
|
import type { HaLogbook } from "../../logbook/ha-logbook";
|
||||||
import { findEntities } from "../common/find-entities";
|
import { findEntities } from "../common/find-entities";
|
||||||
import { processConfigEntities } from "../common/process-config-entities";
|
import { processConfigEntities } from "../common/process-config-entities";
|
||||||
import "../components/hui-warning";
|
import "../components/hui-warning";
|
||||||
@ -56,21 +52,9 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
|
|||||||
|
|
||||||
@state() private _config?: LogbookCardConfig;
|
@state() private _config?: LogbookCardConfig;
|
||||||
|
|
||||||
@state() private _logbookEntries?: LogbookEntry[];
|
@state() private _time?: HaLogbook["time"];
|
||||||
|
|
||||||
@state() private _configEntities?: EntityConfig[];
|
@state() private _entityId?: string[];
|
||||||
|
|
||||||
@state() private _userIdToName = {};
|
|
||||||
|
|
||||||
private _lastLogbookDate?: Date;
|
|
||||||
|
|
||||||
private _fetchUserPromise?: Promise<void>;
|
|
||||||
|
|
||||||
private _error?: string;
|
|
||||||
|
|
||||||
private _throttleGetLogbookEntries = throttle(() => {
|
|
||||||
this._getLogBookData();
|
|
||||||
}, 10000);
|
|
||||||
|
|
||||||
public getCardSize(): number {
|
public getCardSize(): number {
|
||||||
return 9 + (this._config?.title ? 1 : 0);
|
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");
|
throw new Error("Entities must be specified");
|
||||||
}
|
}
|
||||||
|
|
||||||
this._configEntities = processConfigEntities<EntityConfig>(config.entities);
|
|
||||||
|
|
||||||
this._config = {
|
this._config = {
|
||||||
hours_to_show: 24,
|
hours_to_show: 24,
|
||||||
...config,
|
...config,
|
||||||
};
|
};
|
||||||
}
|
this._time = {
|
||||||
|
recent: this._config!.hours_to_show! * 60 * 60 * 1000,
|
||||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
};
|
||||||
if (
|
this._entityId = processConfigEntities<EntityConfig>(config.entities).map(
|
||||||
changedProps.has("_config") ||
|
(entity) => entity.entity
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updated(changedProperties: PropertyValues) {
|
protected updated(changedProperties: PropertyValues) {
|
||||||
@ -139,33 +94,6 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
|
|||||||
) {
|
) {
|
||||||
applyThemesOnElement(this, this.hass.themes, this._config.theme);
|
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 {
|
protected render(): TemplateResult {
|
||||||
@ -189,116 +117,19 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
|
|||||||
class=${classMap({ "no-header": !this._config!.title })}
|
class=${classMap({ "no-header": !this._config!.title })}
|
||||||
>
|
>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
${this._error
|
<ha-logbook
|
||||||
? html`
|
.hass=${this.hass}
|
||||||
<div class="no-entries">
|
.time=${this._time}
|
||||||
${`${this.hass.localize(
|
.entityId=${this._entityId}
|
||||||
"ui.components.logbook.retrieval_error"
|
narrow
|
||||||
)}: ${this._error}`}
|
relative-time
|
||||||
</div>
|
virtualize
|
||||||
`
|
></ha-logbook>
|
||||||
: !this._logbookEntries
|
|
||||||
? html`
|
|
||||||
<ha-circular-progress
|
|
||||||
active
|
|
||||||
alt=${this.hass.localize("ui.common.loading")}
|
|
||||||
></ha-circular-progress>
|
|
||||||
`
|
|
||||||
: this._logbookEntries.length
|
|
||||||
? html`
|
|
||||||
<ha-logbook
|
|
||||||
narrow
|
|
||||||
relative-time
|
|
||||||
virtualize
|
|
||||||
.hass=${this.hass}
|
|
||||||
.entries=${this._logbookEntries}
|
|
||||||
.userIdToName=${this._userIdToName}
|
|
||||||
></ha-logbook>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<div class="no-entries">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.components.logbook.entries_not_found"
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
`}
|
|
||||||
</div>
|
</div>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
css`
|
css`
|
||||||
@ -317,21 +148,10 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
|
|||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-entries {
|
|
||||||
text-align: center;
|
|
||||||
padding: 16px;
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-logbook {
|
ha-logbook {
|
||||||
height: 385px;
|
height: 385px;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-circular-progress {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user