mirror of
https://github.com/home-assistant/frontend.git
synced 2025-04-25 05:47:20 +00:00
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:
parent
c91b28a850
commit
7aa37183b6
@ -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'
|
||||
];
|
||||
|
||||
|
@ -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',
|
||||
};
|
||||
|
||||
|
131
src/components/ha-water_heater-control.js
Normal file
131
src/components/ha-water_heater-control.js
Normal 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);
|
74
src/components/ha-water_heater-state.js
Normal file
74
src/components/ha-water_heater-state.js
Normal 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);
|
@ -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
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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';
|
||||
|
244
src/dialogs/more-info/controls/more-info-water_heater.js
Normal file
244
src/dialogs/more-info/controls/more-info-water_heater.js
Normal 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);
|
@ -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';
|
||||
|
52
src/state-summary/state-card-water_heater.js
Normal file
52
src/state-summary/state-card-water_heater.js
Normal 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);
|
@ -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",
|
||||
|
@ -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',
|
||||
|
Loading…
x
Reference in New Issue
Block a user