diff --git a/src/panels/lovelace/cards/hui-entity-card.ts b/src/panels/lovelace/cards/hui-entity-card.ts index 6e3e3d4728..374a7f483c 100644 --- a/src/panels/lovelace/cards/hui-entity-card.ts +++ b/src/panels/lovelace/cards/hui-entity-card.ts @@ -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`
${name}
- + ${this._config.show_trend && trend + ? html` +
+ + ${trend}% +
+ ` + : html` + + `}
@@ -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 { + 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; diff --git a/src/panels/lovelace/cards/hui-sensor-card.ts b/src/panels/lovelace/cards/hui-sensor-card.ts index 576856e4c1..519a5aea6c 100644 --- a/src/panels/lovelace/cards/hui-sensor-card.ts +++ b/src/panels/lovelace/cards/hui-sensor-card.ts @@ -51,6 +51,7 @@ class HuiSensorCard extends HuiEntityCard { const entityCardConfig: EntityCardConfig = { ...cardConfig, + hours_to_show, type: "entity", }; diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index ac4c4d6289..6b4cc79314 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -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; diff --git a/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts index 845743d300..9d62f4275c 100644 --- a/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts @@ -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
+ +
+ + + + ${this._show_trend + ? html` + + ` + : ""} +
`; } diff --git a/src/panels/lovelace/editor/config-elements/hui-sensor-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-sensor-card-editor.ts index 811d4a8150..114082a691 100644 --- a/src/panels/lovelace/editor/config-elements/hui-sensor-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-sensor-card-editor.ts @@ -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} > + + + `; } @@ -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 }); diff --git a/src/translations/en.json b/src/translations/en.json index adba7e6f88..28aae171db 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3277,7 +3277,8 @@ }, "entity": { "name": "Entity", - "description": "The Entity card gives you a quick overview of your entity’s state." + "description": "The Entity card gives you a quick overview of your entity’s 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",