More Info: Add History Tab (#6758)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Zack Arnett 2020-09-03 18:28:10 -05:00 committed by GitHub
parent be8812e0af
commit 1431e75f8b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 428 additions and 183 deletions

View File

@ -23,7 +23,8 @@ export const getLogbookData = (
hass: HomeAssistant,
startDate: string,
endDate: string,
entityId?: string
entityId?: string,
entity_matches_only?: boolean
) => {
const ALL_ENTITIES = "*";
@ -51,7 +52,8 @@ export const getLogbookData = (
hass,
startDate,
endDate,
entityId !== ALL_ENTITIES ? entityId : undefined
entityId !== ALL_ENTITIES ? entityId : undefined,
entity_matches_only
).then((entries) => entries.reverse());
return DATA_CACHE[cacheKey][entityId];
};
@ -60,11 +62,13 @@ const getLogbookDataFromServer = async (
hass: HomeAssistant,
startDate: string,
endDate: string,
entityId?: string
entityId?: string,
entity_matches_only?: boolean
) => {
const url = `logbook/${startDate}?end_time=${endDate}${
entityId ? `&entity=${entityId}` : ""
}`;
}${entity_matches_only ? `&entity_matches_only` : ""}`;
return hass.callApi<LogbookEntry[]>("GET", url);
};

View File

@ -1,33 +1,34 @@
import "@material/mwc-button";
import "@material/mwc-icon-button";
import "../../components/ha-header-bar";
import "../../components/ha-dialog";
import "../../components/ha-svg-icon";
import "@material/mwc-tab";
import "@material/mwc-tab-bar";
import { mdiClose, mdiCog, mdiPencil } from "@mdi/js";
import {
css,
customElement,
html,
internalProperty,
LitElement,
property,
} from "lit-element";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { DOMAINS_MORE_INFO_NO_HISTORY } from "../../common/const";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { navigate } from "../../common/navigate";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-dialog";
import "../../components/ha-header-bar";
import "../../components/ha-svg-icon";
import "../../components/state-history-charts";
import { removeEntityRegistryEntry } from "../../data/entity_registry";
import { showEntityEditorDialog } from "../../panels/config/entities/show-dialog-entity-editor";
import "../../panels/logbook/ha-logbook";
import { haStyleDialog } from "../../resources/styles";
import "../../state-summary/state-card-content";
import { HomeAssistant } from "../../types";
import { showConfirmationDialog } from "../generic/show-dialog-box";
import "./more-info-content";
import {
customElement,
LitElement,
property,
internalProperty,
css,
html,
} from "lit-element";
import { haStyleDialog } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import { getRecentWithCache } from "../../data/cached-history";
import { computeDomain } from "../../common/entity/compute_domain";
import { mdiClose, mdiCog, mdiPencil } from "@mdi/js";
import { HistoryResult } from "../../data/history";
const DOMAINS_NO_INFO = ["camera", "configurator"];
const EDITABLE_DOMAINS_WITH_ID = ["scene", "automation"];
@ -43,11 +44,9 @@ export class MoreInfoDialog extends LitElement {
@property({ type: Boolean, reflect: true }) public large = false;
@internalProperty() private _stateHistory?: HistoryResult;
@internalProperty() private _entityId?: string | null;
private _historyRefreshInterval?: number;
@internalProperty() private _currTabIndex = 0;
public showDialog(params: MoreInfoDialogParams) {
this._entityId = params.entityId;
@ -55,21 +54,11 @@ export class MoreInfoDialog extends LitElement {
this.closeDialog();
}
this.large = false;
this._stateHistory = undefined;
if (this._computeShowHistoryComponent(this._entityId)) {
this._getStateHistory();
clearInterval(this._historyRefreshInterval);
this._historyRefreshInterval = window.setInterval(() => {
this._getStateHistory();
}, 60 * 1000);
}
}
public closeDialog() {
this._entityId = undefined;
this._stateHistory = undefined;
clearInterval(this._historyRefreshInterval);
this._historyRefreshInterval = undefined;
this._currTabIndex = 0;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@ -93,11 +82,14 @@ export class MoreInfoDialog extends LitElement {
hideActions
data-domain=${domain}
>
<ha-header-bar slot="heading">
<div slot="heading" class="heading">
<ha-header-bar>
<mwc-icon-button
slot="navigationIcon"
.label=${this.hass.localize("ui.dialogs.more_info_control.dismiss")}
dialogAction="cancel"
.label=${this.hass.localize(
"ui.dialogs.more_info_control.dismiss"
)}
>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
@ -105,7 +97,8 @@ export class MoreInfoDialog extends LitElement {
${computeStateName(stateObj)}
</div>
${this.hass.user!.is_admin
? html`<mwc-icon-button
? html`
<mwc-icon-button
slot="actionItems"
.label=${this.hass.localize(
"ui.dialogs.more_info_control.settings"
@ -113,13 +106,15 @@ export class MoreInfoDialog extends LitElement {
@click=${this._gotoSettings}
>
<ha-svg-icon .path=${mdiCog}></ha-svg-icon>
</mwc-icon-button>`
</mwc-icon-button>
`
: ""}
${this.hass.user!.is_admin &&
((EDITABLE_DOMAINS_WITH_ID.includes(domain) &&
stateObj.attributes.id) ||
EDITABLE_DOMAINS.includes(domain))
? html` <mwc-icon-button
? html`
<mwc-icon-button
slot="actionItems"
.label=${this.hass.localize(
"ui.dialogs.more_info_control.edit"
@ -127,36 +122,49 @@ export class MoreInfoDialog extends LitElement {
@click=${this._gotoEdit}
>
<ha-svg-icon .path=${mdiPencil}></ha-svg-icon>
</mwc-icon-button>`
</mwc-icon-button>
`
: ""}
</ha-header-bar>
${this._computeShowHistoryComponent(entityId)
? html`
<mwc-tab-bar
.activeIndex=${this._currTabIndex}
@MDCTabBar:activated=${this._handleTabChanged}
>
<mwc-tab
.label=${this.hass.localize(
"ui.dialogs.more_info_control.controls"
)}
></mwc-tab>
<mwc-tab
.label=${this.hass.localize(
"ui.dialogs.more_info_control.history"
)}
></mwc-tab>
</mwc-tab-bar>
`
: ""}
</div>
<div class="content">
${this._currTabIndex === 0
? html`
${DOMAINS_NO_INFO.includes(domain)
? ""
: html`
<state-card-content
in-dialog
.stateObj=${stateObj}
.hass=${this.hass}
in-dialog
></state-card-content>
`}
${this._computeShowHistoryComponent(entityId)
? html`
<state-history-charts
.hass=${this.hass}
.historyData=${this._stateHistory}
up-to-now
.isLoadingData=${!this._stateHistory}
></state-history-charts>
`
: ""}
<more-info-content
.stateObj=${stateObj}
.hass=${this.hass}
></more-info-content>
${stateObj.attributes.restored
? html`<p>
? html`
<p>
${this.hass.localize(
"ui.dialogs.more_info_control.restored.not_provided"
)}
@ -170,32 +178,27 @@ export class MoreInfoDialog extends LitElement {
${this.hass.localize(
"ui.dialogs.more_info_control.restored.remove_action"
)}
</mwc-button>`
</mwc-button>
`
: ""}
`
: html`
<ha-more-info-tab-history
.hass=${this.hass}
.entityId=${this._entityId}
></ha-more-info-tab-history>
`}
</div>
</ha-dialog>
`;
}
private _enlarge() {
this.large = !this.large;
protected firstUpdated(): void {
import("./ha-more-info-tab-history");
}
private async _getStateHistory(): Promise<void> {
if (!this._entityId) {
return;
}
this._stateHistory = await getRecentWithCache(
this.hass!,
this._entityId,
{
refresh: 60,
cacheKey: `more_info.${this._entityId}`,
hoursToShow: 24,
},
this.hass!.localize,
this.hass!.language
);
private _enlarge() {
this.large = !this.large;
}
private _computeShowHistoryComponent(entityId) {
@ -243,6 +246,15 @@ export class MoreInfoDialog extends LitElement {
this.closeDialog();
}
private _handleTabChanged(ev: CustomEvent): void {
const newTab = ev.detail.index;
if (newTab === this._currTabIndex) {
return;
}
this._currTabIndex = ev.detail.index;
}
static get styles() {
return [
haStyleDialog,
@ -256,8 +268,6 @@ export class MoreInfoDialog extends LitElement {
--mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--mdc-theme-surface);
flex-shrink: 0;
border-bottom: 1px solid
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
}
@media all and (max-width: 450px), all and (max-height: 500px) {
@ -268,6 +278,11 @@ export class MoreInfoDialog extends LitElement {
}
}
.heading {
border-bottom: 1px solid
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
}
@media all and (min-width: 451px) and (min-height: 501px) {
ha-dialog {
--mdc-dialog-max-width: 90vw;
@ -306,8 +321,7 @@ export class MoreInfoDialog extends LitElement {
--dialog-content-padding: 0;
}
state-card-content,
state-history-charts {
state-card-content {
display: block;
margin-bottom: 16px;
}
@ -315,3 +329,9 @@ export class MoreInfoDialog extends LitElement {
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-more-info-dialog": MoreInfoDialog;
}
}

View File

@ -0,0 +1,164 @@
import {
css,
customElement,
html,
internalProperty,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import "../../components/ha-circular-progress";
import "../../components/state-history-charts";
import { getRecentWithCache } from "../../data/cached-history";
import { HistoryResult } from "../../data/history";
import { getLogbookData, LogbookEntry } from "../../data/logbook";
import "../../panels/logbook/ha-logbook";
import { haStyleDialog } from "../../resources/styles";
import { HomeAssistant } from "../../types";
@customElement("ha-more-info-tab-history")
export class MoreInfoTabHistoryDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public entityId!: string;
@internalProperty() private _stateHistory?: HistoryResult;
@internalProperty() private _entries?: LogbookEntry[];
@internalProperty() private _persons = {};
private _historyRefreshInterval?: number;
protected render(): TemplateResult {
if (!this.entityId) {
return html``;
}
const stateObj = this.hass.states[this.entityId];
if (!stateObj) {
return html``;
}
return html`
<state-history-charts
up-to-now
.hass=${this.hass}
.historyData=${this._stateHistory}
.isLoadingData=${!this._stateHistory}
></state-history-charts>
${!this._entries
? html`
<ha-circular-progress
active
alt=${this.hass.localize("ui.common.loading")}
></ha-circular-progress>
`
: html`
<ha-logbook
narrow
no-icon
no-name
class=${classMap({
"has-entries": Boolean(this._entries?.length),
})}
.hass=${this.hass}
.entries=${this._entries}
.userIdToName=${this._persons}
></ha-logbook>
`}
`;
}
protected firstUpdated(): void {
this._fetchPersonNames();
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!this.entityId) {
clearInterval(this._historyRefreshInterval);
}
if (changedProps.has("entityId")) {
this._stateHistory = undefined;
this._entries = undefined;
this._getStateHistory();
this._getLogBookData();
clearInterval(this._historyRefreshInterval);
this._historyRefreshInterval = window.setInterval(() => {
this._getStateHistory();
}, 60 * 1000);
}
}
private async _getStateHistory(): Promise<void> {
this._stateHistory = await getRecentWithCache(
this.hass!,
this.entityId,
{
refresh: 60,
cacheKey: `more_info.${this.entityId}`,
hoursToShow: 24,
},
this.hass!.localize,
this.hass!.language
);
}
private async _getLogBookData() {
const yesterday = new Date(new Date().getTime() - 24 * 60 * 60 * 1000);
const now = new Date();
this._entries = await getLogbookData(
this.hass,
yesterday.toISOString(),
now.toISOString(),
this.entityId,
true
);
}
private _fetchPersonNames() {
Object.values(this.hass.states).forEach((entity) => {
if (
entity.attributes.user_id &&
computeStateDomain(entity) === "person"
) {
this._persons[entity.attributes.user_id] =
entity.attributes.friendly_name;
}
});
}
static get styles() {
return [
haStyleDialog,
css`
state-history-charts {
display: block;
margin-bottom: 16px;
}
ha-logbook.has-entries {
height: 360px;
}
ha-circular-progress {
display: flex;
justify-content: center;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-more-info-tab-history": MoreInfoTabHistoryDialog;
}
}

View File

@ -1,35 +1,48 @@
import {
css,
CSSResult,
customElement,
eventOptions,
html,
LitElement,
property,
PropertyValues,
TemplateResult,
eventOptions,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { scroll } from "lit-virtualizer";
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 { domainIcon } from "../../common/entity/domain_icon";
import { stateIcon } from "../../common/entity/state_icon";
import { computeRTL, emitRTLDirection } from "../../common/util/compute_rtl";
import "../../components/ha-circular-progress";
import "../../components/ha-icon";
import { LogbookEntry } from "../../data/logbook";
import { HomeAssistant } from "../../types";
import { restoreScroll } from "../../common/decorators/restore-scroll";
@customElement("ha-logbook")
class HaLogbook extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public userIdToName = {};
@property({ attribute: false }) public userIdToName = {};
@property() public entries: LogbookEntry[] = [];
@property({ attribute: false }) public entries: LogbookEntry[] = [];
@property({ attribute: "rtl", type: Boolean, reflect: true })
@property({ type: Boolean, attribute: "narrow" })
public narrow = false;
@property({ attribute: "rtl", type: Boolean })
private _rtl = false;
@property({ type: Boolean, attribute: "no-icon" })
public noIcon = false;
@property({ type: Boolean, attribute: "no-name" })
public noName = false;
// @ts-ignore
@restoreScroll(".container") private _savedScrollPos?: number;
@ -52,14 +65,22 @@ class HaLogbook extends LitElement {
protected render(): TemplateResult {
if (!this.entries?.length) {
return html`
<div class="container" .dir=${emitRTLDirection(this._rtl)}>
<div class="container no-entries" .dir=${emitRTLDirection(this._rtl)}>
${this.hass.localize("ui.panel.logbook.entries_not_found")}
</div>
`;
}
return html`
<div class="container" @scroll=${this._saveScrollPos}>
<div
class="container ${classMap({
narrow: this.narrow,
rtl: this._rtl,
"no-name": this.noName,
"no-icon": this.noIcon,
})}"
@scroll=${this._saveScrollPos}
>
${scroll({
items: this.entries,
renderItem: (item: LogbookEntry, index?: number) =>
@ -76,6 +97,7 @@ class HaLogbook extends LitElement {
if (index === undefined) {
return html``;
}
const previous = this.entries[index - 1];
const state = item.entity_id ? this.hass.states[item.entity_id] : undefined;
const item_username =
@ -98,12 +120,18 @@ class HaLogbook extends LitElement {
<div class="time">
${formatTimeWithSeconds(new Date(item.when), this.hass.language)}
</div>
<div class="icon-message">
${!this.noIcon
? html`
<ha-icon
.icon=${state ? stateIcon(state) : domainIcon(item.domain)}
></ha-icon>
`
: ""}
<div class="message">
${!item.entity_id
? html` <span class="name">${item.name}</span> `
${!this.noName
? !item.entity_id
? html`<span class="name">${item.name}</span>`
: html`
<a
href="#"
@ -112,17 +140,16 @@ class HaLogbook extends LitElement {
class="name"
>${item.name}</a
>
`}
<span
>${item.message}${item_username
? ` (${item_username})`
: ``}</span
>
`
: ""}
<span class="item-message">${item.message}</span>
<span>${item_username ? ` (${item_username})` : ``}</span>
${!item.context_event_type
? ""
: item.context_event_type === "call_service"
? // Service Call
html` by service ${item.context_domain}.${item.context_service}`
html` by service
${item.context_domain}.${item.context_service}`
: item.context_entity_id === item.entity_id
? // HomeKit or something that self references
html` by
@ -141,6 +168,7 @@ class HaLogbook extends LitElement {
</div>
</div>
</div>
</div>
`;
}
@ -163,26 +191,36 @@ class HaLogbook extends LitElement {
height: 100%;
}
:host([rtl]) {
.rtl {
direction: ltr;
}
.entry {
display: flex;
line-height: 2em;
padding-bottom: 8px;
}
.time {
width: 65px;
flex-shrink: 0;
font-size: 0.8em;
font-size: 12px;
color: var(--secondary-text-color);
}
:host([rtl]) .date {
.rtl .date {
direction: rtl;
}
.icon-message {
display: flex;
align-items: center;
}
.no-entries {
text-align: center;
}
ha-icon {
margin: 0 8px 0 16px;
flex-shrink: 0;
@ -193,6 +231,10 @@ class HaLogbook extends LitElement {
color: var(--primary-text-color);
}
.no-name .item-message {
text-transform: capitalize;
}
a {
color: var(--primary-color);
}
@ -212,8 +254,21 @@ class HaLogbook extends LitElement {
.uni-virtualizer-host > * {
box-sizing: border-box;
}
.narrow .entry {
flex-direction: column-reverse;
line-height: 1.5;
}
.narrow .icon-message ha-icon {
margin-left: 0;
}
`;
}
}
customElements.define("ha-logbook", HaLogbook);
declare global {
interface HTMLElementTagNameMap {
"ha-logbook": HaLogbook;
}
}

View File

@ -389,6 +389,8 @@
"dismiss": "Dismiss dialog",
"settings": "Entity settings",
"edit": "Edit entity",
"controls": "Controls",
"history": "History",
"script": {
"last_action": "Last Action",
"last_triggered": "Last Triggered"