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/setup.py b/setup.py index 184ea304d9..44c83bb9e0 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20190702.0", + version="20190705.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/home-assistant-polymer", author="The Home Assistant Authors", 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-error-card.ts b/src/panels/lovelace/cards/hui-error-card.ts index 62bbfe4548..4e74586146 100644 --- a/src/panels/lovelace/cards/hui-error-card.ts +++ b/src/panels/lovelace/cards/hui-error-card.ts @@ -58,6 +58,8 @@ export class HuiErrorCard extends LitElement implements LovelaceCard { color: white; padding: 8px; font-weight: 500; + user-select: text; + cursor: default; } `; } 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/panels/lovelace/entity-rows/hui-timer-entity-row.ts b/src/panels/lovelace/entity-rows/hui-timer-entity-row.ts index cd46bccae8..05508ad130 100644 --- a/src/panels/lovelace/entity-rows/hui-timer-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-timer-entity-row.ts @@ -40,6 +40,16 @@ class HuiTimerEntityRow extends LitElement { this._clearInterval(); } + public connectedCallback(): void { + super.connectedCallback(); + if (this._config && this._config.entity) { + const stateObj = this.hass!.states[this._config!.entity]; + if (stateObj) { + this._startInterval(stateObj); + } + } + } + protected render(): TemplateResult | void { if (!this._config || !this.hass) { return html``; 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; diff --git a/translations/cs.json b/translations/cs.json index f2c14c4d72..6fe83e01c0 100644 --- a/translations/cs.json +++ b/translations/cs.json @@ -357,6 +357,7 @@ "stop": "Zastavit" }, "core_config": { + "edit_requires_storage": "Editori pois käytöstä, koska asetukset on määritelty configuration.yaml:ssä", "location_name": "Název instalace Home Assistant", "latitude": "Zeměpisná šířka", "longitude": "Zeměpisná délka", @@ -996,6 +997,8 @@ "labels": { "lights": "Světla", "information": "Informace", + "morning_commute": "Työmatka aamulla", + "commute_home": "Matka kotiin", "entertainment": "Zábava", "activity": "Aktivita", "hdmi_input": "Vstup HDMI", @@ -1006,6 +1009,7 @@ "air": "Vzduch" }, "unit": { + "watching": "tarkkaillaan", "minutes_abbr": "min" } } diff --git a/translations/fi.json b/translations/fi.json index da0849ac5d..2f5b9b9962 100644 --- a/translations/fi.json +++ b/translations/fi.json @@ -387,7 +387,7 @@ "description": "Luo ja muokkaa automaatioita", "picker": { "header": "Automaatioeditori", - "introduction": "Automaatioeditorissa voit luoda ja muokata automaatioita. Kannattaa lukea [ohjeet](https:\/\/home-assistant.io\/docs\/automation\/editor\/) (englanniksi), jotta osaat varmasti kirjoittaa automaatiot oikein.", + "introduction": "Automaatioeditorissa voit luoda ja muokata automaatioita. Kannattaa lukea ohjeet, jotta osaat varmasti kirjoittaa automaatiot oikein.", "pick_automation": "Valitse automaatio, jota haluat muokata", "no_automations": "Ei muokattavia automaatioita", "add_automation": "Lisää automaatio", @@ -561,7 +561,7 @@ }, "learn_more": "Lisätietoja toiminnoista" }, - "load_error_not_editable": "Vain automaatiot tiedostossa automations.yaml ovat muokattavissa", + "load_error_not_editable": "Vain automaatiot tiedostossa automations.yaml ovat muokattavissa.", "load_error_unknown": "Virhe ladatessa automaatiota ( {err_no} )" } }, @@ -653,7 +653,7 @@ "description": "Yleiskatsaus kaikki kotisi alueista.", "picker": { "header": "Aluekisteri", - "introduction": "Alueita käytetään laitteiden järjestämiseen. Näitä tietoja käytetään Kotiavustajassa käyttöliittymän ja käyttöoikeuksien järjestämiseen sekä integroinnin muihin järjestelmiin.", + "introduction": "Alueita käytetään laitteiden järjestämiseen. Näitä tietoja käytetään Home Assistantissa käyttöliittymän ja käyttöoikeuksien järjestämiseen sekä integroinnin muihin järjestelmiin.", "introduction2": "Voit sijoittaa laitteita alueelle siirtymällä alla olevan linkin avulla integraatiot-sivulle ja sitten napauttamalla määritettyyn integraatioon, jotta pääset laitteet -kortteihin.", "integrations_page": "Integraatiot", "no_areas": "Et ole vielä luonut alueita!", @@ -674,8 +674,8 @@ "picker": { "header": "Olemusrekisteri", "unavailable": "(ei saatavilla)", - "introduction": "Kotiavustaja pitää rekisteriä jokaisesta havaitetusta olemuksesta, joka voidaan yksilöidä. Kullekin näille yksiköille määritetään olemus-ID, varattu juuri tälle yksikölle.", - "introduction2": "Yksikkörekisterin avulla voit ohittaa nimeä, muuttaa yksikön tunnusta tai poistaa merkinnän Kotiavustajasta. Huomaa, että rekisterimerkinnän poistaminen ei poista yksikköä sinäänsä. Sitä voit seuraamalla alla olevaa linkkiä ja poistamalla sitä integrointisivulta.", + "introduction": "Home Assistant pitää rekisteriä jokaisesta havaitetusta olemuksesta, joka voidaan yksilöidä. Kullekin näille yksiköille määritetään olemus-ID, varattu juuri tälle yksikölle.", + "introduction2": "Yksikkörekisterin avulla voit ohittaa nimeä, muuttaa yksikön tunnusta tai poistaa merkinnän Home Assistantista. Huomaa, että rekisterimerkinnän poistaminen ei poista yksikköä sinäänsä. Sitä voit seuraamalla alla olevaa linkkiä ja poistamalla sitä integrointisivulta.", "integrations_page": "Integraatiot" }, "editor": { @@ -729,7 +729,7 @@ }, "long_lived_access_tokens": { "header": "Pitkäaikaiset käyttötunnussanomat", - "description": "Luo pitkäikäisiä käyttöoikeustunnuksia, jotta komentosarjasi voivat vuorovaikutttaa Kotiavustajan kanssa. Jokainen tunnus on voimassa 10 vuotta luomisesta. Seuraavat pitkäikäiset käyttöoikeustunnukset ovat tällä hetkellä käytössä.", + "description": "Luo pitkäikäisiä käyttöoikeustunnuksia, jotta komentosarjasi voivat vuorovaikutttaa Home Assistantin kanssa. Jokainen tunnus on voimassa 10 vuotta luomisesta. Seuraavat pitkäikäiset käyttöoikeustunnukset ovat tällä hetkellä käytössä.", "learn_auth_requests": "Opi tekemään tunnistautuneita kutsuja.", "created_at": "Luotu {date}", "confirm_delete": "Haluatko varmasti poistaa {name} käyttöoikeustunnuksen?", @@ -769,7 +769,7 @@ }, "page-authorize": { "initializing": "Alustetaan", - "authorizing_client": "Olet antamassa pääsyn {clientId} Home Assistant ympäristöösi.", + "authorizing_client": "Olet antamassa pääsyn {clientId} Home Assistant -ympäristöösi.", "logging_in_with": "Kirjaudutaan sisään **{authProviderName}**.", "pick_auth_provider": "Tai kirjaudu sisään joillakin seuraavista", "abort_intro": "Kirjautuminen on keskeytetty", @@ -881,7 +881,7 @@ } }, "integration": { - "intro": "Laitteet ja palvelut ovat edustettuna Kotiavustajassa integraatioina. Voit määrittää ne nyt tai tehdä sitä myöhemmin kokoonpanonäytöstä.", + "intro": "Laitteet ja palvelut ovat edustettuna Home Assistantissa integraatioina. Voit määrittää ne nyt tai tehdä sitä myöhemmin kokoonpanonäytöstä.", "more_integrations": "Lisää", "finish": "Valmis" }, @@ -967,7 +967,7 @@ "entity_non_numeric": "Yksikkö ei ole numeerinen: {entity}" }, "changed_toast": { - "message": "Lovelace-asetukset päivitettiin, haluatko päivittää näkymää?", + "message": "Lovelace-asetukset päivitettiin, haluatko päivittää näkymän?", "refresh": "Päivitä" }, "reload_lovelace": "Lataa Lovelace uudelleen" @@ -977,8 +977,8 @@ "demo": { "demo_by": "kirjoittanut {name}", "next_demo": "Seuraava demo", - "introduction": "Tervetuloa kotiin! Olet päätynyt Kotiavustaja-demoon, missä esittelemme yhteisömme parhaat käyttöliittymät.", - "learn_more": "Opi enemmän Kotiavustajasta" + "introduction": "Tervetuloa kotiin! Olet päätynyt Home Assistant -demoon, missä esittelemme yhteisömme parhaat käyttöliittymät.", + "learn_more": "Opi enemmän Home Assistantista" } }, "config": { @@ -1006,7 +1006,7 @@ "volume": "Äänenvoimakkuus", "total_tv_time": "TV-aikaa yhteensä", "turn_tv_off": "Sammuta televisio", - "air": "Air" + "air": "Ilmastointi" }, "unit": { "watching": "katsomassa", diff --git a/translations/pt-BR.json b/translations/pt-BR.json index 820f249b16..16e9ff907d 100644 --- a/translations/pt-BR.json +++ b/translations/pt-BR.json @@ -113,8 +113,8 @@ "on": "Quente" }, "window": { - "off": "Fechada", - "on": "Aberta" + "off": "Fechado", + "on": "Aberto" }, "lock": { "off": "Bloqueado", @@ -499,7 +499,7 @@ "label": "Estado numérico", "above": "Acima", "below": "Abaixo", - "value_template": "Valor de exemplo (opcional)" + "value_template": "Valor do modelo (opcional)" }, "sun": { "label": "Sol", @@ -512,7 +512,7 @@ }, "template": { "label": "Modelo", - "value_template": "Valor de exemplo" + "value_template": "Valor do modelo" }, "time": { "label": "Tempo", @@ -619,7 +619,8 @@ "firmware": "Firmware: {version}", "device_unavailable": "dispositivo indisponível", "entity_unavailable": "entidade indisponível", - "no_area": "Sem área" + "no_area": "Sem área", + "hub": "Conectado via" }, "config_flow": { "external_step": { @@ -970,6 +971,49 @@ "refresh": "Atualizar" }, "reload_lovelace": "Recarregar Lovelace" + }, + "page-demo": { + "cards": { + "demo": { + "demo_by": "por {name}", + "next_demo": "Próxima demonstração", + "introduction": "Bem-vindo! Você chegou no demo do Home Assistant onde mostramos as melhores UIs criados por nossa comunidade.", + "learn_more": "Saiba mais sobre o Home Assistant" + } + }, + "config": { + "arsaboo": { + "names": { + "upstairs": "Andar de cima", + "family_room": "Quarto Familiar", + "kitchen": "Cozinha", + "patio": "Pátio", + "hallway": "Corredor", + "master_bedroom": "Quarto principal", + "left": "Esquerda", + "right": "Direita", + "mirror": "Espelho" + }, + "labels": { + "lights": "Luzes", + "information": "Informação", + "morning_commute": "Comutação da manhã", + "commute_home": "Comutar para casa", + "entertainment": "Entretenimento", + "activity": "Atividade", + "hdmi_input": "Entrada HDMI", + "hdmi_switcher": "Comutador HDMI", + "volume": "Volume", + "total_tv_time": "Tempo total de TV", + "turn_tv_off": "Desligue a televisão", + "air": "Ar" + }, + "unit": { + "watching": "assistindo", + "minutes_abbr": "min" + } + } + } } }, "sidebar": { diff --git a/translations/sv.json b/translations/sv.json index e1f43706df..d3e0f225bc 100644 --- a/translations/sv.json +++ b/translations/sv.json @@ -1006,7 +1006,7 @@ "volume": "Volym", "total_tv_time": "TV-tid totalt", "turn_tv_off": "Stäng av TV", - "air": "Air" + "air": "Fläkt" }, "unit": { "watching": "ser på",