diff --git a/src/panels/lovelace/cards/hui-entity-card.ts b/src/panels/lovelace/cards/hui-entity-card.ts new file mode 100644 index 0000000000..c3f33765c9 --- /dev/null +++ b/src/panels/lovelace/cards/hui-entity-card.ts @@ -0,0 +1,242 @@ +import { + html, + LitElement, + PropertyValues, + TemplateResult, + customElement, + property, + css, + CSSResult, +} from "lit-element"; + +import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; +import { computeStateName } from "../../../common/entity/compute_state_name"; +import { stateIcon } from "../../../common/entity/state_icon"; + +import "../../../components/ha-card"; +import "../../../components/ha-icon"; +import "../components/hui-warning"; + +import { + LovelaceCard, + LovelaceCardEditor, + LovelaceHeaderFooter, +} from "../types"; +import { HomeAssistant } from "../../../types"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { EntityCardConfig } from "./types"; +import { hasConfigOrEntityChanged } from "../common/has-changed"; +import { actionHandler } from "../common/directives/action-handler-directive"; +import { isValidEntityId } from "../../../common/entity/valid_entity_id"; +import { findEntities } from "../common/find-entites"; +import { createHeaderFooterElement } from "../create-element/create-header-footer-element"; +import { UNKNOWN, UNAVAILABLE } from "../../../data/entity"; +import { HuiErrorCard } from "./hui-error-card"; + +@customElement("hui-entity-card") +class HuiEntityCard extends LitElement implements LovelaceCard { + public static async getConfigElement(): Promise { + await import( + /* webpackChunkName: "hui-entity-card-editor" */ "../editor/config-elements/hui-entity-card-editor" + ); + return document.createElement("hui-entity-card-editor"); + } + + public static getStubConfig( + hass: HomeAssistant, + entities: string[], + entitiesFill: string[] + ) { + const includeDomains = ["sensor", "light", "switch"]; + const maxEntities = 1; + const foundEntities = findEntities( + hass, + maxEntities, + entities, + entitiesFill, + includeDomains + ); + + return { + entity: foundEntities[0] || "", + }; + } + + @property() public hass?: HomeAssistant; + @property() private _config?: EntityCardConfig; + private _footerElement?: HuiErrorCard | LovelaceHeaderFooter; + + public setConfig(config: EntityCardConfig): void { + if (config.entity && !isValidEntityId(config.entity)) { + throw new Error("Invalid Entity"); + } + + this._config = config; + + if (this._config.footer) { + this._footerElement = createHeaderFooterElement(this._config.footer); + } else if (this._footerElement) { + this._footerElement = undefined; + } + } + + public getCardSize(): number { + return 1 + (this._config?.footer ? 1 : 0); + } + + protected render(): TemplateResult { + if (!this._config || !this.hass) { + return html``; + } + + const stateObj = this.hass.states[this._config.entity]; + + if (!stateObj) { + return html` + ${this.hass.localize( + "ui.panel.lovelace.warning.entity_not_found", + "entity", + this._config.entity + )} + `; + } + + const showUnit = this._config.attribute + ? this._config.attribute in stateObj.attributes + : stateObj.state !== UNKNOWN && stateObj.state !== UNAVAILABLE; + + return html` + +
+
+
+ ${this._config.name || computeStateName(stateObj)} +
+
+ +
+
+
+ ${"attribute" in this._config + ? stateObj.attributes[this._config.attribute!] || + this.hass.localize("state.default.unknown") + : this.hass.localize(`state.default.${stateObj.state}`) || + this.hass.localize( + `state.${this._config.entity.split(".")[0]}.${ + stateObj.state + }` + ) || + stateObj.state}${showUnit + ? html` + ${this._config.unit || + (this._config.attribute + ? "" + : stateObj.attributes.unit_of_measurement)} + ` + : ""} +
+
+ ${this._footerElement} +
+ `; + } + + protected shouldUpdate(changedProps: PropertyValues): boolean { + // Side Effect used to update footer hass while keeping optimizations + if (this._footerElement) { + this._footerElement.hass = this.hass; + } + + return hasConfigOrEntityChanged(this, changedProps); + } + + protected updated(changedProps: PropertyValues) { + super.updated(changedProps); + if (!this._config || !this.hass) { + return; + } + + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + const oldConfig = changedProps.get("_config") as + | EntityCardConfig + | undefined; + + if ( + !oldHass || + !oldConfig || + oldHass.themes !== this.hass.themes || + oldConfig.theme !== this._config.theme + ) { + applyThemesOnElement(this, this.hass.themes, this._config!.theme); + } + } + + private _handleClick(): void { + fireEvent(this, "hass-more-info", { entityId: this._config!.entity }); + } + + static get styles(): CSSResult { + return css` + ha-card > div { + cursor: pointer; + } + + .header { + display: flex; + padding: 8px 16px 0; + justify-content: space-between; + } + + .name { + color: var(--secondary-text-color); + line-height: 40px; + font-weight: 500; + font-size: 16px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .icon { + color: var(--state-icon-color, #44739e); + line-height: 40px; + } + + .info { + padding: 0px 16px 16px; + margin-top: -4px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .value { + font-size: 28px; + margin-right: 4px; + } + + .measurement { + font-size: 18px; + color: var(--secondary-text-color); + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-entity-card": HuiEntityCard; + } +} diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index e00b58b83f..4460b8dc09 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -22,6 +22,11 @@ export interface EmptyStateCardConfig extends LovelaceCardConfig { title?: string; } +export interface EntityCardConfig extends LovelaceCardConfig { + attribute?: string; + unit?: string; +} + export interface EntitiesCardEntityConfig extends EntityConfig { type?: string; secondary_info?: "entity-id" | "last-changed"; diff --git a/src/panels/lovelace/create-element/create-card-element.ts b/src/panels/lovelace/create-element/create-card-element.ts index 2fab4496c0..3a2ed953d0 100644 --- a/src/panels/lovelace/create-element/create-card-element.ts +++ b/src/panels/lovelace/create-element/create-card-element.ts @@ -1,4 +1,5 @@ import "../cards/hui-entities-card"; +import "../cards/hui-entity-card"; import "../cards/hui-button-card"; import "../cards/hui-entity-button-card"; import "../cards/hui-glance-card"; @@ -16,6 +17,7 @@ import { } from "./create-element-base"; const ALWAYS_LOADED_TYPES = new Set([ + "entity", "entities", "button", "entity-button", diff --git a/src/panels/lovelace/editor/card-editor/hui-card-picker.ts b/src/panels/lovelace/editor/card-editor/hui-card-picker.ts index 78e279b095..374c650016 100644 --- a/src/panels/lovelace/editor/card-editor/hui-card-picker.ts +++ b/src/panels/lovelace/editor/card-editor/hui-card-picker.ts @@ -28,6 +28,7 @@ const previewCards: string[] = [ "alarm-panel", "button", "entities", + "entity", "gauge", "glance", "history-graph", 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 new file mode 100644 index 0000000000..7352d3e75e --- /dev/null +++ b/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts @@ -0,0 +1,189 @@ +import { + html, + LitElement, + TemplateResult, + customElement, + property, +} from "lit-element"; +import "@polymer/paper-input/paper-input"; + +import "../../components/hui-action-editor"; +import "../../components/hui-theme-select-editor"; +import "../../components/hui-entity-editor"; + +import { struct } from "../../common/structs/struct"; +import { EntitiesEditorEvent, EditorTarget } from "../types"; +import { HomeAssistant } from "../../../../types"; +import { LovelaceCardEditor } from "../../types"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import { configElementStyle } from "./config-elements-style"; +import { EntityCardConfig } from "../../cards/types"; +import { headerFooterConfigStructs } from "../../header-footer/types"; + +const cardConfigStruct = struct({ + type: "string", + entity: "string?", + name: "string?", + icon: "string?", + attribute: "string?", + unit: "string?", + theme: "string?", + header: struct.optional(headerFooterConfigStructs), + footer: struct.optional(headerFooterConfigStructs), +}); + +@customElement("hui-entity-card-editor") +export class HuiEntityCardEditor extends LitElement + implements LovelaceCardEditor { + @property() public hass?: HomeAssistant; + + @property() private _config?: EntityCardConfig; + + public setConfig(config: EntityCardConfig): void { + config = cardConfigStruct(config); + this._config = config; + } + + get _entity(): string { + return this._config!.entity || ""; + } + + get _name(): string { + return this._config!.name || ""; + } + + get _icon(): string { + return this._config!.icon || ""; + } + + get _attribute(): string { + return this._config!.attribute || ""; + } + + get _unit(): string { + return this._config!.unit || ""; + } + + get _theme(): string { + return this._config!.theme || "default"; + } + + protected render(): TemplateResult { + if (!this.hass) { + return html``; + } + + return html` + ${configElementStyle} +
+ +
+ + +
+
+ + +
+ +
+ `; + } + + private _valueChanged(ev: EntitiesEditorEvent): void { + if (!this._config || !this.hass) { + return; + } + const target = ev.target! as EditorTarget; + + if ( + this[`_${target.configValue}`] === target.value || + this[`_${target.configValue}`] === target.config + ) { + return; + } + if (target.configValue) { + if (target.value === "") { + delete this._config[target.configValue!]; + } else { + let newValue: string | undefined; + if ( + target.configValue === "icon_height" && + !isNaN(Number(target.value)) + ) { + newValue = `${String(target.value)}px`; + } + this._config = { + ...this._config, + [target.configValue!]: + target.checked !== undefined + ? target.checked + : newValue !== undefined + ? newValue + : target.value + ? target.value + : target.config, + }; + } + } + fireEvent(this, "config-changed", { config: this._config }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-entity-card-editor": HuiEntityCardEditor; + } +} diff --git a/src/translations/en.json b/src/translations/en.json index 51396598d1..277ae98ded 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2021,6 +2021,9 @@ "toggle": "Toggle entities.", "description": "The Entities card is the most common type of card. It groups items together into lists." }, + "entity": { + "name": "Entity" + }, "button": { "name": "Button", "description": "The Button card allows you to add buttons to perform tasks." @@ -2062,6 +2065,7 @@ }, "generic": { "aspect_ratio": "Aspect Ratio", + "attribute": "Attribute", "camera_image": "Camera Entity", "camera_view": "Camera View", "entities": "Entities",