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) {
- [[_localizeState(stateObj.state)]]
+
+ [[_localizeState(localize, stateObj.state)]]
+
+ - [[_localizePreset(localize, stateObj.attributes.preset_mode)]]
+
+
[[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.on_off')]]
-
-
-
-
-
-
-
-
-
- [[localize('ui.card.climate.target_temperature')]]
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
[[localize('ui.card.climate.target_humidity')]]
-
-
- [[stateObj.attributes.humidity]] %
-
-
-
-
-
-
-
-
-
-
-
-
-
- [[_localizeOperationMode(localize, item)]]
-
-
-
-
-
-
-
-
-
-
-
-
- [[_localizeFanMode(localize, item)]]
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
[[localize('ui.card.climate.away_mode')]]
-
-
-
-
-
-
-
-
-
-
[[localize('ui.card.climate.aux_heat')]]
-
-
-
-
-
-
- `;
- }
-
- 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;