diff --git a/src/panels/logbook/ha-logbook.ts b/src/panels/logbook/ha-logbook.ts index 0204f25d48..781e0fca98 100644 --- a/src/panels/logbook/ha-logbook.ts +++ b/src/panels/logbook/ha-logbook.ts @@ -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`
{ + 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(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` + + ${this.hass.localize( + "ui.components.logbook.component_not_loaded" + )} + `; + } + + return html` + +
+ ${!this._logbookEntries + ? html` + + ` + : this._logbookEntries.length + ? html` + + ` + : html` +
+ ${this.hass.localize( + "ui.components.logbook.entries_not_found" + )} +
+ `} +
+
+ `; + } + + private async _getLogBookData() { + if ( + !this.hass || + !this._config || + !isComponentLoaded(this.hass, "logbook") + ) { + return; + } + + const hoursToShowDate = new Date( + new Date().getTime() - this._config!.hours_to_show! * 60 * 60 * 1000 + ); + const lastDate = this._lastLogbookDate || hoursToShowDate; + const now = new Date(); + + 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; + } +} diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index 2c3c9cb6d8..673f4959d4 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -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; diff --git a/src/panels/lovelace/create-element/create-card-element.ts b/src/panels/lovelace/create-element/create-card-element.ts index a10781a209..3f2a398acd 100644 --- a/src/panels/lovelace/create-element/create-card-element.ts +++ b/src/panels/lovelace/create-element/create-card-element.ts @@ -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 diff --git a/src/panels/lovelace/editor/config-elements/hui-logbook-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-logbook-card-editor.ts new file mode 100644 index 0000000000..a0a83acd35 --- /dev/null +++ b/src/panels/lovelace/editor/config-elements/hui-logbook-card-editor.ts @@ -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` +
+ +
+ + +
+

+ ${`${this.hass!.localize( + "ui.panel.lovelace.editor.card.generic.entities" + )} (${this.hass!.localize( + "ui.panel.lovelace.editor.card.config.required" + )})`} +

+ + +
+ `; + } + + 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; + } +} diff --git a/src/panels/lovelace/editor/lovelace-cards.ts b/src/panels/lovelace/editor/lovelace-cards.ts index 5842e566c9..6e29ec6dfd 100644 --- a/src/panels/lovelace/editor/lovelace-cards.ts +++ b/src/panels/lovelace/editor/lovelace-cards.ts @@ -100,6 +100,9 @@ export const coreCards: Card[] = [ { type: "iframe", }, + { + type: "logbook", + }, { type: "vertical-stack", },