mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-18 23:06:40 +00:00
Logbook Card (#6976)
This commit is contained in:
parent
67707fbc90
commit
78f1bb3b91
@ -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;
|
||||
|
310
src/panels/lovelace/cards/hui-logbook-card.ts
Normal file
310
src/panels/lovelace/cards/hui-logbook-card.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -100,6 +100,9 @@ export const coreCards: Card[] = [
|
||||
{
|
||||
type: "iframe",
|
||||
},
|
||||
{
|
||||
type: "logbook",
|
||||
},
|
||||
{
|
||||
type: "vertical-stack",
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user