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 70f8e9cf6a..7f9c75a4e8 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 @@ -84,6 +84,22 @@ "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":500,\"initialValue\":false,\"getValueMethod\":\"getValue\",\"setValueMethod\":\"setValue\",\"showOnOffLabels\":true,\"title\":\"Round switch\"},\"title\":\"Round switch\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"decimals\":2}" } + }, + { + "alias": "led_indicator", + "name": "Led indicator", + "descriptor": { + "type": "rpc", + "sizeX": 2.5, + "sizeY": 2.5, + "resources": [], + "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 \"getValueMethod\": {\n \"title\": \"Get value method\",\n \"type\": \"string\",\n \"default\": \"getValue\"\n },\n \"valuePollingInterval\": {\n \"title\": \"Value polling interval (ms)\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout (ms)\",\n \"type\": \"number\",\n \"default\": 500\n }\n },\n \"required\": [\"getValueMethod\", \"requestTimeout\"]\n },\n \"form\": [\n \"initialValue\",\n \"title\",\n {\n \"key\": \"ledColor\",\n \"type\": \"color\"\n },\n \"getValueMethod\",\n \"valuePollingInterval\",\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,\"getValueMethod\":\"getValue\",\"title\":\"Led indicator\",\"ledColor\":\"#4caf50\",\"valuePollingInterval\":500},\"title\":\"Led indicator\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"decimals\":2}" + } } ] } \ No newline at end of file diff --git a/ui/src/app/widget/lib/rpc/index.js b/ui/src/app/widget/lib/rpc/index.js index 0536fac58f..d57878d86d 100644 --- a/ui/src/app/widget/lib/rpc/index.js +++ b/ui/src/app/widget/lib/rpc/index.js @@ -17,9 +17,11 @@ import tbKnob from './knob.directive'; import tbSwitch from './switch.directive'; import tbRoundSwitch from './round-switch.directive'; +import tbLedIndicator from './led-indicator.directive'; export default angular.module('thingsboard.widgets.rpc', [ tbKnob, tbSwitch, - tbRoundSwitch + tbRoundSwitch, + tbLedIndicator ]).name; diff --git a/ui/src/app/widget/lib/rpc/led-indicator.directive.js b/ui/src/app/widget/lib/rpc/led-indicator.directive.js new file mode 100644 index 0000000000..58a7c4b64a --- /dev/null +++ b/ui/src/app/widget/lib/rpc/led-indicator.directive.js @@ -0,0 +1,201 @@ +/* + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import './led-indicator.scss'; + +import tinycolor from 'tinycolor2'; + +/* eslint-disable import/no-unresolved, import/default */ + +import ledIndicatorTemplate from './led-indicator.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +export default angular.module('thingsboard.widgets.rpc.ledIndicator', []) + .directive('tbLedIndicator', LedIndicator) + .name; + +/*@ngInject*/ +function LedIndicator() { + return { + restrict: "E", + scope: true, + bindToController: { + ctx: '=' + }, + controller: LedIndicatorController, + controllerAs: 'vm', + templateUrl: ledIndicatorTemplate + }; +} + +/*@ngInject*/ +function LedIndicatorController($element, $scope, $timeout) { + let vm = this; + + vm.showTitle = false; + vm.value = false; + vm.error = ''; + + var led = angular.element('.led', $element), + ledContainer = angular.element('#led-container', $element), + textMeasure = angular.element('#text-measure', $element), + ledTitleContainer = angular.element('.title-container', $element), + ledTitle = angular.element('.led-title', $element), + ledErrorContainer = angular.element('.error-container', $element), + ledError = angular.element('.led-error', $element); + + $scope.$watch('vm.ctx', () => { + if (vm.ctx) { + init(); + } + }); + + $scope.$on('$destroy', () => { + vm.destroyed = true; + if (vm.requestValueTimeoutHandle) { + $timeout.cancel(vm.requestValueTimeoutHandle); + } + }); + + resize(); + + function init() { + + vm.title = angular.isDefined(vm.ctx.settings.title) ? vm.ctx.settings.title : ''; + vm.showTitle = vm.title && vm.title.length ? true : false; + + var origColor = angular.isDefined(vm.ctx.settings.ledColor) ? vm.ctx.settings.ledColor : 'green'; + + vm.ledColor = tinycolor(origColor).brighten(30).toHexString(); + vm.ledMiddleColor = tinycolor(origColor).toHexString(); + vm.disabledColor = tinycolor(origColor).darken(40).toHexString(); + vm.disabledMiddleColor = tinycolor(origColor).darken(60).toHexString(); + + vm.ctx.resize = resize; + $scope.$applyAsync(() => { + resize(); + }); + var initialValue = angular.isDefined(vm.ctx.settings.initialValue) ? vm.ctx.settings.initialValue : false; + setValue(initialValue, true); + + var subscription = vm.ctx.defaultSubscription; + var rpcEnabled = subscription.rpcEnabled; + + vm.isSimulated = $scope.widgetEditMode; + + vm.requestTimeout = 500; + if (vm.ctx.settings.requestTimeout) { + vm.requestTimeout = vm.ctx.settings.requestTimeout; + } + vm.valuePollingInterval = 500; + if (vm.ctx.settings.valuePollingInterval) { + vm.valuePollingInterval = vm.ctx.settings.valuePollingInterval; + } + vm.getValueMethod = 'getValue'; + if (vm.ctx.settings.getValueMethod && vm.ctx.settings.getValueMethod.length) { + vm.getValueMethod = vm.ctx.settings.getValueMethod; + } + if (!rpcEnabled) { + onError('Target device is not set!'); + } else { + if (!vm.isSimulated) { + rpcRequestValue(); + } + } + } + + function resize() { + var width = ledContainer.width(); + var height = ledContainer.height(); + var size = Math.min(width, height); + + led.css({width: size, height: size}); + + if (vm.showTitle) { + setFontSize(ledTitle, vm.title, ledTitleContainer.height() * 2 / 3, ledTitleContainer.width()); + } + setFontSize(ledError, vm.error, ledErrorContainer.height(), ledErrorContainer.width()); + } + + function setValue(value, forceUpdate) { + if (vm.value != value || forceUpdate) { + vm.value = value; + updateColor(); + } + } + + function updateColor() { + var color = vm.value ? vm.ledColor : vm.disabledColor; + var middleColor = vm.value ? vm.ledMiddleColor : vm.disabledMiddleColor; + var boxShadow = `#000 0 -1px 6px 1px, inset ${middleColor} 0 -1px 8px, ${color} 0 3px 11px`; + led.css({'backgroundColor': color}); + led.css({'boxShadow': boxShadow}); + if (vm.value) { + led.removeClass( 'disabled' ); + } else { + led.addClass( 'disabled' ); + } + } + + function onError(error) { + $scope.$applyAsync(() => { + vm.error = error; + setFontSize(ledError, vm.error, ledErrorContainer.height(), ledErrorContainer.width()); + }); + } + + function setFontSize(element, text, fontSize, maxWidth) { + var textWidth = measureTextWidth(text, fontSize); + while (textWidth > maxWidth) { + fontSize--; + textWidth = measureTextWidth(text, fontSize); + } + element.css({'fontSize': fontSize+'px', 'lineHeight': fontSize+'px'}); + } + + function measureTextWidth(text, fontSize) { + textMeasure.css({'fontSize': fontSize+'px', 'lineHeight': fontSize+'px'}); + textMeasure.text(text); + return textMeasure.width(); + } + + function rpcRequestValue() { + if (vm.destroyed) { + return; + } + vm.error = ''; + vm.ctx.controlApi.sendTwoWayCommand(vm.getValueMethod, null, vm.requestTimeout).then( + (responseBody) => { + var newValue = responseBody ? true : false; + setValue(newValue); + if (vm.requestValueTimeoutHandle) { + $timeout.cancel(vm.requestValueTimeoutHandle); + } + vm.requestValueTimeoutHandle = $timeout(rpcRequestValue, vm.valuePollingInterval); + }, + () => { + var errorText = vm.ctx.defaultSubscription.rpcErrorText; + onError(errorText); + if (vm.requestValueTimeoutHandle) { + $timeout.cancel(vm.requestValueTimeoutHandle); + } + vm.requestValueTimeoutHandle = $timeout(rpcRequestValue, vm.valuePollingInterval); + } + ); + } + +} diff --git a/ui/src/app/widget/lib/rpc/led-indicator.scss b/ui/src/app/widget/lib/rpc/led-indicator.scss new file mode 100644 index 0000000000..d086a101d6 --- /dev/null +++ b/ui/src/app/widget/lib/rpc/led-indicator.scss @@ -0,0 +1,78 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import "~compass-sass-mixins/lib/compass"; + +$error-height: 14px; + +$background-color: #e6e7e8; + +.tb-led-indicator { + width:100%; + height:100%; + background: $background-color; + + .title-container { + .led-title { + color: #757575; + font-weight: 500; + white-space: nowrap; + } + } + + .error-container { + position:absolute; + top: 1%; + left: 0; + right: 0; + z-index:4; + height: $error-height; + .led-error { + color: #ff3315; + white-space: nowrap; + } + } + #text-measure { + position: absolute; + visibility: hidden; + height: auto; + width: auto; + white-space: nowrap; + } + + #led-container { + padding: 10px; + .led { + cursor: pointer; + position: relative; + border-radius: 50%; + background-image: -owg-radial-gradient(50% 50%, circle closest-corner, transparent, rgba(0, 0, 0, 0.25)); + background-image: -webkit-radial-gradient(50% 50%, circle closest-corner, transparent, rgba(0, 0, 0, 0.25)); + background-image: -moz-radial-gradient(50% 50%, circle closest-corner, transparent, rgba(0, 0, 0, 0.25)); + background-image: -o-radial-gradient(50% 50%, circle closest-corner, transparent, rgba(0, 0, 0, 0.25)); + background-image: radial-gradient(50% 50%, circle closest-corner, transparent, rgba(0, 0, 0, 0.25)); + transition: background-color 0.5s, box-shadow 0.5s; + &.disabled { + background-image: -owg-radial-gradient(50% 50%, circle closest-corner, rgba(255, 255, 255, 0.5), rgba(0, 0, 0, 0.1)); + background-image: -webkit-radial-gradient(50% 50%, circle closest-corner, rgba(255, 255, 255, 0.5), rgba(0, 0, 0, 0.1)); + background-image: -moz-radial-gradient(50% 50%, circle closest-corner, rgba(255, 255, 255, 0.5), rgba(0, 0, 0, 0.1)); + background-image: -o-radial-gradient(50% 50%, circle closest-corner, rgba(255, 255, 255, 0.5), rgba(0, 0, 0, 0.1)); + background-image: radial-gradient(50% 50%, circle closest-corner, rgba(255, 255, 255, 0.5), rgba(0, 0, 0, 0.1)); + } + } + } +} + diff --git a/ui/src/app/widget/lib/rpc/led-indicator.tpl.html b/ui/src/app/widget/lib/rpc/led-indicator.tpl.html new file mode 100644 index 0000000000..a092f42dea --- /dev/null +++ b/ui/src/app/widget/lib/rpc/led-indicator.tpl.html @@ -0,0 +1,32 @@ + +
+
+ {{vm.title}} +
+
+
+
+
+
+ {{ vm.error }} +
+
+
diff --git a/ui/src/app/widget/lib/rpc/round-switch.directive.js b/ui/src/app/widget/lib/rpc/round-switch.directive.js index e930b7077f..edb8c7bb68 100644 --- a/ui/src/app/widget/lib/rpc/round-switch.directive.js +++ b/ui/src/app/widget/lib/rpc/round-switch.directive.js @@ -76,7 +76,6 @@ function RoundSwitchController($element, $scope, utils) { vm.title = angular.isDefined(vm.ctx.settings.title) ? vm.ctx.settings.title : ''; vm.showTitle = vm.title && vm.title.length ? true : false; - vm.showOnOffLabels = angular.isDefined(vm.ctx.settings.showOnOffLabels) ? vm.ctx.settings.showOnOffLabels : true; vm.ctx.resize = resize; $scope.$applyAsync(() => { resize();