mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-10 10:56:34 +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-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-media_player-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.
|
||||
var DOMAINS_WITH_CARD = {
|
||||
camera: 4,
|
||||
history_graph: 4,
|
||||
media_player: 3,
|
||||
persistent_notification: 0,
|
||||
weather: 4,
|
||||
|
@ -1,4 +1,5 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../../bower_components/iron-resizable-behavior/iron-resizable-behavior.html">
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
@ -20,13 +21,13 @@
|
||||
return !isNaN(parsed) && isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
Polymer({
|
||||
is: 'state-history-chart-line',
|
||||
|
||||
properties: {
|
||||
class StateHistoryChartLine extends
|
||||
Polymer.mixinBehaviors([Polymer.IronResizableBehavior], Polymer.Element) {
|
||||
static get is() { return 'state-history-chart-line'; }
|
||||
static get properties() {
|
||||
return {
|
||||
data: {
|
||||
type: Object,
|
||||
observer: 'dataChanged',
|
||||
},
|
||||
|
||||
unit: {
|
||||
@ -38,12 +39,6 @@
|
||||
value: false,
|
||||
},
|
||||
|
||||
isAttached: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
observer: 'dataChanged',
|
||||
},
|
||||
|
||||
endTime: {
|
||||
type: Object,
|
||||
},
|
||||
@ -51,21 +46,27 @@
|
||||
chartEngine: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
created: function () {
|
||||
this.style.display = 'block';
|
||||
},
|
||||
static get observers() {
|
||||
return ['dataChanged(data, endTime)'];
|
||||
}
|
||||
|
||||
attached: function () {
|
||||
this.isAttached = true;
|
||||
},
|
||||
|
||||
dataChanged: function () {
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._isAttached = true;
|
||||
this.drawChart();
|
||||
},
|
||||
this.addEventListener('iron-resize', () => {
|
||||
this.async(this.drawChart, 10);
|
||||
});
|
||||
}
|
||||
|
||||
drawChart: function () {
|
||||
dataChanged() {
|
||||
this.drawChart();
|
||||
}
|
||||
|
||||
drawChart() {
|
||||
var unit = this.unit;
|
||||
var deviceStates = this.data;
|
||||
var options;
|
||||
@ -75,7 +76,7 @@
|
||||
var finalDataTable;
|
||||
var daysDelta;
|
||||
|
||||
if (!this.isAttached) {
|
||||
if (!this._isAttached) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -117,12 +118,12 @@
|
||||
}
|
||||
|
||||
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 ||
|
||||
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()) {
|
||||
endTime = new Date();
|
||||
@ -139,9 +140,8 @@
|
||||
}
|
||||
|
||||
dataTables = deviceStates.map(function (states) {
|
||||
var last = states[states.length - 1];
|
||||
var domain = window.hassUtil.computeDomain(last);
|
||||
var name = window.hassUtil.computeStateName(last);
|
||||
var domain = states.domain;
|
||||
var name = states.name;
|
||||
var data = [];
|
||||
var dataTable = new window.google.visualization.DataTable();
|
||||
// array containing [time, value1, value2, etc]
|
||||
@ -174,7 +174,7 @@
|
||||
if (domain === 'thermostat' || domain === 'climate') {
|
||||
// We differentiate between thermostats that have a target temperature
|
||||
// range versus ones that have just a target temperature
|
||||
hasTargetRange = states.reduce(
|
||||
hasTargetRange = states.states.reduce(
|
||||
function (cum, cur) {
|
||||
return cum || cur.attributes.target_temp_high !== cur.attributes.target_temp_low;
|
||||
}, false);
|
||||
@ -192,7 +192,7 @@
|
||||
var targetHigh = saveParseFloat(state.attributes.target_temp_high);
|
||||
var targetLow = saveParseFloat(state.attributes.target_temp_low);
|
||||
pushData(
|
||||
[new Date(state.last_updated), curTemp, targetHigh, targetLow],
|
||||
[new Date(state.last_changed), curTemp, targetHigh, targetLow],
|
||||
noInterpolations);
|
||||
};
|
||||
} else {
|
||||
@ -203,18 +203,18 @@
|
||||
processState = function (state) {
|
||||
var curTemp = saveParseFloat(state.attributes.current_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 {
|
||||
dataTable.addColumn('number', name);
|
||||
|
||||
// Only disable interpolation for sensors
|
||||
noInterpolations = domain !== 'sensor' && [true];
|
||||
|
||||
states.forEach(function (state) {
|
||||
states.states.forEach(function (state) {
|
||||
var value = saveParseFloat(state.state);
|
||||
pushData([new Date(state.last_changed), value], noInterpolations);
|
||||
});
|
||||
@ -241,7 +241,8 @@
|
||||
}
|
||||
|
||||
this.chartEngine.draw(finalDataTable, options);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define(StateHistoryChartLine.is, StateHistoryChartLine);
|
||||
}());
|
||||
</script>
|
||||
|
@ -1,47 +1,38 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
|
||||
<style>
|
||||
div.charts-tooltip {
|
||||
z-index: 200 !important;
|
||||
}
|
||||
</style>
|
||||
<link rel="import" href="../../bower_components/polymer/polymer-element.html">
|
||||
<link rel="import" href="../../bower_components/iron-resizable-behavior/iron-resizable-behavior.html">
|
||||
|
||||
<script>
|
||||
|
||||
Polymer({
|
||||
is: 'state-history-chart-timeline',
|
||||
|
||||
properties: {
|
||||
class StateHistoryChartTimeline extends
|
||||
Polymer.mixinBehaviors([Polymer.IronResizableBehavior], Polymer.Element) {
|
||||
static get is() { return 'state-history-chart-timeline'; }
|
||||
static get properties() {
|
||||
return {
|
||||
data: {
|
||||
type: Object,
|
||||
observer: 'dataChanged',
|
||||
},
|
||||
noSingle: Boolean,
|
||||
endTime: Date,
|
||||
};
|
||||
}
|
||||
|
||||
endTime: {
|
||||
type: Object,
|
||||
},
|
||||
static get observers() {
|
||||
return ['dataChanged(data, endTime)'];
|
||||
}
|
||||
|
||||
isAttached: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
observer: 'dataChanged',
|
||||
},
|
||||
},
|
||||
|
||||
created: function () {
|
||||
this.style.display = 'block';
|
||||
},
|
||||
|
||||
attached: function () {
|
||||
this.isAttached = true;
|
||||
},
|
||||
|
||||
dataChanged: function () {
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._isAttached = true;
|
||||
this.drawChart();
|
||||
},
|
||||
this.addEventListener('iron-resize', () => {
|
||||
this.async(this.drawChart, 10);
|
||||
});
|
||||
}
|
||||
|
||||
drawChart: function () {
|
||||
var root = Polymer.dom(this);
|
||||
dataChanged() {
|
||||
this.drawChart();
|
||||
}
|
||||
|
||||
drawChart() {
|
||||
var stateHistory = this.data;
|
||||
var chart;
|
||||
var dataTable;
|
||||
@ -51,12 +42,12 @@ Polymer({
|
||||
var format;
|
||||
var daysDelta;
|
||||
|
||||
if (!this.isAttached) {
|
||||
if (!this._isAttached) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (root.node.lastChild) {
|
||||
root.node.removeChild(root.node.lastChild);
|
||||
while (this.lastChild) {
|
||||
this.removeChild(this.lastChild);
|
||||
}
|
||||
|
||||
if (!stateHistory || stateHistory.length === 0) {
|
||||
@ -79,14 +70,15 @@ Polymer({
|
||||
startTime = new Date(
|
||||
stateHistory.reduce(
|
||||
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()));
|
||||
|
||||
// end time is Math.max(startTime, last_event)
|
||||
endTime = this.endTime ||
|
||||
new Date(stateHistory.reduce(
|
||||
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));
|
||||
|
||||
if (endTime > new Date()) {
|
||||
@ -111,11 +103,11 @@ Polymer({
|
||||
var prevState = 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);
|
||||
if (timeStamp > endTime) {
|
||||
// Drop datapoints that are after the requested endTime. This could happen if
|
||||
@ -145,13 +137,14 @@ Polymer({
|
||||
height: 55 + (numTimelines * 42),
|
||||
|
||||
timeline: {
|
||||
showRowLabels: stateHistory.length > 1,
|
||||
showRowLabels: this.noSingle || stateHistory.length > 1,
|
||||
},
|
||||
|
||||
hAxis: {
|
||||
format: format
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define(StateHistoryChartTimeline.is, StateHistoryChartTimeline);
|
||||
</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/google-apis/google-legacy-loader.html">
|
||||
@ -14,6 +14,14 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.google-visualization-tooltip {
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
state-history-chart-timeline, state-history-chart-line {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
@ -40,15 +48,16 @@
|
||||
|
||||
<state-history-chart-timeline
|
||||
data='[[historyData.timeline]]'
|
||||
end-time='[[_computeEndTime(endTime, upToNow)]]'>
|
||||
end-time='[[_computeEndTime(endTime, upToNow, historyData)]]'
|
||||
no-single='[[noSingle]]'>
|
||||
</state-history-chart-timeline>
|
||||
|
||||
<template is='dom-repeat' items='[[historyData.line]]'>
|
||||
<state-history-chart-line
|
||||
unit='[[item.unit]]'
|
||||
data='[[item.data]]'
|
||||
is-single-device='[[_computeIsSingleLineChart(historyData)]]'
|
||||
end-time='[[_computeEndTime(endTime, upToNow)]]'>
|
||||
is-single-device='[[_computeIsSingleLineChart(historyData, noSingle)]]'
|
||||
end-time='[[_computeEndTime(endTime, upToNow, historyData)]]'>
|
||||
</state-history-chart-line>
|
||||
</template>
|
||||
</template>
|
||||
@ -56,10 +65,10 @@
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
Polymer({
|
||||
is: 'state-history-charts',
|
||||
|
||||
properties: {
|
||||
class StateHistoryCharts extends Polymer.Element {
|
||||
static get is() { return 'state-history-charts'; }
|
||||
static get properties() {
|
||||
return {
|
||||
historyData: {
|
||||
type: Object,
|
||||
value: null,
|
||||
@ -74,10 +83,8 @@ Polymer({
|
||||
type: Object,
|
||||
},
|
||||
|
||||
upToNow: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
upToNow: Boolean,
|
||||
noSingle: Boolean,
|
||||
|
||||
_apiLoaded: {
|
||||
type: Boolean,
|
||||
@ -88,33 +95,37 @@ Polymer({
|
||||
type: Boolean,
|
||||
computed: '_computeIsLoading(isLoadingData, _apiLoaded)',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_computeIsSingleLineChart: function (historyData) {
|
||||
return historyData && historyData.line.length === 1;
|
||||
},
|
||||
_computeIsSingleLineChart(historyData, noSingle) {
|
||||
return !noSingle && historyData && historyData.line.length === 1;
|
||||
}
|
||||
|
||||
_googleApiLoaded: function () {
|
||||
_googleApiLoaded() {
|
||||
window.google.load('visualization', '1', {
|
||||
packages: ['timeline', 'corechart'],
|
||||
callback: function () {
|
||||
this._apiLoaded = true;
|
||||
}.bind(this),
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
_computeIsLoading: function (_isLoadingData, _apiLoaded) {
|
||||
_computeIsLoading(_isLoadingData, _apiLoaded) {
|
||||
return _isLoadingData || !_apiLoaded;
|
||||
},
|
||||
}
|
||||
|
||||
_computeIsEmpty: function (historyData) {
|
||||
_computeIsEmpty(historyData) {
|
||||
return (historyData &&
|
||||
historyData.timeline.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;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define(StateHistoryCharts.is, StateHistoryCharts);
|
||||
</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>
|
||||
(function () {
|
||||
var RECENT_THRESHOLD = 60000; // 1 minute
|
||||
var RECENT_CACHE = {};
|
||||
{
|
||||
const RECENT_THRESHOLD = 60000; // 1 minute
|
||||
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) {
|
||||
var lineChartDevices = {};
|
||||
var timelineDevices = [];
|
||||
var unitStates;
|
||||
const lineChartDevices = {};
|
||||
const timelineDevices = [];
|
||||
|
||||
if (!stateHistory) {
|
||||
return { line: [], timeline: [] };
|
||||
}
|
||||
|
||||
stateHistory.forEach(function (stateInfo) {
|
||||
var stateWithUnit;
|
||||
var unit;
|
||||
|
||||
if (stateInfo.size === 0) {
|
||||
stateHistory.forEach((stateInfo) => {
|
||||
if (stateInfo.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
stateWithUnit = stateInfo.find(
|
||||
function (state) { return 'unit_of_measurement' in state.attributes; });
|
||||
const stateWithUnit = stateInfo.find(
|
||||
state => 'unit_of_measurement' in state.attributes);
|
||||
|
||||
unit = stateWithUnit ?
|
||||
const unit = stateWithUnit ?
|
||||
stateWithUnit.attributes.unit_of_measurement : false;
|
||||
|
||||
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) {
|
||||
lineChartDevices[unit].push(stateInfo);
|
||||
} else {
|
||||
@ -37,41 +45,57 @@
|
||||
}
|
||||
});
|
||||
|
||||
unitStates = Object.keys(lineChartDevices).map(
|
||||
function (unit) {
|
||||
return { unit: unit, data: lineChartDevices[unit] };
|
||||
const unitStates = Object.keys(lineChartDevices).map(
|
||||
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({
|
||||
is: 'ha-state-history-data',
|
||||
|
||||
properties: {
|
||||
class HaStateHistoryData extends Polymer.Element {
|
||||
static get is() { return 'ha-state-history-data'; }
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
observer: 'hassChanged',
|
||||
},
|
||||
|
||||
filterType: {
|
||||
type: String,
|
||||
},
|
||||
filterType: String,
|
||||
|
||||
startTime: {
|
||||
type: Date,
|
||||
value: null,
|
||||
},
|
||||
cacheConfig: Object,
|
||||
|
||||
endTime: {
|
||||
type: Date,
|
||||
value: null,
|
||||
},
|
||||
startTime: Date,
|
||||
endTime: Date,
|
||||
|
||||
entityId: {
|
||||
type: String,
|
||||
value: null,
|
||||
},
|
||||
entityId: String,
|
||||
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
@ -86,86 +110,230 @@
|
||||
readOnly: true,
|
||||
notify: true,
|
||||
},
|
||||
},
|
||||
|
||||
observers: [
|
||||
'filterChanged(filterType, entityId, startTime, endTime)',
|
||||
],
|
||||
|
||||
hassChanged: function (newHass, oldHass) {
|
||||
if (!oldHass) {
|
||||
this.filterChanged(this.filterType, this.entityId, this.startTime, this.endTime);
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
filterChanged: function (filterType, entityId, startTime, endTime) {
|
||||
static get observers() {
|
||||
return [
|
||||
'filterChanged(filterType, entityId, startTime, endTime, cacheConfig)',
|
||||
];
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (this._refreshTimeoutId) {
|
||||
window.clearInterval(this._refreshTimeoutId);
|
||||
this._refreshTimeoutId = null;
|
||||
}
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
var data;
|
||||
this._madeFirstCall = true;
|
||||
let data;
|
||||
|
||||
if (filterType === 'date') {
|
||||
if (startTime === null || endTime === null) return;
|
||||
if (!startTime || !endTime) return;
|
||||
data = this.getDate(startTime, endTime);
|
||||
} else if (filterType === 'recent-entity') {
|
||||
if (entityId === null) return;
|
||||
data = this.getRecent(entityId);
|
||||
if (!entityId) return;
|
||||
if (cacheConfig) {
|
||||
data = this.getRecentWithCacheRefresh(entityId, cacheConfig);
|
||||
} else {
|
||||
data = this.getRecent(entityId, startTime, endTime);
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
this._setIsLoading(true);
|
||||
|
||||
data.then(function (stateHistory) {
|
||||
data.then((stateHistory) => {
|
||||
this._setData(stateHistory);
|
||||
this._setIsLoading(false);
|
||||
}.bind(this));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getRecent: function (entityId) {
|
||||
var cache = RECENT_CACHE[entityId];
|
||||
getEmptyCache() {
|
||||
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) {
|
||||
return cache.data;
|
||||
}
|
||||
|
||||
var url = 'history/period';
|
||||
|
||||
if (entityId) {
|
||||
url += '?filter_entity_id=' + entityId;
|
||||
}
|
||||
|
||||
var prom = this.hass.callApi('GET', url).then(
|
||||
function (stateHistory) {
|
||||
return computeHistory(stateHistory);
|
||||
},
|
||||
function () {
|
||||
const prom = this.fetchRecent(entityId, startTime, endTime).then(
|
||||
stateHistory => computeHistory(stateHistory),
|
||||
() => {
|
||||
RECENT_CACHE[entityId] = false;
|
||||
return null;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
RECENT_CACHE[entityId] = {
|
||||
RECENT_CACHE[cacheKey] = {
|
||||
created: Date.now(),
|
||||
data: prom,
|
||||
};
|
||||
|
||||
return prom;
|
||||
},
|
||||
|
||||
getDate: function (startTime, endTime) {
|
||||
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;
|
||||
}
|
||||
);
|
||||
|
||||
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;
|
||||
},
|
||||
});
|
||||
}());
|
||||
}
|
||||
}
|
||||
customElements.define(HaStateHistoryData.is, HaStateHistoryData);
|
||||
}
|
||||
</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-scrollable/paper-dialog-scrollable.html">
|
||||
@ -9,6 +9,7 @@
|
||||
<link rel="import" href="../components/state-history-charts.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='../util/hass-mixins.html'>
|
||||
|
||||
<dom-module id="more-info-dialog">
|
||||
<template>
|
||||
@ -23,6 +24,14 @@
|
||||
width: auto;
|
||||
}
|
||||
|
||||
paper-dialog[data-domain=history_graph] {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
paper-dialog[data-domain=history_graph] h2 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
state-history-charts {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
@ -48,6 +57,13 @@
|
||||
border-bottom-left-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
}
|
||||
paper-dialog[data-domain=history_graph] {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
dom-if {
|
||||
display: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
@ -63,10 +79,11 @@
|
||||
<div>
|
||||
<ha-state-history-data
|
||||
hass='[[hass]]'
|
||||
filter-type='[[_filterType]]'
|
||||
filter-type='recent-entity'
|
||||
entity-id='[[stateObj.entity_id]]'
|
||||
data='{{stateHistory}}'
|
||||
is-loading='{{stateHistoryLoading}}'
|
||||
cache-config='[[computeCacheConfig(stateObj)]]'
|
||||
></ha-state-history-data>
|
||||
<state-history-charts
|
||||
history-data="[[stateHistory]]"
|
||||
@ -84,13 +101,11 @@
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
Polymer({
|
||||
is: 'more-info-dialog',
|
||||
|
||||
properties: {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
class MoreInfoDialog extends window.hassMixins.EventsMixin(Polymer.Element) {
|
||||
static get is() { return 'more-info-dialog'; }
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
|
||||
stateObj: {
|
||||
type: Object,
|
||||
@ -98,13 +113,9 @@ Polymer({
|
||||
observer: 'stateObjChanged',
|
||||
},
|
||||
|
||||
stateHistory: {
|
||||
type: Object,
|
||||
},
|
||||
stateHistory: Object,
|
||||
|
||||
stateHistoryLoading: {
|
||||
type: Boolean,
|
||||
},
|
||||
stateHistoryLoading: Boolean,
|
||||
|
||||
isLoadingHistoryData: {
|
||||
type: Boolean,
|
||||
@ -132,24 +143,29 @@ Polymer({
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_filterType: {
|
||||
type: String,
|
||||
value: 'recent-entity',
|
||||
},
|
||||
},
|
||||
|
||||
ready: function () {
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.$.scrollable.dialogElement = this.$.dialog;
|
||||
},
|
||||
}
|
||||
|
||||
computeDomain: function (stateObj) {
|
||||
computeDomain(stateObj) {
|
||||
return stateObj ? window.hassUtil.computeDomain(stateObj) : '';
|
||||
},
|
||||
}
|
||||
|
||||
computeStateObj: function (hass) {
|
||||
computeStateObj(hass) {
|
||||
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
|
||||
@ -157,40 +173,41 @@ Polymer({
|
||||
* before the dialog is attached to the screen and is unable to determine
|
||||
* graph size resulting in scroll bars.
|
||||
*/
|
||||
computeIsLoadingHistoryData: function (delayedDialogOpen, stateHistoryLoading) {
|
||||
computeIsLoadingHistoryData(delayedDialogOpen, stateHistoryLoading) {
|
||||
return !delayedDialogOpen || stateHistoryLoading;
|
||||
},
|
||||
}
|
||||
|
||||
computeHasHistoryComponent: function (hass) {
|
||||
computeHasHistoryComponent(hass) {
|
||||
return window.hassUtil.isComponentLoaded(hass, 'history');
|
||||
},
|
||||
}
|
||||
|
||||
computeShowHistoryComponent: function (hasHistoryComponent, stateObj) {
|
||||
computeShowHistoryComponent(hasHistoryComponent, stateObj) {
|
||||
return this.hasHistoryComponent && stateObj &&
|
||||
window.hassUtil.DOMAINS_WITH_NO_HISTORY.indexOf(
|
||||
window.hassUtil.computeDomain(stateObj)) === -1;
|
||||
},
|
||||
}
|
||||
|
||||
stateObjChanged: function (newVal) {
|
||||
stateObjChanged(newVal) {
|
||||
if (!newVal) {
|
||||
this.dialogOpen = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.async(function () {
|
||||
window.setTimeout(() => {
|
||||
// allow dialog to render content before showing it so it is
|
||||
// positioned correctly.
|
||||
this.dialogOpen = true;
|
||||
}.bind(this), 10);
|
||||
},
|
||||
}, 10);
|
||||
}
|
||||
|
||||
dialogOpenChanged: function (newVal) {
|
||||
dialogOpenChanged(newVal) {
|
||||
if (newVal) {
|
||||
this.async(function () { this.delayedDialogOpen = true; }.bind(this), 100);
|
||||
window.setTimeout(() => { this.delayedDialogOpen = true; }, 100);
|
||||
} else if (!newVal && this.stateObj) {
|
||||
this.fire('hass-more-info', { entityId: null });
|
||||
this.delayedDialogOpen = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define(MoreInfoDialog.is, MoreInfoDialog);
|
||||
</script>
|
||||
|
@ -8,6 +8,7 @@
|
||||
<link rel='import' href='more-info-cover.html'>
|
||||
<link rel='import' href='more-info-default.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-light.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 = [
|
||||
'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',
|
||||
];
|
||||
|
||||
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 = [
|
||||
'input_select', 'scene', 'script', 'input_number', 'input_text'
|
||||
@ -274,6 +274,9 @@ window.hassUtil.domainIcon = function (domain, state) {
|
||||
case 'fan':
|
||||
return 'mdi:fan';
|
||||
|
||||
case 'history_graph':
|
||||
return 'mdi:chart-line';
|
||||
|
||||
case 'group':
|
||||
return 'mdi:google-circles-communities';
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user