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
This commit is contained in:
Ian Richardson 2019-10-17 14:00:39 -05:00 committed by Bram Kragten
parent 141c3f1ea4
commit f5e3a9ad40
6 changed files with 327 additions and 421 deletions

View File

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

View File

@ -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()}
<ha-card
class="${classMap({
class=${classMap({
[mode]: true,
large: this._broadCard!,
small: !this._broadCard,
})}"
large: this._large!,
medium: this._medium!,
small: this._small!,
longName: name.length > 10,
})}
>
<div id="root">
<paper-icon-button
icon="hass:dots-vertical"
class="more-info"
@click="${this._handleMoreInfo}"
@click=${this._handleMoreInfo}
></paper-icon-button>
<div id="thermostat"></div>
<div id="tooltip">
<div class="title">
${this._config.name || computeStateName(stateObj)}
<div id="thermostat">
${stateObj.state === "unavailable"
? html`
<round-slider
.radius=${this._radius}
disabled="true"
></round-slider>
`
: stateObj.attributes.target_temp_low &&
stateObj.attributes.target_temp_high
? html`
<round-slider
.radius=${this._radius}
.low=${stateObj.attributes.target_temp_low}
.high=${stateObj.attributes.target_temp_high}
.min=${stateObj.attributes.min_temp}
.max=${stateObj.attributes.max_temp}
.step=${this._stepSize}
@value-changing=${this._dragEvent}
@value-changed=${this._setTemperature}
></round-slider>
`
: html`
<round-slider
.radius=${this._radius}
.value=${stateObj.attributes.temperature !== null &&
Number.isFinite(Number(stateObj.attributes.temperature))
? stateObj.attributes.temperature
: stateObj.attributes.min_temp}
.step=${this._stepSize}
.min=${stateObj.attributes.min_temp}
.max=${stateObj.attributes.max_temp}
@value-changing=${this._dragEvent}
@value-changed=${this._setTemperature}
></round-slider>
`}
</div>
<div id="tooltip">
<div class="title">${name}</div>
<div class="current-temperature">
<span class="current-temperature-text">
${stateObj.attributes.current_temperature}
@ -147,7 +179,18 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
</span>
</div>
<div class="climate-info">
<div id="set-temperature"></div>
<div id="set-temperature">
${!this._setTemp
? ""
: Array.isArray(this._setTemp)
? html`
${this._setTemp[0].toFixed(1)} -
${this._setTemp[1].toFixed(1)}
`
: html`
${this._setTemp.toFixed(1)}
`}
</div>
<div class="current-mode">
${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,31 +240,31 @@ 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;
if (!stateObj) {
return;
this._setTemp = this._getSetTemp(this.hass!.states[this._config!.entity]);
}
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);
protected firstUpdated(): void {
this._updated = true;
if (this.isConnected && !this._loaded) {
this._initialLoad();
}
}
private async _initialLoad(): Promise<void> {
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;
}
this._loaded = true;
}
private get _stepSize(): number {
const stateObj = this.hass!.states[this._config!.entity] as ClimateEntity;
@ -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<void> {
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) {
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.handle.value,
target_temp_low: e.detail.low,
target_temp_high: stateObj.attributes.target_temp_high,
});
} else {
} 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.handle.value,
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,25 +354,8 @@ 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}
<style>
static get styles(): CSSResult {
return css`
:host {
display: block;
}
@ -457,11 +412,11 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
--title-position-top: 33% !important;
}
.large {
--thermostat-padding-top: 25px;
--thermostat-margin-bottom: 25px;
--thermostat-padding-top: 32px;
--thermostat-margin-bottom: 32px;
--title-font-size: 28px;
--title-position-top: 27%;
--climate-info-position-top: 81%;
--title-position-top: 25%;
--climate-info-position-top: 80%;
--set-temperature-font-size: 25px;
--current-temperature-font-size: 71px;
--current-temperature-position-top: 10%;
@ -469,6 +424,25 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
--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 {
@ -476,53 +450,35 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
--thermostat-margin-bottom: 15px;
--title-font-size: 18px;
--title-position-top: 28%;
--climate-info-position-top: 79%;
--climate-info-position-top: 78%;
--set-temperature-font-size: 16px;
--current-temperature-font-size: 25px;
--current-temperature-font-size: 55px;
--current-temperature-position-top: 5%;
--current-temperature-text-padding-left: 7px;
--uom-font-size: 12px;
--uom-margin-left: -5px;
--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 .rs-range-color {
background-color: var(--mode-color, var(--disabled-text-color));
}
#thermostat .rs-path-color {
background-color: var(--disabled-text-color);
}
#thermostat .rs-handle {
background-color: var(--paper-card-background-color, white);
padding: 10px;
margin: -10px 0 0 -8px !important;
border: 2px solid var(--disabled-text-color);
}
#thermostat .rs-handle.rs-focus {
border-color: var(--mode-color, var(--disabled-text-color));
}
#thermostat .rs-handle:after {
border-color: var(--mode-color, var(--disabled-text-color));
background-color: var(--mode-color, var(--disabled-text-color));
}
#thermostat .rs-border {
border-color: var(--rail-border-color);
}
#thermostat .rs-bar.rs-transition.rs-first,
.rs-bar.rs-transition.rs-second {
z-index: 20 !important;
}
#thermostat .rs-readonly {
z-index: 10;
top: auto;
}
#thermostat .rs-inner.rs-bg-color.rs-border,
#thermostat .rs-overlay.rs-transition.rs-bg-color {
background-color: var(--paper-card-background-color, white);
#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;
@ -556,9 +512,8 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
.current-mode {
font-size: var(--current-mode-font-size);
color: var(--secondary-text-color);
}
.modes {
margin-top: 16px;
margin-top: var(--current-mod-margin-top);
margin-bottom: var(--current-mod-margin-bottom);
}
.modes ha-icon {
color: var(--disabled-text-color);
@ -592,7 +547,6 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
z-index: 25;
color: var(--secondary-text-color);
}
</style>
`;
}
}

View File

@ -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`
<style>
${roundSliderCSS}
</style>
`;

View File

@ -1,15 +0,0 @@
import { TemplateResult } from "lit-element";
interface LoadedRoundSlider {
roundSliderStyle: TemplateResult;
jQuery: any;
}
let loaded: Promise<LoadedRoundSlider>;
export const loadRoundslider = async (): Promise<LoadedRoundSlider> => {
if (!loaded) {
loaded = import(/* webpackChunkName: "jquery-roundslider" */ "./jquery.roundslider");
}
return loaded;
};

View File

@ -1,5 +0,0 @@
import jQuery_ from "jquery";
(window as any).jQuery = jQuery_;
export const jQuery = jQuery_;

View File

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