Migrate for climate 1.0 (#3333)

* Migrate for climate 1.0

* Update demo

* Fix gallery

* Add preset to thermostat card

* Fix climate entity row
This commit is contained in:
Paulus Schoutsen 2019-07-05 15:13:53 -07:00 committed by GitHub
parent 0a8703ad0a
commit 4fdbec93b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 663 additions and 665 deletions

View File

@ -94,22 +94,19 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
target_temp_high: 24, target_temp_high: 24,
target_temp_low: 20, target_temp_low: 20,
fan_mode: "auto", fan_mode: "auto",
fan_list: ["auto", "on"], fan_modes: ["auto", "on"],
operation_mode: "auto", hvac_modes: ["auto", "cool", "heat", "off"],
operation_list: ["auto", "auxHeatOnly", "cool", "heat", "off"],
hold_mode: null,
away_mode: "off",
aux_heat: "off", aux_heat: "off",
actual_humidity: 30, actual_humidity: 30,
fan: "on", fan: "on",
climate_mode: "Day",
operation: "fan", operation: "fan",
climate_list: ["Away", "Sleep", "Day", "Home"],
fan_min_on_time: 10, fan_min_on_time: 10,
friendly_name: localize( friendly_name: localize(
"ui.panel.page-demo.config.arsaboo.names.upstairs" "ui.panel.page-demo.config.arsaboo.names.upstairs"
), ),
supported_features: 3575, supported_features: 27,
preset_mode: "away",
preset_modes: ["home", "away", "eco", "sleep"],
}, },
}, },
"input_boolean.abodeupdate": { "input_boolean.abodeupdate": {

View File

@ -14,14 +14,14 @@ const ENTITIES = [
target_temp_high: 75, target_temp_high: 75,
target_temp_low: 70, target_temp_low: 70,
fan_mode: "Auto Low", fan_mode: "Auto Low",
fan_list: ["On Low", "On High", "Auto Low", "Auto High", "Off"], fan_modes: ["On Low", "On High", "Auto Low", "Auto High", "Off"],
operation_mode: "auto", hvac_modes: ["heat", "cool", "auto", "off"],
operation_list: ["heat", "cool", "auto", "off"],
hold_mode: "home",
swing_mode: "Auto", swing_mode: "Auto",
swing_list: ["Auto", "1", "2", "3", "Off"], swing_modes: ["Auto", "1", "2", "3", "Off"],
friendly_name: "Ecobee", friendly_name: "Ecobee",
supported_features: 1014, supported_features: 59,
preset_mode: "eco",
preset_modes: ["away", "eco"],
}), }),
getEntity("climate", "nest", "heat", { getEntity("climate", "nest", "heat", {
current_temperature: 17, current_temperature: 17,
@ -29,14 +29,12 @@ const ENTITIES = [
max_temp: 25, max_temp: 25,
temperature: 19, temperature: 19,
fan_mode: "Auto Low", fan_mode: "Auto Low",
fan_list: ["On Low", "On High", "Auto Low", "Auto High", "Off"], fan_modes: ["On Low", "On High", "Auto Low", "Auto High", "Off"],
operation_mode: "heat", hvac_modes: ["heat", "cool", "auto", "off"],
operation_list: ["heat", "cool", "auto", "off"],
hold_mode: "home",
swing_mode: "Auto", swing_mode: "Auto",
swing_list: ["Auto", "1", "2", "3", "Off"], swing_modes: ["Auto", "1", "2", "3", "Off"],
friendly_name: "Nest", friendly_name: "Nest",
supported_features: 1014, supported_features: 43,
}), }),
]; ];

View File

@ -56,7 +56,7 @@ class HaGallery extends PolymerElement {
color: var(--primary-color); color: var(--primary-color);
} }
a paper-item { a {
color: var(--primary-text-color); color: var(--primary-text-color);
text-decoration: none; text-decoration: none;
} }
@ -138,12 +138,22 @@ class HaGallery extends PolymerElement {
</template> </template>
</div> </div>
</app-header-layout> </app-header-layout>
<notification-manager id='notifications'></notification-manager> <notification-manager hass=[[_fakeHass]] id='notifications'></notification-manager>
`; `;
} }
static get properties() { static get properties() {
return { return {
_fakeHass: {
type: Object,
// Just enough for computeRTL
value: {
language: "en",
translationMetadata: {
translations: {},
},
},
},
_demo: { _demo: {
type: String, type: String,
value: document.location.hash.substr(1), value: document.location.hash.substr(1),

View File

@ -44,8 +44,8 @@ module.exports = {
to: "static/images/leaflet/", to: "static/images/leaflet/",
}, },
{ {
from: "../node_modules/@polymer/font-roboto-local/fonts", from: "../node_modules/roboto-fontface/fonts/roboto/*.woff2",
to: "static/fonts", to: "static/fonts/roboto/",
}, },
{ {
from: "../node_modules/leaflet/dist/images", from: "../node_modules/leaflet/dist/images",

View File

@ -38,7 +38,12 @@ class HaClimateState extends LocalizeMixin(PolymerElement) {
<div class="target"> <div class="target">
<template is="dom-if" if="[[_hasKnownState(stateObj.state)]]"> <template is="dom-if" if="[[_hasKnownState(stateObj.state)]]">
<span class="state-label"> [[_localizeState(stateObj.state)]] </span> <span class="state-label">
[[_localizeState(localize, stateObj.state)]]
<template is="dom-if" if="[[stateObj.attributes.preset_mode]]">
- [[_localizePreset(localize, stateObj.attributes.preset_mode)]]
</template>
</span>
</template> </template>
<div class="unit">[[computeTarget(hass, stateObj)]]</div> <div class="unit">[[computeTarget(hass, stateObj)]]</div>
</div> </div>
@ -83,7 +88,7 @@ class HaClimateState extends LocalizeMixin(PolymerElement) {
stateObj.attributes.target_temp_low != null && stateObj.attributes.target_temp_low != null &&
stateObj.attributes.target_temp_high != null stateObj.attributes.target_temp_high != null
) { ) {
return `${stateObj.attributes.target_temp_low} - ${ return `${stateObj.attributes.target_temp_low}-${
stateObj.attributes.target_temp_high stateObj.attributes.target_temp_high
} ${hass.config.unit_system.temperature}`; } ${hass.config.unit_system.temperature}`;
} }
@ -96,9 +101,9 @@ class HaClimateState extends LocalizeMixin(PolymerElement) {
stateObj.attributes.target_humidity_low != null && stateObj.attributes.target_humidity_low != null &&
stateObj.attributes.target_humidity_high != null stateObj.attributes.target_humidity_high != null
) { ) {
return `${stateObj.attributes.target_humidity_low} - ${ return `${stateObj.attributes.target_humidity_low}-${
stateObj.attributes.target_humidity_high stateObj.attributes.target_humidity_high
} %`; }%`;
} }
if (stateObj.attributes.humidity != null) { if (stateObj.attributes.humidity != null) {
return `${stateObj.attributes.humidity} %`; return `${stateObj.attributes.humidity} %`;
@ -111,8 +116,12 @@ class HaClimateState extends LocalizeMixin(PolymerElement) {
return state !== "unknown"; return state !== "unknown";
} }
_localizeState(state) { _localizeState(localize, state) {
return this.localize(`state.climate.${state}`) || state; return localize(`state.climate.${state}`) || state;
}
_localizePreset(localize, preset) {
return localize(`state_attributes.climate.preset_mode.${preset}`) || preset;
} }
} }
customElements.define("ha-climate-state", HaClimateState); customElements.define("ha-climate-state", HaClimateState);

51
src/data/climate.ts Normal file
View File

@ -0,0 +1,51 @@
import {
HassEntityBase,
HassEntityAttributeBase,
} from "home-assistant-js-websocket";
export type HvacMode =
| "off"
| "heat"
| "cool"
| "heat_cool"
| "auto"
| "dry"
| "fan_only";
export type HvacAction = "off" | "Heating" | "cooling" | "drying" | "idle";
export type ClimateEntity = HassEntityBase & {
attributes: HassEntityAttributeBase & {
hvac_mode: HvacMode;
hvac_modes: HvacMode[];
hvac_action?: HvacAction;
current_temperature: number;
min_temp: number;
max_temp: number;
temperature: number;
target_temp_step?: number;
target_temp_high?: number;
target_temp_low?: number;
humidity?: number;
current_humidity?: number;
target_humidity_low?: number;
target_humidity_high?: number;
min_humidity?: number;
max_humidity?: number;
fan_mode?: string;
fan_modes?: string[];
preset_mode?: string;
preset_modes?: string[];
swing_mode?: string;
swing_modes?: string[];
aux_heat?: "on" | "off";
};
};
export const CLIMATE_SUPPORT_TARGET_TEMPERATURE = 1;
export const CLIMATE_SUPPORT_TARGET_TEMPERATURE_RANGE = 2;
export const CLIMATE_SUPPORT_TARGET_HUMIDITY = 4;
export const CLIMATE_SUPPORT_FAN_MODE = 8;
export const CLIMATE_SUPPORT_PRESET_MODE = 16;
export const CLIMATE_SUPPORT_SWING_MODE = 32;
export const CLIMATE_SUPPORT_AUX_HEAT = 64;

View File

@ -1,533 +0,0 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-toggle-button/paper-toggle-button";
import { timeOut } from "@polymer/polymer/lib/utils/async";
import { Debouncer } from "@polymer/polymer/lib/utils/debounce";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-climate-control";
import "../../../components/ha-paper-slider";
import "../../../components/ha-paper-dropdown-menu";
import attributeClassNames from "../../../common/entity/attribute_class_names";
import featureClassNames from "../../../common/entity/feature_class_names";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { EventsMixin } from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
import { computeRTLDirection } from "../../../common/util/compute_rtl";
/*
* @appliesMixin EventsMixin
* @appliesMixin LocalizeMixin
*/
class MoreInfoClimate extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() {
return html`
<style include="iron-flex"></style>
<style>
:host {
color: var(--primary-text-color);
}
.container-on,
.container-away_mode,
.container-aux_heat,
.container-temperature,
.container-humidity,
.container-operation_list,
.container-fan_list,
.container-swing_list {
display: none;
}
.has-on .container-on,
.has-away_mode .container-away_mode,
.has-aux_heat .container-aux_heat,
.has-target_temperature .container-temperature,
.has-target_temperature_low .container-temperature,
.has-target_temperature_high .container-temperature,
.has-target_humidity .container-humidity,
.has-operation_mode .container-operation_list,
.has-fan_mode .container-fan_list,
.has-swing_list .container-swing_list,
.has-swing_mode .container-swing_list {
display: block;
margin-bottom: 5px;
}
.container-operation_list iron-icon,
.container-fan_list iron-icon,
.container-swing_list iron-icon {
margin: 22px 16px 0 0;
}
ha-paper-dropdown-menu {
width: 100%;
}
paper-item {
cursor: pointer;
}
ha-paper-slider {
width: 100%;
}
.container-humidity .single-row {
display: flex;
height: 50px;
}
.target-humidity {
width: 90px;
font-size: 200%;
margin: auto;
direction: ltr;
}
ha-climate-control.range-control-left,
ha-climate-control.range-control-right {
float: left;
width: 46%;
}
ha-climate-control.range-control-left {
margin-right: 4%;
}
ha-climate-control.range-control-right {
margin-left: 4%;
}
.humidity {
--paper-slider-active-color: var(--paper-blue-400);
--paper-slider-secondary-color: var(--paper-blue-400);
}
.single-row {
padding: 8px 0;
}
}
</style>
<div class$="[[computeClassNames(stateObj)]]">
<template is="dom-if" if="[[supportsOn(stateObj)]]">
<div class="container-on">
<div class="center horizontal layout single-row">
<div class="flex">[[localize('ui.card.climate.on_off')]]</div>
<paper-toggle-button
checked="[[onToggleChecked]]"
on-change="onToggleChanged"
>
</paper-toggle-button>
</div>
</div>
</template>
<div class="container-temperature">
<div class$="[[stateObj.attributes.operation_mode]]">
<div hidden$="[[!supportsTemperatureControls(stateObj)]]">
[[localize('ui.card.climate.target_temperature')]]
</div>
<template is="dom-if" if="[[supportsTemperature(stateObj)]]">
<ha-climate-control
value="[[stateObj.attributes.temperature]]"
units="[[hass.config.unit_system.temperature]]"
step="[[computeTemperatureStepSize(hass, stateObj)]]"
min="[[stateObj.attributes.min_temp]]"
max="[[stateObj.attributes.max_temp]]"
on-change="targetTemperatureChanged"
>
</ha-climate-control>
</template>
<template is="dom-if" if="[[supportsTemperatureRange(stateObj)]]">
<ha-climate-control
value="[[stateObj.attributes.target_temp_low]]"
units="[[hass.config.unit_system.temperature]]"
step="[[computeTemperatureStepSize(hass, stateObj)]]"
min="[[stateObj.attributes.min_temp]]"
max="[[stateObj.attributes.target_temp_high]]"
class="range-control-left"
on-change="targetTemperatureLowChanged"
>
</ha-climate-control>
<ha-climate-control
value="[[stateObj.attributes.target_temp_high]]"
units="[[hass.config.unit_system.temperature]]"
step="[[computeTemperatureStepSize(hass, stateObj)]]"
min="[[stateObj.attributes.target_temp_low]]"
max="[[stateObj.attributes.max_temp]]"
class="range-control-right"
on-change="targetTemperatureHighChanged"
>
</ha-climate-control>
</template>
</div>
</div>
<template is="dom-if" if="[[supportsHumidity(stateObj)]]">
<div class="container-humidity">
<div>[[localize('ui.card.climate.target_humidity')]]</div>
<div class="single-row">
<div class="target-humidity">
[[stateObj.attributes.humidity]] %
</div>
<ha-paper-slider
class="humidity"
min="[[stateObj.attributes.min_humidity]]"
max="[[stateObj.attributes.max_humidity]]"
secondary-progress="[[stateObj.attributes.max_humidity]]"
step="1"
pin=""
value="[[stateObj.attributes.humidity]]"
on-change="targetHumiditySliderChanged"
ignore-bar-touch=""
dir="[[rtl]]"
>
</ha-paper-slider>
</div>
</div>
</template>
<template is="dom-if" if="[[supportsOperationMode(stateObj)]]">
<div class="container-operation_list">
<div class="controls">
<ha-paper-dropdown-menu
label-float=""
dynamic-align=""
label="[[localize('ui.card.climate.operation')]]"
>
<paper-listbox
slot="dropdown-content"
selected="[[stateObj.attributes.operation_mode]]"
attr-for-selected="item-name"
on-selected-changed="handleOperationmodeChanged"
>
<template
is="dom-repeat"
items="[[stateObj.attributes.operation_list]]"
>
<paper-item item-name$="[[item]]"
>[[_localizeOperationMode(localize, item)]]</paper-item
>
</template>
</paper-listbox>
</ha-paper-dropdown-menu>
</div>
</div>
</template>
<template is="dom-if" if="[[supportsFanMode(stateObj)]]">
<div class="container-fan_list">
<ha-paper-dropdown-menu
label-float=""
dynamic-align=""
label="[[localize('ui.card.climate.fan_mode')]]"
>
<paper-listbox
slot="dropdown-content"
selected="[[stateObj.attributes.fan_mode]]"
attr-for-selected="item-name"
on-selected-changed="handleFanmodeChanged"
>
<template
is="dom-repeat"
items="[[stateObj.attributes.fan_list]]"
>
<paper-item item-name$="[[item]]"
>[[_localizeFanMode(localize, item)]]
</paper-item>
</template>
</paper-listbox>
</ha-paper-dropdown-menu>
</div>
</template>
<template is="dom-if" if="[[supportsSwingMode(stateObj)]]">
<div class="container-swing_list">
<ha-paper-dropdown-menu
label-float=""
dynamic-align=""
label="[[localize('ui.card.climate.swing_mode')]]"
>
<paper-listbox
slot="dropdown-content"
selected="[[stateObj.attributes.swing_mode]]"
attr-for-selected="item-name"
on-selected-changed="handleSwingmodeChanged"
>
<template
is="dom-repeat"
items="[[stateObj.attributes.swing_list]]"
>
<paper-item item-name$="[[item]]">[[item]]</paper-item>
</template>
</paper-listbox>
</ha-paper-dropdown-menu>
</div>
</template>
<template is="dom-if" if="[[supportsAwayMode(stateObj)]]">
<div class="container-away_mode">
<div class="center horizontal layout single-row">
<div class="flex">[[localize('ui.card.climate.away_mode')]]</div>
<paper-toggle-button
checked="[[awayToggleChecked]]"
on-change="awayToggleChanged"
>
</paper-toggle-button>
</div>
</div>
</template>
<template is="dom-if" if="[[supportsAuxHeat(stateObj)]]">
<div class="container-aux_heat">
<div class="center horizontal layout single-row">
<div class="flex">[[localize('ui.card.climate.aux_heat')]]</div>
<paper-toggle-button
checked="[[auxToggleChecked]]"
on-change="auxToggleChanged"
>
</paper-toggle-button>
</div>
</div>
</template>
</div>
`;
}
static get properties() {
return {
hass: {
type: Object,
},
stateObj: {
type: Object,
observer: "stateObjChanged",
},
awayToggleChecked: Boolean,
auxToggleChecked: Boolean,
onToggleChecked: Boolean,
rtl: {
type: String,
value: "ltr",
computed: "_computeRTLDirection(hass)",
},
};
}
stateObjChanged(newVal, oldVal) {
if (newVal) {
this.setProperties({
awayToggleChecked: newVal.attributes.away_mode === "on",
auxToggleChecked: newVal.attributes.aux_heat === "on",
onToggleChecked: newVal.state !== "off",
});
}
if (oldVal) {
this._debouncer = Debouncer.debounce(
this._debouncer,
timeOut.after(500),
() => {
this.fire("iron-resize");
}
);
}
}
computeTemperatureStepSize(hass, stateObj) {
if (stateObj.attributes.target_temp_step) {
return stateObj.attributes.target_temp_step;
}
if (hass.config.unit_system.temperature.indexOf("F") !== -1) {
return 1;
}
return 0.5;
}
supportsTemperatureControls(stateObj) {
return (
this.supportsTemperature(stateObj) ||
this.supportsTemperatureRange(stateObj)
);
}
supportsTemperature(stateObj) {
return (
supportsFeature(stateObj, 1) &&
typeof stateObj.attributes.temperature === "number"
);
}
supportsTemperatureRange(stateObj) {
return (
supportsFeature(stateObj, 6) &&
(typeof stateObj.attributes.target_temp_low === "number" ||
typeof stateObj.attributes.target_temp_high === "number")
);
}
supportsHumidity(stateObj) {
return supportsFeature(stateObj, 8);
}
supportsFanMode(stateObj) {
return supportsFeature(stateObj, 64);
}
supportsOperationMode(stateObj) {
return supportsFeature(stateObj, 128);
}
supportsSwingMode(stateObj) {
return supportsFeature(stateObj, 512);
}
supportsAwayMode(stateObj) {
return supportsFeature(stateObj, 1024);
}
supportsAuxHeat(stateObj) {
return supportsFeature(stateObj, 2048);
}
supportsOn(stateObj) {
return supportsFeature(stateObj, 4096);
}
computeClassNames(stateObj) {
const _featureClassNames = {
1: "has-target_temperature",
2: "has-target_temperature_high",
4: "has-target_temperature_low",
8: "has-target_humidity",
16: "has-target_humidity_high",
32: "has-target_humidity_low",
64: "has-fan_mode",
128: "has-operation_mode",
256: "has-hold_mode",
512: "has-swing_mode",
1024: "has-away_mode",
2048: "has-aux_heat",
4096: "has-on",
};
var classes = [
attributeClassNames(stateObj, [
"current_temperature",
"current_humidity",
]),
featureClassNames(stateObj, _featureClassNames),
];
classes.push("more-info-climate");
return classes.join(" ");
}
targetTemperatureChanged(ev) {
const temperature = ev.target.value;
if (temperature === this.stateObj.attributes.temperature) return;
this.callServiceHelper("set_temperature", { temperature: temperature });
}
targetTemperatureLowChanged(ev) {
const targetTempLow = ev.currentTarget.value;
if (targetTempLow === this.stateObj.attributes.target_temp_low) return;
this.callServiceHelper("set_temperature", {
target_temp_low: targetTempLow,
target_temp_high: this.stateObj.attributes.target_temp_high,
});
}
targetTemperatureHighChanged(ev) {
const targetTempHigh = ev.currentTarget.value;
if (targetTempHigh === this.stateObj.attributes.target_temp_high) return;
this.callServiceHelper("set_temperature", {
target_temp_low: this.stateObj.attributes.target_temp_low,
target_temp_high: targetTempHigh,
});
}
targetHumiditySliderChanged(ev) {
const humidity = ev.target.value;
if (humidity === this.stateObj.attributes.humidity) return;
this.callServiceHelper("set_humidity", { humidity: humidity });
}
awayToggleChanged(ev) {
const oldVal = this.stateObj.attributes.away_mode === "on";
const newVal = ev.target.checked;
if (oldVal === newVal) return;
this.callServiceHelper("set_away_mode", { away_mode: newVal });
}
auxToggleChanged(ev) {
const oldVal = this.stateObj.attributes.aux_heat === "on";
const newVal = ev.target.checked;
if (oldVal === newVal) return;
this.callServiceHelper("set_aux_heat", { aux_heat: newVal });
}
onToggleChanged(ev) {
const oldVal = this.stateObj.state !== "off";
const newVal = ev.target.checked;
if (oldVal === newVal) return;
this.callServiceHelper(newVal ? "turn_on" : "turn_off", {});
}
handleFanmodeChanged(ev) {
const oldVal = this.stateObj.attributes.fan_mode;
const newVal = ev.detail.value;
if (!newVal || oldVal === newVal) return;
this.callServiceHelper("set_fan_mode", { fan_mode: newVal });
}
handleOperationmodeChanged(ev) {
const oldVal = this.stateObj.attributes.operation_mode;
const newVal = ev.detail.value;
if (!newVal || oldVal === newVal) return;
this.callServiceHelper("set_operation_mode", {
operation_mode: newVal,
});
}
handleSwingmodeChanged(ev) {
const oldVal = this.stateObj.attributes.swing_mode;
const newVal = ev.detail.value;
if (!newVal || oldVal === newVal) return;
this.callServiceHelper("set_swing_mode", { swing_mode: newVal });
}
callServiceHelper(service, data) {
// We call stateChanged after a successful call to re-sync the inputs
// with the state. It will be out of sync if our service call did not
// result in the entity to be turned on. Since the state is not changing,
// the resync is not called automatic.
/* eslint-disable no-param-reassign */
data.entity_id = this.stateObj.entity_id;
/* eslint-enable no-param-reassign */
this.hass.callService("climate", service, data).then(() => {
this.stateObjChanged(this.stateObj);
});
}
_localizeOperationMode(localize, mode) {
return localize(`state.climate.${mode}`) || mode;
}
_localizeFanMode(localize, mode) {
return localize(`state_attributes.climate.fan_mode.${mode}`) || mode;
}
_computeRTLDirection(hass) {
return computeRTLDirection(hass);
}
}
customElements.define("more-info-climate", MoreInfoClimate);

View File

@ -0,0 +1,501 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-toggle-button/paper-toggle-button";
import {
LitElement,
html,
TemplateResult,
CSSResult,
css,
property,
PropertyValues,
} from "lit-element";
import "../../../components/ha-climate-control";
import "../../../components/ha-paper-slider";
import "../../../components/ha-paper-dropdown-menu";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { computeRTLDirection } from "../../../common/util/compute_rtl";
import { HomeAssistant } from "../../../types";
import {
ClimateEntity,
CLIMATE_SUPPORT_TARGET_TEMPERATURE,
CLIMATE_SUPPORT_TARGET_TEMPERATURE_RANGE,
CLIMATE_SUPPORT_TARGET_HUMIDITY,
CLIMATE_SUPPORT_FAN_MODE,
CLIMATE_SUPPORT_SWING_MODE,
CLIMATE_SUPPORT_AUX_HEAT,
CLIMATE_SUPPORT_PRESET_MODE,
} from "../../../data/climate";
import { fireEvent } from "../../../common/dom/fire_event";
import { classMap } from "lit-html/directives/class-map";
class MoreInfoClimate extends LitElement {
@property() public hass!: HomeAssistant;
@property() public stateObj?: ClimateEntity;
private _resizeDebounce?: number;
protected render(): TemplateResult | void {
if (!this.stateObj) {
return html``;
}
const hass = this.hass;
const stateObj = this.stateObj;
const supportTargetTemperature = supportsFeature(
stateObj,
CLIMATE_SUPPORT_TARGET_TEMPERATURE
);
const supportTargetTemperatureRange = supportsFeature(
stateObj,
CLIMATE_SUPPORT_TARGET_TEMPERATURE_RANGE
);
const supportTargetHumidity = supportsFeature(
stateObj,
CLIMATE_SUPPORT_TARGET_HUMIDITY
);
const supportFanMode = supportsFeature(stateObj, CLIMATE_SUPPORT_FAN_MODE);
const supportPresetMode = supportsFeature(
stateObj,
CLIMATE_SUPPORT_PRESET_MODE
);
const supportSwingMode = supportsFeature(
stateObj,
CLIMATE_SUPPORT_SWING_MODE
);
const supportAuxHeat = supportsFeature(stateObj, CLIMATE_SUPPORT_AUX_HEAT);
const temperatureStepSize =
stateObj.attributes.target_temp_step ||
hass.config.unit_system.temperature.indexOf("F") === -1
? 0.5
: 1;
const rtlDirection = computeRTLDirection(hass);
return html`
<div
class=${classMap({
"has-current_temperature":
"current_temperature" in stateObj.attributes,
"has-current_humidity": "current_humidity" in stateObj.attributes,
"has-target_temperature": supportTargetTemperature,
"has-target_temperature_range": supportTargetTemperatureRange,
"has-target_humidity": supportTargetHumidity,
"has-fan_mode": supportFanMode,
"has-swing_mode": supportSwingMode,
"has-aux_heat": supportAuxHeat,
"has-preset_mode": supportPresetMode,
})}
>
<div class="container-temperature">
<div class=${stateObj.state}>
${supportTargetTemperature || supportTargetTemperatureRange
? html`
<div>
${hass.localize("ui.card.climate.target_temperature")}
</div>
`
: ""}
${stateObj.attributes.temperature
? html`
<ha-climate-control
.value=${stateObj.attributes.temperature}
.units=${hass.config.unit_system.temperature}
.step=${temperatureStepSize}
.min=${stateObj.attributes.min_temp}
.max=${stateObj.attributes.max_temp}
@change=${this._targetTemperatureChanged}
></ha-climate-control>
`
: ""}
${stateObj.attributes.target_temp_low ||
stateObj.attributes.target_temp_high
? html`
<ha-climate-control
.value=${stateObj.attributes.target_temp_low}
.units=${hass.config.unit_system.temperature}
.step=${temperatureStepSize}
.min=${stateObj.attributes.min_temp}
.max=${stateObj.attributes.target_temp_high}
class="range-control-left"
@change=${this._targetTemperatureLowChanged}
></ha-climate-control>
<ha-climate-control
.value=${stateObj.attributes.target_temp_high}
.units=${hass.config.unit_system.temperature}
.step=${temperatureStepSize}
.min=${stateObj.attributes.target_temp_low}
.max=${stateObj.attributes.max_temp}
class="range-control-right"
@change=${this._targetTemperatureHighChanged}
></ha-climate-control>
`
: ""}
</div>
</div>
${supportTargetHumidity
? html`
<div class="container-humidity">
<div>${hass.localize("ui.card.climate.target_humidity")}</div>
<div class="single-row">
<div class="target-humidity">
${stateObj.attributes.humidity} %
</div>
<ha-paper-slider
class="humidity"
step="1"
pin
ignore-bar-touch
dir=${rtlDirection}
.min=${stateObj.attributes.min_humidity}
.max=${stateObj.attributes.max_humidity}
.secondaryProgress=${stateObj.attributes.max_humidity}
.value=${stateObj.attributes.humidity}
@change=${this._targetHumiditySliderChanged}
>
</ha-paper-slider>
</div>
</div>
`
: ""}
<div class="container-hvac_modes">
<div class="controls">
<ha-paper-dropdown-menu
label-float
dynamic-align
.label=${hass.localize("ui.card.climate.operation")}
>
<paper-listbox
slot="dropdown-content"
attr-for-selected="item-name"
.selected=${stateObj.state}
@selected-changed=${this._handleOperationmodeChanged}
>
${stateObj.attributes.hvac_modes.map(
(mode) => html`
<paper-item item-name=${mode}>
${hass.localize(`state.climate.${mode}`)}
</paper-item>
`
)}
</paper-listbox>
</ha-paper-dropdown-menu>
</div>
</div>
${supportPresetMode
? html`
<div class="container-preset_modes">
<ha-paper-dropdown-menu
label-float
dynamic-align
.label=${hass.localize("ui.card.climate.preset_mode")}
>
<paper-listbox
slot="dropdown-content"
attr-for-selected="item-name"
.selected=${stateObj.attributes.preset_mode}
@selected-changed=${this._handlePresetmodeChanged}
>
<paper-item item-name="">
${hass.localize(
`state_attributes.climate.preset_mode.none`
)}
</paper-item>
${stateObj.attributes.preset_modes!.map(
(mode) => html`
<paper-item item-name=${mode}>
${hass.localize(
`state_attributes.climate.preset_mode.${mode}`
) || mode}
</paper-item>
`
)}
</paper-listbox>
</ha-paper-dropdown-menu>
</div>
`
: ""}
${supportFanMode
? html`
<div class="container-fan_list">
<ha-paper-dropdown-menu
label-float
dynamic-align
.label=${hass.localize("ui.card.climate.fan_mode")}
>
<paper-listbox
slot="dropdown-content"
attr-for-selected="item-name"
.selected=${stateObj.attributes.fan_mode}
@selected-changed=${this._handleFanmodeChanged}
>
${stateObj.attributes.fan_modes!.map(
(mode) => html`
<paper-item item-name=${mode}>
${hass.localize(
`state_attributes.climate.fan_mode.${mode}`
) || mode}
</paper-item>
`
)}
</paper-listbox>
</ha-paper-dropdown-menu>
</div>
`
: ""}
${supportSwingMode
? html`
<div class="container-swing_list">
<ha-paper-dropdown-menu
label-float
dynamic-align
.label=${hass.localize("ui.card.climate.swing_mode")}
>
<paper-listbox
slot="dropdown-content"
attr-for-selected="item-name"
.selected=${stateObj.attributes.swing_mode}
@selected-changed=${this._handleSwingmodeChanged}
>
${stateObj.attributes.swing_modes!.map(
(mode) => html`
<paper-item item-name=${mode}>${mode}</paper-item>
`
)}
</paper-listbox>
</ha-paper-dropdown-menu>
</div>
`
: ""}
${supportAuxHeat
? html`
<div class="container-aux_heat">
<div class="center horizontal layout single-row">
<div class="flex">
${hass.localize("ui.card.climate.aux_heat")}
</div>
<paper-toggle-button
.checked=${stateObj.attributes.aux_heat === "on"}
@change=${this._auxToggleChanged}
></paper-toggle-button>
</div>
</div>
`
: ""}
</div>
`;
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (!changedProps.has("stateObj") || !this.stateObj) {
return;
}
if (this._resizeDebounce) {
clearTimeout(this._resizeDebounce);
}
this._resizeDebounce = window.setTimeout(() => {
fireEvent(this, "iron-resize");
this._resizeDebounce = undefined;
}, 500);
}
private _targetTemperatureChanged(ev) {
const newVal = ev.target.value;
this._callServiceHelper(
this.stateObj!.attributes.temperature,
newVal,
"set_temperature",
{ temperature: newVal }
);
}
private _targetTemperatureLowChanged(ev) {
const newVal = ev.currentTarget.value;
this._callServiceHelper(
this.stateObj!.attributes.target_temp_low,
newVal,
"set_temperature",
{
target_temp_low: newVal,
target_temp_high: this.stateObj!.attributes.target_temp_high,
}
);
}
private _targetTemperatureHighChanged(ev) {
const newVal = ev.currentTarget.value;
this._callServiceHelper(
this.stateObj!.attributes.target_temp_high,
newVal,
"set_temperature",
{
target_temp_low: this.stateObj!.attributes.target_temp_low,
target_temp_high: newVal,
}
);
}
private _targetHumiditySliderChanged(ev) {
const newVal = ev.target.value;
this._callServiceHelper(
this.stateObj!.attributes.humidity,
newVal,
"set_humidity",
{ humidity: newVal }
);
}
private _auxToggleChanged(ev) {
const newVal = ev.target.checked;
this._callServiceHelper(
this.stateObj!.attributes.aux_heat === "on",
newVal,
"set_aux_heat",
{ aux_heat: newVal }
);
}
private _handleFanmodeChanged(ev) {
const newVal = ev.detail.value;
this._callServiceHelper(
this.stateObj!.attributes.fan_mode,
newVal,
"set_fan_mode",
{ fan_mode: newVal }
);
}
private _handleOperationmodeChanged(ev) {
const newVal = ev.detail.value;
this._callServiceHelper(this.stateObj!.state, newVal, "set_hvac_mode", {
hvac_mode: newVal,
});
}
private _handleSwingmodeChanged(ev) {
const newVal = ev.detail.value;
this._callServiceHelper(
this.stateObj!.attributes.swing_mode,
newVal,
"set_swing_mode",
{ swing_mode: newVal }
);
}
private _handlePresetmodeChanged(ev) {
const newVal = ev.detail.value || null;
this._callServiceHelper(
this.stateObj!.attributes.preset_mode,
newVal,
"set_preset_mode",
{ preset_mode: newVal }
);
}
private async _callServiceHelper(
oldVal: unknown,
newVal: unknown,
service: string,
data: {
entity_id?: string;
[key: string]: unknown;
}
) {
if (oldVal === newVal) {
return;
}
data.entity_id = this.stateObj!.entity_id;
const curState = this.stateObj;
await this.hass.callService("climate", service, data);
// We reset stateObj to re-sync the inputs with the state. It will be out
// of sync if our service call did not result in the entity to be turned
// on. Since the state is not changing, the resync is not called automatic.
await new Promise((resolve) => setTimeout(resolve, 2000));
// No need to resync if we received a new state.
if (this.stateObj !== curState) {
return;
}
this.stateObj = undefined;
await this.updateComplete;
// Only restore if not set yet by a state change
if (this.stateObj === undefined) {
this.stateObj = curState;
}
}
static get styles(): CSSResult {
return css`
:host {
color: var(--primary-text-color);
}
.container-hvac_modes iron-icon,
.container-fan_list iron-icon,
.container-swing_list iron-icon {
margin: 22px 16px 0 0;
}
ha-paper-dropdown-menu {
width: 100%;
}
paper-item {
cursor: pointer;
}
ha-paper-slider {
width: 100%;
}
.container-humidity .single-row {
display: flex;
height: 50px;
}
.target-humidity {
width: 90px;
font-size: 200%;
margin: auto;
direction: ltr;
}
ha-climate-control.range-control-left,
ha-climate-control.range-control-right {
float: left;
width: 46%;
}
ha-climate-control.range-control-left {
margin-right: 4%;
}
ha-climate-control.range-control-right {
margin-left: 4%;
}
.humidity {
--paper-slider-active-color: var(--paper-blue-400);
--paper-slider-secondary-color: var(--paper-blue-400);
}
.single-row {
padding: 8px 0;
}
`;
}
}
customElements.define("more-info-climate", MoreInfoClimate);

View File

@ -619,26 +619,6 @@ export const demoServices: HassServices = {
}, },
}, },
climate: { climate: {
set_away_mode: {
description: "Turn away mode on/off for climate device.",
fields: {
entity_id: {
description: "Name(s) of entities to change.",
example: "climate.kitchen",
},
away_mode: { description: "New value of away mode.", example: "true" },
},
},
set_hold_mode: {
description: "Turn hold mode for climate device.",
fields: {
entity_id: {
description: "Name(s) of entities to change.",
example: "climate.kitchen",
},
hold_mode: { description: "New value of hold mode", example: "away" },
},
},
set_aux_heat: { set_aux_heat: {
description: "Turn auxiliary heater on/off for climate device.", description: "Turn auxiliary heater on/off for climate device.",
fields: { fields: {
@ -701,16 +681,16 @@ export const demoServices: HassServices = {
fan_mode: { description: "New value of fan mode.", example: "On Low" }, fan_mode: { description: "New value of fan mode.", example: "On Low" },
}, },
}, },
set_operation_mode: { set_hvac_mode: {
description: "Set operation mode for climate device.", description: "Set operation mode for climate device.",
fields: { fields: {
entity_id: { entity_id: {
description: "Name(s) of entities to change.", description: "Name(s) of entities to change.",
example: "climate.nest", example: "climate.nest",
}, },
operation_mode: { hvac_mode: {
description: "New value of operation mode.", description: "New value of operation mode.",
example: "Heat", example: "heat",
}, },
}, },
}, },
@ -724,24 +704,6 @@ export const demoServices: HassServices = {
swing_mode: { description: "New value of swing mode.", example: "" }, swing_mode: { description: "New value of swing mode.", example: "" },
}, },
}, },
turn_off: {
description: "Turn climate device off.",
fields: {
entity_id: {
description: "Name(s) of entities to change.",
example: "climate.kitchen",
},
},
},
turn_on: {
description: "Turn climate device on.",
fields: {
entity_id: {
description: "Name(s) of entities to change.",
example: "climate.kitchen",
},
},
},
}, },
image_processing: { image_processing: {
scan: { scan: {

View File

@ -209,11 +209,24 @@ class ClimateEntity extends Entity {
return; return;
} }
if (service === "set_operation_mode") { if (service === "set_hvac_mode") {
this.update( this.update(data.hvac_mode, this.attributes);
data.operation_mode === "heat" ? "heat" : data.operation_mode, } else if (
{ ...this.attributes, operation_mode: data.operation_mode } [
); "set_temperature",
"set_humidity",
"set_hvac_mode",
"set_fan_mode",
"set_preset_mode",
"set_swing_mode",
"set_aux_heat",
].includes(service)
) {
const { entity_id, ...toSet } = data;
this.update(this.state, {
...this.attributes,
...toSet,
});
} else { } else {
super.handleService(domain, service, data); super.handleService(domain, service, data);
} }

View File

@ -17,12 +17,13 @@ import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
import computeStateName from "../../../common/entity/compute_state_name"; import computeStateName from "../../../common/entity/compute_state_name";
import { hasConfigOrEntityChanged } from "../common/has-changed"; import { hasConfigOrEntityChanged } from "../common/has-changed";
import { HomeAssistant, ClimateEntity } from "../../../types"; import { HomeAssistant } from "../../../types";
import { LovelaceCard, LovelaceCardEditor } from "../types"; import { LovelaceCard, LovelaceCardEditor } from "../types";
import { loadRoundslider } from "../../../resources/jquery.roundslider.ondemand"; import { loadRoundslider } from "../../../resources/jquery.roundslider.ondemand";
import { UNIT_F } from "../../../common/const"; import { UNIT_F } from "../../../common/const";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { ThermostatCardConfig } from "./types"; import { ThermostatCardConfig } from "./types";
import { ClimateEntity, HvacMode } from "../../../data/climate";
const thermostatConfig = { const thermostatConfig = {
radius: 150, radius: 150,
@ -35,16 +36,14 @@ const thermostatConfig = {
animation: false, animation: false,
}; };
const modeIcons = { const modeIcons: { [mode in HvacMode]: string } = {
auto: "hass:autorenew", auto: "hass:autorenew",
manual: "hass:cursor-pointer", heat_cool: "hass:autorenew",
heat: "hass:fire", heat: "hass:fire",
cool: "hass:snowflake", cool: "hass:snowflake",
off: "hass:power", off: "hass:power",
fan_only: "hass:fan", fan_only: "hass:fan",
eco: "hass:leaf",
dry: "hass:water-percent", dry: "hass:water-percent",
idle: "hass:power-sleep",
}; };
@customElement("hui-thermostat-card") @customElement("hui-thermostat-card")
@ -109,9 +108,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
`; `;
} }
const mode = modeIcons[stateObj.attributes.operation_mode || ""] const mode = stateObj.state in modeIcons ? stateObj.state : "unknown-mode";
? stateObj.attributes.operation_mode!
: "unknown-mode";
return html` return html`
${this.renderStyle()} ${this.renderStyle()}
<ha-card <ha-card
@ -146,11 +143,23 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
</div> </div>
<div class="climate-info"> <div class="climate-info">
<div id="set-temperature"></div> <div id="set-temperature"></div>
<div class="current-mode">${this.hass!.localize( <div class="current-mode">
`state.climate.${stateObj.state}` ${this.hass!.localize(`state.climate.${stateObj.state}`)}
)}</div> ${
stateObj.attributes.preset_mode
? html`
-
${this.hass!.localize(
`state_attributes.climate.preset_mode.${
stateObj.attributes.preset_mode
}`
) || stateObj.attributes.preset_mode}
`
: ""
}
</div>
<div class="modes"> <div class="modes">
${(stateObj.attributes.operation_list || []).map((modeItem) => ${stateObj.attributes.hvac_modes.map((modeItem) =>
this._renderIcon(modeItem, mode) this._renderIcon(modeItem, mode)
)} )}
</div> </div>
@ -205,7 +214,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
} }
private get _stepSize(): number { private get _stepSize(): number {
const stateObj = this.hass!.states[this._config!.entity]; const stateObj = this.hass!.states[this._config!.entity] as ClimateEntity;
if (stateObj.attributes.target_temp_step) { if (stateObj.attributes.target_temp_step) {
return stateObj.attributes.target_temp_step; return stateObj.attributes.target_temp_step;
@ -348,9 +357,9 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
} }
private _handleModeClick(e: MouseEvent): void { private _handleModeClick(e: MouseEvent): void {
this.hass!.callService("climate", "set_operation_mode", { this.hass!.callService("climate", "set_hvac_mode", {
entity_id: this._config!.entity, entity_id: this._config!.entity,
operation_mode: (e.currentTarget as any).mode, hvac_mode: (e.currentTarget as any).mode,
}); });
} }
@ -394,7 +403,8 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
.auto { .auto,
.heat_cool {
--mode-color: var(--auto-color); --mode-color: var(--auto-color);
} }
.cool { .cool {

View File

@ -11,12 +11,13 @@ export function hasConfigOrEntityChanged(
} }
const oldHass = changedProps.get("hass") as HomeAssistant | undefined; const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (oldHass) { if (!oldHass) {
return ( return true;
oldHass.states[element._config!.entity] !==
element.hass!.states[element._config!.entity]
);
} }
return true; return (
oldHass.states[element._config!.entity] !==
element.hass!.states[element._config!.entity] ||
oldHass.localize !== element.hass.localize
);
} }

View File

@ -176,20 +176,12 @@
}, },
"climate": { "climate": {
"off": "[%key:state::default::off%]", "off": "[%key:state::default::off%]",
"on": "[%key:state::default::on%]",
"heat": "Heat", "heat": "Heat",
"cool": "Cool", "cool": "Cool",
"idle": "Idle", "heat_cool": "Auto",
"auto": "Auto", "auto": "Auto",
"dry": "Dry", "dry": "Dry",
"fan_only": "Fan only", "fan_only": "Fan only"
"eco": "Eco",
"electric": "Electric",
"performance": "Performance",
"high_demand": "High demand",
"heat_pump": "Heat pump",
"gas": "Gas",
"manual": "Manual"
}, },
"configurator": { "configurator": {
"configure": "Configure", "configure": "Configure",
@ -326,6 +318,16 @@
"off": "[%key:state::default::off%]", "off": "[%key:state::default::off%]",
"on": "[%key:state::default::on%]", "on": "[%key:state::default::on%]",
"auto": "[%key:state::climate::auto%]" "auto": "[%key:state::climate::auto%]"
},
"preset_mode": {
"none": "None",
"eco": "Eco",
"away": "Away",
"boost": "Boost",
"comfort": "Comfort",
"home": "Home",
"sleep": "Sleep",
"activity": "Activity"
} }
} }
}, },
@ -393,6 +395,7 @@
"operation": "Operation", "operation": "Operation",
"fan_mode": "Fan mode", "fan_mode": "Fan mode",
"swing_mode": "Swing mode", "swing_mode": "Swing mode",
"preset_mode": "Preset",
"away_mode": "Away mode", "away_mode": "Away mode",
"aux_heat": "Aux heat" "aux_heat": "Aux heat"
}, },

View File

@ -160,30 +160,6 @@ export interface HomeAssistant {
callWS: <T>(msg: MessageBase) => Promise<T>; callWS: <T>(msg: MessageBase) => Promise<T>;
} }
export type ClimateEntity = HassEntityBase & {
attributes: HassEntityAttributeBase & {
current_temperature: number;
min_temp: number;
max_temp: number;
temperature: number;
target_temp_step?: number;
target_temp_high?: number;
target_temp_low?: number;
target_humidity?: number;
target_humidity_low?: number;
target_humidity_high?: number;
fan_mode?: string;
fan_list?: string[];
operation_mode?: string;
operation_list?: string[];
hold_mode?: string;
swing_mode?: string;
swing_list?: string[];
away_mode?: "on" | "off";
aux_heat?: "on" | "off";
};
};
export type LightEntity = HassEntityBase & { export type LightEntity = HassEntityBase & {
attributes: HassEntityAttributeBase & { attributes: HassEntityAttributeBase & {
min_mireds: number; min_mireds: number;