mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-19 07:16:39 +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-icon";
|
||||||
import "../../components/ha-relative-time";
|
import "../../components/ha-relative-time";
|
||||||
import { LogbookEntry } from "../../data/logbook";
|
import { LogbookEntry } from "../../data/logbook";
|
||||||
import { haStyle } from "../../resources/styles";
|
import { haStyle, haStyleScrollbar } from "../../resources/styles";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
|
|
||||||
@customElement("ha-logbook")
|
@customElement("ha-logbook")
|
||||||
@ -81,7 +81,7 @@ class HaLogbook extends LitElement {
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
class="container ${classMap({
|
class="container ha-scrollbar ${classMap({
|
||||||
narrow: this.narrow,
|
narrow: this.narrow,
|
||||||
rtl: this._rtl,
|
rtl: this._rtl,
|
||||||
"no-name": this.noName,
|
"no-name": this.noName,
|
||||||
@ -225,6 +225,7 @@ class HaLogbook extends LitElement {
|
|||||||
static get styles(): CSSResultArray {
|
static get styles(): CSSResultArray {
|
||||||
return [
|
return [
|
||||||
haStyle,
|
haStyle,
|
||||||
|
haStyleScrollbar,
|
||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
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;
|
double_tap_action?: ActionConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LogbookCardConfig extends LovelaceCardConfig {
|
||||||
|
type: "logbook";
|
||||||
|
entities: string[];
|
||||||
|
title?: string;
|
||||||
|
hours_to_show?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MapCardConfig extends LovelaceCardConfig {
|
export interface MapCardConfig extends LovelaceCardConfig {
|
||||||
type: "map";
|
type: "map";
|
||||||
title?: string;
|
title?: string;
|
||||||
|
@ -55,6 +55,7 @@ const LAZY_LOAD_TYPES = {
|
|||||||
markdown: () => import("../cards/hui-markdown-card"),
|
markdown: () => import("../cards/hui-markdown-card"),
|
||||||
picture: () => import("../cards/hui-picture-card"),
|
picture: () => import("../cards/hui-picture-card"),
|
||||||
calendar: () => import("../cards/hui-calendar-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
|
// 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: "iframe",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: "logbook",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: "vertical-stack",
|
type: "vertical-stack",
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user