mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-31 21:17:47 +00:00
Add Trend to Entity and Sensor Card
This commit is contained in:
parent
05711b4636
commit
e3bbe0e93b
@ -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;
|
||||
|
@ -51,6 +51,7 @@ class HuiSensorCard extends HuiEntityCard {
|
||||
|
||||
const entityCardConfig: EntityCardConfig = {
|
||||
...cardConfig,
|
||||
hours_to_show,
|
||||
type: "entity",
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
`;
|
||||
}
|
||||
|
@ -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 });
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user