Thermostat Card LoveLace (#1814)
* POC/WIP: Thermostat Card * Fix jQuery imports * Cleaning out testing code and working on reviews * Colors Dynamic + mode dynamic * Minor changes * adding html prefix * Dynamic Text size and colors - getting somwhere slowly. * Review Changes - Working version (i think) * Updating Gallery Entry * Travies Review * Remove provide plugin, move CSS to JS * Add provideHass to demo * Demo fixes * tweak margins * Travis changes * Style Tweaks * Update to client Width range
This commit is contained in:
parent
c42d9385d1
commit
741c0c08b9
|
@ -99,6 +99,21 @@ export class CoverEntity extends Entity {
|
|||
}
|
||||
}
|
||||
|
||||
export class ClimateEntity extends Entity {
|
||||
async handleService(domain, service, data) {
|
||||
if (domain !== this.domain) return;
|
||||
|
||||
if (service === "set_operation_mode") {
|
||||
this.update(
|
||||
data.operation_mode === "heat" ? "heat" : data.operation_mode,
|
||||
Object.assign(this.attributes, {
|
||||
operation_mode: data.operation_mode,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class GroupEntity extends Entity {
|
||||
async handleService(domain, service, data) {
|
||||
if (!["homeassistant", this.domain].includes(domain)) return;
|
||||
|
@ -115,6 +130,7 @@ export class GroupEntity extends Entity {
|
|||
}
|
||||
|
||||
const TYPES = {
|
||||
climate: ClimateEntity,
|
||||
light: LightEntity,
|
||||
lock: LockEntity,
|
||||
cover: CoverEntity,
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
import { html } from "@polymer/polymer/lib/utils/html-tag.js";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element.js";
|
||||
|
||||
import getEntity from "../data/entity.js";
|
||||
import provideHass from "../data/provide_hass.js";
|
||||
import "../components/demo-cards.js";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("climate", "ecobee", "auto", {
|
||||
current_temperature: 73,
|
||||
min_temp: 45,
|
||||
max_temp: 95,
|
||||
temperature: null,
|
||||
target_temp_high: 75,
|
||||
target_temp_low: 70,
|
||||
fan_mode: "Auto Low",
|
||||
fan_list: ["On Low", "On High", "Auto Low", "Auto High", "Off"],
|
||||
operation_mode: "auto",
|
||||
operation_list: ["heat", "cool", "auto", "off"],
|
||||
hold_mode: "home",
|
||||
swing_mode: "Auto",
|
||||
swing_list: ["Auto", "1", "2", "3", "Off"],
|
||||
friendly_name: "Ecobee",
|
||||
supported_features: 1014,
|
||||
}),
|
||||
getEntity("climate", "nest", "heat", {
|
||||
current_temperature: 17,
|
||||
min_temp: 15,
|
||||
max_temp: 25,
|
||||
temperature: 19,
|
||||
fan_mode: "Auto Low",
|
||||
fan_list: ["On Low", "On High", "Auto Low", "Auto High", "Off"],
|
||||
operation_mode: "heat",
|
||||
operation_list: ["heat", "cool", "auto", "off"],
|
||||
hold_mode: "home",
|
||||
swing_mode: "Auto",
|
||||
swing_list: ["Auto", "1", "2", "3", "Off"],
|
||||
friendly_name: "Nest",
|
||||
supported_features: 1014,
|
||||
}),
|
||||
];
|
||||
|
||||
const CONFIGS = [
|
||||
{
|
||||
heading: "Range example",
|
||||
config: `
|
||||
- type: thermostat
|
||||
entity: climate.ecobee
|
||||
- type: thermostat
|
||||
entity: climate.nest
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Single temp example",
|
||||
config: `
|
||||
- type: thermostat
|
||||
entity: climate.nest
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
class DemoThermostatEntity extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<demo-cards
|
||||
id='demos'
|
||||
configs="[[_configs]]"
|
||||
></demo-cards>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_configs: {
|
||||
type: Object,
|
||||
value: CONFIGS,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-hui-thermostat-card", DemoThermostatEntity);
|
|
@ -17,6 +17,10 @@ module.exports = {
|
|||
module: {
|
||||
rules: [
|
||||
babelLoaderConfig({ latestBuild: true }),
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: "raw-loader",
|
||||
},
|
||||
{
|
||||
test: /\.(html)$/,
|
||||
use: {
|
||||
|
|
|
@ -75,6 +75,7 @@
|
|||
"fecha": "^2.3.3",
|
||||
"home-assistant-js-websocket": "^3.1.4",
|
||||
"intl-messageformat": "^2.2.0",
|
||||
"jquery": "^3.3.1",
|
||||
"js-yaml": "^3.12.0",
|
||||
"leaflet": "^1.3.4",
|
||||
"lit-html": "^0.12.0",
|
||||
|
@ -85,6 +86,7 @@
|
|||
"preact-compat": "^3.18.4",
|
||||
"react-big-calendar": "^0.19.2",
|
||||
"regenerator-runtime": "^0.12.1",
|
||||
"round-slider": "^1.3.2",
|
||||
"unfetch": "^4.0.1",
|
||||
"web-animations-js": "^2.3.1",
|
||||
"xss": "^1.0.3"
|
||||
|
|
|
@ -0,0 +1,349 @@
|
|||
import { html, LitElement } from "@polymer/lit-element";
|
||||
import { classMap } from "lit-html/directives/classMap.js";
|
||||
import { jQuery } from "../../../resources/jquery";
|
||||
|
||||
import "../../../components/ha-card.js";
|
||||
import "../../../components/ha-icon.js";
|
||||
import { roundSliderStyle } from "../../../resources/jquery.roundslider";
|
||||
|
||||
import { HomeAssistant } from "../../../types.js";
|
||||
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
||||
import { LovelaceCard, LovelaceConfig } from "../types.js";
|
||||
|
||||
const thermostatConfig = {
|
||||
radius: 150,
|
||||
step: 1,
|
||||
circleShape: "pie",
|
||||
startAngle: 315,
|
||||
width: 5,
|
||||
lineCap: "round",
|
||||
handleSize: "+10",
|
||||
showTooltip: false,
|
||||
};
|
||||
|
||||
const modeIcons = {
|
||||
auto: "hass:autorenew",
|
||||
heat: "hass:fire",
|
||||
cool: "hass:snowflake",
|
||||
off: "hass:fan-off",
|
||||
};
|
||||
|
||||
interface Config extends LovelaceConfig {
|
||||
entity: string;
|
||||
}
|
||||
|
||||
function formatTemp(temps) {
|
||||
return temps.filter(Boolean).join("-");
|
||||
}
|
||||
|
||||
export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement)
|
||||
implements LovelaceCard {
|
||||
public hass?: HomeAssistant;
|
||||
protected config?: Config;
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {},
|
||||
config: {},
|
||||
};
|
||||
}
|
||||
|
||||
public getCardSize() {
|
||||
return 4;
|
||||
}
|
||||
|
||||
public setConfig(config: Config) {
|
||||
if (!config.entity || config.entity.split(".")[0] !== "climate") {
|
||||
throw new Error("Specify an entity from within the climate domain.");
|
||||
}
|
||||
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this.config) {
|
||||
return html``;
|
||||
}
|
||||
const stateObj = this.hass.states[this.config.entity];
|
||||
const broadCard = this.clientWidth > 390;
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-card
|
||||
class="${classMap({
|
||||
[stateObj.attributes.operation_mode]: true,
|
||||
large: broadCard,
|
||||
small: !broadCard,
|
||||
})}">
|
||||
<div id="root">
|
||||
<div id="thermostat"></div>
|
||||
<div id="tooltip">
|
||||
<div class="title">Upstairs</div>
|
||||
<div class="current-temperature">
|
||||
<span class="current-temperature-text">${
|
||||
stateObj.attributes.current_temperature
|
||||
}
|
||||
<span class="uom">${
|
||||
this.hass.config.unit_system.temperature
|
||||
}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="climate-info">
|
||||
<div id="set-temperature"></div>
|
||||
<div class="current-mode">${this.localize(
|
||||
`state.climate.${stateObj.state}`
|
||||
)}</div>
|
||||
<div class="modes">
|
||||
${stateObj.attributes.operation_list.map((modeItem) =>
|
||||
this._renderIcon(modeItem, stateObj.attributes.operation_mode)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps) {
|
||||
if (changedProps.get("hass")) {
|
||||
return changedProps.get("hass").states[this.config!.entity] !==
|
||||
this.hass!.states[this.config!.entity]
|
||||
? true
|
||||
: false;
|
||||
}
|
||||
return changedProps;
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
const stateObj = this.hass!.states[this.config!.entity];
|
||||
|
||||
const _sliderType =
|
||||
stateObj.attributes.target_temp_low &&
|
||||
stateObj.attributes.target_temp_high
|
||||
? "range"
|
||||
: "min-range";
|
||||
|
||||
jQuery("#thermostat", this.shadowRoot).roundSlider({
|
||||
...thermostatConfig,
|
||||
radius: this.clientWidth / 3,
|
||||
min: stateObj.attributes.min_temp,
|
||||
max: stateObj.attributes.max_temp,
|
||||
sliderType: _sliderType,
|
||||
change: (value) => this._setTemperature(value),
|
||||
drag: (value) => this._dragEvent(value),
|
||||
});
|
||||
}
|
||||
|
||||
protected updated() {
|
||||
const attrs = this.hass!.states[this.config!.entity].attributes;
|
||||
|
||||
let sliderValue;
|
||||
let uiValue;
|
||||
|
||||
if (attrs.target_temp_low && attrs.target_temp_high) {
|
||||
sliderValue = `${attrs.target_temp_low}, ${attrs.target_temp_high}`;
|
||||
uiValue = formatTemp([attrs.target_temp_low, attrs.target_temp_high]);
|
||||
} else {
|
||||
sliderValue = uiValue = attrs.temperature;
|
||||
}
|
||||
|
||||
jQuery("#thermostat", this.shadowRoot).roundSlider({
|
||||
value: sliderValue,
|
||||
});
|
||||
this.shadowRoot!.querySelector("#set-temperature")!.innerHTML = uiValue;
|
||||
}
|
||||
|
||||
private renderStyle() {
|
||||
return html`
|
||||
${roundSliderStyle}
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
#root {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.auto {
|
||||
--mode-color: green;
|
||||
}
|
||||
.cool {
|
||||
--mode-color: #2b9af9;
|
||||
}
|
||||
.heat {
|
||||
--mode-color: #FF8100;
|
||||
}
|
||||
.off {
|
||||
--mode-color: #8a8a8a;
|
||||
}
|
||||
.large {
|
||||
--thermostat-padding-top: 25px;
|
||||
--thermostat-margin-bottom: 25px;
|
||||
--title-font-size: 28px;
|
||||
--title-margin-top: 20%;
|
||||
--climate-info-margin-top: 17%;
|
||||
--modes-margin-top: 2%;
|
||||
--set-temperature-font-size: 25px;
|
||||
--current-temperature-font-size: 71px;
|
||||
--current-temperature-margin-top: 10%;
|
||||
--current-temperature-text-padding-left: 15px;
|
||||
--uom-font-size: 20px;
|
||||
--uom-margin-left: -18px;
|
||||
--current-mode-font-size: 18px;
|
||||
--set-temperature-padding-bottom: 5px;
|
||||
}
|
||||
.small {
|
||||
--thermostat-padding-top: 15px;
|
||||
--thermostat-margin-bottom: 15px;
|
||||
--title-font-size: 18px;
|
||||
--title-margin-top: 20%;
|
||||
--climate-info-margin-top: 7.5%;
|
||||
--modes-margin-top: 1%;
|
||||
--set-temperature-font-size: 16px;
|
||||
--current-temperature-font-size: 25px;
|
||||
--current-temperature-margin-top: 5%;
|
||||
--current-temperature-text-padding-left: 7px;
|
||||
--uom-font-size: 12px;
|
||||
--uom-margin-left: -5px;
|
||||
--current-mode-font-size: 14px;
|
||||
--set-temperature-padding-bottom: 0px;
|
||||
}
|
||||
#thermostat {
|
||||
margin: 0 auto var(--thermostat-margin-bottom);
|
||||
padding-top: var(--thermostat-padding-top);
|
||||
}
|
||||
#thermostat .rs-range-color {
|
||||
background-color: var(--mode-color, var(--disabled-text-color));
|
||||
}
|
||||
#thermostat .rs-path-color {
|
||||
background-color: #d6d6d6;
|
||||
}
|
||||
#thermostat .rs-handle {
|
||||
background-color: #FFF;
|
||||
padding: 7px;
|
||||
border: 2px solid #d6d6d6;
|
||||
}
|
||||
#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: transparent;
|
||||
}
|
||||
#thermostat .rs-bar.rs-transition.rs-first, .rs-bar.rs-transition.rs-second{
|
||||
z-index: 20 !important;
|
||||
}
|
||||
#tooltip {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
z-index: 15;
|
||||
}
|
||||
#set-temperature {
|
||||
font-size: var(--set-temperature-font-size);
|
||||
padding-bottom: var(--set-temperature-padding-bottom);
|
||||
}
|
||||
.title {
|
||||
font-size: var(--title-font-size);
|
||||
margin-top: var(--title-margin-top);
|
||||
}
|
||||
.climate-info {
|
||||
margin-top: var(--climate-info-margin-top);
|
||||
}
|
||||
.current-mode {
|
||||
font-size: var(--current-mode-font-size);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.modes {
|
||||
margin-top: var(--modes-margin-top);
|
||||
}
|
||||
.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 {
|
||||
margin-top: var(--current-temperature-margin-top);
|
||||
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);
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
private _dragEvent(e) {
|
||||
this.shadowRoot!.querySelector("#set-temperature")!.innerHTML = formatTemp(
|
||||
String(e.value).split(",")
|
||||
);
|
||||
}
|
||||
|
||||
private _setTemperature(e) {
|
||||
const stateObj = this.hass!.states[this.config!.entity];
|
||||
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,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.hass!.callService("climate", "set_temperature", {
|
||||
entity_id: this.config!.entity,
|
||||
temperature: e.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _renderIcon(mode, currentMode) {
|
||||
return html`<ha-icon
|
||||
class="${classMap({ "selected-icon": currentMode === mode })}"
|
||||
.mode="${mode}"
|
||||
.icon="${modeIcons[mode]}"
|
||||
@click="${this._handleModeClick}"
|
||||
></ha-icon>`;
|
||||
}
|
||||
|
||||
private _handleModeClick(e: MouseEvent) {
|
||||
this.hass!.callService("climate", "set_operation_mode", {
|
||||
entity_id: this.config!.entity,
|
||||
operation_mode: (e.currentTarget as any).mode,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-thermostat-card": HuiThermostatCard;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-thermostat-card", HuiThermostatCard);
|
|
@ -20,6 +20,7 @@ import "../cards/hui-picture-glance-card";
|
|||
import "../cards/hui-plant-status-card.js";
|
||||
import "../cards/hui-sensor-card.js";
|
||||
import "../cards/hui-vertical-stack-card.ts";
|
||||
import "../cards/hui-thermostat-card.ts";
|
||||
import "../cards/hui-weather-forecast-card";
|
||||
import "../cards/hui-gauge-card.js";
|
||||
|
||||
|
@ -46,6 +47,7 @@ const CARD_TYPES = new Set([
|
|||
"picture-glance",
|
||||
"plant-status",
|
||||
"sensor",
|
||||
"thermostat",
|
||||
"vertical-stack",
|
||||
"weather-forecast",
|
||||
]);
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import { html } from "@polymer/lit-element";
|
||||
import "./jquery";
|
||||
import "round-slider";
|
||||
import roundSliderCSS from "round-slider/dist/roundslider.min.css";
|
||||
|
||||
export const roundSliderStyle = html`<style>${roundSliderCSS}</style>`;
|
|
@ -0,0 +1,5 @@
|
|||
import jQuery_ from "jquery";
|
||||
|
||||
(window as any).jQuery = jQuery_;
|
||||
|
||||
export const jQuery = jQuery_;
|
|
@ -63,6 +63,10 @@ function createConfig(isProdBuild, latestBuild) {
|
|||
module: {
|
||||
rules: [
|
||||
babelLoaderConfig({ latestBuild }),
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: "raw-loader",
|
||||
},
|
||||
{
|
||||
test: /\.(html)$/,
|
||||
use: {
|
||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -9080,6 +9080,11 @@ joi@^11.1.1:
|
|||
isemail "3.x.x"
|
||||
topo "2.x.x"
|
||||
|
||||
"jquery@>= 1.4.1", jquery@^3.3.1:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.3.1.tgz#958ce29e81c9790f31be7792df5d4d95fc57fbca"
|
||||
integrity sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==
|
||||
|
||||
js-levenshtein@^1.1.3:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.4.tgz#3a56e3cbf589ca0081eb22cd9ba0b1290a16d26e"
|
||||
|
@ -12838,6 +12843,13 @@ rollup@^0.58.2:
|
|||
"@types/estree" "0.0.38"
|
||||
"@types/node" "*"
|
||||
|
||||
round-slider@^1.3.2:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/round-slider/-/round-slider-1.3.2.tgz#8fb363f4fe2ab653b8160a13aa4d493634bac050"
|
||||
integrity sha512-JAUSXwuxiLv/kliHNP2GbnXID87hFqoxac38UIcvkpyYTwSzpTKlqvMKLB7xWnDOgX/9MCD7B2Ab41pk6cGiWQ==
|
||||
dependencies:
|
||||
jquery ">= 1.4.1"
|
||||
|
||||
run-async@^2.0.0, run-async@^2.2.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
|
||||
|
|
Loading…
Reference in New Issue