Add Trend to Entity and Sensor Card

This commit is contained in:
Zack Arnett 2021-10-21 10:56:10 -05:00
parent 05711b4636
commit e3bbe0e93b
6 changed files with 180 additions and 20 deletions

View File

@ -1,3 +1,4 @@
import { mdiArrowDown, mdiArrowUp } from "@mdi/js";
import {
css,
CSSResultGroup,
@ -7,6 +8,7 @@ import {
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event";
@ -16,10 +18,12 @@ import { computeStateDomain } from "../../../common/entity/compute_state_domain"
import { computeStateName } from "../../../common/entity/compute_state_name";
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
import { formatNumber } from "../../../common/number/format_number";
import { round } from "../../../common/number/round";
import { iconColorCSS } from "../../../common/style/icon_color_css";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import { UNAVAILABLE_STATES } from "../../../data/entity";
import { fetchRecent } from "../../../data/history";
import { HomeAssistant } from "../../../types";
import { formatAttributeValue } from "../../../util/hass-attributes-util";
import { computeCardSize } from "../common/compute-card-size";
@ -66,8 +70,14 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
@state() private _config?: EntityCardConfig;
@state() private _lastState?: number;
private _footerElement?: HuiErrorCard | LovelaceHeaderFooter;
private _date?: Date;
private _fetching = false;
public setConfig(config: EntityCardConfig): void {
if (!config.entity) {
throw new Error("Entity must be specified");
@ -76,7 +86,10 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
throw new Error("Invalid entity");
}
this._config = config;
this._config = {
hours_to_show: 24,
...config,
};
if (this._config.footer) {
this._footerElement = createHeaderFooterElement(this._config.footer);
@ -115,23 +128,37 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
: !UNAVAILABLE_STATES.includes(stateObj.state);
const name = this._config.name || computeStateName(stateObj);
const trend = this._lastState
? round((Number(stateObj.state) / this._lastState) * 100, 0)
: undefined;
return html`
<ha-card @click=${this._handleClick} tabindex="0">
<div class="header">
<div class="name" .title=${name}>${name}</div>
<div class="icon">
<ha-state-icon
.icon=${this._config.icon}
.state=${stateObj}
data-domain=${ifDefined(
this._config.state_color ||
(domain === "light" && this._config.state_color !== false)
? domain
: undefined
)}
data-state=${stateObj ? computeActiveState(stateObj) : ""}
></ha-state-icon>
${this._config.show_trend && trend
? html`
<div class="trend ${classMap({ error: trend < 100 })}">
<ha-svg-icon
.path=${trend < 100 ? mdiArrowDown : mdiArrowUp}
></ha-svg-icon>
${trend}%
</div>
`
: html`
<ha-icon
.icon=${this._config.icon || stateIcon(stateObj)}
data-domain=${ifDefined(
this._config.state_color ||
(domain === "light" &&
this._config.state_color !== false)
? domain
: undefined
)}
data-state=${stateObj ? computeActiveState(stateObj) : ""}
></ha-icon>
`}
</div>
</div>
<div class="info">
@ -177,7 +204,11 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (!this._config || !this.hass) {
if (
!this._config ||
!this.hass ||
(this._fetching && !changedProps.has("_config"))
) {
return;
}
@ -194,12 +225,46 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
) {
applyThemesOnElement(this, this.hass.themes, this._config!.theme);
}
if (changedProps.has("_config")) {
if (!oldConfig || oldConfig.entity !== this._config.entity) {
this._lastState = undefined;
}
this._getStateHistory();
} else if (Date.now() - this._date!.getTime() >= 60000) {
this._getStateHistory();
}
}
private _handleClick(): void {
fireEvent(this, "hass-more-info", { entityId: this._config!.entity });
}
private async _getStateHistory(): Promise<void> {
if (this._fetching) {
return;
}
this._fetching = true;
const now = new Date();
const startTime = new Date(
new Date().setHours(now.getHours() - this._config!.hours_to_show!)
);
const stateHistory = await fetchRecent(
this.hass!,
this._config!.entity,
startTime,
startTime
);
this._lastState = Number(stateHistory[0][0].state);
this._date = now;
this._fetching = false;
}
static get styles(): CSSResultGroup {
return [
iconColorCSS,
@ -234,6 +299,17 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
line-height: 40px;
}
.trend {
font-size: 16px;
color: var(--success-color);
display: flex;
align-items: center;
}
.trend.error {
color: var(--error-color);
}
.info {
padding: 0px 16px 16px;
margin-top: -4px;

View File

@ -51,6 +51,7 @@ class HuiSensorCard extends HuiEntityCard {
const entityCardConfig: EntityCardConfig = {
...cardConfig,
hours_to_show,
type: "entity",
};

View File

@ -40,6 +40,8 @@ export interface EntityCardConfig extends LovelaceCardConfig {
unit?: string;
theme?: string;
state_color?: boolean;
hours_to_show?: number;
show_trend?: boolean;
}
export interface EntitiesCardEntityConfig extends EntityConfig {
@ -357,6 +359,7 @@ export interface SensorCardConfig extends LovelaceCardConfig {
detail?: number;
theme?: string;
hours_to_show?: number;
show_trend?: boolean;
limits?: {
min?: number;
max?: number;

View File

@ -1,7 +1,15 @@
import "@polymer/paper-input/paper-input";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, boolean, object, optional, string } from "superstruct";
import {
assert,
assign,
boolean,
number,
object,
optional,
string,
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeDomain } from "../../../../common/entity/compute_domain";
import { domainIcon } from "../../../../common/entity/domain_icon";
@ -29,6 +37,8 @@ const cardConfigStruct = assign(
theme: optional(string()),
state_color: optional(boolean()),
footer: optional(headerFooterConfigStructs),
hours_to_show: optional(number()),
show_trend: optional(boolean()),
})
);
@ -74,6 +84,14 @@ export class HuiEntityCardEditor
return this._config!.theme || "";
}
get _hours_to_show(): number | string {
return this._config!.hours_to_show || "24";
}
get _show_trend(): boolean {
return this._config!.show_trend || false;
}
protected render(): TemplateResult {
if (!this.hass || !this._config) {
return html``;
@ -167,6 +185,36 @@ export class HuiEntityCardEditor
</ha-switch>
</ha-formfield>
</div>
<div class="side-by-side">
<ha-formfield
.label=${this.hass.localize(
"ui.panel.lovelace.editor.card.entity.show_trend"
)}
>
<ha-switch
.checked=${this._show_trend}
.configValue=${"show_trend"}
@change=${this._valueChanged}
></ha-switch>
</ha-formfield>
${this._show_trend
? html`
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.hours_to_show"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
type="number"
.value=${this._hours_to_show}
min="1"
.configValue=${"hours_to_show"}
@value-changed=${this._valueChanged}
></paper-input>
`
: ""}
</div>
</div>
`;
}

View File

@ -4,7 +4,15 @@ import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, number, object, optional, string } from "superstruct";
import {
assert,
assign,
boolean,
number,
object,
optional,
string,
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeDomain } from "../../../../common/entity/compute_domain";
import { domainIcon } from "../../../../common/entity/domain_icon";
@ -31,6 +39,7 @@ const cardConfigStruct = assign(
detail: optional(number()),
theme: optional(string()),
hours_to_show: optional(number()),
show_trend: optional(boolean()),
})
);
@ -82,6 +91,10 @@ export class HuiSensorCardEditor
return this._config!.hours_to_show || "24";
}
get _show_trend(): boolean {
return this._config!.show_trend || false;
}
protected render(): TemplateResult {
if (!this.hass || !this._config) {
return html``;
@ -193,6 +206,17 @@ export class HuiSensorCardEditor
@value-changed=${this._valueChanged}
></paper-input>
</div>
<ha-formfield
label=${this.hass.localize(
"ui.panel.lovelace.editor.card.sensor.show_trend"
)}
>
<ha-switch
.checked=${this._show_trend}
.configValue=${"show_trend"}
@change=${this._change}
></ha-switch>
</ha-formfield>
</div>
`;
}
@ -202,15 +226,21 @@ export class HuiSensorCardEditor
return;
}
const value = (ev.target! as EditorTarget).checked ? 2 : 1;
const target = ev.target! as EditorTarget;
const value =
target.configValue === "detail"
? (ev.target! as EditorTarget).checked
? 2
: 1
: target.checked;
if (this._detail === value) {
if (this[`_${target.configValue}`] === value) {
return;
}
this._config = {
...this._config,
detail: value,
[target.configValue!]: value,
};
fireEvent(this, "config-changed", { config: this._config });

View File

@ -3277,7 +3277,8 @@
},
"entity": {
"name": "Entity",
"description": "The Entity card gives you a quick overview of your entitys state."
"description": "The Entity card gives you a quick overview of your entitys state.",
"show_trend": "Show Trend?"
},
"button": {
"name": "Button",
@ -3415,7 +3416,8 @@
"name": "Sensor",
"show_more_detail": "Show more detail",
"graph_type": "Graph Type",
"description": "The Sensor card gives you a quick overview of your sensors state with an optional graph to visualize change over time."
"description": "The Sensor card gives you a quick overview of your sensors state with an optional graph to visualize change over time.",
"show_trend": "Show Trend?"
},
"shopping-list": {
"name": "Shopping List",