diff --git a/application/src/main/data/json/system/widget_bundles/input_widgets.json b/application/src/main/data/json/system/widget_bundles/input_widgets.json
index 66484001d1..5129ea19a7 100644
--- a/application/src/main/data/json/system/widget_bundles/input_widgets.json
+++ b/application/src/main/data/json/system/widget_bundles/input_widgets.json
@@ -276,6 +276,22 @@
"dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"readOnly\": {\n \"title\": \"Value is read only\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"inputTypeNumber\": {\n \"title\": \"Datakey is a number\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"step\": {\n \"title\": \"Step interval between valid values (only for numbers)\",\n \"type\": \"number\",\n \"default\": \"1\"\n },\n \"icon\": {\n \"title\": \"Icon to show before input cell\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"readOnly\",\n \"inputTypeNumber\",\n \"step\",\n\t\t{\n \t\t\"key\": \"icon\",\n\t\t\t\"type\": \"icon\"\n\t\t},\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n }\n ]\n}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update Multiple Attributes\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
}
+ },
+ {
+ "alias": "web_camera_input",
+ "name": "Web Camera Input",
+ "descriptor": {
+ "type": "latest",
+ "sizeX": 9.5,
+ "sizeY": 6.5,
+ "resources": [],
+ "templateHtml": "\n",
+ "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}",
+ "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onDestroy = function() {\n}\n",
+ "settingsSchema": "{}",
+ "dataKeySettingsSchema": "{}\n",
+ "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Web Camera Input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
+ }
}
]
}
diff --git a/ui/src/app/api/widget.service.js b/ui/src/app/api/widget.service.js
index ea59130484..8cb5f884b3 100644
--- a/ui/src/app/api/widget.service.js
+++ b/ui/src/app/api/widget.service.js
@@ -25,6 +25,7 @@ import thingsboardEntitiesHierarchyWidget from '../widget/lib/entities-hierarchy
import thingsboardExtensionsTableWidget from '../widget/lib/extensions-table-widget';
import thingsboardDateRangeNavigatorWidget from '../widget/lib/date-range-navigator/date-range-navigator';
import thingsboardMultipleInputWidget from '../widget/lib/multiple-input-widget';
+import thingsboardWebCameraInputWidget from '../widget/lib/web-camera-input-widget';
import thingsboardRpcWidgets from '../widget/lib/rpc';
@@ -50,7 +51,8 @@ import thingsboardUtils from '../common/utils.service';
export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight,
thingsboardTimeseriesTableWidget, thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget,
thingsboardEntitiesHierarchyWidget, thingsboardExtensionsTableWidget, thingsboardDateRangeNavigatorWidget,
- thingsboardMultipleInputWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils, TripAnimationWidget])
+ thingsboardMultipleInputWidget, thingsboardWebCameraInputWidget, thingsboardRpcWidgets, thingsboardTypes,
+ thingsboardUtils, TripAnimationWidget])
.factory('widgetService', WidgetService)
.name;
diff --git a/ui/src/app/locale/locale.constant-en_US.json b/ui/src/app/locale/locale.constant-en_US.json
index 54a7ad38c1..0292b94de9 100644
--- a/ui/src/app/locale/locale.constant-en_US.json
+++ b/ui/src/app/locale/locale.constant-en_US.json
@@ -1693,10 +1693,15 @@
"entity-timeseries-required": "Entity timeseries is required",
"not-allowed-entity": "Selected entity cannot have shared attributes",
"no-attribute-selected": "No attribute is selected",
+ "no-datakey-selected": "No datakey is selected",
"no-entity-selected": "No entity selected",
+ "no-image": "No image",
+ "no-support-web-camera": "No supported web camera",
"no-timeseries-selected": "No timeseries selected",
"switch-attribute-value": "Switch entity attribute value",
+ "switch-camera": "Switch camera",
"switch-timeseries-value": "Switch entity timeseries value",
+ "take-photo": "Take photo",
"time": "Time",
"timeseries-not-allowed": "Timeseries parameter cannot be used in this widget",
"update-failed": "Update failed",
diff --git a/ui/src/app/widget/lib/web-camera-input-widget.js b/ui/src/app/widget/lib/web-camera-input-widget.js
new file mode 100644
index 0000000000..344d648c40
--- /dev/null
+++ b/ui/src/app/widget/lib/web-camera-input-widget.js
@@ -0,0 +1,196 @@
+/*
+ * Copyright © 2016-2019 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 './web-camera-input-widget.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+import webCameraWidgetTemplate from './web-camera-input-widget.tpl.html';
+/* eslint-enable import/no-unresolved, import/default */
+
+export default angular.module('thingsboard.widgets.webCameraWidget', [])
+ .directive('tbWebCameraWidget', webCameraWidget)
+ .name;
+
+/*@ngInject*/
+function webCameraWidget() {
+ return {
+ restrict: "E",
+ scope: true,
+ bindToController: {
+ ctx: '='
+ },
+ controller: WebCameraWidgetController,
+ controllerAs: 'vm',
+ templateUrl: webCameraWidgetTemplate
+ };
+}
+
+function WebCameraWidgetController($element, $scope, $window, types, utils, attributeService, Fullscreen) {
+ let vm = this;
+
+ vm.videoInput = [];
+ vm.videoDevice = "";
+ vm.previewPhoto = "";
+ vm.isShowCamera = false;
+ vm.isPreviewPhoto = false;
+
+ let streamDevice = null;
+ let indexWebCamera = 0;
+ let videoElement = null;
+ let canvas = null;
+ let photoCamera = null;
+ let dataKeyType = "";
+
+ vm.getStream = getStream;
+ vm.createPhoto = createPhoto;
+ vm.takePhoto = takePhoto;
+ vm.switchWebCamera = switchWebCamera;
+ vm.cancelPhoto = cancelPhoto;
+ vm.closeCamera = closeCamera;
+ vm.savePhoto = savePhoto;
+
+ vm.isEntityDetected = false;
+ vm.dataKeyDetected = false;
+ vm.isCameraSupport = false;
+ vm.isDeviceDetect = false;
+
+ $scope.$watch('vm.ctx', function () {
+ if (vm.ctx && vm.ctx.datasources && vm.ctx.datasources.length) {
+ let datasource = vm.ctx.datasources[0];
+ if (datasource.type === types.datasourceType.entity) {
+ if (datasource.entityType && datasource.entityId) {
+ if (vm.ctx.settings.widgetTitle && vm.ctx.settings.widgetTitle.length) {
+ $scope.titleTemplate = utils.customTranslation(vm.ctx.settings.widgetTitle, vm.ctx.settings.widgetTitle);
+ } else {
+ $scope.titleTemplate = vm.ctx.widgetConfig.title;
+ }
+ vm.isEntityDetected = true;
+ }
+ }
+ if (datasource.dataKeys.length) {
+ $scope.currentKey = datasource.dataKeys[0].name;
+ dataKeyType = datasource.dataKeys[0].type;
+ vm.dataKeyDetected = true;
+ }
+ if (hasGetUserMedia()) {
+ vm.isCameraSupport = true;
+ getDevices().then(gotDevices).then(() => {
+ vm.isDeviceDetect = !!vm.videoInput.length;
+ });
+ }
+ }
+ });
+
+ function hasGetUserMedia() {
+ return !!($window.navigator.mediaDevices && $window.navigator.mediaDevices.getUserMedia);
+ }
+
+ function takePhoto(){
+ vm.isShowCamera = true;
+ videoElement = $element[0].querySelector('#videoStream');
+ photoCamera = $element[0].querySelector('#photoCamera');
+ canvas = $element[0].querySelector('canvas');
+ Fullscreen.enable(photoCamera);
+ getStream();
+ }
+
+ function cancelPhoto() {
+ vm.isPreviewPhoto = false;
+ vm.previewPhoto = "";
+ }
+
+ function switchWebCamera() {
+ indexWebCamera = (indexWebCamera+1)%vm.videoInput.length;
+ vm.videoDevice = vm.videoInput[indexWebCamera].deviceId;
+ getStream();
+ }
+
+ function getDevices() {
+ return $window.navigator.mediaDevices.enumerateDevices();
+ }
+
+ function gotDevices(deviceInfos) {
+ for (const deviceInfo of deviceInfos) {
+ let device = {
+ deviceId: deviceInfo.deviceId,
+ label: ""
+ };
+ if (deviceInfo.kind === 'videoinput') {
+ device.label = deviceInfo.label || `Camera ${vm.videoInput.length + 1}`;
+ vm.videoInput.push(device);
+ }
+ }
+ }
+
+ function getStream() {
+ if (streamDevice !== null) {
+ streamDevice.getTracks().forEach(track => {
+ track.stop();
+ });
+ }
+ const constraints = {
+ video: {deviceId: vm.videoDevice !== "" ? {exact: vm.videoDevice} : undefined}
+ };
+ return $window.navigator.mediaDevices.getUserMedia(constraints).then(gotStream);
+ }
+
+ function gotStream(stream) {
+ streamDevice = stream;
+ if(vm.videoDevice === ""){
+ indexWebCamera = vm.videoInput.findIndex(option => option.label === stream.getVideoTracks()[0].label);
+ indexWebCamera = indexWebCamera === -1 ? 0 : indexWebCamera;
+ vm.videoDevice = vm.videoInput[indexWebCamera].deviceId;
+ }
+ videoElement.srcObject = stream;
+ }
+
+ function createPhoto() {
+ canvas.width = videoElement.videoWidth;
+ canvas.height = videoElement.videoHeight;
+ canvas.getContext('2d').drawImage(videoElement, 0, 0);
+ vm.previewPhoto = canvas.toDataURL('image/png');
+ vm.isPreviewPhoto = true;
+ }
+
+ function closeCamera(){
+ Fullscreen.cancel(photoCamera);
+ vm.isShowCamera = false;
+ if (streamDevice !== null) {
+ streamDevice.getTracks().forEach(track => {
+ track.stop();
+ });
+ }
+ streamDevice = null;
+ videoElement.srcObject = null;
+ }
+
+ function savePhoto(){
+ let promiseData = null;
+ let datasource = vm.ctx.datasources[0];
+ let saveData = [{
+ key: datasource.dataKeys[0].name,
+ value: vm.previewPhoto
+ }];
+ if(dataKeyType === types.dataKeyType.attribute){
+ promiseData = attributeService.saveEntityAttributes(datasource.entityType, datasource.entityId, types.attributesScope.server.value, saveData);
+ } else if(dataKeyType === types.dataKeyType.timeseries){
+ promiseData = attributeService.saveEntityTimeseries(datasource.entityType, datasource.entityId, "scope", saveData);
+ }
+ promiseData.then(()=>{
+ vm.isPreviewPhoto = false;
+ closeCamera();
+ })
+ }
+}
diff --git a/ui/src/app/widget/lib/web-camera-input-widget.scss b/ui/src/app/widget/lib/web-camera-input-widget.scss
new file mode 100644
index 0000000000..3ef958cff3
--- /dev/null
+++ b/ui/src/app/widget/lib/web-camera-input-widget.scss
@@ -0,0 +1,67 @@
+/**
+ * Copyright © 2016-2019 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.
+ */
+
+.tb-web-camera {
+ height: 100%;
+
+ &__last-photo{
+ width: 100%;
+ margin: 5px 0;
+ text-align: center;
+ border: solid 1px;
+
+ &_text{
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ margin-top: -.625em;
+ transform: translate(-50%, -50%);
+ }
+
+ &_img{
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ }
+ }
+
+ .camera {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+
+ .camera-stream{
+ display: block;
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ }
+
+ .camera-controls {
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ padding: 0 5px 5px;
+ }
+ }
+
+ .message-text{
+ font-size: 18px;
+ color: #a0a0a0;
+ text-align: center;
+ }
+}
diff --git a/ui/src/app/widget/lib/web-camera-input-widget.tpl.html b/ui/src/app/widget/lib/web-camera-input-widget.tpl.html
new file mode 100644
index 0000000000..0fc8e16da2
--- /dev/null
+++ b/ui/src/app/widget/lib/web-camera-input-widget.tpl.html
@@ -0,0 +1,73 @@
+
+
+
+
+
+
widgets.input-widgets.no-image
+
![]()
+
+
{{ "widgets.input-widgets.take-photo" | translate }}
+
+
+
+
+
+
+
+ switch_camera
+
+
+ photo_camera
+
+
+ close
+
+
+
+
+
+
![]()
+
+
+
+
+ close
+
+
+ check
+
+
+
+
+
+
+
+ {{ 'widgets.input-widgets.no-entity-selected' | translate }}
+
+
+ {{ 'widgets.input-widgets.no-datakey-selected' | translate }}
+
+
+ {{ 'widgets.input-widgets.no-support-web-camera' | translate }}
+
+
+ {{ 'widgets.input-widgets.no-support-web-camera' | translate }}
+
+