Commit ba6c2c1a1b046ce45e532cf2c312c3332761b58f

Authored by Igor Kulikov
1 parent 70ddf204

UI: Implement state data aggregation. Introduce State chart.

@@ -152,6 +152,22 @@ @@ -152,6 +152,22 @@
152 "dataKeySettingsSchema": "{}", 152 "dataKeySettingsSchema": "{}",
153 "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000},\"aggregation\":{\"limit\":200,\"type\":\"AVG\"}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"legend\":{\"show\":true,\"position\":\"nw\",\"backgroundColor\":\"#f0f0f0\",\"backgroundOpacity\":0.85,\"labelBoxBorderColor\":\"rgba(1, 1, 1, 0.45)\"},\"decimals\":1,\"stack\":true,\"tooltipIndividual\":false},\"title\":\"Timeseries Bars - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}" 153 "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000},\"aggregation\":{\"limit\":200,\"type\":\"AVG\"}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"legend\":{\"show\":true,\"position\":\"nw\",\"backgroundColor\":\"#f0f0f0\",\"backgroundOpacity\":0.85,\"labelBoxBorderColor\":\"rgba(1, 1, 1, 0.45)\"},\"decimals\":1,\"stack\":true,\"tooltipIndividual\":false},\"title\":\"Timeseries Bars - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}"
154 } 154 }
  155 + },
  156 + {
  157 + "alias": "state_chart",
  158 + "name": "State Chart",
  159 + "descriptor": {
  160 + "type": "timeseries",
  161 + "sizeX": 8,
  162 + "sizeY": 5,
  163 + "resources": [],
  164 + "templateHtml": "",
  165 + "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n",
  166 + "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'state'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true\n };\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema;\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true);\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
  167 + "settingsSchema": "{}",
  168 + "dataKeySettingsSchema": "{}",
  169 + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\",\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":false,\"tooltipIndividual\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"smoothLines\":false},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}"
  170 + }
155 } 171 }
156 ] 172 ]
157 } 173 }
@@ -96,9 +96,9 @@ @@ -96,9 +96,9 @@
96 "templateHtml": "<tb-led-indicator ctx='ctx'></tb-led-indicator>", 96 "templateHtml": "<tb-led-indicator ctx='ctx'></tb-led-indicator>",
97 "templateCss": "", 97 "templateCss": "",
98 "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n scope.ctx = self.ctx;\n}\n\nself.onResize = function() {\n if (self.ctx.resize) {\n self.ctx.resize();\n }\n}\n\nself.onDestroy = function() {\n}\n", 98 "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n scope.ctx = self.ctx;\n}\n\nself.onResize = function() {\n if (self.ctx.resize) {\n self.ctx.resize();\n }\n}\n\nself.onDestroy = function() {\n}\n",
99 - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"initialValue\": {\n \"title\": \"Initial value\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"title\": {\n \"title\": \"LED title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"ledColor\": {\n \"title\": \"LED Color\",\n \"type\": \"string\",\n \"default\": \"green\"\n },\n \"scheckStatusMethod\": {\n \"title\": \"RPC check device status method\",\n \"type\": \"string\",\n \"default\": \"checkStatus\"\n },\n \"retrieveValueMethod\": {\n \"title\": \"Retrieve led status value using method\",\n \"type\": \"string\",\n \"default\": \"attribute\"\n },\n \"valueAttribute\": {\n \"title\": \"Device attribute/timeseries containing led status value\",\n \"type\": \"string\",\n \"default\": \"value\"\n },\n \"parseValueFunction\": {\n \"title\": \"Parse led status value function, f(data), returns boolean\",\n \"type\": \"string\",\n \"default\": \"return data ? true : false;\"\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout (ms)\",\n \"type\": \"number\",\n \"default\": 500\n }\n },\n \"required\": [\"scheckStatusMethod\", \"valueAttribute\", \"requestTimeout\"]\n },\n \"form\": [\n \"initialValue\",\n \"title\",\n {\n \"key\": \"ledColor\",\n \"type\": \"color\"\n },\n \"scheckStatusMethod\",\n {\n \"key\": \"retrieveValueMethod\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"attribute\",\n \"label\": \"Subscribe for attribute\"\n },\n {\n \"value\": \"timeseries\",\n \"label\": \"Subscribe for timeseries\"\n }\n ]\n },\n \"valueAttribute\",\n {\n \"key\": \"parseValueFunction\",\n \"type\": \"javascript\"\n },\n \"requestTimeout\"\n ]\n}", 99 + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"initialValue\": {\n \"title\": \"Initial value\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"title\": {\n \"title\": \"LED title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"ledColor\": {\n \"title\": \"LED Color\",\n \"type\": \"string\",\n \"default\": \"green\"\n },\n \"performCheckStatus\": {\n \"title\": \"Perform RPC device status check\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"scheckStatusMethod\": {\n \"title\": \"RPC check device status method\",\n \"type\": \"string\",\n \"default\": \"checkStatus\"\n },\n \"retrieveValueMethod\": {\n \"title\": \"Retrieve led status value using method\",\n \"type\": \"string\",\n \"default\": \"attribute\"\n },\n \"valueAttribute\": {\n \"title\": \"Device attribute/timeseries containing led status value\",\n \"type\": \"string\",\n \"default\": \"value\"\n },\n \"parseValueFunction\": {\n \"title\": \"Parse led status value function, f(data), returns boolean\",\n \"type\": \"string\",\n \"default\": \"return data ? true : false;\"\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout (ms)\",\n \"type\": \"number\",\n \"default\": 500\n }\n },\n \"required\": [\"scheckStatusMethod\", \"valueAttribute\", \"requestTimeout\"]\n },\n \"form\": [\n \"initialValue\",\n \"title\",\n {\n \"key\": \"ledColor\",\n \"type\": \"color\"\n },\n \"performCheckStatus\",\n \"scheckStatusMethod\",\n {\n \"key\": \"retrieveValueMethod\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"attribute\",\n \"label\": \"Subscribe for attribute\"\n },\n {\n \"value\": \"timeseries\",\n \"label\": \"Subscribe for timeseries\"\n }\n ]\n },\n \"valueAttribute\",\n {\n \"key\": \"parseValueFunction\",\n \"type\": \"javascript\"\n },\n \"requestTimeout\"\n ]\n}",
100 "dataKeySettingsSchema": "{}\n", 100 "dataKeySettingsSchema": "{}\n",
101 - "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":500,\"initialValue\":true,\"title\":\"Led indicator\",\"ledColor\":\"#4caf50\",\"scheckStatusMethod\":\"checkStatus\",\"valueAttribute\":\"value\",\"retrieveValueMethod\":\"attribute\",\"parseValueFunction\":\"return data ? true : false;\"},\"title\":\"Led indicator\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"decimals\":2}" 101 + "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":500,\"initialValue\":true,\"title\":\"Led indicator\",\"ledColor\":\"#4caf50\",\"scheckStatusMethod\":\"checkStatus\",\"valueAttribute\":\"value\",\"retrieveValueMethod\":\"attribute\",\"parseValueFunction\":\"return data ? true : false;\",\"performCheckStatus\":true},\"title\":\"Led indicator\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"decimals\":2}"
102 } 102 }
103 } 103 }
104 ] 104 ]
@@ -186,7 +186,7 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService) @@ -186,7 +186,7 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService)
186 types.dataKeyType.timeseries : types.dataKeyType.attribute; 186 types.dataKeyType.timeseries : types.dataKeyType.attribute;
187 187
188 var subscriber = { 188 var subscriber = {
189 - subscriptionCommand: subscriptionCommand, 189 + subscriptionCommands: [subscriptionCommand],
190 type: type, 190 type: type,
191 onData: function (data) { 191 onData: function (data) {
192 if (data.data) { 192 if (data.data) {
@@ -17,7 +17,7 @@ @@ -17,7 +17,7 @@
17 export default class DataAggregator { 17 export default class DataAggregator {
18 18
19 constructor(onDataCb, tsKeyNames, startTs, limit, aggregationType, timeWindow, interval, 19 constructor(onDataCb, tsKeyNames, startTs, limit, aggregationType, timeWindow, interval,
20 - steppedChart, types, $timeout, $filter) { 20 + stateData, types, $timeout, $filter) {
21 this.onDataCb = onDataCb; 21 this.onDataCb = onDataCb;
22 this.tsKeyNames = tsKeyNames; 22 this.tsKeyNames = tsKeyNames;
23 this.dataBuffer = {}; 23 this.dataBuffer = {};
@@ -35,8 +35,10 @@ export default class DataAggregator { @@ -35,8 +35,10 @@ export default class DataAggregator {
35 this.limit = limit; 35 this.limit = limit;
36 this.timeWindow = timeWindow; 36 this.timeWindow = timeWindow;
37 this.interval = interval; 37 this.interval = interval;
38 - this.steppedChart = steppedChart;  
39 - this.firstStepDataReceived = !this.steppedChart; 38 + this.stateData = stateData;
  39 + if (this.stateData) {
  40 + this.lastPrevKvPairData = {};
  41 + }
40 this.aggregationTimeout = Math.max(this.interval, 1000); 42 this.aggregationTimeout = Math.max(this.interval, 1000);
41 switch (aggregationType) { 43 switch (aggregationType) {
42 case types.aggregation.min.value: 44 case types.aggregation.min.value:
@@ -81,10 +83,6 @@ export default class DataAggregator { @@ -81,10 +83,6 @@ export default class DataAggregator {
81 }, this.aggregationTimeout, false); 83 }, this.aggregationTimeout, false);
82 } 84 }
83 85
84 - onFirstStepData(data) {  
85 - this.firstStepData = data;  
86 - }  
87 -  
88 onData(data, update, history, apply) { 86 onData(data, update, history, apply) {
89 if (!this.dataReceived || this.resetPending) { 87 if (!this.dataReceived || this.resetPending) {
90 var updateIntervalScheduledTime = true; 88 var updateIntervalScheduledTime = true;
@@ -158,6 +156,10 @@ export default class DataAggregator { @@ -158,6 +156,10 @@ export default class DataAggregator {
158 var keyData = this.dataBuffer[key]; 156 var keyData = this.dataBuffer[key];
159 for (var aggTimestamp in aggKeyData) { 157 for (var aggTimestamp in aggKeyData) {
160 if (aggTimestamp <= this.startTs) { 158 if (aggTimestamp <= this.startTs) {
  159 + if (this.stateData &&
  160 + (!this.lastPrevKvPairData[key] || this.lastPrevKvPairData[key][0] < aggTimestamp)) {
  161 + this.lastPrevKvPairData[key] = [Number(aggTimestamp), aggKeyData[aggTimestamp].aggValue];
  162 + }
161 delete aggKeyData[aggTimestamp]; 163 delete aggKeyData[aggTimestamp];
162 } else if (aggTimestamp <= this.endTs) { 164 } else if (aggTimestamp <= this.endTs) {
163 var aggData = aggKeyData[aggTimestamp]; 165 var aggData = aggKeyData[aggTimestamp];
@@ -166,6 +168,9 @@ export default class DataAggregator { @@ -166,6 +168,9 @@ export default class DataAggregator {
166 } 168 }
167 } 169 }
168 keyData = this.$filter('orderBy')(keyData, '+this[0]'); 170 keyData = this.$filter('orderBy')(keyData, '+this[0]');
  171 + if (this.stateData) {
  172 + this.updateStateBounds(keyData, angular.copy(this.lastPrevKvPairData[key]));
  173 + }
169 if (keyData.length > this.limit) { 174 if (keyData.length > this.limit) {
170 keyData = keyData.slice(keyData.length - this.limit); 175 keyData = keyData.slice(keyData.length - this.limit);
171 } 176 }
@@ -174,6 +179,34 @@ export default class DataAggregator { @@ -174,6 +179,34 @@ export default class DataAggregator {
174 return this.dataBuffer; 179 return this.dataBuffer;
175 } 180 }
176 181
  182 + updateStateBounds(keyData, lastPrevKvPair) {
  183 + if (lastPrevKvPair) {
  184 + lastPrevKvPair[0] = this.startTs;
  185 + }
  186 + var firstKvPair;
  187 + if (!keyData.length) {
  188 + if (lastPrevKvPair) {
  189 + firstKvPair = lastPrevKvPair;
  190 + keyData.push(firstKvPair);
  191 + }
  192 + } else {
  193 + firstKvPair = keyData[0];
  194 + }
  195 + if (firstKvPair && firstKvPair[0] > this.startTs) {
  196 + if (lastPrevKvPair) {
  197 + keyData.unshift(lastPrevKvPair);
  198 + }
  199 + }
  200 + if (keyData.length) {
  201 + var lastKvPair = keyData[keyData.length-1];
  202 + if (lastKvPair[0] < this.endTs) {
  203 + lastKvPair = angular.copy(lastKvPair);
  204 + lastKvPair[0] = this.endTs;
  205 + keyData.push(lastKvPair);
  206 + }
  207 + }
  208 + }
  209 +
177 destroy() { 210 destroy() {
178 if (this.intervalTimeoutHandle) { 211 if (this.intervalTimeoutHandle) {
179 this.$timeout.cancel(this.intervalTimeoutHandle); 212 this.$timeout.cancel(this.intervalTimeoutHandle);
@@ -105,7 +105,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -105,7 +105,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
105 var datasourceType = datasourceSubscription.datasourceType; 105 var datasourceType = datasourceSubscription.datasourceType;
106 var datasourceData = {}; 106 var datasourceData = {};
107 var dataKeys = {}; 107 var dataKeys = {};
108 - var subscribers = {}; 108 + var subscribers = [];
109 var history = datasourceSubscription.subscriptionTimewindow && 109 var history = datasourceSubscription.subscriptionTimewindow &&
110 datasourceSubscription.subscriptionTimewindow.fixedWindow; 110 datasourceSubscription.subscriptionTimewindow.fixedWindow;
111 var realtime = datasourceSubscription.subscriptionTimewindow && 111 var realtime = datasourceSubscription.subscriptionTimewindow &&
@@ -249,7 +249,6 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -249,7 +249,6 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
249 if (tsKeys.length > 0) { 249 if (tsKeys.length > 0) {
250 250
251 var subscriber; 251 var subscriber;
252 - var subscriptionCommand;  
253 252
254 if (history) { 253 if (history) {
255 254
@@ -265,45 +264,103 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -265,45 +264,103 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
265 }; 264 };
266 265
267 subscriber = { 266 subscriber = {
268 - historyCommand: historyCommand, 267 + historyCommands: [ historyCommand ],
269 type: types.dataKeyType.timeseries, 268 type: types.dataKeyType.timeseries,
270 - onData: function (data) {  
271 - if (data.data) {  
272 - for (var key in data.data) { 269 + subsTw: subsTw
  270 + };
  271 +
  272 + if (subsTw.aggregation.stateData) {
  273 + subscriber.firstStateHistoryCommand = createFirstStateHistoryCommand(subsTw.fixedWindow.startTimeMs, tsKeys);
  274 + subscriber.historyCommands.push(subscriber.firstStateHistoryCommand);
  275 + }
  276 +
  277 + subscriber.onData = function (data, subscriptionId) {
  278 + if (this.subsTw.aggregation.stateData &&
  279 + this.firstStateHistoryCommand && this.firstStateHistoryCommand.cmdId == subscriptionId) {
  280 + if (this.data) {
  281 + onStateHistoryData(data, this.data, this.subsTw.aggregation.limit,
  282 + subsTw.fixedWindow.startTimeMs, this.subsTw.fixedWindow.endTimeMs,
  283 + (data) => {
  284 + onData(data.data, types.dataKeyType.timeseries, true);
  285 + });
  286 + } else {
  287 + this.firstStateData = data;
  288 + }
  289 + } else {
  290 + if (this.subsTw.aggregation.stateData) {
  291 + if (this.firstStateData) {
  292 + onStateHistoryData(this.firstStateData, data, this.subsTw.aggregation.limit,
  293 + this.subsTw.fixedWindow.startTimeMs, this.subsTw.fixedWindow.endTimeMs,
  294 + (data) => {
  295 + onData(data.data, types.dataKeyType.timeseries, true);
  296 + });
  297 + } else {
  298 + this.data = data;
  299 + }
  300 + } else {
  301 + for (key in data.data) {
273 var keyData = data.data[key]; 302 var keyData = data.data[key];
274 data.data[key] = $filter('orderBy')(keyData, '+this[0]'); 303 data.data[key] = $filter('orderBy')(keyData, '+this[0]');
275 } 304 }
276 onData(data.data, types.dataKeyType.timeseries, true); 305 onData(data.data, types.dataKeyType.timeseries, true);
277 } 306 }
278 - },  
279 - onReconnected: function() {} 307 + }
280 }; 308 };
281 - 309 + subscriber.onReconnected = function() {};
282 telemetryWebsocketService.subscribe(subscriber); 310 telemetryWebsocketService.subscribe(subscriber);
283 - subscribers[subscriber.historyCommand.cmdId] = subscriber;  
284 -  
285 - if (subsTw.aggregation.steppedChart) {  
286 - createFirstStepSubscription(subsTw, tsKeys);  
287 - } 311 + subscribers.push(subscriber);
288 312
289 } else { 313 } else {
290 314
291 - subscriptionCommand = { 315 + var subscriptionCommand = {
292 entityType: datasourceSubscription.entityType, 316 entityType: datasourceSubscription.entityType,
293 entityId: datasourceSubscription.entityId, 317 entityId: datasourceSubscription.entityId,
294 keys: tsKeys 318 keys: tsKeys
295 }; 319 };
296 320
297 subscriber = { 321 subscriber = {
298 - subscriptionCommand: subscriptionCommand, 322 + subscriptionCommands: [subscriptionCommand],
299 type: types.dataKeyType.timeseries 323 type: types.dataKeyType.timeseries
300 }; 324 };
301 325
302 if (datasourceSubscription.type === types.widgetType.timeseries.value) { 326 if (datasourceSubscription.type === types.widgetType.timeseries.value) {
  327 + subscriber.subsTw = subsTw;
303 updateRealtimeSubscriptionCommand(subscriptionCommand, subsTw); 328 updateRealtimeSubscriptionCommand(subscriptionCommand, subsTw);
  329 +
  330 + if (subsTw.aggregation.stateData) {
  331 + subscriber.firstStateSubscriptionCommand = createFirstStateHistoryCommand(subsTw.startTs, tsKeys);
  332 + subscriber.historyCommands = [subscriber.firstStateSubscriptionCommand];
  333 + }
304 dataAggregator = createRealtimeDataAggregator(subsTw, tsKeyNames, types.dataKeyType.timeseries); 334 dataAggregator = createRealtimeDataAggregator(subsTw, tsKeyNames, types.dataKeyType.timeseries);
305 - subscriber.onData = function(data) {  
306 - dataAggregator.onData(data, false, false, true); 335 + subscriber.onData = function(data, subscriptionId) {
  336 + if (this.subsTw.aggregation.stateData &&
  337 + this.firstStateSubscriptionCommand && this.firstStateSubscriptionCommand.cmdId == subscriptionId) {
  338 + if (this.data) {
  339 + onStateHistoryData(data, this.data, this.subsTw.aggregation.limit,
  340 + this.subsTw.startTs, this.subsTw.startTs + this.subsTw.aggregation.timeWindow,
  341 + (data) => {
  342 + dataAggregator.onData(data, false, false, true);
  343 + });
  344 + this.stateDataReceived = true;
  345 + } else {
  346 + this.firstStateData = data;
  347 + }
  348 + } else {
  349 + if (this.subsTw.aggregation.stateData && !this.stateDataReceived) {
  350 + if (this.firstStateData) {
  351 + onStateHistoryData(this.firstStateData, data, this.subsTw.aggregation.limit,
  352 + this.subsTw.startTs, this.subsTw.startTs + this.subsTw.aggregation.timeWindow,
  353 + (data) => {
  354 + dataAggregator.onData(data, false, false, true);
  355 + });
  356 + this.stateDataReceived = true;
  357 + } else {
  358 + this.data = data;
  359 + }
  360 + } else {
  361 + dataAggregator.onData(data, false, false, true);
  362 + }
  363 + }
307 } 364 }
308 subscriber.onReconnected = function() { 365 subscriber.onReconnected = function() {
309 var newSubsTw = null; 366 var newSubsTw = null;
@@ -315,14 +372,16 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -315,14 +372,16 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
315 listener.setRealtimeSubscription(newSubsTw); 372 listener.setRealtimeSubscription(newSubsTw);
316 } 373 }
317 } 374 }
318 - updateRealtimeSubscriptionCommand(this.subscriptionCommand, newSubsTw); 375 + this.subsTw = newSubsTw;
  376 + this.firstStateData = null;
  377 + this.data = null;
  378 + this.stateDataReceived = false;
  379 + updateRealtimeSubscriptionCommand(this.subscriptionCommands[0], this.subsTw);
  380 + if (this.subsTw.aggregation.stateData) {
  381 + updateFirstStateHistoryCommand(this.firstStateSubscriptionCommand, this.subsTw.startTs);
  382 + }
319 dataAggregator.reset(newSubsTw.startTs, newSubsTw.aggregation.timeWindow, newSubsTw.aggregation.interval); 383 dataAggregator.reset(newSubsTw.startTs, newSubsTw.aggregation.timeWindow, newSubsTw.aggregation.interval);
320 } 384 }
321 -  
322 - if (subsTw.aggregation.steppedChart) {  
323 - createFirstStepSubscription(subsTw, tsKeys);  
324 - }  
325 -  
326 } else { 385 } else {
327 subscriber.onReconnected = function() {} 386 subscriber.onReconnected = function() {}
328 subscriber.onData = function(data) { 387 subscriber.onData = function(data) {
@@ -333,21 +392,21 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -333,21 +392,21 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
333 } 392 }
334 393
335 telemetryWebsocketService.subscribe(subscriber); 394 telemetryWebsocketService.subscribe(subscriber);
336 - subscribers[subscriber.subscriptionCommand.cmdId] = subscriber; 395 + subscribers.push(subscriber);
337 396
338 } 397 }
339 } 398 }
340 399
341 if (attrKeys.length > 0) { 400 if (attrKeys.length > 0) {
342 401
343 - subscriptionCommand = { 402 + var attrsSubscriptionCommand = {
344 entityType: datasourceSubscription.entityType, 403 entityType: datasourceSubscription.entityType,
345 entityId: datasourceSubscription.entityId, 404 entityId: datasourceSubscription.entityId,
346 keys: attrKeys 405 keys: attrKeys
347 }; 406 };
348 407
349 subscriber = { 408 subscriber = {
350 - subscriptionCommand: subscriptionCommand, 409 + subscriptionCommands: [attrsSubscriptionCommand],
351 type: types.dataKeyType.attribute, 410 type: types.dataKeyType.attribute,
352 onData: function (data) { 411 onData: function (data) {
353 if (data.data) { 412 if (data.data) {
@@ -358,7 +417,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -358,7 +417,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
358 }; 417 };
359 418
360 telemetryWebsocketService.subscribe(subscriber); 419 telemetryWebsocketService.subscribe(subscriber);
361 - subscribers[subscriber.cmdId] = subscriber; 420 + subscribers.push(subscriber);
362 421
363 } 422 }
364 423
@@ -388,35 +447,47 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -388,35 +447,47 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
388 } 447 }
389 } 448 }
390 449
391 - function createFirstStepSubscription(subsTw, tsKeys) {  
392 - var startStepCommand = { 450 + function createFirstStateHistoryCommand(startTs, tsKeys) {
  451 + return {
393 entityType: datasourceSubscription.entityType, 452 entityType: datasourceSubscription.entityType,
394 entityId: datasourceSubscription.entityId, 453 entityId: datasourceSubscription.entityId,
395 keys: tsKeys, 454 keys: tsKeys,
396 - startTs: subsTw.fixedWindow.startTimeMs - YEAR,  
397 - endTs: subsTw.fixedWindow.startTimeMs,  
398 - interval: subsTw.aggregation.interval, 455 + startTs: startTs - YEAR,
  456 + endTs: startTs,
  457 + interval: 1000,
399 limit: 1, 458 limit: 1,
400 - agg: subsTw.aggregation.type  
401 - };  
402 - var subscriber = {  
403 - historyCommand: startStepCommand,  
404 - type: types.dataKeyType.timeseries,  
405 - onData: function (data) {  
406 - if (data.data) {  
407 - for (var key in data.data) {  
408 - var keyData = data.data[key];  
409 - data.data[key] = $filter('orderBy')(keyData, '+this[0]');  
410 - }  
411 - //onData(data.data, types.dataKeyType.timeseries, true);  
412 - //TODO: onStartStepData  
413 - }  
414 - },  
415 - onReconnected: function() {} 459 + agg: types.aggregation.none.value
416 }; 460 };
  461 + }
  462 +
  463 + function updateFirstStateHistoryCommand(stateHistoryCommand, startTs) {
  464 + stateHistoryCommand.startTs = startTs - YEAR;
  465 + stateHistoryCommand.endTs = startTs;
  466 + }
417 467
418 - telemetryWebsocketService.subscribe(subscriber);  
419 - subscribers[subscriber.historyCommand.cmdId] = subscriber; 468 + function onStateHistoryData(firstStateData, data, limit, startTs, endTs, onData) {
  469 + for (var key in data.data) {
  470 + var keyData = data.data[key];
  471 + data.data[key] = $filter('orderBy')(keyData, '+this[0]');
  472 + keyData = data.data[key];
  473 + if (keyData.length < limit) {
  474 + var firstStateKeyData = firstStateData.data[key];
  475 + if (firstStateKeyData.length) {
  476 + var firstStateDataTsKv = firstStateKeyData[0];
  477 + firstStateDataTsKv[0] = startTs;
  478 + firstStateKeyData = [
  479 + [ startTs, firstStateKeyData[0][1] ]
  480 + ];
  481 + keyData.unshift(firstStateDataTsKv);
  482 + }
  483 + }
  484 + if (keyData.length) {
  485 + var lastTsKv = angular.copy(keyData[keyData.length-1]);
  486 + lastTsKv[0] = endTs;
  487 + keyData.push(lastTsKv);
  488 + }
  489 + }
  490 + onData(data);
420 } 491 }
421 492
422 function createRealtimeDataAggregator(subsTw, tsKeyNames, dataKeyType) { 493 function createRealtimeDataAggregator(subsTw, tsKeyNames, dataKeyType) {
@@ -430,7 +501,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -430,7 +501,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
430 subsTw.aggregation.type, 501 subsTw.aggregation.type,
431 subsTw.aggregation.timeWindow, 502 subsTw.aggregation.timeWindow,
432 subsTw.aggregation.interval, 503 subsTw.aggregation.interval,
433 - subsTw.aggregation.steppedChart, 504 + subsTw.aggregation.stateData,
434 types, 505 types,
435 $timeout, 506 $timeout,
436 $filter 507 $filter
@@ -451,14 +522,14 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -451,14 +522,14 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
451 timer = null; 522 timer = null;
452 } 523 }
453 if (datasourceType === types.datasourceType.entity) { 524 if (datasourceType === types.datasourceType.entity) {
454 - for (var cmdId in subscribers) {  
455 - var subscriber = subscribers[cmdId]; 525 + for (var i=0;i<subscribers.length;i++) {
  526 + var subscriber = subscribers[i];
456 telemetryWebsocketService.unsubscribe(subscriber); 527 telemetryWebsocketService.unsubscribe(subscriber);
457 if (subscriber.onDestroy) { 528 if (subscriber.onDestroy) {
458 subscriber.onDestroy(); 529 subscriber.onDestroy();
459 } 530 }
460 } 531 }
461 - subscribers = {}; 532 + subscribers.length = 0;
462 } 533 }
463 if (dataAggregator) { 534 if (dataAggregator) {
464 dataAggregator.destroy(); 535 dataAggregator.destroy();
@@ -128,7 +128,7 @@ export default class Subscription { @@ -128,7 +128,7 @@ export default class Subscription {
128 stDiff: this.ctx.stDiff 128 stDiff: this.ctx.stDiff
129 } 129 }
130 this.useDashboardTimewindow = options.useDashboardTimewindow; 130 this.useDashboardTimewindow = options.useDashboardTimewindow;
131 - this.steppedChart = options.steppedChart; 131 + this.stateData = options.stateData;
132 if (this.useDashboardTimewindow) { 132 if (this.useDashboardTimewindow) {
133 this.timeWindowConfig = angular.copy(options.dashboardTimewindow); 133 this.timeWindowConfig = angular.copy(options.dashboardTimewindow);
134 } else { 134 } else {
@@ -612,7 +612,7 @@ export default class Subscription { @@ -612,7 +612,7 @@ export default class Subscription {
612 this.subscriptionTimewindow = 612 this.subscriptionTimewindow =
613 this.ctx.timeService.createSubscriptionTimewindow( 613 this.ctx.timeService.createSubscriptionTimewindow(
614 this.timeWindowConfig, 614 this.timeWindowConfig,
615 - this.timeWindow.stDiff, this.steppedChart); 615 + this.timeWindow.stDiff, this.stateData);
616 } 616 }
617 this.updateTimewindow(); 617 this.updateTimewindow();
618 return this.subscriptionTimewindow; 618 return this.subscriptionTimewindow;
@@ -34,6 +34,7 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty @@ -34,6 +34,7 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty
34 lastCmdId = 0, 34 lastCmdId = 0,
35 subscribers = {}, 35 subscribers = {},
36 subscribersCount = 0, 36 subscribersCount = 0,
  37 + commands = {},
37 cmdsWrapper = { 38 cmdsWrapper = {
38 tsSubCmds: [], 39 tsSubCmds: [],
39 historyCmds: [], 40 historyCmds: [],
@@ -120,7 +121,10 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty @@ -120,7 +121,10 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty
120 if (!isReconnect) { 121 if (!isReconnect) {
121 reconnectSubscribers = []; 122 reconnectSubscribers = [];
122 for (var id in subscribers) { 123 for (var id in subscribers) {
123 - reconnectSubscribers.push(subscribers[id]); 124 + var subscriber = subscribers[id];
  125 + if (reconnectSubscribers.indexOf(subscriber) === -1) {
  126 + reconnectSubscribers.push(subscriber);
  127 + }
124 } 128 }
125 reset(false); 129 reset(false);
126 isReconnect = true; 130 isReconnect = true;
@@ -138,7 +142,7 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty @@ -138,7 +142,7 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty
138 if (data.subscriptionId) { 142 if (data.subscriptionId) {
139 var subscriber = subscribers[data.subscriptionId]; 143 var subscriber = subscribers[data.subscriptionId];
140 if (subscriber && data) { 144 if (subscriber && data) {
141 - var keys = fetchKeys(subscriber); 145 + var keys = fetchKeys(data.subscriptionId);
142 if (!data.data) { 146 if (!data.data) {
143 data.data = {}; 147 data.data = {};
144 } 148 }
@@ -148,20 +152,15 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty @@ -148,20 +152,15 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty
148 data.data[key] = []; 152 data.data[key] = [];
149 } 153 }
150 } 154 }
151 - subscriber.onData(data); 155 + subscriber.onData(data, data.subscriptionId);
152 } 156 }
153 } 157 }
154 } 158 }
155 checkToClose(); 159 checkToClose();
156 } 160 }
157 161
158 - function fetchKeys(subscriber) {  
159 - var command;  
160 - if (angular.isDefined(subscriber.subscriptionCommand)) {  
161 - command = subscriber.subscriptionCommand;  
162 - } else {  
163 - command = subscriber.historyCommand;  
164 - } 162 + function fetchKeys(subscriptionId) {
  163 + var command = commands[subscriptionId];
165 if (command && command.keys && command.keys.length > 0) { 164 if (command && command.keys && command.keys.length > 0) {
166 return command.keys.split(","); 165 return command.keys.split(",");
167 } else { 166 } else {
@@ -176,41 +175,73 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty @@ -176,41 +175,73 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty
176 175
177 function subscribe (subscriber) { 176 function subscribe (subscriber) {
178 isActive = true; 177 isActive = true;
179 - var cmdId = nextCmdId();  
180 - subscribers[cmdId] = subscriber;  
181 - subscribersCount++;  
182 - if (angular.isDefined(subscriber.subscriptionCommand)) {  
183 - subscriber.subscriptionCommand.cmdId = cmdId;  
184 - if (subscriber.type === types.dataKeyType.timeseries) {  
185 - cmdsWrapper.tsSubCmds.push(subscriber.subscriptionCommand);  
186 - } else if (subscriber.type === types.dataKeyType.attribute) {  
187 - cmdsWrapper.attrSubCmds.push(subscriber.subscriptionCommand); 178 + var cmdId;
  179 + if (angular.isDefined(subscriber.subscriptionCommands)) {
  180 + for (var i=0;i<subscriber.subscriptionCommands.length;i++) {
  181 + var subscriptionCommand = subscriber.subscriptionCommands[i];
  182 + cmdId = nextCmdId();
  183 + subscribers[cmdId] = subscriber;
  184 + subscriptionCommand.cmdId = cmdId;
  185 + commands[cmdId] = subscriptionCommand;
  186 + if (subscriber.type === types.dataKeyType.timeseries) {
  187 + cmdsWrapper.tsSubCmds.push(subscriptionCommand);
  188 + } else if (subscriber.type === types.dataKeyType.attribute) {
  189 + cmdsWrapper.attrSubCmds.push(subscriptionCommand);
  190 + }
188 } 191 }
189 - } else if (angular.isDefined(subscriber.historyCommand)) {  
190 - subscriber.historyCommand.cmdId = cmdId;  
191 - cmdsWrapper.historyCmds.push(subscriber.historyCommand);  
192 } 192 }
  193 + if (angular.isDefined(subscriber.historyCommands)) {
  194 + for (i=0;i<subscriber.historyCommands.length;i++) {
  195 + var historyCommand = subscriber.historyCommands[i];
  196 + cmdId = nextCmdId();
  197 + subscribers[cmdId] = subscriber;
  198 + historyCommand.cmdId = cmdId;
  199 + commands[cmdId] = historyCommand;
  200 + cmdsWrapper.historyCmds.push(historyCommand);
  201 + }
  202 + }
  203 + subscribersCount++;
193 publishCommands(); 204 publishCommands();
194 } 205 }
195 206
196 function unsubscribe (subscriber) { 207 function unsubscribe (subscriber) {
197 if (isActive) { 208 if (isActive) {
198 var cmdId = null; 209 var cmdId = null;
199 - if (subscriber.subscriptionCommand) {  
200 - subscriber.subscriptionCommand.unsubscribe = true;  
201 - if (subscriber.type === types.dataKeyType.timeseries) {  
202 - cmdsWrapper.tsSubCmds.push(subscriber.subscriptionCommand);  
203 - } else if (subscriber.type === types.dataKeyType.attribute) {  
204 - cmdsWrapper.attrSubCmds.push(subscriber.subscriptionCommand); 210 + if (subscriber.subscriptionCommands) {
  211 + for (var i=0;i<subscriber.subscriptionCommands.length;i++) {
  212 + var subscriptionCommand = subscriber.subscriptionCommands[i];
  213 + subscriptionCommand.unsubscribe = true;
  214 + if (subscriber.type === types.dataKeyType.timeseries) {
  215 + cmdsWrapper.tsSubCmds.push(subscriptionCommand);
  216 + } else if (subscriber.type === types.dataKeyType.attribute) {
  217 + cmdsWrapper.attrSubCmds.push(subscriptionCommand);
  218 + }
  219 + cmdId = subscriptionCommand.cmdId;
  220 + if (cmdId) {
  221 + if (subscribers[cmdId]) {
  222 + delete subscribers[cmdId];
  223 + }
  224 + if (commands[cmdId]) {
  225 + delete commands[cmdId];
  226 + }
  227 + }
205 } 228 }
206 - cmdId = subscriber.subscriptionCommand.cmdId;  
207 - } else if (subscriber.historyCommand) {  
208 - cmdId = subscriber.historyCommand.cmdId;  
209 } 229 }
210 - if (cmdId && subscribers[cmdId]) {  
211 - delete subscribers[cmdId];  
212 - subscribersCount--; 230 + if (subscriber.historyCommands) {
  231 + for (i=0;i<subscriber.historyCommands.length;i++) {
  232 + var historyCommand = subscriber.historyCommands[i];
  233 + cmdId = historyCommand.cmdId;
  234 + if (cmdId) {
  235 + if (subscribers[cmdId]) {
  236 + delete subscribers[cmdId];
  237 + }
  238 + if (commands[cmdId]) {
  239 + delete commands[cmdId];
  240 + }
  241 + }
  242 + }
213 } 243 }
  244 + subscribersCount--;
214 publishCommands(); 245 publishCommands();
215 } 246 }
216 } 247 }
@@ -268,6 +299,7 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty @@ -268,6 +299,7 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty
268 lastCmdId = 0; 299 lastCmdId = 0;
269 subscribers = {}; 300 subscribers = {};
270 subscribersCount = 0; 301 subscribersCount = 0;
  302 + commands = {};
271 cmdsWrapper.tsSubCmds = []; 303 cmdsWrapper.tsSubCmds = [];
272 cmdsWrapper.historyCmds = []; 304 cmdsWrapper.historyCmds = [];
273 cmdsWrapper.attrSubCmds = []; 305 cmdsWrapper.attrSubCmds = [];
@@ -261,7 +261,7 @@ function TimeService($translate, types) { @@ -261,7 +261,7 @@ function TimeService($translate, types) {
261 return historyTimewindow; 261 return historyTimewindow;
262 } 262 }
263 263
264 - function createSubscriptionTimewindow(timewindow, stDiff, steppedChart) { 264 + function createSubscriptionTimewindow(timewindow, stDiff, stateData) {
265 265
266 var subscriptionTimewindow = { 266 var subscriptionTimewindow = {
267 fixedWindow: null, 267 fixedWindow: null,
@@ -273,12 +273,12 @@ function TimeService($translate, types) { @@ -273,12 +273,12 @@ function TimeService($translate, types) {
273 } 273 }
274 }; 274 };
275 var aggTimewindow = 0; 275 var aggTimewindow = 0;
276 - if (steppedChart) { 276 + if (stateData) {
277 subscriptionTimewindow.aggregation = { 277 subscriptionTimewindow.aggregation = {
278 interval: SECOND, 278 interval: SECOND,
279 limit: MAX_LIMIT, 279 limit: MAX_LIMIT,
280 type: types.aggregation.none.value, 280 type: types.aggregation.none.value,
281 - steppedChart: true 281 + stateData: true
282 }; 282 };
283 } else { 283 } else {
284 subscriptionTimewindow.aggregation = { 284 subscriptionTimewindow.aggregation = {
@@ -288,7 +288,7 @@ function TimeService($translate, types) { @@ -288,7 +288,7 @@ function TimeService($translate, types) {
288 }; 288 };
289 } 289 }
290 290
291 - if (angular.isDefined(timewindow.aggregation) && !steppedChart) { 291 + if (angular.isDefined(timewindow.aggregation) && !stateData) {
292 subscriptionTimewindow.aggregation = { 292 subscriptionTimewindow.aggregation = {
293 type: timewindow.aggregation.type || types.aggregation.avg.value, 293 type: timewindow.aggregation.type || types.aggregation.avg.value,
294 limit: timewindow.aggregation.limit || AVG_LIMIT 294 limit: timewindow.aggregation.limit || AVG_LIMIT
@@ -561,7 +561,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, $tr @@ -561,7 +561,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, $tr
561 maxDatasources: -1, //unlimited 561 maxDatasources: -1, //unlimited
562 maxDataKeys: -1, //unlimited 562 maxDataKeys: -1, //unlimited
563 dataKeysOptional: false, 563 dataKeysOptional: false,
564 - steppedChart: false 564 + stateData: false
565 }; 565 };
566 ' }\n\n' + 566 ' }\n\n' +
567 567
@@ -632,8 +632,8 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, $tr @@ -632,8 +632,8 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, $tr
632 if (angular.isUndefined(result.typeParameters.dataKeysOptional)) { 632 if (angular.isUndefined(result.typeParameters.dataKeysOptional)) {
633 result.typeParameters.dataKeysOptional = false; 633 result.typeParameters.dataKeysOptional = false;
634 } 634 }
635 - if (angular.isUndefined(result.typeParameters.steppedChart)) {  
636 - result.typeParameters.steppedChart = false; 635 + if (angular.isUndefined(result.typeParameters.stateData)) {
  636 + result.typeParameters.stateData = false;
637 } 637 }
638 if (angular.isFunction(widgetTypeInstance.actionSources)) { 638 if (angular.isFunction(widgetTypeInstance.actionSources)) {
639 result.actionSources = widgetTypeInstance.actionSources(); 639 result.actionSources = widgetTypeInstance.actionSources();
@@ -340,7 +340,7 @@ export default function WidgetController($scope, $state, $timeout, $window, $ele @@ -340,7 +340,7 @@ export default function WidgetController($scope, $state, $timeout, $window, $ele
340 if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) { 340 if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) {
341 options = { 341 options = {
342 type: widget.type, 342 type: widget.type,
343 - steppedChart: vm.typeParameters.steppedChart 343 + stateData: vm.typeParameters.stateData
344 } 344 }
345 if (widget.type == types.widgetType.alarm.value) { 345 if (widget.type == types.widgetType.alarm.value) {
346 options.alarmSource = angular.copy(widget.config.alarmSource); 346 options.alarmSource = angular.copy(widget.config.alarmSource);
@@ -55,7 +55,7 @@ export default class TbFlot { @@ -55,7 +55,7 @@ export default class TbFlot {
55 55
56 var tbFlot = this; 56 var tbFlot = this;
57 57
58 - function seriesInfoDiv(label, color, value, units, trackDecimals, active, percent) { 58 + function seriesInfoDiv(label, color, value, units, trackDecimals, active, percent, valueFormatFunction) {
59 var divElement = $('<div></div>'); 59 var divElement = $('<div></div>');
60 divElement.css({ 60 divElement.css({
61 display: "flex", 61 display: "flex",
@@ -83,7 +83,12 @@ export default class TbFlot { @@ -83,7 +83,12 @@ export default class TbFlot {
83 }); 83 });
84 } 84 }
85 divElement.append(labelSpan); 85 divElement.append(labelSpan);
86 - var valueContent = tbFlot.ctx.utils.formatValue(value, trackDecimals, units); 86 + var valueContent;
  87 + if (valueFormatFunction) {
  88 + valueContent = valueFormatFunction(value);
  89 + } else {
  90 + valueContent = tbFlot.ctx.utils.formatValue(value, trackDecimals, units);
  91 + }
87 if (angular.isNumber(percent)) { 92 if (angular.isNumber(percent)) {
88 valueContent += ' (' + Math.round(percent) + ' %)'; 93 valueContent += ' (' + Math.round(percent) + ' %)';
89 } 94 }
@@ -107,7 +112,7 @@ export default class TbFlot { @@ -107,7 +112,7 @@ export default class TbFlot {
107 var units = item.series.dataKey.units && item.series.dataKey.units.length ? item.series.dataKey.units : tbFlot.ctx.trackUnits; 112 var units = item.series.dataKey.units && item.series.dataKey.units.length ? item.series.dataKey.units : tbFlot.ctx.trackUnits;
108 var decimals = angular.isDefined(item.series.dataKey.decimals) ? item.series.dataKey.decimals : tbFlot.ctx.trackDecimals; 113 var decimals = angular.isDefined(item.series.dataKey.decimals) ? item.series.dataKey.decimals : tbFlot.ctx.trackDecimals;
109 var divElement = seriesInfoDiv(item.series.dataKey.label, item.series.dataKey.color, 114 var divElement = seriesInfoDiv(item.series.dataKey.label, item.series.dataKey.color,
110 - item.datapoint[1][0][1], units, decimals, true, item.series.percent); 115 + item.datapoint[1][0][1], units, decimals, true, item.series.percent, item.series.dataKey.tooltipValueFormatFunction);
111 return divElement.prop('outerHTML'); 116 return divElement.prop('outerHTML');
112 }; 117 };
113 } else { 118 } else {
@@ -132,7 +137,7 @@ export default class TbFlot { @@ -132,7 +137,7 @@ export default class TbFlot {
132 var units = seriesHoverInfo.units && seriesHoverInfo.units.length ? seriesHoverInfo.units : tbFlot.ctx.trackUnits; 137 var units = seriesHoverInfo.units && seriesHoverInfo.units.length ? seriesHoverInfo.units : tbFlot.ctx.trackUnits;
133 var decimals = angular.isDefined(seriesHoverInfo.decimals) ? seriesHoverInfo.decimals : tbFlot.ctx.trackDecimals; 138 var decimals = angular.isDefined(seriesHoverInfo.decimals) ? seriesHoverInfo.decimals : tbFlot.ctx.trackDecimals;
134 var divElement = seriesInfoDiv(seriesHoverInfo.label, seriesHoverInfo.color, 139 var divElement = seriesInfoDiv(seriesHoverInfo.label, seriesHoverInfo.color,
135 - seriesHoverInfo.value, units, decimals, seriesHoverInfo.index === seriesIndex); 140 + seriesHoverInfo.value, units, decimals, seriesHoverInfo.index === seriesIndex, null, seriesHoverInfo.tooltipValueFormatFunction);
136 content += divElement.prop('outerHTML'); 141 content += divElement.prop('outerHTML');
137 } 142 }
138 return content; 143 return content;
@@ -168,7 +173,7 @@ export default class TbFlot { @@ -168,7 +173,7 @@ export default class TbFlot {
168 } 173 }
169 }; 174 };
170 175
171 - if (this.chartType === 'line' || this.chartType === 'bar' || this.chartType === 'stepped') { 176 + if (this.chartType === 'line' || this.chartType === 'bar' || this.chartType === 'state') {
172 options.xaxis = { 177 options.xaxis = {
173 mode: 'time', 178 mode: 'time',
174 timezone: 'browser', 179 timezone: 'browser',
@@ -196,6 +201,9 @@ export default class TbFlot { @@ -196,6 +201,9 @@ export default class TbFlot {
196 if (settings.yaxis && settings.yaxis.showLabels === false) { 201 if (settings.yaxis && settings.yaxis.showLabels === false) {
197 return ''; 202 return '';
198 } 203 }
  204 + if (this.ticksFormatterFunction) {
  205 + return this.ticksFormatterFunction(value);
  206 + }
199 var factor = this.tickDecimals ? Math.pow(10, this.tickDecimals) : 1, 207 var factor = this.tickDecimals ? Math.pow(10, this.tickDecimals) : 1,
200 formatted = "" + Math.round(value * factor) / factor; 208 formatted = "" + Math.round(value * factor) / factor;
201 if (this.tickDecimals != null) { 209 if (this.tickDecimals != null) {
@@ -218,6 +226,13 @@ export default class TbFlot { @@ -218,6 +226,13 @@ export default class TbFlot {
218 this.yaxis.labelFont.color = this.yaxis.font.color; 226 this.yaxis.labelFont.color = this.yaxis.font.color;
219 this.yaxis.labelFont.size = this.yaxis.font.size+2; 227 this.yaxis.labelFont.size = this.yaxis.font.size+2;
220 this.yaxis.labelFont.weight = "bold"; 228 this.yaxis.labelFont.weight = "bold";
  229 + if (settings.yaxis.ticksFormatter && settings.yaxis.ticksFormatter.length) {
  230 + try {
  231 + this.yaxis.ticksFormatterFunction = new Function('value', settings.yaxis.ticksFormatter);
  232 + } catch (e) {
  233 + this.yaxis.ticksFormatterFunction = null;
  234 + }
  235 + }
221 } 236 }
222 237
223 options.grid.borderWidth = 1; 238 options.grid.borderWidth = 1;
@@ -271,7 +286,7 @@ export default class TbFlot { @@ -271,7 +286,7 @@ export default class TbFlot {
271 } 286 }
272 } 287 }
273 288
274 - if (this.chartType === 'stepped') { 289 + if (this.chartType === 'state') {
275 options.series.lines = { 290 options.series.lines = {
276 steps: true, 291 steps: true,
277 show: true 292 show: true
@@ -331,11 +346,28 @@ export default class TbFlot { @@ -331,11 +346,28 @@ export default class TbFlot {
331 var colors = []; 346 var colors = [];
332 this.yaxes = []; 347 this.yaxes = [];
333 var yaxesMap = {}; 348 var yaxesMap = {};
  349 +
  350 + var tooltipValueFormatFunction = null;
  351 + if (this.ctx.settings.tooltipValueFormatter && this.ctx.settings.tooltipValueFormatter.length) {
  352 + try {
  353 + tooltipValueFormatFunction = new Function('value', this.ctx.settings.tooltipValueFormatter);
  354 + } catch (e) {
  355 + tooltipValueFormatFunction = null;
  356 + }
  357 + }
  358 +
334 for (var i = 0; i < this.subscription.data.length; i++) { 359 for (var i = 0; i < this.subscription.data.length; i++) {
335 var series = this.subscription.data[i]; 360 var series = this.subscription.data[i];
336 colors.push(series.dataKey.color); 361 colors.push(series.dataKey.color);
337 var keySettings = series.dataKey.settings; 362 var keySettings = series.dataKey.settings;
338 - 363 + series.dataKey.tooltipValueFormatFunction = tooltipValueFormatFunction;
  364 + if (keySettings.tooltipValueFormatter && keySettings.tooltipValueFormatter.length) {
  365 + try {
  366 + series.dataKey.tooltipValueFormatFunction = new Function('value', keySettings.tooltipValueFormatter);
  367 + } catch (e) {
  368 + series.dataKey.tooltipValueFormatFunction = tooltipValueFormatFunction;
  369 + }
  370 + }
339 series.lines = { 371 series.lines = {
340 fill: keySettings.fillLines === true, 372 fill: keySettings.fillLines === true,
341 show: this.chartType === 'line' ? keySettings.showLines !== false : keySettings.showLines === true 373 show: this.chartType === 'line' ? keySettings.showLines !== false : keySettings.showLines === true
@@ -389,7 +421,7 @@ export default class TbFlot { @@ -389,7 +421,7 @@ export default class TbFlot {
389 421
390 this.options.colors = colors; 422 this.options.colors = colors;
391 this.options.yaxes = angular.copy(this.yaxes); 423 this.options.yaxes = angular.copy(this.yaxes);
392 - if (this.chartType === 'line' || this.chartType === 'bar' || this.chartType === 'stepped') { 424 + if (this.chartType === 'line' || this.chartType === 'bar' || this.chartType === 'state') {
393 if (this.chartType === 'bar') { 425 if (this.chartType === 'bar') {
394 this.options.series.bars.barWidth = this.subscription.timeWindow.interval * 0.6; 426 this.options.series.bars.barWidth = this.subscription.timeWindow.interval * 0.6;
395 } 427 }
@@ -432,6 +464,14 @@ export default class TbFlot { @@ -432,6 +464,14 @@ export default class TbFlot {
432 yaxis.position = position; 464 yaxis.position = position;
433 465
434 yaxis.keysInfo = []; 466 yaxis.keysInfo = [];
  467 +
  468 + if (keySettings.axisTicksFormatter && keySettings.axisTicksFormatter.length) {
  469 + try {
  470 + yaxis.ticksFormatterFunction = new Function('value', keySettings.axisTicksFormatter);
  471 + } catch (e) {
  472 + yaxis.ticksFormatterFunction = this.yaxis.ticksFormatterFunction;
  473 + }
  474 + }
435 return yaxis; 475 return yaxis;
436 } 476 }
437 477
@@ -442,7 +482,7 @@ export default class TbFlot { @@ -442,7 +482,7 @@ export default class TbFlot {
442 } 482 }
443 if (this.subscription) { 483 if (this.subscription) {
444 if (!this.isMouseInteraction && this.ctx.plot) { 484 if (!this.isMouseInteraction && this.ctx.plot) {
445 - if (this.chartType === 'line' || this.chartType === 'bar' || this.chartType === 'stepped') { 485 + if (this.chartType === 'line' || this.chartType === 'bar' || this.chartType === 'state') {
446 486
447 var axisVisibilityChanged = false; 487 var axisVisibilityChanged = false;
448 if (this.yaxis) { 488 if (this.yaxis) {
@@ -654,6 +694,11 @@ export default class TbFlot { @@ -654,6 +694,11 @@ export default class TbFlot {
654 "type": "boolean", 694 "type": "boolean",
655 "default": false 695 "default": false
656 }, 696 },
  697 + "tooltipValueFormatter": {
  698 + "title": "Tooltip value format function, f(value)",
  699 + "type": "string",
  700 + "default": ""
  701 + },
657 "grid": { 702 "grid": {
658 "title": "Grid settings", 703 "title": "Grid settings",
659 "type": "object", 704 "type": "object",
@@ -739,6 +784,11 @@ export default class TbFlot { @@ -739,6 +784,11 @@ export default class TbFlot {
739 "title": "Ticks color", 784 "title": "Ticks color",
740 "type": "string", 785 "type": "string",
741 "default": null 786 "default": null
  787 + },
  788 + "ticksFormatter": {
  789 + "title": "Ticks formatter function, f(value)",
  790 + "type": "string",
  791 + "default": ""
742 } 792 }
743 } 793 }
744 } 794 }
@@ -757,6 +807,10 @@ export default class TbFlot { @@ -757,6 +807,10 @@ export default class TbFlot {
757 "tooltipIndividual", 807 "tooltipIndividual",
758 "tooltipCumulative", 808 "tooltipCumulative",
759 { 809 {
  810 + "key": "tooltipValueFormatter",
  811 + "type": "javascript"
  812 + },
  813 + {
760 "key": "grid", 814 "key": "grid",
761 "items": [ 815 "items": [
762 { 816 {
@@ -797,6 +851,10 @@ export default class TbFlot { @@ -797,6 +851,10 @@ export default class TbFlot {
797 { 851 {
798 "key": "yaxis.color", 852 "key": "yaxis.color",
799 "type": "color" 853 "type": "color"
  854 + },
  855 + {
  856 + "key": "yaxis.ticksFormatter",
  857 + "type": "javascript"
800 } 858 }
801 ] 859 ]
802 } 860 }
@@ -830,6 +888,11 @@ export default class TbFlot { @@ -830,6 +888,11 @@ export default class TbFlot {
830 "type": "boolean", 888 "type": "boolean",
831 "default": false 889 "default": false
832 }, 890 },
  891 + "tooltipValueFormatter": {
  892 + "title": "Tooltip value format function, f(value)",
  893 + "type": "string",
  894 + "default": ""
  895 + },
833 "showSeparateAxis": { 896 "showSeparateAxis": {
834 "title": "Show separate axis", 897 "title": "Show separate axis",
835 "type": "boolean", 898 "type": "boolean",
@@ -849,6 +912,11 @@ export default class TbFlot { @@ -849,6 +912,11 @@ export default class TbFlot {
849 "title": "Axis position", 912 "title": "Axis position",
850 "type": "string", 913 "type": "string",
851 "default": "left" 914 "default": "left"
  915 + },
  916 + "axisTicksFormatter": {
  917 + "title": "Ticks formatter function, f(value)",
  918 + "type": "string",
  919 + "default": ""
852 } 920 }
853 }, 921 },
854 "required": ["showLines", "fillLines", "showPoints"] 922 "required": ["showLines", "fillLines", "showPoints"]
@@ -857,6 +925,10 @@ export default class TbFlot { @@ -857,6 +925,10 @@ export default class TbFlot {
857 "showLines", 925 "showLines",
858 "fillLines", 926 "fillLines",
859 "showPoints", 927 "showPoints",
  928 + {
  929 + "key": "tooltipValueFormatter",
  930 + "type": "javascript"
  931 + },
860 "showSeparateAxis", 932 "showSeparateAxis",
861 "axisTitle", 933 "axisTitle",
862 "axisTickDecimals", 934 "axisTickDecimals",
@@ -874,8 +946,11 @@ export default class TbFlot { @@ -874,8 +946,11 @@ export default class TbFlot {
874 "label": "Right" 946 "label": "Right"
875 } 947 }
876 ] 948 ]
  949 + },
  950 + {
  951 + "key": "axisTicksFormatter",
  952 + "type": "javascript"
877 } 953 }
878 -  
879 ] 954 ]
880 } 955 }
881 } 956 }
@@ -1129,6 +1204,7 @@ export default class TbFlot { @@ -1129,6 +1204,7 @@ export default class TbFlot {
1129 label: series.dataKey.label, 1204 label: series.dataKey.label,
1130 units: series.dataKey.units, 1205 units: series.dataKey.units,
1131 decimals: series.dataKey.decimals, 1206 decimals: series.dataKey.decimals,
  1207 + tooltipValueFormatFunction: series.dataKey.tooltipValueFormatFunction,
1132 time: pointTime, 1208 time: pointTime,
1133 distance: hoverDistance, 1209 distance: hoverDistance,
1134 index: i 1210 index: i
@@ -130,15 +130,22 @@ function LedIndicatorController($element, $scope, $timeout, utils, types) { @@ -130,15 +130,22 @@ function LedIndicatorController($element, $scope, $timeout, utils, types) {
130 } 130 }
131 } 131 }
132 132
133 - vm.checkStatusMethod = 'checkStatus';  
134 - if (vm.ctx.settings.checkStatusMethod && vm.ctx.settings.checkStatusMethod.length) {  
135 - vm.checkStatusMethod = vm.ctx.settings.checkStatusMethod; 133 + vm.performCheckStatus = vm.ctx.settings.performCheckStatus != false;
  134 + if (vm.performCheckStatus) {
  135 + vm.checkStatusMethod = 'checkStatus';
  136 + if (vm.ctx.settings.checkStatusMethod && vm.ctx.settings.checkStatusMethod.length) {
  137 + vm.checkStatusMethod = vm.ctx.settings.checkStatusMethod;
  138 + }
136 } 139 }
137 if (!rpcEnabled) { 140 if (!rpcEnabled) {
138 onError('Target device is not set!'); 141 onError('Target device is not set!');
139 } else { 142 } else {
140 if (!vm.isSimulated) { 143 if (!vm.isSimulated) {
141 - rpcCheckStatus(); 144 + if (vm.performCheckStatus) {
  145 + rpcCheckStatus();
  146 + } else {
  147 + subscribeForValue();
  148 + }
142 } 149 }
143 } 150 }
144 } 151 }