diff --git a/src/common/const.ts b/src/common/const.ts index 2b89dbb81d..bbf051f668 100644 --- a/src/common/const.ts +++ b/src/common/const.ts @@ -34,6 +34,7 @@ export const FIXED_DOMAIN_ICONS = { light: "hass:lightbulb", mailbox: "hass:mailbox", notify: "hass:comment-alert", + number: "hass:ray-vertex", persistent_notification: "hass:bell", person: "hass:account", plant: "hass:flower", @@ -77,6 +78,7 @@ export const DOMAINS_WITH_CARD = [ "input_text", "lock", "media_player", + "number", "scene", "script", "timer", @@ -114,6 +116,7 @@ export const DOMAINS_HIDE_MORE_INFO = [ "input_number", "input_select", "input_text", + "number", "scene", ]; diff --git a/src/data/entity.ts b/src/data/entity.ts index f8dc2efe42..0ade5b2d15 100644 --- a/src/data/entity.ts +++ b/src/data/entity.ts @@ -27,6 +27,7 @@ export const ENTITY_COMPONENT_DOMAINS = [ "lock", "mailbox", "media_player", + "number", "person", "plant", "remember_the_milk", diff --git a/src/panels/lovelace/create-element/create-row-element.ts b/src/panels/lovelace/create-element/create-row-element.ts index b7e54b81b1..86de26dcc1 100644 --- a/src/panels/lovelace/create-element/create-row-element.ts +++ b/src/panels/lovelace/create-element/create-row-element.ts @@ -36,6 +36,7 @@ const LAZY_LOAD_TYPES = { import("../entity-rows/hui-input-select-entity-row"), "input-text-entity": () => import("../entity-rows/hui-input-text-entity-row"), "lock-entity": () => import("../entity-rows/hui-lock-entity-row"), + "number-entity": () => import("../entity-rows/hui-number-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"), @@ -63,6 +64,7 @@ const DOMAIN_TO_ELEMENT_TYPE = { light: "toggle", lock: "lock", media_player: "media-player", + number: "number", remote: "toggle", scene: "scene", script: "script", diff --git a/src/panels/lovelace/entity-rows/hui-number-entity-row.ts b/src/panels/lovelace/entity-rows/hui-number-entity-row.ts new file mode 100644 index 0000000000..ac1e7edcc3 --- /dev/null +++ b/src/panels/lovelace/entity-rows/hui-number-entity-row.ts @@ -0,0 +1,176 @@ +import "@polymer/paper-input/paper-input"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + internalProperty, + PropertyValues, + TemplateResult, +} from "lit-element"; +import { computeRTLDirection } from "../../../common/util/compute_rtl"; +import "../../../components/ha-slider"; +import { UNAVAILABLE_STATES } from "../../../data/entity"; +import { setValue } from "../../../data/input_text"; +import { HomeAssistant } from "../../../types"; +import { hasConfigOrEntityChanged } from "../common/has-changed"; +import "../components/hui-generic-entity-row"; +import { EntityConfig, LovelaceRow } from "./types"; +import { createEntityNotFoundWarning } from "../components/hui-warning"; + +@customElement("hui-number-entity-row") +class HuiNumberEntityRow extends LitElement implements LovelaceRow { + @property({ attribute: false }) public hass?: HomeAssistant; + + @internalProperty() private _config?: EntityConfig; + + private _loaded?: boolean; + + private _updated?: boolean; + + public setConfig(config: EntityConfig): void { + if (!config) { + throw new Error("Invalid configuration"); + } + this._config = config; + } + + public connectedCallback(): void { + super.connectedCallback(); + if (this._updated && !this._loaded) { + this._initialLoad(); + } + } + + protected firstUpdated(): void { + this._updated = true; + if (this.isConnected && !this._loaded) { + this._initialLoad(); + } + } + + 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` + + ${stateObj.attributes.mode === "slider" + ? html` +
+ + + ${Number(stateObj.state)} + ${stateObj.attributes.unit_of_measurement} + +
+ ` + : html` +
+ + ${stateObj.attributes.unit_of_measurement} +
+ `} +
+ `; + } + + static get styles(): CSSResult { + return css` + .flex { + display: flex; + align-items: center; + justify-content: flex-end; + flex-grow: 2; + } + .state { + min-width: 45px; + text-align: end; + } + paper-input { + text-align: end; + } + ha-slider { + width: 100%; + max-width: 200px; + } + :host { + cursor: pointer; + } + `; + } + + private async _initialLoad(): Promise { + this._loaded = true; + await this.updateComplete; + const element = this.shadowRoot!.querySelector(".state") as HTMLElement; + + if (!element || !this.parentElement) { + return; + } + + element.hidden = this.parentElement.clientWidth <= 350; + } + + private get _inputElement(): { value: string } { + // linter recommended the following syntax + return (this.shadowRoot!.getElementById("input") as unknown) as { + value: string; + }; + } + + private _selectedValueChanged(): void { + const element = this._inputElement; + const stateObj = this.hass!.states[this._config!.entity]; + + if (element.value !== stateObj.state) { + setValue(this.hass!, stateObj.entity_id, element.value!); + } + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-number-entity-row": HuiNumberEntityRow; + } +} diff --git a/src/state-summary/state-card-content.js b/src/state-summary/state-card-content.js index 026b6287cb..8a365f612a 100644 --- a/src/state-summary/state-card-content.js +++ b/src/state-summary/state-card-content.js @@ -11,6 +11,7 @@ import "./state-card-input_select"; import "./state-card-input_text"; import "./state-card-lock"; import "./state-card-media_player"; +import "./state-card-number"; import "./state-card-scene"; import "./state-card-script"; import "./state-card-timer"; diff --git a/src/state-summary/state-card-number.js b/src/state-summary/state-card-number.js new file mode 100644 index 0000000000..635d76bcc0 --- /dev/null +++ b/src/state-summary/state-card-number.js @@ -0,0 +1,187 @@ +import "@polymer/iron-flex-layout/iron-flex-layout-classes"; +import { IronResizableBehavior } from "@polymer/iron-resizable-behavior/iron-resizable-behavior"; +import "@polymer/paper-input/paper-input"; +import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class"; +import { html } from "@polymer/polymer/lib/utils/html-tag"; +/* eslint-plugin-disable lit */ +import { PolymerElement } from "@polymer/polymer/polymer-element"; +import "../components/entity/state-info"; +import "../components/ha-slider"; + +class StateCardNumber extends mixinBehaviors( + [IronResizableBehavior], + PolymerElement +) { + static get template() { + return html` + + + +
+ ${this.stateInfoTemplate} + + + + +
+ `; + } + + static get stateInfoTemplate() { + return html` + + `; + } + + ready() { + super.ready(); + if (typeof ResizeObserver === "function") { + const ro = new ResizeObserver((entries) => { + entries.forEach(() => { + this.hiddenState(); + }); + }); + ro.observe(this.$.number_card); + } else { + this.addEventListener("iron-resize", this.hiddenState); + } + } + + static get properties() { + return { + hass: Object, + hiddenbox: { + type: Boolean, + value: true, + }, + hiddenslider: { + type: Boolean, + value: true, + }, + inDialog: { + type: Boolean, + value: false, + }, + stateObj: { + type: Object, + observer: "stateObjectChanged", + }, + min: { + type: Number, + value: 0, + }, + max: { + type: Number, + value: 100, + }, + maxlength: { + type: Number, + value: 3, + }, + step: Number, + value: Number, + mode: String, + }; + } + + hiddenState() { + if (this.mode !== "slider") return; + const sliderwidth = this.$.slider.offsetWidth; + if (sliderwidth < 100) { + this.$.sliderstate.hidden = true; + } else if (sliderwidth >= 145) { + this.$.sliderstate.hidden = false; + } + } + + stateObjectChanged(newVal) { + const prevMode = this.mode; + this.setProperties({ + min: Number(newVal.attributes.min), + max: Number(newVal.attributes.max), + step: Number(newVal.attributes.step), + value: Number(newVal.state), + mode: String(newVal.attributes.mode), + maxlength: String(newVal.attributes.max).length, + hiddenbox: newVal.attributes.mode !== "box", + hiddenslider: newVal.attributes.mode !== "slider", + }); + if (this.mode === "slider" && prevMode !== "slider") { + this.hiddenState(); + } + } + + selectedValueChanged() { + if (this.value === Number(this.stateObj.state)) { + return; + } + this.hass.callService("number", "set_value", { + value: this.value, + entity_id: this.stateObj.entity_id, + }); + } + + stopPropagation(ev) { + ev.stopPropagation(); + } +} + +customElements.define("state-card-number", StateCardNumber);