From 7aa37183b642b00669a53cac44742169857e81ec Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Tue, 2 Oct 2018 08:16:19 -0400 Subject: [PATCH] Convert climate water heaters to new water_heaters component (#1661) * Water heater support * Attempt to fix lint errors. * Fixed another lint issue --- src/common/const.js | 2 + src/common/entity/domain_icon.js | 1 + src/components/ha-water_heater-control.js | 131 ++++++++++ src/components/ha-water_heater-state.js | 74 ++++++ src/components/state-history-chart-line.js | 2 +- src/data/ha-state-history-data.js | 4 +- .../more-info/controls/more-info-content.js | 1 + .../controls/more-info-water_heater.js | 244 ++++++++++++++++++ src/state-summary/state-card-content.js | 1 + src/state-summary/state-card-water_heater.js | 52 ++++ src/translations/en.json | 7 + src/util/hass-attributes-util.js | 2 +- 12 files changed, 518 insertions(+), 3 deletions(-) create mode 100644 src/components/ha-water_heater-control.js create mode 100644 src/components/ha-water_heater-state.js create mode 100644 src/dialogs/more-info/controls/more-info-water_heater.js create mode 100644 src/state-summary/state-card-water_heater.js diff --git a/src/common/const.js b/src/common/const.js index 2f604bd000..6d7ff4f1c8 100644 --- a/src/common/const.js +++ b/src/common/const.js @@ -21,6 +21,7 @@ export const DOMAINS_WITH_CARD = [ 'script', 'timer', 'vacuum', + 'water_heater', 'weblink', ]; @@ -43,6 +44,7 @@ export const DOMAINS_WITH_MORE_INFO = [ 'sun', 'updater', 'vacuum', + 'water_heater', 'weather' ]; diff --git a/src/common/entity/domain_icon.js b/src/common/entity/domain_icon.js index bfc64952bd..f4e6daaa62 100644 --- a/src/common/entity/domain_icon.js +++ b/src/common/entity/domain_icon.js @@ -39,6 +39,7 @@ const fixedIcons = { timer: 'hass:timer', updater: 'hass:cloud-upload', vacuum: 'hass:robot-vacuum', + water_heater: 'hass:thermometer', weblink: 'hass:open-in-new', }; diff --git a/src/components/ha-water_heater-control.js b/src/components/ha-water_heater-control.js new file mode 100644 index 0000000000..22af9ad0b4 --- /dev/null +++ b/src/components/ha-water_heater-control.js @@ -0,0 +1,131 @@ +import '@polymer/iron-flex-layout/iron-flex-layout-classes.js'; +import '@polymer/paper-icon-button/paper-icon-button.js'; +import { html } from '@polymer/polymer/lib/utils/html-tag.js'; +import { PolymerElement } from '@polymer/polymer/polymer-element.js'; + +import EventsMixin from '../mixins/events-mixin.js'; + +/* + * @appliesMixin EventsMixin + */ +class HaWaterHeaterControl extends EventsMixin(PolymerElement) { + static get template() { + return html` + + + + +
+ [[value]] [[units]] +
+
+
+ +
+
+ +
+
+`; + } + + static get properties() { + return { + value: { + type: Number, + observer: 'valueChanged' + }, + units: { + type: String, + }, + min: { + type: Number, + }, + max: { + type: Number, + }, + step: { + type: Number, + value: 1, + }, + }; + } + + temperatureStateInFlux(inFlux) { + this.$.target_temperature.classList.toggle('in-flux', inFlux); + } + + incrementValue() { + const newval = this.value + this.step; + if (this.value < this.max) { + this.last_changed = Date.now(); + this.temperatureStateInFlux(true); + } + if (newval <= this.max) { + // If no initial target_temp + // this forces control to start + // from the min configured instead of 0 + if (newval <= this.min) { + this.value = this.min; + } else { + this.value = newval; + } + } else { + this.value = this.max; + } + } + + decrementValue() { + const newval = this.value - this.step; + if (this.value > this.min) { + this.last_changed = Date.now(); + this.temperatureStateInFlux(true); + } + if (newval >= this.min) { + this.value = newval; + } else { + this.value = this.min; + } + } + + valueChanged() { + // when the last_changed timestamp is changed, + // trigger a potential event fire in + // the future, as long as last changed is far enough in the + // past. + if (this.last_changed) { + window.setTimeout(() => { + const now = Date.now(); + if (now - this.last_changed >= 2000) { + this.fire('change'); + this.temperatureStateInFlux(false); + this.last_changed = null; + } + }, 2010); + } + } +} + +customElements.define('ha-water_heater-control', HaWaterHeaterControl); diff --git a/src/components/ha-water_heater-state.js b/src/components/ha-water_heater-state.js new file mode 100644 index 0000000000..5557e7e431 --- /dev/null +++ b/src/components/ha-water_heater-state.js @@ -0,0 +1,74 @@ +import { html } from '@polymer/polymer/lib/utils/html-tag.js'; +import { PolymerElement } from '@polymer/polymer/polymer-element.js'; + +import LocalizeMixin from '../mixins/localize-mixin.js'; + +/* + * @appliesMixin LocalizeMixin + */ +class HaWaterHeaterState extends LocalizeMixin(PolymerElement) { + static get template() { + return html` + + +
+ + [[_localizeState(stateObj.state)]] + + [[computeTarget(hass, stateObj)]] +
+ + +`; + } + + static get properties() { + return { + hass: Object, + stateObj: Object, + }; + } + + computeTarget(hass, stateObj) { + if (!hass || !stateObj) return null; + // We're using "!= null" on purpose so that we match both null and undefined. + if (stateObj.attributes.target_temp_low != null + && stateObj.attributes.target_temp_high != null) { + return `${stateObj.attributes.target_temp_low} - ${stateObj.attributes.target_temp_high} ${hass.config.unit_system.temperature}`; + } + if (stateObj.attributes.temperature != null) { + return `${stateObj.attributes.temperature} ${hass.config.unit_system.temperature}`; + } + + return ''; + } + + _localizeState(state) { + return this.localize(`state.water_heater.${state}`) || state; + } +} +customElements.define('ha-water_heater-state', HaWaterHeaterState); diff --git a/src/components/state-history-chart-line.js b/src/components/state-history-chart-line.js index ac0e661de8..751675be0b 100644 --- a/src/components/state-history-chart-line.js +++ b/src/components/state-history-chart-line.js @@ -137,7 +137,7 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) { }); } - if (domain === 'thermostat' || domain === 'climate') { + if (domain === 'thermostat' || domain === 'climate' || domain === 'water_heater') { // We differentiate between thermostats that have a target temperature // range versus ones that have just a target temperature diff --git a/src/data/ha-state-history-data.js b/src/data/ha-state-history-data.js index c5830c7349..47d6f0da0c 100644 --- a/src/data/ha-state-history-data.js +++ b/src/data/ha-state-history-data.js @@ -10,7 +10,7 @@ import LocalizeMixin from '../mixins/localize-mixin.js'; const RECENT_THRESHOLD = 60000; // 1 minute const RECENT_CACHE = {}; -const DOMAINS_USE_LAST_UPDATED = ['thermostat', 'climate']; +const DOMAINS_USE_LAST_UPDATED = ['thermostat', 'climate', 'water_heater']; const LINE_ATTRIBUTES_TO_KEEP = ['temperature', 'current_temperature', 'target_temp_low', 'target_temp_high']; const stateHistoryCache = {}; @@ -33,6 +33,8 @@ function computeHistory(hass, stateHistory, localize, language) { unit = stateWithUnit.attributes.unit_of_measurement; } else if (computeStateDomain(stateInfo[0]) === 'climate') { unit = hass.config.unit_system.temperature; + } else if (computeStateDomain(stateInfo[0]) === 'water_heater') { + unit = hass.config.unit_system.temperature; } if (!unit) { diff --git a/src/dialogs/more-info/controls/more-info-content.js b/src/dialogs/more-info/controls/more-info-content.js index d2ffe21cff..4e1f1b340b 100644 --- a/src/dialogs/more-info/controls/more-info-content.js +++ b/src/dialogs/more-info/controls/more-info-content.js @@ -18,6 +18,7 @@ import './more-info-script.js'; import './more-info-sun.js'; import './more-info-updater.js'; import './more-info-vacuum.js'; +import './more-info-water_heater.js'; import './more-info-weather.js'; import stateMoreInfoType from '../../../common/entity/state_more_info_type.js'; diff --git a/src/dialogs/more-info/controls/more-info-water_heater.js b/src/dialogs/more-info/controls/more-info-water_heater.js new file mode 100644 index 0000000000..24b152d1e9 --- /dev/null +++ b/src/dialogs/more-info/controls/more-info-water_heater.js @@ -0,0 +1,244 @@ +import '@polymer/iron-flex-layout/iron-flex-layout-classes.js'; +import '@polymer/paper-dropdown-menu/paper-dropdown-menu.js'; +import '@polymer/paper-item/paper-item.js'; +import '@polymer/paper-listbox/paper-listbox.js'; +import '@polymer/paper-toggle-button/paper-toggle-button.js'; +import { timeOut } from '@polymer/polymer/lib/utils/async.js'; +import { Debouncer } from '@polymer/polymer/lib/utils/debounce.js'; +import { html } from '@polymer/polymer/lib/utils/html-tag.js'; +import { PolymerElement } from '@polymer/polymer/polymer-element.js'; + +import '../../../components/ha-water_heater-control.js'; +import '../../../components/ha-paper-slider.js'; + +import featureClassNames from '../../../common/entity/feature_class_names'; + +import EventsMixin from '../../../mixins/events-mixin.js'; +import LocalizeMixin from '../../../mixins/localize-mixin.js'; + +/* + * @appliesMixin EventsMixin + * @appliesMixin LocalizeMixin + */ +class MoreInfoWaterHeater extends LocalizeMixin(EventsMixin(PolymerElement)) { + static get template() { + return html` + + + +
+ +
+
+
[[localize('ui.card.water_heater.target_temperature')]]
+ +
+
+ + + + +
+`; + } + + static get properties() { + return { + hass: { + type: Object, + }, + + stateObj: { + type: Object, + observer: 'stateObjChanged', + }, + + operationIndex: { + type: Number, + value: -1, + observer: 'handleOperationmodeChanged', + }, + awayToggleChecked: Boolean, + }; + } + + stateObjChanged(newVal, oldVal) { + if (newVal) { + this.setProperties({ + awayToggleChecked: newVal.attributes.away_mode === 'on' + }); + } + + if (oldVal) { + this._debouncer = Debouncer.debounce( + this._debouncer, + timeOut.after(500), + () => { + this.fire('iron-resize'); + } + ); + } + } + + handleOperationListUpdate() { + // force polymer to recognize selected item change (to update actual label) + this.operationIndex = -1; + if (this.stateObj.attributes.operation_list) { + this.operationIndex = this.stateObj.attributes.operation_list.indexOf( + this.stateObj.attributes.operation_mode + ); + } + } + + 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); + } + + supportsTemperature(stateObj) { + return (stateObj.attributes.supported_features & 1) !== 0 + && typeof stateObj.attributes.temperature === 'number'; + } + + supportsOperationMode(stateObj) { + return (stateObj.attributes.supported_features & 2) !== 0; + } + + supportsAwayMode(stateObj) { + return (stateObj.attributes.supported_features & 4) !== 0; + } + + computeClassNames(stateObj) { + const _featureClassNames = { + 1: 'has-target_temperature', + 2: 'has-operation_mode', + 4: 'has-away_mode' + }; + + + var classes = [ + featureClassNames(stateObj, _featureClassNames), + ]; + + classes.push('more-info-water_heater'); + + return classes.join(' '); + } + + targetTemperatureChanged(ev) { + const temperature = ev.target.value; + if (temperature === this.stateObj.attributes.temperature) return; + this.callServiceHelper('set_temperature', { temperature: temperature }); + } + + 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 }); + } + + handleOperationmodeChanged(operationIndex) { + // Selected Option will transition to '' before transitioning to new value + if (operationIndex === '' || operationIndex === -1) return; + const operationInput = this.stateObj.attributes.operation_list[operationIndex]; + if (operationInput === this.stateObj.attributes.operation_mode) return; + + this.callServiceHelper('set_operation_mode', { operation_mode: operationInput }); + } + + 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('water_heater', service, data) + .then(() => { + this.stateObjChanged(this.stateObj); + }); + } + + _localizeOperationMode(localize, mode) { + return localize(`state.water_heater.${mode}`) || mode; + } +} + +customElements.define('more-info-water_heater', MoreInfoWaterHeater); diff --git a/src/state-summary/state-card-content.js b/src/state-summary/state-card-content.js index c183ef3c44..7ec7a84461 100644 --- a/src/state-summary/state-card-content.js +++ b/src/state-summary/state-card-content.js @@ -14,6 +14,7 @@ import './state-card-script.js'; import './state-card-timer.js'; import './state-card-toggle.js'; import './state-card-vacuum.js'; +import './state-card-water_heater.js'; import './state-card-weblink.js'; import stateCardType from '../common/entity/state_card_type.js'; diff --git a/src/state-summary/state-card-water_heater.js b/src/state-summary/state-card-water_heater.js new file mode 100644 index 0000000000..9a9022dc3a --- /dev/null +++ b/src/state-summary/state-card-water_heater.js @@ -0,0 +1,52 @@ +import '@polymer/iron-flex-layout/iron-flex-layout-classes.js'; +import { html } from '@polymer/polymer/lib/utils/html-tag.js'; +import { PolymerElement } from '@polymer/polymer/polymer-element.js'; + +import '../components/entity/state-info.js'; +import '../components/ha-water_heater-state.js'; + +class StateCardWaterHeater extends PolymerElement { + static get template() { + return html` + + + +
+ ${this.stateInfoTemplate} + +
+`; + } + + static get stateInfoTemplate() { + return html` + +`; + } + + static get properties() { + return { + hass: Object, + stateObj: Object, + inDialog: { + type: Boolean, + value: false, + } + }; + } +} +customElements.define('state-card-water_heater', StateCardWaterHeater); diff --git a/src/translations/en.json b/src/translations/en.json index b8408b8174..7710751d4e 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -401,6 +401,13 @@ "turn_off": "Turn off" } }, + "water_heater": { + "currently": "Currently", + "on_off": "On / off", + "target_temperature": "Target temperature", + "operation": "Operation", + "away_mode": "Away mode" + }, "weather": { "attributes": { "air_pressure": "Air pressure", diff --git a/src/util/hass-attributes-util.js b/src/util/hass-attributes-util.js index 466e7987c3..4770f05378 100644 --- a/src/util/hass-attributes-util.js +++ b/src/util/hass-attributes-util.js @@ -81,7 +81,7 @@ hassAttributeUtil.LOGIC_STATE_ATTRIBUTES = hassAttributeUtil.LOGIC_STATE_ATTRIBU hidden: { type: 'boolean', description: 'Hide from UI' }, assumed_state: { type: 'boolean', - domains: ['switch', 'light', 'cover', 'climate', 'fan', 'group'] + domains: ['switch', 'light', 'cover', 'climate', 'fan', 'group', 'water_heater'] }, initial_state: { type: 'string',