Add number entity support (#7876)

This commit is contained in:
Shulyaka 2021-01-27 01:14:23 +03:00 committed by GitHub
parent 9988227d93
commit 46b3836fbd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 370 additions and 0 deletions

View File

@ -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",
];

View File

@ -27,6 +27,7 @@ export const ENTITY_COMPONENT_DOMAINS = [
"lock",
"mailbox",
"media_player",
"number",
"person",
"plant",
"remember_the_milk",

View File

@ -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",

View File

@ -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`
<hui-warning>
${createEntityNotFoundWarning(this.hass, this._config.entity)}
</hui-warning>
`;
}
return html`
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}>
${stateObj.attributes.mode === "slider"
? html`
<div class="flex">
<ha-slider
.disabled=${UNAVAILABLE_STATES.includes(stateObj.state)}
.dir=${computeRTLDirection(this.hass)}
.step="${Number(stateObj.attributes.step)}"
.min="${Number(stateObj.attributes.min)}"
.max="${Number(stateObj.attributes.max)}"
.value="${Number(stateObj.state)}"
pin
@change="${this._selectedValueChanged}"
ignore-bar-touch
id="input"
></ha-slider>
<span class="state">
${Number(stateObj.state)}
${stateObj.attributes.unit_of_measurement}
</span>
</div>
`
: html`
<div class="flex state">
<paper-input
no-label-float
auto-validate
.disabled=${UNAVAILABLE_STATES.includes(stateObj.state)}
pattern="[0-9]+([\\.][0-9]+)?"
.step="${Number(stateObj.attributes.step)}"
.min="${Number(stateObj.attributes.min)}"
.max="${Number(stateObj.attributes.max)}"
.value="${Number(stateObj.state)}"
type="number"
@change="${this._selectedValueChanged}"
id="input"
></paper-input>
${stateObj.attributes.unit_of_measurement}
</div>
`}
</hui-generic-entity-row>
`;
}
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<void> {
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;
}
}

View File

@ -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";

View File

@ -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`
<style include="iron-flex iron-flex-alignment"></style>
<style>
ha-slider {
margin-left: auto;
}
.state {
@apply --paper-font-body1;
color: var(--primary-text-color);
text-align: right;
line-height: 40px;
}
.sliderstate {
min-width: 45px;
}
ha-slider[hidden] {
display: none !important;
}
paper-input {
text-align: right;
margin-left: auto;
}
</style>
<div class="horizontal justified layout" id="number_card">
${this.stateInfoTemplate}
<ha-slider
min="[[min]]"
max="[[max]]"
value="{{value}}"
step="[[step]]"
hidden="[[hiddenslider]]"
pin
on-change="selectedValueChanged"
on-click="stopPropagation"
id="slider"
ignore-bar-touch=""
>
</ha-slider>
<paper-input
no-label-float=""
auto-validate=""
pattern="[0-9]+([\\.][0-9]+)?"
step="[[step]]"
min="[[min]]"
max="[[max]]"
value="{{value}}"
type="number"
on-change="selectedValueChanged"
on-click="stopPropagation"
hidden="[[hiddenbox]]"
>
</paper-input>
<div class="state" hidden="[[hiddenbox]]">
[[stateObj.attributes.unit_of_measurement]]
</div>
<div
id="sliderstate"
class="state sliderstate"
hidden="[[hiddenslider]]"
>
[[value]] [[stateObj.attributes.unit_of_measurement]]
</div>
</div>
`;
}
static get stateInfoTemplate() {
return html`
<state-info
hass="[[hass]]"
state-obj="[[stateObj]]"
in-dialog="[[inDialog]]"
></state-info>
`;
}
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);