mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-10 19:06:36 +00:00
Add support for history_graph component (#432)
* Add support for history_graph component * Change dev 3s to intended 60s * Address comments * Make card header consistent
This commit is contained in:
parent
890dbc6ad7
commit
b0791abb9a
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
<link rel='import' href='./ha-camera-card.html'>
|
<link rel='import' href='./ha-camera-card.html'>
|
||||||
<link rel='import' href='./ha-entities-card.html'>
|
<link rel='import' href='./ha-entities-card.html'>
|
||||||
|
<link rel='import' href='./ha-history_graph-card.html'>
|
||||||
<link rel='import' href='./ha-introduction-card.html'>
|
<link rel='import' href='./ha-introduction-card.html'>
|
||||||
<link rel='import' href='./ha-media_player-card.html'>
|
<link rel='import' href='./ha-media_player-card.html'>
|
||||||
<link rel='import' href='./ha-weather-card.html'>
|
<link rel='import' href='./ha-weather-card.html'>
|
||||||
|
102
src/cards/ha-history_graph-card.html
Normal file
102
src/cards/ha-history_graph-card.html
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<link rel='import' href='../../bower_components/polymer/polymer-element.html'>
|
||||||
|
<link rel="import" href="../../bower_components/paper-card/paper-card.html">
|
||||||
|
|
||||||
|
<link rel="import" href="../components/state-history-charts.html">
|
||||||
|
<link rel="import" href="../data/ha-state-history-data.html">
|
||||||
|
<link rel='import' href='../util/hass-mixins.html'>
|
||||||
|
|
||||||
|
<dom-module id='ha-history_graph-card'>
|
||||||
|
<template>
|
||||||
|
<style>
|
||||||
|
paper-card:not([dialog]) .content {
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
}
|
||||||
|
paper-card {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
@apply(--paper-font-headline);
|
||||||
|
line-height: 40px;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
padding: 20px 16px 12px;
|
||||||
|
@apply(--paper-font-common-nowrap);
|
||||||
|
}
|
||||||
|
paper-card[dialog] .header {
|
||||||
|
padding-top: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<ha-state-history-data
|
||||||
|
hass='[[hass]]'
|
||||||
|
filter-type='recent-entity'
|
||||||
|
entity-id='[[computeHistoryEntities(stateObj)]]'
|
||||||
|
data='{{stateHistory}}'
|
||||||
|
is-loading='{{stateHistoryLoading}}'
|
||||||
|
cache-config='[[computeCacheConfig(stateObj)]]'
|
||||||
|
></ha-state-history-data>
|
||||||
|
<paper-card dialog$='[[inDialog]]'
|
||||||
|
on-tap='cardTapped'
|
||||||
|
elevation='[[computeElevation(inDialog)]]'>
|
||||||
|
<div class='header'>[[computeTitle(stateObj)]]</div>
|
||||||
|
<div class='content'>
|
||||||
|
<state-history-charts
|
||||||
|
history-data="[[stateHistory]]"
|
||||||
|
is-loading-data="[[stateHistoryLoading]]"
|
||||||
|
up-to-now
|
||||||
|
no-single>
|
||||||
|
</state-history-charts>
|
||||||
|
</div>
|
||||||
|
</paper-card>
|
||||||
|
</template>
|
||||||
|
</dom-module>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
class HaHistoryGraphCard extends window.hassMixins.EventsMixin(Polymer.Element) {
|
||||||
|
static get is() { return 'ha-history_graph-card'; }
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
hass: Object,
|
||||||
|
stateObj: Object,
|
||||||
|
inDialog: {
|
||||||
|
type: Boolean,
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
stateHistory: Object,
|
||||||
|
stateHistoryLoading: Boolean,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
computeTitle(stateObj) {
|
||||||
|
return window.hassUtil.computeStateName(stateObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
computeContentClass(inDialog) {
|
||||||
|
return inDialog ? '' : 'content';
|
||||||
|
}
|
||||||
|
|
||||||
|
computeHistoryEntities(stateObj) {
|
||||||
|
return stateObj.attributes.entity_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
computeCacheConfig(stateObj) {
|
||||||
|
return {
|
||||||
|
refresh: stateObj.attributes.refresh || 0,
|
||||||
|
cacheKey: stateObj.entity_id,
|
||||||
|
hoursToShow: (stateObj && stateObj.attributes.hours_to_show) || 24,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
computeElevation(inDialog) {
|
||||||
|
return inDialog ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
cardTapped(ev) {
|
||||||
|
const mq = window.matchMedia('(min-width: 610px) and (min-height: 550px)');
|
||||||
|
if (mq.matches) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this.fire('hass-more-info', { entityId: this.stateObj.entity_id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define(HaHistoryGraphCard.is, HaHistoryGraphCard);
|
||||||
|
</script>
|
@ -83,6 +83,7 @@
|
|||||||
// mapping domain to size of the card.
|
// mapping domain to size of the card.
|
||||||
var DOMAINS_WITH_CARD = {
|
var DOMAINS_WITH_CARD = {
|
||||||
camera: 4,
|
camera: 4,
|
||||||
|
history_graph: 4,
|
||||||
media_player: 3,
|
media_player: 3,
|
||||||
persistent_notification: 0,
|
persistent_notification: 0,
|
||||||
weather: 4,
|
weather: 4,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||||
|
<link rel="import" href="../../bower_components/iron-resizable-behavior/iron-resizable-behavior.html">
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
@ -20,52 +21,52 @@
|
|||||||
return !isNaN(parsed) && isFinite(parsed) ? parsed : null;
|
return !isNaN(parsed) && isFinite(parsed) ? parsed : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Polymer({
|
class StateHistoryChartLine extends
|
||||||
is: 'state-history-chart-line',
|
Polymer.mixinBehaviors([Polymer.IronResizableBehavior], Polymer.Element) {
|
||||||
|
static get is() { return 'state-history-chart-line'; }
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
|
||||||
properties: {
|
unit: {
|
||||||
data: {
|
type: String,
|
||||||
type: Object,
|
},
|
||||||
observer: 'dataChanged',
|
|
||||||
},
|
|
||||||
|
|
||||||
unit: {
|
isSingleDevice: {
|
||||||
type: String,
|
type: Boolean,
|
||||||
},
|
value: false,
|
||||||
|
},
|
||||||
|
|
||||||
isSingleDevice: {
|
endTime: {
|
||||||
type: Boolean,
|
type: Object,
|
||||||
value: false,
|
},
|
||||||
},
|
|
||||||
|
|
||||||
isAttached: {
|
chartEngine: {
|
||||||
type: Boolean,
|
type: Object,
|
||||||
value: false,
|
},
|
||||||
observer: 'dataChanged',
|
};
|
||||||
},
|
}
|
||||||
|
|
||||||
endTime: {
|
static get observers() {
|
||||||
type: Object,
|
return ['dataChanged(data, endTime)'];
|
||||||
},
|
}
|
||||||
|
|
||||||
chartEngine: {
|
connectedCallback() {
|
||||||
type: Object,
|
super.connectedCallback();
|
||||||
},
|
this._isAttached = true;
|
||||||
},
|
|
||||||
|
|
||||||
created: function () {
|
|
||||||
this.style.display = 'block';
|
|
||||||
},
|
|
||||||
|
|
||||||
attached: function () {
|
|
||||||
this.isAttached = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
dataChanged: function () {
|
|
||||||
this.drawChart();
|
this.drawChart();
|
||||||
},
|
this.addEventListener('iron-resize', () => {
|
||||||
|
this.async(this.drawChart, 10);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
drawChart: function () {
|
dataChanged() {
|
||||||
|
this.drawChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
drawChart() {
|
||||||
var unit = this.unit;
|
var unit = this.unit;
|
||||||
var deviceStates = this.data;
|
var deviceStates = this.data;
|
||||||
var options;
|
var options;
|
||||||
@ -75,7 +76,7 @@
|
|||||||
var finalDataTable;
|
var finalDataTable;
|
||||||
var daysDelta;
|
var daysDelta;
|
||||||
|
|
||||||
if (!this.isAttached) {
|
if (!this._isAttached) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,12 +118,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
startTime = new Date(Math.min.apply(null, deviceStates.map(function (states) {
|
startTime = new Date(Math.min.apply(null, deviceStates.map(function (states) {
|
||||||
return new Date(states[0].last_changed);
|
return new Date(states.states[0].last_changed);
|
||||||
})));
|
})));
|
||||||
|
|
||||||
endTime = this.endTime ||
|
endTime = this.endTime ||
|
||||||
new Date(Math.max.apply(null, deviceStates.map(states =>
|
new Date(Math.max.apply(null, deviceStates.map(states =>
|
||||||
new Date(states[states.length - 1].last_changed)
|
new Date(states.states[states.states.length - 1].last_changed)
|
||||||
)));
|
)));
|
||||||
if (endTime > new Date()) {
|
if (endTime > new Date()) {
|
||||||
endTime = new Date();
|
endTime = new Date();
|
||||||
@ -139,9 +140,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
dataTables = deviceStates.map(function (states) {
|
dataTables = deviceStates.map(function (states) {
|
||||||
var last = states[states.length - 1];
|
var domain = states.domain;
|
||||||
var domain = window.hassUtil.computeDomain(last);
|
var name = states.name;
|
||||||
var name = window.hassUtil.computeStateName(last);
|
|
||||||
var data = [];
|
var data = [];
|
||||||
var dataTable = new window.google.visualization.DataTable();
|
var dataTable = new window.google.visualization.DataTable();
|
||||||
// array containing [time, value1, value2, etc]
|
// array containing [time, value1, value2, etc]
|
||||||
@ -174,7 +174,7 @@
|
|||||||
if (domain === 'thermostat' || domain === 'climate') {
|
if (domain === 'thermostat' || domain === 'climate') {
|
||||||
// We differentiate between thermostats that have a target temperature
|
// We differentiate between thermostats that have a target temperature
|
||||||
// range versus ones that have just a target temperature
|
// range versus ones that have just a target temperature
|
||||||
hasTargetRange = states.reduce(
|
hasTargetRange = states.states.reduce(
|
||||||
function (cum, cur) {
|
function (cum, cur) {
|
||||||
return cum || cur.attributes.target_temp_high !== cur.attributes.target_temp_low;
|
return cum || cur.attributes.target_temp_high !== cur.attributes.target_temp_low;
|
||||||
}, false);
|
}, false);
|
||||||
@ -192,7 +192,7 @@
|
|||||||
var targetHigh = saveParseFloat(state.attributes.target_temp_high);
|
var targetHigh = saveParseFloat(state.attributes.target_temp_high);
|
||||||
var targetLow = saveParseFloat(state.attributes.target_temp_low);
|
var targetLow = saveParseFloat(state.attributes.target_temp_low);
|
||||||
pushData(
|
pushData(
|
||||||
[new Date(state.last_updated), curTemp, targetHigh, targetLow],
|
[new Date(state.last_changed), curTemp, targetHigh, targetLow],
|
||||||
noInterpolations);
|
noInterpolations);
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@ -203,18 +203,18 @@
|
|||||||
processState = function (state) {
|
processState = function (state) {
|
||||||
var curTemp = saveParseFloat(state.attributes.current_temperature);
|
var curTemp = saveParseFloat(state.attributes.current_temperature);
|
||||||
var target = saveParseFloat(state.attributes.temperature);
|
var target = saveParseFloat(state.attributes.temperature);
|
||||||
pushData([new Date(state.last_updated), curTemp, target], noInterpolations);
|
pushData([new Date(state.last_changed), curTemp, target], noInterpolations);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
states.forEach(processState);
|
states.states.forEach(processState);
|
||||||
} else {
|
} else {
|
||||||
dataTable.addColumn('number', name);
|
dataTable.addColumn('number', name);
|
||||||
|
|
||||||
// Only disable interpolation for sensors
|
// Only disable interpolation for sensors
|
||||||
noInterpolations = domain !== 'sensor' && [true];
|
noInterpolations = domain !== 'sensor' && [true];
|
||||||
|
|
||||||
states.forEach(function (state) {
|
states.states.forEach(function (state) {
|
||||||
var value = saveParseFloat(state.state);
|
var value = saveParseFloat(state.state);
|
||||||
pushData([new Date(state.last_changed), value], noInterpolations);
|
pushData([new Date(state.last_changed), value], noInterpolations);
|
||||||
});
|
});
|
||||||
@ -241,7 +241,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.chartEngine.draw(finalDataTable, options);
|
this.chartEngine.draw(finalDataTable, options);
|
||||||
},
|
}
|
||||||
});
|
}
|
||||||
|
customElements.define(StateHistoryChartLine.is, StateHistoryChartLine);
|
||||||
}());
|
}());
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,47 +1,38 @@
|
|||||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
<link rel="import" href="../../bower_components/polymer/polymer-element.html">
|
||||||
|
<link rel="import" href="../../bower_components/iron-resizable-behavior/iron-resizable-behavior.html">
|
||||||
<style>
|
|
||||||
div.charts-tooltip {
|
|
||||||
z-index: 200 !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
class StateHistoryChartTimeline extends
|
||||||
|
Polymer.mixinBehaviors([Polymer.IronResizableBehavior], Polymer.Element) {
|
||||||
|
static get is() { return 'state-history-chart-timeline'; }
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
noSingle: Boolean,
|
||||||
|
endTime: Date,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Polymer({
|
static get observers() {
|
||||||
is: 'state-history-chart-timeline',
|
return ['dataChanged(data, endTime)'];
|
||||||
|
}
|
||||||
|
|
||||||
properties: {
|
connectedCallback() {
|
||||||
data: {
|
super.connectedCallback();
|
||||||
type: Object,
|
this._isAttached = true;
|
||||||
observer: 'dataChanged',
|
|
||||||
},
|
|
||||||
|
|
||||||
endTime: {
|
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
|
|
||||||
isAttached: {
|
|
||||||
type: Boolean,
|
|
||||||
value: false,
|
|
||||||
observer: 'dataChanged',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
created: function () {
|
|
||||||
this.style.display = 'block';
|
|
||||||
},
|
|
||||||
|
|
||||||
attached: function () {
|
|
||||||
this.isAttached = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
dataChanged: function () {
|
|
||||||
this.drawChart();
|
this.drawChart();
|
||||||
},
|
this.addEventListener('iron-resize', () => {
|
||||||
|
this.async(this.drawChart, 10);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
drawChart: function () {
|
dataChanged() {
|
||||||
var root = Polymer.dom(this);
|
this.drawChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
drawChart() {
|
||||||
var stateHistory = this.data;
|
var stateHistory = this.data;
|
||||||
var chart;
|
var chart;
|
||||||
var dataTable;
|
var dataTable;
|
||||||
@ -51,12 +42,12 @@ Polymer({
|
|||||||
var format;
|
var format;
|
||||||
var daysDelta;
|
var daysDelta;
|
||||||
|
|
||||||
if (!this.isAttached) {
|
if (!this._isAttached) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (root.node.lastChild) {
|
while (this.lastChild) {
|
||||||
root.node.removeChild(root.node.lastChild);
|
this.removeChild(this.lastChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!stateHistory || stateHistory.length === 0) {
|
if (!stateHistory || stateHistory.length === 0) {
|
||||||
@ -79,14 +70,15 @@ Polymer({
|
|||||||
startTime = new Date(
|
startTime = new Date(
|
||||||
stateHistory.reduce(
|
stateHistory.reduce(
|
||||||
function (minTime, stateInfo) {
|
function (minTime, stateInfo) {
|
||||||
return Math.min(minTime, new Date(stateInfo[0].last_changed));
|
return Math.min(minTime, new Date(stateInfo.data[0].last_changed));
|
||||||
}, new Date()));
|
}, new Date()));
|
||||||
|
|
||||||
// end time is Math.max(startTime, last_event)
|
// end time is Math.max(startTime, last_event)
|
||||||
endTime = this.endTime ||
|
endTime = this.endTime ||
|
||||||
new Date(stateHistory.reduce(
|
new Date(stateHistory.reduce(
|
||||||
function (maxTime, stateInfo) {
|
function (maxTime, stateInfo) {
|
||||||
return Math.max(maxTime, new Date(stateInfo[stateInfo.length - 1].last_changed));
|
return Math.max(maxTime,
|
||||||
|
new Date(stateInfo.data[stateInfo.data.length - 1].last_changed));
|
||||||
}, startTime));
|
}, startTime));
|
||||||
|
|
||||||
if (endTime > new Date()) {
|
if (endTime > new Date()) {
|
||||||
@ -111,11 +103,11 @@ Polymer({
|
|||||||
var prevState = null;
|
var prevState = null;
|
||||||
var prevLastChanged = null;
|
var prevLastChanged = null;
|
||||||
|
|
||||||
if (stateInfo.length === 0) return;
|
if (stateInfo.data.length === 0) return;
|
||||||
|
|
||||||
entityDisplay = window.hassUtil.computeStateName(stateInfo[0]);
|
entityDisplay = stateInfo.name;
|
||||||
|
|
||||||
stateInfo.forEach(function (state) {
|
stateInfo.data.forEach(function (state) {
|
||||||
var timeStamp = new Date(state.last_changed);
|
var timeStamp = new Date(state.last_changed);
|
||||||
if (timeStamp > endTime) {
|
if (timeStamp > endTime) {
|
||||||
// Drop datapoints that are after the requested endTime. This could happen if
|
// Drop datapoints that are after the requested endTime. This could happen if
|
||||||
@ -145,13 +137,14 @@ Polymer({
|
|||||||
height: 55 + (numTimelines * 42),
|
height: 55 + (numTimelines * 42),
|
||||||
|
|
||||||
timeline: {
|
timeline: {
|
||||||
showRowLabels: stateHistory.length > 1,
|
showRowLabels: this.noSingle || stateHistory.length > 1,
|
||||||
},
|
},
|
||||||
|
|
||||||
hAxis: {
|
hAxis: {
|
||||||
format: format
|
format: format
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
});
|
}
|
||||||
|
customElements.define(StateHistoryChartTimeline.is, StateHistoryChartTimeline);
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
<link rel="import" href="../../bower_components/polymer/polymer-element.html">
|
||||||
<link rel="import" href="../../bower_components/paper-spinner/paper-spinner.html">
|
<link rel="import" href="../../bower_components/paper-spinner/paper-spinner.html">
|
||||||
|
|
||||||
<link rel="import" href="../../bower_components/google-apis/google-legacy-loader.html">
|
<link rel="import" href="../../bower_components/google-apis/google-legacy-loader.html">
|
||||||
@ -14,6 +14,14 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.google-visualization-tooltip {
|
||||||
|
z-index: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
state-history-chart-timeline, state-history-chart-line {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.loading-container {
|
.loading-container {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
@ -40,15 +48,16 @@
|
|||||||
|
|
||||||
<state-history-chart-timeline
|
<state-history-chart-timeline
|
||||||
data='[[historyData.timeline]]'
|
data='[[historyData.timeline]]'
|
||||||
end-time='[[_computeEndTime(endTime, upToNow)]]'>
|
end-time='[[_computeEndTime(endTime, upToNow, historyData)]]'
|
||||||
|
no-single='[[noSingle]]'>
|
||||||
</state-history-chart-timeline>
|
</state-history-chart-timeline>
|
||||||
|
|
||||||
<template is='dom-repeat' items='[[historyData.line]]'>
|
<template is='dom-repeat' items='[[historyData.line]]'>
|
||||||
<state-history-chart-line
|
<state-history-chart-line
|
||||||
unit='[[item.unit]]'
|
unit='[[item.unit]]'
|
||||||
data='[[item.data]]'
|
data='[[item.data]]'
|
||||||
is-single-device='[[_computeIsSingleLineChart(historyData)]]'
|
is-single-device='[[_computeIsSingleLineChart(historyData, noSingle)]]'
|
||||||
end-time='[[_computeEndTime(endTime, upToNow)]]'>
|
end-time='[[_computeEndTime(endTime, upToNow, historyData)]]'>
|
||||||
</state-history-chart-line>
|
</state-history-chart-line>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
@ -56,65 +65,67 @@
|
|||||||
</dom-module>
|
</dom-module>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
Polymer({
|
class StateHistoryCharts extends Polymer.Element {
|
||||||
is: 'state-history-charts',
|
static get is() { return 'state-history-charts'; }
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
historyData: {
|
||||||
|
type: Object,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
|
||||||
properties: {
|
isLoadingData: {
|
||||||
historyData: {
|
type: Boolean,
|
||||||
type: Object,
|
value: true,
|
||||||
value: null,
|
},
|
||||||
},
|
|
||||||
|
|
||||||
isLoadingData: {
|
endTime: {
|
||||||
type: Boolean,
|
type: Object,
|
||||||
value: true,
|
},
|
||||||
},
|
|
||||||
|
|
||||||
endTime: {
|
upToNow: Boolean,
|
||||||
type: Object,
|
noSingle: Boolean,
|
||||||
},
|
|
||||||
|
|
||||||
upToNow: {
|
_apiLoaded: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
value: false,
|
value: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
_apiLoaded: {
|
_isLoading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
value: false,
|
computed: '_computeIsLoading(isLoadingData, _apiLoaded)',
|
||||||
},
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
_isLoading: {
|
_computeIsSingleLineChart(historyData, noSingle) {
|
||||||
type: Boolean,
|
return !noSingle && historyData && historyData.line.length === 1;
|
||||||
computed: '_computeIsLoading(isLoadingData, _apiLoaded)',
|
}
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
_computeIsSingleLineChart: function (historyData) {
|
_googleApiLoaded() {
|
||||||
return historyData && historyData.line.length === 1;
|
|
||||||
},
|
|
||||||
|
|
||||||
_googleApiLoaded: function () {
|
|
||||||
window.google.load('visualization', '1', {
|
window.google.load('visualization', '1', {
|
||||||
packages: ['timeline', 'corechart'],
|
packages: ['timeline', 'corechart'],
|
||||||
callback: function () {
|
callback: function () {
|
||||||
this._apiLoaded = true;
|
this._apiLoaded = true;
|
||||||
}.bind(this),
|
}.bind(this),
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
_computeIsLoading: function (_isLoadingData, _apiLoaded) {
|
_computeIsLoading(_isLoadingData, _apiLoaded) {
|
||||||
return _isLoadingData || !_apiLoaded;
|
return _isLoadingData || !_apiLoaded;
|
||||||
},
|
}
|
||||||
|
|
||||||
_computeIsEmpty: function (historyData) {
|
_computeIsEmpty(historyData) {
|
||||||
return (historyData &&
|
return (historyData &&
|
||||||
historyData.timeline.length === 0 &&
|
historyData.timeline.length === 0 &&
|
||||||
historyData.line.length === 0);
|
historyData.line.length === 0);
|
||||||
},
|
}
|
||||||
|
|
||||||
_computeEndTime: function (endTime, upToNow) {
|
_computeEndTime(endTime, upToNow) {
|
||||||
|
// We don't really care about the value of historyData, but if it change we want to update
|
||||||
|
// endTime.
|
||||||
return upToNow ? new Date() : endTime;
|
return upToNow ? new Date() : endTime;
|
||||||
},
|
}
|
||||||
});
|
}
|
||||||
|
customElements.define(StateHistoryCharts.is, StateHistoryCharts);
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,35 +1,43 @@
|
|||||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
<link rel="import" href="../../bower_components/polymer/polymer-element.html">
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
{
|
||||||
var RECENT_THRESHOLD = 60000; // 1 minute
|
const RECENT_THRESHOLD = 60000; // 1 minute
|
||||||
var RECENT_CACHE = {};
|
const RECENT_CACHE = {};
|
||||||
|
const DOMAINS_USE_LAST_UPDATED = ['thermostat', 'climate'];
|
||||||
|
const LINE_ATTRIBUTES_TO_KEEP = ['temperature', 'current_temperature', 'target_temp_low', 'target_temp_high'];
|
||||||
|
window.stateHistoryCache = window.stateHistoryCache || {};
|
||||||
|
|
||||||
function computeHistory(stateHistory) {
|
function computeHistory(stateHistory) {
|
||||||
var lineChartDevices = {};
|
const lineChartDevices = {};
|
||||||
var timelineDevices = [];
|
const timelineDevices = [];
|
||||||
var unitStates;
|
|
||||||
|
|
||||||
if (!stateHistory) {
|
if (!stateHistory) {
|
||||||
return { line: [], timeline: [] };
|
return { line: [], timeline: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
stateHistory.forEach(function (stateInfo) {
|
stateHistory.forEach((stateInfo) => {
|
||||||
var stateWithUnit;
|
if (stateInfo.length === 0) {
|
||||||
var unit;
|
|
||||||
|
|
||||||
if (stateInfo.size === 0) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
stateWithUnit = stateInfo.find(
|
const stateWithUnit = stateInfo.find(
|
||||||
function (state) { return 'unit_of_measurement' in state.attributes; });
|
state => 'unit_of_measurement' in state.attributes);
|
||||||
|
|
||||||
unit = stateWithUnit ?
|
const unit = stateWithUnit ?
|
||||||
stateWithUnit.attributes.unit_of_measurement : false;
|
stateWithUnit.attributes.unit_of_measurement : false;
|
||||||
|
|
||||||
if (!unit) {
|
if (!unit) {
|
||||||
timelineDevices.push(stateInfo);
|
timelineDevices.push({
|
||||||
|
name: window.hassUtil.computeStateName(stateInfo[0]),
|
||||||
|
entity_id: stateInfo[0].entity_id,
|
||||||
|
data: stateInfo
|
||||||
|
.map(state => ({ state: state.state, last_changed: state.last_changed }))
|
||||||
|
.filter((element, index, arr) => {
|
||||||
|
if (index === 0) return true;
|
||||||
|
return element.state !== arr[index - 1].state;
|
||||||
|
})
|
||||||
|
});
|
||||||
} else if (unit in lineChartDevices) {
|
} else if (unit in lineChartDevices) {
|
||||||
lineChartDevices[unit].push(stateInfo);
|
lineChartDevices[unit].push(stateInfo);
|
||||||
} else {
|
} else {
|
||||||
@ -37,135 +45,295 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
unitStates = Object.keys(lineChartDevices).map(
|
const unitStates = Object.keys(lineChartDevices).map(
|
||||||
function (unit) {
|
unit => ({
|
||||||
return { unit: unit, data: lineChartDevices[unit] };
|
unit: unit,
|
||||||
});
|
data: lineChartDevices[unit].map((states) => {
|
||||||
|
const last = states[states.length - 1];
|
||||||
|
const domain = window.hassUtil.computeDomain(last);
|
||||||
|
return {
|
||||||
|
domain: domain,
|
||||||
|
name: window.hassUtil.computeStateName(last),
|
||||||
|
entity_id: last.entity_id,
|
||||||
|
states: states.map((state) => {
|
||||||
|
const result = {
|
||||||
|
state: state.state,
|
||||||
|
last_changed: state.last_changed,
|
||||||
|
};
|
||||||
|
if (DOMAINS_USE_LAST_UPDATED.indexOf(domain) !== -1) {
|
||||||
|
result.last_changed = state.last_updated;
|
||||||
|
}
|
||||||
|
LINE_ATTRIBUTES_TO_KEEP.forEach((attr) => {
|
||||||
|
if (attr in state.attributes) {
|
||||||
|
result.attributes = result.attributes || {};
|
||||||
|
result.attributes[attr] = state.attributes[attr];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
return { line: unitStates, timeline: timelineDevices };
|
return {
|
||||||
|
line: unitStates, timeline: timelineDevices };
|
||||||
}
|
}
|
||||||
|
|
||||||
Polymer({
|
class HaStateHistoryData extends Polymer.Element {
|
||||||
is: 'ha-state-history-data',
|
static get is() { return 'ha-state-history-data'; }
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
hass: {
|
||||||
|
type: Object,
|
||||||
|
observer: 'hassChanged',
|
||||||
|
},
|
||||||
|
|
||||||
properties: {
|
filterType: String,
|
||||||
hass: {
|
|
||||||
type: Object,
|
|
||||||
observer: 'hassChanged',
|
|
||||||
},
|
|
||||||
|
|
||||||
filterType: {
|
cacheConfig: Object,
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
startTime: {
|
startTime: Date,
|
||||||
type: Date,
|
endTime: Date,
|
||||||
value: null,
|
|
||||||
},
|
|
||||||
|
|
||||||
endTime: {
|
entityId: String,
|
||||||
type: Date,
|
|
||||||
value: null,
|
|
||||||
},
|
|
||||||
|
|
||||||
entityId: {
|
isLoading: {
|
||||||
type: String,
|
type: Boolean,
|
||||||
value: null,
|
value: true,
|
||||||
},
|
readOnly: true,
|
||||||
|
notify: true,
|
||||||
|
},
|
||||||
|
|
||||||
isLoading: {
|
data: {
|
||||||
type: Boolean,
|
type: Object,
|
||||||
value: true,
|
value: null,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
notify: true,
|
notify: true,
|
||||||
},
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
data: {
|
static get observers() {
|
||||||
type: Object,
|
return [
|
||||||
value: null,
|
'filterChanged(filterType, entityId, startTime, endTime, cacheConfig)',
|
||||||
readOnly: true,
|
];
|
||||||
notify: true,
|
}
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
observers: [
|
disconnectedCallback() {
|
||||||
'filterChanged(filterType, entityId, startTime, endTime)',
|
if (this._refreshTimeoutId) {
|
||||||
],
|
window.clearInterval(this._refreshTimeoutId);
|
||||||
|
this._refreshTimeoutId = null;
|
||||||
hassChanged: function (newHass, oldHass) {
|
|
||||||
if (!oldHass) {
|
|
||||||
this.filterChanged(this.filterType, this.entityId, this.startTime, this.endTime);
|
|
||||||
}
|
}
|
||||||
},
|
super.disconnectedCallback();
|
||||||
|
}
|
||||||
|
|
||||||
filterChanged: function (filterType, entityId, startTime, endTime) {
|
hassChanged(newHass, oldHass) {
|
||||||
|
if (!oldHass && !this._madeFirstCall) {
|
||||||
|
this.filterChanged(this.filterType, this.entityId, this.startTime, this.endTime,
|
||||||
|
this.cacheConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filterChanged(filterType, entityId, startTime, endTime, cacheConfig) {
|
||||||
if (!this.hass) return;
|
if (!this.hass) return;
|
||||||
|
this._madeFirstCall = true;
|
||||||
var data;
|
let data;
|
||||||
|
|
||||||
if (filterType === 'date') {
|
if (filterType === 'date') {
|
||||||
if (startTime === null || endTime === null) return;
|
if (!startTime || !endTime) return;
|
||||||
data = this.getDate(startTime, endTime);
|
data = this.getDate(startTime, endTime);
|
||||||
} else if (filterType === 'recent-entity') {
|
} else if (filterType === 'recent-entity') {
|
||||||
if (entityId === null) return;
|
if (!entityId) return;
|
||||||
data = this.getRecent(entityId);
|
if (cacheConfig) {
|
||||||
|
data = this.getRecentWithCacheRefresh(entityId, cacheConfig);
|
||||||
|
} else {
|
||||||
|
data = this.getRecent(entityId, startTime, endTime);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._setIsLoading(true);
|
this._setIsLoading(true);
|
||||||
|
|
||||||
data.then(function (stateHistory) {
|
data.then((stateHistory) => {
|
||||||
this._setData(stateHistory);
|
this._setData(stateHistory);
|
||||||
this._setIsLoading(false);
|
this._setIsLoading(false);
|
||||||
}.bind(this));
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
getRecent: function (entityId) {
|
getEmptyCache() {
|
||||||
var cache = RECENT_CACHE[entityId];
|
return {
|
||||||
|
prom: Promise.resolve({ line: [], timeline: [] }),
|
||||||
|
data: { line: [], timeline: [] },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getRecentWithCacheRefresh(entityId, cacheConfig) {
|
||||||
|
if (this._refreshTimeoutId) {
|
||||||
|
window.clearInterval(this._refreshTimeoutId);
|
||||||
|
}
|
||||||
|
if (cacheConfig.refresh) {
|
||||||
|
this._refreshTimeoutId = window.setInterval(() => {
|
||||||
|
this.getRecentWithCache(entityId, cacheConfig)
|
||||||
|
.then((stateHistory) => {
|
||||||
|
this._setData(Object.assign({}, stateHistory));
|
||||||
|
});
|
||||||
|
}, cacheConfig.refresh * 1000);
|
||||||
|
}
|
||||||
|
return this.getRecentWithCache(entityId, cacheConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeLine(historyLines, cacheLines) {
|
||||||
|
historyLines.forEach((line) => {
|
||||||
|
const unit = line.unit;
|
||||||
|
const oldLine = cacheLines.find(cacheLine => cacheLine.unit === unit);
|
||||||
|
if (oldLine) {
|
||||||
|
line.data.forEach((entity) => {
|
||||||
|
const oldEntity =
|
||||||
|
oldLine.data.find(cacheEntity => entity.entity_id === cacheEntity.entity_id);
|
||||||
|
if (oldEntity) {
|
||||||
|
oldEntity.states = oldEntity.state.concat(entity.states);
|
||||||
|
} else {
|
||||||
|
oldLine.data.push(entity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cacheLines.push(line);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeTimeline(historyTimelines, cacheTimelines) {
|
||||||
|
historyTimelines.forEach((timeline) => {
|
||||||
|
const oldTimeline =
|
||||||
|
cacheTimelines.find(cacheTimeline => cacheTimeline.entity_id === timeline.entity_id);
|
||||||
|
if (oldTimeline) {
|
||||||
|
oldTimeline.data = oldTimeline.data.concat(timeline.data);
|
||||||
|
} else {
|
||||||
|
cacheTimelines.push(timeline);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pruneArray(originalStartTime, arr) {
|
||||||
|
if (arr.length === 0) return arr;
|
||||||
|
const changedAfterStartTime = arr.findIndex((state) => {
|
||||||
|
const lastChanged = new Date(state.last_changed);
|
||||||
|
return lastChanged > originalStartTime;
|
||||||
|
});
|
||||||
|
if (changedAfterStartTime === 0) {
|
||||||
|
// If all changes happened after originalStartTime then we are done.
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all changes happened at or before originalStartTime. Use last index.
|
||||||
|
const updateIndex = changedAfterStartTime === -1 ? arr.length - 1 : changedAfterStartTime - 1;
|
||||||
|
arr[updateIndex].last_changed = originalStartTime;
|
||||||
|
return arr.slice(updateIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
pruneStartTime(originalStartTime, cacheData) {
|
||||||
|
cacheData.line.forEach((line) => {
|
||||||
|
line.data.forEach((entity) => {
|
||||||
|
entity.states = this.pruneArray(originalStartTime, entity.states);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
cacheData.timeline.forEach((timeline) => {
|
||||||
|
timeline.data = this.pruneArray(originalStartTime, timeline.data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getRecentWithCache(entityId, cacheConfig) {
|
||||||
|
const cacheKey = cacheConfig.cacheKey;
|
||||||
|
const endTime = new Date();
|
||||||
|
const originalStartTime = new Date(endTime);
|
||||||
|
originalStartTime.setHours(originalStartTime.getHours() - cacheConfig.hoursToShow);
|
||||||
|
let startTime = originalStartTime;
|
||||||
|
let appendingToCache = false;
|
||||||
|
let cache = window.stateHistoryCache[cacheKey];
|
||||||
|
if (cache && startTime >= cache.startTime && startTime <= cache.endTime) {
|
||||||
|
startTime = cache.endTime;
|
||||||
|
appendingToCache = true;
|
||||||
|
if (endTime <= cache.endTime) {
|
||||||
|
return cache.prom;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cache = window.stateHistoryCache[cacheKey] = this.getEmptyCache();
|
||||||
|
}
|
||||||
|
// Use Promise.all in order to make sure the old and the new fetches have both completed.
|
||||||
|
const prom = Promise.all([cache.prom,
|
||||||
|
this.fetchRecent(entityId, startTime, endTime, appendingToCache)])
|
||||||
|
// Use only data from the new fetch. Old fetch is already stored in cache.data
|
||||||
|
.then(oldAndNew => oldAndNew[1])
|
||||||
|
// Convert data into format state-history-chart-* understands.
|
||||||
|
.then(stateHistory => computeHistory(stateHistory))
|
||||||
|
// Merge old and new.
|
||||||
|
.then((stateHistory) => {
|
||||||
|
this.mergeLine(stateHistory.line, cache.data.line);
|
||||||
|
this.mergeTimeline(stateHistory.timeline, cache.data.timeline);
|
||||||
|
if (appendingToCache) {
|
||||||
|
this.pruneStartTime(originalStartTime, cache.data);
|
||||||
|
}
|
||||||
|
return cache.data;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
window.stateHistoryCache[cacheKey] = undefined;
|
||||||
|
});
|
||||||
|
cache.prom = prom;
|
||||||
|
cache.startTime = originalStartTime;
|
||||||
|
cache.endTime = endTime;
|
||||||
|
return prom;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRecent(entityId, startTime, endTime) {
|
||||||
|
const cacheKey = entityId;
|
||||||
|
const cache = RECENT_CACHE[cacheKey];
|
||||||
|
|
||||||
if (cache && Date.now() - cache.created < RECENT_THRESHOLD) {
|
if (cache && Date.now() - cache.created < RECENT_THRESHOLD) {
|
||||||
return cache.data;
|
return cache.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
var url = 'history/period';
|
const prom = this.fetchRecent(entityId, startTime, endTime).then(
|
||||||
|
stateHistory => computeHistory(stateHistory),
|
||||||
if (entityId) {
|
() => {
|
||||||
url += '?filter_entity_id=' + entityId;
|
|
||||||
}
|
|
||||||
|
|
||||||
var prom = this.hass.callApi('GET', url).then(
|
|
||||||
function (stateHistory) {
|
|
||||||
return computeHistory(stateHistory);
|
|
||||||
},
|
|
||||||
function () {
|
|
||||||
RECENT_CACHE[entityId] = false;
|
RECENT_CACHE[entityId] = false;
|
||||||
return null;
|
return null;
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
RECENT_CACHE[entityId] = {
|
RECENT_CACHE[cacheKey] = {
|
||||||
created: Date.now(),
|
created: Date.now(),
|
||||||
data: prom,
|
data: prom,
|
||||||
};
|
};
|
||||||
|
return prom;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchRecent(entityId, startTime, endTime, skipInitialState = false) {
|
||||||
|
let url = 'history/period';
|
||||||
|
if (startTime) {
|
||||||
|
url += '/' + startTime.toISOString();
|
||||||
|
}
|
||||||
|
url += '?filter_entity_id=' + entityId;
|
||||||
|
if (endTime) {
|
||||||
|
url += '&end_time=' + endTime.toISOString();
|
||||||
|
}
|
||||||
|
if (skipInitialState) {
|
||||||
|
url += '&skip_initial_state';
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.hass.callApi('GET', url);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDate(startTime, endTime) {
|
||||||
|
const filter = startTime.toISOString() + '?end_time=' + endTime.toISOString();
|
||||||
|
|
||||||
|
const prom = this.hass.callApi('GET', 'history/period/' + filter).then(
|
||||||
|
stateHistory => computeHistory(stateHistory),
|
||||||
|
() => null);
|
||||||
|
|
||||||
return prom;
|
return prom;
|
||||||
},
|
}
|
||||||
|
}
|
||||||
getDate: function (startTime, endTime) {
|
customElements.define(HaStateHistoryData.is, HaStateHistoryData);
|
||||||
var filter = startTime.toISOString() + '?end_time=' + endTime.toISOString();
|
}
|
||||||
|
|
||||||
var prom = this.hass.callApi('GET', 'history/period/' + filter).then(
|
|
||||||
function (stateHistory) {
|
|
||||||
return computeHistory(stateHistory);
|
|
||||||
},
|
|
||||||
function () {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return prom;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}());
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
<link rel="import" href="../../bower_components/polymer/polymer-element.html">
|
||||||
|
|
||||||
<link rel="import" href="../../bower_components/paper-dialog/paper-dialog.html">
|
<link rel="import" href="../../bower_components/paper-dialog/paper-dialog.html">
|
||||||
<link rel="import" href="../../bower_components/paper-dialog-scrollable/paper-dialog-scrollable.html">
|
<link rel="import" href="../../bower_components/paper-dialog-scrollable/paper-dialog-scrollable.html">
|
||||||
@ -9,6 +9,7 @@
|
|||||||
<link rel="import" href="../components/state-history-charts.html">
|
<link rel="import" href="../components/state-history-charts.html">
|
||||||
<link rel="import" href="../more-infos/more-info-content.html">
|
<link rel="import" href="../more-infos/more-info-content.html">
|
||||||
<link rel="import" href="../data/ha-state-history-data.html">
|
<link rel="import" href="../data/ha-state-history-data.html">
|
||||||
|
<link rel='import' href='../util/hass-mixins.html'>
|
||||||
|
|
||||||
<dom-module id="more-info-dialog">
|
<dom-module id="more-info-dialog">
|
||||||
<template>
|
<template>
|
||||||
@ -23,6 +24,14 @@
|
|||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
paper-dialog[data-domain=history_graph] {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
paper-dialog[data-domain=history_graph] h2 {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
state-history-charts {
|
state-history-charts {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@ -48,6 +57,13 @@
|
|||||||
border-bottom-left-radius: 0px;
|
border-bottom-left-radius: 0px;
|
||||||
border-bottom-right-radius: 0px;
|
border-bottom-right-radius: 0px;
|
||||||
}
|
}
|
||||||
|
paper-dialog[data-domain=history_graph] {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dom-if {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@ -63,10 +79,11 @@
|
|||||||
<div>
|
<div>
|
||||||
<ha-state-history-data
|
<ha-state-history-data
|
||||||
hass='[[hass]]'
|
hass='[[hass]]'
|
||||||
filter-type='[[_filterType]]'
|
filter-type='recent-entity'
|
||||||
entity-id='[[stateObj.entity_id]]'
|
entity-id='[[stateObj.entity_id]]'
|
||||||
data='{{stateHistory}}'
|
data='{{stateHistory}}'
|
||||||
is-loading='{{stateHistoryLoading}}'
|
is-loading='{{stateHistoryLoading}}'
|
||||||
|
cache-config='[[computeCacheConfig(stateObj)]]'
|
||||||
></ha-state-history-data>
|
></ha-state-history-data>
|
||||||
<state-history-charts
|
<state-history-charts
|
||||||
history-data="[[stateHistory]]"
|
history-data="[[stateHistory]]"
|
||||||
@ -84,72 +101,71 @@
|
|||||||
</dom-module>
|
</dom-module>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
Polymer({
|
class MoreInfoDialog extends window.hassMixins.EventsMixin(Polymer.Element) {
|
||||||
is: 'more-info-dialog',
|
static get is() { return 'more-info-dialog'; }
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
hass: Object,
|
||||||
|
|
||||||
properties: {
|
stateObj: {
|
||||||
hass: {
|
type: Object,
|
||||||
type: Object,
|
computed: 'computeStateObj(hass)',
|
||||||
},
|
observer: 'stateObjChanged',
|
||||||
|
},
|
||||||
|
|
||||||
stateObj: {
|
stateHistory: Object,
|
||||||
type: Object,
|
|
||||||
computed: 'computeStateObj(hass)',
|
|
||||||
observer: 'stateObjChanged',
|
|
||||||
},
|
|
||||||
|
|
||||||
stateHistory: {
|
stateHistoryLoading: Boolean,
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
|
|
||||||
stateHistoryLoading: {
|
isLoadingHistoryData: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
computed: 'computeIsLoadingHistoryData(delayedDialogOpen, stateHistoryLoading)',
|
||||||
|
},
|
||||||
|
|
||||||
isLoadingHistoryData: {
|
hasHistoryComponent: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
computed: 'computeIsLoadingHistoryData(delayedDialogOpen, stateHistoryLoading)',
|
computed: 'computeHasHistoryComponent(hass)',
|
||||||
},
|
},
|
||||||
|
|
||||||
hasHistoryComponent: {
|
showHistoryComponent: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
computed: 'computeHasHistoryComponent(hass)',
|
value: false,
|
||||||
},
|
computed: 'computeShowHistoryComponent(hasHistoryComponent, stateObj)',
|
||||||
|
},
|
||||||
|
|
||||||
showHistoryComponent: {
|
dialogOpen: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
value: false,
|
value: false,
|
||||||
computed: 'computeShowHistoryComponent(hasHistoryComponent, stateObj)',
|
observer: 'dialogOpenChanged',
|
||||||
},
|
},
|
||||||
|
|
||||||
dialogOpen: {
|
delayedDialogOpen: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
value: false,
|
value: false,
|
||||||
observer: 'dialogOpenChanged',
|
},
|
||||||
},
|
};
|
||||||
|
}
|
||||||
|
|
||||||
delayedDialogOpen: {
|
connectedCallback() {
|
||||||
type: Boolean,
|
super.connectedCallback();
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
_filterType: {
|
|
||||||
type: String,
|
|
||||||
value: 'recent-entity',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
ready: function () {
|
|
||||||
this.$.scrollable.dialogElement = this.$.dialog;
|
this.$.scrollable.dialogElement = this.$.dialog;
|
||||||
},
|
}
|
||||||
|
|
||||||
computeDomain: function (stateObj) {
|
computeDomain(stateObj) {
|
||||||
return stateObj ? window.hassUtil.computeDomain(stateObj) : '';
|
return stateObj ? window.hassUtil.computeDomain(stateObj) : '';
|
||||||
},
|
}
|
||||||
|
|
||||||
computeStateObj: function (hass) {
|
computeStateObj(hass) {
|
||||||
return hass.states[hass.moreInfoEntityId] || null;
|
return hass.states[hass.moreInfoEntityId] || null;
|
||||||
},
|
}
|
||||||
|
|
||||||
|
computeCacheConfig(stateObj) {
|
||||||
|
return {
|
||||||
|
refresh: 60,
|
||||||
|
cacheKey: 'more_info.' + stateObj.entity_id,
|
||||||
|
hoursToShow: 24,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We depend on a delayed dialogOpen value to tell the chart component
|
* We depend on a delayed dialogOpen value to tell the chart component
|
||||||
@ -157,40 +173,41 @@ Polymer({
|
|||||||
* before the dialog is attached to the screen and is unable to determine
|
* before the dialog is attached to the screen and is unable to determine
|
||||||
* graph size resulting in scroll bars.
|
* graph size resulting in scroll bars.
|
||||||
*/
|
*/
|
||||||
computeIsLoadingHistoryData: function (delayedDialogOpen, stateHistoryLoading) {
|
computeIsLoadingHistoryData(delayedDialogOpen, stateHistoryLoading) {
|
||||||
return !delayedDialogOpen || stateHistoryLoading;
|
return !delayedDialogOpen || stateHistoryLoading;
|
||||||
},
|
}
|
||||||
|
|
||||||
computeHasHistoryComponent: function (hass) {
|
computeHasHistoryComponent(hass) {
|
||||||
return window.hassUtil.isComponentLoaded(hass, 'history');
|
return window.hassUtil.isComponentLoaded(hass, 'history');
|
||||||
},
|
}
|
||||||
|
|
||||||
computeShowHistoryComponent: function (hasHistoryComponent, stateObj) {
|
computeShowHistoryComponent(hasHistoryComponent, stateObj) {
|
||||||
return this.hasHistoryComponent && stateObj &&
|
return this.hasHistoryComponent && stateObj &&
|
||||||
window.hassUtil.DOMAINS_WITH_NO_HISTORY.indexOf(
|
window.hassUtil.DOMAINS_WITH_NO_HISTORY.indexOf(
|
||||||
window.hassUtil.computeDomain(stateObj)) === -1;
|
window.hassUtil.computeDomain(stateObj)) === -1;
|
||||||
},
|
}
|
||||||
|
|
||||||
stateObjChanged: function (newVal) {
|
stateObjChanged(newVal) {
|
||||||
if (!newVal) {
|
if (!newVal) {
|
||||||
this.dialogOpen = false;
|
this.dialogOpen = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.async(function () {
|
window.setTimeout(() => {
|
||||||
// allow dialog to render content before showing it so it is
|
// allow dialog to render content before showing it so it is
|
||||||
// positioned correctly.
|
// positioned correctly.
|
||||||
this.dialogOpen = true;
|
this.dialogOpen = true;
|
||||||
}.bind(this), 10);
|
}, 10);
|
||||||
},
|
}
|
||||||
|
|
||||||
dialogOpenChanged: function (newVal) {
|
dialogOpenChanged(newVal) {
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
this.async(function () { this.delayedDialogOpen = true; }.bind(this), 100);
|
window.setTimeout(() => { this.delayedDialogOpen = true; }, 100);
|
||||||
} else if (!newVal && this.stateObj) {
|
} else if (!newVal && this.stateObj) {
|
||||||
this.fire('hass-more-info', { entityId: null });
|
this.fire('hass-more-info', { entityId: null });
|
||||||
this.delayedDialogOpen = false;
|
this.delayedDialogOpen = false;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
});
|
}
|
||||||
|
customElements.define(MoreInfoDialog.is, MoreInfoDialog);
|
||||||
</script>
|
</script>
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
<link rel='import' href='more-info-cover.html'>
|
<link rel='import' href='more-info-cover.html'>
|
||||||
<link rel='import' href='more-info-default.html'>
|
<link rel='import' href='more-info-default.html'>
|
||||||
<link rel='import' href='more-info-fan.html'>
|
<link rel='import' href='more-info-fan.html'>
|
||||||
|
<link rel='import' href='more-info-history_graph.html'>
|
||||||
<link rel='import' href='more-info-group.html'>
|
<link rel='import' href='more-info-group.html'>
|
||||||
<link rel='import' href='more-info-light.html'>
|
<link rel='import' href='more-info-light.html'>
|
||||||
<link rel='import' href='more-info-lock.html'>
|
<link rel='import' href='more-info-lock.html'>
|
||||||
|
35
src/more-infos/more-info-history_graph.html
Normal file
35
src/more-infos/more-info-history_graph.html
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<link rel="import" href="../../bower_components/polymer/polymer-element.html">
|
||||||
|
|
||||||
|
<link rel="import" href="../cards/ha-history_graph-card.html">
|
||||||
|
|
||||||
|
<link rel="import" href="../components/ha-attributes.html">
|
||||||
|
|
||||||
|
<dom-module id="more-info-history_graph">
|
||||||
|
<template>
|
||||||
|
<style>
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<ha-history_graph-card
|
||||||
|
hass='[[hass]]'
|
||||||
|
state-obj='[[stateObj]]'
|
||||||
|
in-dialog
|
||||||
|
></ha-graph-card>
|
||||||
|
<ha-attributes state-obj="[[stateObj]]"></ha-attributes>
|
||||||
|
</template>
|
||||||
|
</dom-module>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
class MoreInfoHistoryGraph extends Polymer.Element {
|
||||||
|
static get is() { return 'more-info-history_graph'; }
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
hass: Object,
|
||||||
|
stateObj: Object,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define(MoreInfoHistoryGraph.is, MoreInfoHistoryGraph);
|
||||||
|
</script>
|
@ -25,11 +25,11 @@ window.hassUtil.DOMAINS_WITH_CARD = [
|
|||||||
|
|
||||||
window.hassUtil.DOMAINS_WITH_MORE_INFO = [
|
window.hassUtil.DOMAINS_WITH_MORE_INFO = [
|
||||||
'alarm_control_panel', 'automation', 'camera', 'climate', 'configurator',
|
'alarm_control_panel', 'automation', 'camera', 'climate', 'configurator',
|
||||||
'cover', 'fan', 'group', 'light', 'lock', 'media_player', 'script',
|
'cover', 'fan', 'group', 'history_graph', 'light', 'lock', 'media_player', 'script',
|
||||||
'sun', 'updater', 'vacuum',
|
'sun', 'updater', 'vacuum',
|
||||||
];
|
];
|
||||||
|
|
||||||
window.hassUtil.DOMAINS_WITH_NO_HISTORY = ['camera', 'configurator', 'scene'];
|
window.hassUtil.DOMAINS_WITH_NO_HISTORY = ['camera', 'configurator', 'history_graph', 'scene'];
|
||||||
|
|
||||||
window.hassUtil.HIDE_MORE_INFO = [
|
window.hassUtil.HIDE_MORE_INFO = [
|
||||||
'input_select', 'scene', 'script', 'input_number', 'input_text'
|
'input_select', 'scene', 'script', 'input_number', 'input_text'
|
||||||
@ -274,6 +274,9 @@ window.hassUtil.domainIcon = function (domain, state) {
|
|||||||
case 'fan':
|
case 'fan':
|
||||||
return 'mdi:fan';
|
return 'mdi:fan';
|
||||||
|
|
||||||
|
case 'history_graph':
|
||||||
|
return 'mdi:chart-line';
|
||||||
|
|
||||||
case 'group':
|
case 'group':
|
||||||
return 'mdi:google-circles-communities';
|
return 'mdi:google-circles-communities';
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user