Logbook Card (#6976)

This commit is contained in:
Zack Barett 2020-11-11 04:49:56 -06:00 committed by GitHub
parent 67707fbc90
commit 78f1bb3b91
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 479 additions and 2 deletions

View File

@ -22,7 +22,7 @@ import "../../components/ha-circular-progress";
import "../../components/ha-icon";
import "../../components/ha-relative-time";
import { LogbookEntry } from "../../data/logbook";
import { haStyle } from "../../resources/styles";
import { haStyle, haStyleScrollbar } from "../../resources/styles";
import { HomeAssistant } from "../../types";
@customElement("ha-logbook")
@ -81,7 +81,7 @@ class HaLogbook extends LitElement {
return html`
<div
class="container ${classMap({
class="container ha-scrollbar ${classMap({
narrow: this.narrow,
rtl: this._rtl,
"no-name": this.noName,
@ -225,6 +225,7 @@ class HaLogbook extends LitElement {
static get styles(): CSSResultArray {
return [
haStyle,
haStyleScrollbar,
css`
:host {
display: block;

View File

@ -0,0 +1,310 @@
import {
css,
CSSResultArray,
customElement,
html,
internalProperty,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { throttle } from "../../../common/util/throttle";
import "../../../components/ha-card";
import "../../../components/ha-circular-progress";
import { getLogbookData, LogbookEntry } from "../../../data/logbook";
import type { HomeAssistant } from "../../../types";
import "../../logbook/ha-logbook";
import { findEntities } from "../common/find-entites";
import { processConfigEntities } from "../common/process-config-entities";
import "../components/hui-warning";
import type { EntityConfig } from "../entity-rows/types";
import type { LovelaceCard, LovelaceCardEditor } from "../types";
import type { LogbookCardConfig } from "./types";
@customElement("hui-logbook-card")
export class HuiLogbookCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(
/* webpackChunkName: "hui-logbook-card-editor" */ "../editor/config-elements/hui-logbook-card-editor"
);
return document.createElement("hui-logbook-card-editor");
}
public static getStubConfig(
hass: HomeAssistant,
entities: string[],
entitiesFill: string[]
) {
const includeDomains = ["light", "switch"];
const maxEntities = 3;
const foundEntities = findEntities(
hass,
maxEntities,
entities,
entitiesFill,
includeDomains
);
return {
entity: foundEntities,
};
}
@property({ attribute: false }) public hass?: HomeAssistant;
@internalProperty() private _config?: LogbookCardConfig;
@internalProperty() private _logbookEntries?: LogbookEntry[];
@internalProperty() private _persons = {};
@internalProperty() private _configEntities?: EntityConfig[];
private _lastLogbookDate?: Date;
private _throttleGetLogbookEntries = throttle(() => {
this._getLogBookData();
}, 10000);
public getCardSize(): number {
return 9 + (this._config?.title ? 1 : 0);
}
public setConfig(config: LogbookCardConfig): void {
this._configEntities = processConfigEntities<EntityConfig>(config.entities);
this._config = {
hours_to_show: 24,
...config,
};
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
if (
changedProps.has("_config") ||
changedProps.has("_persons") ||
changedProps.has("_logbookEntries")
) {
return true;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (
!this._configEntities ||
!oldHass ||
oldHass.themes !== this.hass!.themes ||
oldHass.language !== this.hass!.language
) {
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._fetchPersonNames();
}
protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (!this._config || !this.hass) {
return;
}
const configChanged = changedProperties.has("_config");
const hassChanged = changedProperties.has("hass");
const oldHass = changedProperties.get("hass") as HomeAssistant | undefined;
const oldConfig = changedProperties.get("_config") as LogbookCardConfig;
if (
(hassChanged && oldHass?.themes !== this.hass.themes) ||
(configChanged && oldConfig?.theme !== 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 {
if (!this.hass || !this._config) {
return html``;
}
if (!isComponentLoaded(this.hass, "logbook")) {
return html`
<hui-warning>
${this.hass.localize(
"ui.components.logbook.component_not_loaded"
)}</hui-warning
>
`;
}
return html`
<ha-card
.header=${this._config!.title}
class=${classMap({ "no-header": !this._config!.title })}
>
<div class="content">
${!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._persons}
></ha-logbook>
`
: html`
<div class="no-entries">
${this.hass.localize(
"ui.components.logbook.entries_not_found"
)}
</div>
`}
</div>
</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();
const newEntries = await getLogbookData(
this.hass,
lastDate.toISOString(),
now.toISOString(),
this._configEntities!.map((entity) => entity.entity).toString(),
true
);
const logbookEntries = this._logbookEntries
? [...newEntries, ...this._logbookEntries]
: newEntries;
this._logbookEntries = logbookEntries.filter(
(logEntry) => new Date(logEntry.when) > hoursToShowDate
);
this._lastLogbookDate = now;
}
private _fetchPersonNames() {
if (!this.hass) {
return;
}
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(): CSSResultArray {
return [
css`
ha-card {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.content {
padding: 0 16px 16px;
}
.no-header .content {
padding-top: 16px;
}
.no-entries {
text-align: center;
padding: 16px;
color: var(--secondary-text-color);
}
ha-logbook {
height: 385px;
overflow: auto;
display: block;
}
ha-circular-progress {
display: flex;
justify-content: center;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-logbook-card": HuiLogbookCard;
}
}

View File

@ -169,6 +169,13 @@ export interface LightCardConfig extends LovelaceCardConfig {
double_tap_action?: ActionConfig;
}
export interface LogbookCardConfig extends LovelaceCardConfig {
type: "logbook";
entities: string[];
title?: string;
hours_to_show?: number;
}
export interface MapCardConfig extends LovelaceCardConfig {
type: "map";
title?: string;

View File

@ -55,6 +55,7 @@ const LAZY_LOAD_TYPES = {
markdown: () => import("../cards/hui-markdown-card"),
picture: () => import("../cards/hui-picture-card"),
calendar: () => import("../cards/hui-calendar-card"),
logbook: () => import("../cards/hui-logbook-card"),
};
// This will not return an error card but will throw the error

View File

@ -0,0 +1,155 @@
import "@polymer/paper-input/paper-input";
import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { array, assert, number, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/entity/ha-entities-picker";
import "../../../../components/entity/ha-entity-picker";
import { HomeAssistant } from "../../../../types";
import { LogbookCardConfig } from "../../cards/types";
import "../../components/hui-entity-editor";
import "../../components/hui-theme-select-editor";
import { LovelaceCardEditor } from "../../types";
import { EditorTarget } from "../types";
import { configElementStyle } from "./config-elements-style";
const cardConfigStruct = object({
type: string(),
entities: optional(array(string())),
title: optional(string()),
hours_to_show: optional(number()),
theme: optional(string()),
});
@customElement("hui-logbook-card-editor")
export class HuiLogbookCardEditor extends LitElement
implements LovelaceCardEditor {
@property({ attribute: false }) public hass?: HomeAssistant;
@internalProperty() private _config?: LogbookCardConfig;
@internalProperty() private _configEntities?: string[];
public setConfig(config: LogbookCardConfig): void {
assert(config, cardConfigStruct);
this._config = config;
this._configEntities = config.entities;
}
get _title(): string {
return this._config!.title || "";
}
get _entities(): string[] {
return this._config!.entities || [];
}
get _hours_to_show(): number {
return this._config!.hours_to_show || 24;
}
get _theme(): string {
return this._config!.theme || "";
}
protected render(): TemplateResult {
if (!this.hass || !this._config) {
return html``;
}
return html`
<div class="card-config">
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.title"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._title}
.configValue=${"title"}
@value-changed=${this._valueChanged}
></paper-input>
<div class="side-by-side">
<hui-theme-select-editor
.hass=${this.hass}
.value=${this._theme}
.configValue=${"theme"}
@value-changed=${this._valueChanged}
></hui-theme-select-editor>
<paper-input
type="number"
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.hours_to_show"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._hours_to_show}
.configValue=${"hours_to_show"}
@value-changed=${this._valueChanged}
></paper-input>
</div>
<h3>
${`${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.entities"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.required"
)})`}
</h3>
<ha-entities-picker
.hass=${this.hass}
.value=${this._configEntities}
@value-changed=${this._valueChanged}
>
</ha-entities-picker>
</div>
`;
}
private _valueChanged(ev: CustomEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
if (this[`_${target.configValue}`] === target.value) {
return;
}
if (ev.detail && ev.detail.value && Array.isArray(ev.detail.value)) {
this._config = { ...this._config, entities: ev.detail.value };
} else if (target.configValue) {
if (target.value === "") {
this._config = { ...this._config };
delete this._config[target.configValue!];
} else {
let value: any = target.value;
if (target.type === "number") {
value = Number(value);
}
this._config = {
...this._config,
[target.configValue!]: value,
};
}
}
fireEvent(this, "config-changed", { config: this._config });
}
static get styles(): CSSResult {
return configElementStyle;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-logbook-card-editor": HuiLogbookCardEditor;
}
}

View File

@ -100,6 +100,9 @@ export const coreCards: Card[] = [
{
type: "iframe",
},
{
type: "logbook",
},
{
type: "vertical-stack",
},