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 daa19675d1..a264ba6515 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 6f4685c018..716e48a510 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": "", "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 53d5341139..3f4b971182 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 43b2e939f4..64e4e40ff7 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 a68e14ce71..a01704c70d 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 }; + } - telemetryWebsocketService.subscribe(subscriber); - subscribers[subscriber.historyCommand.cmdId] = subscriber; + function updateFirstStateHistoryCommand(stateHistoryCommand, startTs) { + stateHistoryCommand.startTs = startTs - YEAR; + stateHistoryCommand.endTs = startTs; + } + + 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 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'); 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": "" } } } @@ -756,6 +806,10 @@ export default class TbFlot { "fontSize", "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 04615c7b98..ac0658c75b 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(); + } } } }