From a9d44fcb6128207d8b2e43f997b996170b371a39 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 24 Nov 2022 21:23:50 +0100 Subject: [PATCH] Add Text entity (#14447) --- demo/src/custom-cards/card-tools.js | 2 +- gallery/src/pages/lovelace/entities-card.ts | 8 ++ src/common/const.ts | 3 + src/data/text.ts | 19 +++++ src/dialogs/more-info/const.ts | 3 +- .../create-element/create-row-element.ts | 8 +- .../entity-rows/hui-simple-entity-row.ts | 73 +++++++++++++++++ .../entity-rows/hui-text-entity-row.ts | 78 +++++++++++++------ src/state-summary/state-card-content.js | 1 + src/state-summary/state-card-text.ts | 73 +++++++++++++++++ src/translations/en.json | 3 + 11 files changed, 241 insertions(+), 30 deletions(-) create mode 100644 src/data/text.ts create mode 100644 src/panels/lovelace/entity-rows/hui-simple-entity-row.ts create mode 100644 src/state-summary/state-card-text.ts diff --git a/demo/src/custom-cards/card-tools.js b/demo/src/custom-cards/card-tools.js index 27b37cabeb..86f7c597b2 100644 --- a/demo/src/custom-cards/card-tools.js +++ b/demo/src/custom-cards/card-tools.js @@ -138,7 +138,7 @@ if (!window.cardTools) { return cardTools.createThing("row", config); const domain = config.entity.split(".", 1)[0]; - Object.assign(config, { type: DEFAULT_ROWS[domain] || "text" }); + Object.assign(config, { type: DEFAULT_ROWS[domain] || "simple" }); return cardTools.createThing("entity-row", config); }; diff --git a/gallery/src/pages/lovelace/entities-card.ts b/gallery/src/pages/lovelace/entities-card.ts index 6cec33cba5..9aebd67b7d 100644 --- a/gallery/src/pages/lovelace/entities-card.ts +++ b/gallery/src/pages/lovelace/entities-card.ts @@ -98,6 +98,9 @@ const ENTITIES = [ minimum: 0, maximum: 10, }), + getEntity("text", "message", "Hello!", { + friendly_name: "Message", + }), getEntity("light", "unavailable", "unavailable", { friendly_name: "Bed Light", @@ -129,6 +132,9 @@ const ENTITIES = [ friendly_name: "Who cooks", icon: "mdi:cheff", }), + getEntity("text", "unavailable", "unavailable", { + friendly_name: "Message", + }), ]; const CONFIGS = [ @@ -147,6 +153,7 @@ const CONFIGS = [ - climate.ecobee - input_number.number - sensor.humidity + - text.message `, }, { @@ -219,6 +226,7 @@ const CONFIGS = [ - climate.unavailable - input_number.unavailable - input_select.unavailable + - text.unavailable `, }, { diff --git a/src/common/const.ts b/src/common/const.ts index 359eb677da..53ac073b8e 100644 --- a/src/common/const.ts +++ b/src/common/const.ts @@ -114,6 +114,7 @@ export const FIXED_DOMAIN_ICONS = { siren: mdiBullhorn, simple_alarm: mdiBell, sun: mdiWhiteBalanceSunny, + text: mdiFormTextbox, timer: mdiTimerOutline, updater: mdiCloudUpload, vacuum: mdiRobotVacuum, @@ -182,6 +183,7 @@ export const DOMAINS_WITH_CARD = [ "script", "select", "timer", + "text", "vacuum", "water_heater", ]; @@ -214,6 +216,7 @@ export const DOMAINS_INPUT_ROW = [ "script", "select", "switch", + "text", "vacuum", ]; diff --git a/src/data/text.ts b/src/data/text.ts new file mode 100644 index 0000000000..45b501f457 --- /dev/null +++ b/src/data/text.ts @@ -0,0 +1,19 @@ +import { + HassEntityAttributeBase, + HassEntityBase, +} from "home-assistant-js-websocket"; +import { HomeAssistant } from "../types"; + +interface TextEntityAttributes extends HassEntityAttributeBase { + min?: number; + max?: number; + pattern?: string; + mode?: "text" | "password"; +} + +export interface TextEntity extends HassEntityBase { + attributes: TextEntityAttributes; +} + +export const setValue = (hass: HomeAssistant, entity: string, value: string) => + hass.callService("text", "set_value", { value }, { entity_id: entity }); diff --git a/src/dialogs/more-info/const.ts b/src/dialogs/more-info/const.ts index 15ba2da551..30ac679e33 100644 --- a/src/dialogs/more-info/const.ts +++ b/src/dialogs/more-info/const.ts @@ -50,8 +50,9 @@ export const DOMAINS_HIDE_DEFAULT_MORE_INFO = [ "input_text", "number", "scene", - "update", "select", + "text", + "update", ]; /** Domains that should have the history hidden in the more info dialog. */ diff --git a/src/panels/lovelace/create-element/create-row-element.ts b/src/panels/lovelace/create-element/create-row-element.ts index b805707259..71f8a2359e 100644 --- a/src/panels/lovelace/create-element/create-row-element.ts +++ b/src/panels/lovelace/create-element/create-row-element.ts @@ -2,7 +2,7 @@ import "../entity-rows/hui-media-player-entity-row"; import "../entity-rows/hui-scene-entity-row"; import "../entity-rows/hui-script-entity-row"; import "../entity-rows/hui-sensor-entity-row"; -import "../entity-rows/hui-text-entity-row"; +import "../entity-rows/hui-simple-entity-row"; import "../entity-rows/hui-toggle-entity-row"; import { LovelaceRowConfig } from "../entity-rows/types"; import "../special-rows/hui-attribute-row"; @@ -18,7 +18,7 @@ const ALWAYS_LOADED_TYPES = new Set([ "scene-entity", "script-entity", "sensor-entity", - "text-entity", + "simple-entity", "toggle-entity", "button", "call-service", @@ -41,6 +41,7 @@ const LAZY_LOAD_TYPES = { "lock-entity": () => import("../entity-rows/hui-lock-entity-row"), "number-entity": () => import("../entity-rows/hui-number-entity-row"), "select-entity": () => import("../entity-rows/hui-select-entity-row"), + "text-entity": () => import("../entity-rows/hui-text-entity-row"), "timer-entity": () => import("../entity-rows/hui-timer-entity-row"), conditional: () => import("../special-rows/hui-conditional-row"), "weather-entity": () => import("../entity-rows/hui-weather-entity-row"), @@ -53,7 +54,7 @@ const LAZY_LOAD_TYPES = { text: () => import("../special-rows/hui-text-row"), }; const DOMAIN_TO_ELEMENT_TYPE = { - _domain_not_found: "text", + _domain_not_found: "simple", alert: "toggle", automation: "toggle", button: "button", @@ -78,6 +79,7 @@ const DOMAIN_TO_ELEMENT_TYPE = { sensor: "sensor", siren: "toggle", switch: "toggle", + text: "text", timer: "timer", vacuum: "toggle", // Temporary. Once climate is rewritten, diff --git a/src/panels/lovelace/entity-rows/hui-simple-entity-row.ts b/src/panels/lovelace/entity-rows/hui-simple-entity-row.ts new file mode 100644 index 0000000000..e0dde82dd5 --- /dev/null +++ b/src/panels/lovelace/entity-rows/hui-simple-entity-row.ts @@ -0,0 +1,73 @@ +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { computeStateDisplay } from "../../../common/entity/compute_state_display"; +import { HomeAssistant } from "../../../types"; +import { EntitiesCardEntityConfig } from "../cards/types"; +import { hasConfigOrEntityChanged } from "../common/has-changed"; +import "../components/hui-generic-entity-row"; +import { createEntityNotFoundWarning } from "../components/hui-warning"; +import { LovelaceRow } from "./types"; + +@customElement("hui-simple-entity-row") +class HuiSimpleEntityRow extends LitElement implements LovelaceRow { + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() private _config?: EntitiesCardEntityConfig; + + public setConfig(config: EntitiesCardEntityConfig): void { + if (!config) { + throw new Error("Invalid configuration"); + } + this._config = config; + } + + protected shouldUpdate(changedProps: PropertyValues): boolean { + return hasConfigOrEntityChanged(this, changedProps); + } + + protected render(): TemplateResult { + if (!this._config || !this.hass) { + return html``; + } + + const stateObj = this.hass.states[this._config.entity]; + + if (!stateObj) { + return html` + + ${createEntityNotFoundWarning(this.hass, this._config.entity)} + + `; + } + + return html` + + ${computeStateDisplay(this.hass!.localize, stateObj, this.hass.locale)} + + `; + } + + static get styles(): CSSResultGroup { + return css` + div { + text-align: right; + } + .pointer { + cursor: pointer; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-simple-entity-row": HuiSimpleEntityRow; + } +} diff --git a/src/panels/lovelace/entity-rows/hui-text-entity-row.ts b/src/panels/lovelace/entity-rows/hui-text-entity-row.ts index 0c82d55e56..222a788ae3 100644 --- a/src/panels/lovelace/entity-rows/hui-text-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-text-entity-row.ts @@ -1,27 +1,22 @@ -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; +import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { computeStateDisplay } from "../../../common/entity/compute_state_display"; +import { UNAVAILABLE, UNAVAILABLE_STATES } from "../../../data/entity"; +import { TextEntity, setValue } from "../../../data/text"; import { HomeAssistant } from "../../../types"; -import { EntitiesCardEntityConfig } from "../cards/types"; import { hasConfigOrEntityChanged } from "../common/has-changed"; import "../components/hui-generic-entity-row"; import { createEntityNotFoundWarning } from "../components/hui-warning"; -import { LovelaceRow } from "./types"; +import { EntityConfig, LovelaceRow } from "./types"; +import "../../../components/ha-textfield"; +import { computeStateName } from "../../../common/entity/compute_state_name"; @customElement("hui-text-entity-row") class HuiTextEntityRow extends LitElement implements LovelaceRow { @property({ attribute: false }) public hass?: HomeAssistant; - @state() private _config?: EntitiesCardEntityConfig; + @state() private _config?: EntityConfig; - public setConfig(config: EntitiesCardEntityConfig): void { + public setConfig(config: EntityConfig): void { if (!config) { throw new Error("Invalid configuration"); } @@ -37,7 +32,9 @@ class HuiTextEntityRow extends LitElement implements LovelaceRow { return html``; } - const stateObj = this.hass.states[this._config.entity]; + const stateObj = this.hass.states[this._config.entity] as + | TextEntity + | undefined; if (!stateObj) { return html` @@ -48,22 +45,53 @@ class HuiTextEntityRow extends LitElement implements LovelaceRow { } return html` - - ${computeStateDisplay(this.hass!.localize, stateObj, this.hass.locale)} + + `; } - static get styles(): CSSResultGroup { - return css` - div { - text-align: right; - } - .pointer { - cursor: pointer; - } - `; + private _valueChanged(ev): void { + const stateObj = this.hass!.states[this._config!.entity] as TextEntity; + const newValue = ev.target.value; + + // Filter out invalid text states + if (newValue && UNAVAILABLE_STATES.includes(newValue)) { + ev.target.value = stateObj.state; + return; + } + + if (newValue !== stateObj.state) { + setValue(this.hass!, stateObj.entity_id, newValue); + } + + ev.target.blur(); } + + static styles = css` + hui-generic-entity-row { + display: flex; + align-items: center; + } + ha-textfield { + width: 100%; + } + `; } declare global { diff --git a/src/state-summary/state-card-content.js b/src/state-summary/state-card-content.js index 5f2d5ea484..21ca9d08c1 100644 --- a/src/state-summary/state-card-content.js +++ b/src/state-summary/state-card-content.js @@ -17,6 +17,7 @@ import "./state-card-number"; import "./state-card-scene"; import "./state-card-script"; import "./state-card-select"; +import "./state-card-text"; import "./state-card-timer"; import "./state-card-toggle"; import "./state-card-vacuum"; diff --git a/src/state-summary/state-card-text.ts b/src/state-summary/state-card-text.ts new file mode 100644 index 0000000000..5ef0456b95 --- /dev/null +++ b/src/state-summary/state-card-text.ts @@ -0,0 +1,73 @@ +import "../components/ha-textfield"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; +import { computeStateName } from "../common/entity/compute_state_name"; +import { stopPropagation } from "../common/dom/stop_propagation"; +import "../components/entity/state-badge"; +import { UNAVAILABLE, UNAVAILABLE_STATES } from "../data/entity"; +import { TextEntity, setValue } from "../data/text"; +import type { HomeAssistant } from "../types"; + +@customElement("state-card-text") +class StateCardText extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public stateObj!: TextEntity; + + protected render(): TemplateResult { + return html` + + + `; + } + + private _valueChanged(ev): void { + const value = ev.target.value; + + // Filter out invalid text states + if (value && UNAVAILABLE_STATES.includes(value)) { + ev.target.value = this.stateObj.state; + return; + } + + if (value === this.stateObj.state) { + return; + } + setValue(this.hass!, this.stateObj.entity_id, value); + } + + static get styles(): CSSResultGroup { + return css` + :host { + display: flex; + } + + state-badge { + float: left; + margin-top: 10px; + } + + ha-textfield { + width: 100%; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "state-card-text": StateCardText; + } +} diff --git a/src/translations/en.json b/src/translations/en.json index f46100b1b2..091b488c16 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -235,6 +235,9 @@ "installing_with_progress": "Installing ({progress}%)", "up_to_date": "Up-to-date" }, + "text": { + "emtpy_value": "(empty value)" + }, "timer": { "actions": { "start": "start",