thingsboard-aplcache

Details

diff --git a/application/src/main/data/json/system/widget_bundles/charts.json b/application/src/main/data/json/system/widget_bundles/charts.json
index daa1967..a264ba6 100644
--- a/application/src/main/data/json/system/widget_bundles/charts.json
+++ b/application/src/main/data/json/system/widget_bundles/charts.json
@@ -152,6 +152,22 @@
         "dataKeySettingsSchema": "{}",
         "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}"
       }
+    },
+    {
+      "alias": "state_chart",
+      "name": "State Chart",
+      "descriptor": {
+        "type": "timeseries",
+        "sizeX": 8,
+        "sizeY": 5,
+        "resources": [],
+        "templateHtml": "",
+        "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",
+        "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",
+        "settingsSchema": "{}",
+        "dataKeySettingsSchema": "{}",
+        "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}}"
+      }
     }
   ]
 }
\ No newline at end of file
diff --git a/application/src/main/data/json/system/widget_bundles/control_widgets.json b/application/src/main/data/json/system/widget_bundles/control_widgets.json
index 6f4685c..716e48a 100644
--- a/application/src/main/data/json/system/widget_bundles/control_widgets.json
+++ b/application/src/main/data/json/system/widget_bundles/control_widgets.json
@@ -96,9 +96,9 @@
         "templateHtml": "<tb-led-indicator ctx='ctx'></tb-led-indicator>",
         "templateCss": "",
         "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",
-        "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}",
+        "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}",
         "dataKeySettingsSchema": "{}\n",
-        "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}"
+        "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}"
       }
     }
   ]
diff --git a/ui/src/app/api/attribute.service.js b/ui/src/app/api/attribute.service.js
index 53d5341..3f4b971 100644
--- a/ui/src/app/api/attribute.service.js
+++ b/ui/src/app/api/attribute.service.js
@@ -186,7 +186,7 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService) 
                 types.dataKeyType.timeseries : types.dataKeyType.attribute;
 
             var subscriber = {
-                subscriptionCommand: subscriptionCommand,
+                subscriptionCommands: [subscriptionCommand],
                 type: type,
                 onData: function (data) {
                     if (data.data) {
diff --git a/ui/src/app/api/data-aggregator.js b/ui/src/app/api/data-aggregator.js
index 43b2e93..64e4e40 100644
--- a/ui/src/app/api/data-aggregator.js
+++ b/ui/src/app/api/data-aggregator.js
@@ -17,7 +17,7 @@
 export default class DataAggregator {
 
     constructor(onDataCb, tsKeyNames, startTs, limit, aggregationType, timeWindow, interval,
-                steppedChart, types, $timeout, $filter) {
+                stateData, types, $timeout, $filter) {
         this.onDataCb = onDataCb;
         this.tsKeyNames = tsKeyNames;
         this.dataBuffer = {};
@@ -35,8 +35,10 @@ export default class DataAggregator {
         this.limit = limit;
         this.timeWindow = timeWindow;
         this.interval = interval;
-        this.steppedChart = steppedChart;
-        this.firstStepDataReceived = !this.steppedChart;
+        this.stateData = stateData;
+        if (this.stateData) {
+            this.lastPrevKvPairData = {};
+        }
         this.aggregationTimeout = Math.max(this.interval, 1000);
         switch (aggregationType) {
             case types.aggregation.min.value:
@@ -81,10 +83,6 @@ export default class DataAggregator {
         }, this.aggregationTimeout, false);
     }
 
-    onFirstStepData(data) {
-        this.firstStepData = data;
-    }
-
     onData(data, update, history, apply) {
         if (!this.dataReceived || this.resetPending) {
             var updateIntervalScheduledTime = true;
@@ -158,6 +156,10 @@ export default class DataAggregator {
             var keyData = this.dataBuffer[key];
             for (var aggTimestamp in aggKeyData) {
                 if (aggTimestamp <= this.startTs) {
+                    if (this.stateData &&
+                        (!this.lastPrevKvPairData[key] || this.lastPrevKvPairData[key][0] < aggTimestamp)) {
+                        this.lastPrevKvPairData[key] = [Number(aggTimestamp), aggKeyData[aggTimestamp].aggValue];
+                    }
                     delete aggKeyData[aggTimestamp];
                 } else if (aggTimestamp <= this.endTs) {
                     var aggData = aggKeyData[aggTimestamp];
@@ -166,6 +168,9 @@ export default class DataAggregator {
                 }
             }
             keyData = this.$filter('orderBy')(keyData, '+this[0]');
+            if (this.stateData) {
+                this.updateStateBounds(keyData, angular.copy(this.lastPrevKvPairData[key]));
+            }
             if (keyData.length > this.limit) {
                 keyData = keyData.slice(keyData.length - this.limit);
             }
@@ -174,6 +179,34 @@ export default class DataAggregator {
         return this.dataBuffer;
     }
 
+    updateStateBounds(keyData, lastPrevKvPair) {
+        if (lastPrevKvPair) {
+            lastPrevKvPair[0] = this.startTs;
+        }
+        var firstKvPair;
+        if (!keyData.length) {
+            if (lastPrevKvPair) {
+                firstKvPair = lastPrevKvPair;
+                keyData.push(firstKvPair);
+            }
+        } else {
+            firstKvPair = keyData[0];
+        }
+        if (firstKvPair && firstKvPair[0] > this.startTs) {
+            if (lastPrevKvPair) {
+                keyData.unshift(lastPrevKvPair);
+            }
+        }
+        if (keyData.length) {
+            var lastKvPair = keyData[keyData.length-1];
+            if (lastKvPair[0] < this.endTs) {
+                lastKvPair = angular.copy(lastKvPair);
+                lastKvPair[0] = this.endTs;
+                keyData.push(lastKvPair);
+            }
+        }
+    }
+
     destroy() {
         if (this.intervalTimeoutHandle) {
             this.$timeout.cancel(this.intervalTimeoutHandle);
diff --git a/ui/src/app/api/datasource.service.js b/ui/src/app/api/datasource.service.js
index a68e14c..a01704c 100644
--- a/ui/src/app/api/datasource.service.js
+++ b/ui/src/app/api/datasource.service.js
@@ -105,7 +105,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
     var datasourceType = datasourceSubscription.datasourceType;
     var datasourceData = {};
     var dataKeys = {};
-    var subscribers = {};
+    var subscribers = [];
     var history = datasourceSubscription.subscriptionTimewindow &&
         datasourceSubscription.subscriptionTimewindow.fixedWindow;
     var realtime = datasourceSubscription.subscriptionTimewindow &&
@@ -249,7 +249,6 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
             if (tsKeys.length > 0) {
 
                 var subscriber;
-                var subscriptionCommand;
 
                 if (history) {
 
@@ -265,45 +264,103 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
                     };
 
                     subscriber = {
-                        historyCommand: historyCommand,
+                        historyCommands: [ historyCommand ],
                         type: types.dataKeyType.timeseries,
-                        onData: function (data) {
-                            if (data.data) {
-                                for (var key in data.data) {
+                        subsTw: subsTw
+                    };
+
+                    if (subsTw.aggregation.stateData) {
+                        subscriber.firstStateHistoryCommand = createFirstStateHistoryCommand(subsTw.fixedWindow.startTimeMs, tsKeys);
+                        subscriber.historyCommands.push(subscriber.firstStateHistoryCommand);
+                    }
+
+                    subscriber.onData = function (data, subscriptionId) {
+                        if (this.subsTw.aggregation.stateData &&
+                            this.firstStateHistoryCommand && this.firstStateHistoryCommand.cmdId == subscriptionId) {
+                            if (this.data) {
+                                onStateHistoryData(data, this.data, this.subsTw.aggregation.limit,
+                                    subsTw.fixedWindow.startTimeMs, this.subsTw.fixedWindow.endTimeMs,
+                                    (data) => {
+                                        onData(data.data, types.dataKeyType.timeseries, true);
+                                    });
+                            } else {
+                                this.firstStateData = data;
+                            }
+                        } else {
+                            if (this.subsTw.aggregation.stateData) {
+                                if (this.firstStateData) {
+                                    onStateHistoryData(this.firstStateData, data, this.subsTw.aggregation.limit,
+                                        this.subsTw.fixedWindow.startTimeMs, this.subsTw.fixedWindow.endTimeMs,
+                                        (data) => {
+                                            onData(data.data, types.dataKeyType.timeseries, true);
+                                        });
+                                } else {
+                                    this.data = data;
+                                }
+                            } else {
+                                for (key in data.data) {
                                     var keyData = data.data[key];
                                     data.data[key] = $filter('orderBy')(keyData, '+this[0]');
                                 }
                                 onData(data.data, types.dataKeyType.timeseries, true);
                             }
-                        },
-                        onReconnected: function() {}
+                        }
                     };
-
+                    subscriber.onReconnected = function() {};
                     telemetryWebsocketService.subscribe(subscriber);
-                    subscribers[subscriber.historyCommand.cmdId] = subscriber;
-
-                    if (subsTw.aggregation.steppedChart) {
-                        createFirstStepSubscription(subsTw, tsKeys);
-                    }
+                    subscribers.push(subscriber);
 
                 } else {
 
-                    subscriptionCommand = {
+                    var subscriptionCommand = {
                         entityType: datasourceSubscription.entityType,
                         entityId: datasourceSubscription.entityId,
                         keys: tsKeys
                     };
 
                     subscriber = {
-                        subscriptionCommand: subscriptionCommand,
+                        subscriptionCommands: [subscriptionCommand],
                         type: types.dataKeyType.timeseries
                     };
 
                     if (datasourceSubscription.type === types.widgetType.timeseries.value) {
+                        subscriber.subsTw = subsTw;
                         updateRealtimeSubscriptionCommand(subscriptionCommand, subsTw);
+
+                        if (subsTw.aggregation.stateData) {
+                            subscriber.firstStateSubscriptionCommand = createFirstStateHistoryCommand(subsTw.startTs, tsKeys);
+                            subscriber.historyCommands = [subscriber.firstStateSubscriptionCommand];
+                        }
                         dataAggregator = createRealtimeDataAggregator(subsTw, tsKeyNames, types.dataKeyType.timeseries);
-                        subscriber.onData = function(data) {
-                            dataAggregator.onData(data, false, false, true);
+                        subscriber.onData = function(data, subscriptionId) {
+                            if (this.subsTw.aggregation.stateData &&
+                                this.firstStateSubscriptionCommand && this.firstStateSubscriptionCommand.cmdId == subscriptionId) {
+                                if (this.data) {
+                                    onStateHistoryData(data, this.data, this.subsTw.aggregation.limit,
+                                        this.subsTw.startTs, this.subsTw.startTs + this.subsTw.aggregation.timeWindow,
+                                        (data) => {
+                                            dataAggregator.onData(data, false, false, true);
+                                        });
+                                    this.stateDataReceived = true;
+                                } else {
+                                    this.firstStateData = data;
+                                }
+                            } else {
+                                if (this.subsTw.aggregation.stateData && !this.stateDataReceived) {
+                                    if (this.firstStateData) {
+                                        onStateHistoryData(this.firstStateData, data, this.subsTw.aggregation.limit,
+                                            this.subsTw.startTs, this.subsTw.startTs + this.subsTw.aggregation.timeWindow,
+                                            (data) => {
+                                                dataAggregator.onData(data, false, false, true);
+                                            });
+                                        this.stateDataReceived = true;
+                                    } else {
+                                        this.data = data;
+                                    }
+                                } else {
+                                    dataAggregator.onData(data, false, false, true);
+                                }
+                            }
                         }
                         subscriber.onReconnected = function() {
                             var newSubsTw = null;
@@ -315,14 +372,16 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
                                     listener.setRealtimeSubscription(newSubsTw);
                                 }
                             }
-                            updateRealtimeSubscriptionCommand(this.subscriptionCommand, newSubsTw);
+                            this.subsTw = newSubsTw;
+                            this.firstStateData = null;
+                            this.data = null;
+                            this.stateDataReceived = false;
+                            updateRealtimeSubscriptionCommand(this.subscriptionCommands[0], this.subsTw);
+                            if (this.subsTw.aggregation.stateData) {
+                                updateFirstStateHistoryCommand(this.firstStateSubscriptionCommand, this.subsTw.startTs);
+                            }
                             dataAggregator.reset(newSubsTw.startTs,  newSubsTw.aggregation.timeWindow, newSubsTw.aggregation.interval);
                         }
-
-                        if (subsTw.aggregation.steppedChart) {
-                            createFirstStepSubscription(subsTw, tsKeys);
-                        }
-
                     } else {
                         subscriber.onReconnected = function() {}
                         subscriber.onData = function(data) {
@@ -333,21 +392,21 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
                     }
 
                     telemetryWebsocketService.subscribe(subscriber);
-                    subscribers[subscriber.subscriptionCommand.cmdId] = subscriber;
+                    subscribers.push(subscriber);
 
                 }
             }
 
             if (attrKeys.length > 0) {
 
-                subscriptionCommand = {
+                var attrsSubscriptionCommand = {
                     entityType: datasourceSubscription.entityType,
                     entityId: datasourceSubscription.entityId,
                     keys: attrKeys
                 };
 
                 subscriber = {
-                    subscriptionCommand: subscriptionCommand,
+                    subscriptionCommands: [attrsSubscriptionCommand],
                     type: types.dataKeyType.attribute,
                     onData: function (data) {
                         if (data.data) {
@@ -358,7 +417,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
                 };
 
                 telemetryWebsocketService.subscribe(subscriber);
-                subscribers[subscriber.cmdId] = subscriber;
+                subscribers.push(subscriber);
 
             }
 
@@ -388,35 +447,47 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
         }
     }
 
-    function createFirstStepSubscription(subsTw, tsKeys) {
-        var startStepCommand = {
+    function createFirstStateHistoryCommand(startTs, tsKeys) {
+        return {
             entityType: datasourceSubscription.entityType,
             entityId: datasourceSubscription.entityId,
             keys: tsKeys,
-            startTs: subsTw.fixedWindow.startTimeMs - YEAR,
-            endTs: subsTw.fixedWindow.startTimeMs,
-            interval: subsTw.aggregation.interval,
+            startTs: startTs - YEAR,
+            endTs: startTs,
+            interval: 1000,
             limit: 1,
-            agg: subsTw.aggregation.type
-        };
-        var subscriber = {
-            historyCommand: startStepCommand,
-            type: types.dataKeyType.timeseries,
-            onData: function (data) {
-                if (data.data) {
-                    for (var key in data.data) {
-                        var keyData = data.data[key];
-                        data.data[key] = $filter('orderBy')(keyData, '+this[0]');
-                    }
-                    //onData(data.data, types.dataKeyType.timeseries, true);
-                    //TODO: onStartStepData
-                }
-            },
-            onReconnected: function() {}
+            agg: types.aggregation.none.value
         };
+    }
+
+    function updateFirstStateHistoryCommand(stateHistoryCommand, startTs) {
+        stateHistoryCommand.startTs = startTs - YEAR;
+        stateHistoryCommand.endTs = startTs;
+    }
 
-        telemetryWebsocketService.subscribe(subscriber);
-        subscribers[subscriber.historyCommand.cmdId] = subscriber;
+    function onStateHistoryData(firstStateData, data, limit, startTs, endTs, onData) {
+        for (var key in data.data) {
+            var keyData = data.data[key];
+            data.data[key] = $filter('orderBy')(keyData, '+this[0]');
+            keyData = data.data[key];
+            if (keyData.length < limit) {
+                var firstStateKeyData = firstStateData.data[key];
+                if (firstStateKeyData.length) {
+                    var firstStateDataTsKv = firstStateKeyData[0];
+                    firstStateDataTsKv[0] = startTs;
+                    firstStateKeyData = [
+                        [ startTs, firstStateKeyData[0][1] ]
+                    ];
+                    keyData.unshift(firstStateDataTsKv);
+                }
+            }
+            if (keyData.length) {
+                var lastTsKv = angular.copy(keyData[keyData.length-1]);
+                lastTsKv[0] = endTs;
+                keyData.push(lastTsKv);
+            }
+        }
+        onData(data);
     }
 
     function createRealtimeDataAggregator(subsTw, tsKeyNames, dataKeyType) {
@@ -430,7 +501,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
             subsTw.aggregation.type,
             subsTw.aggregation.timeWindow,
             subsTw.aggregation.interval,
-            subsTw.aggregation.steppedChart,
+            subsTw.aggregation.stateData,
             types,
             $timeout,
             $filter
@@ -451,14 +522,14 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
             timer = null;
         }
         if (datasourceType === types.datasourceType.entity) {
-            for (var cmdId in subscribers) {
-                var subscriber = subscribers[cmdId];
+            for (var i=0;i<subscribers.length;i++) {
+                var subscriber = subscribers[i];
                 telemetryWebsocketService.unsubscribe(subscriber);
                 if (subscriber.onDestroy) {
                     subscriber.onDestroy();
                 }
             }
-            subscribers = {};
+            subscribers.length = 0;
         }
         if (dataAggregator) {
             dataAggregator.destroy();
diff --git a/ui/src/app/api/subscription.js b/ui/src/app/api/subscription.js
index a464617..ac8cefe 100644
--- a/ui/src/app/api/subscription.js
+++ b/ui/src/app/api/subscription.js
@@ -128,7 +128,7 @@ export default class Subscription {
                 stDiff: this.ctx.stDiff
             }
             this.useDashboardTimewindow = options.useDashboardTimewindow;
-            this.steppedChart = options.steppedChart;
+            this.stateData = options.stateData;
             if (this.useDashboardTimewindow) {
                 this.timeWindowConfig = angular.copy(options.dashboardTimewindow);
             } else {
@@ -612,7 +612,7 @@ export default class Subscription {
             this.subscriptionTimewindow =
                 this.ctx.timeService.createSubscriptionTimewindow(
                     this.timeWindowConfig,
-                    this.timeWindow.stDiff, this.steppedChart);
+                    this.timeWindow.stDiff, this.stateData);
         }
         this.updateTimewindow();
         return this.subscriptionTimewindow;
diff --git a/ui/src/app/api/telemetry-websocket.service.js b/ui/src/app/api/telemetry-websocket.service.js
index e69c264..74f7bfd 100644
--- a/ui/src/app/api/telemetry-websocket.service.js
+++ b/ui/src/app/api/telemetry-websocket.service.js
@@ -34,6 +34,7 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty
         lastCmdId = 0,
         subscribers = {},
         subscribersCount = 0,
+        commands = {},
         cmdsWrapper = {
             tsSubCmds: [],
             historyCmds: [],
@@ -120,7 +121,10 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty
             if (!isReconnect) {
                 reconnectSubscribers = [];
                 for (var id in subscribers) {
-                    reconnectSubscribers.push(subscribers[id]);
+                    var subscriber = subscribers[id];
+                    if (reconnectSubscribers.indexOf(subscriber) === -1) {
+                        reconnectSubscribers.push(subscriber);
+                    }
                 }
                 reset(false);
                 isReconnect = true;
@@ -138,7 +142,7 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty
             if (data.subscriptionId) {
                 var subscriber = subscribers[data.subscriptionId];
                 if (subscriber && data) {
-                    var keys = fetchKeys(subscriber);
+                    var keys = fetchKeys(data.subscriptionId);
                     if (!data.data) {
                         data.data = {};
                     }
@@ -148,20 +152,15 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty
                             data.data[key] = [];
                         }
                     }
-                    subscriber.onData(data);
+                    subscriber.onData(data, data.subscriptionId);
                 }
             }
         }
         checkToClose();
     }
 
-    function fetchKeys(subscriber) {
-        var command;
-        if (angular.isDefined(subscriber.subscriptionCommand)) {
-            command = subscriber.subscriptionCommand;
-        } else {
-            command = subscriber.historyCommand;
-        }
+    function fetchKeys(subscriptionId) {
+        var command = commands[subscriptionId];
         if (command && command.keys && command.keys.length > 0) {
             return command.keys.split(",");
         } else {
@@ -176,41 +175,73 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty
 
     function subscribe (subscriber) {
         isActive = true;
-        var cmdId = nextCmdId();
-        subscribers[cmdId] = subscriber;
-        subscribersCount++;
-        if (angular.isDefined(subscriber.subscriptionCommand)) {
-            subscriber.subscriptionCommand.cmdId = cmdId;
-            if (subscriber.type === types.dataKeyType.timeseries) {
-                cmdsWrapper.tsSubCmds.push(subscriber.subscriptionCommand);
-            } else if (subscriber.type === types.dataKeyType.attribute) {
-                cmdsWrapper.attrSubCmds.push(subscriber.subscriptionCommand);
+        var cmdId;
+        if (angular.isDefined(subscriber.subscriptionCommands)) {
+            for (var i=0;i<subscriber.subscriptionCommands.length;i++) {
+                var subscriptionCommand = subscriber.subscriptionCommands[i];
+                cmdId = nextCmdId();
+                subscribers[cmdId] = subscriber;
+                subscriptionCommand.cmdId = cmdId;
+                commands[cmdId] = subscriptionCommand;
+                if (subscriber.type === types.dataKeyType.timeseries) {
+                    cmdsWrapper.tsSubCmds.push(subscriptionCommand);
+                } else if (subscriber.type === types.dataKeyType.attribute) {
+                    cmdsWrapper.attrSubCmds.push(subscriptionCommand);
+                }
             }
-        } else if (angular.isDefined(subscriber.historyCommand)) {
-            subscriber.historyCommand.cmdId = cmdId;
-            cmdsWrapper.historyCmds.push(subscriber.historyCommand);
         }
+        if (angular.isDefined(subscriber.historyCommands)) {
+            for (i=0;i<subscriber.historyCommands.length;i++) {
+                var historyCommand = subscriber.historyCommands[i];
+                cmdId = nextCmdId();
+                subscribers[cmdId] = subscriber;
+                historyCommand.cmdId = cmdId;
+                commands[cmdId] = historyCommand;
+                cmdsWrapper.historyCmds.push(historyCommand);
+            }
+        }
+        subscribersCount++;
         publishCommands();
     }
 
     function unsubscribe (subscriber) {
         if (isActive) {
             var cmdId = null;
-            if (subscriber.subscriptionCommand) {
-                subscriber.subscriptionCommand.unsubscribe = true;
-                if (subscriber.type === types.dataKeyType.timeseries) {
-                    cmdsWrapper.tsSubCmds.push(subscriber.subscriptionCommand);
-                } else if (subscriber.type === types.dataKeyType.attribute) {
-                    cmdsWrapper.attrSubCmds.push(subscriber.subscriptionCommand);
+            if (subscriber.subscriptionCommands) {
+                for (var i=0;i<subscriber.subscriptionCommands.length;i++) {
+                    var subscriptionCommand = subscriber.subscriptionCommands[i];
+                    subscriptionCommand.unsubscribe = true;
+                    if (subscriber.type === types.dataKeyType.timeseries) {
+                        cmdsWrapper.tsSubCmds.push(subscriptionCommand);
+                    } else if (subscriber.type === types.dataKeyType.attribute) {
+                        cmdsWrapper.attrSubCmds.push(subscriptionCommand);
+                    }
+                    cmdId = subscriptionCommand.cmdId;
+                    if (cmdId) {
+                        if (subscribers[cmdId]) {
+                            delete subscribers[cmdId];
+                        }
+                        if (commands[cmdId]) {
+                            delete commands[cmdId];
+                        }
+                    }
                 }
-                cmdId = subscriber.subscriptionCommand.cmdId;
-            } else if (subscriber.historyCommand) {
-                cmdId = subscriber.historyCommand.cmdId;
             }
-            if (cmdId && subscribers[cmdId]) {
-                delete subscribers[cmdId];
-                subscribersCount--;
+            if (subscriber.historyCommands) {
+                for (i=0;i<subscriber.historyCommands.length;i++) {
+                    var historyCommand = subscriber.historyCommands[i];
+                    cmdId = historyCommand.cmdId;
+                    if (cmdId) {
+                        if (subscribers[cmdId]) {
+                            delete subscribers[cmdId];
+                        }
+                        if (commands[cmdId]) {
+                            delete commands[cmdId];
+                        }
+                    }
+                }
             }
+            subscribersCount--;
             publishCommands();
         }
     }
@@ -268,6 +299,7 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty
         lastCmdId = 0;
         subscribers = {};
         subscribersCount = 0;
+        commands = {};
         cmdsWrapper.tsSubCmds = [];
         cmdsWrapper.historyCmds = [];
         cmdsWrapper.attrSubCmds = [];
diff --git a/ui/src/app/api/time.service.js b/ui/src/app/api/time.service.js
index d34c77e..cce12bf 100644
--- a/ui/src/app/api/time.service.js
+++ b/ui/src/app/api/time.service.js
@@ -261,7 +261,7 @@ function TimeService($translate, types) {
         return historyTimewindow;
     }
 
-    function createSubscriptionTimewindow(timewindow, stDiff, steppedChart) {
+    function createSubscriptionTimewindow(timewindow, stDiff, stateData) {
 
         var subscriptionTimewindow = {
             fixedWindow: null,
@@ -273,12 +273,12 @@ function TimeService($translate, types) {
             }
         };
         var aggTimewindow = 0;
-        if (steppedChart) {
+        if (stateData) {
             subscriptionTimewindow.aggregation = {
                 interval: SECOND,
                 limit: MAX_LIMIT,
                 type: types.aggregation.none.value,
-                steppedChart: true
+                stateData: true
             };
         } else {
             subscriptionTimewindow.aggregation = {
@@ -288,7 +288,7 @@ function TimeService($translate, types) {
             };
         }
 
-        if (angular.isDefined(timewindow.aggregation) && !steppedChart) {
+        if (angular.isDefined(timewindow.aggregation) && !stateData) {
             subscriptionTimewindow.aggregation = {
                 type: timewindow.aggregation.type || types.aggregation.avg.value,
                 limit: timewindow.aggregation.limit || AVG_LIMIT
diff --git a/ui/src/app/api/widget.service.js b/ui/src/app/api/widget.service.js
index 9fbb819..511dc17 100644
--- a/ui/src/app/api/widget.service.js
+++ b/ui/src/app/api/widget.service.js
@@ -561,7 +561,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, $tr
                                 maxDatasources: -1, //unlimited
                                 maxDataKeys: -1, //unlimited
                                 dataKeysOptional: false,
-                                steppedChart: false
+                                stateData: false
                            };
          '    }\n\n' +
 
@@ -632,8 +632,8 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, $tr
             if (angular.isUndefined(result.typeParameters.dataKeysOptional)) {
                 result.typeParameters.dataKeysOptional = false;
             }
-            if (angular.isUndefined(result.typeParameters.steppedChart)) {
-                result.typeParameters.steppedChart = false;
+            if (angular.isUndefined(result.typeParameters.stateData)) {
+                result.typeParameters.stateData = false;
             }
             if (angular.isFunction(widgetTypeInstance.actionSources)) {
                 result.actionSources = widgetTypeInstance.actionSources();
diff --git a/ui/src/app/components/widget/widget.controller.js b/ui/src/app/components/widget/widget.controller.js
index 34a48f7..a4846be 100644
--- a/ui/src/app/components/widget/widget.controller.js
+++ b/ui/src/app/components/widget/widget.controller.js
@@ -340,7 +340,7 @@ export default function WidgetController($scope, $state, $timeout, $window, $ele
         if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) {
             options = {
                 type: widget.type,
-                steppedChart: vm.typeParameters.steppedChart
+                stateData: vm.typeParameters.stateData
             }
             if (widget.type == types.widgetType.alarm.value) {
                 options.alarmSource = angular.copy(widget.config.alarmSource);
diff --git a/ui/src/app/widget/lib/flot-widget.js b/ui/src/app/widget/lib/flot-widget.js
index bf2b77e..8776c8e 100644
--- a/ui/src/app/widget/lib/flot-widget.js
+++ b/ui/src/app/widget/lib/flot-widget.js
@@ -55,7 +55,7 @@ export default class TbFlot {
 
         var tbFlot = this;
 
-        function seriesInfoDiv(label, color, value, units, trackDecimals, active, percent) {
+        function seriesInfoDiv(label, color, value, units, trackDecimals, active, percent, valueFormatFunction) {
             var divElement = $('<div></div>');
             divElement.css({
                 display: "flex",
@@ -83,7 +83,12 @@ export default class TbFlot {
                 });
             }
             divElement.append(labelSpan);
-            var valueContent = tbFlot.ctx.utils.formatValue(value, trackDecimals, units);
+            var valueContent;
+            if (valueFormatFunction) {
+                valueContent = valueFormatFunction(value);
+            } else {
+                valueContent = tbFlot.ctx.utils.formatValue(value, trackDecimals, units);
+            }
             if (angular.isNumber(percent)) {
                 valueContent += ' (' + Math.round(percent) + ' %)';
             }
@@ -107,7 +112,7 @@ export default class TbFlot {
                 var units = item.series.dataKey.units && item.series.dataKey.units.length ? item.series.dataKey.units : tbFlot.ctx.trackUnits;
                 var decimals = angular.isDefined(item.series.dataKey.decimals) ? item.series.dataKey.decimals : tbFlot.ctx.trackDecimals;
                 var divElement = seriesInfoDiv(item.series.dataKey.label, item.series.dataKey.color,
-                    item.datapoint[1][0][1], units, decimals, true, item.series.percent);
+                    item.datapoint[1][0][1], units, decimals, true, item.series.percent, item.series.dataKey.tooltipValueFormatFunction);
                 return divElement.prop('outerHTML');
             };
         } else {
@@ -132,7 +137,7 @@ export default class TbFlot {
                     var units = seriesHoverInfo.units && seriesHoverInfo.units.length ? seriesHoverInfo.units : tbFlot.ctx.trackUnits;
                     var decimals = angular.isDefined(seriesHoverInfo.decimals) ? seriesHoverInfo.decimals : tbFlot.ctx.trackDecimals;
                     var divElement = seriesInfoDiv(seriesHoverInfo.label, seriesHoverInfo.color,
-                        seriesHoverInfo.value, units, decimals, seriesHoverInfo.index === seriesIndex);
+                        seriesHoverInfo.value, units, decimals, seriesHoverInfo.index === seriesIndex, null, seriesHoverInfo.tooltipValueFormatFunction);
                     content += divElement.prop('outerHTML');
                 }
                 return content;
@@ -168,7 +173,7 @@ export default class TbFlot {
             }
         };
 
-        if (this.chartType === 'line' || this.chartType === 'bar' || this.chartType === 'stepped') {
+        if (this.chartType === 'line' || this.chartType === 'bar' || this.chartType === 'state') {
             options.xaxis = {
                 mode: 'time',
                 timezone: 'browser',
@@ -196,6 +201,9 @@ export default class TbFlot {
                 if (settings.yaxis && settings.yaxis.showLabels === false) {
                     return '';
                 }
+                if (this.ticksFormatterFunction) {
+                    return this.ticksFormatterFunction(value);
+                }
                 var factor = this.tickDecimals ? Math.pow(10, this.tickDecimals) : 1,
                     formatted = "" + Math.round(value * factor) / factor;
                 if (this.tickDecimals != null) {
@@ -218,6 +226,13 @@ export default class TbFlot {
                 this.yaxis.labelFont.color = this.yaxis.font.color;
                 this.yaxis.labelFont.size = this.yaxis.font.size+2;
                 this.yaxis.labelFont.weight = "bold";
+                if (settings.yaxis.ticksFormatter && settings.yaxis.ticksFormatter.length) {
+                    try {
+                        this.yaxis.ticksFormatterFunction = new Function('value', settings.yaxis.ticksFormatter);
+                    } catch (e) {
+                        this.yaxis.ticksFormatterFunction = null;
+                    }
+                }
             }
 
             options.grid.borderWidth = 1;
@@ -271,7 +286,7 @@ export default class TbFlot {
                 }
             }
 
-            if (this.chartType === 'stepped') {
+            if (this.chartType === 'state') {
                 options.series.lines = {
                     steps: true,
                     show: true
@@ -331,11 +346,28 @@ export default class TbFlot {
         var colors = [];
         this.yaxes = [];
         var yaxesMap = {};
+
+        var tooltipValueFormatFunction = null;
+        if (this.ctx.settings.tooltipValueFormatter && this.ctx.settings.tooltipValueFormatter.length) {
+            try {
+                tooltipValueFormatFunction = new Function('value', this.ctx.settings.tooltipValueFormatter);
+            } catch (e) {
+                tooltipValueFormatFunction = null;
+            }
+        }
+
         for (var i = 0; i < this.subscription.data.length; i++) {
             var series = this.subscription.data[i];
             colors.push(series.dataKey.color);
             var keySettings = series.dataKey.settings;
-
+            series.dataKey.tooltipValueFormatFunction = tooltipValueFormatFunction;
+            if (keySettings.tooltipValueFormatter && keySettings.tooltipValueFormatter.length) {
+                try {
+                    series.dataKey.tooltipValueFormatFunction = new Function('value', keySettings.tooltipValueFormatter);
+                } catch (e) {
+                    series.dataKey.tooltipValueFormatFunction = tooltipValueFormatFunction;
+                }
+            }
             series.lines = {
                 fill: keySettings.fillLines === true,
                 show: this.chartType === 'line' ? keySettings.showLines !== false : keySettings.showLines === true
@@ -389,7 +421,7 @@ export default class TbFlot {
 
         this.options.colors = colors;
         this.options.yaxes = angular.copy(this.yaxes);
-        if (this.chartType === 'line' || this.chartType === 'bar' || this.chartType === 'stepped') {
+        if (this.chartType === 'line' || this.chartType === 'bar' || this.chartType === 'state') {
             if (this.chartType === 'bar') {
                 this.options.series.bars.barWidth = this.subscription.timeWindow.interval * 0.6;
             }
@@ -432,6 +464,14 @@ export default class TbFlot {
         yaxis.position = position;
 
         yaxis.keysInfo = [];
+
+        if (keySettings.axisTicksFormatter && keySettings.axisTicksFormatter.length) {
+            try {
+                yaxis.ticksFormatterFunction = new Function('value', keySettings.axisTicksFormatter);
+            } catch (e) {
+                yaxis.ticksFormatterFunction = this.yaxis.ticksFormatterFunction;
+            }
+        }
         return yaxis;
     }
 
@@ -442,7 +482,7 @@ export default class TbFlot {
         }
         if (this.subscription) {
             if (!this.isMouseInteraction && this.ctx.plot) {
-                if (this.chartType === 'line' || this.chartType === 'bar' || this.chartType === 'stepped') {
+                if (this.chartType === 'line' || this.chartType === 'bar' || this.chartType === 'state') {
 
                     var axisVisibilityChanged = false;
                     if (this.yaxis) {
@@ -654,6 +694,11 @@ export default class TbFlot {
                         "type": "boolean",
                         "default": false
                     },
+                    "tooltipValueFormatter": {
+                        "title": "Tooltip value format function, f(value)",
+                        "type": "string",
+                        "default": ""
+                    },
                     "grid": {
                         "title": "Grid settings",
                         "type": "object",
@@ -739,6 +784,11 @@ export default class TbFlot {
                                 "title": "Ticks color",
                                 "type": "string",
                                 "default": null
+                            },
+                            "ticksFormatter": {
+                                "title": "Ticks formatter function, f(value)",
+                                "type": "string",
+                                "default": ""
                             }
                         }
                     }
@@ -757,6 +807,10 @@ export default class TbFlot {
                 "tooltipIndividual",
                 "tooltipCumulative",
                 {
+                    "key": "tooltipValueFormatter",
+                    "type": "javascript"
+                },
+                {
                     "key": "grid",
                     "items": [
                         {
@@ -797,6 +851,10 @@ export default class TbFlot {
                         {
                             "key": "yaxis.color",
                             "type": "color"
+                        },
+                        {
+                            "key": "yaxis.ticksFormatter",
+                            "type": "javascript"
                         }
                     ]
                 }
@@ -830,6 +888,11 @@ export default class TbFlot {
                             "type": "boolean",
                             "default": false
                     },
+                    "tooltipValueFormatter": {
+                        "title": "Tooltip value format function, f(value)",
+                        "type": "string",
+                        "default": ""
+                    },
                     "showSeparateAxis": {
                         "title": "Show separate axis",
                         "type": "boolean",
@@ -849,6 +912,11 @@ export default class TbFlot {
                         "title": "Axis position",
                         "type": "string",
                         "default": "left"
+                    },
+                    "axisTicksFormatter": {
+                        "title": "Ticks formatter function, f(value)",
+                        "type": "string",
+                        "default": ""
                     }
                 },
                 "required": ["showLines", "fillLines", "showPoints"]
@@ -857,6 +925,10 @@ export default class TbFlot {
                 "showLines",
                 "fillLines",
                 "showPoints",
+                {
+                    "key": "tooltipValueFormatter",
+                    "type": "javascript"
+                },
                 "showSeparateAxis",
                 "axisTitle",
                 "axisTickDecimals",
@@ -874,8 +946,11 @@ export default class TbFlot {
                             "label": "Right"
                         }
                     ]
+                },
+                {
+                    "key": "axisTicksFormatter",
+                    "type": "javascript"
                 }
-
             ]
         }
     }
@@ -1129,6 +1204,7 @@ export default class TbFlot {
                     label: series.dataKey.label,
                     units: series.dataKey.units,
                     decimals: series.dataKey.decimals,
+                    tooltipValueFormatFunction: series.dataKey.tooltipValueFormatFunction,
                     time: pointTime,
                     distance: hoverDistance,
                     index: i
diff --git a/ui/src/app/widget/lib/rpc/led-indicator.directive.js b/ui/src/app/widget/lib/rpc/led-indicator.directive.js
index 04615c7..ac0658c 100644
--- a/ui/src/app/widget/lib/rpc/led-indicator.directive.js
+++ b/ui/src/app/widget/lib/rpc/led-indicator.directive.js
@@ -130,15 +130,22 @@ function LedIndicatorController($element, $scope, $timeout, utils, types) {
             }
         }
 
-        vm.checkStatusMethod = 'checkStatus';
-        if (vm.ctx.settings.checkStatusMethod && vm.ctx.settings.checkStatusMethod.length) {
-            vm.checkStatusMethod = vm.ctx.settings.checkStatusMethod;
+        vm.performCheckStatus = vm.ctx.settings.performCheckStatus != false;
+        if (vm.performCheckStatus) {
+            vm.checkStatusMethod = 'checkStatus';
+            if (vm.ctx.settings.checkStatusMethod && vm.ctx.settings.checkStatusMethod.length) {
+                vm.checkStatusMethod = vm.ctx.settings.checkStatusMethod;
+            }
         }
         if (!rpcEnabled) {
             onError('Target device is not set!');
         } else {
             if (!vm.isSimulated) {
-                rpcCheckStatus();
+                if (vm.performCheckStatus) {
+                    rpcCheckStatus();
+                } else {
+                    subscribeForValue();
+                }
             }
         }
     }