From 4fdbec93b3ae9085824d4e179874bcbdccf752f3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 5 Jul 2019 15:13:53 -0700 Subject: [PATCH] Migrate for climate 1.0 (#3333) * Migrate for climate 1.0 * Update demo * Fix gallery * Add preset to thermostat card * Fix climate entity row --- demo/src/configs/arsaboo/entities.ts | 13 +- gallery/src/demos/demo-hui-thermostat-card.ts | 22 +- gallery/src/ha-gallery.js | 14 +- gallery/webpack.config.js | 4 +- src/components/ha-climate-state.js | 21 +- src/data/climate.ts | 51 ++ .../more-info/controls/more-info-climate.js | 533 ------------------ .../more-info/controls/more-info-climate.ts | 501 ++++++++++++++++ src/fake_data/demo_services.ts | 44 +- src/fake_data/entity.ts | 23 +- .../lovelace/cards/hui-thermostat-card.ts | 42 +- src/panels/lovelace/common/has-changed.ts | 13 +- src/translations/en.json | 23 +- src/types.ts | 24 - 14 files changed, 663 insertions(+), 665 deletions(-) create mode 100644 src/data/climate.ts delete mode 100644 src/dialogs/more-info/controls/more-info-climate.js create mode 100644 src/dialogs/more-info/controls/more-info-climate.ts diff --git a/demo/src/configs/arsaboo/entities.ts b/demo/src/configs/arsaboo/entities.ts index 4acd753e76..703b9751f0 100644 --- a/demo/src/configs/arsaboo/entities.ts +++ b/demo/src/configs/arsaboo/entities.ts @@ -94,22 +94,19 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) => target_temp_high: 24, target_temp_low: 20, fan_mode: "auto", - fan_list: ["auto", "on"], - operation_mode: "auto", - operation_list: ["auto", "auxHeatOnly", "cool", "heat", "off"], - hold_mode: null, - away_mode: "off", + fan_modes: ["auto", "on"], + hvac_modes: ["auto", "cool", "heat", "off"], aux_heat: "off", actual_humidity: 30, fan: "on", - climate_mode: "Day", operation: "fan", - climate_list: ["Away", "Sleep", "Day", "Home"], fan_min_on_time: 10, friendly_name: localize( "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": { diff --git a/gallery/src/demos/demo-hui-thermostat-card.ts b/gallery/src/demos/demo-hui-thermostat-card.ts index f0545381d5..45124374e0 100644 --- a/gallery/src/demos/demo-hui-thermostat-card.ts +++ b/gallery/src/demos/demo-hui-thermostat-card.ts @@ -14,14 +14,14 @@ const ENTITIES = [ 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", + fan_modes: ["On Low", "On High", "Auto Low", "Auto High", "Off"], + hvac_modes: ["heat", "cool", "auto", "off"], swing_mode: "Auto", - swing_list: ["Auto", "1", "2", "3", "Off"], + swing_modes: ["Auto", "1", "2", "3", "Off"], friendly_name: "Ecobee", - supported_features: 1014, + supported_features: 59, + preset_mode: "eco", + preset_modes: ["away", "eco"], }), getEntity("climate", "nest", "heat", { current_temperature: 17, @@ -29,14 +29,12 @@ const ENTITIES = [ 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", + fan_modes: ["On Low", "On High", "Auto Low", "Auto High", "Off"], + hvac_modes: ["heat", "cool", "auto", "off"], swing_mode: "Auto", - swing_list: ["Auto", "1", "2", "3", "Off"], + swing_modes: ["Auto", "1", "2", "3", "Off"], friendly_name: "Nest", - supported_features: 1014, + supported_features: 43, }), ]; diff --git a/gallery/src/ha-gallery.js b/gallery/src/ha-gallery.js index 395b3a8fcb..a53fa66462 100644 --- a/gallery/src/ha-gallery.js +++ b/gallery/src/ha-gallery.js @@ -56,7 +56,7 @@ class HaGallery extends PolymerElement { color: var(--primary-color); } - a paper-item { + a { color: var(--primary-text-color); text-decoration: none; } @@ -138,12 +138,22 @@ class HaGallery extends PolymerElement { - + `; } static get properties() { return { + _fakeHass: { + type: Object, + // Just enough for computeRTL + value: { + language: "en", + translationMetadata: { + translations: {}, + }, + }, + }, _demo: { type: String, value: document.location.hash.substr(1), diff --git a/gallery/webpack.config.js b/gallery/webpack.config.js index b252c03551..4ffbef7911 100644 --- a/gallery/webpack.config.js +++ b/gallery/webpack.config.js @@ -44,8 +44,8 @@ module.exports = { to: "static/images/leaflet/", }, { - from: "../node_modules/@polymer/font-roboto-local/fonts", - to: "static/fonts", + from: "../node_modules/roboto-fontface/fonts/roboto/*.woff2", + to: "static/fonts/roboto/", }, { from: "../node_modules/leaflet/dist/images", diff --git a/src/components/ha-climate-state.js b/src/components/ha-climate-state.js index 27693bded9..14a2485b0b 100644 --- a/src/components/ha-climate-state.js +++ b/src/components/ha-climate-state.js @@ -38,7 +38,12 @@ class HaClimateState extends LocalizeMixin(PolymerElement) {
[[computeTarget(hass, stateObj)]]
@@ -83,7 +88,7 @@ class HaClimateState extends LocalizeMixin(PolymerElement) { stateObj.attributes.target_temp_low != null && stateObj.attributes.target_temp_high != null ) { - return `${stateObj.attributes.target_temp_low} - ${ + return `${stateObj.attributes.target_temp_low}-${ stateObj.attributes.target_temp_high } ${hass.config.unit_system.temperature}`; } @@ -96,9 +101,9 @@ class HaClimateState extends LocalizeMixin(PolymerElement) { stateObj.attributes.target_humidity_low != null && stateObj.attributes.target_humidity_high != null ) { - return `${stateObj.attributes.target_humidity_low} - ${ + return `${stateObj.attributes.target_humidity_low}-${ stateObj.attributes.target_humidity_high - } %`; + }%`; } if (stateObj.attributes.humidity != null) { return `${stateObj.attributes.humidity} %`; @@ -111,8 +116,12 @@ class HaClimateState extends LocalizeMixin(PolymerElement) { return state !== "unknown"; } - _localizeState(state) { - return this.localize(`state.climate.${state}`) || state; + _localizeState(localize, 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); diff --git a/src/data/climate.ts b/src/data/climate.ts new file mode 100644 index 0000000000..e3dab8dd9e --- /dev/null +++ b/src/data/climate.ts @@ -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; diff --git a/src/dialogs/more-info/controls/more-info-climate.js b/src/dialogs/more-info/controls/more-info-climate.js deleted file mode 100644 index 2bd0c4b648..0000000000 --- a/src/dialogs/more-info/controls/more-info-climate.js +++ /dev/null @@ -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` - - - -
- - -
-
-
- [[localize('ui.card.climate.target_temperature')]] -
- - -
-
- - - - - - - - - - - - -
- `; - } - - 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); diff --git a/src/dialogs/more-info/controls/more-info-climate.ts b/src/dialogs/more-info/controls/more-info-climate.ts new file mode 100644 index 0000000000..73ba4c9564 --- /dev/null +++ b/src/dialogs/more-info/controls/more-info-climate.ts @@ -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` +
+
+
+ ${supportTargetTemperature || supportTargetTemperatureRange + ? html` +
+ ${hass.localize("ui.card.climate.target_temperature")} +
+ ` + : ""} + ${stateObj.attributes.temperature + ? html` + + ` + : ""} + ${stateObj.attributes.target_temp_low || + stateObj.attributes.target_temp_high + ? html` + + + ` + : ""} +
+
+ + ${supportTargetHumidity + ? html` +
+
${hass.localize("ui.card.climate.target_humidity")}
+
+
+ ${stateObj.attributes.humidity} % +
+ + +
+
+ ` + : ""} + +
+
+ + + ${stateObj.attributes.hvac_modes.map( + (mode) => html` + + ${hass.localize(`state.climate.${mode}`)} + + ` + )} + + +
+
+ + ${supportPresetMode + ? html` +
+ + + + ${hass.localize( + `state_attributes.climate.preset_mode.none` + )} + + ${stateObj.attributes.preset_modes!.map( + (mode) => html` + + ${hass.localize( + `state_attributes.climate.preset_mode.${mode}` + ) || mode} + + ` + )} + + +
+ ` + : ""} + ${supportFanMode + ? html` +
+ + + ${stateObj.attributes.fan_modes!.map( + (mode) => html` + + ${hass.localize( + `state_attributes.climate.fan_mode.${mode}` + ) || mode} + + ` + )} + + +
+ ` + : ""} + ${supportSwingMode + ? html` +
+ + + ${stateObj.attributes.swing_modes!.map( + (mode) => html` + ${mode} + ` + )} + + +
+ ` + : ""} + ${supportAuxHeat + ? html` +
+
+
+ ${hass.localize("ui.card.climate.aux_heat")} +
+ +
+
+ ` + : ""} +
+ `; + } + + 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); diff --git a/src/fake_data/demo_services.ts b/src/fake_data/demo_services.ts index 4d518f88bc..82b452b749 100644 --- a/src/fake_data/demo_services.ts +++ b/src/fake_data/demo_services.ts @@ -619,26 +619,6 @@ export const demoServices: HassServices = { }, }, 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: { description: "Turn auxiliary heater on/off for climate device.", fields: { @@ -701,16 +681,16 @@ export const demoServices: HassServices = { fan_mode: { description: "New value of fan mode.", example: "On Low" }, }, }, - set_operation_mode: { + set_hvac_mode: { description: "Set operation mode for climate device.", fields: { entity_id: { description: "Name(s) of entities to change.", example: "climate.nest", }, - operation_mode: { + hvac_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: "" }, }, }, - 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: { scan: { diff --git a/src/fake_data/entity.ts b/src/fake_data/entity.ts index 71a6ab61a9..857222781e 100644 --- a/src/fake_data/entity.ts +++ b/src/fake_data/entity.ts @@ -209,11 +209,24 @@ class ClimateEntity extends Entity { return; } - if (service === "set_operation_mode") { - this.update( - data.operation_mode === "heat" ? "heat" : data.operation_mode, - { ...this.attributes, operation_mode: data.operation_mode } - ); + if (service === "set_hvac_mode") { + this.update(data.hvac_mode, this.attributes); + } else if ( + [ + "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 { super.handleService(domain, service, data); } diff --git a/src/panels/lovelace/cards/hui-thermostat-card.ts b/src/panels/lovelace/cards/hui-thermostat-card.ts index a6af4caf24..6b1c07a549 100644 --- a/src/panels/lovelace/cards/hui-thermostat-card.ts +++ b/src/panels/lovelace/cards/hui-thermostat-card.ts @@ -17,12 +17,13 @@ import applyThemesOnElement from "../../../common/dom/apply_themes_on_element"; import computeStateName from "../../../common/entity/compute_state_name"; import { hasConfigOrEntityChanged } from "../common/has-changed"; -import { HomeAssistant, ClimateEntity } from "../../../types"; +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"; +import { ClimateEntity, HvacMode } from "../../../data/climate"; const thermostatConfig = { radius: 150, @@ -35,16 +36,14 @@ const thermostatConfig = { animation: false, }; -const modeIcons = { +const modeIcons: { [mode in HvacMode]: string } = { auto: "hass:autorenew", - manual: "hass:cursor-pointer", + heat_cool: "hass:autorenew", heat: "hass:fire", cool: "hass:snowflake", off: "hass:power", fan_only: "hass:fan", - eco: "hass:leaf", dry: "hass:water-percent", - idle: "hass:power-sleep", }; @customElement("hui-thermostat-card") @@ -109,9 +108,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { `; } - const mode = modeIcons[stateObj.attributes.operation_mode || ""] - ? stateObj.attributes.operation_mode! - : "unknown-mode"; + const mode = stateObj.state in modeIcons ? stateObj.state : "unknown-mode"; return html` ${this.renderStyle()}
-
${this.hass!.localize( - `state.climate.${stateObj.state}` - )}
+
+ ${this.hass!.localize(`state.climate.${stateObj.state}`)} + ${ + stateObj.attributes.preset_mode + ? html` + - + ${this.hass!.localize( + `state_attributes.climate.preset_mode.${ + stateObj.attributes.preset_mode + }` + ) || stateObj.attributes.preset_mode} + ` + : "" + } +
- ${(stateObj.attributes.operation_list || []).map((modeItem) => + ${stateObj.attributes.hvac_modes.map((modeItem) => this._renderIcon(modeItem, mode) )}
@@ -205,7 +214,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { } 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) { return stateObj.attributes.target_temp_step; @@ -348,9 +357,9 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { } private _handleModeClick(e: MouseEvent): void { - this.hass!.callService("climate", "set_operation_mode", { + this.hass!.callService("climate", "set_hvac_mode", { 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; overflow: hidden; } - .auto { + .auto, + .heat_cool { --mode-color: var(--auto-color); } .cool { diff --git a/src/panels/lovelace/common/has-changed.ts b/src/panels/lovelace/common/has-changed.ts index 19aed21579..904a7c2462 100644 --- a/src/panels/lovelace/common/has-changed.ts +++ b/src/panels/lovelace/common/has-changed.ts @@ -11,12 +11,13 @@ export function hasConfigOrEntityChanged( } const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - if (oldHass) { - return ( - oldHass.states[element._config!.entity] !== - element.hass!.states[element._config!.entity] - ); + if (!oldHass) { + return true; } - return true; + return ( + oldHass.states[element._config!.entity] !== + element.hass!.states[element._config!.entity] || + oldHass.localize !== element.hass.localize + ); } diff --git a/src/translations/en.json b/src/translations/en.json index ba7f174d28..c4aa870314 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -176,20 +176,12 @@ }, "climate": { "off": "[%key:state::default::off%]", - "on": "[%key:state::default::on%]", "heat": "Heat", "cool": "Cool", - "idle": "Idle", + "heat_cool": "Auto", "auto": "Auto", "dry": "Dry", - "fan_only": "Fan only", - "eco": "Eco", - "electric": "Electric", - "performance": "Performance", - "high_demand": "High demand", - "heat_pump": "Heat pump", - "gas": "Gas", - "manual": "Manual" + "fan_only": "Fan only" }, "configurator": { "configure": "Configure", @@ -326,6 +318,16 @@ "off": "[%key:state::default::off%]", "on": "[%key:state::default::on%]", "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", "fan_mode": "Fan mode", "swing_mode": "Swing mode", + "preset_mode": "Preset", "away_mode": "Away mode", "aux_heat": "Aux heat" }, diff --git a/src/types.ts b/src/types.ts index b0d4aee814..089e88744e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -160,30 +160,6 @@ export interface HomeAssistant { callWS: (msg: MessageBase) => Promise; } -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 & { attributes: HassEntityAttributeBase & { min_mireds: number;