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 53 @Slf4j
54 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 60 @Value("${cassandra.query.ts_key_value_partitioning}")
60 61 private String partitioning;
... ...
... ... @@ -234,7 +234,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
234 234 return new PluginCallback<List<TsKvEntry>>() {
235 235 @Override
236 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 239 Map<String, Long> subState = new HashMap<>(keys.size());
240 240 keys.forEach(key -> subState.put(key, startTs));
... ...
... ... @@ -26,10 +26,16 @@ public class SubscriptionUpdate {
26 26 private int errorCode;
27 27 private String errorMsg;
28 28 private Map<String, List<Object>> data;
  29 + private long serverStartTs;
29 30
30 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 36 super();
32 37 this.subscriptionId = subscriptionId;
  38 + this.serverStartTs = serverStartTs;
33 39 this.data = new TreeMap<>();
34 40 for (TsKvEntry tsEntry : data) {
35 41 List<Object> values = this.data.get(tsEntry.getKey());
... ... @@ -89,9 +95,13 @@ public class SubscriptionUpdate {
89 95 return errorMsg;
90 96 }
91 97
  98 + public long getServerStartTs() {
  99 + return serverStartTs;
  100 + }
  101 +
92 102 @Override
93 103 public String toString() {
94 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 17 import thingsboardApiTelemetryWebsocket from './telemetry-websocket.service';
18 18 import thingsboardTypes from '../common/types.constant';
19 19 import thingsboardUtils from '../common/utils.service';
  20 +import DataAggregator from './data-aggregator';
20 21
21 22 export default angular.module('thingsboard.api.datasource', [thingsboardApiDevice, thingsboardApiTelemetryWebsocket, thingsboardTypes, thingsboardUtils])
22 23 .factory('datasourceService', DatasourceService)
23 24 .name;
24 25
25 26 /*@ngInject*/
26   -function DatasourceService($timeout, $log, telemetryWebsocketService, types, utils) {
  27 +function DatasourceService($timeout, $filter, $log, telemetryWebsocketService, types, utils) {
27 28
28 29 var subscriptions = {};
29 30
... ... @@ -73,7 +74,7 @@ function DatasourceService($timeout, $log, telemetryWebsocketService, types, uti
73 74 subscription = subscriptions[listener.datasourceSubscriptionKey];
74 75 subscription.syncListener(listener);
75 76 } else {
76   - subscription = new DatasourceSubscription(datasourceSubscription, telemetryWebsocketService, $timeout, $log, types, utils);
  77 + subscription = new DatasourceSubscription(datasourceSubscription, telemetryWebsocketService, $timeout, $filter, $log, types, utils);
77 78 subscriptions[listener.datasourceSubscriptionKey] = subscription;
78 79 subscription.start();
79 80 }
... ... @@ -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 102 var listeners = [];
102 103 var datasourceType = datasourceSubscription.datasourceType;
... ... @@ -134,7 +135,9 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
134 135 if (!dataKey.func) {
135 136 dataKey.func = new Function("time", "prevValue", dataKey.funcBody);
136 137 }
137   - datasourceData[key] = [];
  138 + datasourceData[key] = {
  139 + data: []
  140 + };
138 141 dataKeys[key] = dataKey;
139 142 } else if (datasourceType === types.datasourceType.device) {
140 143 key = dataKey.name + '_' + dataKey.type;
... ... @@ -147,7 +150,9 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
147 150 dataKeys[key] = dataKeysList;
148 151 }
149 152 var index = dataKeysList.push(dataKey) - 1;
150   - datasourceData[key + '_' + index] = [];
  153 + datasourceData[key + '_' + index] = {
  154 + data: []
  155 + };
151 156 }
152 157 dataKey.key = key;
153 158 }
... ... @@ -248,14 +253,18 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
248 253 deviceId: datasourceSubscription.deviceId,
249 254 keys: tsKeys,
250 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 261 subscriber = {
255 262 historyCommand: historyCommand,
256 263 type: types.dataKeyType.timeseries,
257 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 269 onReconnected: function() {
261 270 onReconnected();
... ... @@ -272,20 +281,46 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
272 281 keys: tsKeys
273 282 };
274 283
275   - if (datasourceSubscription.type === types.widgetType.timeseries.value) {
276   - subscriptionCommand.timeWindow = datasourceSubscription.subscriptionTimewindow.realtimeWindowMs;
277   - }
278   -
279 284 subscriber = {
280 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 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 325 telemetryWebsocketService.subscribe(subscriber);
291 326 subscribers[subscriber.subscriptionCommand.cmdId] = subscriber;
... ... @@ -304,7 +339,9 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
304 339 subscriptionCommand: subscriptionCommand,
305 340 type: types.dataKeyType.attribute,
306 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 346 onReconnected: function() {
310 347 onReconnected();
... ... @@ -332,11 +369,14 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
332 369 }
333 370 if (datasourceType === types.datasourceType.device) {
334 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 378 subscribers = {};
338 379 }
339   - //$log.debug("unsibscribed!");
340 380 }
341 381
342 382 function boundToInterval(data, timewindowMs) {
... ... @@ -360,7 +400,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
360 400 function generateSeries(dataKey, startTime, endTime) {
361 401 var data = [];
362 402 var prevSeries;
363   - var datasourceKeyData = datasourceData[dataKey.key];
  403 + var datasourceKeyData = datasourceData[dataKey.key].data;
364 404 if (datasourceKeyData.length > 0) {
365 405 prevSeries = datasourceKeyData[datasourceKeyData.length - 1];
366 406 } else {
... ... @@ -378,10 +418,10 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
378 418 dataKey.lastUpdateTime = data[data.length - 1][0];
379 419 }
380 420 if (realtime) {
381   - datasourceData[dataKey.key] = boundToInterval(datasourceKeyData.concat(data),
  421 + datasourceData[dataKey.key].data = boundToInterval(datasourceKeyData.concat(data),
382 422 datasourceSubscription.subscriptionTimewindow.realtimeWindowMs);
383 423 } else {
384   - datasourceData[dataKey.key] = data;
  424 + datasourceData[dataKey.key].data = data;
385 425 }
386 426 for (var i in listeners) {
387 427 var listener = listeners[i];
... ... @@ -393,7 +433,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
393 433
394 434 function generateLatest(dataKey) {
395 435 var prevSeries;
396   - var datasourceKeyData = datasourceData[dataKey.key];
  436 + var datasourceKeyData = datasourceData[dataKey.key].data;
397 437 if (datasourceKeyData.length > 0) {
398 438 prevSeries = datasourceKeyData[datasourceKeyData.length - 1];
399 439 } else {
... ... @@ -404,7 +444,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
404 444 series.push(time);
405 445 var value = dataKey.func(time, prevSeries[1]);
406 446 series.push(value);
407   - datasourceData[dataKey.key] = [series];
  447 + datasourceData[dataKey.key].data = [series];
408 448 for (var i in listeners) {
409 449 var listener = listeners[i];
410 450 listener.dataUpdated(datasourceData[dataKey.key],
... ... @@ -453,7 +493,9 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
453 493 for (var i = 0; i < dataKeysList.length; i++) {
454 494 var dataKey = dataKeysList[i];
455 495 var datasourceKey = key + '_' + i;
456   - datasourceData[datasourceKey] = [];
  496 + datasourceData[datasourceKey] = {
  497 + data: []
  498 + };
457 499 for (var l in listeners) {
458 500 var listener = listeners[l];
459 501 listener.dataUpdated(datasourceData[datasourceKey],
... ... @@ -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 523 for (var keyName in sourceData) {
482 524 var keyData = sourceData[keyName];
483 525 var key = keyName + '_' + type;
484 526 var dataKeyList = dataKeys[key];
485 527 for (var keyIndex = 0; keyIndex < dataKeyList.length; keyIndex++) {
486 528 var datasourceKey = key + "_" + keyIndex;
487   - if (datasourceData[datasourceKey]) {
  529 + if (datasourceData[datasourceKey].data) {
488 530 var dataKey = dataKeyList[keyIndex];
489 531 var data = [];
490 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 539 if (datasourceKeyData.length > 0) {
493 540 prevSeries = datasourceKeyData[datasourceKeyData.length - 1];
494 541 } else {
... ... @@ -519,12 +566,10 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
519 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 573 for (var i2 in listeners) {
529 574 var listener = listeners[i2];
530 575 listener.dataUpdated(datasourceData[datasourceKey],
... ... @@ -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 304 subscriptionCommand: subscriptionCommand,
305 305 type: type,
306 306 onData: function (data) {
307   - onSubscriptionData(data, subscriptionId);
  307 + if (data.data) {
  308 + onSubscriptionData(data.data, subscriptionId);
  309 + }
308 310 }
309 311 };
310 312 deviceAttributesSubscription = {
... ...
... ... @@ -131,8 +131,8 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty
131 131 var data = angular.fromJson(message.data);
132 132 if (data.subscriptionId) {
133 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 33 id: {
34 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 62 datasourceType: {
37 63 function: "function",
38 64 device: "device"
... ...
... ... @@ -47,7 +47,7 @@
47 47 padding: vm.widgetPadding(widget)}">
48 48 <div class="tb-widget-title" layout="column" ng-show="vm.showWidgetTitle(widget) || vm.hasTimewindow(widget)">
49 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 51 </div>
52 52 <div class="tb-widget-actions" layout="row" layout-align="start center">
53 53 <md-button id="expand-button"
... ...
... ... @@ -14,14 +14,16 @@
14 14 * limitations under the License.
15 15 */
16 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 19 var vm = this;
20 20
21 21 vm._mdPanelRef = mdPanelRef;
22 22 vm.timewindow = timewindow;
23 23 vm.historyOnly = historyOnly;
  24 + vm.aggregation = aggregation;
24 25 vm.onTimewindowUpdate = onTimewindowUpdate;
  26 + vm.aggregationTypes = types.aggregation;
25 27
26 28 if (vm.historyOnly) {
27 29 vm.timewindow.selectedTab = 1;
... ...
... ... @@ -17,7 +17,7 @@
17 17 -->
18 18 <form name="theForm" ng-submit="vm.update()">
19 19 <fieldset ng-disabled="loading">
20   - <section layout="column">
  20 + <md-content layout="column">
21 21 <md-tabs ng-class="{'tb-headless': vm.historyOnly}" flex md-dynamic-height md-selected="vm.timewindow.selectedTab" md-border-bottom>
22 22 <md-tab label="{{ 'timewindow.realtime' | translate }}">
23 23 <md-content class="md-padding" layout="column">
... ... @@ -52,6 +52,24 @@
52 52 </md-content>
53 53 </md-tab>
54 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 73 <section layout="row" layout-alignment="start center">
56 74 <span flex></span>
57 75 <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary">
... ...
... ... @@ -15,6 +15,7 @@
15 15 */
16 16 import './timewindow.scss';
17 17
  18 +import $ from 'jquery';
18 19 import thingsboardTimeinterval from './timeinterval.directive';
19 20 import thingsboardDatetimePeriod from './datetime-period.directive';
20 21
... ... @@ -34,8 +35,9 @@ export default angular.module('thingsboard.directives.timewindow', [thingsboardT
34 35 .filter('milliSecondsToTimeString', MillisecondsToTimeString)
35 36 .name;
36 37
  38 +/* eslint-disable angular/angularelement */
37 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 42 var linker = function (scope, element, attrs, ngModelCtrl) {
41 43
... ... @@ -50,12 +52,18 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $tra
50 52 * startTimeMs: 0,
51 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 63 scope.historyOnly = angular.isDefined(attrs.historyOnly);
58 64
  65 + scope.aggregation = angular.isDefined(attrs.aggregation);
  66 +
59 67 var translationPending = false;
60 68
61 69 $translate.onReady(function() {
... ... @@ -84,9 +92,27 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $tra
84 92 }
85 93
86 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 116 var config = {
91 117 attachTo: angular.element($document[0].body),
92 118 controller: 'TimewindowPanelController',
... ... @@ -94,9 +120,11 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $tra
94 120 templateUrl: timewindowPanelTemplate,
95 121 panelClass: 'tb-timewindow-panel',
96 122 position: position,
  123 + fullscreen: !isGtSm,
97 124 locals: {
98 125 'timewindow': angular.copy(scope.model),
99 126 'historyOnly': scope.historyOnly,
  127 + 'aggregation': scope.aggregation,
100 128 'onTimewindowUpdate': function (timewindow) {
101 129 scope.model = timewindow;
102 130 scope.updateView();
... ... @@ -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 166 ngModelCtrl.$setViewValue(value);
136 167 scope.updateDisplayValue();
137 168 }
... ... @@ -173,6 +204,10 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $tra
173 204 startTimeMs: currentTime - 24 * 60 * 60 * 1000, // 1 day by default
174 205 endTimeMs: currentTime
175 206 }
  207 + },
  208 + aggregation: {
  209 + limit: 200,
  210 + type: types.aggregation.avg.value
176 211 }
177 212 };
178 213 if (ngModelCtrl.$viewValue) {
... ... @@ -192,6 +227,12 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $tra
192 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 237 scope.updateDisplayValue();
197 238 };
... ... @@ -240,4 +281,5 @@ function MillisecondsToTimeString($translate) {
240 281 }
241 282 return timeString;
242 283 }
243   -}
\ No newline at end of file
  284 +}
  285 +/* eslint-enable angular/angularelement */
\ No newline at end of file
... ...
... ... @@ -13,8 +13,15 @@
13 13 * See the License for the specific language governing permissions and
14 14 * limitations under the License.
15 15 */
  16 +
  17 +.md-panel {
  18 + &.tb-timewindow-panel {
  19 + position: absolute;
  20 + }
  21 +}
  22 +
16 23 .tb-timewindow-panel {
17   - position: absolute;
  24 + min-height: 375px;
18 25 background: white;
19 26 border-radius: 4px;
20 27 box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2),
... ...
... ... @@ -91,7 +91,7 @@
91 91 <div ng-show="widgetType === types.widgetType.timeseries.value" layout="row"
92 92 layout-align="center center">
93 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 95 </div>
96 96 <v-accordion id="datasources-accordion" control="datasourcesAccordion" class="vAccordion--default"
97 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 43 var originalTimewindow = null;
44 44 var subscriptionTimewindow = {
45 45 fixedWindow: null,
46   - realtimeWindowMs: null
  46 + realtimeWindowMs: null,
  47 + aggregation: null
47 48 };
48   - var timer = null;
49 49 var dataUpdateTimer = null;
50 50 var dataUpdateCaf = null;
51 51
... ... @@ -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 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 161 } else if (subscriptionTimewindow.fixedWindow) {
162 162 widgetContext.timeWindow.maxTime = subscriptionTimewindow.fixedWindow.endTimeMs;
163 163 widgetContext.timeWindow.minTime = subscriptionTimewindow.fixedWindow.startTimeMs;
... ... @@ -170,13 +170,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
170 170 dataUpdateTimer = null;
171 171 }
172 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 173 if (dataUpdateCaf) {
181 174 dataUpdateCaf();
182 175 dataUpdateCaf = null;
... ... @@ -188,7 +181,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
188 181 handleWidgetException(e);
189 182 }
190 183 });
191   - widgetContext.tickUpdate = false;
192 184 } else {
193 185 widgetContext.dataUpdatePending = true;
194 186 }
... ... @@ -512,17 +504,20 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
512 504 var update = true;
513 505 if (widget.type === types.widgetType.latest.value) {
514 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 508 var prevValue = prevData[0][1];
517   - if (prevValue === sourceData[0][1]) {
  509 + if (prevValue === sourceData.data[0][1]) {
518 510 update = false;
519 511 }
520 512 }
521 513 }
522 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 519 if (widgetContext.data.length > 1 && !dataUpdateTimer) {
525   - dataUpdateTimer = $timeout(onDataUpdated, 100, false);
  520 + dataUpdateTimer = $timeout(onDataUpdated, 300, false);
526 521 } else {
527 522 onDataUpdated();
528 523 }
... ... @@ -557,10 +552,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
557 552
558 553 function unsubscribe() {
559 554 if (widget.type !== types.widgetType.rpc.value) {
560   - if (timer) {
561   - $timeout.cancel(timer);
562   - timer = null;
563   - }
564 555 if (dataUpdateTimer) {
565 556 $timeout.cancel(dataUpdateTimer);
566 557 dataUpdateTimer = null;
... ... @@ -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 567 function subscribe() {
583 568 if (widget.type !== types.widgetType.rpc.value) {
584 569 var index = 0;
585 570 subscriptionTimewindow.fixedWindow = null;
586 571 subscriptionTimewindow.realtimeWindowMs = null;
  572 + subscriptionTimewindow.aggregation = {
  573 + limit: 200,
  574 + type: types.aggregation.avg.value
  575 + };
587 576 if (widget.type === types.widgetType.timeseries.value &&
588 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 586 if (angular.isDefined(widget.config.timewindow.realtime)) {
590 587 subscriptionTimewindow.realtimeWindowMs = widget.config.timewindow.realtime.timewindowMs;
591 588 } else if (angular.isDefined(widget.config.timewindow.history)) {
... ... @@ -635,10 +632,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
635 632 datasourceListeners.push(listener);
636 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 63 "import": "Import",
64 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 77 "admin": {
67 78 "general": "General",
68 79 "general-settings": "General Settings",
... ...