Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/panels/logbook/ha-logbook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -225,6 +225,7 @@ class HaLogbook extends LitElement {
static get styles(): CSSResultArray {
return [
haStyle,
haStyleScrollbar,
css`
:host {
display: block;
Expand Down
310 changes: 310 additions & 0 deletions src/panels/lovelace/cards/hui-logbook-card.ts
Original file line number Diff line number Diff line change
@@ -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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we virtualize this list?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idk how to do that :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
}
}
7 changes: 7 additions & 0 deletions src/panels/lovelace/cards/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,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;
Expand Down
1 change: 1 addition & 0 deletions src/panels/lovelace/create-element/create-card-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,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
Expand Down
Loading