mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-01 13:37: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 {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
@ -7,6 +8,7 @@ import {
|
|||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
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 { computeStateName } from "../../../common/entity/compute_state_name";
|
||||||
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
||||||
import { formatNumber } from "../../../common/number/format_number";
|
import { formatNumber } from "../../../common/number/format_number";
|
||||||
|
import { round } from "../../../common/number/round";
|
||||||
import { iconColorCSS } from "../../../common/style/icon_color_css";
|
import { iconColorCSS } from "../../../common/style/icon_color_css";
|
||||||
import "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
import "../../../components/ha-icon";
|
import "../../../components/ha-icon";
|
||||||
import { UNAVAILABLE_STATES } from "../../../data/entity";
|
import { UNAVAILABLE_STATES } from "../../../data/entity";
|
||||||
|
import { fetchRecent } from "../../../data/history";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import { formatAttributeValue } from "../../../util/hass-attributes-util";
|
import { formatAttributeValue } from "../../../util/hass-attributes-util";
|
||||||
import { computeCardSize } from "../common/compute-card-size";
|
import { computeCardSize } from "../common/compute-card-size";
|
||||||
@ -66,8 +70,14 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
|
|||||||
|
|
||||||
@state() private _config?: EntityCardConfig;
|
@state() private _config?: EntityCardConfig;
|
||||||
|
|
||||||
|
@state() private _lastState?: number;
|
||||||
|
|
||||||
private _footerElement?: HuiErrorCard | LovelaceHeaderFooter;
|
private _footerElement?: HuiErrorCard | LovelaceHeaderFooter;
|
||||||
|
|
||||||
|
private _date?: Date;
|
||||||
|
|
||||||
|
private _fetching = false;
|
||||||
|
|
||||||
public setConfig(config: EntityCardConfig): void {
|
public setConfig(config: EntityCardConfig): void {
|
||||||
if (!config.entity) {
|
if (!config.entity) {
|
||||||
throw new Error("Entity must be specified");
|
throw new Error("Entity must be specified");
|
||||||
@ -76,7 +86,10 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
|
|||||||
throw new Error("Invalid entity");
|
throw new Error("Invalid entity");
|
||||||
}
|
}
|
||||||
|
|
||||||
this._config = config;
|
this._config = {
|
||||||
|
hours_to_show: 24,
|
||||||
|
...config,
|
||||||
|
};
|
||||||
|
|
||||||
if (this._config.footer) {
|
if (this._config.footer) {
|
||||||
this._footerElement = createHeaderFooterElement(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);
|
: !UNAVAILABLE_STATES.includes(stateObj.state);
|
||||||
|
|
||||||
const name = this._config.name || computeStateName(stateObj);
|
const name = this._config.name || computeStateName(stateObj);
|
||||||
|
const trend = this._lastState
|
||||||
|
? round((Number(stateObj.state) / this._lastState) * 100, 0)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-card @click=${this._handleClick} tabindex="0">
|
<ha-card @click=${this._handleClick} tabindex="0">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="name" .title=${name}>${name}</div>
|
<div class="name" .title=${name}>${name}</div>
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<ha-state-icon
|
${this._config.show_trend && trend
|
||||||
.icon=${this._config.icon}
|
? html`
|
||||||
.state=${stateObj}
|
<div class="trend ${classMap({ error: trend < 100 })}">
|
||||||
data-domain=${ifDefined(
|
<ha-svg-icon
|
||||||
this._config.state_color ||
|
.path=${trend < 100 ? mdiArrowDown : mdiArrowUp}
|
||||||
(domain === "light" && this._config.state_color !== false)
|
></ha-svg-icon>
|
||||||
? domain
|
${trend}%
|
||||||
: undefined
|
</div>
|
||||||
)}
|
`
|
||||||
data-state=${stateObj ? computeActiveState(stateObj) : ""}
|
: html`
|
||||||
></ha-state-icon>
|
<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>
|
</div>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
@ -177,7 +204,11 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
|
|||||||
|
|
||||||
protected updated(changedProps: PropertyValues) {
|
protected updated(changedProps: PropertyValues) {
|
||||||
super.updated(changedProps);
|
super.updated(changedProps);
|
||||||
if (!this._config || !this.hass) {
|
if (
|
||||||
|
!this._config ||
|
||||||
|
!this.hass ||
|
||||||
|
(this._fetching && !changedProps.has("_config"))
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,12 +225,46 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
|
|||||||
) {
|
) {
|
||||||
applyThemesOnElement(this, this.hass.themes, this._config!.theme);
|
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 {
|
private _handleClick(): void {
|
||||||
fireEvent(this, "hass-more-info", { entityId: this._config!.entity });
|
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 {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
iconColorCSS,
|
iconColorCSS,
|
||||||
@ -234,6 +299,17 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
|
|||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.trend {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--success-color);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend.error {
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
padding: 0px 16px 16px;
|
padding: 0px 16px 16px;
|
||||||
margin-top: -4px;
|
margin-top: -4px;
|
||||||
|
@ -51,6 +51,7 @@ class HuiSensorCard extends HuiEntityCard {
|
|||||||
|
|
||||||
const entityCardConfig: EntityCardConfig = {
|
const entityCardConfig: EntityCardConfig = {
|
||||||
...cardConfig,
|
...cardConfig,
|
||||||
|
hours_to_show,
|
||||||
type: "entity",
|
type: "entity",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -40,6 +40,8 @@ export interface EntityCardConfig extends LovelaceCardConfig {
|
|||||||
unit?: string;
|
unit?: string;
|
||||||
theme?: string;
|
theme?: string;
|
||||||
state_color?: boolean;
|
state_color?: boolean;
|
||||||
|
hours_to_show?: number;
|
||||||
|
show_trend?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EntitiesCardEntityConfig extends EntityConfig {
|
export interface EntitiesCardEntityConfig extends EntityConfig {
|
||||||
@ -357,6 +359,7 @@ export interface SensorCardConfig extends LovelaceCardConfig {
|
|||||||
detail?: number;
|
detail?: number;
|
||||||
theme?: string;
|
theme?: string;
|
||||||
hours_to_show?: number;
|
hours_to_show?: number;
|
||||||
|
show_trend?: boolean;
|
||||||
limits?: {
|
limits?: {
|
||||||
min?: number;
|
min?: number;
|
||||||
max?: number;
|
max?: number;
|
||||||
|
@ -1,7 +1,15 @@
|
|||||||
import "@polymer/paper-input/paper-input";
|
import "@polymer/paper-input/paper-input";
|
||||||
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
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 { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||||
import { domainIcon } from "../../../../common/entity/domain_icon";
|
import { domainIcon } from "../../../../common/entity/domain_icon";
|
||||||
@ -29,6 +37,8 @@ const cardConfigStruct = assign(
|
|||||||
theme: optional(string()),
|
theme: optional(string()),
|
||||||
state_color: optional(boolean()),
|
state_color: optional(boolean()),
|
||||||
footer: optional(headerFooterConfigStructs),
|
footer: optional(headerFooterConfigStructs),
|
||||||
|
hours_to_show: optional(number()),
|
||||||
|
show_trend: optional(boolean()),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -74,6 +84,14 @@ export class HuiEntityCardEditor
|
|||||||
return this._config!.theme || "";
|
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 {
|
protected render(): TemplateResult {
|
||||||
if (!this.hass || !this._config) {
|
if (!this.hass || !this._config) {
|
||||||
return html``;
|
return html``;
|
||||||
@ -167,6 +185,36 @@ export class HuiEntityCardEditor
|
|||||||
</ha-switch>
|
</ha-switch>
|
||||||
</ha-formfield>
|
</ha-formfield>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,15 @@ import "@polymer/paper-item/paper-item";
|
|||||||
import "@polymer/paper-listbox/paper-listbox";
|
import "@polymer/paper-listbox/paper-listbox";
|
||||||
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
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 { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||||
import { domainIcon } from "../../../../common/entity/domain_icon";
|
import { domainIcon } from "../../../../common/entity/domain_icon";
|
||||||
@ -31,6 +39,7 @@ const cardConfigStruct = assign(
|
|||||||
detail: optional(number()),
|
detail: optional(number()),
|
||||||
theme: optional(string()),
|
theme: optional(string()),
|
||||||
hours_to_show: optional(number()),
|
hours_to_show: optional(number()),
|
||||||
|
show_trend: optional(boolean()),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -82,6 +91,10 @@ export class HuiSensorCardEditor
|
|||||||
return this._config!.hours_to_show || "24";
|
return this._config!.hours_to_show || "24";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get _show_trend(): boolean {
|
||||||
|
return this._config!.show_trend || false;
|
||||||
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (!this.hass || !this._config) {
|
if (!this.hass || !this._config) {
|
||||||
return html``;
|
return html``;
|
||||||
@ -193,6 +206,17 @@ export class HuiSensorCardEditor
|
|||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
></paper-input>
|
></paper-input>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -202,15 +226,21 @@ export class HuiSensorCardEditor
|
|||||||
return;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._config = {
|
this._config = {
|
||||||
...this._config,
|
...this._config,
|
||||||
detail: value,
|
[target.configValue!]: value,
|
||||||
};
|
};
|
||||||
|
|
||||||
fireEvent(this, "config-changed", { config: this._config });
|
fireEvent(this, "config-changed", { config: this._config });
|
||||||
|
@ -3277,7 +3277,8 @@
|
|||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"name": "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": {
|
"button": {
|
||||||
"name": "Button",
|
"name": "Button",
|
||||||
@ -3415,7 +3416,8 @@
|
|||||||
"name": "Sensor",
|
"name": "Sensor",
|
||||||
"show_more_detail": "Show more detail",
|
"show_more_detail": "Show more detail",
|
||||||
"graph_type": "Graph Type",
|
"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": {
|
"shopping-list": {
|
||||||
"name": "Shopping List",
|
"name": "Shopping List",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user