From f5e3a9ad403a5199ae27247df978000e82bbd58a Mon Sep 17 00:00:00 2001 From: Ian Richardson Date: Thu, 17 Oct 2019 14:00:39 -0500 Subject: [PATCH] Convert thermostat to round-slider (#3734) * Convert to round-slider Closes https://github.com/home-assistant/home-assistant-polymer/issues/3622 Closes https://github.com/home-assistant/home-assistant-polymer/issues/2756 * scaling * address review comments * css tweaks * remove jquery * address comments * simplify set-temperature * handle long name * remove increased handleSize * address comments * address comments * address comments * address comment * need coffee --- package.json | 2 - .../lovelace/cards/hui-thermostat-card.ts | 700 ++++++++---------- src/resources/jquery.roundslider.js | 14 - src/resources/jquery.roundslider.ondemand.ts | 15 - src/resources/jquery.ts | 5 - yarn.lock | 12 - 6 files changed, 327 insertions(+), 421 deletions(-) delete mode 100644 src/resources/jquery.roundslider.js delete mode 100644 src/resources/jquery.roundslider.ondemand.ts delete mode 100644 src/resources/jquery.ts diff --git a/package.json b/package.json index 00ead71952..64bc94cb62 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,6 @@ "hls.js": "^0.12.4", "home-assistant-js-websocket": "^4.4.0", "intl-messageformat": "^2.2.0", - "jquery": "^3.4.0", "js-yaml": "^3.13.1", "leaflet": "^1.4.0", "lit-element": "^2.2.1", @@ -98,7 +97,6 @@ "react-big-calendar": "^0.20.4", "regenerator-runtime": "^0.13.2", "roboto-fontface": "^0.10.0", - "round-slider": "^1.3.3", "superstruct": "^0.6.1", "tslib": "^1.10.0", "unfetch": "^4.1.0", diff --git a/src/panels/lovelace/cards/hui-thermostat-card.ts b/src/panels/lovelace/cards/hui-thermostat-card.ts index e593b93ee1..d4ca705b41 100644 --- a/src/panels/lovelace/cards/hui-thermostat-card.ts +++ b/src/panels/lovelace/cards/hui-thermostat-card.ts @@ -5,9 +5,12 @@ import { TemplateResult, customElement, property, + css, + CSSResult, } from "lit-element"; import { classMap } from "lit-html/directives/class-map"; import "@polymer/paper-icon-button/paper-icon-button"; +import "@thomasloven/round-slider"; import "../../../components/ha-card"; import "../../../components/ha-icon"; @@ -19,7 +22,6 @@ import { computeStateName } from "../../../common/entity/compute_state_name"; import { hasConfigOrEntityChanged } from "../common/has-changed"; import { HomeAssistant } from "../../../types"; import { LovelaceCard, LovelaceCardEditor } from "../types"; -import { loadRoundslider } from "../../../resources/jquery.roundslider.ondemand"; import { UNIT_F } from "../../../common/const"; import { fireEvent } from "../../../common/dom/fire_event"; import { ThermostatCardConfig } from "./types"; @@ -29,17 +31,7 @@ import { compareClimateHvacModes, CLIMATE_PRESET_NONE, } from "../../../data/climate"; - -const thermostatConfig = { - radius: 150, - circleShape: "pie", - startAngle: 315, - width: 5, - lineCap: "round", - handleSize: "+10", - showTooltip: false, - animation: false, -}; +import { HassEntity } from "home-assistant-js-websocket"; const modeIcons: { [mode in HvacMode]: string } = { auto: "hass:calendar-repeat", @@ -63,18 +55,15 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { } @property() public hass?: HomeAssistant; - @property() private _config?: ThermostatCardConfig; - - @property() private _roundSliderStyle?: TemplateResult; - - @property() private _jQuery?: any; - - private _broadCard?: boolean; - - private _loaded?: boolean; + @property() private _loaded?: boolean; + @property() private _setTemp?: number | number[]; private _updated?: boolean; + private _large?: boolean; + private _medium?: boolean; + private _small?: boolean; + private _radius?: number; public getCardSize(): number { return 4; @@ -114,26 +103,69 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { } const mode = stateObj.state in modeIcons ? stateObj.state : "unknown-mode"; + const name = + this._config!.name || + computeStateName(this.hass!.states[this._config!.entity]); + + if (!this._radius || this._radius === 0) { + this._radius = 100; + } + return html` - ${this.renderStyle()} 10, + })} >
-
+
+ ${stateObj.state === "unavailable" + ? html` + + ` + : stateObj.attributes.target_temp_low && + stateObj.attributes.target_temp_high + ? html` + + ` + : html` + + `} +
-
- ${this._config.name || computeStateName(stateObj)} -
+
${name}
${stateObj.attributes.current_temperature} @@ -147,7 +179,18 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
-
+
+ ${!this._setTemp + ? "" + : Array.isArray(this._setTemp) + ? html` + ${this._setTemp[0].toFixed(1)} - + ${this._setTemp[1].toFixed(1)} + ` + : html` + ${this._setTemp.toFixed(1)} + `} +
${stateObj.attributes.hvac_action ? this.hass!.localize( @@ -185,13 +228,6 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { return hasConfigOrEntityChanged(this, changedProps); } - protected firstUpdated(): void { - this._updated = true; - if (this.isConnected && !this._loaded) { - this._initialLoad(); - } - } - protected updated(changedProps: PropertyValues): void { super.updated(changedProps); if (!this._config || !this.hass || !changedProps.has("hass")) { @@ -204,29 +240,29 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { applyThemesOnElement(this, this.hass.themes, this._config.theme); } - const stateObj = this.hass.states[this._config.entity] as ClimateEntity; + this._setTemp = this._getSetTemp(this.hass!.states[this._config!.entity]); + } - if (!stateObj) { - return; + protected firstUpdated(): void { + this._updated = true; + if (this.isConnected && !this._loaded) { + this._initialLoad(); + } + } + + private async _initialLoad(): Promise { + this._large = this._medium = this._small = false; + this._radius = this.clientWidth / 3.9; + + if (this.clientWidth > 450) { + this._large = true; + } else if (this.clientWidth < 350) { + this._small = true; + } else { + this._medium = true; } - if ( - this._jQuery && - // If jQuery changed, we just rendered in firstUpdated - !changedProps.has("_jQuery") && - (!oldHass || oldHass.states[this._config.entity] !== stateObj) - ) { - const [sliderValue, uiValue, sliderType] = this._genSliderValue(stateObj); - - this._jQuery("#thermostat", this.shadowRoot).roundSlider({ - sliderType, - value: sliderValue, - disabled: sliderValue === null, - min: stateObj.attributes.min_temp, - max: stateObj.attributes.max_temp, - }); - this._updateSetTemp(uiValue); - } + this._loaded = true; } private get _stepSize(): number { @@ -238,119 +274,55 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { return this.hass!.config.unit_system.temperature === UNIT_F ? 1 : 0.5; } - private async _initialLoad(): Promise { - const stateObj = this.hass!.states[this._config!.entity] as ClimateEntity; - - if (!stateObj) { - // Card will require refresh to work again - return; - } - - this._loaded = true; - - await this.updateComplete; - - let radius = this.clientWidth / 3.2; - this._broadCard = this.clientWidth > 390; - - if (radius === 0) { - radius = 100; - } - - (this.shadowRoot!.querySelector( - "#thermostat" - ) as HTMLElement)!.style.height = radius * 2 + "px"; - - const loaded = await loadRoundslider(); - - this._roundSliderStyle = loaded.roundSliderStyle; - this._jQuery = loaded.jQuery; - - const [sliderValue, uiValue, sliderType] = this._genSliderValue(stateObj); - - this._jQuery("#thermostat", this.shadowRoot).roundSlider({ - ...thermostatConfig, - radius, - min: stateObj.attributes.min_temp, - max: stateObj.attributes.max_temp, - sliderType, - change: (value) => this._setTemperature(value), - drag: (value) => this._dragEvent(value), - value: sliderValue, - disabled: sliderValue === null, - step: this._stepSize, - }); - this._updateSetTemp(uiValue); - } - - private _genSliderValue( - stateObj: ClimateEntity - ): [string | number | null, string, string] { - let sliderType: string; - let sliderValue: string | number | null; - let uiValue: string; - + private _getSetTemp(stateObj: HassEntity) { if (stateObj.state === "unavailable") { - sliderType = "min-range"; - sliderValue = null; - uiValue = this.hass!.localize("state.default.unavailable"); - } else if ( - stateObj.attributes.target_temp_low && - stateObj.attributes.target_temp_high - ) { - sliderType = "range"; - sliderValue = `${stateObj.attributes.target_temp_low}, ${ - stateObj.attributes.target_temp_high - }`; - uiValue = this.formatTemp( - [ - String(stateObj.attributes.target_temp_low), - String(stateObj.attributes.target_temp_high), - ], - false - ); - } else { - sliderType = "min-range"; - sliderValue = Number.isFinite(Number(stateObj.attributes.temperature)) - ? stateObj.attributes.temperature - : null; - uiValue = sliderValue !== null ? String(sliderValue) : ""; + return this.hass!.localize("state.default.unavailable"); } - return [sliderValue, uiValue, sliderType]; - } - - private _updateSetTemp(value: string): void { - this.shadowRoot!.querySelector("#set-temperature")!.innerHTML = value; - } - - private _dragEvent(e): void { - this._updateSetTemp(this.formatTemp(String(e.value).split(","), true)); - } - - private _setTemperature(e): void { - const stateObj = this.hass!.states[this._config!.entity] as ClimateEntity; if ( stateObj.attributes.target_temp_low && stateObj.attributes.target_temp_high ) { - if (e.handle.index === 1) { - this.hass!.callService("climate", "set_temperature", { - entity_id: this._config!.entity, - target_temp_low: e.handle.value, - target_temp_high: stateObj.attributes.target_temp_high, - }); - } else { - this.hass!.callService("climate", "set_temperature", { - entity_id: this._config!.entity, - target_temp_low: stateObj.attributes.target_temp_low, - target_temp_high: e.handle.value, - }); - } + return [ + stateObj.attributes.target_temp_low, + stateObj.attributes.target_temp_high, + ]; + } + + return stateObj.attributes.temperature; + } + + private _dragEvent(e): void { + const stateObj = this.hass!.states[this._config!.entity] as ClimateEntity; + + if (e.detail.low) { + this._setTemp = [e.detail.low, stateObj.attributes.target_temp_high]; + } else if (e.detail.high) { + this._setTemp = [stateObj.attributes.target_temp_low, e.detail.high]; + } else { + this._setTemp = e.detail.value; + } + } + + private _setTemperature(e): void { + const stateObj = this.hass!.states[this._config!.entity] as ClimateEntity; + + if (e.detail.low) { + this.hass!.callService("climate", "set_temperature", { + entity_id: this._config!.entity, + target_temp_low: e.detail.low, + target_temp_high: stateObj.attributes.target_temp_high, + }); + } else if (e.detail.high) { + this.hass!.callService("climate", "set_temperature", { + entity_id: this._config!.entity, + target_temp_low: stateObj.attributes.target_temp_low, + target_temp_high: e.detail.high, + }); } else { this.hass!.callService("climate", "set_temperature", { entity_id: this._config!.entity, - temperature: e.value, + temperature: e.detail.value, }); } } @@ -382,217 +354,199 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { }); } - private formatTemp(temps: string[], spaceStepSize: boolean): string { - temps = temps.filter(Boolean); - - // If we are sliding the slider, append 0 to the temperatures if we're - // having a 0.5 step size, so that the text doesn't jump while sliding - if (spaceStepSize) { - const stepSize = this._stepSize; - temps = temps.map((val) => - val.includes(".") || stepSize === 1 ? val : `${val}.0` - ); - } - - return temps.join("-"); - } - - private renderStyle(): TemplateResult { - return html` - ${this._roundSliderStyle} - + static get styles(): CSSResult { + return css` + :host { + display: block; + } + ha-card { + overflow: hidden; + --rail-border-color: transparent; + --auto-color: green; + --eco-color: springgreen; + --cool-color: #2b9af9; + --heat-color: #ff8100; + --manual-color: #44739e; + --off-color: #8a8a8a; + --fan_only-color: #8a8a8a; + --dry-color: #efbd07; + --idle-color: #8a8a8a; + --unknown-color: #bac; + } + #root { + position: relative; + overflow: hidden; + } + .auto, + .heat_cool { + --mode-color: var(--auto-color); + } + .cool { + --mode-color: var(--cool-color); + } + .heat { + --mode-color: var(--heat-color); + } + .manual { + --mode-color: var(--manual-color); + } + .off { + --mode-color: var(--off-color); + } + .fan_only { + --mode-color: var(--fan_only-color); + } + .eco { + --mode-color: var(--eco-color); + } + .dry { + --mode-color: var(--dry-color); + } + .idle { + --mode-color: var(--idle-color); + } + .unknown-mode { + --mode-color: var(--unknown-color); + } + .no-title { + --title-position-top: 33% !important; + } + .large { + --thermostat-padding-top: 32px; + --thermostat-margin-bottom: 32px; + --title-font-size: 28px; + --title-position-top: 25%; + --climate-info-position-top: 80%; + --set-temperature-font-size: 25px; + --current-temperature-font-size: 71px; + --current-temperature-position-top: 10%; + --current-temperature-text-padding-left: 15px; + --uom-font-size: 20px; + --uom-margin-left: -18px; + --current-mode-font-size: 18px; + --current-mod-margin-top: 6px; + --current-mod-margin-bottom: 12px; + --set-temperature-margin-bottom: -5px; + } + .medium { + --thermostat-padding-top: 20px; + --thermostat-margin-bottom: 20px; + --title-font-size: 23px; + --title-position-top: 27%; + --climate-info-position-top: 84%; + --set-temperature-font-size: 20px; + --current-temperature-font-size: 65px; + --current-temperature-position-top: 10%; + --current-temperature-text-padding-left: 15px; + --uom-font-size: 18px; + --uom-margin-left: -16px; + --current-mode-font-size: 16px; + --current-mod-margin-top: 4px; + --current-mod-margin-bottom: 4px; + --set-temperature-margin-bottom: -5px; + } + .small { + --thermostat-padding-top: 15px; + --thermostat-margin-bottom: 15px; + --title-font-size: 18px; + --title-position-top: 28%; + --climate-info-position-top: 78%; + --set-temperature-font-size: 16px; + --current-temperature-font-size: 55px; + --current-temperature-position-top: 5%; + --current-temperature-text-padding-left: 16px; + --uom-font-size: 16px; + --uom-margin-left: -14px; + --current-mode-font-size: 14px; + --current-mod-margin-top: 2px; + --current-mod-margin-bottom: 4px; + --set-temperature-margin-bottom: 0px; + } + .longName { + --title-font-size: 18px; + } + #thermostat { + margin: 0 auto var(--thermostat-margin-bottom); + padding-top: var(--thermostat-padding-top); + padding-bottom: 32px; + display: flex; + justify-content: center; + align-items: center; + } + #thermostat round-slider { + margin: 0 auto; + display: inline-block; + --round-slider-path-color: var(--disabled-text-color); + --round-slider-bar-color: var(--mode-color); + z-index: 20; + } + #tooltip { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 100%; + text-align: center; + z-index: 15; + color: var(--primary-text-color); + } + #set-temperature { + font-size: var(--set-temperature-font-size); + margin-bottom: var(--set-temperature-margin-bottom); + min-height: 1.2em; + } + .title { + font-size: var(--title-font-size); + position: absolute; + top: var(--title-position-top); + left: 50%; + transform: translate(-50%, -50%); + } + .climate-info { + position: absolute; + top: var(--climate-info-position-top); + left: 50%; + transform: translate(-50%, -50%); + width: 100%; + } + .current-mode { + font-size: var(--current-mode-font-size); + color: var(--secondary-text-color); + margin-top: var(--current-mod-margin-top); + margin-bottom: var(--current-mod-margin-bottom); + } + .modes ha-icon { + color: var(--disabled-text-color); + cursor: pointer; + display: inline-block; + margin: 0 10px; + } + .modes ha-icon.selected-icon { + color: var(--mode-color); + } + .current-temperature { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: var(--current-temperature-font-size); + } + .current-temperature-text { + padding-left: var(--current-temperature-text-padding-left); + } + .uom { + font-size: var(--uom-font-size); + vertical-align: top; + margin-left: var(--uom-margin-left); + } + .more-info { + position: absolute; + cursor: pointer; + top: 0; + right: 0; + z-index: 25; + color: var(--secondary-text-color); + } `; } } diff --git a/src/resources/jquery.roundslider.js b/src/resources/jquery.roundslider.js deleted file mode 100644 index 920cff0fb9..0000000000 --- a/src/resources/jquery.roundslider.js +++ /dev/null @@ -1,14 +0,0 @@ -import { html } from "lit-element"; -// jQuery import should come before plugin import -import { jQuery as jQuery_ } from "./jquery"; -import "round-slider"; -// eslint-disable-next-line -import roundSliderCSS from "round-slider/dist/roundslider.min.css"; - -export const jQuery = jQuery_; - -export const roundSliderStyle = html` - -`; diff --git a/src/resources/jquery.roundslider.ondemand.ts b/src/resources/jquery.roundslider.ondemand.ts deleted file mode 100644 index 487b367f8b..0000000000 --- a/src/resources/jquery.roundslider.ondemand.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { TemplateResult } from "lit-element"; - -interface LoadedRoundSlider { - roundSliderStyle: TemplateResult; - jQuery: any; -} - -let loaded: Promise; - -export const loadRoundslider = async (): Promise => { - if (!loaded) { - loaded = import(/* webpackChunkName: "jquery-roundslider" */ "./jquery.roundslider"); - } - return loaded; -}; diff --git a/src/resources/jquery.ts b/src/resources/jquery.ts deleted file mode 100644 index 650365b431..0000000000 --- a/src/resources/jquery.ts +++ /dev/null @@ -1,5 +0,0 @@ -import jQuery_ from "jquery"; - -(window as any).jQuery = jQuery_; - -export const jQuery = jQuery_; diff --git a/yarn.lock b/yarn.lock index 6e51ae9efa..87f4d1b083 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7686,11 +7686,6 @@ joi@^14.3.1: isemail "3.x.x" topo "3.x.x" -"jquery@>= 1.4.1", jquery@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.4.0.tgz#8de513fa0fa4b2c7d2e48a530e26f0596936efdf" - integrity sha512-ggRCXln9zEqv6OqAGXFEcshF5dSBvCkzj6Gm2gzuR5fWawaX8t7cxKVkkygKODrDAzKdoYw3l/e3pm3vlT4IbQ== - js-levenshtein@^1.1.3: version "1.1.6" resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d" @@ -11050,13 +11045,6 @@ rollup@^1.3.0: "@types/node" "^11.11.6" acorn "^6.1.1" -round-slider@^1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/round-slider/-/round-slider-1.3.3.tgz#0ec82261317b0aba35ac86a48fbd024baa50b261" - integrity sha512-Mo6HYiN5l6O0UNI/ytjr8ERgtZcXGRZ6A6+V6f2S/hKkbSaQnKCW+lpLPfoxWuW1xiPbAJs3qrXeps1xC+sjVg== - dependencies: - jquery ">= 1.4.1" - run-async@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"