Convert climate water heaters to new water_heaters component (#1661)

* Water heater support

* Attempt to fix lint errors.

* Fixed another lint issue
This commit is contained in:
William Scanlon 2018-10-02 08:16:19 -04:00 committed by Paulus Schoutsen
parent c91b28a850
commit 7aa37183b6
12 changed files with 518 additions and 3 deletions

View File

@ -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'
];

View File

@ -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',
};

View File

@ -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`
<style include="iron-flex iron-flex-alignment"></style>
<style>
/* local DOM styles go here */
:host {
@apply --layout-flex;
@apply --layout-horizontal;
@apply --layout-justified;
}
.in-flux#target_temperature {
color: var(--google-red-500);
}
#target_temperature {
@apply --layout-self-center;
font-size: 200%;
}
.control-buttons {
font-size: 200%;
text-align: right;
}
paper-icon-button {
height: 48px;
width: 48px;
}
</style>
<!-- local DOM goes here -->
<div id="target_temperature">
[[value]] [[units]]
</div>
<div class="control-buttons">
<div>
<paper-icon-button icon="hass:chevron-up" on-click="incrementValue"></paper-icon-button>
</div>
<div>
<paper-icon-button icon="hass:chevron-down" on-click="decrementValue"></paper-icon-button>
</div>
</div>
`;
}
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);

View File

@ -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`
<style>
:host {
display: flex;
flex-direction: column;
justify-content: center;
white-space: nowrap;
}
.target {
color: var(--primary-text-color);
}
.current {
color: var(--secondary-text-color);
}
.state-label {
font-weight: bold;
text-transform: capitalize;
}
</style>
<div class="target">
<span class="state-label">
[[_localizeState(stateObj.state)]]
</span>
[[computeTarget(hass, stateObj)]]
</div>
<template is="dom-if" if="[[currentStatus]]">
<div class="current">
[[localize('ui.card.water_heater.currently')]]: [[currentStatus]]
</div>
</template>
`;
}
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);

View File

@ -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

View File

@ -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) {

View File

@ -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';

View File

@ -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`
<style include="iron-flex"></style>
<style>
:host {
color: var(--primary-text-color);
}
.container-away_mode,
.container-temperature,
.container-operation_list,
.has-away_mode .container-away_mode,
.has-target_temperature .container-temperature,
.has-operation_mode .container-operation_list,
.container-operation_list iron-icon,
paper-dropdown-menu {
width: 100%;
}
paper-item {
cursor: pointer;
}
ha-paper-slider {
width: 100%;
}
ha-water_heater-control.range-control-left,
ha-water_heater-control.range-control-right {
float: left;
width: 46%;
}
ha-water_heater-control.range-control-left {
margin-right: 4%;
}
ha-water_heater-control.range-control-right {
margin-left: 4%;
}
.single-row {
padding: 8px 0;
}
}
</style>
<div class$="[[computeClassNames(stateObj)]]">
<div class="container-temperature">
<div class$="[[stateObj.attributes.operation_mode]]">
<div hidden$="[[!supportsTemperatureControls(stateObj)]]">[[localize('ui.card.water_heater.target_temperature')]]</div>
<template is="dom-if" if="[[supportsTemperature(stateObj)]]">
<ha-water_heater-control value="[[stateObj.attributes.temperature]]" units="[[hass.config.unit_system.temperature]]" step="[[computeTemperatureStepSize(hass, stateObj)]]" min="[[stateObj.attributes.min_temp]]" max="[[stateObj.attributes.max_temp]]" on-change="targetTemperatureChanged">
</ha-water_heater-control>
</template>
</div>
</div>
<template is="dom-if" if="[[supportsOperationMode(stateObj)]]">
<div class="container-operation_list">
<div class="controls">
<paper-dropdown-menu label-float="" dynamic-align="" label="[[localize('ui.card.water_heater.operation')]]">
<paper-listbox slot="dropdown-content" selected="{{operationIndex}}">
<template is="dom-repeat" items="[[stateObj.attributes.operation_list]]" on-dom-change="handleOperationListUpdate">
<paper-item>[[_localizeOperationMode(localize, item)]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>
</div>
</div>
</template>
<template is="dom-if" if="[[supportsAwayMode(stateObj)]]">
<div class="container-away_mode">
<div class="center horizontal layout single-row">
<div class="flex">[[localize('ui.card.water_heater.away_mode')]]</div>
<paper-toggle-button checked="[[awayToggleChecked]]" on-change="awayToggleChanged">
</paper-toggle-button>
</div>
</div>
</template>
</div>
`;
}
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);

View File

@ -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';

View File

@ -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`
<style include="iron-flex iron-flex-alignment"></style>
<style>
:host {
@apply --paper-font-body1;
line-height: 1.5;
}
ha-water_heater-state {
margin-left: 16px;
text-align: right;
}
</style>
<div class="horizontal justified layout">
${this.stateInfoTemplate}
<ha-water_heater-state hass="[[hass]]" state-obj="[[stateObj]]"></ha-water_heater-state>
</div>
`;
}
static get stateInfoTemplate() {
return html`
<state-info
hass="[[hass]]"
state-obj="[[stateObj]]"
in-dialog="[[inDialog]]"
></state-info>
`;
}
static get properties() {
return {
hass: Object,
stateObj: Object,
inDialog: {
type: Boolean,
value: false,
}
};
}
}
customElements.define('state-card-water_heater', StateCardWaterHeater);

View File

@ -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",

View File

@ -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',