Commit e4963de151f660b07f3fcc5296e5191e7926a67e

Authored by Igor Kulikov
1 parent 93250456

UI: Add timeseries aggregation support

@@ -53,8 +53,9 @@ import static com.datastax.driver.core.querybuilder.QueryBuilder.select; @@ -53,8 +53,9 @@ import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
53 @Slf4j 53 @Slf4j
54 public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao { 54 public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao {
55 55
56 - @Value("${cassandra.query.min_aggregation_step_ms}")  
57 - private int minAggregationStepMs; 56 + //@Value("${cassandra.query.min_aggregation_step_ms}")
  57 + //TODO:
  58 + private int minAggregationStepMs = 1000;
58 59
59 @Value("${cassandra.query.ts_key_value_partitioning}") 60 @Value("${cassandra.query.ts_key_value_partitioning}")
60 private String partitioning; 61 private String partitioning;
@@ -234,7 +234,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler { @@ -234,7 +234,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
234 return new PluginCallback<List<TsKvEntry>>() { 234 return new PluginCallback<List<TsKvEntry>>() {
235 @Override 235 @Override
236 public void onSuccess(PluginContext ctx, List<TsKvEntry> data) { 236 public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
237 - sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data)); 237 + sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), startTs, data));
238 238
239 Map<String, Long> subState = new HashMap<>(keys.size()); 239 Map<String, Long> subState = new HashMap<>(keys.size());
240 keys.forEach(key -> subState.put(key, startTs)); 240 keys.forEach(key -> subState.put(key, startTs));
@@ -26,10 +26,16 @@ public class SubscriptionUpdate { @@ -26,10 +26,16 @@ public class SubscriptionUpdate {
26 private int errorCode; 26 private int errorCode;
27 private String errorMsg; 27 private String errorMsg;
28 private Map<String, List<Object>> data; 28 private Map<String, List<Object>> data;
  29 + private long serverStartTs;
29 30
30 public SubscriptionUpdate(int subscriptionId, List<TsKvEntry> data) { 31 public SubscriptionUpdate(int subscriptionId, List<TsKvEntry> data) {
  32 + this(subscriptionId, 0L, data);
  33 + }
  34 +
  35 + public SubscriptionUpdate(int subscriptionId, long serverStartTs, List<TsKvEntry> data) {
31 super(); 36 super();
32 this.subscriptionId = subscriptionId; 37 this.subscriptionId = subscriptionId;
  38 + this.serverStartTs = serverStartTs;
33 this.data = new TreeMap<>(); 39 this.data = new TreeMap<>();
34 for (TsKvEntry tsEntry : data) { 40 for (TsKvEntry tsEntry : data) {
35 List<Object> values = this.data.get(tsEntry.getKey()); 41 List<Object> values = this.data.get(tsEntry.getKey());
@@ -89,9 +95,13 @@ public class SubscriptionUpdate { @@ -89,9 +95,13 @@ public class SubscriptionUpdate {
89 return errorMsg; 95 return errorMsg;
90 } 96 }
91 97
  98 + public long getServerStartTs() {
  99 + return serverStartTs;
  100 + }
  101 +
92 @Override 102 @Override
93 public String toString() { 103 public String toString() {
94 return "SubscriptionUpdate [subscriptionId=" + subscriptionId + ", errorCode=" + errorCode + ", errorMsg=" + errorMsg + ", data=" 104 return "SubscriptionUpdate [subscriptionId=" + subscriptionId + ", errorCode=" + errorCode + ", errorMsg=" + errorMsg + ", data="
95 - + data + "]"; 105 + + data + ", serverStartTs=" + serverStartTs+ "]";
96 } 106 }
97 } 107 }
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +export default class DataAggregator {
  18 +
  19 + constructor(onDataCb, limit, aggregationType, timeWindow, types, $timeout, $filter) {
  20 + this.onDataCb = onDataCb;
  21 + this.aggregationType = aggregationType;
  22 + this.types = types;
  23 + this.$timeout = $timeout;
  24 + this.$filter = $filter;
  25 + this.dataReceived = false;
  26 + this.noAggregation = aggregationType === types.aggregation.none.value;
  27 + var interval = Math.floor(timeWindow / limit);
  28 + if (!this.noAggregation) {
  29 + this.interval = Math.max(interval, 1000);
  30 + this.limit = Math.ceil(interval/this.interval * limit);
  31 + this.timeWindow = this.interval * this.limit;
  32 + } else {
  33 + this.limit = limit;
  34 + this.timeWindow = interval * this.limit;
  35 + this.interval = 1000;
  36 + }
  37 + this.aggregationTimeout = this.interval;
  38 + switch (aggregationType) {
  39 + case types.aggregation.min.value:
  40 + this.aggFunction = min;
  41 + break;
  42 + case types.aggregation.max.value:
  43 + this.aggFunction = max
  44 + break;
  45 + case types.aggregation.avg.value:
  46 + this.aggFunction = avg;
  47 + break;
  48 + case types.aggregation.sum.value:
  49 + this.aggFunction = sum;
  50 + break;
  51 + case types.aggregation.count.value:
  52 + this.aggFunction = count;
  53 + break;
  54 + case types.aggregation.none.value:
  55 + this.aggFunction = none;
  56 + break;
  57 + default:
  58 + this.aggFunction = avg;
  59 + }
  60 + }
  61 +
  62 + onData(data) {
  63 + if (!this.dataReceived) {
  64 + this.elapsed = 0;
  65 + this.dataReceived = true;
  66 + this.startTs = data.serverStartTs;
  67 + this.endTs = this.startTs + this.timeWindow;
  68 + this.aggregationMap = processAggregatedData(data.data, this.aggregationType === this.types.aggregation.count.value, this.noAggregation);
  69 + this.onInterval(currentTime());
  70 + } else {
  71 + updateAggregatedData(this.aggregationMap, this.aggregationType === this.types.aggregation.count.value,
  72 + this.noAggregation, this.aggFunction, data.data, this.interval, this.startTs);
  73 + }
  74 + }
  75 +
  76 + onInterval(startedTime) {
  77 + var now = currentTime();
  78 + this.elapsed += now - startedTime;
  79 + if (this.intervalTimeoutHandle) {
  80 + this.$timeout.cancel(this.intervalTimeoutHandle);
  81 + this.intervalTimeoutHandle = null;
  82 + }
  83 + var delta = Math.floor(this.elapsed / this.interval);
  84 + if (delta || !this.data) {
  85 + this.startTs += delta * this.interval;
  86 + this.endTs += delta * this.interval;
  87 + this.data = toData(this.aggregationMap, this.startTs, this.endTs, this.$filter, this.limit);
  88 + this.elapsed = this.elapsed - delta * this.interval;
  89 + }
  90 + if (this.onDataCb) {
  91 + this.onDataCb(this.data, this.startTs, this.endTs);
  92 + }
  93 +
  94 + var self = this;
  95 + this.intervalTimeoutHandle = this.$timeout(function() {
  96 + self.onInterval(now);
  97 + }, this.aggregationTimeout, false);
  98 + }
  99 +
  100 + reset() {
  101 + this.destroy();
  102 + this.dataReceived = false;
  103 + }
  104 +
  105 + destroy() {
  106 + if (this.intervalTimeoutHandle) {
  107 + this.$timeout.cancel(this.intervalTimeoutHandle);
  108 + this.intervalTimeoutHandle = null;
  109 + }
  110 + this.aggregationMap = null;
  111 + }
  112 +
  113 +}
  114 +
  115 +/* eslint-disable */
  116 +function currentTime() {
  117 + return window.performance && window.performance.now ?
  118 + window.performance.now() : Date.now();
  119 +}
  120 +/* eslint-enable */
  121 +
  122 +function processAggregatedData(data, isCount, noAggregation) {
  123 + var aggregationMap = {};
  124 + for (var key in data) {
  125 + var aggKeyData = aggregationMap[key];
  126 + if (!aggKeyData) {
  127 + aggKeyData = {};
  128 + aggregationMap[key] = aggKeyData;
  129 + }
  130 + var keyData = data[key];
  131 + for (var i in keyData) {
  132 + var kvPair = keyData[i];
  133 + var timestamp = kvPair[0];
  134 + var value = convertValue(kvPair[1], noAggregation);
  135 + var aggKey = timestamp;
  136 + var aggData = {
  137 + count: isCount ? value : 1,
  138 + sum: value,
  139 + aggValue: value
  140 + }
  141 + aggKeyData[aggKey] = aggData;
  142 + }
  143 + }
  144 + return aggregationMap;
  145 +}
  146 +
  147 +function updateAggregatedData(aggregationMap, isCount, noAggregation, aggFunction, data, interval, startTs) {
  148 + for (var key in data) {
  149 + var aggKeyData = aggregationMap[key];
  150 + if (!aggKeyData) {
  151 + aggKeyData = {};
  152 + aggregationMap[key] = aggKeyData;
  153 + }
  154 + var keyData = data[key];
  155 + for (var i in keyData) {
  156 + var kvPair = keyData[i];
  157 + var timestamp = kvPair[0];
  158 + var value = convertValue(kvPair[1], noAggregation);
  159 + var aggTimestamp = noAggregation ? timestamp : (startTs + Math.floor((timestamp - startTs) / interval) * interval + interval/2);
  160 + var aggData = aggKeyData[aggTimestamp];
  161 + if (!aggData) {
  162 + aggData = {
  163 + count: 1,
  164 + sum: value,
  165 + aggValue: isCount ? 1 : value
  166 + }
  167 + aggKeyData[aggTimestamp] = aggData;
  168 + } else {
  169 + aggFunction(aggData, value);
  170 + }
  171 + }
  172 + }
  173 +}
  174 +
  175 +function toData(aggregationMap, startTs, endTs, $filter, limit) {
  176 + var data = {};
  177 + for (var key in aggregationMap) {
  178 + if (!data[key]) {
  179 + data[key] = [];
  180 + }
  181 + var aggKeyData = aggregationMap[key];
  182 + var keyData = data[key];
  183 + for (var aggTimestamp in aggKeyData) {
  184 + if (aggTimestamp <= startTs) {
  185 + delete aggKeyData[aggTimestamp];
  186 + } else if (aggTimestamp <= endTs) {
  187 + var aggData = aggKeyData[aggTimestamp];
  188 + var kvPair = [aggTimestamp, aggData.aggValue];
  189 + keyData.push(kvPair);
  190 + }
  191 + }
  192 + keyData = $filter('orderBy')(keyData, '+this[0]');
  193 + if (keyData.length > limit) {
  194 + keyData = keyData.slice(keyData.length - limit);
  195 + }
  196 + data[key] = keyData;
  197 + }
  198 + return data;
  199 +}
  200 +
  201 +function convertValue(value, noAggregation) {
  202 + if (!noAggregation || value && isNumeric(value)) {
  203 + return Number(value);
  204 + } else {
  205 + return value;
  206 + }
  207 +}
  208 +
  209 +function isNumeric(value) {
  210 + return (value - parseFloat( value ) + 1) >= 0;
  211 +}
  212 +
  213 +function avg(aggData, value) {
  214 + aggData.count++;
  215 + aggData.sum += value;
  216 + aggData.aggValue = aggData.sum / aggData.count;
  217 +}
  218 +
  219 +function min(aggData, value) {
  220 + aggData.aggValue = Math.min(aggData.aggValue, value);
  221 +}
  222 +
  223 +function max(aggData, value) {
  224 + aggData.aggValue = Math.max(aggData.aggValue, value);
  225 +}
  226 +
  227 +function sum(aggData, value) {
  228 + aggData.aggValue = aggData.aggValue + value;
  229 +}
  230 +
  231 +function count(aggData) {
  232 + aggData.count++;
  233 + aggData.aggValue = aggData.count;
  234 +}
  235 +
  236 +function none(aggData, value) {
  237 + aggData.aggValue = value;
  238 +}
@@ -17,13 +17,14 @@ import thingsboardApiDevice from './device.service'; @@ -17,13 +17,14 @@ import thingsboardApiDevice from './device.service';
17 import thingsboardApiTelemetryWebsocket from './telemetry-websocket.service'; 17 import thingsboardApiTelemetryWebsocket from './telemetry-websocket.service';
18 import thingsboardTypes from '../common/types.constant'; 18 import thingsboardTypes from '../common/types.constant';
19 import thingsboardUtils from '../common/utils.service'; 19 import thingsboardUtils from '../common/utils.service';
  20 +import DataAggregator from './data-aggregator';
20 21
21 export default angular.module('thingsboard.api.datasource', [thingsboardApiDevice, thingsboardApiTelemetryWebsocket, thingsboardTypes, thingsboardUtils]) 22 export default angular.module('thingsboard.api.datasource', [thingsboardApiDevice, thingsboardApiTelemetryWebsocket, thingsboardTypes, thingsboardUtils])
22 .factory('datasourceService', DatasourceService) 23 .factory('datasourceService', DatasourceService)
23 .name; 24 .name;
24 25
25 /*@ngInject*/ 26 /*@ngInject*/
26 -function DatasourceService($timeout, $log, telemetryWebsocketService, types, utils) { 27 +function DatasourceService($timeout, $filter, $log, telemetryWebsocketService, types, utils) {
27 28
28 var subscriptions = {}; 29 var subscriptions = {};
29 30
@@ -73,7 +74,7 @@ function DatasourceService($timeout, $log, telemetryWebsocketService, types, uti @@ -73,7 +74,7 @@ function DatasourceService($timeout, $log, telemetryWebsocketService, types, uti
73 subscription = subscriptions[listener.datasourceSubscriptionKey]; 74 subscription = subscriptions[listener.datasourceSubscriptionKey];
74 subscription.syncListener(listener); 75 subscription.syncListener(listener);
75 } else { 76 } else {
76 - subscription = new DatasourceSubscription(datasourceSubscription, telemetryWebsocketService, $timeout, $log, types, utils); 77 + subscription = new DatasourceSubscription(datasourceSubscription, telemetryWebsocketService, $timeout, $filter, $log, types, utils);
77 subscriptions[listener.datasourceSubscriptionKey] = subscription; 78 subscriptions[listener.datasourceSubscriptionKey] = subscription;
78 subscription.start(); 79 subscription.start();
79 } 80 }
@@ -96,7 +97,7 @@ function DatasourceService($timeout, $log, telemetryWebsocketService, types, uti @@ -96,7 +97,7 @@ function DatasourceService($timeout, $log, telemetryWebsocketService, types, uti
96 97
97 } 98 }
98 99
99 -function DatasourceSubscription(datasourceSubscription, telemetryWebsocketService, $timeout, $log, types, utils) { 100 +function DatasourceSubscription(datasourceSubscription, telemetryWebsocketService, $timeout, $filter, $log, types, utils) {
100 101
101 var listeners = []; 102 var listeners = [];
102 var datasourceType = datasourceSubscription.datasourceType; 103 var datasourceType = datasourceSubscription.datasourceType;
@@ -134,7 +135,9 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -134,7 +135,9 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
134 if (!dataKey.func) { 135 if (!dataKey.func) {
135 dataKey.func = new Function("time", "prevValue", dataKey.funcBody); 136 dataKey.func = new Function("time", "prevValue", dataKey.funcBody);
136 } 137 }
137 - datasourceData[key] = []; 138 + datasourceData[key] = {
  139 + data: []
  140 + };
138 dataKeys[key] = dataKey; 141 dataKeys[key] = dataKey;
139 } else if (datasourceType === types.datasourceType.device) { 142 } else if (datasourceType === types.datasourceType.device) {
140 key = dataKey.name + '_' + dataKey.type; 143 key = dataKey.name + '_' + dataKey.type;
@@ -147,7 +150,9 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -147,7 +150,9 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
147 dataKeys[key] = dataKeysList; 150 dataKeys[key] = dataKeysList;
148 } 151 }
149 var index = dataKeysList.push(dataKey) - 1; 152 var index = dataKeysList.push(dataKey) - 1;
150 - datasourceData[key + '_' + index] = []; 153 + datasourceData[key + '_' + index] = {
  154 + data: []
  155 + };
151 } 156 }
152 dataKey.key = key; 157 dataKey.key = key;
153 } 158 }
@@ -248,14 +253,18 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -248,14 +253,18 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
248 deviceId: datasourceSubscription.deviceId, 253 deviceId: datasourceSubscription.deviceId,
249 keys: tsKeys, 254 keys: tsKeys,
250 startTs: datasourceSubscription.subscriptionTimewindow.fixedWindow.startTimeMs, 255 startTs: datasourceSubscription.subscriptionTimewindow.fixedWindow.startTimeMs,
251 - endTs: datasourceSubscription.subscriptionTimewindow.fixedWindow.endTimeMs 256 + endTs: datasourceSubscription.subscriptionTimewindow.fixedWindow.endTimeMs,
  257 + limit: datasourceSubscription.subscriptionTimewindow.aggregation.limit,
  258 + agg: datasourceSubscription.subscriptionTimewindow.aggregation.type
252 }; 259 };
253 260
254 subscriber = { 261 subscriber = {
255 historyCommand: historyCommand, 262 historyCommand: historyCommand,
256 type: types.dataKeyType.timeseries, 263 type: types.dataKeyType.timeseries,
257 onData: function (data) { 264 onData: function (data) {
258 - onData(data, types.dataKeyType.timeseries); 265 + if (data.data) {
  266 + onData(data.data, types.dataKeyType.timeseries);
  267 + }
259 }, 268 },
260 onReconnected: function() { 269 onReconnected: function() {
261 onReconnected(); 270 onReconnected();
@@ -272,20 +281,46 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -272,20 +281,46 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
272 keys: tsKeys 281 keys: tsKeys
273 }; 282 };
274 283
275 - if (datasourceSubscription.type === types.widgetType.timeseries.value) {  
276 - subscriptionCommand.timeWindow = datasourceSubscription.subscriptionTimewindow.realtimeWindowMs;  
277 - }  
278 -  
279 subscriber = { 284 subscriber = {
280 subscriptionCommand: subscriptionCommand, 285 subscriptionCommand: subscriptionCommand,
281 - type: types.dataKeyType.timeseries,  
282 - onData: function (data) {  
283 - onData(data, types.dataKeyType.timeseries);  
284 - },  
285 - onReconnected: function() { 286 + type: types.dataKeyType.timeseries
  287 + };
  288 +
  289 + if (datasourceSubscription.type === types.widgetType.timeseries.value) {
  290 + subscriptionCommand.timeWindow = datasourceSubscription.subscriptionTimewindow.realtimeWindowMs;
  291 + subscriptionCommand.limit = datasourceSubscription.subscriptionTimewindow.aggregation.limit;
  292 + subscriptionCommand.agg = datasourceSubscription.subscriptionTimewindow.aggregation.type;
  293 + var dataAggregator = new DataAggregator(
  294 + function(data, startTs, endTs) {
  295 + onData(data, types.dataKeyType.timeseries, startTs, endTs);
  296 + },
  297 + subscriptionCommand.limit,
  298 + subscriptionCommand.agg,
  299 + subscriptionCommand.timeWindow,
  300 + types,
  301 + $timeout,
  302 + $filter
  303 + );
  304 + subscriber.onData = function(data) {
  305 + dataAggregator.onData(data);
  306 + }
  307 + subscriber.onReconnected = function() {
  308 + dataAggregator.reset();
286 onReconnected(); 309 onReconnected();
287 } 310 }
288 - }; 311 + subscriber.onDestroy = function() {
  312 + dataAggregator.destroy();
  313 + }
  314 + } else {
  315 + subscriber.onReconnected = function() {
  316 + onReconnected();
  317 + }
  318 + subscriber.onData = function(data) {
  319 + if (data.data) {
  320 + onData(data.data, types.dataKeyType.timeseries);
  321 + }
  322 + }
  323 + }
289 324
290 telemetryWebsocketService.subscribe(subscriber); 325 telemetryWebsocketService.subscribe(subscriber);
291 subscribers[subscriber.subscriptionCommand.cmdId] = subscriber; 326 subscribers[subscriber.subscriptionCommand.cmdId] = subscriber;
@@ -304,7 +339,9 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -304,7 +339,9 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
304 subscriptionCommand: subscriptionCommand, 339 subscriptionCommand: subscriptionCommand,
305 type: types.dataKeyType.attribute, 340 type: types.dataKeyType.attribute,
306 onData: function (data) { 341 onData: function (data) {
307 - onData(data, types.dataKeyType.attribute); 342 + if (data.data) {
  343 + onData(data.data, types.dataKeyType.attribute);
  344 + }
308 }, 345 },
309 onReconnected: function() { 346 onReconnected: function() {
310 onReconnected(); 347 onReconnected();
@@ -332,11 +369,14 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -332,11 +369,14 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
332 } 369 }
333 if (datasourceType === types.datasourceType.device) { 370 if (datasourceType === types.datasourceType.device) {
334 for (var cmdId in subscribers) { 371 for (var cmdId in subscribers) {
335 - telemetryWebsocketService.unsubscribe(subscribers[cmdId]); 372 + var subscriber = subscribers[cmdId];
  373 + telemetryWebsocketService.unsubscribe(subscriber);
  374 + if (subscriber.onDestroy) {
  375 + subscriber.onDestroy();
  376 + }
336 } 377 }
337 subscribers = {}; 378 subscribers = {};
338 } 379 }
339 - //$log.debug("unsibscribed!");  
340 } 380 }
341 381
342 function boundToInterval(data, timewindowMs) { 382 function boundToInterval(data, timewindowMs) {
@@ -360,7 +400,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -360,7 +400,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
360 function generateSeries(dataKey, startTime, endTime) { 400 function generateSeries(dataKey, startTime, endTime) {
361 var data = []; 401 var data = [];
362 var prevSeries; 402 var prevSeries;
363 - var datasourceKeyData = datasourceData[dataKey.key]; 403 + var datasourceKeyData = datasourceData[dataKey.key].data;
364 if (datasourceKeyData.length > 0) { 404 if (datasourceKeyData.length > 0) {
365 prevSeries = datasourceKeyData[datasourceKeyData.length - 1]; 405 prevSeries = datasourceKeyData[datasourceKeyData.length - 1];
366 } else { 406 } else {
@@ -378,10 +418,10 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -378,10 +418,10 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
378 dataKey.lastUpdateTime = data[data.length - 1][0]; 418 dataKey.lastUpdateTime = data[data.length - 1][0];
379 } 419 }
380 if (realtime) { 420 if (realtime) {
381 - datasourceData[dataKey.key] = boundToInterval(datasourceKeyData.concat(data), 421 + datasourceData[dataKey.key].data = boundToInterval(datasourceKeyData.concat(data),
382 datasourceSubscription.subscriptionTimewindow.realtimeWindowMs); 422 datasourceSubscription.subscriptionTimewindow.realtimeWindowMs);
383 } else { 423 } else {
384 - datasourceData[dataKey.key] = data; 424 + datasourceData[dataKey.key].data = data;
385 } 425 }
386 for (var i in listeners) { 426 for (var i in listeners) {
387 var listener = listeners[i]; 427 var listener = listeners[i];
@@ -393,7 +433,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -393,7 +433,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
393 433
394 function generateLatest(dataKey) { 434 function generateLatest(dataKey) {
395 var prevSeries; 435 var prevSeries;
396 - var datasourceKeyData = datasourceData[dataKey.key]; 436 + var datasourceKeyData = datasourceData[dataKey.key].data;
397 if (datasourceKeyData.length > 0) { 437 if (datasourceKeyData.length > 0) {
398 prevSeries = datasourceKeyData[datasourceKeyData.length - 1]; 438 prevSeries = datasourceKeyData[datasourceKeyData.length - 1];
399 } else { 439 } else {
@@ -404,7 +444,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -404,7 +444,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
404 series.push(time); 444 series.push(time);
405 var value = dataKey.func(time, prevSeries[1]); 445 var value = dataKey.func(time, prevSeries[1]);
406 series.push(value); 446 series.push(value);
407 - datasourceData[dataKey.key] = [series]; 447 + datasourceData[dataKey.key].data = [series];
408 for (var i in listeners) { 448 for (var i in listeners) {
409 var listener = listeners[i]; 449 var listener = listeners[i];
410 listener.dataUpdated(datasourceData[dataKey.key], 450 listener.dataUpdated(datasourceData[dataKey.key],
@@ -453,7 +493,9 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -453,7 +493,9 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
453 for (var i = 0; i < dataKeysList.length; i++) { 493 for (var i = 0; i < dataKeysList.length; i++) {
454 var dataKey = dataKeysList[i]; 494 var dataKey = dataKeysList[i];
455 var datasourceKey = key + '_' + i; 495 var datasourceKey = key + '_' + i;
456 - datasourceData[datasourceKey] = []; 496 + datasourceData[datasourceKey] = {
  497 + data: []
  498 + };
457 for (var l in listeners) { 499 for (var l in listeners) {
458 var listener = listeners[l]; 500 var listener = listeners[l];
459 listener.dataUpdated(datasourceData[datasourceKey], 501 listener.dataUpdated(datasourceData[datasourceKey],
@@ -477,18 +519,23 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -477,18 +519,23 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
477 } 519 }
478 } 520 }
479 521
480 - function onData(sourceData, type) { 522 + function onData(sourceData, type, startTs, endTs) {
481 for (var keyName in sourceData) { 523 for (var keyName in sourceData) {
482 var keyData = sourceData[keyName]; 524 var keyData = sourceData[keyName];
483 var key = keyName + '_' + type; 525 var key = keyName + '_' + type;
484 var dataKeyList = dataKeys[key]; 526 var dataKeyList = dataKeys[key];
485 for (var keyIndex = 0; keyIndex < dataKeyList.length; keyIndex++) { 527 for (var keyIndex = 0; keyIndex < dataKeyList.length; keyIndex++) {
486 var datasourceKey = key + "_" + keyIndex; 528 var datasourceKey = key + "_" + keyIndex;
487 - if (datasourceData[datasourceKey]) { 529 + if (datasourceData[datasourceKey].data) {
488 var dataKey = dataKeyList[keyIndex]; 530 var dataKey = dataKeyList[keyIndex];
489 var data = []; 531 var data = [];
490 var prevSeries; 532 var prevSeries;
491 - var datasourceKeyData = datasourceData[datasourceKey]; 533 + var datasourceKeyData;
  534 + if (realtime) {
  535 + datasourceKeyData = [];
  536 + } else {
  537 + datasourceKeyData = datasourceData[datasourceKey].data;
  538 + }
492 if (datasourceKeyData.length > 0) { 539 if (datasourceKeyData.length > 0) {
493 prevSeries = datasourceKeyData[datasourceKeyData.length - 1]; 540 prevSeries = datasourceKeyData[datasourceKeyData.length - 1];
494 } else { 541 } else {
@@ -519,12 +566,10 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -519,12 +566,10 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
519 data.push(series); 566 data.push(series);
520 } 567 }
521 } 568 }
522 - if (data.length > 0) {  
523 - if (realtime) {  
524 - datasourceData[datasourceKey] = boundToInterval(datasourceKeyData.concat(data), datasourceSubscription.subscriptionTimewindow.realtimeWindowMs);  
525 - } else {  
526 - datasourceData[datasourceKey] = data;  
527 - } 569 + if (data.length > 0 || (startTs && endTs)) {
  570 + datasourceData[datasourceKey].data = data;
  571 + datasourceData[datasourceKey].startTs = startTs;
  572 + datasourceData[datasourceKey].endTs = endTs;
528 for (var i2 in listeners) { 573 for (var i2 in listeners) {
529 var listener = listeners[i2]; 574 var listener = listeners[i2];
530 listener.dataUpdated(datasourceData[datasourceKey], 575 listener.dataUpdated(datasourceData[datasourceKey],
@@ -537,3 +582,4 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -537,3 +582,4 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
537 } 582 }
538 } 583 }
539 } 584 }
  585 +
@@ -304,7 +304,9 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) { @@ -304,7 +304,9 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
304 subscriptionCommand: subscriptionCommand, 304 subscriptionCommand: subscriptionCommand,
305 type: type, 305 type: type,
306 onData: function (data) { 306 onData: function (data) {
307 - onSubscriptionData(data, subscriptionId); 307 + if (data.data) {
  308 + onSubscriptionData(data.data, subscriptionId);
  309 + }
308 } 310 }
309 }; 311 };
310 deviceAttributesSubscription = { 312 deviceAttributesSubscription = {
@@ -131,8 +131,8 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty @@ -131,8 +131,8 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty
131 var data = angular.fromJson(message.data); 131 var data = angular.fromJson(message.data);
132 if (data.subscriptionId) { 132 if (data.subscriptionId) {
133 var subscriber = subscribers[data.subscriptionId]; 133 var subscriber = subscribers[data.subscriptionId];
134 - if (subscriber && data.data) {  
135 - subscriber.onData(data.data); 134 + if (subscriber && data) {
  135 + subscriber.onData(data);
136 } 136 }
137 } 137 }
138 } 138 }
@@ -33,6 +33,32 @@ export default angular.module('thingsboard.types', []) @@ -33,6 +33,32 @@ export default angular.module('thingsboard.types', [])
33 id: { 33 id: {
34 nullUid: "13814000-1dd2-11b2-8080-808080808080", 34 nullUid: "13814000-1dd2-11b2-8080-808080808080",
35 }, 35 },
  36 + aggregation: {
  37 + min: {
  38 + value: "MIN",
  39 + name: "aggregation.min"
  40 + },
  41 + max: {
  42 + value: "MAX",
  43 + name: "aggregation.max"
  44 + },
  45 + avg: {
  46 + value: "AVG",
  47 + name: "aggregation.avg"
  48 + },
  49 + sum: {
  50 + value: "SUM",
  51 + name: "aggregation.sum"
  52 + },
  53 + count: {
  54 + value: "COUNT",
  55 + name: "aggregation.count"
  56 + },
  57 + none: {
  58 + value: "NONE",
  59 + name: "aggregation.none"
  60 + }
  61 + },
36 datasourceType: { 62 datasourceType: {
37 function: "function", 63 function: "function",
38 device: "device" 64 device: "device"
@@ -47,7 +47,7 @@ @@ -47,7 +47,7 @@
47 padding: vm.widgetPadding(widget)}"> 47 padding: vm.widgetPadding(widget)}">
48 <div class="tb-widget-title" layout="column" ng-show="vm.showWidgetTitle(widget) || vm.hasTimewindow(widget)"> 48 <div class="tb-widget-title" layout="column" ng-show="vm.showWidgetTitle(widget) || vm.hasTimewindow(widget)">
49 <span ng-show="vm.showWidgetTitle(widget)" ng-style="vm.widgetTitleStyle(widget)" class="md-subhead">{{widget.config.title}}</span> 49 <span ng-show="vm.showWidgetTitle(widget)" ng-style="vm.widgetTitleStyle(widget)" class="md-subhead">{{widget.config.title}}</span>
50 - <tb-timewindow button-color="vm.widgetColor(widget)" ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow> 50 + <tb-timewindow button-color="vm.widgetColor(widget)" aggregation ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow>
51 </div> 51 </div>
52 <div class="tb-widget-actions" layout="row" layout-align="start center"> 52 <div class="tb-widget-actions" layout="row" layout-align="start center">
53 <md-button id="expand-button" 53 <md-button id="expand-button"
@@ -14,14 +14,16 @@ @@ -14,14 +14,16 @@
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
16 /*@ngInject*/ 16 /*@ngInject*/
17 -export default function TimewindowPanelController(mdPanelRef, $scope, timewindow, historyOnly, onTimewindowUpdate) { 17 +export default function TimewindowPanelController(mdPanelRef, $scope, types, timewindow, historyOnly, aggregation, onTimewindowUpdate) {
18 18
19 var vm = this; 19 var vm = this;
20 20
21 vm._mdPanelRef = mdPanelRef; 21 vm._mdPanelRef = mdPanelRef;
22 vm.timewindow = timewindow; 22 vm.timewindow = timewindow;
23 vm.historyOnly = historyOnly; 23 vm.historyOnly = historyOnly;
  24 + vm.aggregation = aggregation;
24 vm.onTimewindowUpdate = onTimewindowUpdate; 25 vm.onTimewindowUpdate = onTimewindowUpdate;
  26 + vm.aggregationTypes = types.aggregation;
25 27
26 if (vm.historyOnly) { 28 if (vm.historyOnly) {
27 vm.timewindow.selectedTab = 1; 29 vm.timewindow.selectedTab = 1;
@@ -17,7 +17,7 @@ @@ -17,7 +17,7 @@
17 --> 17 -->
18 <form name="theForm" ng-submit="vm.update()"> 18 <form name="theForm" ng-submit="vm.update()">
19 <fieldset ng-disabled="loading"> 19 <fieldset ng-disabled="loading">
20 - <section layout="column"> 20 + <md-content layout="column">
21 <md-tabs ng-class="{'tb-headless': vm.historyOnly}" flex md-dynamic-height md-selected="vm.timewindow.selectedTab" md-border-bottom> 21 <md-tabs ng-class="{'tb-headless': vm.historyOnly}" flex md-dynamic-height md-selected="vm.timewindow.selectedTab" md-border-bottom>
22 <md-tab label="{{ 'timewindow.realtime' | translate }}"> 22 <md-tab label="{{ 'timewindow.realtime' | translate }}">
23 <md-content class="md-padding" layout="column"> 23 <md-content class="md-padding" layout="column">
@@ -52,6 +52,24 @@ @@ -52,6 +52,24 @@
52 </md-content> 52 </md-content>
53 </md-tab> 53 </md-tab>
54 </md-tabs> 54 </md-tabs>
  55 + <md-content ng-if="vm.aggregation" class="md-padding" layout="column">
  56 + <md-input-container>
  57 + <label translate>aggregation.function</label>
  58 + <md-select ng-model="vm.timewindow.aggregation.type" style="min-width: 150px;">
  59 + <md-option ng-repeat="type in vm.aggregationTypes" ng-value="type.value">
  60 + {{type.name | translate}}
  61 + </md-option>
  62 + </md-select>
  63 + </md-input-container>
  64 + <md-slider-container>
  65 + <span translate>aggregation.limit</span>
  66 + <md-slider flex min="10" max="500" ng-model="vm.timewindow.aggregation.limit" aria-label="limit" id="limit-slider">
  67 + </md-slider>
  68 + <md-input-container>
  69 + <input flex type="number" ng-model="vm.timewindow.aggregation.limit" aria-label="limit" aria-controls="limit-slider">
  70 + </md-input-container>
  71 + </md-slider-container>
  72 + </md-content>
55 <section layout="row" layout-alignment="start center"> 73 <section layout="row" layout-alignment="start center">
56 <span flex></span> 74 <span flex></span>
57 <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary"> 75 <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary">
@@ -15,6 +15,7 @@ @@ -15,6 +15,7 @@
15 */ 15 */
16 import './timewindow.scss'; 16 import './timewindow.scss';
17 17
  18 +import $ from 'jquery';
18 import thingsboardTimeinterval from './timeinterval.directive'; 19 import thingsboardTimeinterval from './timeinterval.directive';
19 import thingsboardDatetimePeriod from './datetime-period.directive'; 20 import thingsboardDatetimePeriod from './datetime-period.directive';
20 21
@@ -34,8 +35,9 @@ export default angular.module('thingsboard.directives.timewindow', [thingsboardT @@ -34,8 +35,9 @@ export default angular.module('thingsboard.directives.timewindow', [thingsboardT
34 .filter('milliSecondsToTimeString', MillisecondsToTimeString) 35 .filter('milliSecondsToTimeString', MillisecondsToTimeString)
35 .name; 36 .name;
36 37
  38 +/* eslint-disable angular/angularelement */
37 /*@ngInject*/ 39 /*@ngInject*/
38 -function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $translate) { 40 +function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdMedia, $translate, types) {
39 41
40 var linker = function (scope, element, attrs, ngModelCtrl) { 42 var linker = function (scope, element, attrs, ngModelCtrl) {
41 43
@@ -50,12 +52,18 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $tra @@ -50,12 +52,18 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $tra
50 * startTimeMs: 0, 52 * startTimeMs: 0,
51 * endTimeMs: 0 53 * endTimeMs: 0
52 * } 54 * }
  55 + * },
  56 + * aggregation: {
  57 + * limit: 200,
  58 + * type: types.aggregation.avg.value
53 * } 59 * }
54 * } 60 * }
55 */ 61 */
56 62
57 scope.historyOnly = angular.isDefined(attrs.historyOnly); 63 scope.historyOnly = angular.isDefined(attrs.historyOnly);
58 64
  65 + scope.aggregation = angular.isDefined(attrs.aggregation);
  66 +
59 var translationPending = false; 67 var translationPending = false;
60 68
61 $translate.onReady(function() { 69 $translate.onReady(function() {
@@ -84,9 +92,27 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $tra @@ -84,9 +92,27 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $tra
84 } 92 }
85 93
86 scope.openEditMode = function (event) { 94 scope.openEditMode = function (event) {
87 - var position = $mdPanel.newPanelPosition()  
88 - .relativeTo(element)  
89 - .addPanelPosition($mdPanel.xPosition.ALIGN_START, $mdPanel.yPosition.BELOW); 95 + var position;
  96 + var isGtSm = $mdMedia('gt-sm');
  97 + if (isGtSm) {
  98 + var panelHeight = 375;
  99 + var offset = element[0].getBoundingClientRect();
  100 + var bottomY = offset.bottom - $(window).scrollTop(); //eslint-disable-line
  101 + var yPosition;
  102 + if (bottomY + panelHeight > $( window ).height()) { //eslint-disable-line
  103 + yPosition = $mdPanel.yPosition.ABOVE;
  104 + } else {
  105 + yPosition = $mdPanel.yPosition.BELOW;
  106 + }
  107 + position = $mdPanel.newPanelPosition()
  108 + .relativeTo(element)
  109 + .addPanelPosition($mdPanel.xPosition.ALIGN_START, yPosition);
  110 + } else {
  111 + position = $mdPanel.newPanelPosition()
  112 + .absolute()
  113 + .top('0%')
  114 + .left('0%');
  115 + }
90 var config = { 116 var config = {
91 attachTo: angular.element($document[0].body), 117 attachTo: angular.element($document[0].body),
92 controller: 'TimewindowPanelController', 118 controller: 'TimewindowPanelController',
@@ -94,9 +120,11 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $tra @@ -94,9 +120,11 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $tra
94 templateUrl: timewindowPanelTemplate, 120 templateUrl: timewindowPanelTemplate,
95 panelClass: 'tb-timewindow-panel', 121 panelClass: 'tb-timewindow-panel',
96 position: position, 122 position: position,
  123 + fullscreen: !isGtSm,
97 locals: { 124 locals: {
98 'timewindow': angular.copy(scope.model), 125 'timewindow': angular.copy(scope.model),
99 'historyOnly': scope.historyOnly, 126 'historyOnly': scope.historyOnly,
  127 + 'aggregation': scope.aggregation,
100 'onTimewindowUpdate': function (timewindow) { 128 'onTimewindowUpdate': function (timewindow) {
101 scope.model = timewindow; 129 scope.model = timewindow;
102 scope.updateView(); 130 scope.updateView();
@@ -131,7 +159,10 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $tra @@ -131,7 +159,10 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $tra
131 }; 159 };
132 } 160 }
133 } 161 }
134 - 162 + value.aggregation = {
  163 + limit: model.aggregation.limit,
  164 + type: model.aggregation.type
  165 + };
135 ngModelCtrl.$setViewValue(value); 166 ngModelCtrl.$setViewValue(value);
136 scope.updateDisplayValue(); 167 scope.updateDisplayValue();
137 } 168 }
@@ -173,6 +204,10 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $tra @@ -173,6 +204,10 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $tra
173 startTimeMs: currentTime - 24 * 60 * 60 * 1000, // 1 day by default 204 startTimeMs: currentTime - 24 * 60 * 60 * 1000, // 1 day by default
174 endTimeMs: currentTime 205 endTimeMs: currentTime
175 } 206 }
  207 + },
  208 + aggregation: {
  209 + limit: 200,
  210 + type: types.aggregation.avg.value
176 } 211 }
177 }; 212 };
178 if (ngModelCtrl.$viewValue) { 213 if (ngModelCtrl.$viewValue) {
@@ -192,6 +227,12 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $tra @@ -192,6 +227,12 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $tra
192 model.history.fixedTimewindow.endTimeMs = value.history.fixedTimewindow.endTimeMs; 227 model.history.fixedTimewindow.endTimeMs = value.history.fixedTimewindow.endTimeMs;
193 } 228 }
194 } 229 }
  230 + if (angular.isDefined(value.aggregation)) {
  231 + model.aggregation.limit = value.aggregation.limit || 200;
  232 + if (angular.isDefined(value.aggregation.type) && value.aggregation.type.length > 0) {
  233 + model.aggregation.type = value.aggregation.type;
  234 + }
  235 + }
195 } 236 }
196 scope.updateDisplayValue(); 237 scope.updateDisplayValue();
197 }; 238 };
@@ -240,4 +281,5 @@ function MillisecondsToTimeString($translate) { @@ -240,4 +281,5 @@ function MillisecondsToTimeString($translate) {
240 } 281 }
241 return timeString; 282 return timeString;
242 } 283 }
243 -}  
  284 +}
  285 +/* eslint-enable angular/angularelement */
@@ -13,8 +13,15 @@ @@ -13,8 +13,15 @@
13 * See the License for the specific language governing permissions and 13 * See the License for the specific language governing permissions and
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
  16 +
  17 +.md-panel {
  18 + &.tb-timewindow-panel {
  19 + position: absolute;
  20 + }
  21 +}
  22 +
16 .tb-timewindow-panel { 23 .tb-timewindow-panel {
17 - position: absolute; 24 + min-height: 375px;
18 background: white; 25 background: white;
19 border-radius: 4px; 26 border-radius: 4px;
20 box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2), 27 box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2),
@@ -91,7 +91,7 @@ @@ -91,7 +91,7 @@
91 <div ng-show="widgetType === types.widgetType.timeseries.value" layout="row" 91 <div ng-show="widgetType === types.widgetType.timeseries.value" layout="row"
92 layout-align="center center"> 92 layout-align="center center">
93 <span translate style="padding-right: 8px;">widget-config.timewindow</span> 93 <span translate style="padding-right: 8px;">widget-config.timewindow</span>
94 - <tb-timewindow as-button="true" flex ng-model="timewindow"></tb-timewindow> 94 + <tb-timewindow as-button="true" aggregation flex ng-model="timewindow"></tb-timewindow>
95 </div> 95 </div>
96 <v-accordion id="datasources-accordion" control="datasourcesAccordion" class="vAccordion--default" 96 <v-accordion id="datasources-accordion" control="datasourcesAccordion" class="vAccordion--default"
97 ng-show="widgetType !== types.widgetType.rpc.value && widgetType !== types.widgetType.static.value"> 97 ng-show="widgetType !== types.widgetType.rpc.value && widgetType !== types.widgetType.static.value">
@@ -43,9 +43,9 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -43,9 +43,9 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
43 var originalTimewindow = null; 43 var originalTimewindow = null;
44 var subscriptionTimewindow = { 44 var subscriptionTimewindow = {
45 fixedWindow: null, 45 fixedWindow: null,
46 - realtimeWindowMs: null 46 + realtimeWindowMs: null,
  47 + aggregation: null
47 }; 48 };
48 - var timer = null;  
49 var dataUpdateTimer = null; 49 var dataUpdateTimer = null;
50 var dataUpdateCaf = null; 50 var dataUpdateCaf = null;
51 51
@@ -154,10 +154,10 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -154,10 +154,10 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
154 } 154 }
155 } 155 }
156 156
157 - function updateTimewindow() { 157 + function updateTimewindow(startTs, endTs) {
158 if (subscriptionTimewindow.realtimeWindowMs) { 158 if (subscriptionTimewindow.realtimeWindowMs) {
159 - widgetContext.timeWindow.maxTime = (new Date).getTime();  
160 - widgetContext.timeWindow.minTime = widgetContext.timeWindow.maxTime - subscriptionTimewindow.realtimeWindowMs; 159 + widgetContext.timeWindow.maxTime = endTs || (new Date).getTime();
  160 + widgetContext.timeWindow.minTime = startTs || (widgetContext.timeWindow.maxTime - subscriptionTimewindow.realtimeWindowMs);
161 } else if (subscriptionTimewindow.fixedWindow) { 161 } else if (subscriptionTimewindow.fixedWindow) {
162 widgetContext.timeWindow.maxTime = subscriptionTimewindow.fixedWindow.endTimeMs; 162 widgetContext.timeWindow.maxTime = subscriptionTimewindow.fixedWindow.endTimeMs;
163 widgetContext.timeWindow.minTime = subscriptionTimewindow.fixedWindow.startTimeMs; 163 widgetContext.timeWindow.minTime = subscriptionTimewindow.fixedWindow.startTimeMs;
@@ -170,13 +170,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -170,13 +170,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
170 dataUpdateTimer = null; 170 dataUpdateTimer = null;
171 } 171 }
172 if (widgetContext.inited) { 172 if (widgetContext.inited) {
173 - if (widget.type === types.widgetType.timeseries.value) {  
174 - if (!widgetContext.tickUpdate && timer) {  
175 - $timeout.cancel(timer);  
176 - timer = $timeout(onTick, 1500, false);  
177 - }  
178 - updateTimewindow();  
179 - }  
180 if (dataUpdateCaf) { 173 if (dataUpdateCaf) {
181 dataUpdateCaf(); 174 dataUpdateCaf();
182 dataUpdateCaf = null; 175 dataUpdateCaf = null;
@@ -188,7 +181,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -188,7 +181,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
188 handleWidgetException(e); 181 handleWidgetException(e);
189 } 182 }
190 }); 183 });
191 - widgetContext.tickUpdate = false;  
192 } else { 184 } else {
193 widgetContext.dataUpdatePending = true; 185 widgetContext.dataUpdatePending = true;
194 } 186 }
@@ -512,17 +504,20 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -512,17 +504,20 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
512 var update = true; 504 var update = true;
513 if (widget.type === types.widgetType.latest.value) { 505 if (widget.type === types.widgetType.latest.value) {
514 var prevData = widgetContext.data[datasourceIndex + dataKeyIndex].data; 506 var prevData = widgetContext.data[datasourceIndex + dataKeyIndex].data;
515 - if (prevData && prevData[0] && prevData[0].length > 1 && sourceData.length > 0) { 507 + if (prevData && prevData[0] && prevData[0].length > 1 && sourceData.data.length > 0) {
516 var prevValue = prevData[0][1]; 508 var prevValue = prevData[0][1];
517 - if (prevValue === sourceData[0][1]) { 509 + if (prevValue === sourceData.data[0][1]) {
518 update = false; 510 update = false;
519 } 511 }
520 } 512 }
521 } 513 }
522 if (update) { 514 if (update) {
523 - widgetContext.data[datasourceIndex + dataKeyIndex].data = sourceData; 515 + if (subscriptionTimewindow.realtimeWindowMs) {
  516 + updateTimewindow(sourceData.startTs, sourceData.endTs);
  517 + }
  518 + widgetContext.data[datasourceIndex + dataKeyIndex].data = sourceData.data;
524 if (widgetContext.data.length > 1 && !dataUpdateTimer) { 519 if (widgetContext.data.length > 1 && !dataUpdateTimer) {
525 - dataUpdateTimer = $timeout(onDataUpdated, 100, false); 520 + dataUpdateTimer = $timeout(onDataUpdated, 300, false);
526 } else { 521 } else {
527 onDataUpdated(); 522 onDataUpdated();
528 } 523 }
@@ -557,10 +552,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -557,10 +552,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
557 552
558 function unsubscribe() { 553 function unsubscribe() {
559 if (widget.type !== types.widgetType.rpc.value) { 554 if (widget.type !== types.widgetType.rpc.value) {
560 - if (timer) {  
561 - $timeout.cancel(timer);  
562 - timer = null;  
563 - }  
564 if (dataUpdateTimer) { 555 if (dataUpdateTimer) {
565 $timeout.cancel(dataUpdateTimer); 556 $timeout.cancel(dataUpdateTimer);
566 dataUpdateTimer = null; 557 dataUpdateTimer = null;
@@ -573,19 +564,25 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -573,19 +564,25 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
573 } 564 }
574 } 565 }
575 566
576 - function onTick() {  
577 - widgetContext.tickUpdate = true;  
578 - onDataUpdated();  
579 - timer = $timeout(onTick, 1000, false);  
580 - }  
581 -  
582 function subscribe() { 567 function subscribe() {
583 if (widget.type !== types.widgetType.rpc.value) { 568 if (widget.type !== types.widgetType.rpc.value) {
584 var index = 0; 569 var index = 0;
585 subscriptionTimewindow.fixedWindow = null; 570 subscriptionTimewindow.fixedWindow = null;
586 subscriptionTimewindow.realtimeWindowMs = null; 571 subscriptionTimewindow.realtimeWindowMs = null;
  572 + subscriptionTimewindow.aggregation = {
  573 + limit: 200,
  574 + type: types.aggregation.avg.value
  575 + };
587 if (widget.type === types.widgetType.timeseries.value && 576 if (widget.type === types.widgetType.timeseries.value &&
588 angular.isDefined(widget.config.timewindow)) { 577 angular.isDefined(widget.config.timewindow)) {
  578 +
  579 + if (angular.isDefined(widget.config.timewindow.aggregation)) {
  580 + subscriptionTimewindow.aggregation = {
  581 + limit: widget.config.timewindow.aggregation.limit || 200,
  582 + type: widget.config.timewindow.aggregation.type || types.aggregation.avg.value
  583 + };
  584 + }
  585 +
589 if (angular.isDefined(widget.config.timewindow.realtime)) { 586 if (angular.isDefined(widget.config.timewindow.realtime)) {
590 subscriptionTimewindow.realtimeWindowMs = widget.config.timewindow.realtime.timewindowMs; 587 subscriptionTimewindow.realtimeWindowMs = widget.config.timewindow.realtime.timewindowMs;
591 } else if (angular.isDefined(widget.config.timewindow.history)) { 588 } else if (angular.isDefined(widget.config.timewindow.history)) {
@@ -635,10 +632,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -635,10 +632,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
635 datasourceListeners.push(listener); 632 datasourceListeners.push(listener);
636 datasourceService.subscribeToDatasource(listener); 633 datasourceService.subscribeToDatasource(listener);
637 } 634 }
638 -  
639 - if (subscriptionTimewindow.realtimeWindowMs) {  
640 - timer = $timeout(onTick, 0, false);  
641 - }  
642 } 635 }
643 } 636 }
644 637
@@ -63,6 +63,17 @@ export default angular.module('thingsboard.locale', []) @@ -63,6 +63,17 @@ export default angular.module('thingsboard.locale', [])
63 "import": "Import", 63 "import": "Import",
64 "export": "Export" 64 "export": "Export"
65 }, 65 },
  66 + "aggregation": {
  67 + "aggregation": "Aggregation",
  68 + "function": "Data aggregation function",
  69 + "limit": "Max values",
  70 + "min": "Min",
  71 + "max": "Max",
  72 + "avg": "Average",
  73 + "sum": "Sum",
  74 + "count": "Count",
  75 + "none": "None"
  76 + },
66 "admin": { 77 "admin": {
67 "general": "General", 78 "general": "General",
68 "general-settings": "General Settings", 79 "general-settings": "General Settings",