diff --git a/application/src/main/data/json/system/widget_types/alarms_table.json b/application/src/main/data/json/system/widget_types/alarms_table.json index 12716482c9..2abf10e995 100644 --- a/application/src/main/data/json/system/widget_types/alarms_table.json +++ b/application/src/main/data/json/system/widget_types/alarms_table.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.alarmsTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.alarmsTableWidget.onEditModeChanged();\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.alarmsTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.alarmsTableWidget.onEditModeChanged();\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "tb-alarms-table-widget-settings", diff --git a/application/src/main/data/json/system/widget_types/asset_admin_table.json b/application/src/main/data/json/system/widget_types/asset_admin_table.json index fe5722a01a..a74d2dd644 100644 --- a/application/src/main/data/json/system/widget_types/asset_admin_table.json +++ b/application/src/main/data/json/system/widget_types/asset_admin_table.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "tb-entities-table-widget-settings", diff --git a/application/src/main/data/json/system/widget_types/device_admin_table.json b/application/src/main/data/json/system/widget_types/device_admin_table.json index 35cb457590..3265554da1 100644 --- a/application/src/main/data/json/system/widget_types/device_admin_table.json +++ b/application/src/main/data/json/system/widget_types/device_admin_table.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "tb-entities-table-widget-settings", diff --git a/application/src/main/data/json/system/widget_types/entities_table.json b/application/src/main/data/json/system/widget_types/entities_table.json index 73a546cb87..ce3ec3add1 100644 --- a/application/src/main/data/json/system/widget_types/entities_table.json +++ b/application/src/main/data/json/system/widget_types/entities_table.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'name', type: 'entityField' }];\n }\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'name', type: 'entityField' }];\n }\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "tb-entities-table-widget-settings", diff --git a/application/src/main/data/json/tenant/dashboards/gateways.json b/application/src/main/data/json/tenant/dashboards/gateways.json index 4ccee86d5e..59e5672b53 100644 --- a/application/src/main/data/json/tenant/dashboards/gateways.json +++ b/application/src/main/data/json/tenant/dashboards/gateways.json @@ -1,8 +1,8 @@ { "title": "ThingsBoard IoT Gateways", - "image": null, + "image": "tb-image:Z2F0ZXdheS1kYXNoYm9hcmQucG5n:Z2F0ZXdheS1kYXNoYm9hcmQucG5n;data:image/png;base64,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", "mobileHide": false, - "mobileOrder": null, + "mobileOrder": 6, "configuration": { "description": "", "widgets": { @@ -136,7 +136,7 @@ { "name": "active_connectors", "type": "attribute", - "label": "Active Connectors", + "label": "Enabled Connectors", "color": "#3f51b5", "settings": { "columnWidth": "20%", @@ -248,7 +248,7 @@ "type": "customPretty", "customHtml": "
\r\n \r\n

Add gateway

\r\n \r\n \r\n
\r\n \r\n \r\n
\r\n
\r\n
\r\n \r\n Name\r\n \r\n \r\n Gateway name is required.\r\n \r\n \r\n
\r\n
\r\n \r\n
\r\n
\r\n
\r\n \r\n \r\n
\r\n
\r\n", "customCss": ".add-entity-form {\r\n min-width: 400px !important;\r\n}\r\n\r\n.add-entity-form .boolean-value-input {\r\n padding-left: 5px;\r\n}\r\n\r\n.add-entity-form .boolean-value-input .checkbox-label {\r\n margin-bottom: 8px;\r\n color: rgba(0,0,0,0.54);\r\n font-size: 12px;\r\n}\r\n\r\n.relations-list .header {\r\n padding-right: 5px;\r\n padding-bottom: 5px;\r\n padding-left: 5px;\r\n}\r\n\r\n.relations-list .header .cell {\r\n padding-right: 5px;\r\n padding-left: 5px;\r\n font-size: 12px;\r\n font-weight: 700;\r\n color: rgba(0, 0, 0, .54);\r\n white-space: nowrap;\r\n}\r\n\r\n.relations-list .mat-form-field-infix {\r\n width: auto !important;\r\n}\r\n\r\n.relations-list .body {\r\n padding-right: 5px;\r\n padding-bottom: 15px;\r\n padding-left: 5px;\r\n}\r\n\r\n.relations-list .body .row {\r\n padding-top: 5px;\r\n}\r\n\r\n.relations-list .body .cell {\r\n padding-right: 5px;\r\n padding-left: 5px;\r\n}\r\n\r\n.relations-list .body .md-button {\r\n margin: 0;\r\n}\r\n\r\n", - "customFunction": "let $injector = widgetContext.$scope.$injector;\r\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\r\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\r\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\r\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\r\nlet entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService'));\r\nlet userSettingsService = $injector.get(widgetContext.servicesMap.get('userSettingsService'));\r\n\r\nopenAddEntityDialog();\r\n\r\nfunction openAddEntityDialog() {\r\n customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();\r\n}\r\n\r\nfunction AddEntityDialogController(instance) {\r\n let vm = instance;\r\n let userSettings;\r\n userSettingsService.loadUserSettings().subscribe(settings=> {\r\n userSettings = settings;\r\n if (!userSettings.createdGatewaysCount) userSettings.createdGatewaysCount = 0;\r\n });\r\n \r\n\r\n vm.addEntityFormGroup = vm.fb.group({\r\n entityName: ['', [vm.validators.required]],\r\n entityType: ['DEVICE'],\r\n entityLabel: [''],\r\n type: ['', [vm.validators.required]],\r\n });\r\n\r\n vm.cancel = function() {\r\n vm.dialogRef.close(null);\r\n };\r\n\r\n\r\n vm.save = function($event) {\r\n vm.addEntityFormGroup.markAsPristine();\r\n saveEntityObservable().subscribe(\r\n function (device) {\r\n widgetContext.updateAliases();\r\n userSettingsService.putUserSettings({ createdGatewaysCount: ++userSettings.createdGatewaysCount }).subscribe(_=>{\r\n });\r\n vm.dialogRef.close(null);\r\n openCommandDialog(device, $event);\r\n }\r\n );\r\n };\r\n \r\n function openCommandDialog(device, $event) {\r\n vm.device = device;\r\n let openCommandAction = widgetContext.actionsApi.getActionDescriptors(\"actionCellButton\").find(action => action.name == \"Docker commands\");\r\n widgetContext.actionsApi.handleWidgetAction($event, openCommandAction, device.id, device.name, {newDevice: true});\r\n setTimeout(function() {\r\n document.querySelector(\".dashboard-state-dialog .mat-mdc-button-touch-target\").addEventListener('click', goToConfigState);\r\n document.querySelector(\".dashboard-state-dialog .mat-mdc-button.mat-primary\").addEventListener('click', goToConfigState);\r\n }, 500);\r\n }\r\n\r\n \r\n function goToConfigState() {\r\n document.querySelector(\".dashboard-state-dialog .mat-mdc-button-touch-target\").removeEventListener('click', goToConfigState);\r\n document.querySelector(\".dashboard-state-dialog .mat-mdc-button.mat-primary\").removeEventListener('click', goToConfigState);\r\n const stateParams = {};\r\n stateParams.entityId = vm.device.id;\r\n stateParams.entityName = vm.device.name;\r\n const newStateParams = {\r\n targetEntityParamName: 'default',\r\n new_gateway: {\r\n entityId: vm.device.id,\r\n entityName: vm.device.name\r\n }\r\n }\r\n const params = {...stateParams, ...newStateParams};\r\n widgetContext.stateController.openState('gateway_details', params, false);\r\n }\r\n\r\n function saveEntityObservable() {\r\n const formValues = vm.addEntityFormGroup.value;\r\n let entity = {\r\n name: formValues.entityName,\r\n type: formValues.type,\r\n label: formValues.entityLabel,\r\n additionalInfo: {\r\n gateway: true\r\n }\r\n };\r\n return deviceService.saveDevice(entity);\r\n }\r\n}\r\n", + "customFunction": "let $injector = widgetContext.$scope.$injector;\r\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\r\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\r\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\r\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\r\nlet entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService'));\r\nlet userSettingsService = $injector.get(widgetContext.servicesMap.get('userSettingsService'));\r\n\r\nopenAddEntityDialog();\r\n\r\nfunction openAddEntityDialog() {\r\n customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();\r\n}\r\n\r\nfunction AddEntityDialogController(instance) {\r\n let vm = instance;\r\n let userSettings;\r\n userSettingsService.loadUserSettings().subscribe(settings=> {\r\n userSettings = settings;\r\n if (!userSettings.createdGatewaysCount) userSettings.createdGatewaysCount = 0;\r\n });\r\n \r\n\r\n vm.addEntityFormGroup = vm.fb.group({\r\n entityName: ['', [vm.validators.required]],\r\n entityType: ['DEVICE'],\r\n entityLabel: [''],\r\n type: ['', [vm.validators.required]],\r\n });\r\n\r\n vm.cancel = function() {\r\n vm.dialogRef.close(null);\r\n };\r\n\r\n\r\n vm.save = function($event) {\r\n vm.addEntityFormGroup.markAsPristine();\r\n saveEntityObservable().subscribe(\r\n function (device) {\r\n widgetContext.updateAliases();\r\n userSettingsService.putUserSettings({ createdGatewaysCount: ++userSettings.createdGatewaysCount }).subscribe(_=>{\r\n });\r\n vm.dialogRef.close(null);\r\n openCommandDialog(device, $event);\r\n }\r\n );\r\n };\r\n \r\n function openCommandDialog(device, $event) {\r\n vm.device = device;\r\n let openCommandAction = widgetContext.actionsApi.getActionDescriptors(\"actionCellButton\").find(action => action.name == \"Docker commands\");\r\n widgetContext.actionsApi.handleWidgetAction($event, openCommandAction, device.id, device.name, {newDevice: true});\r\n goToConfigState();\r\n }\r\n\r\n \r\n function goToConfigState() {\r\n const stateParams = {};\r\n stateParams.entityId = vm.device.id;\r\n stateParams.entityName = vm.device.name;\r\n const newStateParams = {\r\n targetEntityParamName: 'default',\r\n new_gateway: {\r\n entityId: vm.device.id,\r\n entityName: vm.device.name\r\n }\r\n }\r\n const params = {...stateParams, ...newStateParams};\r\n widgetContext.stateController.openState('gateway_details', params, false);\r\n }\r\n\r\n function saveEntityObservable() {\r\n const formValues = vm.addEntityFormGroup.value;\r\n let entity = {\r\n name: formValues.entityName,\r\n type: formValues.type,\r\n label: formValues.entityLabel,\r\n additionalInfo: {\r\n gateway: true\r\n }\r\n };\r\n return deviceService.saveDevice(entity);\r\n }\r\n}\r\n", "customResources": [], "openInSeparateDialog": false, "openInPopover": false, @@ -567,7 +567,8 @@ "padding": "8px", "settings": { "useMarkdownTextFunction": true, - "markdownTextFunction": "var blockData = '';\nvar connectorsIndex = ctx.actionsApi.getActionDescriptors('elementClick').findIndex(action=>action.name==\"Connectors\");\nvar logsIndex = ctx.actionsApi.getActionDescriptors('elementClick').findIndex(action=>action.name==\"Logs\");\nfunction generateMatHeader(index) {\n if( index !== undefined && index > -1) {\n return ``\n } else {\n return \"\" \n }\n}\nfunction createDataBlock(value, label, dividerStyle, mobile, index) {\n blockData += `\n \n
\n \n ${generateMatHeader(index)}\n ${label}\n
\n ${value}\n `;\n}\ncreateDataBlock(data[0].Status, \"Status\", data[0].Status === \"Active\"? 'divider-green' : 'divider-red');\ncreateDataBlock(data[0].Name, \"Gateway Name\", '', ctx.isMobile);\ncreateDataBlock(data[0].Type, \"Gateway Type\", '');\ncreateDataBlock(\n `${(data[1]?data[1].count:0)} `\n + \" | \" + \n `${(data[2]?data[2][\"count 2\"]:0)} `\n , \"Devices (Active | Inactive)\", '');\ncreateDataBlock(\n `${(data[0].active_connectors?JSON.parse(data[0].active_connectors).length:0)} `\n + \" | \" + \n `${(data[0].inactive_connectors?JSON.parse(data[0].inactive_connectors).length:0)} `\n , \"Connectors (Active | Inactive)\", '', '', connectorsIndex);\ncreateDataBlock(data[0].ALL_ERRORS_COUNT || 0, \"Errors\", (data[0].ALL_ERRORS_COUNT || 0) === 0 ? 'divider-green' : 'divider-red', '', logsIndex);\nreturn `
${blockData}
`;", + "markdownTextPattern": "# Markdown/HTML card \\n - **Current entity**: **${entityName}**. \\n - **Current value**: **${Random}**.", + "markdownTextFunction": "var blockData = '';\nvar connectorsIndex = ctx.actionsApi.getActionDescriptors('elementClick').findIndex(action=>action.name==\"Connectors\");\nvar logsIndex = ctx.actionsApi.getActionDescriptors('elementClick').findIndex(action=>action.name==\"Logs\");\nfunction generateMatHeader(index) {\n if( index !== undefined && index > -1) {\n return ``\n } else {\n return \"\" \n }\n}\nfunction createDataBlock(value, label, dividerStyle, mobile, index) {\n blockData += `\n \n
\n \n ${generateMatHeader(index)}\n ${label}\n
\n ${value}\n `;\n}\ncreateDataBlock(data[0].Status, \"Status\", data[0].Status === \"Active\"? 'divider-green' : 'divider-red');\ncreateDataBlock(data[0].Name, \"Gateway Name\", '', ctx.isMobile);\ncreateDataBlock(data[0].Type, \"Gateway Type\", '');\ncreateDataBlock(\n `${(data[1]?data[1].count:0)} `\n + \" | \" + \n `${(data[2]?data[2][\"count 2\"]:0)} `\n , \"Devices (Active | Inactive)\", '');\ncreateDataBlock(\n `${(data[0].active_connectors?JSON.parse(data[0].active_connectors).length:0)} `\n + \" | \" + \n `${(data[0].inactive_connectors?JSON.parse(data[0].inactive_connectors).length:0)} `\n , \"Connectors (Enabled | Disabled)\", '', '', connectorsIndex);\ncreateDataBlock(data[0].ALL_ERRORS_COUNT || 0, \"Errors\", (data[0].ALL_ERRORS_COUNT || 0) === 0 ? 'divider-green' : 'divider-red', '', logsIndex);\nreturn `
${blockData}
`;", "applyDefaultMarkdownStyle": false, "markdownCss": ".divider {\n position: absolute;\n width: 3px;\n top: 8px;\n border-radius: 2px;\n bottom: 8px;\n border: 1px solid rgba(31, 70, 144, 1);\n background-color: rgba(31, 70, 144, 1);\n left: 10px;\n}\n.divider-green .divider {\n border: 1px solid rgb(25,128,56);\n background-color: rgb(25,128,56);\n}\n\n.divider-green .mat-mdc-card-content {\n color: rgb(25,128,56);\n}\n\n.divider-red .divider {\n border: 1px solid rgb(203,37,48);\n background-color: rgb(203,37,48);\n}\n\n.divider-red .mat-mdc-card-content {\n color: rgb(203,37,48);\n}\n\n.mdc-card {\n position: relative;\n padding-left: 10px;\n margin-bottom: 1px;\n}\n\n.mat-mdc-card-subtitle {\n font-weight: 400;\n font-size: 12px;\n}\n\n.mat-mdc-card-header {\n padding: 8px 16px 0;\n}\n\n.mat-mdc-card-content:last-child {\n padding-bottom: 8px;\n font-size: 16px;\n}\n\n.cards-container {\n height: calc(100% - 1px);\n justify-content: stretch;\n align-items: center;\n margin-bottom: 1px;\n}\n\n::ng-deep.tb-home-widget-link > div {\n flex-grow: 1;\n cursor: pointer;\n}\n\n .tb-home-widget-link {\n width: 100%;\n }\n\n .tb-home-widget-link:hover::after{\n color: inherit;\n }\n \n .tb-home-widget-link::after{\n content: 'arrow_forward';\n display: inline-block;\n transform: rotate(315deg);\n font-family: 'Material Icons';\n font-weight: normal;\n font-style: normal;\n font-size: 18px;\n color: rgba(0, 0, 0, 0.12);\n vertical-align: bottom;\n margin-left: 6px;\n}" }, @@ -5218,7 +5219,7 @@ "widgets": { "02a05113-d878-b086-328e-75bd6f02b2bb": { "sizeX": 24, - "sizeY": 11, + "sizeY": 12, "row": 0, "col": 0 } @@ -5230,7 +5231,7 @@ "backgroundSizeMode": "100%", "autoFillHeight": true, "backgroundImageUrl": null, - "mobileAutoFillHeight": false, + "mobileAutoFillHeight": true, "mobileRowHeight": 70, "outerMargin": true } @@ -6741,4 +6742,4 @@ }, "externalId": null, "name": "Gateway" -} +} \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java index 0c3146a6c6..38f44898ee 100644 --- a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java @@ -33,7 +33,6 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MClientCredential; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode; -import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; import org.thingsboard.server.common.data.device.data.DeviceData; import org.thingsboard.server.common.data.device.data.PowerMode; import org.thingsboard.server.common.data.device.data.SnmpDeviceTransportConfiguration; @@ -68,6 +67,8 @@ import java.util.Set; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import static org.eclipse.leshan.core.LwM2m.Version.V1_0; + @Service @TbCoreComponent @RequiredArgsConstructor @@ -256,7 +257,7 @@ public class DeviceBulkImportService extends AbstractBulkImportService { Lwm2mDeviceProfileTransportConfiguration transportConfiguration = new Lwm2mDeviceProfileTransportConfiguration(); transportConfiguration.setBootstrap(Collections.emptyList()); - transportConfiguration.setClientLwM2mSettings(new OtherConfiguration(1, 1, 1, PowerMode.DRX, null, null, null, null, null)); + transportConfiguration.setClientLwM2mSettings(new OtherConfiguration(1, 1, 1, PowerMode.DRX, null, null, null, null, null, V1_0.toString())); transportConfiguration.setObserveAttr(new TelemetryMappingConfiguration(Collections.emptyMap(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptyMap())); DeviceProfileData deviceProfileData = new DeviceProfileData(); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java index 0c91d72ea5..438cb58b91 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java @@ -22,6 +22,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; +import org.springframework.core.env.Environment; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; @@ -39,11 +40,15 @@ import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.UUID; @Slf4j public abstract class AbstractTbEntityService { + @Autowired + private Environment env; + @Value("${server.log_controller_error_stack_trace}") @Getter private boolean logControllerErrorStackTrace; @@ -67,6 +72,10 @@ public abstract class AbstractTbEntityService { @Lazy private EntitiesVersionControlService vcService; + protected boolean isTestProfile() { + return Set.of(this.env.getActiveProfiles()).contains("test"); + } + protected T checkNotNull(T reference) throws ThingsboardException { return checkNotNull(reference, "Requested item wasn't found!"); } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java index 8096722940..74061ee9e5 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.entitiy.tenant; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.beans.factory.annotation.Value; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.TenantId; @@ -52,7 +53,9 @@ public class DefaultTbTenantService extends AbstractTbEntityService implements T Tenant savedTenant = tenantService.saveTenant(tenant, tenantId -> { installScripts.createDefaultRuleChains(tenantId); installScripts.createDefaultEdgeRuleChains(tenantId); - installScripts.createDefaultTenantDashboards(tenantId, null); + if (!isTestProfile()) { + installScripts.createDefaultTenantDashboards(tenantId, null); + } }); tenantProfileCache.evict(savedTenant.getId()); diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java index ae16f925a0..37a55c99d6 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java @@ -27,11 +27,11 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperService import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; import org.thingsboard.server.queue.housekeeper.HousekeeperConfig; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.queue.consumer.QueueConsumerManager; import javax.annotation.PreDestroy; import java.util.LinkedHashSet; diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java index 06e38d0160..eea92eb75b 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java @@ -27,13 +27,13 @@ import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; import org.thingsboard.server.queue.housekeeper.HousekeeperConfig; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.housekeeper.processor.HousekeeperTaskProcessor; import org.thingsboard.server.service.housekeeper.stats.HousekeeperStatsService; -import org.thingsboard.server.service.queue.consumer.QueueConsumerManager; import javax.annotation.PreDestroy; import java.util.List; diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java index 999079e913..9db2a99e7f 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java +++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java @@ -317,7 +317,8 @@ public class InstallScripts { @SneakyThrows public void loadSystemImages() { log.info("Loading system images..."); - Stream dashboardsFiles = Files.list(Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, DASHBOARDS_DIR)); + Stream dashboardsFiles = Stream.concat(Files.list(Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, DASHBOARDS_DIR)), + Files.list(Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, DASHBOARDS_DIR))); try (dashboardsFiles) { dashboardsFiles.forEach(file -> { try { diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index 06cbdc2230..9626190f1b 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -39,6 +39,7 @@ import org.thingsboard.server.dao.device.DeviceConnectivityConfiguration; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.sql.JpaExecutorService; +import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.component.RuleNodeClassInfo; import org.thingsboard.server.utils.TbNodeUpgradeUtils; @@ -77,6 +78,8 @@ public class DefaultDataUpdateService implements DataUpdateService { @Autowired private CustomerService customerService; + @Autowired + private TenantProfileService tenantProfileService; @Override public void updateData(String fromVersion) throws Exception { @@ -88,12 +91,31 @@ public class DefaultDataUpdateService implements DataUpdateService { case "3.6.4": log.info("Updating data from version 3.6.4 to 3.7.0 ..."); updateCustomersWithTheSameTitle(); + updateMaxRuleNodeExecsPerMessage(); break; default: throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion); } } + private void updateMaxRuleNodeExecsPerMessage() { + var tenantProfiles = new PageDataIterable<>( + link -> tenantProfileService.findTenantProfiles(TenantId.SYS_TENANT_ID, link), DEFAULT_PAGE_SIZE); + tenantProfiles.forEach(tenantProfile -> { + var configurationOpt = tenantProfile.getProfileConfiguration(); + configurationOpt.ifPresent(configuration -> { + if (configuration.getMaxRuleNodeExecsPerMessage() == 0) { + configuration.setMaxRuleNodeExecutionsPerMessage(1000); + try { + tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + } catch (Exception e) { + log.error("Failed to update tenant profile with id: {} due to: ", tenantProfile.getId(), e); + } + } + }); + }); + } + private void updateCustomersWithTheSameTitle() { var customers = new ArrayList(); new PageDataIterable<>(pageLink -> diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java index 29db1d7d76..257dfa14b1 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java @@ -16,12 +16,13 @@ package org.thingsboard.server.service.mail; import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.Futures; import freemarker.template.Configuration; import freemarker.template.Template; -import jakarta.xml.bind.DatatypeConverter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Lazy; import org.springframework.core.NestedRuntimeException; @@ -31,29 +32,36 @@ import org.springframework.mail.javamail.JavaMailSenderImpl; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Service; import org.springframework.ui.freemarker.FreeMarkerTemplateUtils; +import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.TbEmail; +import org.thingsboard.server.cache.limits.RateLimitService; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.ApiFeature; import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.ApiUsageRecordState; import org.thingsboard.server.common.data.ApiUsageStateValue; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.exception.RateLimitExceededException; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import jakarta.mail.internet.MimeMessage; import java.io.ByteArrayInputStream; import java.util.HashMap; import java.util.Locale; import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -76,13 +84,21 @@ public class DefaultMailService implements MailService { private TbApiUsageStateService apiUsageStateService; @Autowired - private MailExecutorService mailExecutorService; + private MailSenderInternalExecutorService mailExecutorService; @Autowired private PasswordResetExecutorService passwordResetExecutorService; @Autowired - private TbMailContextComponent tbMailContextComponent; + private TbMailContextComponent ctx; + + @Autowired + private RateLimitService rateLimitService; + + @Value("${mail.per_tenant_rate_limits:}") + private String perTenantRateLimitConfig; + + private final ScheduledExecutorService timeoutScheduler; private TbMailSender mailSender; @@ -95,6 +111,7 @@ public class DefaultMailService implements MailService { this.freemarkerConfig = freemarkerConfig; this.adminSettingsService = adminSettingsService; this.apiUsageClient = apiUsageClient; + this.timeoutScheduler = Executors.newScheduledThreadPool(1, ThingsBoardThreadFactory.forName("mail-service-watchdog")); } @PostConstruct @@ -102,12 +119,19 @@ public class DefaultMailService implements MailService { updateMailConfiguration(); } + @PreDestroy + public void destroy() { + if (timeoutScheduler != null) { + timeoutScheduler.shutdownNow(); + } + } + @Override public void updateMailConfiguration() { AdminSettings settings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail"); if (settings != null) { JsonNode jsonConfig = settings.getJsonValue(); - mailSender = new TbMailSender(tbMailContextComponent, jsonConfig); + mailSender = new TbMailSender(ctx, jsonConfig); mailFrom = jsonConfig.get("mailFrom").asText(); timeout = jsonConfig.get("timeout").asLong(DEFAULT_TIMEOUT); } else { @@ -122,7 +146,7 @@ public class DefaultMailService implements MailService { @Override public void sendTestMail(JsonNode jsonConfig, String email) throws ThingsboardException { - TbMailSender testMailSender = new TbMailSender(tbMailContextComponent, jsonConfig); + TbMailSender testMailSender = new TbMailSender(ctx, jsonConfig); String mailFrom = jsonConfig.get("mailFrom").asText(); String subject = messages.getMessage("test.message.subject", null, Locale.US); long timeout = jsonConfig.get("timeout").asLong(DEFAULT_TIMEOUT); @@ -214,6 +238,10 @@ public class DefaultMailService implements MailService { private void sendMail(TenantId tenantId, CustomerId customerId, TbEmail tbEmail, JavaMailSender javaMailSender, long timeout) throws ThingsboardException { if (apiUsageStateService.getApiUsageState(tenantId).isEmailSendEnabled()) { + if (tenantId != null && !tenantId.isSysTenantId() && StringUtils.isNotEmpty(perTenantRateLimitConfig) && + !rateLimitService.checkRateLimit(LimitedApi.EMAILS, (Object) tenantId, perTenantRateLimitConfig)) { + throw new RateLimitExceededException(LimitedApi.EMAILS); + } try { MimeMessage mailMsg = javaMailSender.createMimeMessage(); boolean multipart = (tbEmail.getImages() != null && !tbEmail.getImages().isEmpty()); @@ -415,8 +443,11 @@ public class DefaultMailService implements MailService { } private void sendMailWithTimeout(JavaMailSender mailSender, MimeMessage msg, long timeout) { + var submittedMail = Futures.withTimeout( + mailExecutorService.submit(() -> mailSender.send(msg)), + timeout, TimeUnit.MILLISECONDS, timeoutScheduler); try { - mailExecutorService.submit(() -> mailSender.send(msg)).get(timeout, TimeUnit.MILLISECONDS); + submittedMail.get(timeout, TimeUnit.MILLISECONDS); } catch (TimeoutException e) { log.debug("Error during mail submission", e); throw new RuntimeException("Timeout!"); diff --git a/application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java b/application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java new file mode 100644 index 0000000000..0c8feff6c5 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2024 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. + */ +package org.thingsboard.server.service.mail; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.AbstractListeningExecutor; + +/** + * Executor have the sole purpose to send mails. It should be used only by Mail Service. + * For other purposes please use the MailExecutorService component + * */ +@Component +public class MailSenderInternalExecutorService extends AbstractListeningExecutor { + + @Value("${actors.rule.mail_thread_pool_size}") + private int mailExecutorThreadPoolSize; + + @Override + protected int getThreadPollSize() { + return mailExecutorThreadPoolSize; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/mail/TbMailContextComponent.java b/application/src/main/java/org/thingsboard/server/service/mail/TbMailContextComponent.java index 4df152936f..97c20fdcda 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/TbMailContextComponent.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/TbMailContextComponent.java @@ -20,7 +20,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import org.thingsboard.server.dao.settings.AdminSettingsService; -import org.thingsboard.server.queue.util.TbCoreComponent; @Component @Data diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 42c516a8a3..fbef0cfd29 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -20,12 +20,12 @@ import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; +import lombok.Data; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @@ -43,6 +43,7 @@ import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger; +import org.thingsboard.server.common.data.queue.QueueConfig; import org.thingsboard.server.common.data.rpc.RpcError; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.TbActorMsg; @@ -78,10 +79,11 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceM import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; -import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.edge.EdgeNotificationService; @@ -89,6 +91,7 @@ import org.thingsboard.server.service.notification.NotificationSchedulerService; import org.thingsboard.server.service.ota.OtaPackageStateService; import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; +import org.thingsboard.server.service.queue.consumer.MainQueueConsumerManager; import org.thingsboard.server.service.queue.processing.AbstractConsumerService; import org.thingsboard.server.service.queue.processing.IdMsgPair; import org.thingsboard.server.service.resource.TbImageService; @@ -109,7 +112,6 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -122,9 +124,11 @@ import java.util.stream.Collectors; public class DefaultTbCoreConsumerService extends AbstractConsumerService implements TbCoreConsumerService { @Value("${queue.core.poll-interval}") - private long pollDuration; + private long pollInterval; @Value("${queue.core.pack-processing-timeout}") private long packProcessingTimeout; + @Value("${queue.core.consumer-per-partition:true}") + private boolean consumerPerPartition; @Value("${queue.core.stats.enabled:false}") private boolean statsEnabled; @@ -133,7 +137,6 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> mainConsumer; private final DeviceStateService stateService; private final TbApiUsageStateService statsService; private final TbLocalSubscriptionService localSubscriptionService; @@ -144,14 +147,14 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> usageStatsConsumer; - private final TbQueueConsumer> firmwareStatesConsumer; + private final TbCoreQueueFactory queueFactory; private final TbImageService imageService; + private final TbCoreConsumerStats stats; + + private MainQueueConsumerManager, CoreQueueConfig> mainConsumer; + private QueueConsumerManager> usageStatsConsumer; + private QueueConsumerManager> firmwareStatesConsumer; - protected volatile ExecutorService consumersExecutor; - protected volatile ExecutorService usageStatsExecutor; - private volatile ExecutorService firmwareStatesExecutor; private volatile ListeningExecutorService deviceActivityEventsExecutor; public DefaultTbCoreConsumerService(TbCoreQueueFactory tbCoreQueueFactory, @@ -176,10 +179,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService, CoreQueueConfig>builder() + .queueKey(new QueueKey(ServiceType.TB_CORE)) + .config(CoreQueueConfig.of(consumerPerPartition, (int) pollInterval)) + .msgPackProcessor(this::processMsgs) + .consumerCreator(config -> queueFactory.createToCoreMsgConsumer()) + .consumerExecutor(consumersExecutor) + .scheduler(scheduler) + .taskExecutor(mgmtExecutor) + .build(); + this.usageStatsConsumer = QueueConsumerManager.>builder() + .name("TB Usage Stats") + .msgPackProcessor(this::processUsageStatsMsg) + .pollInterval(pollInterval) + .consumerCreator(queueFactory::createToUsageStatsServiceMsgConsumer) + .consumerExecutor(consumersExecutor) + .threadPrefix("usage-stats") + .build(); + this.firmwareStatesConsumer = QueueConsumerManager.>builder() + .name("TB Ota Package States") + .msgPackProcessor(this::processFirmwareMsgs) + .pollInterval(pollInterval) + .consumerCreator(queueFactory::createToOtaPackageStateServiceMsgConsumer) + .consumerExecutor(consumersExecutor) + .threadPrefix("firmware") + .build(); } @PreDestroy public void destroy() { super.destroy(); - if (consumersExecutor != null) { - consumersExecutor.shutdownNow(); - } - if (usageStatsExecutor != null) { - usageStatsExecutor.shutdownNow(); - } - if (firmwareStatesExecutor != null) { - firmwareStatesExecutor.shutdownNow(); - } if (deviceActivityEventsExecutor != null) { deviceActivityEventsExecutor.shutdownNow(); } } - @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) - public void onApplicationEvent(ApplicationReadyEvent event) { - super.onApplicationEvent(event); - launchUsageStatsConsumer(); - launchOtaPackageUpdateNotificationConsumer(); + @Override + protected void startConsumers() { + super.startConsumers(); + firmwareStatesConsumer.subscribe(); + firmwareStatesConsumer.launch(); + usageStatsConsumer.launch(); } @Override protected void onTbApplicationEvent(PartitionChangeEvent event) { log.info("Subscribing to partitions: {}", event.getPartitions()); - this.mainConsumer.subscribe(event.getPartitions()); - this.usageStatsConsumer.subscribe( - event - .getPartitions() - .stream() - .map(tpi -> tpi.newByTopic(usageStatsConsumer.getTopic())) - .collect(Collectors.toSet())); - this.firmwareStatesConsumer.subscribe(); - } - - @Override - protected void launchMainConsumers() { - consumersExecutor.submit(() -> { - while (!stopped) { + mainConsumer.update(event.getPartitions()); + usageStatsConsumer.subscribe(event.getPartitions() + .stream() + .map(tpi -> tpi.newByTopic(usageStatsConsumer.getConsumer().getTopic())) + .collect(Collectors.toSet())); + } + + private void processMsgs(List> msgs, TbQueueConsumer> consumer, CoreQueueConfig config) throws Exception { + List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).collect(Collectors.toList()); + ConcurrentMap> pendingMap = orderedMsgList.stream().collect( + Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); + CountDownLatch processingTimeoutLatch = new CountDownLatch(1); + TbPackProcessingContext> ctx = new TbPackProcessingContext<>( + processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); + PendingMsgHolder pendingMsgHolder = new PendingMsgHolder(); + Future packSubmitFuture = consumersExecutor.submit(() -> { + orderedMsgList.forEach((element) -> { + UUID id = element.getUuid(); + TbProtoQueueMsg msg = element.getMsg(); + log.trace("[{}] Creating main callback for message: {}", id, msg.getValue()); + TbCallback callback = new TbPackCallback<>(id, ctx); try { - List> msgs = mainConsumer.poll(pollDuration); - if (msgs.isEmpty()) { - continue; - } - List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).collect(Collectors.toList()); - ConcurrentMap> pendingMap = orderedMsgList.stream().collect( - Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); - CountDownLatch processingTimeoutLatch = new CountDownLatch(1); - TbPackProcessingContext> ctx = new TbPackProcessingContext<>( - processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); - PendingMsgHolder pendingMsgHolder = new PendingMsgHolder(); - Future packSubmitFuture = consumersExecutor.submit(() -> { - orderedMsgList.forEach((element) -> { - UUID id = element.getUuid(); - TbProtoQueueMsg msg = element.getMsg(); - log.trace("[{}] Creating main callback for message: {}", id, msg.getValue()); - TbCallback callback = new TbPackCallback<>(id, ctx); - try { - ToCoreMsg toCoreMsg = msg.getValue(); - pendingMsgHolder.setToCoreMsg(toCoreMsg); - if (toCoreMsg.hasToSubscriptionMgrMsg()) { - log.trace("[{}] Forwarding message to subscription manager service {}", id, toCoreMsg.getToSubscriptionMgrMsg()); - forwardToSubMgrService(toCoreMsg.getToSubscriptionMgrMsg(), callback); - } else if (toCoreMsg.hasToDeviceActorMsg()) { - log.trace("[{}] Forwarding message to device actor {}", id, toCoreMsg.getToDeviceActorMsg()); - forwardToDeviceActor(toCoreMsg.getToDeviceActorMsg(), callback); - } else if (toCoreMsg.hasDeviceStateServiceMsg()) { - log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceStateServiceMsg()); - forwardToStateService(toCoreMsg.getDeviceStateServiceMsg(), callback); - } else if (toCoreMsg.hasEdgeNotificationMsg()) { - log.trace("[{}] Forwarding message to edge service {}", id, toCoreMsg.getEdgeNotificationMsg()); - forwardToEdgeNotificationService(toCoreMsg.getEdgeNotificationMsg(), callback); - } else if (toCoreMsg.hasDeviceConnectMsg()) { - log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceConnectMsg()); - forwardToStateService(toCoreMsg.getDeviceConnectMsg(), callback); - } else if (toCoreMsg.hasDeviceActivityMsg()) { - log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceActivityMsg()); - forwardToStateService(toCoreMsg.getDeviceActivityMsg(), callback); - } else if (toCoreMsg.hasDeviceDisconnectMsg()) { - log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceDisconnectMsg()); - forwardToStateService(toCoreMsg.getDeviceDisconnectMsg(), callback); - } else if (toCoreMsg.hasDeviceInactivityMsg()) { - log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceInactivityMsg()); - forwardToStateService(toCoreMsg.getDeviceInactivityMsg(), callback); - } else if (toCoreMsg.hasToDeviceActorNotification()) { - TbActorMsg actorMsg = ProtoUtils.fromProto(toCoreMsg.getToDeviceActorNotification()); - if (actorMsg != null) { - if (actorMsg.getMsgType().equals(MsgType.DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG)) { - tbCoreDeviceRpcService.forwardRpcRequestToDeviceActor((ToDeviceRpcRequestActorMsg) actorMsg); - } else { - log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg); - actorContext.tell(actorMsg); - } - } - callback.onSuccess(); - } else if (toCoreMsg.hasNotificationSchedulerServiceMsg()) { - TransportProtos.NotificationSchedulerServiceMsg notificationSchedulerServiceMsg = toCoreMsg.getNotificationSchedulerServiceMsg(); - log.trace("[{}] Forwarding message to notification scheduler service {}", id, toCoreMsg.getNotificationSchedulerServiceMsg()); - forwardToNotificationSchedulerService(notificationSchedulerServiceMsg, callback); - } else if (toCoreMsg.hasErrorEventMsg()) { - forwardToEventService(toCoreMsg.getErrorEventMsg(), callback); - } else if (toCoreMsg.hasLifecycleEventMsg()) { - forwardToEventService(toCoreMsg.getLifecycleEventMsg(), callback); - } - } catch (Throwable e) { - log.warn("[{}] Failed to process message: {}", id, msg, e); - callback.onFailure(e); + ToCoreMsg toCoreMsg = msg.getValue(); + pendingMsgHolder.setToCoreMsg(toCoreMsg); + if (toCoreMsg.hasToSubscriptionMgrMsg()) { + log.trace("[{}] Forwarding message to subscription manager service {}", id, toCoreMsg.getToSubscriptionMgrMsg()); + forwardToSubMgrService(toCoreMsg.getToSubscriptionMgrMsg(), callback); + } else if (toCoreMsg.hasToDeviceActorMsg()) { + log.trace("[{}] Forwarding message to device actor {}", id, toCoreMsg.getToDeviceActorMsg()); + forwardToDeviceActor(toCoreMsg.getToDeviceActorMsg(), callback); + } else if (toCoreMsg.hasDeviceStateServiceMsg()) { + log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceStateServiceMsg()); + forwardToStateService(toCoreMsg.getDeviceStateServiceMsg(), callback); + } else if (toCoreMsg.hasEdgeNotificationMsg()) { + log.trace("[{}] Forwarding message to edge service {}", id, toCoreMsg.getEdgeNotificationMsg()); + forwardToEdgeNotificationService(toCoreMsg.getEdgeNotificationMsg(), callback); + } else if (toCoreMsg.hasDeviceConnectMsg()) { + log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceConnectMsg()); + forwardToStateService(toCoreMsg.getDeviceConnectMsg(), callback); + } else if (toCoreMsg.hasDeviceActivityMsg()) { + log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceActivityMsg()); + forwardToStateService(toCoreMsg.getDeviceActivityMsg(), callback); + } else if (toCoreMsg.hasDeviceDisconnectMsg()) { + log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceDisconnectMsg()); + forwardToStateService(toCoreMsg.getDeviceDisconnectMsg(), callback); + } else if (toCoreMsg.hasDeviceInactivityMsg()) { + log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceInactivityMsg()); + forwardToStateService(toCoreMsg.getDeviceInactivityMsg(), callback); + } else if (toCoreMsg.hasToDeviceActorNotification()) { + TbActorMsg actorMsg = ProtoUtils.fromProto(toCoreMsg.getToDeviceActorNotification()); + if (actorMsg != null) { + if (actorMsg.getMsgType().equals(MsgType.DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG)) { + tbCoreDeviceRpcService.forwardRpcRequestToDeviceActor((ToDeviceRpcRequestActorMsg) actorMsg); + } else { + log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg); + actorContext.tell(actorMsg); } - }); - }); - if (!processingTimeoutLatch.await(packProcessingTimeout, TimeUnit.MILLISECONDS)) { - if (!packSubmitFuture.isDone()) { - packSubmitFuture.cancel(true); - ToCoreMsg lastSubmitMsg = pendingMsgHolder.getToCoreMsg(); - log.info("Timeout to process message: {}", lastSubmitMsg); - } - ctx.getAckMap().forEach((id, msg) -> log.debug("[{}] Timeout to process message: {}", id, msg.getValue())); - ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process message: {}", id, msg.getValue())); - } - mainConsumer.commit(); - } catch (Exception e) { - if (!stopped) { - log.warn("Failed to obtain messages from queue.", e); - try { - Thread.sleep(pollDuration); - } catch (InterruptedException e2) { - log.trace("Failed to wait until the server has capacity to handle new requests", e2); } + callback.onSuccess(); + } else if (toCoreMsg.hasNotificationSchedulerServiceMsg()) { + TransportProtos.NotificationSchedulerServiceMsg notificationSchedulerServiceMsg = toCoreMsg.getNotificationSchedulerServiceMsg(); + log.trace("[{}] Forwarding message to notification scheduler service {}", id, toCoreMsg.getNotificationSchedulerServiceMsg()); + forwardToNotificationSchedulerService(notificationSchedulerServiceMsg, callback); + } else if (toCoreMsg.hasErrorEventMsg()) { + forwardToEventService(toCoreMsg.getErrorEventMsg(), callback); + } else if (toCoreMsg.hasLifecycleEventMsg()) { + forwardToEventService(toCoreMsg.getLifecycleEventMsg(), callback); } + } catch (Throwable e) { + log.warn("[{}] Failed to process message: {}", id, msg, e); + callback.onFailure(e); } - } - log.info("TB Core Consumer stopped."); + }); }); + if (!processingTimeoutLatch.await(packProcessingTimeout, TimeUnit.MILLISECONDS)) { + if (!packSubmitFuture.isDone()) { + packSubmitFuture.cancel(true); + ToCoreMsg lastSubmitMsg = pendingMsgHolder.getToCoreMsg(); + log.info("Timeout to process message: {}", lastSubmitMsg); + } + if (log.isDebugEnabled()) { + ctx.getAckMap().forEach((id, msg) -> log.debug("[{}] Timeout to process message: {}", id, msg.getValue())); + } + ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process message: {}", id, msg.getValue())); + } + consumer.commit(); } private static class PendingMsgHolder { @@ -353,7 +347,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> createNotificationsConsumer() { + return queueFactory.createToCoreNotificationsMsgConsumer(); + } + @Override protected void handleNotification(UUID id, TbProtoQueueMsg msg, TbCallback callback) { ToCoreNotificationMsg toCoreNotification = msg.getValue(); @@ -409,92 +413,55 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService { - while (!stopped) { - try { - List> msgs = usageStatsConsumer.poll(getNotificationPollDuration()); - if (msgs.isEmpty()) { - continue; - } - ConcurrentMap> pendingMap = msgs.stream().collect( - Collectors.toConcurrentMap(s -> UUID.randomUUID(), Function.identity())); - CountDownLatch processingTimeoutLatch = new CountDownLatch(1); - TbPackProcessingContext> ctx = new TbPackProcessingContext<>( - processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); - pendingMap.forEach((id, msg) -> { - log.trace("[{}] Creating usage stats callback for message: {}", id, msg.getValue()); - TbCallback callback = new TbPackCallback<>(id, ctx); - try { - handleUsageStats(msg, callback); - } catch (Throwable e) { - log.warn("[{}] Failed to process usage stats: {}", id, msg, e); - callback.onFailure(e); - } - }); - if (!processingTimeoutLatch.await(getNotificationPackProcessingTimeout(), TimeUnit.MILLISECONDS)) { - ctx.getAckMap().forEach((id, msg) -> log.warn("[{}] Timeout to process usage stats: {}", id, msg.getValue())); - ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process usage stats: {}", id, msg.getValue())); - } - usageStatsConsumer.commit(); - } catch (Exception e) { - if (!stopped) { - log.warn("Failed to obtain usage stats from queue.", e); - try { - Thread.sleep(getNotificationPollDuration()); - } catch (InterruptedException e2) { - log.trace("Failed to wait until the server has capacity to handle new usage stats", e2); - } - } - } + private void processUsageStatsMsg(List> msgs, TbQueueConsumer> consumer) throws Exception { + ConcurrentMap> pendingMap = msgs.stream().collect( + Collectors.toConcurrentMap(s -> UUID.randomUUID(), Function.identity())); + CountDownLatch processingTimeoutLatch = new CountDownLatch(1); + TbPackProcessingContext> ctx = new TbPackProcessingContext<>( + processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); + pendingMap.forEach((id, msg) -> { + log.trace("[{}] Creating usage stats callback for message: {}", id, msg.getValue()); + TbCallback callback = new TbPackCallback<>(id, ctx); + try { + handleUsageStats(msg, callback); + } catch (Throwable e) { + log.warn("[{}] Failed to process usage stats: {}", id, msg, e); + callback.onFailure(e); } - log.info("TB Usage Stats Consumer stopped."); }); + if (!processingTimeoutLatch.await(getNotificationPackProcessingTimeout(), TimeUnit.MILLISECONDS)) { + ctx.getAckMap().forEach((id, msg) -> log.warn("[{}] Timeout to process usage stats: {}", id, msg.getValue())); + ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process usage stats: {}", id, msg.getValue())); + } + consumer.commit(); + } - private void launchOtaPackageUpdateNotificationConsumer() { + private void processFirmwareMsgs(List> msgs, TbQueueConsumer> consumer) { long maxProcessingTimeoutPerRecord = firmwarePackInterval / firmwarePackSize; - firmwareStatesExecutor.submit(() -> { - while (!stopped) { - try { - List> msgs = firmwareStatesConsumer.poll(getNotificationPollDuration()); - if (msgs.isEmpty()) { - continue; - } - long timeToSleep = maxProcessingTimeoutPerRecord; - for (TbProtoQueueMsg msg : msgs) { - try { - long startTime = System.currentTimeMillis(); - boolean isSuccessUpdate = handleOtaPackageUpdates(msg); - long endTime = System.currentTimeMillis(); - long spentTime = endTime - startTime; - timeToSleep = timeToSleep - spentTime; - if (isSuccessUpdate) { - if (timeToSleep > 0) { - log.debug("Spent time per record is: [{}]!", spentTime); - Thread.sleep(timeToSleep); - timeToSleep = 0; - } - timeToSleep += maxProcessingTimeoutPerRecord; - } - } catch (Throwable e) { - log.warn("Failed to process firmware update msg: {}", msg, e); - } - } - firmwareStatesConsumer.commit(); - } catch (Exception e) { - if (!stopped) { - log.warn("Failed to obtain usage stats from queue.", e); - try { - Thread.sleep(getNotificationPollDuration()); - } catch (InterruptedException e2) { - log.trace("Failed to wait until the server has capacity to handle new firmware updates", e2); - } + long timeToSleep = maxProcessingTimeoutPerRecord; + for (TbProtoQueueMsg msg : msgs) { + try { + long startTime = System.currentTimeMillis(); + boolean isSuccessUpdate = handleOtaPackageUpdates(msg); + long endTime = System.currentTimeMillis(); + long spentTime = endTime - startTime; + timeToSleep = timeToSleep - spentTime; + if (isSuccessUpdate) { + if (timeToSleep > 0) { + log.debug("Spent time per record is: [{}]!", spentTime); + Thread.sleep(timeToSleep); + timeToSleep = 0; } + timeToSleep += maxProcessingTimeoutPerRecord; } + } catch (InterruptedException e) { + return; + } catch (Throwable e) { + log.warn("Failed to process firmware update msg: {}", msg, e); } - log.info("TB Ota Package States Consumer stopped."); - }); + } + consumer.commit(); } private void handleUsageStats(TbProtoQueueMsg msg, TbCallback callback) { @@ -782,15 +749,17 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService consumers = new ConcurrentHashMap<>(); public DefaultTbRuleEngineConsumerService(TbRuleEngineConsumerContext ctx, - TbRuleEngineQueueFactory tbRuleEngineQueueFactory, ActorSystemContext actorContext, TbRuleEngineDeviceRpcService tbDeviceRpcService, QueueService queueService, @@ -88,8 +84,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< PartitionService partitionService, ApplicationEventPublisher eventPublisher, JwtSettingsService jwtSettingsService) { - super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, - eventPublisher, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer(), jwtSettingsService); + super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService); this.ctx = ctx; this.tbDeviceRpcService = tbDeviceRpcService; this.queueService = queueService; @@ -97,7 +92,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< @PostConstruct public void init() { - super.init("tb-rule-engine-notifications-consumer"); + super.init("tb-rule-engine"); List queues = queueService.findAllQueues(); for (Queue configuration : queues) { if (partitionService.isManagedByCurrentService(configuration.getTenantId())) { @@ -137,20 +132,11 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< }); } - @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) - public void onApplicationEvent(ApplicationReadyEvent event) { - super.onApplicationEvent(event); - ctx.setReady(true); - } - - @Override - protected void launchMainConsumers() {} - @Override protected void stopConsumers() { + super.stopConsumers(); consumers.values().forEach(TbRuleEngineQueueConsumerManager::stop); consumers.values().forEach(TbRuleEngineQueueConsumerManager::awaitStop); - ctx.stop(); } @Override @@ -168,6 +154,16 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< return ctx.getPackProcessingTimeout(); } + @Override + protected int getMgmtThreadPoolSize() { + return ctx.getMgmtThreadPoolSize(); + } + + @Override + protected TbQueueConsumer> createNotificationsConsumer() { + return ctx.getQueueFactory().createToRuleEngineNotificationsMsgConsumer(); + } + @Override protected void handleNotification(UUID id, TbProtoQueueMsg msg, TbCallback callback) throws Exception { ToRuleEngineNotificationMsg nfMsg = msg.getValue(); @@ -244,7 +240,13 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< } private TbRuleEngineQueueConsumerManager createConsumer(QueueKey queueKey, Queue queue) { - var consumer = new TbRuleEngineQueueConsumerManager(ctx, queueKey); + var consumer = TbRuleEngineQueueConsumerManager.create() + .ctx(ctx) + .queueKey(queueKey) + .consumerExecutor(consumersExecutor) + .scheduler(scheduler) + .taskExecutor(mgmtExecutor) + .build(); consumers.put(queueKey, consumer); consumer.init(queue); return consumer; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/consumer/MainQueueConsumerManager.java b/application/src/main/java/org/thingsboard/server/service/queue/consumer/MainQueueConsumerManager.java new file mode 100644 index 0000000000..6792422b57 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/queue/consumer/MainQueueConsumerManager.java @@ -0,0 +1,322 @@ +/** + * Copyright © 2016-2024 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. + */ +package org.thingsboard.server.service.queue.consumer; + +import lombok.Builder; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.queue.QueueConfig; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.queue.TbQueueConsumer; +import org.thingsboard.server.queue.TbQueueMsg; +import org.thingsboard.server.queue.discovery.QueueKey; +import org.thingsboard.server.service.queue.ruleengine.QueueEvent; +import org.thingsboard.server.service.queue.ruleengine.TbQueueConsumerManagerTask; +import org.thingsboard.server.service.queue.ruleengine.TbQueueConsumerTask; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Slf4j +public class MainQueueConsumerManager { + + protected final QueueKey queueKey; + @Getter + protected C config; + protected final MsgPackProcessor msgPackProcessor; + protected final Function> consumerCreator; + protected final ExecutorService consumerExecutor; + protected final ScheduledExecutorService scheduler; + protected final ExecutorService taskExecutor; + + private final java.util.Queue tasks = new ConcurrentLinkedQueue<>(); + private final ReentrantLock lock = new ReentrantLock(); + + @Getter + private volatile Set partitions; + protected volatile ConsumerWrapper consumerWrapper; + protected volatile boolean stopped; + + @Builder + public MainQueueConsumerManager(QueueKey queueKey, C config, + MsgPackProcessor msgPackProcessor, + Function> consumerCreator, + ExecutorService consumerExecutor, + ScheduledExecutorService scheduler, + ExecutorService taskExecutor) { + this.queueKey = queueKey; + this.config = config; + this.msgPackProcessor = msgPackProcessor; + this.consumerCreator = consumerCreator; + this.consumerExecutor = consumerExecutor; + this.scheduler = scheduler; + this.taskExecutor = taskExecutor; + if (config != null) { + init(config); + } + } + + public void init(C config) { + this.config = config; + if (config.isConsumerPerPartition()) { + this.consumerWrapper = new ConsumerPerPartitionWrapper(); + } else { + this.consumerWrapper = new SingleConsumerWrapper(); + } + log.debug("[{}] Initialized consumer for queue: {}", queueKey, config); + } + + public void update(C config) { + addTask(TbQueueConsumerManagerTask.configUpdate(config)); + } + + public void update(Set partitions) { + addTask(TbQueueConsumerManagerTask.partitionChange(partitions)); + } + + protected void addTask(TbQueueConsumerManagerTask todo) { + if (stopped) { + return; + } + tasks.add(todo); + log.trace("[{}] Added task: {}", queueKey, todo); + tryProcessTasks(); + } + + private void tryProcessTasks() { + taskExecutor.submit(() -> { + if (lock.tryLock()) { + try { + C newConfig = null; + Set newPartitions = null; + while (!stopped) { + TbQueueConsumerManagerTask task = tasks.poll(); + if (task == null) { + break; + } + log.trace("[{}] Processing task: {}", queueKey, task); + + if (task.getEvent() == QueueEvent.PARTITION_CHANGE) { + newPartitions = task.getPartitions(); + } else if (task.getEvent() == QueueEvent.CONFIG_UPDATE) { + newConfig = (C) task.getConfig(); + } else { + processTask(task); + } + } + if (stopped) { + return; + } + if (newConfig != null) { + doUpdate(newConfig); + } + if (newPartitions != null) { + doUpdate(newPartitions); + } + } catch (Exception e) { + log.error("[{}] Failed to process tasks", queueKey, e); + } finally { + lock.unlock(); + } + } else { + log.trace("[{}] Failed to acquire lock", queueKey); + scheduler.schedule(this::tryProcessTasks, 1, TimeUnit.SECONDS); + } + }); + } + + protected void processTask(TbQueueConsumerManagerTask task) { + } + + private void doUpdate(C newConfig) { + log.info("[{}] Processing queue update: {}", queueKey, newConfig); + var oldConfig = this.config; + this.config = newConfig; + if (log.isTraceEnabled()) { + log.trace("[{}] Old queue configuration: {}", queueKey, oldConfig); + log.trace("[{}] New queue configuration: {}", queueKey, newConfig); + } + + if (oldConfig == null) { + init(config); + } else if (newConfig.isConsumerPerPartition() != oldConfig.isConsumerPerPartition()) { + consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::initiateStop); + consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::awaitCompletion); + + init(config); + if (partitions != null) { + doUpdate(partitions); // even if partitions number was changed, there can be no partition change event + } + } else { + log.trace("[{}] Silently applied new config, because consumer-per-partition not changed", queueKey); + // do nothing, because partitions change (if they changed) will be handled on PartitionChangeEvent, + // and changes to other config values will be picked up by consumer on the fly, + // and queue topic and name are immutable + } + } + + private void doUpdate(Set partitions) { + this.partitions = partitions; + consumerWrapper.updatePartitions(partitions); + } + + private void launchConsumer(TbQueueConsumerTask consumerTask) { + log.info("[{}] Launching consumer", consumerTask.getKey()); + Future consumerLoop = consumerExecutor.submit(() -> { + ThingsBoardThreadFactory.updateCurrentThreadName(consumerTask.getKey().toString()); + try { + consumerLoop(consumerTask.getConsumer()); + } catch (Throwable e) { + log.error("Failure in consumer loop", e); + } + log.info("[{}] Consumer stopped", consumerTask.getKey()); + }); + consumerTask.setTask(consumerLoop); + } + + private void consumerLoop(TbQueueConsumer consumer) { + while (!stopped && !consumer.isStopped()) { + try { + List msgs = consumer.poll(config.getPollInterval()); + if (msgs.isEmpty()) { + continue; + } + processMsgs(msgs, consumer, config); + } catch (Exception e) { + if (!consumer.isStopped()) { + log.warn("Failed to process messages from queue", e); + try { + Thread.sleep(config.getPollInterval()); + } catch (InterruptedException e2) { + log.trace("Failed to wait until the server has capacity to handle new requests", e2); + } + } + } + } + if (consumer.isStopped()) { + consumer.unsubscribe(); + } + } + + protected void processMsgs(List msgs, TbQueueConsumer consumer, C config) throws Exception { + msgPackProcessor.process(msgs, consumer, config); + } + + public void stop() { + log.debug("[{}] Stopping consumers", queueKey); + consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::initiateStop); + stopped = true; + } + + public void awaitStop() { + log.debug("[{}] Waiting for consumers to stop", queueKey); + consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::awaitCompletion); + log.debug("[{}] Unsubscribed and stopped consumers", queueKey); + } + + private static String partitionsToString(Collection partitions) { + return partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.joining(", ", "[", "]")); + } + + public interface MsgPackProcessor { + void process(List msgs, TbQueueConsumer consumer, C config) throws Exception; + } + + public interface ConsumerWrapper { + + void updatePartitions(Set partitions); + + Collection> getConsumers(); + + } + + class ConsumerPerPartitionWrapper implements ConsumerWrapper { + private final Map> consumers = new HashMap<>(); + + @Override + public void updatePartitions(Set partitions) { + Set addedPartitions = new HashSet<>(partitions); + addedPartitions.removeAll(consumers.keySet()); + + Set removedPartitions = new HashSet<>(consumers.keySet()); + removedPartitions.removeAll(partitions); + log.info("[{}] Added partitions: {}, removed partitions: {}", queueKey, partitionsToString(addedPartitions), partitionsToString(removedPartitions)); + + removedPartitions.forEach((tpi) -> consumers.get(tpi).initiateStop()); + removedPartitions.forEach((tpi) -> consumers.remove(tpi).awaitCompletion()); + + addedPartitions.forEach((tpi) -> { + String key = queueKey + "-" + tpi.getPartition().orElse(-1); + TbQueueConsumerTask consumer = new TbQueueConsumerTask<>(key, consumerCreator.apply(config)); + consumers.put(tpi, consumer); + consumer.subscribe(Set.of(tpi)); + launchConsumer(consumer); + }); + } + + @Override + public Collection> getConsumers() { + return consumers.values(); + } + } + + class SingleConsumerWrapper implements ConsumerWrapper { + private TbQueueConsumerTask consumer; + + @Override + public void updatePartitions(Set partitions) { + log.info("[{}] New partitions: {}", queueKey, partitionsToString(partitions)); + if (partitions.isEmpty()) { + if (consumer != null && consumer.isRunning()) { + consumer.initiateStop(); + consumer.awaitCompletion(); + } + consumer = null; + return; + } + + if (consumer == null) { + consumer = new TbQueueConsumerTask<>(queueKey, consumerCreator.apply(config)); + } + consumer.subscribe(partitions); + if (!consumer.isRunning()) { + launchConsumer(consumer); + } + } + + @Override + public Collection> getConsumers() { + if (consumer == null) { + return Collections.emptyList(); + } + return List.of(consumer); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java index fe15b457f2..a264f912cb 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java @@ -15,10 +15,11 @@ */ package org.thingsboard.server.service.queue.processing; +import jakarta.annotation.PreDestroy; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.ApplicationEventPublisher; +import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.common.data.EntityType; @@ -36,6 +37,7 @@ import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbApplicationEventListener; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; @@ -47,9 +49,6 @@ import org.thingsboard.server.service.queue.TbPackCallback; import org.thingsboard.server.service.queue.TbPackProcessingContext; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; -import jakarta.annotation.PreDestroy; -import org.thingsboard.server.service.security.model.token.JwtTokenFactory; - import java.util.List; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -57,6 +56,7 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -64,9 +64,6 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public abstract class AbstractConsumerService extends TbApplicationEventListener { - protected volatile ExecutorService notificationsConsumerExecutor; - protected volatile boolean stopped = false; - protected volatile boolean isReady = false; protected final ActorSystemContext actorContext; protected final TbTenantProfileCache tenantProfileCache; protected final TbDeviceProfileCache deviceProfileCache; @@ -74,21 +71,37 @@ public abstract class AbstractConsumerService> nfConsumer; protected final JwtSettingsService jwtSettingsService; - public void init(String nfConsumerThreadName) { - this.notificationsConsumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName(nfConsumerThreadName)); + protected QueueConsumerManager> nfConsumer; + + protected ExecutorService consumersExecutor; + protected ExecutorService mgmtExecutor; + protected ScheduledExecutorService scheduler; + + public void init(String prefix) { + this.consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName(prefix + "-consumer")); + this.mgmtExecutor = ThingsBoardExecutors.newWorkStealingPool(getMgmtThreadPoolSize(), prefix + "-mgmt"); + this.scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName(prefix + "-consumer-scheduler")); + + this.nfConsumer = QueueConsumerManager.>builder() + .name(getServiceType().getLabel() + " Notifications") + .msgPackProcessor(this::processNotifications) + .pollInterval(getNotificationPollDuration()) + .consumerCreator(this::createNotificationsConsumer) + .consumerExecutor(consumersExecutor) + .threadPrefix("notifications") + .build(); } @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) - public void onApplicationEvent(ApplicationReadyEvent event) { - log.info("Subscribing to notifications: {}", nfConsumer.getTopic()); - this.nfConsumer.subscribe(); - this.isReady = true; - launchNotificationsConsumer(); - launchMainConsumers(); + public void afterStartUp() { + startConsumers(); + } + + protected void startConsumers() { + nfConsumer.subscribe(); + nfConsumer.launch(); } @Override @@ -98,58 +111,42 @@ public abstract class AbstractConsumerService { - while (!stopped) { - try { - List> msgs = nfConsumer.poll(getNotificationPollDuration()); - if (msgs.isEmpty()) { - continue; - } - List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).collect(Collectors.toList()); - ConcurrentMap> pendingMap = orderedMsgList.stream().collect( - Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); - CountDownLatch processingTimeoutLatch = new CountDownLatch(1); - TbPackProcessingContext> ctx = new TbPackProcessingContext<>( - processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); - orderedMsgList.forEach(element -> { - UUID id = element.getUuid(); - TbProtoQueueMsg msg = element.getMsg(); - log.trace("[{}] Creating notification callback for message: {}", id, msg.getValue()); - TbCallback callback = new TbPackCallback<>(id, ctx); - try { - handleNotification(id, msg, callback); - } catch (Throwable e) { - log.warn("[{}] Failed to process notification: {}", id, msg, e); - callback.onFailure(e); - } - }); - if (!processingTimeoutLatch.await(getNotificationPackProcessingTimeout(), TimeUnit.MILLISECONDS)) { - ctx.getAckMap().forEach((id, msg) -> log.warn("[{}] Timeout to process notification: {}", id, msg.getValue())); - ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process notification: {}", id, msg.getValue())); - } - nfConsumer.commit(); - } catch (Exception e) { - if (!stopped) { - log.warn("Failed to obtain notifications from queue.", e); - try { - Thread.sleep(getNotificationPollDuration()); - } catch (InterruptedException e2) { - log.trace("Failed to wait until the server has capacity to handle new notifications", e2); - } - } - } + protected abstract int getMgmtThreadPoolSize(); + + protected abstract TbQueueConsumer> createNotificationsConsumer(); + + protected void processNotifications(List> msgs, TbQueueConsumer> consumer) throws Exception { + List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).collect(Collectors.toList()); + ConcurrentMap> pendingMap = orderedMsgList.stream().collect( + Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); + CountDownLatch processingTimeoutLatch = new CountDownLatch(1); + TbPackProcessingContext> ctx = new TbPackProcessingContext<>( + processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); + orderedMsgList.forEach(element -> { + UUID id = element.getUuid(); + TbProtoQueueMsg msg = element.getMsg(); + log.trace("[{}] Creating notification callback for message: {}", id, msg.getValue()); + TbCallback callback = new TbPackCallback<>(id, ctx); + try { + handleNotification(id, msg, callback); + } catch (Throwable e) { + log.warn("[{}] Failed to process notification: {}", id, msg, e); + callback.onFailure(e); } - log.info("TB Notifications Consumer stopped."); }); + if (!processingTimeoutLatch.await(getNotificationPackProcessingTimeout(), TimeUnit.MILLISECONDS)) { + ctx.getAckMap().forEach((id, msg) -> log.warn("[{}] Timeout to process notification: {}", id, msg.getValue())); + ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process notification: {}", id, msg.getValue())); + } + consumer.commit(); } protected final void handleComponentLifecycleMsg(UUID id, ComponentLifecycleMsg componentLifecycleMsg) { @@ -203,13 +200,15 @@ public abstract class AbstractConsumerService partitions; private boolean drainQueue; @@ -37,8 +37,8 @@ public class TbQueueConsumerManagerTask { return new TbQueueConsumerManagerTask(QueueEvent.DELETE, null, null, drainQueue); } - public static TbQueueConsumerManagerTask configUpdate(Queue queue) { - return new TbQueueConsumerManagerTask(QueueEvent.CONFIG_UPDATE, queue, null, false); + public static TbQueueConsumerManagerTask configUpdate(QueueConfig config) { + return new TbQueueConsumerManagerTask(QueueEvent.CONFIG_UPDATE, config, null, false); } public static TbQueueConsumerManagerTask partitionChange(Set partitions) { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerTask.java b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerTask.java index 0e25efd014..84539d4d7c 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerTask.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerTask.java @@ -20,9 +20,8 @@ import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.TbQueueConsumer; -import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.TbQueueMsg; import java.util.Set; import java.util.concurrent.Future; @@ -30,12 +29,12 @@ import java.util.concurrent.TimeUnit; @RequiredArgsConstructor @Slf4j -public class TbQueueConsumerTask { +public class TbQueueConsumerTask { @Getter private final Object key; @Getter - private final TbQueueConsumer> consumer; + private final TbQueueConsumer consumer; @Setter private Future task; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineConsumerContext.java b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineConsumerContext.java index cf876c6947..08d2931246 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineConsumerContext.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineConsumerContext.java @@ -19,8 +19,6 @@ import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import org.thingsboard.common.util.ThingsBoardExecutors; -import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.queue.TbQueueAdmin; @@ -33,11 +31,6 @@ import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingStr import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategyFactory; import org.thingsboard.server.service.stats.RuleEngineStatisticsService; -import javax.annotation.PostConstruct; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; - @Component @TbRuleEngineComponent @Slf4j @@ -68,22 +61,4 @@ public class TbRuleEngineConsumerContext { private final TbQueueProducerProvider producerProvider; private final TbQueueAdmin queueAdmin; - private ExecutorService consumersExecutor; - private ExecutorService mgmtExecutor; - private ScheduledExecutorService scheduler; - - private volatile boolean isReady = false; - - @PostConstruct - void init() { - this.consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("tb-rule-engine-consumer")); - this.mgmtExecutor = ThingsBoardExecutors.newWorkStealingPool(mgmtThreadPoolSize, "tb-rule-engine-mgmt"); - this.scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("tb-rule-engine-consumer-scheduler")); - } - - public void stop() { - scheduler.shutdownNow(); - consumersExecutor.shutdownNow(); - mgmtExecutor.shutdownNow(); - } } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java index 2da3dbc6dc..c2823d3c00 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java @@ -16,9 +16,8 @@ package org.thingsboard.server.service.queue.ruleengine; import com.google.protobuf.ProtocolStringList; -import lombok.Getter; +import lombok.Builder; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; @@ -38,173 +37,54 @@ import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.service.queue.TbMsgPackCallback; import org.thingsboard.server.service.queue.TbMsgPackProcessingContext; import org.thingsboard.server.service.queue.TbRuleEngineConsumerStats; +import org.thingsboard.server.service.queue.consumer.MainQueueConsumerManager; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingDecision; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingResult; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingStrategy; import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategy; -import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.Future; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; @Slf4j -public class TbRuleEngineQueueConsumerManager { +public class TbRuleEngineQueueConsumerManager extends MainQueueConsumerManager, Queue> { public static final String SUCCESSFUL_STATUS = "successful"; public static final String FAILED_STATUS = "failed"; private final TbRuleEngineConsumerContext ctx; - private final QueueKey queueKey; private final TbRuleEngineConsumerStats stats; - private final ReentrantLock lock = new ReentrantLock(); //NonfairSync - @Getter - private volatile Queue queue; - @Getter - private volatile Set partitions; - private volatile ConsumerWrapper consumerWrapper; - - private volatile boolean stopped; - - private final java.util.Queue tasks = new ConcurrentLinkedQueue<>(); - - public TbRuleEngineQueueConsumerManager(TbRuleEngineConsumerContext ctx, QueueKey queueKey) { + @Builder(builderMethodName = "create") // not to conflict with super.builder() + public TbRuleEngineQueueConsumerManager(TbRuleEngineConsumerContext ctx, + QueueKey queueKey, + ExecutorService consumerExecutor, + ScheduledExecutorService scheduler, + ExecutorService taskExecutor) { + super(queueKey, null, null, ctx.getQueueFactory()::createToRuleEngineMsgConsumer, consumerExecutor, scheduler, taskExecutor); this.ctx = ctx; - this.queueKey = queueKey; this.stats = new TbRuleEngineConsumerStats(queueKey, ctx.getStatsFactory()); } - public void init(Queue queue) { - this.queue = queue; - if (queue.isConsumerPerPartition()) { - this.consumerWrapper = new ConsumerPerPartitionWrapper(); - } else { - this.consumerWrapper = new SingleConsumerWrapper(); - } - log.debug("[{}] Initialized consumer for queue: {}", queueKey, queue); - } - - public void update(Queue queue) { - addTask(TbQueueConsumerManagerTask.configUpdate(queue)); - } - - public void update(Set partitions) { - addTask(TbQueueConsumerManagerTask.partitionChange(partitions)); - } - public void delete(boolean drainQueue) { addTask(TbQueueConsumerManagerTask.delete(drainQueue)); } - private void addTask(TbQueueConsumerManagerTask todo) { - if (stopped) { - return; - } - tasks.add(todo); - log.trace("[{}] Added task: {}", queueKey, todo); - tryProcessTasks(); - } - - private void tryProcessTasks() { - if (!ctx.isReady()) { - log.debug("[{}] TbRuleEngineConsumerContext is not ready yet, will process tasks later", queueKey); - ctx.getScheduler().schedule(this::tryProcessTasks, 1, TimeUnit.SECONDS); - return; - } - ctx.getMgmtExecutor().submit(() -> { - if (lock.tryLock()) { - try { - Queue newConfiguration = null; - Set newPartitions = null; - while (!stopped) { - TbQueueConsumerManagerTask task = tasks.poll(); - if (task == null) { - break; - } - log.trace("[{}] Processing task: {}", queueKey, task); - - if (task.getEvent() == QueueEvent.PARTITION_CHANGE) { - newPartitions = task.getPartitions(); - } else if (task.getEvent() == QueueEvent.CONFIG_UPDATE) { - newConfiguration = task.getQueue(); - } else if (task.getEvent() == QueueEvent.DELETE) { - doDelete(task.isDrainQueue()); - return; - } - } - if (stopped) { - return; - } - if (newConfiguration != null) { - doUpdate(newConfiguration); - } - if (newPartitions != null) { - doUpdate(newPartitions); - } - } catch (Exception e) { - log.error("[{}] Failed to process tasks", queueKey, e); - } finally { - lock.unlock(); - } - } else { - log.trace("[{}] Failed to acquire lock", queueKey); - ctx.getScheduler().schedule(this::tryProcessTasks, 1, TimeUnit.SECONDS); - } - }); - } - - private void doUpdate(Queue newQueue) { - log.info("[{}] Processing queue update: {}", queueKey, newQueue); - var oldQueue = this.queue; - this.queue = newQueue; - if (log.isTraceEnabled()) { - log.trace("[{}] Old queue configuration: {}", queueKey, oldQueue); - log.trace("[{}] New queue configuration: {}", queueKey, newQueue); - } - - if (oldQueue == null) { - init(queue); - } else if (newQueue.isConsumerPerPartition() != oldQueue.isConsumerPerPartition()) { - consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::initiateStop); - consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::awaitCompletion); - - init(queue); - if (partitions != null) { - doUpdate(partitions); // even if partitions number was changed, there can be no partition change event - } - } else { - // do nothing, because partitions change (if they changed) will be handled on PartitionChangeEvent, - // and changes to pollInterval/packProcessingTimeout/submitStrategy/processingStrategy will be picked up by consumer on the fly, - // and queue topic and name are immutable + @Override + protected void processTask(TbQueueConsumerManagerTask task) { + if (task.getEvent() == QueueEvent.DELETE) { + doDelete(task.isDrainQueue()); } } - private void doUpdate(Set partitions) { - this.partitions = partitions; - consumerWrapper.updatePartitions(partitions); - } - - public void stop() { - log.debug("[{}] Stopping consumers", queueKey); - consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::initiateStop); - stopped = true; - } - - public void awaitStop() { - consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::awaitCompletion); - log.debug("[{}] Unsubscribed and stopped consumers", queueKey); - } - private void doDelete(boolean drainQueue) { stopped = true; log.info("[{}] Handling queue deletion", queueKey); @@ -212,7 +92,7 @@ public class TbRuleEngineQueueConsumerManager { List>> queueConsumers = consumerWrapper.getConsumers().stream() .map(TbQueueConsumerTask::getConsumer).collect(Collectors.toList()); - ctx.getConsumersExecutor().submit(() -> { + consumerExecutor.submit(() -> { if (drainQueue) { drainQueue(queueConsumers); } @@ -235,47 +115,10 @@ public class TbRuleEngineQueueConsumerManager { }); } - private void launchConsumer(TbQueueConsumerTask consumerTask) { - log.info("[{}] Launching consumer", consumerTask.getKey()); - Future consumerLoop = ctx.getConsumersExecutor().submit(() -> { - ThingsBoardThreadFactory.updateCurrentThreadName(consumerTask.getKey().toString()); - try { - consumerLoop(consumerTask.getConsumer()); - } catch (Throwable e) { - log.error("Failure in consumer loop", e); - } - }); - consumerTask.setTask(consumerLoop); - } - - private void consumerLoop(TbQueueConsumer> consumer) { - while (!stopped && !consumer.isStopped()) { - try { - List> msgs = consumer.poll(queue.getPollInterval()); - if (msgs.isEmpty()) { - continue; - } - processMsgs(msgs, consumer, queue); - } catch (Exception e) { - if (!consumer.isStopped()) { - log.warn("Failed to process messages from queue", e); - try { - Thread.sleep(ctx.getPollDuration()); - } catch (InterruptedException e2) { - log.trace("Failed to wait until the server has capacity to handle new requests", e2); - } - } - } - } - if (consumer.isStopped()) { - consumer.unsubscribe(); - } - log.info("Rule Engine consumer stopped"); - } - - private void processMsgs(List> msgs, - TbQueueConsumer> consumer, - Queue queue) throws InterruptedException { + @Override + protected void processMsgs(List> msgs, + TbQueueConsumer> consumer, + Queue queue) throws Exception { TbRuleEngineSubmitStrategy submitStrategy = getSubmitStrategy(queue); TbRuleEngineProcessingStrategy ackStrategy = getProcessingStrategy(queue); submitStrategy.init(msgs); @@ -320,7 +163,7 @@ public class TbRuleEngineQueueConsumerManager { } private void submitMessage(TbMsgPackProcessingContext packCtx, UUID id, TbProtoQueueMsg msg) { - log.trace("[{}] Creating callback for topic {} message: {}", id, queue.getName(), msg.getValue()); + log.trace("[{}] Creating callback for topic {} message: {}", id, config.getName(), msg.getValue()); ToRuleEngineMsg toRuleEngineMsg = msg.getValue(); TenantId tenantId = TenantId.fromUUID(new UUID(toRuleEngineMsg.getTenantIdMSB(), toRuleEngineMsg.getTenantIdLSB())); TbMsgCallback callback = ctx.isPrometheusStatsEnabled() ? @@ -328,7 +171,7 @@ public class TbRuleEngineQueueConsumerManager { new TbMsgPackCallback(id, tenantId, packCtx); try { if (!toRuleEngineMsg.getTbMsg().isEmpty()) { - forwardToRuleEngineActor(queue.getName(), tenantId, toRuleEngineMsg, callback); + forwardToRuleEngineActor(config.getName(), tenantId, toRuleEngineMsg, callback); } else { callback.onSuccess(); } @@ -356,7 +199,7 @@ public class TbRuleEngineQueueConsumerManager { log.info("[{}] {} to process [{}] messages", queueKey, prefix, map.size()); for (Map.Entry> pending : map.entrySet()) { ToRuleEngineMsg tmp = pending.getValue().getValue(); - TbMsg tmpMsg = TbMsg.fromBytes(queue.getName(), tmp.getTbMsg().toByteArray(), TbMsgCallback.EMPTY); + TbMsg tmpMsg = TbMsg.fromBytes(config.getName(), tmp.getTbMsg().toByteArray(), TbMsgCallback.EMPTY); RuleNodeInfo ruleNodeInfo = ctx.getLastVisitedRuleNode(pending.getKey()); if (printAll) { log.trace("[{}][{}] {} to process message: {}, Last Rule Node: {}", queueKey, TenantId.fromUUID(new UUID(tmp.getTenantIdMSB(), tmp.getTenantIdLSB())), prefix, tmpMsg, ruleNodeInfo); @@ -379,7 +222,7 @@ public class TbRuleEngineQueueConsumerManager { int n = 0; while (System.currentTimeMillis() <= finishTs) { for (TbQueueConsumer> consumer : consumers) { - List> msgs = consumer.poll(queue.getPollInterval()); + List> msgs = consumer.poll(config.getPollInterval()); if (msgs.isEmpty()) { continue; } @@ -388,7 +231,7 @@ public class TbRuleEngineQueueConsumerManager { MsgProtos.TbMsgProto tbMsgProto = MsgProtos.TbMsgProto.parseFrom(msg.getValue().getTbMsg().toByteArray()); EntityId originator = EntityIdFactory.getByTypeAndUuid(tbMsgProto.getEntityType(), new UUID(tbMsgProto.getEntityIdMSB(), tbMsgProto.getEntityIdLSB())); - TopicPartitionInfo tpi = ctx.getPartitionService().resolve(ServiceType.TB_RULE_ENGINE, queue.getName(), TenantId.SYS_TENANT_ID, originator); + TopicPartitionInfo tpi = ctx.getPartitionService().resolve(ServiceType.TB_RULE_ENGINE, config.getName(), TenantId.SYS_TENANT_ID, originator); ctx.getProducerProvider().getRuleEngineMsgProducer().send(tpi, msg, null); n++; } catch (Throwable e) { @@ -399,90 +242,11 @@ public class TbRuleEngineQueueConsumerManager { } } if (n > 0) { - log.info("Moved {} messages from {} to system {}", n, queueKey, queue.getName()); + log.info("Moved {} messages from {} to system {}", n, queueKey, config.getName()); } } catch (Exception e) { log.error("[{}] Failed to drain queue", queueKey, e); } } - private static String partitionsToString(Collection partitions) { - return partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.joining(", ", "[", "]")); - } - - interface ConsumerWrapper { - - void updatePartitions(Set partitions); - - Collection getConsumers(); - - } - - class ConsumerPerPartitionWrapper implements ConsumerWrapper { - private final Map consumers = new HashMap<>(); - - @Override - public void updatePartitions(Set partitions) { - Set addedPartitions = new HashSet<>(partitions); - addedPartitions.removeAll(consumers.keySet()); - - Set removedPartitions = new HashSet<>(consumers.keySet()); - removedPartitions.removeAll(partitions); - log.info("[{}] Added partitions: {}, removed partitions: {}", queueKey, partitionsToString(addedPartitions), partitionsToString(removedPartitions)); - - removedPartitions.forEach((tpi) -> { - consumers.get(tpi).initiateStop(); - }); - removedPartitions.forEach((tpi) -> { - consumers.remove(tpi).awaitCompletion(); - }); - - addedPartitions.forEach((tpi) -> { - String key = queueKey + "-" + tpi.getPartition().orElse(-999999); - TbQueueConsumerTask consumer = new TbQueueConsumerTask(key, ctx.getQueueFactory().createToRuleEngineMsgConsumer(queue)); - consumers.put(tpi, consumer); - consumer.subscribe(Set.of(tpi)); - launchConsumer(consumer); - }); - } - - @Override - public Collection getConsumers() { - return consumers.values(); - } - } - - class SingleConsumerWrapper implements ConsumerWrapper { - private TbQueueConsumerTask consumer; - - @Override - public void updatePartitions(Set partitions) { - log.info("[{}] New partitions: {}", queueKey, partitionsToString(partitions)); - if (partitions.isEmpty()) { - if (consumer != null && consumer.isRunning()) { - consumer.initiateStop(); - consumer.awaitCompletion(); - } - consumer = null; - return; - } - - if (consumer == null) { - consumer = new TbQueueConsumerTask(queueKey, ctx.getQueueFactory().createToRuleEngineMsgConsumer(queue)); - } - consumer.subscribe(partitions); - if (!consumer.isRunning()) { - launchConsumer(consumer); - } - } - - @Override - public Collection getConsumers() { - if (consumer == null) { - return Collections.emptyList(); - } - return List.of(consumer); - } - } - } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index bfd470b611..25a2d617b7 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -164,6 +164,8 @@ mail: oauth2: # Interval for checking refresh token expiration in seconds(by default, 1 day). refreshTokenCheckingInterval: "${REFRESH_TOKEN_EXPIRATION_CHECKING_INTERVAL:86400}" + # Rate limits for sending mails per tenant. As example for 1000 per minute and 10000 per hour is "1000:60,10000:3600" + per_tenant_rate_limits: "${MAIL_PER_TENANT_RATE_LIMITS:}" # Usage statistics parameters usage: @@ -1633,6 +1635,8 @@ queue: partitions: "${TB_QUEUE_CORE_PARTITIONS:10}" # Timeout for processing a message pack by Core microservices pack-processing-timeout: "${TB_QUEUE_CORE_PACK_PROCESSING_TIMEOUT_MS:2000}" + # Enable/disable a separate consumer per partition for Core queue + consumer-per-partition: "${TB_QUEUE_CORE_CONSUMER_PER_PARTITION:true}" ota: # Default topic name for OTA updates topic: "${TB_QUEUE_CORE_OTA_TOPIC:tb_ota_package}" diff --git a/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java b/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java index 31c82408c1..55af29ab0b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java @@ -92,7 +92,7 @@ public class HomePageApiTest extends AbstractControllerTest { @MockBean private SmsService smsService; - private static final int DEFAULT_DASHBOARDS_COUNT = 1; + private static final int DEFAULT_DASHBOARDS_COUNT = 0; //For system administrator @Test diff --git a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java index 8e0b294241..50a61cc9e7 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java @@ -27,6 +27,8 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import org.testcontainers.shaded.org.apache.commons.lang3.RandomUtils; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.QueueId; @@ -65,6 +67,9 @@ import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; @@ -86,7 +91,6 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @@ -112,6 +116,9 @@ public class TbRuleEngineQueueConsumerManagerTest { @Mock private TbQueueAdmin queueAdmin; private TbRuleEngineConsumerContext ruleEngineConsumerContext; + private ExecutorService consumersExecutor; + private ScheduledExecutorService scheduler; + private ExecutorService mgmtExecutor; private TbRuleEngineQueueConsumerManager consumerManager; private Queue queue; @@ -141,10 +148,10 @@ public class TbRuleEngineQueueConsumerManagerTest { }).when(actorContext).tell(any()); ruleEngineMsgProducer = mock(TbQueueProducer.class); when(producerProvider.getRuleEngineMsgProducer()).thenReturn(ruleEngineMsgProducer); - ruleEngineConsumerContext.setMgmtThreadPoolSize(2); + consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("tb-rule-engine-consumer")); + mgmtExecutor = ThingsBoardExecutors.newWorkStealingPool(3, "tb-rule-engine-mgmt"); + scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("tb-rule-engine-consumer-scheduler")); ruleEngineConsumerContext.setTopicDeletionDelayInSec(5); - ruleEngineConsumerContext.init(); - ruleEngineConsumerContext.setReady(false); queue = new Queue(); queue.setName("Test"); @@ -174,14 +181,23 @@ public class TbRuleEngineQueueConsumerManagerTest { }).when(queueFactory).createToRuleEngineMsgConsumer(any()); QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queue); - consumerManager = new TbRuleEngineQueueConsumerManager(ruleEngineConsumerContext, queueKey); + consumerManager = TbRuleEngineQueueConsumerManager.create() + .ctx(ruleEngineConsumerContext) + .queueKey(queueKey) + .consumerExecutor(consumersExecutor) + .scheduler(scheduler) + .taskExecutor(mgmtExecutor) + .build(); } @After public void afterEach() { consumerManager.stop(); consumerManager.awaitStop(); - ruleEngineConsumerContext.stop(); + + consumersExecutor.shutdownNow(); + scheduler.shutdownNow(); + mgmtExecutor.shutdownNow(); if (generateQueueMsgs) { await().atMost(10, TimeUnit.SECONDS) @@ -199,14 +215,7 @@ public class TbRuleEngineQueueConsumerManagerTest { Set partitions = createTpis(2, 3, 4); consumerManager.update(partitions); - partitions = createTpis(3, 4, 5); - consumerManager.update(partitions); - partitions = createTpis(1, 2, 3); - consumerManager.update(partitions); - // simulated multiple partition change events before consumer is ready; only latest partitions should be processed - verifyNoInteractions(queueFactory); - ruleEngineConsumerContext.setReady(true); await().atMost(2, TimeUnit.SECONDS) .until(() -> consumers.size() == 3); for (TopicPartitionInfo partition : partitions) { @@ -222,14 +231,7 @@ public class TbRuleEngineQueueConsumerManagerTest { Set partitions = createTpis(2, 3, 4); consumerManager.update(partitions); - partitions = createTpis(3, 4, 5); - consumerManager.update(partitions); - partitions = createTpis(1, 2, 3); - consumerManager.update(partitions); - - verifyNoInteractions(queueFactory); - ruleEngineConsumerContext.setReady(true); await().atMost(2, TimeUnit.SECONDS) .until(() -> consumers.size() == 1); TestConsumer consumer = getConsumer(); @@ -240,7 +242,6 @@ public class TbRuleEngineQueueConsumerManagerTest { public void testPartitionsUpdate_singleConsumer() { queue.setConsumerPerPartition(false); consumerManager.init(queue); - ruleEngineConsumerContext.setReady(true); Set partitions = Collections.emptySet(); consumerManager.update(partitions); @@ -273,7 +274,6 @@ public class TbRuleEngineQueueConsumerManagerTest { public void testPartitionsUpdate_consumerPerPartition() { queue.setConsumerPerPartition(true); consumerManager.init(queue); - ruleEngineConsumerContext.setReady(true); consumerManager.update(Collections.emptySet()); verify(queueFactory, after(1000).never()).createToRuleEngineMsgConsumer(any()); @@ -316,7 +316,6 @@ public class TbRuleEngineQueueConsumerManagerTest { public void testConfigUpdate_singleConsumer() { queue.setConsumerPerPartition(false); consumerManager.init(queue); - ruleEngineConsumerContext.setReady(true); Set partitions = createTpis(1, 2, 3); consumerManager.update(partitions); TestConsumer consumer = getConsumer(); @@ -342,7 +341,6 @@ public class TbRuleEngineQueueConsumerManagerTest { public void testConfigUpdate_consumerPerPartition() { queue.setConsumerPerPartition(true); consumerManager.init(queue); - ruleEngineConsumerContext.setReady(true); Set partitions = createTpis(1, 2, 3); consumerManager.update(partitions); TestConsumer consumer1 = getConsumer(1); @@ -375,7 +373,6 @@ public class TbRuleEngineQueueConsumerManagerTest { public void testConfigUpdate_fromSingleToConsumerPerPartition() { queue.setConsumerPerPartition(false); consumerManager.init(queue); - ruleEngineConsumerContext.setReady(true); Set partitions = createTpis(1, 2, 3); consumerManager.update(partitions); TestConsumer consumer = getConsumer(); @@ -395,7 +392,6 @@ public class TbRuleEngineQueueConsumerManagerTest { public void testConfigUpdate_fromConsumerPerPartitionToSingle() { queue.setConsumerPerPartition(true); consumerManager.init(queue); - ruleEngineConsumerContext.setReady(true); Set partitions = createTpis(1, 2, 3); consumerManager.update(partitions); TestConsumer consumer1 = getConsumer(1); @@ -419,7 +415,6 @@ public class TbRuleEngineQueueConsumerManagerTest { public void testStop() { queue.setConsumerPerPartition(true); consumerManager.init(queue); - ruleEngineConsumerContext.setReady(true); consumerManager.update(createTpis(1)); TestConsumer consumer = getConsumer(1); verifySubscribedAndLaunched(consumer, 1); @@ -437,7 +432,6 @@ public class TbRuleEngineQueueConsumerManagerTest { public void testDelete_consumerPerPartition() { queue.setConsumerPerPartition(true); consumerManager.init(queue); - ruleEngineConsumerContext.setReady(true); Set partitions = createTpis(1, 2); consumerManager.update(partitions); TestConsumer consumer1 = getConsumer(1); @@ -459,7 +453,7 @@ public class TbRuleEngineQueueConsumerManagerTest { int msgCount = totalConsumedMsgs.get(); await().atLeast(2, TimeUnit.SECONDS) // based on topicDeletionDelayInSec(5) = 5 - ( 3 seconds the code may execute starting consumerManager.delete() call) - .atMost(7, TimeUnit.SECONDS) + .atMost(20, TimeUnit.SECONDS) .untilAsserted(() -> { partitions.stream() .map(TopicPartitionInfo::getFullTopicName) @@ -481,7 +475,6 @@ public class TbRuleEngineQueueConsumerManagerTest { public void testDelete_singleConsumer() { queue.setConsumerPerPartition(false); consumerManager.init(queue); - ruleEngineConsumerContext.setReady(true); Set partitions = createTpis(1, 2); consumerManager.update(partitions); TestConsumer consumer = getConsumer(); @@ -499,7 +492,7 @@ public class TbRuleEngineQueueConsumerManagerTest { int msgCount = totalConsumedMsgs.get(); await().atLeast(2, TimeUnit.SECONDS) // based on topicDeletionDelayInSec(5) = 5 - ( 3 seconds the code may execute starting consumerManager.delete() call) - .atMost(7, TimeUnit.SECONDS) + .atMost(20, TimeUnit.SECONDS) .untilAsserted(() -> { partitions.stream() .map(TopicPartitionInfo::getFullTopicName) @@ -520,10 +513,9 @@ public class TbRuleEngineQueueConsumerManagerTest { public void testManyDifferentUpdates() throws Exception { queue.setConsumerPerPartition(RandomUtils.nextBoolean()); consumerManager.init(queue); - ruleEngineConsumerContext.setReady(true); Supplier queueConfigUpdater = () -> { - Queue oldConfig = consumerManager.getQueue(); + Queue oldConfig = consumerManager.getConfig(); Queue newConfig = JacksonUtil.clone(oldConfig); newConfig.setConsumerPerPartition(RandomUtils.nextBoolean()); newConfig.setPollInterval(RandomUtils.nextInt(100, 501)); @@ -571,7 +563,7 @@ public class TbRuleEngineQueueConsumerManagerTest { Set expectedPartitions = latestPartitions; await().atMost(5, TimeUnit.SECONDS) .untilAsserted(() -> { - assertThat(consumerManager.getQueue()).isEqualTo(expectedConfig); + assertThat(consumerManager.getConfig()).isEqualTo(expectedConfig); assertThat(consumerManager.getPartitions()).isEqualTo(expectedPartitions); }); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java index 36179502ad..7ab5ee3b56 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java @@ -39,4 +39,5 @@ public class OtherConfiguration extends PowerSavingConfiguration { private Long pagingTransmissionWindow; private String fwUpdateResource; private String swUpdateResource; + private String defaultObjectIDVer; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/exception/RateLimitExceededException.java b/common/data/src/main/java/org/thingsboard/server/common/data/exception/RateLimitExceededException.java new file mode 100644 index 0000000000..27be0f65d2 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/exception/RateLimitExceededException.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2024 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. + */ +package org.thingsboard.server.common.data.exception; + +import org.thingsboard.server.common.data.limit.LimitedApi; + +public class RateLimitExceededException extends AbstractRateLimitException { + + public RateLimitExceededException(String message) { + super(message); + } + + public RateLimitExceededException(LimitedApi api) { + super("Rate limit for " + api.getLabel() + " is exceeded"); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java b/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java index 1a0f038c90..4afead3435 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java @@ -39,7 +39,8 @@ public enum LimitedApi { TWO_FA_VERIFICATION_CODE_SEND(false, true), TWO_FA_VERIFICATION_CODE_CHECK(false, true), TRANSPORT_MESSAGES_PER_TENANT("transport messages", true), - TRANSPORT_MESSAGES_PER_DEVICE("transport messages per device", false); + TRANSPORT_MESSAGES_PER_DEVICE("transport messages per device", false), + EMAILS("emails sending", true); private Function configExtractor; @Getter diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java b/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java index e3c6836aab..1c08072e61 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java @@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.validation.NoXss; import java.util.Optional; @Data -public class Queue extends BaseDataWithAdditionalInfo implements HasName, HasTenantId { +public class Queue extends BaseDataWithAdditionalInfo implements HasName, HasTenantId, QueueConfig { private TenantId tenantId; @NoXss @Length(fieldName = "name") diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/queue/QueueConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/queue/QueueConfig.java new file mode 100644 index 0000000000..383c613a38 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/queue/QueueConfig.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2024 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. + */ +package org.thingsboard.server.common.data.queue; + +public interface QueueConfig { + + boolean isConsumerPerPartition(); + + int getPollInterval(); + +} diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/ServiceType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/ServiceType.java index c823ea4d73..f3a0e47d09 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/ServiceType.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/ServiceType.java @@ -15,11 +15,23 @@ */ package org.thingsboard.server.common.msg.queue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter public enum ServiceType { - TB_CORE, TB_RULE_ENGINE, TB_TRANSPORT, JS_EXECUTOR, TB_VC_EXECUTOR; + TB_CORE("TB Core"), + TB_RULE_ENGINE("TB Rule Engine"), + TB_TRANSPORT("TB Transport"), + JS_EXECUTOR("JS Executor"), + TB_VC_EXECUTOR("TB VC Executor"); + + private final String label; public static ServiceType of(String serviceType) { return ServiceType.valueOf(serviceType.replace("-", "_").toUpperCase()); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/consumer/QueueConsumerManager.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueConsumerManager.java similarity index 98% rename from application/src/main/java/org/thingsboard/server/service/queue/consumer/QueueConsumerManager.java rename to common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueConsumerManager.java index e7b3a2f2b7..90f4cf050c 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/consumer/QueueConsumerManager.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueConsumerManager.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.queue.consumer; +package org.thingsboard.server.queue.common.consumer; import lombok.Builder; import lombok.Getter; @@ -106,4 +106,5 @@ public class QueueConsumerManager { public interface MsgPackProcessor { void process(List msgs, TbQueueConsumer consumer) throws Exception; } -} \ No newline at end of file + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java index cd09f682c6..d8efad2182 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java @@ -217,7 +217,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); consumerBuilder.settings(kafkaSettings); consumerBuilder.topic(topicService.buildTopicName(coreSettings.getTopic())); - consumerBuilder.clientId("monolith-core-consumer-" + serviceInfoProvider.getServiceId()); + consumerBuilder.clientId("monolith-core-consumer-" + serviceInfoProvider.getServiceId() + "-" + consumerCount.incrementAndGet()); consumerBuilder.groupId(topicService.buildTopicName("monolith-core-consumer")); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreMsg.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.admin(coreAdmin); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java index 95fea30d7b..ddb2be8f2a 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java @@ -56,6 +56,7 @@ import org.thingsboard.server.queue.settings.TbQueueVersionControlSettings; import jakarta.annotation.PreDestroy; import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicLong; @Component @ConditionalOnExpression("'${queue.type:null}'=='kafka' && '${service.type:null}'=='tb-core'") @@ -84,6 +85,8 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { private final TbQueueAdmin housekeeperAdmin; private final TbQueueAdmin housekeeperReprocessingAdmin; + private final AtomicLong consumerCount = new AtomicLong(); + public KafkaTbCoreQueueFactory(TopicService topicService, TbKafkaSettings kafkaSettings, TbServiceInfoProvider serviceInfoProvider, @@ -174,7 +177,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); consumerBuilder.settings(kafkaSettings); consumerBuilder.topic(topicService.buildTopicName(coreSettings.getTopic())); - consumerBuilder.clientId("tb-core-consumer-" + serviceInfoProvider.getServiceId()); + consumerBuilder.clientId("tb-core-consumer-" + serviceInfoProvider.getServiceId() + "-" + consumerCount.incrementAndGet()); consumerBuilder.groupId(topicService.buildTopicName("tb-core-node")); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreMsg.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.admin(coreAdmin); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java index b59914e2ac..7797242b51 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java @@ -29,6 +29,7 @@ import org.eclipse.leshan.server.registration.Registration; import org.eclipse.leshan.server.registration.RegistrationListener; import org.eclipse.leshan.server.registration.RegistrationUpdate; import org.eclipse.leshan.server.send.SendListener; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; import java.util.Collection; @@ -101,7 +102,10 @@ public class LwM2mServerListener { @Override public void onResponse(SingleObservation observation, Registration registration, ObserveResponse response) { if (registration != null) { - service.onUpdateValueAfterReadResponse(registration, convertObjectIdToVersionedId(observation.getPath().toString(), registration), response); + LwM2mClient lwM2MClient = service.getClientContext().getClientByEndpoint(registration.getEndpoint()); + if (lwM2MClient != null) { + service.onUpdateValueAfterReadResponse(registration, convertObjectIdToVersionedId(observation.getPath().toString(), lwM2MClient), response); + } } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java index 3e407bdfc9..78f76adc1d 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java @@ -27,10 +27,10 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; import java.util.ArrayList; -import java.util.Base64; import java.util.Collection; import java.util.Map; import java.util.Optional; @@ -107,7 +107,8 @@ public class LwM2mVersionedModelProvider implements LwM2mModelProvider { @Override public ObjectModel getObjectModel(int objectId) { - String version = String.valueOf(registration.getSupportedVersion(objectId)); + LwM2mClient lwM2mClient = lwM2mClientContext.getClientByEndpoint(registration.getEndpoint()); + String version = lwM2mClient.getSupportedObjectVersion(objectId).toString(); if (version != null) { return this.getObjectModelDynamic(objectId, version); } @@ -116,7 +117,8 @@ public class LwM2mVersionedModelProvider implements LwM2mModelProvider { @Override public Collection getObjectModels() { - Map supportedObjects = this.registration.getSupportedObject(); + LwM2mClient lwM2mClient = lwM2mClientContext.getClientByEndpoint(registration.getEndpoint()); + Map supportedObjects = lwM2mClient.getSupportedClientObjects(); Collection result = new ArrayList<>(supportedObjects.size()); for (Map.Entry supportedObject : supportedObjects.entrySet()) { ObjectModel objectModel = this.getObjectModelDynamic(supportedObject.getKey(), String.valueOf(supportedObject.getValue())); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java index cb391b27e9..705ed45995 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java @@ -21,6 +21,8 @@ import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.LwM2m; +import org.eclipse.leshan.core.LwM2m.Version; +import org.eclipse.leshan.core.link.Link; import org.eclipse.leshan.core.link.attributes.Attribute; import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ResourceModel; @@ -37,6 +39,7 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.device.data.Lwm2mDeviceTransportConfiguration; import org.thingsboard.server.common.data.device.data.PowerMode; +import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; @@ -61,8 +64,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH; +import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.LWM2M_OBJECT_VERSION_DEFAULT; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertMultiResourceValuesFromRpcBody; -import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertObjectIdToVersionedId; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.equalsResourceTypeGetSimpleName; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.getVerFromPathIdVerOrId; @@ -129,6 +132,12 @@ public class LwM2mClient { @Setter private UUID lastSentRpcId; + @Setter + private LwM2m.Version defaultObjectIDVer; + + @Getter + private Map supportedClientObjects; + public Object clone() throws CloneNotSupportedException { return super.clone(); } @@ -142,6 +151,7 @@ public class LwM2mClient { this.state = LwM2MClientState.CREATED; this.lock = new ReentrantLock(); this.retryAttempts = new AtomicInteger(0); + this.supportedClientObjects = null; } public void init(ValidateDeviceCredentialsResponse credentials, UUID sessionId) { @@ -153,12 +163,14 @@ public class LwM2mClient { this.edrxCycle = credentials.getDeviceInfo().getEdrxCycle(); this.psmActivityTimer = credentials.getDeviceInfo().getPsmActivityTimer(); this.pagingTransmissionWindow = credentials.getDeviceInfo().getPagingTransmissionWindow(); + this.defaultObjectIDVer = getObjectIDVerFromDeviceProfile(credentials.getDeviceProfile()); } public void setRegistration(Registration registration) { this.registration = registration; this.clientSupportContentFormats = clientSupportContentFormat(registration); this.defaultContentFormat = calculateDefaultContentFormat(registration); + this.setSupportedClientObjects(); } public void lock() { @@ -197,6 +209,18 @@ public class LwM2mClient { builder.setDeviceType(deviceProfile.getName()); } + private LwM2m.Version getObjectIDVerFromDeviceProfile(DeviceProfile deviceProfile) { + String defaultObjectIdVer = null; + if (deviceProfile != null) { + defaultObjectIdVer = ((Lwm2mDeviceProfileTransportConfiguration) deviceProfile + .getProfileData() + .getTransportConfiguration()) + .getClientLwM2mSettings() + .getDefaultObjectIDVer(); + } + return new Version(defaultObjectIdVer == null ? LWM2M_OBJECT_VERSION_DEFAULT : defaultObjectIdVer); + } + public void refreshSessionId(String nodeId) { UUID newId = UUID.randomUUID(); SessionInfoProto.Builder builder = SessionInfoProto.newBuilder().mergeFrom(session); @@ -240,67 +264,28 @@ public class LwM2mClient { } } - public Object getResourceValue(String pathRezIdVer, String pathRezId) { - String pathRez = pathRezIdVer == null ? convertObjectIdToVersionedId(pathRezId, this.registration) : pathRezIdVer; - if (this.resources.get(pathRez) != null) { - return this.resources.get(pathRez).getLwM2mResource().getValue(); - } - return null; - } - - public Object getResourceNameByRezId(String pathRezIdVer, String pathRezId) { - String pathRez = pathRezIdVer == null ? convertObjectIdToVersionedId(pathRezId, this.registration) : pathRezIdVer; - if (this.resources.get(pathRez) != null) { - return this.resources.get(pathRez).getResourceModel().name; - } - return null; - } - - public String getRezIdByResourceNameAndObjectInstanceId(String resourceName, String pathObjectInstanceIdVer, LwM2mModelProvider modelProvider) { - LwM2mPath pathIds = getLwM2mPathFromString(pathObjectInstanceIdVer); - if (pathIds.isObjectInstance()) { - Set rezIds = modelProvider.getObjectModel(registration) - .getObjectModel(pathIds.getObjectId()).resources.entrySet() - .stream() - .filter(map -> resourceName.equals(map.getValue().name)) - .map(map -> map.getKey()) - .collect(Collectors.toSet()); - return rezIds.size() > 0 ? String.valueOf(rezIds.stream().findFirst().get()) : null; - } - return null; - } - public ResourceModel getResourceModel(String pathIdVer, LwM2mModelProvider modelProvider) { LwM2mPath pathIds = getLwM2mPathFromString(pathIdVer); - String verSupportedObject = String.valueOf(registration.getSupportedObject().get(pathIds.getObjectId())); + String verSupportedObject = String.valueOf(this.getSupportedObjectVersion(pathIds.getObjectId())); String verRez = getVerFromPathIdVerOrId(pathIdVer); return verRez != null && verRez.equals(verSupportedObject) ? modelProvider.getObjectModel(registration) .getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()) : null; } - public boolean isResourceMultiInstances(String pathIdVer, LwM2mModelProvider modelProvider) { - var resourceModel = getResourceModel(pathIdVer, modelProvider); - if (resourceModel != null && resourceModel.multiple != null) { - return resourceModel.multiple; - } else { - return false; - } - } - public ObjectModel getObjectModel(String pathIdVer, LwM2mModelProvider modelProvider) { try { LwM2mPath pathIds = getLwM2mPathFromString(pathIdVer); - String verSupportedObject = String.valueOf(registration.getSupportedObject().get(pathIds.getObjectId())); + String verSupportedObject = String.valueOf(this.getSupportedObjectVersion(pathIds.getObjectId())); String verRez = getVerFromPathIdVerOrId(pathIdVer); return verRez != null && verRez.equals(verSupportedObject) ? modelProvider.getObjectModel(registration) .getObjectModel(pathIds.getObjectId()) : null; } catch (Exception e) { if (registration == null) { log.error("[{}] Failed Registration is null, GetObjectModelRegistration. ", this.endpoint, e); - } else if (registration.getSupportedObject() == null) { + } else if (this.getSupportedClientObjects() == null) { log.error("[{}] Failed SupportedObject in Registration, GetObjectModelRegistration.", this.endpoint, e); } else { - log.error("[{}] Failed ModelProvider.getObjectModel [{}] in Registration. ", this.endpoint, registration.getSupportedObject(), e); + log.error("[{}] Failed ModelProvider.getObjectModel [{}] in Registration. ", this.endpoint, this.getSupportedClientObjects(), e); } return null; } @@ -371,7 +356,7 @@ public class LwM2mClient { public String isValidObjectVersion(String path) { LwM2mPath pathIds = getLwM2mPathFromString(path); - LwM2m.Version verSupportedObject = registration.getSupportedObject().get(pathIds.getObjectId()); + LwM2m.Version verSupportedObject = this.getSupportedObjectVersion(pathIds.getObjectId()); if (verSupportedObject == null) { return String.format("Specified object id %s absent in the list supported objects of the client or is security object!", pathIds.getObjectId()); } else { @@ -460,5 +445,31 @@ public class LwM2mClient { return new LwM2mPath(fromVersionedIdToObjectId(path)); } + public LwM2m.Version getDefaultObjectIDVer() { + return this.defaultObjectIDVer == null ? new Version(LWM2M_OBJECT_VERSION_DEFAULT) : this.defaultObjectIDVer; + } + + public LwM2m.Version getSupportedObjectVersion(Integer objectid) { + return this.supportedClientObjects.get(objectid); + } + + private void setSupportedClientObjects(){ + this.supportedClientObjects = new ConcurrentHashMap<>(); + for (Link link: this.registration.getSortedObjectLinks()) { + LwM2mPath lwM2mPath = new LwM2mPath(link.getUriReference()); + if (lwM2mPath.isObject()) { + LwM2m.Version ver; + if (link.getAttributes().get("ver")!= null) { + ver = (Version) link.getAttributes().get("ver").getValue(); + } else { + ver = getDefaultObjectIDVer(); + } + this.supportedClientObjects.put(lwM2mPath.getObjectId(), ver); + } else if (lwM2mPath.getObjectId() != null && this.supportedClientObjects.get(lwM2mPath.getObjectId()) == null){ + this.supportedClientObjects.put(lwM2mPath.getObjectId(), getDefaultObjectIDVer()); + } + } + } + } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java index 4f35489250..851b517db6 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java @@ -396,7 +396,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { Arrays.stream(client.getRegistration().getObjectLinks()).forEach(link -> { LwM2mPath pathIds = new LwM2mPath(link.getUriReference()); if (!pathIds.isRoot()) { - clientObjects.add(convertObjectIdToVersionedId(link.getUriReference(), client.getRegistration())); + clientObjects.add(convertObjectIdToVersionedId(link.getUriReference(), client)); } }); return (clientObjects.size() > 0) ? clientObjects : null; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java index f9d23383cd..0b5e995ea5 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java @@ -481,7 +481,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl } private void startUpdateUsingUrl(LwM2mClient client, String id, String url) { - String targetIdVer = convertObjectIdToVersionedId(id, client.getRegistration()); + String targetIdVer = convertObjectIdToVersionedId(id, client); TbLwM2MWriteReplaceRequest request = TbLwM2MWriteReplaceRequest.builder().versionedId(targetIdVer).value(url).timeout(clientContext.getRequestTimeout(client)).build(); downlinkHandler.sendWriteReplaceRequest(client, request, new TbLwM2MWriteResponseCallback(uplinkHandler, logService, client, targetIdVer)); } @@ -512,10 +512,10 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl } switch (strategy) { case OBJ_5_BINARY: - startUpdateUsingBinary(client, convertObjectIdToVersionedId(FW_PACKAGE_5_ID, client.getRegistration()), otaPackageId); + startUpdateUsingBinary(client, convertObjectIdToVersionedId(FW_PACKAGE_5_ID, client), otaPackageId); break; case OBJ_19_BINARY: - startUpdateUsingBinary(client, convertObjectIdToVersionedId(FW_PACKAGE_19_ID, client.getRegistration()), otaPackageId); + startUpdateUsingBinary(client, convertObjectIdToVersionedId(FW_PACKAGE_19_ID, client), otaPackageId); break; case OBJ_5_TEMP_URL: startUpdateUsingUrl(client, FW_URL_ID, info.getBaseUrl() + "/" + FIRMWARE_UPDATE_COAP_RESOURCE + "/" + otaPackageId.toString()); @@ -534,7 +534,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl LwM2MSoftwareUpdateStrategy strategy = info.getStrategy(); switch (strategy) { case BINARY: - startUpdateUsingBinary(client, convertObjectIdToVersionedId(SW_PACKAGE_ID, client.getRegistration()), otaPackageId); + startUpdateUsingBinary(client, convertObjectIdToVersionedId(SW_PACKAGE_ID, client), otaPackageId); break; case TEMP_URL: startUpdateUsingUrl(client, SW_PACKAGE_URI_ID, info.getBaseUrl() + "/" + FIRMWARE_UPDATE_COAP_RESOURCE + "/" + otaPackageId.toString()); @@ -566,19 +566,19 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl } private void executeFwUpdate(LwM2mClient client) { - String fwExecuteVerId = convertObjectIdToVersionedId(FW_EXECUTE_ID, client.getRegistration()); + String fwExecuteVerId = convertObjectIdToVersionedId(FW_EXECUTE_ID, client); TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(fwExecuteVerId).timeout(clientContext.getRequestTimeout(client)).build(); downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, fwExecuteVerId)); } private void executeSwInstall(LwM2mClient client) { - String swInstallVerId = convertObjectIdToVersionedId(SW_INSTALL_ID, client.getRegistration()); + String swInstallVerId = convertObjectIdToVersionedId(SW_INSTALL_ID, client); TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(swInstallVerId).timeout(clientContext.getRequestTimeout(client)).build(); downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, swInstallVerId)); } private void executeSwUninstallForUpdate(LwM2mClient client) { - String swInInstallVerId = convertObjectIdToVersionedId(SW_UN_INSTALL_ID, client.getRegistration()); + String swInInstallVerId = convertObjectIdToVersionedId(SW_UN_INSTALL_ID, client); TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(swInInstallVerId).params("1").timeout(clientContext.getRequestTimeout(client)).build(); downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, swInInstallVerId)); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java index 17fef8acb0..8cafc2bfd0 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java @@ -23,6 +23,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.protobuf.util.JsonFormat; import lombok.SneakyThrows; +import org.eclipse.leshan.core.LwM2m.Version; import org.eclipse.leshan.core.model.ResourceModel; import org.eclipse.leshan.core.node.LwM2mMultipleResource; import org.eclipse.leshan.core.node.LwM2mNodeException; @@ -107,6 +108,8 @@ public class LwM2MClientSerDes { if (client.getPagingTransmissionWindow() != null) { o.addProperty("pagingTransmissionWindow", client.getPagingTransmissionWindow()); } + o.addProperty("defaultObjectIDVer", client.getDefaultObjectIDVer().toString()); + if (client.getRegistration() != null) { String registrationAddress = client.getRegistration().getAddress().toString(); JsonNode registrationNode = registrationSerDes.jSerialize(client.getRegistration()); @@ -339,6 +342,13 @@ public class LwM2MClientSerDes { pagingTransmissionWindowField.set(lwM2mClient, pagingTransmissionWindow.getAsLong()); } + JsonElement defaultObjectIDVer = o.get("defaultObjectIDVer"); + if (defaultObjectIDVer != null) { + Field defaultObjectIDVerField = lwM2mClientClass.getDeclaredField("defaultObjectIDVer"); + defaultObjectIDVerField.setAccessible(true); + defaultObjectIDVerField.set(lwM2mClient, new Version(defaultObjectIDVer.getAsString())); + } + JsonElement registration = o.get("registration"); if (registration != null) { lwM2mClient.setRegistration(registrationSerDes.deserialize(toJsonNode(registration.getAsString()))); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java index fc95bb3728..bfc9f32dd8 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java @@ -368,7 +368,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl LwM2mPath path = entry.getKey(); LwM2mNode node = entry.getValue(); LwM2mClient lwM2MClient = clientContext.getClientByEndpoint(registration.getEndpoint()); - String stringPath = convertObjectIdToVersionedId(path.toString(), registration); + String stringPath = convertObjectIdToVersionedId(path.toString(), lwM2MClient); ObjectModel objectModelVersion = lwM2MClient.getObjectModel(stringPath, modelProvider); if (objectModelVersion != null) { if (node instanceof LwM2mObject) { @@ -584,27 +584,27 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl private void updateResourcesValue(LwM2mClient lwM2MClient, LwM2mResource lwM2mResource, String path, Mode mode, int code) { Registration registration = lwM2MClient.getRegistration(); if (lwM2MClient.saveResourceValue(path, lwM2mResource, modelProvider, mode)) { - if (path.equals(convertObjectIdToVersionedId(FW_NAME_ID, registration))) { + if (path.equals(convertObjectIdToVersionedId(FW_NAME_ID, lwM2MClient))) { otaService.onCurrentFirmwareNameUpdate(lwM2MClient, (String) lwM2mResource.getValue()); - } else if (path.equals(convertObjectIdToVersionedId(FW_3_VER_ID, registration))) { + } else if (path.equals(convertObjectIdToVersionedId(FW_3_VER_ID, lwM2MClient))) { otaService.onCurrentFirmwareVersion3Update(lwM2MClient, (String) lwM2mResource.getValue()); - } else if (path.equals(convertObjectIdToVersionedId(FW_VER_ID, registration))) { + } else if (path.equals(convertObjectIdToVersionedId(FW_VER_ID, lwM2MClient))) { otaService.onCurrentFirmwareVersionUpdate(lwM2MClient, (String) lwM2mResource.getValue()); - } else if (path.equals(convertObjectIdToVersionedId(FW_STATE_ID, registration))) { + } else if (path.equals(convertObjectIdToVersionedId(FW_STATE_ID, lwM2MClient))) { otaService.onCurrentFirmwareStateUpdate(lwM2MClient, (Long) lwM2mResource.getValue()); - } else if (path.equals(convertObjectIdToVersionedId(FW_RESULT_ID, registration))) { + } else if (path.equals(convertObjectIdToVersionedId(FW_RESULT_ID, lwM2MClient))) { otaService.onCurrentFirmwareResultUpdate(lwM2MClient, (Long) lwM2mResource.getValue()); - } else if (path.equals(convertObjectIdToVersionedId(FW_DELIVERY_METHOD, registration))) { + } else if (path.equals(convertObjectIdToVersionedId(FW_DELIVERY_METHOD, lwM2MClient))) { otaService.onCurrentFirmwareDeliveryMethodUpdate(lwM2MClient, (Long) lwM2mResource.getValue()); - } else if (path.equals(convertObjectIdToVersionedId(SW_NAME_ID, registration))) { + } else if (path.equals(convertObjectIdToVersionedId(SW_NAME_ID, lwM2MClient))) { otaService.onCurrentSoftwareNameUpdate(lwM2MClient, (String) lwM2mResource.getValue()); - } else if (path.equals(convertObjectIdToVersionedId(SW_VER_ID, registration))) { + } else if (path.equals(convertObjectIdToVersionedId(SW_VER_ID, lwM2MClient))) { otaService.onCurrentSoftwareVersionUpdate(lwM2MClient, (String) lwM2mResource.getValue()); - } else if (path.equals(convertObjectIdToVersionedId(SW_3_VER_ID, registration))) { + } else if (path.equals(convertObjectIdToVersionedId(SW_3_VER_ID, lwM2MClient))) { otaService.onCurrentSoftwareVersion3Update(lwM2MClient, (String) lwM2mResource.getValue()); - } else if (path.equals(convertObjectIdToVersionedId(SW_STATE_ID, registration))) { + } else if (path.equals(convertObjectIdToVersionedId(SW_STATE_ID, lwM2MClient))) { otaService.onCurrentSoftwareStateUpdate(lwM2MClient, (Long) lwM2mResource.getValue()); - } else if (path.equals(convertObjectIdToVersionedId(SW_RESULT_ID, registration))) { + } else if (path.equals(convertObjectIdToVersionedId(SW_RESULT_ID, lwM2MClient))) { otaService.onCurrentSoftwareResultUpdate(lwM2MClient, (Long) lwM2mResource.getValue()); } if (ResponseCode.BAD_REQUEST.getCode() > code) { @@ -969,6 +969,10 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl } } + public LwM2mClientContext getClientContext(){ + return this.clientContext; + }; + private Map getNamesFromProfileForSharedAttributes(LwM2mClient lwM2MClient) { Lwm2mDeviceProfileTransportConfiguration profile = clientContext.getProfile(lwM2MClient.getProfileId()); return profile.getObserveAttr().getKeyName(); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java index 65c7fb9654..0896963be2 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; import java.util.Collection; import java.util.Optional; @@ -77,4 +78,5 @@ public interface LwM2mUplinkMsgHandler { LwM2mValueConverter getConverter(); + LwM2mClientContext getClientContext(); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java index db8398cd57..e2e31f2768 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java @@ -20,16 +20,13 @@ import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.elements.config.Configuration; import org.eclipse.leshan.core.model.LwM2mModel; import org.eclipse.leshan.core.model.ObjectLoader; -import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ResourceModel; import org.eclipse.leshan.core.model.StaticModel; import org.eclipse.leshan.core.node.LwM2mMultipleResource; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.LwM2mResource; import org.eclipse.leshan.core.node.LwM2mSingleResource; -import org.eclipse.leshan.core.node.codec.CodecException; import org.eclipse.leshan.core.util.Hex; -import org.eclipse.leshan.server.registration.Registration; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceTransportType; @@ -49,8 +46,6 @@ import org.thingsboard.server.transport.lwm2m.server.ota.firmware.FirmwareUpdate import org.thingsboard.server.transport.lwm2m.server.ota.software.SoftwareUpdateResult; import org.thingsboard.server.transport.lwm2m.server.ota.software.SoftwareUpdateState; -import java.util.Arrays; -import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -84,56 +79,6 @@ public class LwM2MTransportUtil { public static final String LOG_LWM2M_WARN = "warn"; public static final int BOOTSTRAP_DEFAULT_SHORT_ID_0 = 0; - public enum LwM2MClientStrategy { - CLIENT_STRATEGY_1(1, "Read only resources marked as observation"), - CLIENT_STRATEGY_2(2, "Read all client resources"); - - public int code; - public String type; - - LwM2MClientStrategy(int code, String type) { - this.code = code; - this.type = type; - } - - public static LwM2MClientStrategy fromStrategyClientByType(String type) { - for (LwM2MClientStrategy to : LwM2MClientStrategy.values()) { - if (to.type.equals(type)) { - return to; - } - } - throw new IllegalArgumentException(String.format("Unsupported Client Strategy type : %s", type)); - } - - public static LwM2MClientStrategy fromStrategyClientByCode(int code) { - for (LwM2MClientStrategy to : LwM2MClientStrategy.values()) { - if (to.code == code) { - return to; - } - } - throw new IllegalArgumentException(String.format("Unsupported Client Strategy code : %s", code)); - } - } - - public static boolean equalsResourceValue(Object valueOld, Object valueNew, ResourceModel.Type type, LwM2mPath - resourcePath) throws CodecException { - switch (type) { - case BOOLEAN: - case INTEGER: - case FLOAT: - return String.valueOf(valueOld).equals(String.valueOf(valueNew)); - case TIME: - return ((Date) valueOld).getTime() == ((Date) valueNew).getTime(); - case STRING: - case OBJLNK: - return valueOld.equals(valueNew); - case OPAQUE: - return Arrays.equals(Hex.decodeHex(((String) valueOld).toCharArray()), Hex.decodeHex(((String) valueNew).toCharArray())); - default: - throw new CodecException("Invalid value type for resource %s, type %s", resourcePath, type); - } - } - public static LwM2mOtaConvert convertOtaUpdateValueToString(String pathIdVer, Object value, ResourceModel.Type currentType) { String path = fromVersionedIdToObjectId(pathIdVer); LwM2mOtaConvert lwM2mOtaConvert = new LwM2mOtaConvert(); @@ -210,22 +155,8 @@ public class LwM2MTransportUtil { return null; } - public static String validPathIdVer(String pathIdVer, Registration registration) throws - IllegalArgumentException { - if (!pathIdVer.contains(LWM2M_SEPARATOR_PATH)) { - throw new IllegalArgumentException(String.format("Error:")); - } else { - String[] keyArray = pathIdVer.split(LWM2M_SEPARATOR_PATH); - if (keyArray.length > 1 && keyArray[1].split(LWM2M_SEPARATOR_KEY).length == 2) { - return pathIdVer; - } else { - return convertObjectIdToVersionedId(pathIdVer, registration); - } - } - } - - public static String convertObjectIdToVersionedId(String path, Registration registration) { - String ver = String.valueOf(registration.getSupportedObject().get(new LwM2mPath(path).getObjectId())); + public static String convertObjectIdToVersionedId(String path, LwM2mClient lwM2MClient) { + String ver = String.valueOf(lwM2MClient.getSupportedObjectVersion(new LwM2mPath(path).getObjectId())); return convertObjectIdToVerId(path, ver); } public static String convertObjectIdToVerId(String path, String ver) { @@ -243,14 +174,6 @@ public class LwM2MTransportUtil { } } - public static String validateObjectVerFromKey(String key) { - try { - return (key.split(LWM2M_SEPARATOR_PATH)[1].split(LWM2M_SEPARATOR_KEY)[1]); - } catch (Exception e) { - return ObjectModel.DEFAULT_VERSION; - } - } - /** * "UNSIGNED_INTEGER": // Number -> Integer Example: * Alarm Timestamp [32-bit unsigned integer] diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index 41886bd88a..8b7b88c261 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -21,6 +21,8 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.google.gson.Gson; import com.google.gson.JsonObject; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -97,6 +99,7 @@ import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.AsyncCallbackTemplate; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TopicService; @@ -106,14 +109,12 @@ import org.thingsboard.server.queue.scheduler.SchedulerComponent; import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.TbTransportComponent; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Random; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; @@ -179,19 +180,17 @@ public class DefaultTransportService extends TransportActivityManager implements protected TbQueueRequestTemplate, TbProtoQueueMsg> transportApiRequestTemplate; protected TbQueueProducer> ruleEngineMsgProducer; protected TbQueueProducer> tbCoreMsgProducer; - protected TbQueueConsumer> transportNotificationsConsumer; + protected QueueConsumerManager> transportNotificationsConsumer; protected MessagesStats ruleEngineProducerStats; protected MessagesStats tbCoreProducerStats; protected MessagesStats transportApiStats; protected ExecutorService transportCallbackExecutor; - private ExecutorService mainConsumerExecutor; + private ExecutorService consumerExecutor; private final Map toServerRpcPendingMap = new ConcurrentHashMap<>(); - private volatile boolean stopped = false; - public DefaultTransportService(PartitionService partitionService, TbServiceInfoProvider serviceInfoProvider, TbTransportQueueFactory queueProvider, @@ -232,42 +231,33 @@ public class DefaultTransportService extends TransportActivityManager implements transportApiRequestTemplate.setMessagesStats(transportApiStats); ruleEngineMsgProducer = producerProvider.getRuleEngineMsgProducer(); tbCoreMsgProducer = producerProvider.getTbCoreMsgProducer(); - transportNotificationsConsumer = queueProvider.createTransportNotificationsConsumer(); - TopicPartitionInfo tpi = topicService.getNotificationsTopic(ServiceType.TB_TRANSPORT, serviceInfoProvider.getServiceId()); - transportNotificationsConsumer.subscribe(Collections.singleton(tpi)); transportApiRequestTemplate.init(); - mainConsumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("transport-consumer")); + consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("transport-consumer")); + transportNotificationsConsumer = QueueConsumerManager.>builder() + .name("TB Transport") + .msgPackProcessor(this::processNotificationMsgs) + .pollInterval(notificationsPollDuration) + .consumerCreator(queueProvider::createTransportNotificationsConsumer) + .consumerExecutor(consumerExecutor) + .build(); } @AfterStartUp(order = AfterStartUp.TRANSPORT_SERVICE) public void start() { - mainConsumerExecutor.execute(() -> { - while (!stopped) { - try { - List> records = transportNotificationsConsumer.poll(notificationsPollDuration); - if (records.size() == 0) { - continue; - } - records.forEach(record -> { - try { - processToTransportMsg(record.getValue()); - } catch (Throwable e) { - log.warn("Failed to process the notification.", e); - } - }); - transportNotificationsConsumer.commit(); - } catch (Exception e) { - if (!stopped) { - log.warn("Failed to obtain messages from queue.", e); - try { - Thread.sleep(notificationsPollDuration); - } catch (InterruptedException e2) { - log.trace("Failed to wait until the server has capacity to handle new requests", e2); - } - } - } + TopicPartitionInfo tpi = topicService.getNotificationsTopic(ServiceType.TB_TRANSPORT, serviceInfoProvider.getServiceId()); + transportNotificationsConsumer.subscribe(Set.of(tpi)); + transportNotificationsConsumer.launch(); + } + + private void processNotificationMsgs(List> msgs, TbQueueConsumer> consumer) { + msgs.forEach(msg -> { + try { + processToTransportMsg(msg.getValue()); + } catch (Throwable e) { + log.warn("Failed to process the notification.", e); } }); + consumer.commit(); } private void invalidateRateLimits() { @@ -276,16 +266,14 @@ public class DefaultTransportService extends TransportActivityManager implements @PreDestroy public void destroy() { - stopped = true; - if (transportNotificationsConsumer != null) { - transportNotificationsConsumer.unsubscribe(); + transportNotificationsConsumer.stop(); } if (transportCallbackExecutor != null) { transportCallbackExecutor.shutdownNow(); } - if (mainConsumerExecutor != null) { - mainConsumerExecutor.shutdownNow(); + if (consumerExecutor != null) { + consumerExecutor.shutdownNow(); } if (transportApiRequestTemplate != null) { transportApiRequestTemplate.stop(); diff --git a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java index edf998dc8e..34171c19cd 100644 --- a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java +++ b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java @@ -21,14 +21,13 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.jgit.errors.LargeObjectException; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.context.event.EventListener; -import org.springframework.core.annotation.Order; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.EntityType; @@ -68,16 +67,16 @@ import org.thingsboard.server.gen.transport.TransportProtos.VersionedEntityInfoP import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbApplicationEventListener; import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.provider.TbVersionControlQueueFactory; +import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.TbVersionControlComponent; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; @@ -115,9 +114,8 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe private final Map pendingCommitMap = new HashMap<>(); private volatile ExecutorService consumerExecutor; - private volatile TbQueueConsumer> consumer; + private volatile QueueConsumerManager> consumer; private volatile TbQueueProducer> producer; - private volatile boolean stopped = false; @Value("${queue.vc.poll-interval:25}") private long pollDuration; @@ -134,20 +132,25 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe @PostConstruct public void init() { - consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("vc-consumer")); + consumerExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("vc-consumer")); var threadFactory = ThingsBoardThreadFactory.forName("vc-io-thread"); for (int i = 0; i < ioPoolSize; i++) { ioThreads.add(MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(threadFactory))); } producer = producerProvider.getTbCoreNotificationsMsgProducer(); - consumer = queueFactory.createToVersionControlMsgConsumer(); + consumer = QueueConsumerManager.>builder() + .name("TB Version Control") + .msgPackProcessor(this::processMsgs) + .pollInterval(pollDuration) + .consumerCreator(queueFactory::createToVersionControlMsgConsumer) + .consumerExecutor(consumerExecutor) + .build(); } @PreDestroy public void stop() { - stopped = true; if (consumer != null) { - consumer.unsubscribe(); + consumer.stop(); } if (consumerExecutor != null) { consumerExecutor.shutdownNow(); @@ -179,48 +182,29 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe return ServiceType.TB_VC_EXECUTOR.equals(event.getServiceType()); } - @EventListener(ApplicationReadyEvent.class) - @Order(value = 2) - public void onApplicationEvent(ApplicationReadyEvent event) { - consumerExecutor.execute(() -> consumerLoop(consumer)); + @AfterStartUp(order = 2) + public void afterStartUp() { + consumer.launch(); } - void consumerLoop(TbQueueConsumer> consumer) { - while (!stopped && !consumer.isStopped()) { - List> futures = new ArrayList<>(); - try { - List> msgs = consumer.poll(pollDuration); - if (msgs.isEmpty()) { - continue; - } - for (TbProtoQueueMsg msgWrapper : msgs) { - ToVersionControlServiceMsg msg = msgWrapper.getValue(); - var ctx = new VersionControlRequestCtx(msg, msg.hasClearRepositoryRequest() ? null : ProtoUtils.fromProto(msg.getVcSettings())); - long startTs = System.currentTimeMillis(); - log.trace("[{}][{}] RECEIVED task: {}", ctx.getTenantId(), ctx.getRequestId(), msg); - int threadIdx = Math.abs(ctx.getTenantId().hashCode() % ioPoolSize); - ListenableFuture future = ioThreads.get(threadIdx).submit(() -> processMessage(ctx, msg)); - logTaskExecution(ctx, future, startTs); - futures.add(future); - } - try { - Futures.allAsList(futures).get(packProcessingTimeout, TimeUnit.MILLISECONDS); - } catch (TimeoutException e) { - log.info("Timeout for processing the version control tasks.", e); - } - consumer.commit(); - } catch (Exception e) { - if (!stopped) { - log.warn("Failed to obtain version control requests from queue.", e); - try { - Thread.sleep(pollDuration); - } catch (InterruptedException e2) { - log.trace("Failed to wait until the server has capacity to handle new version control messages", e2); - } - } - } + void processMsgs(List> msgs, TbQueueConsumer> consumer) throws Exception { + List> futures = new ArrayList<>(); + for (TbProtoQueueMsg msgWrapper : msgs) { + ToVersionControlServiceMsg msg = msgWrapper.getValue(); + var ctx = new VersionControlRequestCtx(msg, msg.hasClearRepositoryRequest() ? null : ProtoUtils.fromProto(msg.getVcSettings())); + long startTs = System.currentTimeMillis(); + log.trace("[{}][{}] RECEIVED task: {}", ctx.getTenantId(), ctx.getRequestId(), msg); + int threadIdx = Math.abs(ctx.getTenantId().hashCode() % ioPoolSize); + ListenableFuture future = ioThreads.get(threadIdx).submit(() -> processMessage(ctx, msg)); + logTaskExecution(ctx, future, startTs); + futures.add(future); + } + try { + Futures.allAsList(futures).get(packProcessingTimeout, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + log.info("Timeout for processing the version control tasks.", e); } - log.info("TB Version Control request consumer stopped."); + consumer.commit(); } private Void processMessage(VersionControlRequestCtx ctx, ToVersionControlServiceMsg msg) { @@ -273,7 +257,7 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe var ids = vcService.listEntitiesAtVersion(ctx.getTenantId(), request.getVersionId(), path) .stream().skip(request.getOffset()).limit(request.getLimit()).collect(Collectors.toList()); if (!ids.isEmpty()) { - for (int i = 0; i < ids.size(); i++){ + for (int i = 0; i < ids.size(); i++) { VersionedEntityInfo info = ids.get(i); var data = vcService.getFileContentAtCommit(ctx.getTenantId(), getRelativePath(info.getExternalId().getEntityType(), info.getExternalId().getId().toString()), request.getVersionId()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java index f65e0db1c6..bf4798fa4d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java @@ -41,7 +41,9 @@ public interface AssetRepository extends JpaRepository, Expor AssetInfoEntity findAssetInfoById(@Param("assetId") UUID assetId); @Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " + - "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true)") + "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " + + " OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true " + + " OR ilike(a.type, CONCAT('%', :textSearch, '%')) = true)") Page findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @@ -54,14 +56,16 @@ public interface AssetRepository extends JpaRepository, Expor "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " + " OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true " + " OR ilike(p.name, CONCAT('%', :textSearch, '%')) = true " + - " OR ilike(c.title, CONCAT('%', :textSearch, '%')) = true) ") + " OR ilike(c.title, CONCAT('%', :textSearch, '%')) = true)") Page findAssetInfosByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " + "AND a.customerId = :customerId " + - "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true)") + "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " + + " OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true " + + " OR ilike(a.type, CONCAT('%', :textSearch, '%')) = true)") Page findByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("textSearch") String textSearch, @@ -69,7 +73,8 @@ public interface AssetRepository extends JpaRepository, Expor @Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " + "AND a.assetProfileId = :profileId " + - "AND (:searchText IS NULL OR ilike(a.name, CONCAT('%', :searchText, '%')) = true)") + "AND (:searchText IS NULL OR ilike(a.name, CONCAT('%', :searchText, '%')) = true " + + " OR ilike(a.label, CONCAT('%', :searchText, '%')) = true)") Page findByTenantIdAndProfileId(@Param("tenantId") UUID tenantId, @Param("profileId") UUID profileId, @Param("searchText") String searchText, @@ -81,7 +86,10 @@ public interface AssetRepository extends JpaRepository, Expor "LEFT JOIN AssetProfileEntity p on p.id = a.assetProfileId " + "WHERE a.tenantId = :tenantId " + "AND a.customerId = :customerId " + - "AND (:searchText IS NULL OR ilike(a.name, CONCAT('%', :searchText, '%')) = true)") + "AND (:searchText IS NULL OR ilike(a.name, CONCAT('%', :searchText, '%')) = true " + + " OR ilike(a.label, CONCAT('%', :searchText, '%')) = true " + + " OR ilike(c.title, CONCAT('%', :searchText, '%')) = true " + + " OR ilike(p.name, CONCAT('%', :searchText, '%')) = true) ") Page findAssetInfosByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("searchText") String searchText, @@ -95,7 +103,8 @@ public interface AssetRepository extends JpaRepository, Expor @Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " + "AND a.type = :type " + - "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true)") + "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " + + " OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true)") Page findByTenantIdAndType(@Param("tenantId") UUID tenantId, @Param("type") String type, @Param("textSearch") String textSearch, @@ -123,7 +132,8 @@ public interface AssetRepository extends JpaRepository, Expor "AND a.assetProfileId = :assetProfileId " + "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " + " OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true " + - " OR ilike(c.title, CONCAT('%', :textSearch, '%')) = true) ") + " OR ilike(c.title, CONCAT('%', :textSearch, '%')) = true " + + " OR ilike(a.type, CONCAT('%', :textSearch, '%')) = true) ") Page findAssetInfosByTenantIdAndAssetProfileId(@Param("tenantId") UUID tenantId, @Param("assetProfileId") UUID assetProfileId, @Param("textSearch") String textSearch, @@ -132,7 +142,8 @@ public interface AssetRepository extends JpaRepository, Expor @Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " + "AND a.customerId = :customerId AND a.type = :type " + - "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true)") + "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " + + " OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true) ") Page findByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("type") String type, @@ -146,7 +157,9 @@ public interface AssetRepository extends JpaRepository, Expor "WHERE a.tenantId = :tenantId " + "AND a.customerId = :customerId " + "AND a.type = :type " + - "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true)") + "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " + + " OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true " + + " OR ilike(c.title, CONCAT('%', :textSearch, '%')) = true) ") Page findAssetInfosByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("type") String type, @@ -160,7 +173,10 @@ public interface AssetRepository extends JpaRepository, Expor "WHERE a.tenantId = :tenantId " + "AND a.customerId = :customerId " + "AND a.assetProfileId = :assetProfileId " + - "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true)") + "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " + + " OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true " + + " OR ilike(c.title, CONCAT('%', :textSearch, '%')) = true " + + " OR ilike(a.type, CONCAT('%', :textSearch, '%')) = true) ") Page findAssetInfosByTenantIdAndCustomerIdAndAssetProfileId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("assetProfileId") UUID assetProfileId, @@ -172,7 +188,9 @@ public interface AssetRepository extends JpaRepository, Expor @Query("SELECT a FROM AssetEntity a, RelationEntity re WHERE a.tenantId = :tenantId " + "AND a.id = re.toId AND re.toType = 'ASSET' AND re.relationTypeGroup = 'EDGE' " + "AND re.relationType = 'Contains' AND re.fromId = :edgeId AND re.fromType = 'EDGE' " + - "AND (:searchText IS NULL OR ilike(a.name, CONCAT('%', :searchText, '%')) = true)") + "AND (:searchText IS NULL OR ilike(a.name, CONCAT('%', :searchText, '%')) = true " + + " OR ilike(a.label, CONCAT('%', :searchText, '%')) = true " + + " OR ilike(a.type, CONCAT('%', :searchText, '%')) = true) ") Page findByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId, @Param("edgeId") UUID edgeId, @Param("searchText") String searchText, @@ -182,7 +200,8 @@ public interface AssetRepository extends JpaRepository, Expor "AND a.id = re.toId AND re.toType = 'ASSET' AND re.relationTypeGroup = 'EDGE' " + "AND re.relationType = 'Contains' AND re.fromId = :edgeId AND re.fromType = 'EDGE' " + "AND a.type = :type " + - "AND (:searchText IS NULL OR ilike(a.name, CONCAT('%', :searchText, '%')) = true)") + "AND (:searchText IS NULL OR ilike(a.name, CONCAT('%', :searchText, '%')) = true " + + " OR ilike(a.label, CONCAT('%', :searchText, '%')) = true) ") Page findByTenantIdAndEdgeIdAndType(@Param("tenantId") UUID tenantId, @Param("edgeId") UUID edgeId, @Param("type") String type, diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java index 073dfd7d95..ddd6c3c881 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java @@ -45,7 +45,7 @@ public interface AttributeKvRepository extends JpaRepository findAllKeysByDeviceProfileId(@Param("tenantId") UUID tenantId, - @Param("deviceProfileId") UUID deviceProfileId); + @Param("deviceProfileId") UUID deviceProfileId); @Query(value = "SELECT DISTINCT attribute_key FROM attribute_kv WHERE " + "entity_id in (SELECT id FROM device WHERE tenant_id = :tenantId limit 100) ORDER BY attribute_key", nativeQuery = true) @@ -58,6 +58,6 @@ public interface AttributeKvRepository extends JpaRepository findAllKeysByEntityIdsAndAttributeType(@Param("entityIds") List entityIds, - @Param("attributeType") String attributeType); + @Param("attributeType") int attributeType); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java index 3bf8e43c70..320e0e67fb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java @@ -173,7 +173,7 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl @Override public List findAllKeysByEntityIdsAndAttributeType(TenantId tenantId, List entityIds, String attributeType) { return attributeKvRepository - .findAllKeysByEntityIdsAndAttributeType(entityIds.stream().map(EntityId::getId).collect(Collectors.toList()), attributeType) + .findAllKeysByEntityIdsAndAttributeType(entityIds.stream().map(EntityId::getId).collect(Collectors.toList()), AttributeScope.valueOf(attributeType).getId()) .stream().map(id -> keyDictionaryDao.getKey(id)).collect(Collectors.toList()); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java index 707155c86c..98a8cf9d47 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java @@ -21,6 +21,7 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; +import org.awaitility.Awaitility; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -47,6 +48,8 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Andrew Shvayka */ @@ -221,6 +224,45 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest { Assert.assertEquals(NEW_VALUE, value.get(1)); } + @Test + public void testFindAllKeysByEntityId() { + var tenantId = new TenantId(UUID.randomUUID()); + var deviceId = new DeviceId(UUID.randomUUID()); + saveAttribute(tenantId, deviceId, AttributeScope.SERVER_SCOPE, "key1", "123"); + saveAttribute(tenantId, deviceId, AttributeScope.SERVER_SCOPE, "key2", "123"); + + Awaitility.await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + List keys = attributesService.findAllKeysByEntityIds(tenantId, List.of(deviceId)); + assertThat(keys).containsOnly("key1", "key2"); + }); + } + + @Test + public void testFindAllKeysByEntityIdAndAttributeType() { + var tenantId = new TenantId(UUID.randomUUID()); + var deviceId = new DeviceId(UUID.randomUUID()); + saveAttribute(tenantId, deviceId, AttributeScope.SERVER_SCOPE, "key1", "123"); + saveAttribute(tenantId, deviceId, AttributeScope.SERVER_SCOPE, "key2", "123"); + + Awaitility.await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + List keys = attributesService.findAllKeysByEntityIds(tenantId, List.of(deviceId), AttributeScope.SERVER_SCOPE.name()); + assertThat(keys).containsOnly("key1", "key2"); + }); + } + + @Test + public void testFindAllByEntityIdAndAttributeType() { + var tenantId = new TenantId(UUID.randomUUID()); + var deviceId = new DeviceId(UUID.randomUUID()); + saveAttribute(tenantId, deviceId, AttributeScope.SERVER_SCOPE, "key1", "123"); + saveAttribute(tenantId, deviceId, AttributeScope.SERVER_SCOPE, "key2", "123"); + + Awaitility.await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + List attributes = attributesService.findAll(tenantId, deviceId, AttributeScope.SERVER_SCOPE).get(); + assertThat(attributes).extracting(KvEntry::getKey).containsOnly("key1", "key2"); + }); + } + private void testConcurrentFetchAndUpdate(TenantId tenantId, DeviceId deviceId, ListeningExecutorService pool) throws Exception { var scope = AttributeScope.SERVER_SCOPE; var key = "TEST"; diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java index bd6669f471..3e4b7ca38a 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java @@ -78,7 +78,7 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest { UUID assetId = Uuids.timeBased(); UUID tenantId = i % 2 == 0 ? tenantId1 : tenantId2; UUID customerId = i % 2 == 0 ? customerId1 : customerId2; - assets.add(saveAsset(assetId, tenantId, customerId, "ASSET_" + i)); + assets.add(saveAsset(assetId, tenantId, customerId, "ASSET_" + i, "label_" + i)); } assertEquals(assets.size(), assetDao.find(TenantId.fromUUID(tenantId1)).size()); } @@ -131,6 +131,21 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest { assertEquals(0, assets3.getData().size()); } + @Test + public void testFindAssetsByTenantIdAndCustomerIdByLabel() { + PageLink pageLink = new PageLink(20, 0, "label_"); + PageData assets1 = assetDao.findAssetsByTenantIdAndCustomerId(tenantId1, customerId1, pageLink); + assertEquals(20, assets1.getData().size()); + + pageLink = pageLink.nextPageLink(); + PageData assets2 = assetDao.findAssetsByTenantIdAndCustomerId(tenantId1, customerId1, pageLink); + assertEquals(10, assets2.getData().size()); + + pageLink = pageLink.nextPageLink(); + PageData assets3 = assetDao.findAssetsByTenantIdAndCustomerId(tenantId1, customerId1, pageLink); + assertEquals(0, assets3.getData().size()); + } + @Test public void testFindAssetsByTenantIdAndIdsAsync() throws ExecutionException, InterruptedException, TimeoutException { List searchIds = getAssetsUuids(tenantId1); @@ -180,21 +195,31 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest { @Test public void testFindAssetsByTenantIdAndType() { String type = "TYPE_2"; - assets.add(saveAsset(Uuids.timeBased(), tenantId2, customerId2, "TEST_ASSET", type)); + String testLabel = "test_label"; + assets.add(saveAsset(Uuids.timeBased(), tenantId2, customerId2, "TEST_ASSET", type, testLabel)); List foundedAssetsByType = assetDao .findAssetsByTenantIdAndType(tenantId2, type, new PageLink(3)).getData(); compareFoundedAssetByType(foundedAssetsByType, type); + + List foundedAssetsByTypeAndLabel = assetDao + .findAssetsByTenantIdAndType(tenantId2, type, new PageLink(3, 0, testLabel)).getData(); + assertEquals(1, foundedAssetsByTypeAndLabel.size()); } @Test public void testFindAssetsByTenantIdAndCustomerIdAndType() { String type = "TYPE_2"; - assets.add(saveAsset(Uuids.timeBased(), tenantId2, customerId2, "TEST_ASSET", type)); + String testLabel = "test_label"; + assets.add(saveAsset(Uuids.timeBased(), tenantId2, customerId2, "TEST_ASSET", type, testLabel)); List foundedAssetsByType = assetDao .findAssetsByTenantIdAndCustomerIdAndType(tenantId2, customerId2, type, new PageLink(3)).getData(); compareFoundedAssetByType(foundedAssetsByType, type); + + List foundedAssetsByTypeAndLabel = assetDao + .findAssetsByTenantIdAndCustomerIdAndType(tenantId2, customerId2, type, new PageLink(3, 0, testLabel)).getData(); + assertEquals(1, foundedAssetsByTypeAndLabel.size()); } private void compareFoundedAssetByType(List foundedAssetsByType, String type) { @@ -228,10 +253,14 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest { } private Asset saveAsset(UUID id, UUID tenantId, UUID customerId, String name) { - return saveAsset(id, tenantId, customerId, name, null); + return saveAsset(id, tenantId, customerId, name, null, null); + } + + private Asset saveAsset(UUID id, UUID tenantId, UUID customerId, String name, String label) { + return saveAsset(id, tenantId, customerId, name, null, label); } - private Asset saveAsset(UUID id, UUID tenantId, UUID customerId, String name, String type) { + private Asset saveAsset(UUID id, UUID tenantId, UUID customerId, String name, String type, String label) { if (type == null) { type = "default"; } @@ -241,6 +270,7 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest { asset.setCustomerId(new CustomerId(customerId)); asset.setName(name); asset.setType(type); + asset.setLabel(label); asset.setAssetProfileId(assetProfileId(type)); return assetDao.save(TenantId.fromUUID(tenantId), asset); } diff --git a/monitoring/src/main/resources/lwm2m/device_profile.json b/monitoring/src/main/resources/lwm2m/device_profile.json index cc78187938..c75fe9ce7a 100644 --- a/monitoring/src/main/resources/lwm2m/device_profile.json +++ b/monitoring/src/main/resources/lwm2m/device_profile.json @@ -12,14 +12,14 @@ "transportConfiguration": { "observeAttr": { "observe": [ - "/3_1.1/0/0" + "/3_1.0/0/0" ], "attribute": [], "telemetry": [ - "/3_1.1/0/0" + "/3_1.0/0/0" ], "keyName": { - "/3_1.1/0/0": "testData" + "/3_1.0/0/0": "testData" }, "attributeLwm2m": {} }, @@ -44,8 +44,7 @@ "clientOnlyObserveAfterConnect": 1, "fwUpdateStrategy": 1, "swUpdateStrategy": 1, - "powerMode": "DRX", - "compositeOperationsSupport": false + "powerMode": "DRX" }, "bootstrapServerUpdateEnable": false, "type": "LWM2M" diff --git a/monitoring/src/main/resources/lwm2m/models/test-model.xml b/monitoring/src/main/resources/lwm2m/models/test-model.xml index b13934531d..7b71b5e783 100644 --- a/monitoring/src/main/resources/lwm2m/models/test-model.xml +++ b/monitoring/src/main/resources/lwm2m/models/test-model.xml @@ -23,9 +23,9 @@ 3 - urn:oma:lwm2m:oma:3:1.1 + urn:oma:lwm2m:oma:3 1.1 - 1.1 + 1.0 Single Mandatory diff --git a/monitoring/src/main/resources/lwm2m/resource.json b/monitoring/src/main/resources/lwm2m/resource.json index 3f4ec5c3fe..dcc0182bdd 100644 --- a/monitoring/src/main/resources/lwm2m/resource.json +++ b/monitoring/src/main/resources/lwm2m/resource.json @@ -2,5 +2,5 @@ "title": "", "resourceType": "LWM2M_MODEL", "fileName": "test-model.xml", - "data": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLQoKICAgIENvcHlyaWdodCDCqSAyMDE2LTIwMjIgVGhlIFRoaW5nc2JvYXJkIEF1dGhvcnMKCiAgICBMaWNlbnNlZCB1bmRlciB0aGUgQXBhY2hlIExpY2Vuc2UsIFZlcnNpb24gMi4wICh0aGUgIkxpY2Vuc2UiKTsKICAgIHlvdSBtYXkgbm90IHVzZSB0aGlzIGZpbGUgZXhjZXB0IGluIGNvbXBsaWFuY2Ugd2l0aCB0aGUgTGljZW5zZS4KICAgIFlvdSBtYXkgb2J0YWluIGEgY29weSBvZiB0aGUgTGljZW5zZSBhdAoKICAgICAgICBodHRwOi8vd3d3LmFwYWNoZS5vcmcvbGljZW5zZXMvTElDRU5TRS0yLjAKCiAgICBVbmxlc3MgcmVxdWlyZWQgYnkgYXBwbGljYWJsZSBsYXcgb3IgYWdyZWVkIHRvIGluIHdyaXRpbmcsIHNvZnR3YXJlCiAgICBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICAgV0lUSE9VVCBXQVJSQU5USUVTIE9SIENPTkRJVElPTlMgT0YgQU5ZIEtJTkQsIGVpdGhlciBleHByZXNzIG9yIGltcGxpZWQuCiAgICBTZWUgdGhlIExpY2Vuc2UgZm9yIHRoZSBzcGVjaWZpYyBsYW5ndWFnZSBnb3Zlcm5pbmcgcGVybWlzc2lvbnMgYW5kCiAgICBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4KCi0tPgo8TFdNMk0geG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIKICAgICAgIHhzaTpub05hbWVzcGFjZVNjaGVtYUxvY2F0aW9uPSJodHRwOi8vd3d3Lm9wZW5tb2JpbGVhbGxpYW5jZS5vcmcvdGVjaC9wcm9maWxlcy9MV00yTS12MV8xLnhzZCI+CiAgICA8T2JqZWN0IE9iamVjdFR5cGU9Ik1PRGVmaW5pdGlvbiI+CiAgICAgICAgPE5hbWU+THdNMk0gTW9uaXRvcmluZzwvTmFtZT4KICAgICAgICA8RGVzY3JpcHRpb24xPgogICAgICAgICAgICA8IVtDREFUQVtdXT48L0Rlc2NyaXB0aW9uMT4KICAgICAgICA8T2JqZWN0SUQ+MzwvT2JqZWN0SUQ+CiAgICAgICAgPE9iamVjdFVSTj51cm46b21hOmx3bTJtOm9tYTozOjEuMTwvT2JqZWN0VVJOPgogICAgICAgIDxMV00yTVZlcnNpb24+MS4xPC9MV00yTVZlcnNpb24+CiAgICAgICAgPE9iamVjdFZlcnNpb24+MS4xPC9PYmplY3RWZXJzaW9uPgogICAgICAgIDxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgogICAgICAgIDxNYW5kYXRvcnk+TWFuZGF0b3J5PC9NYW5kYXRvcnk+CiAgICAgICAgPFJlc291cmNlcz4KICAgICAgICAgICAgPEl0ZW0gSUQ9IjAiPgogICAgICAgICAgICAgICAgPE5hbWU+VGVzdCBkYXRhPC9OYW1lPgogICAgICAgICAgICAgICAgPE9wZXJhdGlvbnM+UjwvT3BlcmF0aW9ucz4KICAgICAgICAgICAgICAgIDxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgogICAgICAgICAgICAgICAgPE1hbmRhdG9yeT5PcHRpb25hbDwvTWFuZGF0b3J5PgogICAgICAgICAgICAgICAgPFR5cGU+U3RyaW5nPC9UeXBlPgogICAgICAgICAgICAgICAgPFJhbmdlRW51bWVyYXRpb24+PC9SYW5nZUVudW1lcmF0aW9uPgogICAgICAgICAgICAgICAgPFVuaXRzPjwvVW5pdHM+CiAgICAgICAgICAgICAgICA8RGVzY3JpcHRpb24+PCFbQ0RBVEFbVGVzdCBkYXRhXV0+PC9EZXNjcmlwdGlvbj4KICAgICAgICAgICAgPC9JdGVtPgogICAgICAgIDwvUmVzb3VyY2VzPgogICAgICAgIDxEZXNjcmlwdGlvbjI+PC9EZXNjcmlwdGlvbjI+CiAgICA8L09iamVjdD4KPC9MV00yTT4K" + "data": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLQoKICAgIENvcHlyaWdodCDCqSAyMDE2LTIwMjQgVGhlIFRoaW5nc2JvYXJkIEF1dGhvcnMKCiAgICBMaWNlbnNlZCB1bmRlciB0aGUgQXBhY2hlIExpY2Vuc2UsIFZlcnNpb24gMi4wICh0aGUgIkxpY2Vuc2UiKTsKICAgIHlvdSBtYXkgbm90IHVzZSB0aGlzIGZpbGUgZXhjZXB0IGluIGNvbXBsaWFuY2Ugd2l0aCB0aGUgTGljZW5zZS4KICAgIFlvdSBtYXkgb2J0YWluIGEgY29weSBvZiB0aGUgTGljZW5zZSBhdAoKICAgICAgICBodHRwOi8vd3d3LmFwYWNoZS5vcmcvbGljZW5zZXMvTElDRU5TRS0yLjAKCiAgICBVbmxlc3MgcmVxdWlyZWQgYnkgYXBwbGljYWJsZSBsYXcgb3IgYWdyZWVkIHRvIGluIHdyaXRpbmcsIHNvZnR3YXJlCiAgICBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICAgV0lUSE9VVCBXQVJSQU5USUVTIE9SIENPTkRJVElPTlMgT0YgQU5ZIEtJTkQsIGVpdGhlciBleHByZXNzIG9yIGltcGxpZWQuCiAgICBTZWUgdGhlIExpY2Vuc2UgZm9yIHRoZSBzcGVjaWZpYyBsYW5ndWFnZSBnb3Zlcm5pbmcgcGVybWlzc2lvbnMgYW5kCiAgICBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4KCi0tPgo8TFdNMk0geG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIKICAgICAgIHhzaTpub05hbWVzcGFjZVNjaGVtYUxvY2F0aW9uPSJodHRwOi8vd3d3Lm9wZW5tb2JpbGVhbGxpYW5jZS5vcmcvdGVjaC9wcm9maWxlcy9MV00yTS12MV8xLnhzZCI+CiAgICA8T2JqZWN0IE9iamVjdFR5cGU9Ik1PRGVmaW5pdGlvbiI+CiAgICAgICAgPE5hbWU+THdNMk0gTW9uaXRvcmluZzwvTmFtZT4KICAgICAgICA8RGVzY3JpcHRpb24xPgogICAgICAgICAgICA8IVtDREFUQVtdXT48L0Rlc2NyaXB0aW9uMT4KICAgICAgICA8T2JqZWN0SUQ+MzwvT2JqZWN0SUQ+CiAgICAgICAgPE9iamVjdFVSTj51cm46b21hOmx3bTJtOm9tYTozPC9PYmplY3RVUk4+CiAgICAgICAgPExXTTJNVmVyc2lvbj4xLjE8L0xXTTJNVmVyc2lvbj4KICAgICAgICA8T2JqZWN0VmVyc2lvbj4xLjA8L09iamVjdFZlcnNpb24+CiAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CiAgICAgICAgPE1hbmRhdG9yeT5NYW5kYXRvcnk8L01hbmRhdG9yeT4KICAgICAgICA8UmVzb3VyY2VzPgogICAgICAgICAgICA8SXRlbSBJRD0iMCI+CiAgICAgICAgICAgICAgICA8TmFtZT5UZXN0IGRhdGE8L05hbWU+CiAgICAgICAgICAgICAgICA8T3BlcmF0aW9ucz5SPC9PcGVyYXRpb25zPgogICAgICAgICAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CiAgICAgICAgICAgICAgICA8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CiAgICAgICAgICAgICAgICA8VHlwZT5TdHJpbmc8L1R5cGU+CiAgICAgICAgICAgICAgICA8UmFuZ2VFbnVtZXJhdGlvbj48L1JhbmdlRW51bWVyYXRpb24+CiAgICAgICAgICAgICAgICA8VW5pdHM+PC9Vbml0cz4KICAgICAgICAgICAgICAgIDxEZXNjcmlwdGlvbj48IVtDREFUQVtUZXN0IGRhdGFdXT48L0Rlc2NyaXB0aW9uPgogICAgICAgICAgICA8L0l0ZW0+CiAgICAgICAgPC9SZXNvdXJjZXM+CiAgICAgICAgPERlc2NyaXB0aW9uMj48L0Rlc2NyaXB0aW9uMj4KICAgIDwvT2JqZWN0Pgo8L0xXTTJNPgo=" } \ No newline at end of file diff --git a/pom.xml b/pom.xml index c06166e497..c3451544a4 100755 --- a/pom.xml +++ b/pom.xml @@ -1476,6 +1476,12 @@ org.apache.httpcomponents httpclient ${apache-httpclient.version} + + + commons-logging + commons-logging + + org.apache.httpcomponents @@ -1943,6 +1949,12 @@ org.elasticsearch.client elasticsearch-rest-client ${elasticsearch.version} + + + commons-logging + commons-logging + + org.javadelight @@ -1968,11 +1980,23 @@ com.amazonaws aws-java-sdk-sqs ${aws.sdk.version} + + + commons-logging + commons-logging + + com.amazonaws aws-java-sdk-sns ${aws.sdk.version} + + + commons-logging + commons-logging + + com.google.cloud @@ -2102,6 +2126,10 @@ org.slf4j * + + commons-logging + commons-logging + com.github.spotbugs spotbugs-annotations @@ -2136,6 +2164,12 @@ mockserver-netty ${mock-server.version} test + + + commons-logging + commons-logging + + org.mock-server @@ -2168,6 +2202,12 @@ com.google.firebase firebase-admin ${firebase-admin.version} + + + commons-logging + commons-logging + + org.eclipse.jgit @@ -2203,6 +2243,12 @@ org.apache.xmlgraphics batik-transcoder ${apache-xmlgraphics.version} + + + commons-logging + commons-logging + + org.apache.xmlgraphics diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNode.java index 4baec1c882..0b30d02842 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNode.java @@ -15,6 +15,8 @@ */ package org.thingsboard.rule.engine.flow; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -22,10 +24,14 @@ import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; +import java.util.Optional; import java.util.UUID; @Slf4j @@ -33,11 +39,13 @@ import java.util.UUID; type = ComponentType.FLOW, name = "rule chain", configClazz = TbRuleChainInputNodeConfiguration.class, - nodeDescription = "transfers the message to another rule chain", - nodeDetails = "Allows to nest the rule chain similar to single rule node. " + - "The incoming message is forwarded to the input node of the specified target rule chain. " + - "The target rule chain may produce multiple labeled outputs. " + - "You may use the outputs to forward the results of processing to other rule nodes.", + version = 1, + nodeDescription = "Transfers the message to another rule chain", + nodeDetails = "The incoming message is forwarded to the input node of target rule chain. " + + "If 'Forward message to the originator's default rule chain' is enabled, " + + "then target rule chain might be resolved dynamically based on incoming message originator. " + + "In this case rule chain specified in the configuration will be used as fallback rule chain.

" + + "Output connections: Any connection(s) produced by output node(s) in the target rule chain.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbFlowNodeRuleChainInputConfig", relationTypes = {}, @@ -46,19 +54,57 @@ import java.util.UUID; ) public class TbRuleChainInputNode implements TbNode { - private TbRuleChainInputNodeConfiguration config; private RuleChainId ruleChainId; + private boolean forwardMsgToDefaultRuleChain; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbRuleChainInputNodeConfiguration.class); - this.ruleChainId = new RuleChainId(UUID.fromString(config.getRuleChainId())); + TbRuleChainInputNodeConfiguration config = TbNodeUtils.convert(configuration, TbRuleChainInputNodeConfiguration.class); + if (config.getRuleChainId() == null) { + throw new TbNodeException("Rule chain must be set!", true); + } + UUID ruleChainUUID; + try { + ruleChainUUID = UUID.fromString(config.getRuleChainId()); + } catch (Exception e) { + throw new TbNodeException("Failed to parse rule chain id: " + config.getRuleChainId(), true); + } + ruleChainId = new RuleChainId(ruleChainUUID); ctx.checkTenantEntity(ruleChainId); + forwardMsgToDefaultRuleChain = config.isForwardMsgToDefaultRuleChain(); } @Override - public void onMsg(TbContext ctx, TbMsg msg) { - ctx.input(msg, ruleChainId); + public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException { + RuleChainId targetRuleChainId = forwardMsgToDefaultRuleChain ? + getOriginatorDefaultRuleChainId(ctx, msg).orElse(ruleChainId) : ruleChainId; + ctx.input(msg, targetRuleChainId); } + @Override + public TbPair upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException { + boolean hasChanges = false; + switch (fromVersion) { + case 0 -> { + if (!oldConfiguration.has("forwardMsgToDefaultRuleChain")) { + hasChanges = true; + ((ObjectNode) oldConfiguration).put("forwardMsgToDefaultRuleChain", false); + } + } + default -> { + } + } + return new TbPair<>(hasChanges, oldConfiguration); + } + + private Optional getOriginatorDefaultRuleChainId(TbContext ctx, TbMsg msg) { + return Optional.ofNullable( + switch (msg.getOriginator().getEntityType()) { + case DEVICE -> + ctx.getDeviceProfileCache().get(ctx.getTenantId(), (DeviceId) msg.getOriginator()).getDefaultRuleChainId(); + case ASSET -> + ctx.getAssetProfileCache().get(ctx.getTenantId(), (AssetId) msg.getOriginator()).getDefaultRuleChainId(); + default -> null; + }); + } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNodeConfiguration.java index 30c2a49821..84ddb779f7 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNodeConfiguration.java @@ -22,10 +22,13 @@ import org.thingsboard.rule.engine.api.NodeConfiguration; public class TbRuleChainInputNodeConfiguration implements NodeConfiguration { private String ruleChainId; + private boolean forwardMsgToDefaultRuleChain; @Override public TbRuleChainInputNodeConfiguration defaultConfiguration() { - return new TbRuleChainInputNodeConfiguration(); + TbRuleChainInputNodeConfiguration configuration = new TbRuleChainInputNodeConfiguration(); + configuration.setForwardMsgToDefaultRuleChain(false); + return configuration; } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java index 281fdcb16a..f03d1e46ba 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java @@ -244,27 +244,27 @@ class AlarmState { return null; } } else { - currentAlarm = new Alarm(); - currentAlarm.setType(alarmDefinition.getAlarmType()); - currentAlarm.setAcknowledged(false); - currentAlarm.setCleared(false); - currentAlarm.setSeverity(severity); + var newAlarm = new Alarm(); + newAlarm.setType(alarmDefinition.getAlarmType()); + newAlarm.setAcknowledged(false); + newAlarm.setCleared(false); + newAlarm.setSeverity(severity); long startTs = dataSnapshot.getTs(); if (startTs == 0L) { startTs = System.currentTimeMillis(); } - currentAlarm.setStartTs(startTs); - currentAlarm.setEndTs(currentAlarm.getStartTs()); - currentAlarm.setDetails(createDetails(ruleState)); - currentAlarm.setOriginator(originator); - currentAlarm.setTenantId(ctx.getTenantId()); - currentAlarm.setPropagate(alarmDefinition.isPropagate()); - currentAlarm.setPropagateToOwner(alarmDefinition.isPropagateToOwner()); - currentAlarm.setPropagateToTenant(alarmDefinition.isPropagateToTenant()); + newAlarm.setStartTs(startTs); + newAlarm.setEndTs(startTs); + newAlarm.setDetails(createDetails(ruleState)); + newAlarm.setOriginator(originator); + newAlarm.setTenantId(ctx.getTenantId()); + newAlarm.setPropagate(alarmDefinition.isPropagate()); + newAlarm.setPropagateToOwner(alarmDefinition.isPropagateToOwner()); + newAlarm.setPropagateToTenant(alarmDefinition.isPropagateToTenant()); if (alarmDefinition.getPropagateRelationTypes() != null) { - currentAlarm.setPropagateRelationTypes(alarmDefinition.getPropagateRelationTypes()); + newAlarm.setPropagateRelationTypes(alarmDefinition.getPropagateRelationTypes()); } - AlarmApiCallResult result = ctx.getAlarmService().createAlarm(AlarmCreateOrUpdateActiveRequest.fromAlarm(currentAlarm)); + AlarmApiCallResult result = ctx.getAlarmService().createAlarm(AlarmCreateOrUpdateActiveRequest.fromAlarm(newAlarm)); currentAlarm = result.getAlarm(); return TbAlarmResult.fromAlarmResult(result); } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNodeTest.java new file mode 100644 index 0000000000..579ed7f035 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNodeTest.java @@ -0,0 +1,299 @@ +/** + * Copyright © 2016-2024 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. + */ +package org.thingsboard.rule.engine.flow; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest; +import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache; +import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.UUID; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class TbRuleChainInputNodeTest extends AbstractRuleNodeUpgradeTest { + + private final TenantId TENANT_ID = new TenantId(UUID.fromString("4ba69ea5-6b27-42df-ab66-e7a727a67027")); + private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("97731954-2147-4176-8f1a-d14f1b73e4e6")); + private final AssetId ASSET_ID = new AssetId(UUID.fromString("841a47bd-4e8e-4ea5-88e6-420da0d70e51")); + + private TbRuleChainInputNode node; + private TbRuleChainInputNodeConfiguration config; + private TbNodeConfiguration nodeConfiguration; + + @Mock + private TbContext ctxMock; + @Mock + private RuleEngineDeviceProfileCache deviceProfileCacheMock; + @Mock + private RuleEngineAssetProfileCache assetProfileCacheMock; + + @BeforeEach + public void setUp() { + node = spy(new TbRuleChainInputNode()); + config = new TbRuleChainInputNodeConfiguration().defaultConfiguration(); + nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + } + + @ParameterizedTest + @MethodSource + public void givenValidConfig_whenInit_thenOk(String ruleChainIdStr, boolean forwardMsgToDefaultRuleChain) throws TbNodeException { + //GIVEN + config.setRuleChainId(ruleChainIdStr); + config.setForwardMsgToDefaultRuleChain(forwardMsgToDefaultRuleChain); + nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + + //WHEN + assertThatCode(() -> node.init(ctxMock, nodeConfiguration)) + .doesNotThrowAnyException(); + + //THEN + verify(ctxMock).checkTenantEntity(new RuleChainId(UUID.fromString(ruleChainIdStr))); + } + + private static Stream givenValidConfig_whenInit_thenOk() { + return Stream.of( + Arguments.of("45bba7c4-04bf-419b-ae03-6ceb9724f10e", false), + Arguments.of("52d57e1b-70bb-480e-bcc4-6710e1dcc9d8", true) + ); + } + + @ParameterizedTest + @ValueSource(strings = {"91acbce0-079fdb", "", " ", "my test string"}) + public void givenInvalidRuleChainId_whenInit_thenThrowsException(String ruleChainIdStr) { + //GIVEN + config.setRuleChainId(ruleChainIdStr); + nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + + //WHEN-THEN + Assertions.assertThatThrownBy(() -> node.init(ctxMock, nodeConfiguration)) + .isInstanceOf(TbNodeException.class) + .hasMessage("Failed to parse rule chain id: " + ruleChainIdStr); + } + + @Test + public void givenRuleChainIdIsNotSet_whenInit_thenThrowsException() { + assertThatThrownBy(() -> node.init(ctxMock, nodeConfiguration)) + .isInstanceOf(TbNodeException.class) + .hasMessage("Rule chain must be set!"); + } + + @Test + public void givenForwardMsgToDefaultIsTrue_whenOnMsg_thenShouldTransferToDeviceDefaultRuleChain() throws TbNodeException { + //GIVEN + DeviceProfile deviceProfile = new DeviceProfile(); + RuleChainId defaultRuleChainId = new RuleChainId(UUID.fromString("196e3cd5-68b8-421e-a0cf-1d44fa377cdf")); + deviceProfile.setDefaultRuleChainId(defaultRuleChainId); + + TbMsg msg = getMsg(DEVICE_ID); + + String ruleChainIdFromConfigStr = "acbc924f-7f95-4a9b-a854-e4822deb74c7"; + config.setRuleChainId(ruleChainIdFromConfigStr); + config.setForwardMsgToDefaultRuleChain(true); + nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + + when(ctxMock.getTenantId()).thenReturn(TENANT_ID); + when(ctxMock.getDeviceProfileCache()).thenReturn(deviceProfileCacheMock); + when(deviceProfileCacheMock.get(any(TenantId.class), any(DeviceId.class))).thenReturn(deviceProfile); + + node.init(ctxMock, nodeConfiguration); + + //WHEN + node.onMsg(ctxMock, msg); + + //THEN + ArgumentCaptor ruleChainArgumentCaptor = ArgumentCaptor.forClass(RuleChainId.class); + verify(ctxMock).input(eq(msg), ruleChainArgumentCaptor.capture()); + RuleChainId expectedRuleChainId = ruleChainArgumentCaptor.getValue(); + assertThat(expectedRuleChainId).isEqualTo(defaultRuleChainId); + + RuleChainId ruleChainId = (RuleChainId) ReflectionTestUtils.getField(node, "ruleChainId"); + assertThat(ruleChainId).isEqualTo(new RuleChainId(UUID.fromString(ruleChainIdFromConfigStr))); + } + + @Test + public void givenForwardMsgToDefaultIsTrue_whenOnMsg_thenShouldTransferToAssetDefaultRuleChain() throws TbNodeException { + //GIVEN + AssetProfile assetProfile = new AssetProfile(); + RuleChainId defaultRuleChainId = new RuleChainId(UUID.fromString("f0a3cd58-980c-4730-a40c-8f59064d2065")); + assetProfile.setDefaultRuleChainId(defaultRuleChainId); + + TbMsg msg = getMsg(ASSET_ID); + + String ruleChainIdFromConfigStr = "56f1c0b8-1a00-4ce0-b3ab-a1416d7cc429"; + config.setRuleChainId(ruleChainIdFromConfigStr); + config.setForwardMsgToDefaultRuleChain(true); + nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + + when(ctxMock.getTenantId()).thenReturn(TENANT_ID); + when(ctxMock.getAssetProfileCache()).thenReturn(assetProfileCacheMock); + when(assetProfileCacheMock.get(any(TenantId.class), any(AssetId.class))).thenReturn(assetProfile); + + node.init(ctxMock, nodeConfiguration); + + //WHEN + node.onMsg(ctxMock, msg); + + //THEN + ArgumentCaptor ruleChainArgumentCaptor = ArgumentCaptor.forClass(RuleChainId.class); + verify(ctxMock).input(eq(msg), ruleChainArgumentCaptor.capture()); + RuleChainId expectedRuleChainId = ruleChainArgumentCaptor.getValue(); + assertThat(expectedRuleChainId).isEqualTo(defaultRuleChainId); + + RuleChainId ruleChainId = (RuleChainId) ReflectionTestUtils.getField(node, "ruleChainId"); + assertThat(ruleChainId).isEqualTo(new RuleChainId(UUID.fromString(ruleChainIdFromConfigStr))); + } + + @Test + public void givenForwardMsgToDefaultIsTrueWithoutDeviceDefaultRuleChain_whenOnMsg_thenShouldTransferToRuleChainFromConfig() throws TbNodeException { + //GIVEN + DeviceProfile deviceProfile = new DeviceProfile(); + + TbMsg msg = getMsg(DEVICE_ID); + + String ruleChainIdFromConfigStr = "357c2785-e7cc-46a8-9797-957180dabdeb"; + RuleChainId ruleChainIdFromConfig = new RuleChainId(UUID.fromString(ruleChainIdFromConfigStr)); + config.setRuleChainId(ruleChainIdFromConfigStr); + config.setForwardMsgToDefaultRuleChain(true); + nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + + when(ctxMock.getTenantId()).thenReturn(TENANT_ID); + when(ctxMock.getDeviceProfileCache()).thenReturn(deviceProfileCacheMock); + when(deviceProfileCacheMock.get(any(TenantId.class), any(DeviceId.class))).thenReturn(deviceProfile); + + node.init(ctxMock, nodeConfiguration); + + //WHEN + node.onMsg(ctxMock, msg); + + //THEN + ArgumentCaptor ruleChainArgumentCaptor = ArgumentCaptor.forClass(RuleChainId.class); + verify(ctxMock).input(eq(msg), ruleChainArgumentCaptor.capture()); + assertThat(ruleChainArgumentCaptor.getValue()).isEqualTo(ruleChainIdFromConfig); + } + + @Test + public void givenForwardMsgToDefaultIsTrueWithoutAssetDefaultRuleChain_whenOnMsg_thenShouldTransferToRuleChainFromConfig() throws TbNodeException { + //GIVEN + AssetProfile assetProfile = new AssetProfile(); + + TbMsg msg = getMsg(ASSET_ID); + + String ruleChainIdFromConfigStr = "12883c3d-c10b-4d5b-b606-a59385a920bc"; + RuleChainId ruleChainIdFromConfig = new RuleChainId(UUID.fromString(ruleChainIdFromConfigStr)); + config.setRuleChainId(ruleChainIdFromConfigStr); + config.setForwardMsgToDefaultRuleChain(true); + nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + + when(ctxMock.getTenantId()).thenReturn(TENANT_ID); + when(ctxMock.getAssetProfileCache()).thenReturn(assetProfileCacheMock); + when(assetProfileCacheMock.get(any(TenantId.class), any(AssetId.class))).thenReturn(assetProfile); + + node.init(ctxMock, nodeConfiguration); + + //WHEN + node.onMsg(ctxMock, msg); + + //THEN + ArgumentCaptor ruleChainArgumentCaptor = ArgumentCaptor.forClass(RuleChainId.class); + verify(ctxMock).input(eq(msg), ruleChainArgumentCaptor.capture()); + assertThat(ruleChainArgumentCaptor.getValue()).isEqualTo(ruleChainIdFromConfig); + } + + @Test + public void givenRuleChainInConfig_whenOnMsg_thenShouldTransferToRuleChainFromConfig() throws TbNodeException { + //GIVEN + String ruleChainIdFromConfigStr = "3c02c8b3-645c-4e67-aac5-f984f59471d1"; + RuleChainId ruleChainIdFromConfig = new RuleChainId(UUID.fromString(ruleChainIdFromConfigStr)); + + TbMsg msg = getMsg(DEVICE_ID); + + config.setRuleChainId(ruleChainIdFromConfigStr); + config.setForwardMsgToDefaultRuleChain(false); + nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + + node.init(ctxMock, nodeConfiguration); + + //WHEN + node.onMsg(ctxMock, msg); + + //THEN + ArgumentCaptor ruleChainArgumentCaptor = ArgumentCaptor.forClass(RuleChainId.class); + verify(ctxMock).input(eq(msg), ruleChainArgumentCaptor.capture()); + assertThat(ruleChainArgumentCaptor.getValue()).isEqualTo(ruleChainIdFromConfig); + } + + private static Stream givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() { + return Stream.of( + //config for version 0 + Arguments.of(0, + "{\"ruleChainId\": null}", + true, + "{\"ruleChainId\": null, \"forwardMsgToDefaultRuleChain\": false}" + ), + //config for version 1 with upgrade from version 0 + Arguments.of(1, + "{\"ruleChainId\": null, \"forwardMsgToDefaultRuleChain\": false}", + false, + "{\"ruleChainId\": null, \"forwardMsgToDefaultRuleChain\": false}" + ) + ); + } + + @Override + protected TbNode getTestNode() { + return node; + } + + private TbMsg getMsg(EntityId entityId) { + return TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, entityId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + } +} diff --git a/ui-ngx/src/app/core/api/data-aggregator.ts b/ui-ngx/src/app/core/api/data-aggregator.ts index 5bce9115e8..d437bd73d0 100644 --- a/ui-ngx/src/app/core/api/data-aggregator.ts +++ b/ui-ngx/src/app/core/api/data-aggregator.ts @@ -48,7 +48,8 @@ class AggDataMap { constructor( private subsTw: SubscriptionTimewindow, - private endTs: number + private endTs: number, + private aggType: AggregationType ){}; set(ts: number, data: AggData) { @@ -78,7 +79,7 @@ class AggDataMap { } calculateAggInterval(timestamp: number): [number, number] { - return calculateAggIntervalWithSubscriptionTimeWindow(this.subsTw, this.endTs, timestamp); + return calculateAggIntervalWithSubscriptionTimeWindow(this.subsTw, this.endTs, timestamp, this.aggType); } updateLastInterval(endTs: number) { @@ -87,7 +88,7 @@ class AggDataMap { const lastTs = this.map.maxKey(); if (lastTs) { const data = this.map.get(lastTs); - const interval = calculateAggIntervalWithSubscriptionTimeWindow(this.subsTw, endTs, data.ts); + const interval = calculateAggIntervalWithSubscriptionTimeWindow(this.subsTw, endTs, data.ts, this.aggType); data.interval = interval; data.ts = interval[0] + Math.floor((interval[1] - interval[0]) / 2); } @@ -415,7 +416,7 @@ export class DataAggregator { const noAggregation = aggType === AggregationType.NONE; let aggKeyData = aggregationMap.aggMap[id]; if (!aggKeyData) { - aggKeyData = new AggDataMap(this.subsTw, this.endTs); + aggKeyData = new AggDataMap(this.subsTw, this.endTs, aggType); aggregationMap.aggMap[id] = aggKeyData; } const keyData = data[id]; @@ -449,7 +450,7 @@ export class DataAggregator { const noAggregation = aggType === AggregationType.NONE; let aggKeyData = this.aggregationMap.aggMap[id]; if (!aggKeyData) { - aggKeyData = new AggDataMap(this.subsTw, this.endTs); + aggKeyData = new AggDataMap(this.subsTw, this.endTs, aggType); this.aggregationMap.aggMap[id] = aggKeyData; } const keyData = data[id]; diff --git a/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts b/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts index 00bc0c3e64..8414e6ff0e 100644 --- a/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts +++ b/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts @@ -17,7 +17,6 @@ import { Compiler, Component, - ComponentFactory, Injectable, Injector, NgModule, @@ -26,9 +25,9 @@ import { Type, ɵresetCompiledComponents } from '@angular/core'; -import { from, Observable } from 'rxjs'; +import { from, Observable, of } from 'rxjs'; import { CommonModule } from '@angular/common'; -import { catchError, map, mergeMap } from 'rxjs/operators'; +import { mergeMap } from 'rxjs/operators'; @NgModule() export abstract class DynamicComponentModule implements OnDestroy { @@ -79,33 +78,32 @@ export class DynamicComponentFactoryService { declarations: [comp], imports: moduleImports })(class DynamicComponentInstanceModule extends DynamicComponentModule {}); - return from(this.compiler.compileModuleAsync(dynamicComponentInstanceModule)).pipe( - map((module) => { - let moduleRef: NgModuleRef; - try { - moduleRef = module.create(this.injector); - } catch (e) { - this.compiler.clearCacheFor(module.moduleType); - throw e; - } - this.dynamicComponentModulesMap.set(comp, { - moduleRef, - moduleType: module.moduleType - }); - return { - componentType: comp, - componentModuleRef: moduleRef - }; - }), - catchError((error) => { - if (compileAttempt === 1) { - ɵresetCompiledComponents(); - return this.createDynamicComponent(componentType, template, modules, preserveWhitespaces, ++compileAttempt, styles); - } else { - throw error; - } - }) - ); + try { + const module = this.compiler.compileModuleSync(dynamicComponentInstanceModule); + let moduleRef: NgModuleRef; + try { + moduleRef = module.create(this.injector); + } catch (e) { + this.compiler.clearCacheFor(module.moduleType); + throw e; + } + this.dynamicComponentModulesMap.set(comp, { + moduleRef, + moduleType: module.moduleType + }); + return of( { + componentType: comp, + componentModuleRef: moduleRef + }); + } catch (error) { + if (compileAttempt === 1) { + ɵresetCompiledComponents(); + return this.createDynamicComponent(componentType, template, modules, preserveWhitespaces, ++compileAttempt, styles); + } else { + console.error(error); + throw error; + } + } }) ); } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html index 4dc332afa1..22920a609c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html @@ -91,26 +91,16 @@ - - - - - - - - - - - - - - - - - - - - +
+ device-profile.lwm2m.default-object-id + + + + {{ objectIDVerTranslationMap.get(objectIDVer) | translate }} + + + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts index 6d4fe85fd5..fbc72d9498 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts @@ -40,9 +40,11 @@ import { ObjectLwM2M, OBSERVE, PowerMode, + ObjectIDVer, RESOURCES, ServerSecurityConfig, - TELEMETRY + TELEMETRY, + ObjectIDVerTranslationMap } from './lwm2m-profile-config.models'; import { DeviceProfileService } from '@core/http/device-profile.service'; import { deepClone, isDefinedAndNotNull, isEmpty, isUndefined } from '@core/utils'; @@ -80,6 +82,10 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro lwm2mDeviceProfileFormGroup: UntypedFormGroup; configurationValue: Lwm2mProfileConfigModels; + + objectIDVers = Object.values(ObjectIDVer) as ObjectIDVer[]; + objectIDVerTranslationMap = ObjectIDVerTranslationMap; + sortFunction: (key: string, value: object) => object; get required(): boolean { @@ -116,7 +122,8 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro powerMode: [PowerMode.DRX, Validators.required], edrxCycle: [{disabled: true, value: 0}, Validators.required], psmActivityTimer: [{disabled: true, value: 0}, Validators.required], - pagingTransmissionWindow: [{disabled: true, value: 0}, Validators.required] + pagingTransmissionWindow: [{disabled: true, value: 0}, Validators.required], + defaultObjectIDVer: [ObjectIDVer.V1_0, Validators.required] }) }); @@ -274,7 +281,8 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro edrxCycle: this.configurationValue.clientLwM2mSettings.edrxCycle || DEFAULT_EDRX_CYCLE, pagingTransmissionWindow: this.configurationValue.clientLwM2mSettings.pagingTransmissionWindow || DEFAULT_PAGING_TRANSMISSION_WINDOW, - psmActivityTimer: this.configurationValue.clientLwM2mSettings.psmActivityTimer || DEFAULT_PSM_ACTIVITY_TIMER + psmActivityTimer: this.configurationValue.clientLwM2mSettings.psmActivityTimer || DEFAULT_PSM_ACTIVITY_TIMER, + defaultObjectIDVer: this.configurationValue.clientLwM2mSettings.defaultObjectIDVer || ObjectIDVer.V1_0 } }, {emitEvent: false}); diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts index e5c292ef0a..8c1bb56a49 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts @@ -122,6 +122,18 @@ export const PowerModeTranslationMap = new Map( ] ); +export enum ObjectIDVer { + V1_0 = '1.0', + V1_1 = '1.1' +} + +export const ObjectIDVerTranslationMap = new Map( + [ + [ObjectIDVer.V1_0, 'device-profile.lwm2m.default-object-id-ver.v1-0'], + [ObjectIDVer.V1_1, 'device-profile.lwm2m.default-object-id-ver.v1-1'] + ] +); + export interface ServerSecurityConfig { host?: string; port?: number; @@ -163,7 +175,7 @@ export interface ClientLwM2mSettings { edrxCycle?: number; pagingTransmissionWindow?: number; psmActivityTimer?: number; - compositeOperationsSupport: boolean; + defaultObjectIDVer: ObjectIDVer; } export interface ObservableAttributes { @@ -190,7 +202,7 @@ export function getDefaultProfileClientLwM2mSettingsConfig(): ClientLwM2mSetting fwUpdateStrategy: 1, swUpdateStrategy: 1, powerMode: PowerMode.DRX, - compositeOperationsSupport: false + defaultObjectIDVer: ObjectIDVer.V1_0 }; } diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.models.ts index 428fb6fa1b..df63694df0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.models.ts @@ -15,6 +15,7 @@ /// import { + CellClickColumnInfo, WidgetActionDescriptor, WidgetActionSource, widgetActionTypeTranslationMap @@ -30,6 +31,7 @@ import { deepClone } from '@core/utils'; export interface WidgetActionCallbacks { fetchDashboardStates: (query: string) => Array; + fetchCellClickColumns: () => Array; } export interface WidgetActionsData { diff --git a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html index f75b2e44e7..0330ad5621 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html @@ -50,6 +50,27 @@ +
+
{{'widget-config.column-index' | translate}}*
+ + + + {{ getCellClickColumnInfo($index, column) }} + + + + warning + + +
{{'widget-config.action-name' | translate}}*
diff --git a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts index 2c7f94b8d5..230811fbd1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, Inject, OnDestroy, OnInit, SkipSelf } from '@angular/core'; +import { Component, Inject, OnDestroy, OnInit, SkipSelf, ViewChild } from '@angular/core'; import { ErrorStateMatcher } from '@angular/material/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; @@ -40,6 +40,7 @@ import { import { UtilsService } from '@core/services/utils.service'; import { actionDescriptorToAction, + CellClickColumnInfo, defaultWidgetAction, WidgetActionSource, widgetType @@ -47,6 +48,9 @@ import { import { takeUntil } from 'rxjs/operators'; import { CustomActionEditorCompleter } from '@home/components/widget/lib/settings/common/action/custom-action.models'; import { WidgetService } from '@core/http/widget.service'; +import { isDefinedAndNotNull, isNotEmptyStr } from '@core/utils'; +import { MatSelect } from '@angular/material/select'; +import { TranslateService } from '@ngx-translate/core'; export interface WidgetActionDialogData { isAdd: boolean; @@ -78,6 +82,12 @@ export class WidgetActionDialogComponent extends DialogComponent = []; + usedCellClickColumns: Array = []; + + @ViewChild('columnIndexSelect') columnIndexSelect: MatSelect; + columnIndexPlaceholderText = this.translate.instant('widget-config.select-column-index'); + constructor(protected store: Store, protected router: Router, private utils: UtilsService, @@ -85,7 +95,8 @@ export class WidgetActionDialogComponent extends DialogComponent, - public fb: FormBuilder) { + public fb: FormBuilder, + private translate: TranslateService) { super(store, router, dialogRef); this.isAdd = data.isAdd; if (this.isAdd) { @@ -99,11 +110,15 @@ export class WidgetActionDialogComponent extends DialogComponent { + ).subscribe((value) => { this.widgetActionFormGroup.get('name').updateValueAndValidity(); this.updateShowWidgetActionForm(); + if (value === 'cellClick') { + this.widgetActionFormGroup.get('columnIndex').setValidators([Validators.required]); + this.getCellClickColumnsInfo(); + } else { + this.widgetActionFormGroup.get('columnIndex').clearValidators(); + } + this.widgetActionFormGroup.get('columnIndex').updateValueAndValidity(); }); this.widgetActionFormGroup.get('useShowWidgetActionFunction').valueChanges.pipe( takeUntil(this.destroy$) ).subscribe(() => { this.updateShowWidgetActionForm(); }); + setTimeout(() => { + if (this.action?.actionSourceId === 'cellClick' && isDefinedAndNotNull(this.action.columnIndex) && + this.widgetActionFormGroup.get('columnIndex').value === null) { + this.columnIndexPlaceholderText = `${this.action.columnIndex} (${this.translate.instant('widget-config.not-set')})`; + this.columnIndexSelect.focus(); + } + }); } ngOnDestroy() { @@ -155,6 +184,22 @@ export class WidgetActionDialogComponent extends DialogComponent { + const actionColumn = this.configuredColumns[action.columnIndex]; + if (actionColumn && action.columnIndex !== this.action.columnIndex) { + this.usedCellClickColumns.push(action.columnIndex); + } + }); + } + } + private validateActionName(): ValidatorFn { return (c: FormControl) => { const newName = c.value; @@ -193,6 +238,10 @@ export class WidgetActionDialogComponent extends DialogComponent - + {{ column.title }} + [ngStyle]="cellStyle(alarm, column, row)" + (click)="onCellClick($event, alarm, column, $index)" + [class.tb-pointer]="columnHasCellClick($index)"> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts index b0a8fafd08..1d1aa533cd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts @@ -204,6 +204,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, private columnWidth: {[key: string]: string} = {}; private columnDefaultVisibility: {[key: string]: boolean} = {}; private columnSelectionAvailability: {[key: string]: boolean} = {}; + private columnsWithCellClick: Array = []; private rowStylesInfo: RowStyleInfo; @@ -358,6 +359,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, this.enableStickyAction = isDefined(this.settings.enableStickyAction) ? this.settings.enableStickyAction : false; this.showCellActionsMenu = isDefined(this.settings.showCellActionsMenu) ? this.settings.showCellActionsMenu : true; this.columnDisplayAction.show = isDefined(this.settings.enableSelectColumnDisplay) ? this.settings.enableSelectColumnDisplay : true; + this.columnsWithCellClick = this.ctx.actionsApi.getActionDescriptors('cellClick').map(action => action.columnIndex); let enableFilter; if (isDefined(this.settings.enableFilter)) { enableFilter = this.settings.enableFilter; @@ -794,6 +796,33 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, return res; } + public onCellClick($event: Event, alarm: AlarmDataInfo, key: EntityColumn, columnIndex: number) { + this.alarmsDatasource.toggleCurrentAlarm(alarm); + const descriptors = this.ctx.actionsApi.getActionDescriptors('cellClick'); + let descriptor; + if (descriptors.length) { + descriptor = descriptors.find(desc => desc.columnIndex === columnIndex); + } + if ($event && descriptor) { + $event.stopPropagation(); + let entityId; + let entityName; + let entityLabel; + if (alarm && alarm.originator) { + entityId = alarm.originator; + entityName = alarm.entityName; + entityLabel = alarm.entityLabel; + } + this.ctx.actionsApi.handleWidgetAction($event, descriptor, entityId, entityName, {alarm, key}, entityLabel); + } + } + + public columnHasCellClick(columnIndex: number) { + if (this.columnsWithCellClick.length) { + return this.columnsWithCellClick.includes(columnIndex); + } + } + public onRowClick($event: Event, alarm: AlarmDataInfo) { if ($event) { $event.stopPropagation(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.html index 22e79fd526..36d58742a6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.html @@ -40,12 +40,14 @@
- + {{ column.title }} - - + + +
+
+
gateway.security
+ + + {{ SecurityTypeTranslationsMap.get(type) | translate }} + + +
+ + +
+
gateway.username
+
+ + + + warning + + +
+
+
+
gateway.password
+
+ + +
+ +
+
+
+
+
+ +
{{ 'gateway.path-hint' | translate }}
+
+
gateway.CA-certificate-path
+
+ + + +
+
+
+
gateway.private-key-path
+
+ + + +
+
+
+
gateway.client-cert-path
+
+ + + +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.scss new file mode 100644 index 0000000000..416f368279 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.scss @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2024 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. + */ +:host { + width: 100%; + height: 100%; + display: block; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.ts new file mode 100644 index 0000000000..655545e5fb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.ts @@ -0,0 +1,165 @@ +/// +/// Copyright © 2016-2024 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 { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + forwardRef, + NgZone, + OnDestroy, + ViewContainerRef +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { MatDialog } from '@angular/material/dialog'; +import { DialogService } from '@core/services/dialog.service'; +import { Subject } from 'rxjs'; +import { Overlay } from '@angular/cdk/overlay'; +import { UtilsService } from '@core/services/utils.service'; +import { EntityService } from '@core/http/entity.service'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormGroup, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { + BrokerSecurityType, + BrokerSecurityTypeTranslationsMap, + noLeadTrailSpacesRegex +} from '@home/components/widget/lib/gateway/gateway-widget.models'; +import { takeUntil } from 'rxjs/operators'; + +@Component({ + selector: 'tb-broker-security', + templateUrl: './broker-security.component.html', + styleUrls: ['./broker-security.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => BrokerSecurityComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => BrokerSecurityComponent), + multi: true + } + ] +}) +export class BrokerSecurityComponent extends PageComponent implements ControlValueAccessor, Validator, OnDestroy { + + BrokerSecurityType = BrokerSecurityType; + + securityTypes = Object.values(BrokerSecurityType); + + SecurityTypeTranslationsMap = BrokerSecurityTypeTranslationsMap; + + securityFormGroup: UntypedFormGroup; + + private destroy$ = new Subject(); + private propagateChange = (v: any) => {}; + + constructor(protected store: Store, + public translate: TranslateService, + public dialog: MatDialog, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, + private dialogService: DialogService, + private entityService: EntityService, + private utils: UtilsService, + private zone: NgZone, + private cd: ChangeDetectorRef, + private elementRef: ElementRef, + private fb: FormBuilder) { + super(store); + this.securityFormGroup = this.fb.group({ + type: [BrokerSecurityType.ANONYMOUS, []], + username: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + password: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], + pathToCACert: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], + pathToPrivateKey: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], + pathToClientCert: ['', [Validators.pattern(noLeadTrailSpacesRegex)]] + }); + this.securityFormGroup.valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((value) => { + this.updateView(value); + }); + this.securityFormGroup.get('type').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((type) => { + this.updateValidators(type); + }); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + super.ngOnDestroy(); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void {} + + writeValue(deviceInfo: any) { + if (!deviceInfo.type) { + deviceInfo.type = BrokerSecurityType.ANONYMOUS; + } + this.securityFormGroup.reset(deviceInfo); + this.updateView(deviceInfo); + } + + validate(): ValidationErrors | null { + return this.securityFormGroup.valid ? null : { + securityForm: { valid: false } + }; + } + + updateView(value: any) { + this.propagateChange(value); + } + + private updateValidators(type) { + if (type) { + this.securityFormGroup.get('username').disable({emitEvent: false}); + this.securityFormGroup.get('password').disable({emitEvent: false}); + this.securityFormGroup.get('pathToCACert').disable({emitEvent: false}); + this.securityFormGroup.get('pathToPrivateKey').disable({emitEvent: false}); + this.securityFormGroup.get('pathToClientCert').disable({emitEvent: false}); + if (type === BrokerSecurityType.BASIC) { + this.securityFormGroup.get('username').enable({emitEvent: false}); + this.securityFormGroup.get('password').enable({emitEvent: false}); + } else if (type === BrokerSecurityType.CERTIFICATES) { + this.securityFormGroup.get('pathToCACert').enable({emitEvent: false}); + this.securityFormGroup.get('pathToPrivateKey').enable({emitEvent: false}); + this.securityFormGroup.get('pathToClientCert').enable({emitEvent: false}); + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.html new file mode 100644 index 0000000000..2a3108b8b4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.html @@ -0,0 +1,97 @@ + +
+
device.device
+
+
+
gateway.device-info.entity-field
+
gateway.device-info.source
+
+ gateway.device-info.expression +
+
+
+
+
gateway.device-info.name
+
+ + + + {{ SourceTypeTranslationsMap.get(type) | translate }} + + + +
+
+ + + + warning + +
+
+
+
+
+
+
gateway.device-info.profile-name
+
+ + + + {{ SourceTypeTranslationsMap.get(type) | translate }} + + + +
+
+ + + + warning + +
+
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.scss new file mode 100644 index 0000000000..9dbaa52831 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.scss @@ -0,0 +1,57 @@ +/** + * Copyright © 2016-2024 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. + */ +:host { + width: 100%; + height: 100%; + display: block; + + .tb-form-row { + &.bottom-same-padding { + padding-bottom: 16px; + } + + &.top-same-padding { + padding-top: 16px; + } + + .fixed-title-width { + width: 19%; + } + } + + .table-column { + width: 40%; + } + + .table-name-column { + width: 20%; + } + + .raw-name { + width: 19%; + } + + .raw-value-option { + max-width: 40%; + } + +} + +:host ::ng-deep { + .mat-mdc-form-field-icon-suffix { + display: flex; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts new file mode 100644 index 0000000000..8a40f25430 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts @@ -0,0 +1,179 @@ +/// +/// Copyright © 2016-2024 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 { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + forwardRef, + Input, + NgZone, + OnDestroy, + OnInit, + ViewContainerRef +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { MatDialog } from '@angular/material/dialog'; +import { DialogService } from '@core/services/dialog.service'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { Overlay } from '@angular/cdk/overlay'; +import { UtilsService } from '@core/services/utils.service'; +import { EntityService } from '@core/http/entity.service'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormGroup, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { + DeviceInfoType, + noLeadTrailSpacesRegex, + SourceTypes, + SourceTypeTranslationsMap +} from '@home/components/widget/lib/gateway/gateway-widget.models'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +@Component({ + selector: 'tb-device-info-table', + templateUrl: './device-info-table.component.html', + styleUrls: ['./device-info-table.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceInfoTableComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DeviceInfoTableComponent), + multi: true + } + ] +}) +export class DeviceInfoTableComponent extends PageComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy { + + SourceTypeTranslationsMap = SourceTypeTranslationsMap; + + DeviceInfoType = DeviceInfoType; + + @coerceBoolean() + @Input() + useSource = true; + + @coerceBoolean() + @Input() + required = false; + + @Input() + sourceTypes: Array = Object.values(SourceTypes); + + deviceInfoTypeValue: any; + + get deviceInfoType(): any { + return this.deviceInfoTypeValue; + } + + @Input() + set deviceInfoType(value: any) { + if (this.deviceInfoTypeValue !== value) { + this.deviceInfoTypeValue = value; + } + } + + mappingFormGroup: UntypedFormGroup; + + private destroy$ = new Subject(); + private propagateChange = (v: any) => {}; + + constructor(protected store: Store, + public translate: TranslateService, + public dialog: MatDialog, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, + private dialogService: DialogService, + private entityService: EntityService, + private utils: UtilsService, + private zone: NgZone, + private cd: ChangeDetectorRef, + private elementRef: ElementRef, + private fb: FormBuilder) { + super(store); + } + + ngOnInit() { + this.mappingFormGroup = this.fb.group({ + deviceNameExpression: ['', this.required ? + [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)] : [Validators.pattern(noLeadTrailSpacesRegex)]] + }); + + if (this.useSource) { + this.mappingFormGroup.addControl('deviceNameExpressionSource', + this.fb.control(SourceTypes.MSG, [])); + } + + if (this.deviceInfoType === DeviceInfoType.FULL) { + if (this.useSource) { + this.mappingFormGroup.addControl('deviceProfileExpressionSource', + this.fb.control(SourceTypes.MSG, [])); + } + this.mappingFormGroup.addControl('deviceProfileExpression', + this.fb.control('', this.required ? + [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)] : [Validators.pattern(noLeadTrailSpacesRegex)])); + } + + this.mappingFormGroup.valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((value) => { + this.updateView(value); + }); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void {} + + writeValue(deviceInfo: any) { + this.mappingFormGroup.patchValue(deviceInfo, {emitEvent: false}); + } + + validate(): ValidationErrors | null { + return this.mappingFormGroup.valid ? null : { + mappingForm: { valid: false } + }; + } + + updateView(value: any) { + this.propagateChange(value); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/ellipsis-chip-list.directive.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/ellipsis-chip-list.directive.ts new file mode 100644 index 0000000000..aa4738d707 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/ellipsis-chip-list.directive.ts @@ -0,0 +1,128 @@ +/// +/// Copyright © 2016-2024 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 { + Directive, + ElementRef, + Inject, + Input, + OnDestroy, + Renderer2 +} from '@angular/core'; +import { isEqual } from '@core/utils'; +import { TranslateService } from '@ngx-translate/core'; +import { WINDOW } from '@core/services/window.service'; +import { fromEvent, Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +@Directive({ + // eslint-disable-next-line @angular-eslint/directive-selector + selector: '[tb-ellipsis-chip-list]' +}) +export class EllipsisChipListDirective implements OnDestroy { + + chipsValue: string[]; + + private destroy$ = new Subject(); + + @Input('tb-ellipsis-chip-list') + set chips(value: string[]) { + if (!isEqual(this.chipsValue, value)) { + this.chipsValue = value; + setTimeout(() => { + this.adjustChips(); + }, 0); + } + } + + constructor(private el: ElementRef, + private renderer: Renderer2, + private translate: TranslateService, + @Inject(WINDOW) private window: Window) { + this.renderer.setStyle(this.el.nativeElement, 'max-height', '48px'); + this.renderer.setStyle(this.el.nativeElement, 'overflow', 'auto'); + fromEvent(window, 'resize').pipe( + takeUntil(this.destroy$) + ).subscribe(() => { + this.adjustChips(); + }); + } + + private adjustChips(): void { + const chipListElement = this.el.nativeElement; + const ellipsisChip = this.el.nativeElement.querySelector('.ellipsis-chip'); + const margin = parseFloat(this.window.getComputedStyle(ellipsisChip).marginLeft) || 0; + const chipNodes = chipListElement.querySelectorAll('mat-chip:not(.ellipsis-chip)'); + + if (this.chipsValue.length > 1) { + const ellipsisText = this.el.nativeElement.querySelector('.ellipsis-text'); + this.renderer.setStyle(ellipsisChip, 'display', 'inline-flex'); + ellipsisText.innerHTML = this.translate.instant('gateway.ellipsis-chips-text', + {count: (this.chipsValue.length)}); + + const availableWidth = chipListElement.offsetWidth - (ellipsisChip.offsetWidth + margin); + let usedWidth = 0; + let visibleChipsCount = 0; + + chipNodes.forEach((chip) => { + this.renderer.setStyle(chip, 'display', 'inline-flex'); + const textLabelContainer = chip.querySelector('.mdc-evolution-chip__text-label'); + + this.applyMaxChipTextWidth(textLabelContainer, (availableWidth / 3)); + + if ((usedWidth + (chip.offsetWidth + margin) <= availableWidth) && (visibleChipsCount < this.chipsValue.length)) { + visibleChipsCount++; + usedWidth += chip.offsetWidth + margin; + } else { + this.renderer.setStyle(chip, 'display', 'none'); + } + }); + + ellipsisText.innerHTML = this.translate.instant('gateway.ellipsis-chips-text', + {count: (this.chipsValue.length - visibleChipsCount)}); + + if (visibleChipsCount === this.chipsValue?.length) { + this.renderer.setStyle(ellipsisChip, 'display', 'none'); + } + } else if (this.chipsValue.length === 1) { + const chipLabelContainer = chipNodes[0].querySelector('.mdc-evolution-chip__action'); + const textLabelContainer = chipLabelContainer.querySelector('.mdc-evolution-chip__text-label'); + const leftPadding = parseFloat(this.window.getComputedStyle(chipLabelContainer).paddingLeft) || 0; + const rightPadding = parseFloat(this.window.getComputedStyle(chipLabelContainer).paddingRight) || 0; + const computedTextWidth = chipListElement.offsetWidth - margin - + (leftPadding + rightPadding); + + this.renderer.setStyle(ellipsisChip, 'display', 'none'); + this.renderer.setStyle(chipNodes[0], 'display', 'inline-flex'); + + this.applyMaxChipTextWidth(textLabelContainer, computedTextWidth); + } else { + this.renderer.setStyle(ellipsisChip, 'display', 'none'); + } + } + + private applyMaxChipTextWidth(element: HTMLElement, widthLimit: number): void { + this.renderer.setStyle(element, 'max-width', widthLimit + 'px'); + this.renderer.setStyle(element, 'overflow', 'hidden'); + this.renderer.setStyle(element, 'text-overflow', 'ellipsis'); + this.renderer.setStyle(element, 'white-space', 'nowrap'); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.html new file mode 100644 index 0000000000..4c3a6434fd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.html @@ -0,0 +1,195 @@ + +
+
+
{{ panelTitle | translate }}{{' (' + keysListFormArray.controls.length + ')'}}
+
+
+
+ + + + +
{{ keyControl.get('key').value }}
+ {{ '-' }} +
{{ valueTitle(keyControl.get('value').value) }}
+
+
+ +
+
+
gateway.platform-side
+
+
+ gateway.key +
+
+ + + + warning + +
+
+
+
+
+
+
+
gateway.connector-side
+
+
gateway.type
+ + + +
+ + + + {{ (rawData ? 'gateway.raw' : valueTypes.get(keyControl.get('type').value)?.name) | translate}} + +
+
+ + + + + {{ valueTypes.get(valueType).name | translate }} + + + + + {{ 'gateway.raw' | translate }} + + +
+
+
+
+
+ gateway.value +
+ + + + warning + +
+
+
+
+
+
+ +
+
gateway.key
+
+ + + + warning + + +
+
+
+
gateway.value
+ + + + warning + + +
+
+
+
+
+
+ +
+
+
+ +
+
+ +
+ {{ noKeysText }} +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.scss new file mode 100644 index 0000000000..24498863c9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.scss @@ -0,0 +1,60 @@ +/** + * Copyright © 2016-2024 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. + */ + +:host { + .tb-mapping-keys-panel { + width: 77vw; + max-width: 700px; + + .title-container { + max-width: 11vw; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap + } + + .key-panel { + height: 500px; + overflow: auto; + } + + tb-value-input { + width: 100%; + } + + .tb-form-panel { + .mat-mdc-icon-button { + width: 56px; + height: 56px; + padding: 16px; + color: rgba(0, 0, 0, 0.54); + } + } + + .see-example { + width: 32px; + height: 32px; + margin: 4px; + } + } +} + +:host ::ng-deep { + .mat-mdc-form-field-icon-suffix { + display: flex; + } +} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.ts new file mode 100644 index 0000000000..40ee2728bd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.ts @@ -0,0 +1,173 @@ +/// +/// Copyright © 2016-2024 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 { + Component, + EventEmitter, + Input, + OnInit, + Output +} from '@angular/core'; +import { + AbstractControl, + UntypedFormArray, + UntypedFormBuilder, + Validators +} from '@angular/forms'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { PageComponent } from '@shared/components/page.component'; +import { isDefinedAndNotNull } from '@core/utils'; +import { + MappingDataKey, + MappingKeysType, + MappingValueType, + mappingValueTypesMap, + noLeadTrailSpacesRegex +} from '@home/components/widget/lib/gateway/gateway-widget.models'; + +@Component({ + selector: 'tb-mapping-data-keys-panel', + templateUrl: './mapping-data-keys-panel.component.html', + styleUrls: ['./mapping-data-keys-panel.component.scss'], + providers: [] +}) +export class MappingDataKeysPanelComponent extends PageComponent implements OnInit { + + @Input() + panelTitle: string; + + @Input() + addKeyTitle: string; + + @Input() + deleteKeyTitle: string; + + @Input() + noKeysText: string; + + @Input() + keys: Array | {[key: string]: any}; + + @Input() + keysType: string; + + @Input() + @coerceBoolean() + rawData = false; + + @Input() + popover: TbPopoverComponent; + + @Output() + keysDataApplied = new EventEmitter | {[key: string]: any}>(); + + valueTypeKeys = Object.values(MappingValueType); + + MappingKeysType = MappingKeysType; + + valueTypeEnum = MappingValueType; + + valueTypes = mappingValueTypesMap; + + dataKeyType: DataKeyType; + + keysListFormArray: UntypedFormArray; + + errorText = ''; + + constructor(private fb: UntypedFormBuilder, + protected store: Store) { + super(store); + } + + ngOnInit() { + this.keysListFormArray = this.prepareKeysFormArray(this.keys) + } + + trackByKey(index: number, keyControl: AbstractControl): any { + return keyControl; + } + + addKey(): void { + const dataKeyFormGroup = this.fb.group({ + key: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + value: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]] + }); + if (this.keysType !== MappingKeysType.CUSTOM) { + dataKeyFormGroup.addControl('type', this.fb.control(this.rawData ? 'raw' : MappingValueType.STRING)); + } + this.keysListFormArray.push(dataKeyFormGroup); + } + + deleteKey($event: Event, index: number): void { + if ($event) { + $event.stopPropagation(); + } + this.keysListFormArray.removeAt(index); + this.keysListFormArray.markAsDirty(); + } + + cancel() { + this.popover?.hide(); + } + + applyKeysData() { + let keys = this.keysListFormArray.value; + if (this.keysType === MappingKeysType.CUSTOM) { + keys = {}; + for (let key of this.keysListFormArray.value) { + keys[key.key] = key.value; + } + } + this.keysDataApplied.emit(keys); + } + + private prepareKeysFormArray(keys: Array | {[key: string]: any}): UntypedFormArray { + const keysControlGroups: Array = []; + if (keys) { + if (this.keysType === MappingKeysType.CUSTOM) { + keys = Object.keys(keys).map(key => { + return {key, value: keys[key], type: ''}; + }); + } + keys.forEach((keyData) => { + const { key, value, type } = keyData; + const dataKeyFormGroup = this.fb.group({ + key: [key, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + value: [value, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + type: [type, []] + }); + keysControlGroups.push(dataKeyFormGroup); + }); + } + return this.fb.array(keysControlGroups); + } + + valueTitle(value: any): string { + if (isDefinedAndNotNull(value)) { + if (typeof value === 'object') { + return JSON.stringify(value); + } + return value; + } + return ''; + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.html new file mode 100644 index 0000000000..e5c703d8d0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.html @@ -0,0 +1,123 @@ + +
+
+ +
+
+ {{mappingTypeTranslationsMap.get(mappingType) | translate}} +
+ + + +
+
+ +
+ + +   + + + +
+
+
+
+ + + {{ column.title | translate }} + + + {{ mapping[column.def] }} + + + + + + +
+ + +
+
+ + + + + +
+
+
+ + +
+
+ +
+
+ + widget.no-data-found + +
+ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.scss new file mode 100644 index 0000000000..707b5fa056 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.scss @@ -0,0 +1,101 @@ +/** + * Copyright © 2016-2024 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 '../scss/constants'; + +:host { + width: 100%; + height: 100%; + display: block; + .tb-mapping-table { + .tb-mapping-table-content { + width: 100%; + height: 100%; + background: #fff; + overflow: hidden; + + &.tb-outlined-border { + box-shadow: 0 0 0 0 rgb(0 0 0 / 20%), 0 0 0 0 rgb(0 0 0 / 14%), 0 0 0 0 rgb(0 0 0 / 12%); + border: solid 1px #e0e0e0; + border-radius: 4px; + } + + .mat-toolbar-tools{ + min-height: auto; + } + + .title-container{ + overflow: hidden; + } + + .tb-mapping-table-title { + padding-right: 20px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .table-container { + overflow: auto; + .mat-mdc-table { + table-layout: fixed; + min-width: 450px; + + .table-value-column { + padding: 0 12px; + width: 23%; + + &.request-column { + width: 38%; + } + } + } + } + + .ellipsis { + overflow: hidden; + text-overflow: ellipsis; + } + } + } + + .no-data-found { + height: calc(100% - 120px); + } + + @media #{$mat-xs} { + .mat-toolbar { + height: auto; + min-height: 100px; + + .tb-mapping-table-title{ + padding-bottom: 5px; + width: 100%; + } + } + } +} + +:host ::ng-deep { + mat-cell.tb-value-cell { + cursor: pointer; + .mat-icon { + height: 24px; + width: 24px; + font-size: 24px; + color: #757575 + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.ts new file mode 100644 index 0000000000..4ef9695c65 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.ts @@ -0,0 +1,339 @@ +/// +/// Copyright © 2016-2024 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 { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + forwardRef, + Input, + NgZone, + OnDestroy, + OnInit, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { PageLink } from '@shared/models/page/page-link'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { MatDialog } from '@angular/material/dialog'; +import { DialogService } from '@core/services/dialog.service'; +import { BehaviorSubject, Observable, Subject } from 'rxjs'; +import { debounceTime, distinctUntilChanged, map, takeUntil } from 'rxjs/operators'; +import { Overlay } from '@angular/cdk/overlay'; +import { UtilsService } from '@core/services/utils.service'; +import { EntityService } from '@core/http/entity.service'; +import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR, UntypedFormArray } from '@angular/forms'; +import { + ConvertorTypeTranslationsMap, + MappingInfo, + MappingType, + MappingTypeTranslationsMap, + RequestType, + RequestTypesTranslationsMap +} from '@home/components/widget/lib/gateway/gateway-widget.models'; +import { CollectionViewer, DataSource } from '@angular/cdk/collections'; +import { MappingDialogComponent } from '@home/components/widget/lib/gateway/dialog/mapping-dialog.component'; +import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils'; + +@Component({ + selector: 'tb-mapping-table', + templateUrl: './mapping-table.component.html', + styleUrls: ['./mapping-table.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MappingTableComponent), + multi: true + } + ] +}) +export class MappingTableComponent extends PageComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy { + + mappingTypeTranslationsMap = MappingTypeTranslationsMap; + mappingTypeEnum = MappingType; + displayedColumns = []; + mappingColumns = []; + textSearchMode = false; + dataSource: MappingDatasource; + hidePageSize = false; + + activeValue = false; + dirtyValue = false; + + viewsInited = false; + + mappingTypeValue: MappingType; + + get mappingType(): MappingType { + return this.mappingTypeValue; + } + + @Input() + set mappingType(value: MappingType) { + if (this.mappingTypeValue !== value) { + this.mappingTypeValue = value; + } + } + + @ViewChild('searchInput') searchInputField: ElementRef; + + mappingFormGroup: UntypedFormArray; + textSearch = this.fb.control('', {nonNullable: true}); + + private destroy$ = new Subject(); + private propagateChange = (v: any) => {}; + + constructor(protected store: Store, + public translate: TranslateService, + public dialog: MatDialog, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, + private dialogService: DialogService, + private entityService: EntityService, + private utils: UtilsService, + private zone: NgZone, + private cd: ChangeDetectorRef, + private elementRef: ElementRef, + private fb: FormBuilder) { + super(store); + this.mappingFormGroup = this.fb.array([]); + this.dirtyValue = !this.activeValue; + this.dataSource = new MappingDatasource(); + } + + ngOnInit() { + if (this.mappingType === MappingType.DATA) { + this.mappingColumns.push( + {def: 'topicFilter', title: 'gateway.topic-filter'}, + {def: 'QoS', title: 'gateway.mqtt-qos'}, + {def: 'converter', title: 'gateway.payload-type'} + ) + } else { + this.mappingColumns.push( + {def: 'type', title: 'gateway.type'}, + {def: 'details', title: 'gateway.details'} + ); + } + this.displayedColumns.push(...this.mappingColumns.map(column => column.def), 'actions'); + this.mappingFormGroup.valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((value) => { + this.updateTableData(value); + this.updateView(value); + }); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + ngAfterViewInit() { + this.textSearch.valueChanges.pipe( + debounceTime(150), + distinctUntilChanged((prev, current) => (prev ?? '') === current.trim()), + takeUntil(this.destroy$) + ).subscribe((text) => { + const searchText = text.trim(); + this.updateTableData(this.mappingFormGroup.value, searchText.trim()) + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void {} + + writeValue(config: any) { + if (isUndefinedOrNull(config)) { + config = this.mappingType === MappingType.REQUESTS ? {} : []; + } + let mappingConfigs = config; + if (this.mappingType === MappingType.REQUESTS) { + mappingConfigs = []; + + Object.keys(config).forEach((configKey) => { + for (let mapping of config[configKey]) { + mappingConfigs.push({ + requestType: configKey, + requestValue: mapping + }); + } + }); + } + this.mappingFormGroup.clear({emitEvent: false}); + for (let mapping of mappingConfigs) { + this.mappingFormGroup.push(this.fb.group(mapping), {emitEvent: false}); + } + this.updateTableData(mappingConfigs); + } + + updateView(mappingConfigs: Array<{[key: string]: any}>) { + let config; + if (this.mappingType === MappingType.REQUESTS) { + config = {}; + for (let mappingConfig of mappingConfigs) { + if (config[mappingConfig.requestType]) { + config[mappingConfig.requestType].push(mappingConfig.requestValue); + } else { + config[mappingConfig.requestType] = [mappingConfig.requestValue]; + } + } + } else { + config = mappingConfigs; + } + + this.propagateChange(config); + } + + enterFilterMode() { + this.textSearchMode = true; + setTimeout(() => { + this.searchInputField.nativeElement.focus(); + this.searchInputField.nativeElement.setSelectionRange(0, 0); + }, 10); + } + + exitFilterMode() { + this.updateTableData(this.mappingFormGroup.value); + this.textSearchMode = false; + this.textSearch.reset(); + } + + manageMapping($event: Event, index?: number) { + if ($event) { + $event.stopPropagation(); + } + const value = isDefinedAndNotNull(index) ? this.mappingFormGroup.at(index).value : {}; + this.dialog.open(MappingDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + mappingType: this.mappingType, + value, + buttonTitle: isUndefinedOrNull(index) ? 'action.add' : 'action.apply' + } + }).afterClosed().subscribe( + (res) => { + if (res) { + if (isDefinedAndNotNull(index)) { + this.mappingFormGroup.at(index).patchValue(res); + } else { + this.mappingFormGroup.push(this.fb.group(res)); + } + } + } + ); + } + + updateTableData(value: Array<{[key: string]: any}>, textSearch?: string): void { + let tableValue = value; + if (this.mappingType === MappingType.DATA) { + tableValue = tableValue.map((value) => { + return { + topicFilter: value.topicFilter, + QoS: value.subscriptionQos, + converter: this.translate.instant(ConvertorTypeTranslationsMap.get(value.converter.type)) + }; + }); + } else { + tableValue = tableValue.map((value) => { + let details; + if (value.requestType === RequestType.ATTRIBUTE_UPDATE) { + details = value.requestValue.attributeFilter; + } else if (value.requestType === RequestType.SERVER_SIDE_RPC) { + details = value.requestValue.methodFilter; + } else { + details = value.requestValue.topicFilter; + } + return { + type: this.translate.instant(RequestTypesTranslationsMap.get(value.requestType)), + details + }; + }); + } + if (textSearch) { + tableValue = tableValue.filter(value => + Object.values(value).some(val => + val.toString().toLowerCase().includes(textSearch.toLowerCase()) + ) + ); + } + this.dataSource.loadMappings(tableValue); + } + + deleteMapping($event: Event, index: number) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('gateway.delete-mapping-title'), + '', + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((result) => { + if (result) { + this.mappingFormGroup.removeAt(index); + } + }); + } + +} + +export class MappingDatasource implements DataSource<{[key: string]: any}> { + + private mappingSubject = new BehaviorSubject>([]); + + private allMappings: Observable>; + + constructor() {} + + connect(collectionViewer: CollectionViewer): Observable> { + return this.mappingSubject.asObservable(); + } + + disconnect(collectionViewer: CollectionViewer): void { + this.mappingSubject.complete(); + } + + loadMappings(mappings: Array<{[key: string]: any}>, pageLink?: PageLink, reload: boolean = false): void { + if (reload) { + this.allMappings = null; + } + this.mappingSubject.next(mappings); + } + + isEmpty(): Observable { + return this.mappingSubject.pipe( + map((mappings) => !mappings.length) + ); + } + + total(): Observable { + return this.mappingSubject.pipe( + map((mappings) => mappings.length) + ); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.html new file mode 100644 index 0000000000..ec1fa9a87f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.html @@ -0,0 +1,100 @@ + +
+ +

{{ "gateway.add-connector" | translate}}

+ +
+ +
+
+
+
+
gateway.type
+
+ + + + {{ type.value }} + + + +
+
+
+
gateway.name
+
+ + + + warning + + +
+
+
+
gateway.remote-logging-level
+
+ + + {{ logLevel }} + + +
+
+
+ + + {{ 'gateway.fill-connector-defaults' | translate }} + + +
+
+ + + {{ 'gateway.send-change-data' | translate }} + + +
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.scss new file mode 100644 index 0000000000..56e4bc229c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.scss @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2024 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. + */ + +:host { + .add-connector { + min-width: 400px; + width: 500px; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts new file mode 100644 index 0000000000..78a22cd267 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts @@ -0,0 +1,122 @@ +/// +/// Copyright © 2016-2024 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 { Component, Inject, OnDestroy } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, UntypedFormControl, UntypedFormGroup, ValidatorFn, Validators } from '@angular/forms'; +import { BaseData, HasId } from '@shared/models/base-data'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; +import { + AddConnectorConfigData, + ConnectorType, + CreatedConnectorConfigData, + GatewayConnectorDefaultTypesTranslatesMap, + GatewayLogLevel, + getDefaultConfig, + noLeadTrailSpacesRegex +} from '@home/components/widget/lib/gateway/gateway-widget.models'; +import { Subject } from 'rxjs'; +import { ResourcesService } from '@core/services/resources.service'; + +@Component({ + selector: 'tb-add-connector-dialog', + templateUrl: './add-connector-dialog.component.html', + styleUrls: ['./add-connector-dialog.component.scss'], + providers: [], +}) +export class AddConnectorDialogComponent extends DialogComponent> implements OnDestroy { + + connectorForm: UntypedFormGroup; + + connectorType = ConnectorType; + + gatewayConnectorDefaultTypesTranslatesMap = GatewayConnectorDefaultTypesTranslatesMap; + gatewayLogLevel = Object.values(GatewayLogLevel); + + submitted = false; + + private destroy$ = new Subject(); + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AddConnectorConfigData, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + private resourcesService: ResourcesService) { + super(store, router, dialogRef); + this.connectorForm = this.fb.group({ + type: [ConnectorType.MQTT, []], + name: ['', [Validators.required, this.uniqNameRequired(), Validators.pattern(noLeadTrailSpacesRegex)]], + logLevel: [GatewayLogLevel.INFO, []], + useDefaults: [true, []], + sendDataOnlyOnChange: [false, []], + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + super.ngOnDestroy(); + } + + helpLinkId(): string { + return 'https://thingsboard.io/docs/iot-gateway/configuration/'; + } + + cancel(): void { + this.dialogRef.close(null); + } + + add(): void { + this.submitted = true; + const value = this.connectorForm.getRawValue(); + if (value.useDefaults) { + getDefaultConfig(this.resourcesService, value.type).subscribe((defaultConfig) => { + value.configurationJson = defaultConfig; + if (this.connectorForm.valid) { + this.dialogRef.close(value); + } + }); + } + if (this.connectorForm.valid) { + this.dialogRef.close(value); + } + } + + private uniqNameRequired(): ValidatorFn { + return (c: UntypedFormControl) => { + const newName = c.value.trim().toLowerCase(); + const found = this.data.dataSourceData.find((connectorAttr) => { + const connectorData = connectorAttr.value; + return connectorData.name.toLowerCase() === newName; + }); + if (found) { + if (c.hasError('required')) { + return c.getError('required'); + } + return { + duplicateName: { + valid: false + } + }; + } + return null; + }; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html new file mode 100644 index 0000000000..78ceaa566f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html @@ -0,0 +1,634 @@ + +
+ +

{{ MappingTypeTranslationsMap.get(this.data?.mappingType) | translate}}

+ +
+ +
+
+
+
+ {{ MappingHintTranslationsMap.get(this.data?.mappingType) | translate }} +
+ + +
+
gateway.topic-filter
+
+ + + + warning + +
+
+
+
+
+
+
+ gateway.mqtt-qos +
+
+ + + + {{ QualityTranslationsMap.get(type) | translate }} + + + +
+
+ +
+
gateway.payload-type
+ + + {{ ConvertorTypeTranslationsMap.get(type) | translate }} + + +
+
+
gateway.data-conversion
+
+ {{ DataConversionTranslationsMap.get(converterType) | translate }} +
+ + + + + + + + + +
+
+
gateway.attributes
+
+ + + {{ attribute }} + + + + + + +
+
+
+
gateway.timeseries
+
+ + + {{ telemetry }} + + + + + + +
+
+
+
+
+
+ gateway.extension +
+
+ + + + warning + + +
+
+
+
gateway.extension-configuration
+
{{ 'gateway.extension-configuration-hint' | translate }}
+
+
gateway.keys
+
+ + + {{ telemetry }} + + + + + + +
+
+
+
+
+
+
+
+ +
+
gateway.request-type
+
+ + + + {{ RequestTypesTranslationsMap.get(type) | translate }} + + + +
+
+ + +
+
gateway.topic-filter
+
+ + + + warning + +
+
+
+
+
+ + + + + + + + + +
+
gateway.from-device-request-settings
+
+ gateway.from-device-request-settings-hint +
+
+
+
gateway.device-info.device-name-expression
+
+
+ + + + {{ SourceTypeTranslationsMap.get(type) | translate }} + + + + + + + warning + +
+
+
+
+
+
+
gateway.attribute-name-expression
+
+ + + + {{ SourceTypeTranslationsMap.get(type) | translate }} + + + + + + + warning + +
+
+
+
+
+
+
+
gateway.to-device-response-settings
+
+ gateway.to-device-response-settings-hint +
+
+
gateway.response-value-expression
+
+ + + + warning + +
+
+
+
+
+
+
gateway.response-topic-expression
+
+ + + + warning + +
+
+
+
+
+
+ + + {{ 'gateway.retain' | translate }} + + +
+
+
+ +
+
+ gateway.device-name-filter +
+
+ + + + warning + + +
+
+
+
+ gateway.attribute-filter +
+
+ + + + warning + + +
+
+
+
gateway.response-value-expression
+
+ + + + warning + +
+
+
+
+
+
+
gateway.response-topic-expression
+
+ + + + warning + +
+
+
+
+
+
+ + + {{ 'gateway.retain' | translate }} + + +
+
+ +
+ + + {{ 'gateway.with-response' | translate }} + + + {{ 'gateway.without-response' | translate }} + + +
+
+
+ gateway.device-name-filter +
+
+ + + + warning + + +
+
+
+
+ gateway.method-filter +
+
+ + + + warning + + +
+
+
+
gateway.request-topic-expression
+
+ + + + warning + +
+
+
+
+
+
+
gateway.value-expression
+
+ + + + warning + +
+
+
+
+
+ +
+
gateway.response-topic-expression
+
+ + + + warning + +
+
+
+
+
+
+
+ gateway.response-topic-Qos +
+ + + + {{ QualityTranslationsMap.get(type) | translate }} + + + +
+
+
gateway.response-timeout
+
+ + + + warning + + +
+
+
+
+
+
+
+
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss new file mode 100644 index 0000000000..db98bc07cb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss @@ -0,0 +1,76 @@ +/** + * Copyright © 2016-2024 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. + */ + +:host { + display: grid; + height: 100%; + + .key-mapping { + max-width: 900px; + display: flex; + flex-direction: column; + + .mat-toolbar { + min-height: 64px; + } + + tb-toggle-select { + padding: 4px 0; + } + } + + .mat-mdc-dialog-content { + max-height: 670px; + height: 670px; + } + + .ellipsis-chips-container { + max-width: 70%; + } +} + +:host ::ng-deep { + .key-mapping { + .mat-mdc-chip-listbox { + .mdc-evolution-chip-set__chips { + justify-content: flex-end; + align-items: center; + } + } + } + .tb-form-row { + .fixed-title-width { + min-width: 40px; + width: 35%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .mat-mdc-form-field { + width: 0; + } + } + + .see-example { + width: 32px; + height: 32px; + margin: 4px; + } + + .mat-mdc-form-field-icon-suffix { + display: flex; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts new file mode 100644 index 0000000000..d01050b52e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts @@ -0,0 +1,360 @@ +/// +/// Copyright © 2016-2024 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 { Component, Inject, OnDestroy, Renderer2, ViewContainerRef } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { BaseData, HasId } from '@shared/models/base-data'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; +import { + ConvertorType, + ConvertorTypeTranslationsMap, + DataConversionTranslationsMap, + DeviceInfoType, + MappingHintTranslationsMap, + MappingInfo, + MappingKeysAddKeyTranslationsMap, + MappingKeysDeleteKeyTranslationsMap, + MappingKeysNoKeysTextTranslationsMap, + MappingKeysPanelTitleTranslationsMap, + MappingKeysType, + MappingType, + MappingTypeTranslationsMap, + noLeadTrailSpacesRegex, + QualityTypes, + QualityTypeTranslationsMap, + RequestType, + RequestTypesTranslationsMap, + ServerSideRPCType, + SourceTypes, + SourceTypeTranslationsMap +} from '@home/components/widget/lib/gateway/gateway-widget.models'; +import { Subject } from 'rxjs'; +import { startWith, takeUntil } from 'rxjs/operators'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { MappingDataKeysPanelComponent } from '@home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component'; +import { TranslateService } from '@ngx-translate/core'; + +@Component({ + selector: 'tb-mapping-dialog', + templateUrl: './mapping-dialog.component.html', + styleUrls: ['./mapping-dialog.component.scss'], + providers: [], +}) +export class MappingDialogComponent extends DialogComponent> implements OnDestroy { + + mappingForm: UntypedFormGroup; + + MappingType = MappingType; + + qualityTypes = QualityTypes; + QualityTranslationsMap = QualityTypeTranslationsMap; + + convertorTypes = Object.values(ConvertorType); + ConvertorTypeEnum = ConvertorType; + ConvertorTypeTranslationsMap = ConvertorTypeTranslationsMap; + + sourceTypes = Object.values(SourceTypes); + sourceTypesEnum = SourceTypes; + SourceTypeTranslationsMap = SourceTypeTranslationsMap; + + requestTypes = Object.values(RequestType); + RequestTypeEnum = RequestType; + RequestTypesTranslationsMap = RequestTypesTranslationsMap; + + DeviceInfoType = DeviceInfoType; + + ServerSideRPCType = ServerSideRPCType; + + MappingKeysType = MappingKeysType; + + MappingHintTranslationsMap = MappingHintTranslationsMap; + + MappingTypeTranslationsMap = MappingTypeTranslationsMap; + + DataConversionTranslationsMap = DataConversionTranslationsMap; + + hiddenAttributesCount = 0; + + keysPopupClosed = true; + + submitted = false; + + private destroy$ = new Subject(); + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: MappingInfo, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef, + private translate: TranslateService) { + super(store, router, dialogRef); + + this.createMappingForm(); + } + + get converterAttributes(): Array { + if (this.converterType) { + return this.mappingForm.get('converter').get(this.converterType).value.attributes.map(value => value.key); + } + } + + get converterTelemetry(): Array { + if (this.converterType) { + return this.mappingForm.get('converter').get(this.converterType).value.timeseries.map(value => value.key); + } + } + + get converterType(): ConvertorType { + return this.mappingForm.get('converter').get('type').value; + } + + get customKeys(): Array { + return Object.keys(this.mappingForm.get('converter').get('custom').value.extensionConfig); + } + + get requestMappingType(): RequestType { + return this.mappingForm.get('requestType').value; + } + + get responseTimeoutErrorTooltip(): string { + const control = this.mappingForm.get('requestValue.serverSideRpc.responseTimeout'); + if (control.hasError('required')) { + return this.translate.instant('gateway.response-timeout-required'); + } else if (control.hasError('min')) { + return this.translate.instant('gateway.response-timeout-limits-error', {min: 1}); + } + return ''; + } + + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + super.ngOnDestroy(); + } + + private createMappingForm(): void { + this.mappingForm = this.fb.group({}); + if (this.data.mappingType === MappingType.DATA) { + this.mappingForm.addControl('topicFilter', + this.fb.control('', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)])); + this.mappingForm.addControl('subscriptionQos', this.fb.control(0)); + this.mappingForm.addControl('converter', this.fb.group({ + type: [ConvertorType.JSON, []], + json: this.fb.group({ + deviceInfo: [{}, []], + attributes: [[], []], + timeseries: [[], []] + }), + bytes: this.fb.group({ + deviceInfo: [{}, []], + attributes: [[], []], + timeseries: [[], []] + }), + custom: this.fb.group({ + extension: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + extensionConfig: [{}, []] + }), + })); + this.mappingForm.patchValue(this.prepareFormValueData()); + this.mappingForm.get('converter.type').valueChanges.pipe( + startWith(this.mappingForm.get('converter.type').value), + takeUntil(this.destroy$) + ).subscribe((value) => { + const converterGroup = this.mappingForm.get('converter'); + converterGroup.get('json').disable({emitEvent: false}); + converterGroup.get('bytes').disable({emitEvent: false}); + converterGroup.get('custom').disable({emitEvent: false}); + converterGroup.get(value).enable({emitEvent: false}); + }) + } + + if (this.data.mappingType === MappingType.REQUESTS) { + this.mappingForm.addControl('requestType', this.fb.control(RequestType.CONNECT_REQUEST, [])); + this.mappingForm.addControl('requestValue', this.fb.group({ + connectRequests: this.fb.group({ + topicFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + deviceInfo: [{}, []] + }), + disconnectRequests: this.fb.group({ + topicFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + deviceInfo: [{}, []] + }), + attributeRequests: this.fb.group({ + topicFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + deviceInfo: this.fb.group({ + deviceNameExpressionSource: [SourceTypes.MSG, []], + deviceNameExpression: ['', [Validators.required]], + }), + attributeNameExpressionSource: [SourceTypes.MSG, []], + attributeNameExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + topicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + valueExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + retain: [false, []] + }), + attributeUpdates: this.fb.group({ + deviceNameFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + attributeFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + topicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + valueExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + retain: [true, []] + }), + serverSideRpc: this.fb.group({ + type: [ServerSideRPCType.TWO_WAY, []], + deviceNameFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + methodFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + requestTopicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + responseTopicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + valueExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + responseTopicQoS: [0, []], + responseTimeout: [10000, [Validators.required, Validators.min(1)]], + }) + })); + this.mappingForm.get('requestType').valueChanges.pipe( + startWith(this.mappingForm.get('requestType').value), + takeUntil(this.destroy$) + ).subscribe((value) => { + const requestValueGroup = this.mappingForm.get('requestValue'); + requestValueGroup.get('connectRequests').disable({emitEvent: false}); + requestValueGroup.get('disconnectRequests').disable({emitEvent: false}); + requestValueGroup.get('attributeRequests').disable({emitEvent: false}); + requestValueGroup.get('attributeUpdates').disable({emitEvent: false}); + requestValueGroup.get('serverSideRpc').disable({emitEvent: false}); + requestValueGroup.get(value).enable(); + }); + this.mappingForm.get('requestValue.serverSideRpc.type').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((value) => { + const requestValueGroup = this.mappingForm.get('requestValue.serverSideRpc'); + if (value === ServerSideRPCType.ONE_WAY) { + requestValueGroup.get('responseTopicExpression').disable({emitEvent: false}); + requestValueGroup.get('responseTopicQoS').disable({emitEvent: false}); + requestValueGroup.get('responseTimeout').disable({emitEvent: false}); + } else { + requestValueGroup.get('responseTopicExpression').enable({emitEvent: false}); + requestValueGroup.get('responseTopicQoS').enable({emitEvent: false}); + requestValueGroup.get('responseTimeout').enable({emitEvent: false}); + } + }); + this.mappingForm.patchValue(this.prepareFormValueData()); + } + } + + helpLinkId(): string { + return 'https://thingsboard.io/docs/iot-gateway/config/mqtt/#section-mapping'; + } + + cancel(): void { + if (this.keysPopupClosed) { + this.dialogRef.close(null); + } + } + + add(): void { + this.submitted = true; + if (this.mappingForm.valid) { + this.dialogRef.close(this.prepareMappingData()); + } + } + + manageKeys($event: Event, matButton: MatButton, keysType: MappingKeysType) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const keysControl = this.mappingForm.get('converter').get(this.converterType).get(keysType); + const ctx: any = { + keys: keysControl.value, + keysType: keysType, + rawData: this.mappingForm.get('converter.type').value === ConvertorType.BYTES, + panelTitle: MappingKeysPanelTitleTranslationsMap.get(keysType), + addKeyTitle: MappingKeysAddKeyTranslationsMap.get(keysType), + deleteKeyTitle: MappingKeysDeleteKeyTranslationsMap.get(keysType), + noKeysText: MappingKeysNoKeysTextTranslationsMap.get(keysType) + }; + this.keysPopupClosed = false; + const dataKeysPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, MappingDataKeysPanelComponent, 'leftBottom', false, null, + ctx, + {}, + {}, {}, true); + dataKeysPanelPopover.tbComponentRef.instance.popover = dataKeysPanelPopover; + dataKeysPanelPopover.tbComponentRef.instance.keysDataApplied.subscribe((keysData) => { + dataKeysPanelPopover.hide(); + keysControl.patchValue(keysData); + keysControl.markAsDirty(); + }); + dataKeysPanelPopover.tbHideStart.subscribe(() => { + this.keysPopupClosed = true; + }); + } + } + + private prepareMappingData(): {[key: string]: any} { + const formValue = this.mappingForm.value; + if (this.data.mappingType === MappingType.DATA) { + const { converter, topicFilter, subscriptionQos } = formValue; + return { + topicFilter, + subscriptionQos, + converter: { + type: converter.type, + ...converter[converter.type] + } + }; + } else { + return { + requestType: formValue.requestType, + requestValue: formValue.requestValue[formValue.requestType] + }; + } + } + + private prepareFormValueData(): {[key: string]: any} { + if (this.data.value && Object.keys(this.data.value).length) { + if (this.data.mappingType === MappingType.DATA) { + const { converter, topicFilter, subscriptionQos } = this.data.value; + return { + topicFilter, + subscriptionQos, + converter: { + type: converter.type, + [converter.type]: { ...converter } + } + }; + } else { + return { + requestType: this.data.value.requestType, + requestValue: { + [this.data.value.requestType]: this.data.value.requestValue + } + }; + } + } + return this.data.value; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html index 153b6b0e14..8aeaeef2ed 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html @@ -80,6 +80,15 @@ {{ 'security.access-token-required' | translate }} + + info_outlined @@ -92,6 +101,15 @@ {{ 'security.clientId-required' | translate }} + + info_outlined @@ -102,6 +120,15 @@ {{ 'security.username-required' | translate }} + + info_outlined @@ -109,8 +136,17 @@ - security.password + gateway.password + + info_outlined diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.scss index 46292aa36e..f7562c748e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.scss @@ -97,5 +97,9 @@ color: #9E9E9E; } } + + .mat-mdc-form-field-icon-suffix { + display: flex; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html index 2b9329dc21..3a7f5403b9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html @@ -22,13 +22,21 @@
+
+ +
@@ -74,6 +82,7 @@ @@ -119,7 +128,7 @@ private_connectivity
-
-
-
- - {{ 'gateway.connectors-table-name' | translate }} - - - - {{ 'gateway.connectors-table-type' | translate }} - - {{ type.value }} - - - - {{ 'gateway.connectors-table-key' | translate }} - - - - {{ 'gateway.connectors-table-class' | translate }} - - - - gateway.remote-logging-level - - {{ logLevel }} - - -
-
-
- - +
+
+
+ {{ initialConnector?.type ? gatewayConnectorDefaultTypes.get(initialConnector.type) : '' }} + {{ 'gateway.configuration' | translate }} +
+ + + {{ 'gateway.basic' | translate }} + + + {{ 'gateway.advanced' | translate }} + + +
+ + gateway.select-connector + +
+ + +
+
+
gateway.name
+
+ + + + warning + + +
+
+
+
gateway.logs-configuration
+
+ + + {{ 'gateway.enable-remote-logging' | translate }} + + +
+
+
gateway.remote-logging-level
+
+ + + {{ logLevel }} + + +
+
+
+
+ + + {{ 'gateway.send-change-data' | translate }} + + +
+
+
+ + + +
+
+
gateway.host
+
+ + + + warning + + +
+
+
+
gateway.port
+
+ + + + warning + + +
+
+
+
gateway.mqtt-version
+
+ + + {{ version.name }} + + +
+
+
+
gateway.client-id
+
+ + + + +
+
+ + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+
+ gateway.max-number-of-workers +
+
+ + + + warning + + +
+
+
+
+ gateway.max-messages-queue-for-worker +
+
+ + + + warning + + +
+
+
+
+
+
+
+ + + + + + +