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:
Andrey 2017-10-05 19:15:17 +03:00 committed by Paulus Schoutsen
parent 890dbc6ad7
commit b0791abb9a
11 changed files with 654 additions and 321 deletions

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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