diff --git a/.github/ISSUE_TEMPLATE/---bug-report.md b/.github/ISSUE_TEMPLATE/---bug-report.md
index f5d093f424..a126a28e34 100644
--- a/.github/ISSUE_TEMPLATE/---bug-report.md
+++ b/.github/ISSUE_TEMPLATE/---bug-report.md
@@ -12,8 +12,8 @@ A clear and concise description of what the bug is.
**Your Server Environment**
-* [https://demo.thingsboard.io](demo.thingsboard.io)
-* [https://thingsboard.cloud](thingsboard.cloud)
+* [Live Demo](https://demo.thingsboard.io)
+* [ThingsBoard Cloud](https://thingsboard.cloud)
* own setup
* Deployment: monolith or microservices
* Deployment type: deb, rpm, exe, docker-compose, k8s, ami
@@ -60,3 +60,8 @@ If applicable, please add screenshots to help explain your problem.
**Additional context**
Please feel free to add any other context about the problem here.
+
+________________________________________________________________
+**Disclaimer**
+
+We appreciate your contribution whether it is a bug report, feature request, or pull request with improvement (hopefully). Please comply with the [Community ethics policy](https://docs.github.com/en/site-policy/acceptable-use-policies/github-acceptable-use-policies), and do not expect us to answer your requests immediately. Also, do not treat *GitHub issues* as a support channel.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index 1168adaf15..9430e961d5 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -18,3 +18,8 @@ A clear and concise description of any alternative solutions or features you've
**Additional context**
Add any other context or screenshots about the feature request here.
+
+_____________________________________________________
+**Disclaimer**
+
+We appreciate your contribution whether it is a bug report, feature request, or pull request with improvement (hopefully). Please comply with the [Community ethics policy](https://docs.github.com/en/site-policy/acceptable-use-policies/github-acceptable-use-policies)), and do not expect us to answer your requests immediately. Also, do not treat *GitHub issues* as a support channel.
diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md
index dea6318ac5..7f4becfe82 100644
--- a/.github/ISSUE_TEMPLATE/question.md
+++ b/.github/ISSUE_TEMPLATE/question.md
@@ -23,3 +23,8 @@ Clear and concise details.
* OS: name and version
* ThingsBoard: version
* Browser: name and version
+
+___________________________________________________________
+**Disclaimer**
+
+We appreciate your contribution whether it is a bug report, feature request, or pull request with improvement (hopefully). Please comply with the [Community ethics policy](https://docs.github.com/en/site-policy/acceptable-use-policies/github-acceptable-use-policies), and do not expect us to answer your requests immediately. Also, do not treat *GitHub issues* as a support channel.
diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json
index 39996839f1..e0f4f4c615 100644
--- a/application/src/main/data/json/system/widget_bundles/cards.json
+++ b/application/src/main/data/json/system/widget_bundles/cards.json
@@ -17,6 +17,7 @@
"cards.label_widget",
"cards.dashboard_state_widget",
"cards.qr_code",
+ "mobile_app_qr_code",
"cards.attributes_card",
"cards.html_card",
"cards.html_value_card",
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/system/widget_types/mobile_app_qr_code.json b/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json
new file mode 100644
index 0000000000..37f7df0ae6
--- /dev/null
+++ b/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json
@@ -0,0 +1,26 @@
+{
+ "fqn": "mobile_app_qr_code",
+ "name": "Mobile app QR code",
+ "deprecated": false,
+ "image": "tb-image:bW9iaWxlX2FwcF9xcl9jb2RlX3dpZGdldC5zdmc=:bW9iaWxlX2FwcF9xcl9jb2RlX3dpZGdldC5zdmc=;data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMDAiIGhlaWdodD0iMTYwIiBmaWxsPSJub25lIj48cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgZmlsbD0iI2ZmZiIgcng9IjQiLz48cmVjdCB3aWR0aD0iMTk5IiBoZWlnaHQ9IjE1OSIgeD0iLjUiIHk9Ii41IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1vcGFjaXR5PSIuMSIgcng9IjMuNSIvPjxnIGNsaXAtcGF0aD0idXJsKCNhKSI+PHJlY3Qgd2lkdGg9IjEzNyIgaGVpZ2h0PSIxMzciIHg9IjMxLjUiIHk9IjExLjUiIGZpbGw9IiNmZmYiIHJ4PSI4LjIiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTY4LjUgMTEuNWgtMTM3djEzN2gxMzd2LTEzN1oiLz48bWFzayBpZD0iYiIgd2lkdGg9IjEyNCIgaGVpZ2h0PSIxMjQiIHg9IjM4IiB5PSIxOCIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgc3R5bGU9Im1hc2stdHlwZTpsdW1pbmFuY2UiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00Mi43IDUyLjRoLTQuM3Y0LjJoNC4zdi00LjJaTTQyLjcgNjAuOWgtNC4zVjY1aDQuM1Y2MVpNNDIuNyA2NS4xaC00LjN2NC4zaDQuM1Y2NVpNNDIuNyA5NC45aC00LjNWOTloNC4zVjk1Wk00NyA1Ni42aC00LjNWNjFoNC4ydi00LjNaTTQ3IDgyLjFoLTQuM3Y0LjNoNC4yVjgyWk00NyA4Ni40aC00LjN2NC4yaDQuMnYtNC4yWk00NyA5NC45aC00LjNWOTloNC4yVjk1Wk00NyAxMDMuNGgtNC4zdjQuMmg0LjJ2LTQuMlpNNTEuMiA1Mi40aC00LjN2NC4yaDQuM3YtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik01MS4yIDYwLjloLTQuM1Y2NWg0LjNWNjFaTTUxLjIgNzMuNmgtNC4zVjc4aDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik01MS4yIDc3LjloLTQuM1Y4Mmg0LjNWNzhaTTUxLjIgODIuMWgtNC4zdjQuM2g0LjNWODJaTTUxLjIgOTAuNmgtNC4zVjk1aDQuM3YtNC4zWk01MS4yIDk5LjFoLTQuM3Y0LjNoNC4zVjk5Wk01NS40IDUyLjRoLTQuMnY0LjJoNC4ydi00LjJaTTU1LjQgNjkuNGgtNC4ydjQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNTUuNCA4Ni40aC00LjJ2NC4yaDQuMnYtNC4yWk01NS40IDk5LjFoLTQuMnY0LjNoNC4yVjk5Wk01NS40IDEwMy40aC00LjJ2NC4yaDQuMnYtNC4yWk01OS43IDY1LjFoLTQuM3Y0LjNoNC4zVjY1Wk01OS43IDY5LjRoLTQuM3Y0LjJoNC4zdi00LjJaTTU5LjcgODYuNGgtNC4zdjQuMmg0LjN2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNTkuNyA5MC42aC00LjNWOTVoNC4zdi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTU5LjcgOTQuOWgtNC4zVjk5aDQuM1Y5NVpNNjMuOSA1Mi40aC00LjJ2NC4yaDQuMnYtNC4yWk02My45IDY1LjFoLTQuMnY0LjNoNC4yVjY1Wk02My45IDY5LjRoLTQuMnY0LjJoNC4ydi00LjJaTTYzLjkgODYuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNNjMuOSA5OS4xaC00LjJ2NC4zaDQuMlY5OVpNNjguMSA1Mi40SDY0djQuMmg0LjJ2LTQuMlpNNjguMSA2MC45SDY0VjY1aDQuMlY2MVpNNjguMSA2OS40SDY0djQuMmg0LjJ2LTQuMlpNNjguMSA3Ny45SDY0VjgyaDQuMlY3OFpNNjguMSA4Ni40SDY0djQuMmg0LjJ2LTQuMlpNNjguMSA5NC45SDY0Vjk5aDQuMlY5NVpNNjguMSAxMDMuNEg2NHY0LjJoNC4ydi00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTcyLjQgNTIuNEg2OHY0LjJoNC4zdi00LjJaTTcyLjQgNjkuNEg2OHY0LjJoNC4zdi00LjJaTTcyLjQgNzcuOUg2OFY4Mmg0LjNWNzhaTTcyLjQgODIuMUg2OHY0LjNoNC4zVjgyWk03Mi40IDg2LjRINjh2NC4yaDQuM3YtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik03Mi40IDkwLjZINjhWOTVoNC4zdi00LjNaTTcyLjQgOTkuMUg2OHY0LjNoNC4zVjk5Wk03Mi40IDEwMy40SDY4djQuMmg0LjN2LTQuMlpNNzYuNiAyMi43aC00LjJ2NC4yaDQuMnYtNC4yWk03Ni42IDMxLjJoLTQuMnY0LjJoNC4ydi00LjJaTTc2LjYgMzkuN2gtNC4ydjQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNzYuNiA0My45aC00LjJ2NC4yaDQuMlY0NFpNNzYuNiA0OC4xaC00LjJ2NC4zaDQuMnYtNC4zWk03Ni42IDUyLjRoLTQuMnY0LjJoNC4ydi00LjJaTTc2LjYgNTYuNmgtNC4yVjYxaDQuMnYtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik03Ni42IDYwLjloLTQuMlY2NWg0LjJWNjFaTTc2LjYgNjUuMWgtNC4ydjQuM2g0LjJWNjVaTTc2LjYgNjkuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNNzYuNiA5NC45aC00LjJWOTloNC4yVjk1Wk03Ni42IDEwMy40aC00LjJ2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik03Ni42IDEwNy42aC00LjJ2NC4zaDQuMnYtNC4zWk03Ni42IDExMS45aC00LjJ2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik03Ni42IDExNi4xaC00LjJ2NC4yaDQuMnYtNC4yWk03Ni42IDEyNC42aC00LjJ2NC4yaDQuMnYtNC4yWk03Ni42IDEyOC44aC00LjJ2NC4zaDQuMnYtNC4zWk03Ni42IDEzNy4zaC00LjJ2NC4zaDQuMnYtNC4zWk04MC45IDE4LjRoLTQuM3Y0LjNIODF2LTQuM1pNODAuOSAyN2gtNC4zdjQuMkg4MXYtNC4zWk04MC45IDMxLjJoLTQuM3Y0LjJIODF2LTQuMlpNODAuOSAzNS40aC00LjN2NC4zSDgxdi00LjNaTTgwLjkgMzkuN2gtNC4zdjQuMkg4MXYtNC4yWk04MC45IDQ4LjFoLTQuM3Y0LjNIODF2LTQuM1pNODAuOSA2MC45aC00LjNWNjVIODFWNjFaTTgwLjkgNjUuMWgtNC4zdjQuM0g4MVY2NVpNODAuOSA3My42aC00LjNWNzhIODF2LTQuM1pNODAuOSA4Mi4xaC00LjN2NC4zSDgxVjgyWk04MC45IDk0LjloLTQuM1Y5OUg4MVY5NVpNODAuOSA5OS4xaC00LjN2NC4zSDgxVjk5Wk04MC45IDEwMy40aC00LjN2NC4ySDgxdi00LjJaTTgwLjkgMTExLjloLTQuM3Y0LjJIODF2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNODAuOSAxMTYuMWgtNC4zdjQuMkg4MXYtNC4yWk04MC45IDEyNC42aC00LjN2NC4ySDgxdi00LjJaTTgwLjkgMTM3LjNoLTQuM3Y0LjNIODF2LTQuM1pNODUuMSAxOC40SDgxdjQuM0g4NXYtNC4zWk04NS4xIDMxLjJIODF2NC4ySDg1di00LjJaTTg1LjEgMzUuNEg4MXY0LjNIODV2LTQuM1pNODUuMSA0My45SDgxdjQuMkg4NVY0NFpNODUuMSA1Ni42SDgxVjYxSDg1di00LjNaTTg1LjEgNzMuNkg4MVY3OEg4NXYtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik04NS4xIDc3LjlIODFWODJIODVWNzhaTTg1LjEgODIuMUg4MXY0LjNIODVWODJaTTg1LjEgODYuNEg4MXY0LjJIODV2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNODUuMSA5MC42SDgxVjk1SDg1di00LjNaTTg1LjEgOTkuMUg4MXY0LjNIODVWOTlaTTg1LjEgMTExLjlIODF2NC4ySDg1di00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTg1LjEgMTE2LjFIODF2NC4ySDg1di00LjJaTTg1LjEgMTMzSDgxdjQuM0g4NXYtNC4yWk04OS40IDIyLjdIODV2NC4yaDQuM3YtNC4yWk04OS40IDMxLjJIODV2NC4yaDQuM3YtNC4yWk04OS40IDM1LjRIODV2NC4zaDQuM3YtNC4zWk04OS40IDQ4LjFIODV2NC4zaDQuM3YtNC4zWk04OS40IDUyLjRIODV2NC4yaDQuM3YtNC4yWk04OS40IDU2LjZIODVWNjFoNC4zdi00LjNaTTg5LjQgNjUuMUg4NXY0LjNoNC4zVjY1Wk04OS40IDEwMy40SDg1djQuMmg0LjN2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNODkuNCAxMDcuNkg4NXY0LjNoNC4zdi00LjNaTTg5LjQgMTExLjlIODV2NC4yaDQuM3YtNC4yWk04OS40IDEyOC44SDg1djQuM2g0LjN2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNODkuNCAxMzNIODV2NC4zaDQuM3YtNC4yWk05My42IDE4LjRoLTQuMnY0LjNoNC4ydi00LjNaTTkzLjYgMjIuN2gtNC4ydjQuMmg0LjJ2LTQuMlpNOTMuNiAyN2gtNC4ydjQuMmg0LjJ2LTQuM1pNOTMuNiAzMS4yaC00LjJ2NC4yaDQuMnYtNC4yWk05My42IDQzLjloLTQuMnY0LjJoNC4yVjQ0Wk05My42IDU2LjZoLTQuMlY2MWg0LjJ2LTQuM1pNOTMuNiA2NS4xaC00LjJ2NC4zaDQuMlY2NVpNOTMuNiA2OS40aC00LjJ2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik05My42IDczLjZoLTQuMlY3OGg0LjJ2LTQuM1pNOTMuNiA4Mi4xaC00LjJ2NC4zaDQuMlY4MlpNOTMuNiA4Ni40aC00LjJ2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik05My42IDkwLjZoLTQuMlY5NWg0LjJ2LTQuM1pNOTMuNiAxMTEuOWgtNC4ydjQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNOTMuNiAxMTYuMWgtNC4ydjQuMmg0LjJ2LTQuMlpNOTcuOSAxOC40aC00LjN2NC4zSDk4di00LjNaTTk3LjkgMjIuN2gtNC4zdjQuMkg5OHYtNC4yWk05Ny45IDM1LjRoLTQuM3Y0LjNIOTh2LTQuM1pNOTcuOSA0OC4xaC00LjN2NC4zSDk4di00LjNaTTk3LjkgNTIuNGgtNC4zdjQuMkg5OHYtNC4yWk05Ny45IDYwLjloLTQuM1Y2NUg5OFY2MVpNOTcuOSA2NS4xaC00LjN2NC4zSDk4VjY1Wk05Ny45IDczLjZoLTQuM1Y3OEg5OHYtNC4zWk05Ny45IDgyLjFoLTQuM3Y0LjNIOThWODJaTTk3LjkgODYuNGgtNC4zdjQuMkg5OHYtNC4yWk05Ny45IDk0LjloLTQuM1Y5OUg5OFY5NVpNOTcuOSA5OS4xaC00LjN2NC4zSDk4Vjk5Wk05Ny45IDEwMy40aC00LjN2NC4ySDk4di00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTk3LjkgMTA3LjZoLTQuM3Y0LjNIOTh2LTQuM1pNOTcuOSAxMTYuMWgtNC4zdjQuMkg5OHYtNC4yWk05Ny45IDEyNC42aC00LjN2NC4ySDk4di00LjJaTTk3LjkgMTMzaC00LjN2NC4zSDk4di00LjJaTTEwMi4xIDIyLjdIOTh2NC4yaDQuMnYtNC4yWk0xMDIuMSA0My45SDk4djQuMmg0LjJWNDRaTTEwMi4xIDUyLjRIOTh2NC4yaDQuMnYtNC4yWk0xMDIuMSA2OS40SDk4djQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTAyLjEgNzMuNkg5OFY3OGg0LjJ2LTQuM1pNMTAyLjEgOTAuNkg5OFY5NWg0LjJ2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTAyLjEgOTQuOUg5OFY5OWg0LjJWOTVaTTEwMi4xIDEwMy40SDk4djQuMmg0LjJ2LTQuMlpNMTAyLjEgMTMzSDk4djQuM2g0LjJ2LTQuMlpNMTAyLjEgMTM3LjNIOTh2NC4zaDQuMnYtNC4zWk0xMDYuNCAxOC40SDEwMnY0LjNoNC4zdi00LjNaTTEwNi40IDIyLjdIMTAydjQuMmg0LjN2LTQuMlpNMTA2LjQgNDguMUgxMDJ2NC4zaDQuM3YtNC4zWk0xMDYuNCA2MC45SDEwMlY2NWg0LjNWNjFaTTEwNi40IDczLjZIMTAyVjc4aDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMDYuNCA3Ny45SDEwMlY4Mmg0LjNWNzhaTTEwNi40IDgyLjFIMTAydjQuM2g0LjNWODJaTTEwNi40IDg2LjRIMTAydjQuMmg0LjN2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTA2LjQgOTAuNkgxMDJWOTVoNC4zdi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTEwNi40IDk0LjlIMTAyVjk5aDQuM1Y5NVpNMTA2LjQgMTAzLjRIMTAydjQuMmg0LjN2LTQuMlpNMTA2LjQgMTE2LjFIMTAydjQuMmg0LjN2LTQuMlpNMTA2LjQgMTMzSDEwMnY0LjNoNC4zdi00LjJaTTEwNi40IDEzNy4zSDEwMnY0LjNoNC4zdi00LjNaTTExMC42IDI3aC00LjJ2NC4yaDQuMnYtNC4zWk0xMTAuNiAzOS43aC00LjJ2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMTAuNiA0My45aC00LjJ2NC4yaDQuMlY0NFpNMTEwLjYgNDguMWgtNC4ydjQuM2g0LjJ2LTQuM1pNMTEwLjYgNTIuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNMTEwLjYgNTYuNmgtNC4yVjYxaDQuMnYtNC4zWk0xMTAuNiA5MC42aC00LjJWOTVoNC4ydi00LjNaTTExMC42IDk5LjFoLTQuMnY0LjNoNC4yVjk5Wk0xMTAuNiAxMDcuNmgtNC4ydjQuM2g0LjJ2LTQuM1pNMTEwLjYgMTE2LjFoLTQuMnY0LjJoNC4ydi00LjJaTTExMC42IDEyMC4zaC00LjJ2NC4zaDQuMnYtNC4zWk0xMTAuNiAxMzNoLTQuMnY0LjNoNC4ydi00LjJaTTExMC42IDEzNy4zaC00LjJ2NC4zaDQuMnYtNC4zWk0xMTQuOSAzMS4yaC00LjN2NC4yaDQuM3YtNC4yWk0xMTQuOSA0OC4xaC00LjN2NC4zaDQuM3YtNC4zWk0xMTQuOSA1Mi40aC00LjN2NC4yaDQuM3YtNC4yWk0xMTQuOSA2NS4xaC00LjN2NC4zaDQuM1Y2NVpNMTE0LjkgNzMuNmgtNC4zVjc4aDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMTQuOSA3Ny45aC00LjNWODJoNC4zVjc4Wk0xMTQuOSA4Mi4xaC00LjN2NC4zaDQuM1Y4MlpNMTE0LjkgODYuNGgtNC4zdjQuMmg0LjN2LTQuMlpNMTE0LjkgOTkuMWgtNC4zdjQuM2g0LjNWOTlaTTExNC45IDExMS45aC00LjN2NC4yaDQuM3YtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMTQuOSAxMTYuMWgtNC4zdjQuMmg0LjN2LTQuMlpNMTE0LjkgMTMzaC00LjN2NC4zaDQuM3YtNC4yWk0xMTQuOSAxMzcuM2gtNC4zdjQuM2g0LjN2LTQuM1pNMTE5LjEgMTguNEgxMTV2NC4zaDQuMnYtNC4zWk0xMTkuMSAzNS40SDExNXY0LjNoNC4ydi00LjNaTTExOS4xIDM5LjdIMTE1djQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTE5LjEgNDMuOUgxMTV2NC4yaDQuMlY0NFpNMTE5LjEgNjUuMUgxMTV2NC4zaDQuMlY2NVpNMTE5LjEgNjkuNEgxMTV2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMTkuMSA3My42SDExNVY3OGg0LjJ2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTE5LjEgNzcuOUgxMTVWODJoNC4yVjc4Wk0xMTkuMSA4Mi4xSDExNXY0LjNoNC4yVjgyWk0xMTkuMSA4Ni40SDExNXY0LjJoNC4ydi00LjJaTTExOS4xIDk0LjlIMTE1Vjk5aDQuMlY5NVpNMTE5LjEgMTAzLjRIMTE1djQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTE5LjEgMTA3LjZIMTE1djQuM2g0LjJ2LTQuM1pNMTE5LjEgMTE2LjFIMTE1djQuMmg0LjJ2LTQuMlpNMTE5LjEgMTI0LjZIMTE1djQuMmg0LjJ2LTQuMlpNMTE5LjEgMTMzSDExNXY0LjNoNC4ydi00LjJaTTExOS4xIDEzNy4zSDExNXY0LjNoNC4ydi00LjNaTTEyMy40IDMxLjJIMTE5djQuMmg0LjN2LTQuMlpNMTIzLjQgMzUuNEgxMTl2NC4zaDQuM3YtNC4zWk0xMjMuNCA1Ni42SDExOVY2MWg0LjN2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTIzLjQgNjAuOUgxMTlWNjVoNC4zVjYxWk0xMjMuNCA5MC42SDExOVY5NWg0LjN2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTIzLjQgOTQuOUgxMTlWOTloNC4zVjk1Wk0xMjMuNCA5OS4xSDExOXY0LjNoNC4zVjk5Wk0xMjMuNCAxMjAuM0gxMTl2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMjMuNCAxMjQuNkgxMTl2NC4yaDQuM3YtNC4yWk0xMjcuNiAxOC40aC00LjJ2NC4zaDQuMnYtNC4zWk0xMjcuNiAyN2gtNC4ydjQuMmg0LjJ2LTQuM1pNMTI3LjYgMzEuMmgtNC4ydjQuMmg0LjJ2LTQuMlpNMTI3LjYgMzkuN2gtNC4ydjQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTI3LjYgNDMuOWgtNC4ydjQuMmg0LjJWNDRaTTEyNy42IDQ4LjFoLTQuMnY0LjNoNC4ydi00LjNaTTEyNy42IDU2LjZoLTQuMlY2MWg0LjJ2LTQuM1pNMTI3LjYgNjUuMWgtNC4ydjQuM2g0LjJWNjVaTTEyNy42IDY5LjRoLTQuMnY0LjJoNC4ydi00LjJaTTEyNy42IDc3LjloLTQuMlY4Mmg0LjJWNzhaTTEyNy42IDgyLjFoLTQuMnY0LjNoNC4yVjgyWk0xMjcuNiA5OS4xaC00LjJ2NC4zaDQuMlY5OVpNMTI3LjYgMTAzLjRoLTQuMnY0LjJoNC4ydi00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTEyNy42IDEwNy42aC00LjJ2NC4zaDQuMnYtNC4zWk0xMjcuNiAxMTEuOWgtNC4ydjQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTI3LjYgMTE2LjFoLTQuMnY0LjJoNC4ydi00LjJaTTEyNy42IDEyMC4zaC00LjJ2NC4zaDQuMnYtNC4zWk0xMjcuNiAxMjguOGgtNC4ydjQuM2g0LjJ2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTI3LjYgMTMzaC00LjJ2NC4zaDQuMnYtNC4yWk0xMzEuOSA1Mi40aC00LjN2NC4yaDQuM3YtNC4yWk0xMzEuOSA2NS4xaC00LjN2NC4zaDQuM1Y2NVpNMTMxLjkgNzcuOWgtNC4zVjgyaDQuM1Y3OFpNMTMxLjkgODYuNGgtNC4zdjQuMmg0LjN2LTQuMlpNMTMxLjkgOTQuOWgtNC4zVjk5aDQuM1Y5NVpNMTMxLjkgMTAzLjRoLTQuM3Y0LjJoNC4zdi00LjJaTTEzMS45IDEyMC4zaC00LjN2NC4zaDQuM3YtNC4zWk0xMzEuOSAxMzNoLTQuM3Y0LjNoNC4zdi00LjJaTTEzMS45IDEzNy4zaC00LjN2NC4zaDQuM3YtNC4zWk0xMzYuMSA1Mi40aC00LjJ2NC4yaDQuMnYtNC4yWk0xMzYuMSA1Ni42aC00LjJWNjFoNC4ydi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTEzNi4xIDYwLjloLTQuMlY2NWg0LjJWNjFaTTEzNi4xIDY1LjFoLTQuMnY0LjNoNC4yVjY1Wk0xMzYuMSA2OS40aC00LjJ2NC4yaDQuMnYtNC4yWk0xMzYuMSA5NC45aC00LjJWOTloNC4yVjk1Wk0xMzYuMSAxMDMuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNMTM2LjEgMTExLjloLTQuMnY0LjJoNC4ydi00LjJaTTEzNi4xIDEyMC4zaC00LjJ2NC4zaDQuMnYtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMzYuMSAxMjQuNmgtNC4ydjQuMmg0LjJ2LTQuMlpNMTM2LjEgMTI4LjhoLTQuMnY0LjNoNC4ydi00LjNaTTE0MC4zIDUyLjRoLTQuMnY0LjJoNC4ydi00LjJaTTE0MC4zIDU2LjZoLTQuMlY2MWg0LjJ2LTQuM1pNMTQwLjMgNjUuMWgtNC4ydjQuM2g0LjJWNjVaTTE0MC4zIDczLjZoLTQuMlY3OGg0LjJ2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTQwLjMgNzcuOWgtNC4yVjgyaDQuMlY3OFpNMTQwLjMgODYuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNMTQwLjMgOTQuOWgtNC4yVjk5aDQuMlY5NVpNMTQwLjMgMTAzLjRoLTQuMnY0LjJoNC4ydi00LjJaTTE0MC4zIDEyMC4zaC00LjJ2NC4zaDQuMnYtNC4zWk0xNDAuMyAxMjguOGgtNC4ydjQuM2g0LjJ2LTQuM1pNMTQ0LjYgNjAuOWgtNC4zVjY1aDQuM1Y2MVoiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTQ0LjYgODYuNGgtNC4zdjQuMmg0LjN2LTQuMlpNMTQ0LjYgMTAzLjRoLTQuM3Y0LjJoNC4zdi00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE0NC42IDEwNy42aC00LjN2NC4zaDQuM3YtNC4zWk0xNDQuNiAxMTEuOWgtNC4zdjQuMmg0LjN2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTQ0LjYgMTE2LjFoLTQuM3Y0LjJoNC4zdi00LjJaTTE0NC42IDEyMC4zaC00LjN2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNDQuNiAxMjQuNmgtNC4zdjQuMmg0LjN2LTQuMlpNMTQ4LjggNTIuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNMTQ4LjggNjAuOWgtNC4yVjY1aDQuMlY2MVpNMTQ4LjggNzMuNmgtNC4yVjc4aDQuMnYtNC4zWk0xNDguOCA4Mi4xaC00LjJ2NC4zaDQuMlY4MlpNMTQ4LjggODYuNGgtNC4ydjQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTQ4LjggOTAuNmgtNC4yVjk1aDQuMnYtNC4zWk0xNDguOCA5OS4xaC00LjJ2NC4zaDQuMlY5OVpNMTQ4LjggMTAzLjRoLTQuMnY0LjJoNC4ydi00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE0OC44IDEwNy42aC00LjJ2NC4zaDQuMnYtNC4zWk0xNDguOCAxMTEuOWgtNC4ydjQuMmg0LjJ2LTQuMlpNMTQ4LjggMTIwLjNoLTQuMnY0LjNoNC4ydi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE0OC44IDEyNC42aC00LjJ2NC4yaDQuMnYtNC4yWk0xNDguOCAxMjguOGgtNC4ydjQuM2g0LjJ2LTQuM1pNMTQ4LjggMTM3LjNoLTQuMnY0LjNoNC4ydi00LjNaTTE1MyA1Ni42aC00LjJWNjFoNC4zdi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE1MyA2MC45aC00LjJWNjVoNC4zVjYxWk0xNTMgNjUuMWgtNC4ydjQuM2g0LjNWNjVaTTE1MyA5NC45aC00LjJWOTloNC4zVjk1Wk0xNTMgOTkuMWgtNC4ydjQuM2g0LjNWOTlaTTE1MyAxMDMuNGgtNC4ydjQuMmg0LjN2LTQuMlpNMTUzIDEyMC4zaC00LjJ2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNTMgMTI0LjZoLTQuMnY0LjJoNC4zdi00LjJaTTE1MyAxMzcuM2gtNC4ydjQuM2g0LjN2LTQuM1pNMTU3LjMgNTIuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNMTU3LjMgNTYuNmgtNC4yVjYxaDQuMnYtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNTcuMyA2MC45aC00LjJWNjVoNC4yVjYxWk0xNTcuMyA2NS4xaC00LjJ2NC4zaDQuMlY2NVpNMTU3LjMgOTAuNmgtNC4yVjk1aDQuMnYtNC4zWk0xNTcuMyAxMTYuMWgtNC4ydjQuMmg0LjJ2LTQuMlpNMTU3LjMgMTI0LjZoLTQuMnY0LjJoNC4ydi00LjJaTTE1Ny4zIDEzM2gtNC4ydjQuM2g0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTYxLjYgNTYuNmgtNC4zVjYxaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNjEuNiA2MC45aC00LjNWNjVoNC4zVjYxWk0xNjEuNiA2NS4xaC00LjN2NC4zaDQuM1Y2NVpNMTYxLjYgNzMuNmgtNC4zVjc4aDQuM3YtNC4zWk0xNjEuNiA5MC42aC00LjNWOTVoNC4zdi00LjNaTTE2MS42IDEwMy40aC00LjN2NC4yaDQuM3YtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNjEuNiAxMDcuNmgtNC4zdjQuM2g0LjN2LTQuM1pNMTYxLjYgMTExLjloLTQuM3Y0LjJoNC4zdi00LjJaTTE2MS42IDEyMC4zaC00LjN2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNjEuNiAxMjQuNmgtNC4zdjQuMmg0LjN2LTQuMlpNMTYxLjYgMTMzaC00LjN2NC4zaDQuM3YtNC4yWiIvPjwvbWFzaz48ZyBtYXNrPSJ1cmwoI2IpIj48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTYxLjYgMTguNEgzOC40djEyMy4yaDEyMy4yVjE4LjRaIi8+PC9nPjxtYXNrIGlkPSJjIiB3aWR0aD0iMzEiIGhlaWdodD0iMzEiIHg9IjM4IiB5PSIxOCIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgc3R5bGU9Im1hc2stdHlwZTpsdW1pbmFuY2UiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00Mi43IDE4LjRoLTQuM3Y0LjNoNC4zdi00LjNaTTQyLjcgMjIuN2gtNC4zdjQuMmg0LjN2LTQuMlpNNDIuNyAyN2gtNC4zdjQuMmg0LjN2LTQuM1pNNDIuNyAzMS4yaC00LjN2NC4yaDQuM3YtNC4yWk00Mi43IDM1LjRoLTQuM3Y0LjNoNC4zdi00LjNaTTQyLjcgMzkuN2gtNC4zdjQuMmg0LjN2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNDIuNyA0My45aC00LjN2NC4yaDQuM1Y0NFpNNDcgMTguNGgtNC4zdjQuM2g0LjJ2LTQuM1pNNDcgNDMuOWgtNC4zdjQuMmg0LjJWNDRaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTUxLjIgMTguNGgtNC4zdjQuM2g0LjN2LTQuM1pNNTEuMiA0My45aC00LjN2NC4yaDQuM1Y0NFoiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNTUuNCAxOC40aC00LjJ2NC4zaDQuMnYtNC4zWk01NS40IDQzLjloLTQuMnY0LjJoNC4yVjQ0Wk01OS43IDE4LjRoLTQuM3Y0LjNoNC4zdi00LjNaTTU5LjcgNDMuOWgtNC4zdjQuMmg0LjNWNDRaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTYzLjkgMTguNGgtNC4ydjQuM2g0LjJ2LTQuM1pNNjMuOSA0My45aC00LjJ2NC4yaDQuMlY0NFpNNjguMSAxOC40SDY0djQuM2g0LjJ2LTQuM1pNNjguMSAyMi43SDY0djQuMmg0LjJ2LTQuMlpNNjguMSAyN0g2NHY0LjJoNC4ydi00LjNaTTY4LjEgMzEuMkg2NHY0LjJoNC4ydi00LjJaTTY4LjEgMzUuNEg2NHY0LjNoNC4ydi00LjNaTTY4LjEgMzkuN0g2NHY0LjJoNC4ydi00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTY4LjEgNDMuOUg2NHY0LjJoNC4yVjQ0WiIvPjwvbWFzaz48ZyBtYXNrPSJ1cmwoI2MpIj48cGF0aCBmaWxsPSIjMDAwIiBkPSJNNjguMSAxOC40SDM4LjR2MjkuN2gyOS43VjE4LjRaIi8+PC9nPjxtYXNrIGlkPSJkIiB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHg9IjQ2IiB5PSIyNiIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgc3R5bGU9Im1hc2stdHlwZTpsdW1pbmFuY2UiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik01MS4yIDI3aC00LjN2NC4yaDQuM3YtNC4zWk01MS4yIDMxLjJoLTQuM3Y0LjJoNC4zdi00LjJaTTUxLjIgMzUuNGgtNC4zdjQuM2g0LjN2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNTUuNCAyN2gtNC4ydjQuMmg0LjJ2LTQuM1pNNTUuNCAzMS4yaC00LjJ2NC4yaDQuMnYtNC4yWk01NS40IDM1LjRoLTQuMnY0LjNoNC4ydi00LjNaTTU5LjcgMjdoLTQuM3Y0LjJoNC4zdi00LjNaTTU5LjcgMzEuMmgtNC4zdjQuMmg0LjN2LTQuMlpNNTkuNyAzNS40aC00LjN2NC4zaDQuM3YtNC4zWiIvPjwvbWFzaz48ZyBtYXNrPSJ1cmwoI2QpIj48cGF0aCBmaWxsPSIjMDAwIiBkPSJNNTkuNyAyN0g0Ni45djEyLjdoMTIuOFYyNi45WiIvPjwvZz48bWFzayBpZD0iZSIgd2lkdGg9IjMxIiBoZWlnaHQ9IjMxIiB4PSIxMzEiIHk9IjE4IiBtYXNrVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBzdHlsZT0ibWFzay10eXBlOmx1bWluYW5jZSI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTEzNi4xIDE4LjRoLTQuMnY0LjNoNC4ydi00LjNaTTEzNi4xIDIyLjdoLTQuMnY0LjJoNC4ydi00LjJaTTEzNi4xIDI3aC00LjJ2NC4yaDQuMnYtNC4zWk0xMzYuMSAzMS4yaC00LjJ2NC4yaDQuMnYtNC4yWk0xMzYuMSAzNS40aC00LjJ2NC4zaDQuMnYtNC4zWk0xMzYuMSAzOS43aC00LjJ2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMzYuMSA0My45aC00LjJ2NC4yaDQuMlY0NFpNMTQwLjMgMTguNGgtNC4ydjQuM2g0LjJ2LTQuM1pNMTQwLjMgNDMuOWgtNC4ydjQuMmg0LjJWNDRaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE0NC42IDE4LjRoLTQuM3Y0LjNoNC4zdi00LjNaTTE0NC42IDQzLjloLTQuM3Y0LjJoNC4zVjQ0Wk0xNDguOCAxOC40aC00LjJ2NC4zaDQuMnYtNC4zWk0xNDguOCA0My45aC00LjJ2NC4yaDQuMlY0NFpNMTUzIDE4LjRoLTQuMnY0LjNoNC4zdi00LjNaTTE1MyA0My45aC00LjJ2NC4yaDQuM1Y0NFpNMTU3LjMgMTguNGgtNC4ydjQuM2g0LjJ2LTQuM1pNMTU3LjMgNDMuOWgtNC4ydjQuMmg0LjJWNDRaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE2MS42IDE4LjRoLTQuM3Y0LjNoNC4zdi00LjNaTTE2MS42IDIyLjdoLTQuM3Y0LjJoNC4zdi00LjJaTTE2MS42IDI3aC00LjN2NC4yaDQuM3YtNC4zWk0xNjEuNiAzMS4yaC00LjN2NC4yaDQuM3YtNC4yWk0xNjEuNiAzNS40aC00LjN2NC4zaDQuM3YtNC4zWk0xNjEuNiAzOS43aC00LjN2NC4yaDQuM3YtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNjEuNiA0My45aC00LjN2NC4yaDQuM1Y0NFoiLz48L21hc2s+PGcgbWFzaz0idXJsKCNlKSI+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTE2MS42IDE4LjRoLTI5Ljd2MjkuN2gyOS43VjE4LjRaIi8+PC9nPjxtYXNrIGlkPSJmIiB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHg9IjE0MCIgeT0iMjYiIG1hc2tVbml0cz0idXNlclNwYWNlT25Vc2UiIHN0eWxlPSJtYXNrLXR5cGU6bHVtaW5hbmNlIj48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTQ0LjYgMjdoLTQuM3Y0LjJoNC4zdi00LjNaTTE0NC42IDMxLjJoLTQuM3Y0LjJoNC4zdi00LjJaTTE0NC42IDM1LjRoLTQuM3Y0LjNoNC4zdi00LjNaTTE0OC44IDI3aC00LjJ2NC4yaDQuMnYtNC4zWk0xNDguOCAzMS4yaC00LjJ2NC4yaDQuMnYtNC4yWk0xNDguOCAzNS40aC00LjJ2NC4zaDQuMnYtNC4zWk0xNTMgMjdoLTQuMnY0LjJoNC4zdi00LjNaTTE1MyAzMS4yaC00LjJ2NC4yaDQuM3YtNC4yWk0xNTMgMzUuNGgtNC4ydjQuM2g0LjN2LTQuM1oiLz48L21hc2s+PGcgbWFzaz0idXJsKCNmKSI+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTE1MyAyN2gtMTIuN3YxMi43aDEyLjhWMjYuOVoiLz48L2c+PG1hc2sgaWQ9ImciIHdpZHRoPSIzMSIgaGVpZ2h0PSIzMSIgeD0iMzgiIHk9IjExMSIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgc3R5bGU9Im1hc2stdHlwZTpsdW1pbmFuY2UiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00Mi43IDExMS45aC00LjN2NC4yaDQuM3YtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00Mi43IDExNi4xaC00LjN2NC4yaDQuM3YtNC4yWk00Mi43IDEyMC4zaC00LjN2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00Mi43IDEyNC42aC00LjN2NC4yaDQuM3YtNC4yWk00Mi43IDEyOC44aC00LjN2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00Mi43IDEzM2gtNC4zdjQuM2g0LjN2LTQuMlpNNDIuNyAxMzcuM2gtNC4zdjQuM2g0LjN2LTQuM1pNNDcgMTExLjloLTQuM3Y0LjJoNC4ydi00LjJaTTQ3IDEzNy4zaC00LjN2NC4zaDQuMnYtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik01MS4yIDExMS45aC00LjN2NC4yaDQuM3YtNC4yWk01MS4yIDEzNy4zaC00LjN2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik01NS40IDExMS45aC00LjJ2NC4yaDQuMnYtNC4yWk01NS40IDEzNy4zaC00LjJ2NC4zaDQuMnYtNC4zWk01OS43IDExMS45aC00LjN2NC4yaDQuM3YtNC4yWk01OS43IDEzNy4zaC00LjN2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik02My45IDExMS45aC00LjJ2NC4yaDQuMnYtNC4yWk02My45IDEzNy4zaC00LjJ2NC4zaDQuMnYtNC4zWk02OC4xIDExMS45SDY0djQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNjguMSAxMTYuMUg2NHY0LjJoNC4ydi00LjJaTTY4LjEgMTIwLjNINjR2NC4zaDQuMnYtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik02OC4xIDEyNC42SDY0djQuMmg0LjJ2LTQuMlpNNjguMSAxMjguOEg2NHY0LjNoNC4ydi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTY4LjEgMTMzSDY0djQuM2g0LjJ2LTQuMlpNNjguMSAxMzcuM0g2NHY0LjNoNC4ydi00LjNaIi8+PC9tYXNrPjxnIG1hc2s9InVybCgjZykiPjxwYXRoIGZpbGw9IiMwMDAiIGQ9Ik02OC4xIDExMS45SDM4LjR2MjkuN2gyOS43di0yOS43WiIvPjwvZz48bWFzayBpZD0iaCIgd2lkdGg9IjE0IiBoZWlnaHQ9IjE0IiB4PSI0NiIgeT0iMTIwIiBtYXNrVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBzdHlsZT0ibWFzay10eXBlOmx1bWluYW5jZSI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTUxLjIgMTIwLjNoLTQuM3Y0LjNoNC4zdi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTUxLjIgMTI0LjZoLTQuM3Y0LjJoNC4zdi00LjJaTTUxLjIgMTI4LjhoLTQuM3Y0LjNoNC4zdi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTU1LjQgMTIwLjNoLTQuMnY0LjNoNC4ydi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTU1LjQgMTI0LjZoLTQuMnY0LjJoNC4ydi00LjJaTTU1LjQgMTI4LjhoLTQuMnY0LjNoNC4ydi00LjNaTTU5LjcgMTIwLjNoLTQuM3Y0LjNoNC4zdi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTU5LjcgMTI0LjZoLTQuM3Y0LjJoNC4zdi00LjJaTTU5LjcgMTI4LjhoLTQuM3Y0LjNoNC4zdi00LjNaIi8+PC9tYXNrPjxnIG1hc2s9InVybCgjaCkiPjxwYXRoIGZpbGw9IiMwMDAiIGQ9Ik01OS43IDEyMC4zSDQ2Ljl2MTIuOGgxMi44di0xMi44WiIvPjwvZz48L2c+PGRlZnM+PGNsaXBQYXRoIGlkPSJhIj48cmVjdCB3aWR0aD0iMTM3IiBoZWlnaHQ9IjEzNyIgeD0iMzEuNSIgeT0iMTEuNSIgZmlsbD0iI2ZmZiIgcng9IjguMiIvPjwvY2xpcFBhdGg+PC9kZWZzPjwvc3ZnPg==",
+ "description": "Enables users to open the ThingsBoard mobile app by scanning the QR code from dashboard.",
+ "descriptor": {
+ "type": "static",
+ "sizeX": 7.5,
+ "sizeY": 3,
+ "resources": [],
+ "templateHtml": "\n",
+ "templateCss": "",
+ "controllerScript": "self.onInit = function() {\n}\n",
+ "settingsSchema": "",
+ "dataKeySettingsSchema": "",
+ "settingsDirective": "tb-mobile-app-qr-code-widget-settings",
+ "defaultConfig": "{\n \"showTitle\": true,\n \"backgroundColor\": \"rgb(255, 255, 255)\",\n \"color\": \"rgba(0, 0, 0, 0.87)\",\n \"padding\": \"8px\",\n \"settings\": {\n \"useSystemSettings\": true,\n \"androidConfig\": {\n \"enabled\": true\n },\n \"iosConfig\": {\n \"enabled\": true\n },\n \"qrCodeConfig\": {\n \"badgeEnabled\": true,\n \"badgeStyle\": \"ORIGINAL\",\n \"badgePosition\": \"RIGHT\",\n \"qrCodeLabelEnabled\": true,\n \"qrCodeLabel\": \"Scan to connect or download mobile app\"\n }\n },\n \"title\": \"Mobile app QR code\",\n \"dropShadow\": true,\n \"enableFullscreen\": false,\n \"widgetStyle\": {},\n \"widgetCss\": \"\",\n \"pageSize\": 1024,\n \"noDataDisplayMessage\": \"\",\n \"showTitleIcon\": false,\n \"titleTooltip\": \"\",\n \"titleStyle\": {\n \"fontSize\": \"16px\",\n \"fontWeight\": 400\n }\n}\n"
+ },
+ "tags": [
+ "ios",
+ "android",
+ "qr code",
+ "mobile application"
+ ]
+}
\ No newline at end of file
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,iVBORw0KGgoAAAANSUhEUgAAAagAAAGoCAYAAAATsnHAAAAmgUlEQVR4Ae3de2xc5bnv8cd2jO04tmM7V8dJHEfQxpB9ykXFFNhtyWkq0cKptPNHuIgKyt7/UIlIRagCCaFKjRClbegp0tERUG1EaCqlPToN7D8S0tNuOBC0S6E7wSmhnkBw7Ni5OI7jeBw7yZ7HzkrGk5m13nWZmXfNfD/SlMRek4uh6+fnfZ/1vBUCP+ZkeVWmXhUXfywXf14pAMrN+YzXhYv/nMrygoEKQTYaMFelvarlchgBQFhnU69zqVfy4o/1dV4wCwE1Q4NnrsyEUZ1croYAoFA0pCZTrzMXf1z2lVY5B1StzIRSjcwEEwDYRENqQmYCKyllqNwCygmlemG5DkB8aDWlYXVayiisyiGgCCUApcQJq1MyU2WVrFINKA2ieXJ5CQ8ASpEG1KjMVFYlp9QCSoOpMfVqEKolAOXDqapOSgk1V5RKQGnX3XyZWcbLm/vuu6/xrrvuau/s7Gxvbm5ub2hoaK+urm6sra1tr6qqaqysrNRwlNTH2gVAWTl//vypc+fOnXL+OTU1dWp8fLxvdHS0b3h4uC+RSPRt3LixR/JvTEokqOIeUBpMCyQPy3gaRo8++mj3kiVLupqamrrq6uq6CB4AYU1MTPSkXn0jIyM9Bw4c2POrX/2qZ+vWrackerEPqrgGVOQVkwbSE088sb61tbWrpaVlPWEEoFA0tMbGxnp6e3t3/vGPf+x5/PHH+yQ6sQ2quAVUpHtMb775ZvcXv/jF7tRyXffcuXO7BQAscObMmT39/f0733///T0RLgtqSGlDRWwmVsQpoLQjr1lCTnl49tln2++9994NixYt2kCVBMB2k5OTfSdOnNj5yiuvvBxBZaVV1IjEpOsvDgEVep9Jl++efvrpDW1tbeuplADElVZWiURi+9q1a7dLOLFY9rM9oHQ5r0kCLuc51dLSpUsfcjrsACDutKoaHR3d89JLL20JUVXpUp82Z4yIpWwNqFBVk+4t3XLLLZuolgCUutTy3/aQQaWjk46LhdWUjQEVuGoimACUq5BBpdXUsFi2N2VTQGkgaet4g/ikS3mPPPLIcwQTgHIXMqi0y0/3pqzo9LMloHRJb7H47NDT5oef//znmxYuXPiQAAAuCRFUutQ3KBYs+dkQUNo+3io+l/R6e3sf6ujo2ETzAwBkp80UH3/88ZYAXX9aQem+1BkpomIPVNUlvYV+/hzbtm3rGhsb29bZ2fkU4QQAuemzntddd91zZ8+efVu3Qny8Ve/Jem9ukiIqZgXVIj73m/r6+jYtW7ZskwAAfDt8+PCW9vb2LeKP7kudkCIoRgWlv+cS8RFOmvzJZPLfCCcACE7voQGqKb1X+1rpikqhKyhtgtC/6FWmb2CvCQCipUeCfPrpp1tWr179so+36eGIR6WAzROFTESnU88onLRDb2ho6Cn2mgAgWnpP1XvrkSNHntJ7reHb9N7tu9s6jCopDF9t5Fp+bt68+V8bGxvXCwAgL+bNm3f9XSkNDQ07d+3aZXImlRY12nk9LgV4VqoQS3y+wkmnQXz961//31RNAFAY2o7+u9/97l98HO1RkGel8h1QvsLpo48+2tDV1fWcAAAKLpFI/MjHvlTeQyqfAaWloPGeEy3kAFB8PlvRtXFCQyovy335bJLQaeSEEwDEiN6L9Z5seLne4xdKnuSrgjJ+CFc79ZilBwB28VlJ5eVh3nwElI4vMhqPMTw8/Nz8+fM3CADAOjpwtrW19THDy3UKeqSHH0a9xKfth0bhpCUk4QQA9mppadmgz0oZXq7FyVyJUJQBpZ16rSYXsucEAPGwePHih3zsSWkGRPYgb1QB5XTsef56hBMAxIuPxgnjLDARVUBpaeeZmjpXj3ACgPjRe/fevXtNtmU0C+ZLBKIIqHli0LGn5zjp7CcBAMSSni2l034MLtVMCD0NKOwsPmc6uWs3oM7We/DBB/+1qqqK8UUAEGMdHR3r582bt8Ngdl+NzJzIG/gh3rABtSj1qna7QCflPvPMM/9HT3YUAECsVVRU1Nx0003dBw8e3JFa8ptwu1RmHuQdk4DCPAdl9LwTD+ICQOkZHBx8ecmSJT8yuHQ49TKZlH6FoHtQurTnue+kTRGEEwCUHm0/13u8waVayARqPQ+6xKejjGrcLtB9pzvvvPN/ajkoAICS09TUdL3BflTgpb4gS3zatef5QK6ee8++EwCUtomJiZ7a2to7DS7V4+LPiA9+l/j0es99J32gi3ACgNJXU1PTdejQIZPnW5vFZ+b4DShtE3ddS9SlPR7GBYDysXz58k36rKvHZUa9C+n8LPHpL77M6yKW9gCg/Jw5c2ZPfX39Ro/L9JmoATE8hddPBeU5ukKPbCecAKD8zJ07t9ugq08zx3gMkmlAafVU73aBLu1dffXVLO0BQJnq6OjYpMMZPC7TLDFqOzdd4lsgHgF1/Pjx5/TsEAEAlC3DB3iTeqnHNUYVlFH1RDgBAPQBXs0Ej8tqxaCKMqmgqJ4AAMYMGyb0wd1jbhd4VVBUTwAAX7RhwuBYDs0W1wzyqqCongAAvhlWUSdTr5Fcn/SqoDzn7RFOAIBMhlWUdvzlzCG3Cspz5h7VEwAgF8MqKudxHG4VlOtICqonAIAbraIMOvrqcn0iVwWlo9GXiguqJwCAl88//3zLihUrtnhcps9EJTM/mKuC8noSWBoaGrzWFgEAZW7ZsmUPGUyXyFpF5Qoo1+YIZu4BAExUVlY2Pv30016rbfOyvjfLxzyf8O3o6GBpDwBgpK2tbb3HJZpFtdk+mClrkjl0w0s3vgQAAAOaGZs3b/bdLJEtoFyX9x544AGvceoAAMxy//33+17mywwoz+W9lpYWr1INAIBZlixZ4hVQVyzzZQbUXHGhR/rSHAEA8Euzw2CyxKxlvsyAcl3eu/HGG9l7AgAEcs0113hlSM4KSpf2rnJ7p0EnBgAAWbW2tnoFlGbQpVyqzPhETnTvAQDC0AwxeGj30lZTZbYPZvO1r32tSwAACOGHP/yh10pctfODymwfzGb16tUs7wEAQlm4cKFXsXNFBaX/dF3iq6+vp4ICAIRi8KiS9kNMZ5MTUK7hpGuGNTU1BBQAIBRtNzfYh5rOJKOAevDBBwknAEAkvv/975t0810KqFq3Kw161wEAMLJs2TKvomdWQFW5XdnU1EQFBQCIhEGmTDftGS3xpfafGG8EAIhEXV2dV0BNz4StFI/hsIoGCQBAVAwaJaazyTOgdECsAAAQobvuustrZc47oDo7O1neAwBEauXKleEDqrm5mYACAERqwYIF4QOqoaGBgAIARMogW6YDqtLtirq6OgIKABCp6upqr2kSFZ4BNWfOHK9fBAAAX2pra72Kn0oNpwq3K6qqqggoAECkDLLFe4mvsrKSgAIARMokWzwDigoKAFAElVRQAICC02kSHpdUVgoAABYioAAAViKgAABWIqAAAFYioAAAViKgAABWIqAAAFYioAAAViKgAABWIqAAAFYioAAAViKgAABWIqAAAFYioAAAViKgAABWIqAAAFYioAAAViKgAABWIqAAAFYioAAAViKgAABWIqAAAFYioAAAViKgAABWIqAAAFYioAAAViKgAABWIqAAAFYioAAAViKgAABWIqAAAFYioAAAViKgAABWIqAAAFYioAAAViKgAABWIqAAAFYioAAAViKgAABWIqAAAFYioAAAViKgAABWIqAAAFYioAAAViKgAABWmiMAIpfYOyIf/GFIBhKnZXxsSpoX1Urz4lpZdV2T3LBukQDwVpF6rXS74MKFC58KACMaTL99/oCcHJrIeY2G1R33rCCoUPYqKio6XD8vBBQQid2vHZI/bDtkfP0dG1fIuntXCFCuvAKKPSggAn7DSen1+j4A2RFQQEjDg0nf4eTQ9x1MLQsCuBIBBYS0+9fhqqCw7wdKFQEFhJAcm5ru1gvj4L6R6V8HwGwEFBBCf2JMopDYxzIfkImAAkIYHkpKFE4OTgiA2QgowALjLPEBVyCggBDq6qMZxrK0s14AzEZAASF0rm2SKOh0CQCzEVBACLWpCkrn64Wh4bR0FRUUkImAAkJad0+4cUV33MO4IyAbAgoIaVVqme/6O4INfr1h3WKGxgI5EFBABL79z52+l+n0+m89vEoAZMc0cyBCb7yYkHd+3+953a13t00v7dXWcyQbyhfHbQAFpsNjdb6ejjBKPxdKw0iX87q6W0M3VgClgIACikhn7CXHzqXCqYpqCcjgFVD8PwbIIw0lggkIhiYJAICV+NYOyIN9u0QaFqReC2d+PpXaiho/JXL6qEhnt0hdowDwQEABeTDQk3rl+JwGFABvLPEBAKxEQAEArERAAQCsREABAKxEQAEArEQXH5AHtbSRA6Ex6ggAUBReo45Y4gMAWImAAgBYiT0oxJZOCu9PjMnAwbHpHyM4PTyxc20Tg21hFf5rRCztfu2QvLOjn2CKkIbTV+5qk3X3rhDABjRJIFY0kF798f7pwwCRH1pNPbx5LdUU8o4mCZQUrZwIp/zSJdPtWz4RoNgIKMRGYu/I9LIe8m//e8fl4F6+EUBxEVCIjb/sHhQUzu5fHxKgmAgoxMaR1NITCmeArzeKjF1QxIbJDVM39pd21k//2GuJSq+bv6hWBhKn5eTQhGA2OiRRbAQUSsa6e1bIHfdcbpEeHkzKi0/uvSJ8NMTuf3KNrLqu6dLHdPnwjRcPclMGLMISH0rCDesWzwon1by4Vh7+8dorrv3Ww52zwsl5/1fubhMA9iCgUBJuWLco68c1pK4Mo+zX3kpAAVYhoFASmlN7SbmkP3Bqeh2A4iOgUBJ69hzP+bmBg6cv/Xh4KJlzn0n3rADYg4BCSXjn9/1ZO/F08kTmx3NNSdAmCQD2YE0DJUEroxef2DvdKKGz5MZTVdL+VFWVbfKETklwrtU9Kq2c/vBrRigBtmFYLGLjybvfFhTWj39/mwD5wrBYAEAsEVAAACsRUAAAKxFQAAArEVAAACsRUAAAKxFQAAArEVCIjfmLagSF4za3ECgEAgqxoUdioHBWrW0SoJgIKMTGDXcsYuJ4Aa3LOF8LKDQCCrGhc/PWbeSmWQgaTiypotgIKMTKV/5HG9/Z55l+fe/gawwLMCwWsTSQGJM3XkrIwb1MII+K7jlpOGWeQAzki9ewWAIKsaaHDw4cHBMEp/t6zanlPPb3UGgEFADAShy3AQCIJWp6wND4qdyfq65J/Z8pQNPb1ITI5ETuz9c1ClC2CCjA0Ec7RYYPZ//ctd8QaesS34Z6U7/uruyf03C67UEByhZLfAAAKxFQAAArEVAAACsRUAAAKxFQAAArEVAAACsRUAAAKxFQAAArEVAAACsRUAAAKxFQAAArEVAAACsRUAAAKzHNHCVBj6043CNy+qj7sRhhjB6TyLkd0aHHcPx5u4T6tRetDjZlHbABAYXY608F08f/PhNSxRL03Ca39+nfJ9fxHqaOJkR63xP54j+KLFwtQKywxIdY690zc55SMcNJNSyUQPR9QQ469COZqig/fD0V5PsFiBUCCrE1mlrOS7wnRadLaGFCZsWXpCA+/lP+lj+BfCCgEFu5TqItJF2iW32zhLLy+sIc7a5Vpg1fM8AUAYVY0kpAK6hi0lC56Z9EakOGi1Zf+usUIqSG+4q/HAqYIqAQS8UMJw0UXdbrvjd8ODlqL4Zd2xrJO5b5EBd08SGW3KoAbTzI176OhlNLe34aGzSkrl0v8oWvzgRwmCBx62qkgkJcEFAoOU6FE1f6529OhWCzBKfdjQQR4o4lPgCAlQgoAICVCCgAgJUIKACAlQgoAICV6OJDLOlDrbk69eoapOSc6JtpO9e/t7a5e9Ep5lNns38uSIu8398fiEJF6rXS7YILFy58KgCKQkPhr6/PfjB5adfMdPJ8D5l16JEf6VPVnYeKCzH5AqWtoqKiw+3zLPEBFvto55VTMwZ6RD77QApCB8xmHvmh09GZ6YdCIKAAS7mdB3XoQymIoUT2jw/3MTIJ+UdAAZaadJkEUagpEUlCCEVEQAEArERAAQCsRJs5rJAcm5LhwQkZT/2z0FoW18r8RQVqiTM0PDQhA58k5eRw7msSeyXv3H7/zw+INLTMkaWd9QLkA23mKLq/7B6SN15MTIdUsXzl7jb51sOdUmz////2yzs7DsvJofiMIm9eVCt33LNCbli3SAA/vNrMCSgU1fBgUp775z+LDfQmuy71KgYN51d/vF8O7huRuFq6ql7uf7LLumoU9uI5KFht968PiS3e+X2/FMuLT+yNdTipgYNjqZDtESAqBBSKyqalrGItMf5l9+D0zb0U6N/Dpm86EG8EFFBkpXZDL2YlitJCQAFFNJAYi1VDhAmtREulIkRx0WaOopq/uEbmD5Xvprqftvra+jmpV5UUS3LsnPEyqAavNk0AYRBQKKoNj14j5UBHE/3tTyJHE7PHFB3pT3q+99a726Y7DDWgik27Lj/4w5DnsqQOkz2+b/bHdAr76puZgg5zBBRQAO++Fmyunba931Gk1vdsmhfPPPNUO3eOvPFSwtd7dQr76aMi3fcKYIQ9KCDPhnqDhZNWTDaFU7ob/vuiQBWdHh2iXw/ABBUUiqq/x/zYhtXdEojeEDPPVIr693Bj+ntnqrNgSS8XZz8sSGu+fj30xF/ACwGFotKAynXmUaYwATWw3+zafAQUey6z8fWAKZb4gDzTaoGb8gz9OrR1CWCEgALybE6NyE3/JNLcLmVN//76dQBMscQHFEBt48zNWVvM09vM//Ntkb8ZLj9meutls+talotc+43LP//rDpFThvtitz8kgXzhqyL/cOvsj9VSRcInAgooIK2m5qQ9l1wV4hnl5KjZdZlNKHqUvOl7g5pzFYGE8FjiAwBYiQoKsXGiTwIZz3O14Ie2WE+mLfGdDtiCbjut2jL/fbWU+R4c/COgEBvv/1ZiS4Ppw9evfGD3SIkO/k68J3ImYxqSsw9HRyNMscQH5JlWE9nCqdzo3//Pv53dJAK4IaCAPBvuI5wc+nXo59BdGCKggDwbJ5xmmaSCgiECCkVVDq3INAfM1rBQACMEFIqq2qKzCufk6c+iExTKfYqEQ8OJQbEwRRcfisqm76bz+Wf50rdFet+b2Y9KX+KqPiGBmVafmcGr76vNc+t9dZYHdZd1iaz4kgDGCCgUlX43raev2qBtjeSNhsQX/vHKj7+/W2TvBxLI7Q9KINetl7y75qsiN6wTIBSW+FBUeuO2ZfmLvSLALgQUim71zVJ0egQEs+MAu7DEh6JzmgiGA44yikIhQlIfUD3cM/tB1aMhjj/v3WN2XeYZTIU6xbi3/vLPtVLmXCz4RUAhL/QmrM0Apjck3Z/Z85oUhd6E8109aSh8/O9XTlE4GmLUkY4TMqHhnxlQ+T7F+GhCpGp89scOpP7+167P714fSgsBBV+cG6xbS7ZWBft2zVyrN37tYPPqkNPPa0jpTbyQ9PftzHP1pNWKLY0gxfbxn1KVVGf+WvpRWtiDgjFdUvp//2vm9eGO3DPV/pZWKTjz10ysuD74d+xBaDgV4oTXoRDLeKVG/7tg1BFMEVAwojfZ9CUlXcLJtgei1ULm3Dm9KZnueWg1U4iQco4f5zv5wmPUEUyxxAcjo1nOLTLdx/BLQ0r3KbTyinqOnQaSBmAhHxhtZLTPLLTzwxQVFALLtsSXa3SR3w493bu67cHUpvo3onlOygkmfbi10NMM9M9fyKVLm+nXgbFPMEUFBSN1Psbq1DaklvkyRumMBjw5VrvP9JW8eEKr/jp6Qq7XmUL659CwnLdgpoIp9k1Rq0L9ugwlZv/ZxyZT/7NfSo7+95L+Ndefa3PEQubwwQcCCoHp8pvebDP3cfTGNJBx0+3fP/Pdc9A9n9qMZ3niyAnbdBW7Rf6jwJ2LhaCBzKgjhMUSH4zkqkCyVUbLsgSJBlmv4XM7AKAIKBjRJZps1c+JLHtLGmbZrj30wUwlBQAmWOKDMX1uKLPZIVcnn44OyvbQrT6oqUcxlONehIb5QM9Mm7WzD3XwgARm+gxX5jcLX/iq9x5eWJ+lvhk5P3z5586eVNyXaVFYBBSMNS/LElAXGxcyJ0XoQ7daLWUuAeqN8cPXZz6v3XTlMptNnxnLNpooOSKBBW38KMQZXKePpf7bqL78c/1GRv970H1LOhphiiU+GMv13W+uSQk64ihXU4Qu9+nsPR2JpO/X6iLqZ55soX+vBPtv0xLvFXcoMOKFCgrGnGWazBvMoQ9nwiuzGtLOO32O6a+vZ//1tJrSJa+BntnvMZndFyeMOppNq2qehYIJKij4km0StQZNriGvesTCf/u2eXu5Pu/04euCGEiOnRMgnwgo+JKtUlI6wTxXh56G1C33mu83aUiV0nKftt0Hef5reCgpB/eG2KTKo+HBZCqgpiSIRTysC0MEFHzTZbtstEMvV7Ckjy7yCir9fCk1T2g46VEiQfz2+U/k5JBd01U1nF58cq8EoR2EnFwMUxWp10q3Cy5cuPCpABl0kGu2zW69+Wj7s1fATB+a1zfT3ZUeanoz1/eX0h6UQyvDwd7ZLd6f/OegvPX6J57vXXNzqyztrJdi07Ds2XPcs3q6/dvXyNX/sOjSz/Xf6+LVhBNmq6io6HD9vBBQCEBvtu++lv15GtOQckxNzD4IsZyOwBhIjMkvN30gpebhzWtl1XVNArjxCiiW+BCI06GXjYaXtpCbHkw3PWC2ceZVbuczNS+ukdr60mumXbqq+NUe4o+AQmC62Z3roUutiPSYc33OqVSfb4qChtOam1uklNywbnFJhi4Kj4BCKF4n4OozTm//iqBys+6eFSVzQ9e/h/59gCiwB4VI6ISAbEfAZ9JxSTqHT09VLcVGiKC08WDr5vhP0r3/iTWyprtVABM0SaBgdM9JH9j1M4hUD/FzmikaFs0MmS23fSiH075tW1u5iVVrm+Rb3+u0otMQ8UFAoaC0QUKX84LOW9PqSkcdlbOD+0YksXdEjn4+Mf0AdC7psxGdE4ezyTzddqg39zcRWtWmV7ZujS7676qhpUq6UhUTHXsIgoBCUeiNTZf9guw7feNRgcx87XT/Lpf0r5N+vbUpJRsNp/SjOf68PfcxKZl7iruel5z0wetymUaP/PAKKFptkBfO8eZhggpAeSOgkFdOUOmSny4t6Xfu2Y6Jd3BWEAAHAYWC0GUmZx9E9z80pMYzhsK2tHMMQyG4NaFU11x57VT8ejZQIggoFJze9KYDS+CmrjF3QGQGudteUOYRKdoEcTSR/drM1n/9ebaGl1Ib6As78aAuYDFtu8+koXVdxpgpDaxsZ3VpiLRkhNnK67OHy4rrrww+ncKereLSa4F8o4sPsJw2muhZW7ocqhWNhlauh5zTr9VzqFZ8KXvAaFu6Plg9emzm8xpinTdn/zUzr9Xfn6VYRIE2cwCAlZhmDgCIJQIKAGAlAgoAYCUCCgBgJQIKAGAlAgoAYCUCCgBgJQIKAGAlAgoAYCUCCgBgJQIKAGAlAgoAYCUCCgBgJQIKAGAlAgoAYCUCCgBgJQIKAGAlAgoAYCUCCgBgJQIKAGAlAgoAYCUCCgBgJQIKAGAlAgoAYCUCCgBgJQIKAGAlAgoAYCUCCgBgJQIKAGAlAgoAYCUCCgBgJQIKAGAlAgoAYCUCCgBgJQIKAGAlAgoAYCUCCgBgJQIKAGAlAgoAYCUCCgBgJQIKAGAlAgoAYCUCCgBgJQIKAGAlAgoAYCUNqPNuF0xOTvYJAAAROn/+/CmvSzwDCgCAqJ07d84ooNyv8E45AAB8Ma2gzrldYZByAAD4YlpBuS7xJZNJ9qAAAJGampoKH1CTk5NUUACASI2Pj3sVPxc0oKbcrhgdHaWCAgBEyiBbpjwDanh4mIACAETq2LFj4QMqkUgQUACASH322WfhA2rHjh0EFAAgUhs3buzxuGSq4uIPlovL2KOzZ8++XV1d3S4AAIQ0MTHRU1tbe6fHZZ85oeRaRY2Pj3slHQAARlIB5bUyd1b/xwmoSbcrR0ZGCCgAQCQMMmW6aHIC6qzblUeOHCGgAACR2L9//x6PSyb0f4wC6vnnn/f6xQAAMPLKK694FT3TmeQ0SWhQLXe9mkYJAEBIhg0Sn0vaNHMdd+TaKHHixImdAgBACGNjYybV0/QIvvTW8nG3dxw/fpx9KABAKAcOHPAqdi5tOVVm+2A2mzdvpoICAITy1ltveRU7l4qlirQPeu5DpUqzbXPnzu0WAAB8OnPmzJ76+vqNHpcdlow2c6Vrfq5VVGqZj24+AEAg/f39Jst7l/ohMscbTbi9M7V2SEABAAJ5//33vTIkmf6TioxP1qZei8UF7eYAAL8mJyf7rrrqqts8LhuUtJDKrKD0E64n7B45cmS7AADgg8GjSrq0N6uCyjbBfExcbNu2jYACAPjyyiuvvOxxSTLzAxVZLvJc5qObDwBgyrB7b9bynspWQXku8xl0YgAAMC2RSHitvF2xvKeqclw8J/WqyfE5XUvs/c53vnNfRUVFzmsAANDmiLa2tsc8LjsjWaYZVbpcnNPWrVtPHT582Gs9EQBQ5kZHR00eTxrN9sFcAaWlluszUTRLAAC8vPTSS1s8LjkrOYZEVLi8qTH1anb5PM0SAICcUttB21tbW72W946nXqezfaLS5U36BtdmiXfffdcrGQEAZcqgetLmiNO5PulWQan5qVeT2wVUUQCATIbVk4bT8VyfdKug1CmPz1NFAQCuYFA9qRG3T3pVUKo19ZrndgFVFADAEUX1pLwqKDXidcELL7zg9QcBAJSJKKonZVJBqSXi8uCuOnLkyFOLFy9+SAAAZSuq6kmZVFDqmNcFP/jBD7acP3/ec88KAFCadGpEVNWTMq2glOdeVG9v70OdnZ1PCQCg7Ozbt++xtWvXeg1xMKqelJ+A0vl8S8Wj6qJhAgDKj+GBhOqwpB3r7sZ0iU8u/oKjXhft2LHjRwIAKCtbtmzxOk5DnRTDcFJ+KiilgaZV1By3iw4dOrRp+fLlmwQAUPI+//zzLStWrDCZGjEgHhOK0vkNKDU39VrodVEymfy3mpqaLgEAlCwfS3s5Z+7l4meJz6FHcUx4XfSLX/ziX+jqA4DSpfd4w6W90+IznFSQCkoZNUzQ1QcApevvf//7j66++mqvswF1SU+X9oz3nhxBKii5+Bt59rGvXr365cHBQQ42BIASo/d2g3BSupLmO5xU0IByflPPpT59gHdiYqJHAAAlQfed9N5ucKkefmv0UG42VRKO/ub68G7OpcK9e/dONDY2/umWW27ZUFFRUSMAgNjScNJ9p5/97GdHPS7Vpb0h8dG1lynoHlQ6z5N31Ztvvtm9bt26bQIAiK3f/OY3d27cuNFkVcx3116mKAJKaUA1el300Ucfbejq6npOAACxY9gUoXQLaFhCCrMHlU7XGD03wa699trt+kCXAABiRe/dhuFk1ERnIqqA0jXGQTFYa9SnjQkpAIgPw0kRyjgLTEQVUEpT02hCrf5FaT8HAPv5CCelGRCopTybqPag0s1PvZpMLhweHn5u/vz5GwQAYB3DwwcdOgg2kqU9R5QVlEP/kEYjjpqbmx9juQ8A7KOrXD7CSe/5kYaTykdAKe3e8HyIV7EnBQB20XvykiVLTI9O0vmsoTv2sslXQCl9QOusyYWEFADYweeek97jjXoPgshnQGkXhz5pbLRhpl+QRCLBYYcAUCR6ZLuPcNJ7u97jI+nYyyafAaX0LzAohiGlw2X1KWUdpSEAgILQYzN27ty5ce3atdsN3+Lr3h5U2Fl8JjRdx2XmoEPPQNy+ffvRhoaGnV/+8pfXV1VVeU6nAAAEp8O8dbbed7/7XdOh3gUJJ1WIgFK+QmrXrl2nUst922+99daaefPmXS8AgMhpp94jjzzymMHgV0fBwknl4zkoN3rQoR4Xf5XpG/TQw46Ojk2VlZVUUwAQAV3SSxUBpqOLHNoQYdxXEIV870FlctJ33PQNui/105/+lH0pAIiALunpPdVnOOnRSgWrnByFrqDSGU1AT3fo0KFNy5cv3yQAAN98tpA7IplMHkShK6h0+hf29eSxfmF/8pOf3EY1BQDmzpw5s0c7pAOEk04GKko4qWJWUA5tnGgVn2GpZ0ulStRN1dXV7QIAuELAvabpt8rMA7hnpIiKWUE59AswID7XNvVsKW2N1GGGAgCYRTv0HnjggdsChJPei/WeXNRwUjZUUA4NS52C7rtb79lnn23/3ve+t6mlpYXJ6ADKmi7nvfDCC489/vjjQbZCnKGveZsO4YdNAeWYJzMNFL6rO4IKQLnSYHr77be3fPOb39wj/mkg6X7TqFjExoBS+rzUgtSrRgIgqACUi5DBpLSFPNKDBqNia0A59PDDBgm4V+YEVUNDQzfNFABKhTY/HD58+OVt27ZtD7iUN/3LiIVVUzrbA0ppNaV7U/MkBO366+jo2DB37txuAYAY0mqpv79/59NPP71969atRgfD5mBt1ZQuDgHl0IDSoJojIWhV9cADDzyUWv5bT1UFwHb63OeRI0e2v/rqq9ufeOKJsM+AaiDpc01F79AzEaeAUrrUp11+TRKBVHncdeONN3a3tbWtp7ICYAutlI4fP75n//79e0LsLaXT5TytuEbFkg49E3ELKEcky37ptLK6/fbbu6655pr19fX1XTU1NV0CAAWgVdKJEyd2Hj16tOeZZ57ZGXL5LtNpmWkdt3o5L5u4BpQj8qBy3HfffY2ppcCuNWvWdDc1NWlgtRNaAMLSMBofH+8ZGRnpOXz4cM8vf/nLPREHkiMW+0xu4h5QjrwFVSZdFly5cmX7ggUL2hsaGtrr6ura58yZ06iHK+qRIM4/BUBZcWaEaofduXPnTiWTyb7Ux06Njo72HTt2rO+zzz7r27FjR1+ewihdbCumTKUSUA4nqGolZDMFAMRILPeYvJRaQKXTakqfoTI+HBEAYkaX8fR8Pa2aSiaYHKUcUA4NKA0qqioApUCDSANJgykpJawcAiqdhlS9EFYA4qVsQilduQVUOg2puov/ZBkQgG3OyuUlvLIJpXTlHFDptJrSkKq7+E8CC0ChadedTniYvPjPkttT8ouAyk4nVjhBpRVWlRBaAKKhwaNhpEF0Nu1V9oGUiYDyZ06WV+XFV0Xaj204qRhAYZ2XyyFzLu3nU1leMPBf2AatVWx9ge0AAAAASUVORK5CYII=",
"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",
"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/data/upgrade/3.6.4/schema_update.sql b/application/src/main/data/upgrade/3.6.4/schema_update.sql
index af354acf62..6fb1e35156 100644
--- a/application/src/main/data/upgrade/3.6.4/schema_update.sql
+++ b/application/src/main/data/upgrade/3.6.4/schema_update.sql
@@ -145,3 +145,18 @@ DELETE FROM asset WHERE type='TbServiceQueue';
DELETE FROM asset_profile WHERE name ='TbServiceQueue';
-- QUEUE STATS UPDATE END
+
+-- MOBILE APP SETTINGS TABLE CREATE START
+
+CREATE TABLE IF NOT EXISTS mobile_app_settings (
+ id uuid NOT NULL CONSTRAINT mobile_app_settings_pkey PRIMARY KEY,
+ created_time bigint NOT NULL,
+ tenant_id uuid NOT NULL,
+ use_default_app boolean,
+ android_config VARCHAR(1000),
+ ios_config VARCHAR(1000),
+ qr_code_config VARCHAR(100000),
+ CONSTRAINT mobile_app_settings_tenant_id_unq_key UNIQUE (tenant_id)
+);
+
+-- MOBILE APP SETTINGS TABLE CREATE END
\ No newline at end of file
diff --git a/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java b/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java
index 60f69cd8a7..6ec00b5661 100644
--- a/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java
+++ b/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java
@@ -15,24 +15,32 @@
*/
package org.thingsboard.server;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.ComponentScan;
+import org.springframework.core.Ordered;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
+import org.thingsboard.server.queue.util.AfterStartUp;
import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
@SpringBootConfiguration
@EnableAsync
@EnableScheduling
@ComponentScan({"org.thingsboard.server", "org.thingsboard.script"})
+@Slf4j
public class ThingsboardServerApplication {
private static final String SPRING_CONFIG_NAME_KEY = "--spring.config.name";
private static final String DEFAULT_SPRING_CONFIG_PARAM = SPRING_CONFIG_NAME_KEY + "=" + "thingsboard";
+ private static long startTs;
+
public static void main(String[] args) {
+ startTs = System.currentTimeMillis();
SpringApplication.run(ThingsboardServerApplication.class, updateArguments(args));
}
@@ -45,4 +53,11 @@ public class ThingsboardServerApplication {
}
return args;
}
+
+ @AfterStartUp(order = Ordered.LOWEST_PRECEDENCE)
+ public void afterStartUp() {
+ long startupTimeMs = System.currentTimeMillis() - startTs;
+ log.info("Started ThingsBoard in {} seconds", TimeUnit.MILLISECONDS.toSeconds(startupTimeMs));
+ }
+
}
diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
index 6c90e6e27d..a3111cb137 100644
--- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
+++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
@@ -76,7 +76,7 @@ public class ThingsboardSecurityConfiguration {
public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login";
public static final String PUBLIC_LOGIN_ENTRY_POINT = "/api/auth/login/public";
public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token";
- protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[]{"/index.html", "/assets/**", "/static/**", "/api/noauth/**", "/webjars/**", "/api/license/**", "/api/images/public/**"};
+ protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[]{"/index.html", "/assets/**", "/static/**", "/api/noauth/**", "/webjars/**", "/api/license/**", "/api/images/public/**", "/.well-known/**"};
public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**";
public static final String WS_ENTRY_POINT = "/api/ws/**";
public static final String MAIL_OAUTH2_PROCESSING_ENTRY_POINT = "/api/admin/mail/oauth2/code";
diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java
index 2898689760..571f32d171 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java
@@ -256,7 +256,9 @@ public class AuthController extends BaseController {
}
}
- return tokenFactory.createTokenPair(securityUser);
+ var tokenPair = tokenFactory.createTokenPair(securityUser);
+ systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(request), ActionType.LOGIN, null);
+ return tokenPair;
}
@ApiOperation(value = "Reset password (resetPassword)",
diff --git a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java
index 5271de2329..6967b3c13b 100644
--- a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java
@@ -68,9 +68,9 @@ public class ComponentDescriptorController extends BaseController {
@RequestMapping(value = "/components/{componentType}", method = RequestMethod.GET)
@ResponseBody
public List getComponentDescriptorsByType(
- @Parameter(description = "Type of the Rule Node", schema = @Schema(allowableValues = "ENRICHMENT,FILTER,TRANSFORMATION,ACTION,EXTERNAL", requiredMode = Schema.RequiredMode.REQUIRED))
+ @Parameter(description = "Type of the Rule Node", schema = @Schema(allowableValues = {"ENRICHMENT", "FILTER,TRANSFORMATION", "ACTION,EXTERNAL"}, requiredMode = Schema.RequiredMode.REQUIRED))
@PathVariable("componentType") String strComponentType,
- @Parameter(description = "Type of the Rule Chain", schema = @Schema(allowableValues = "CORE,EDGE"))
+ @Parameter(description = "Type of the Rule Chain", schema = @Schema(allowableValues = {"CORE", "EDGE"}))
@RequestParam(value = "ruleChainType", required = false) String strRuleChainType) throws ThingsboardException {
checkParameter("componentType", strComponentType);
return checkComponentDescriptorsByType(ComponentType.valueOf(strComponentType), getRuleChainType(strRuleChainType));
@@ -85,7 +85,7 @@ public class ComponentDescriptorController extends BaseController {
public List getComponentDescriptorsByTypes(
@Parameter(description = "List of types of the Rule Nodes, (ENRICHMENT, FILTER, TRANSFORMATION, ACTION or EXTERNAL)", required = true)
@RequestParam("componentTypes") String[] strComponentTypes,
- @Parameter(description = "Type of the Rule Chain", schema = @Schema(allowableValues = "CORE,EDGE"))
+ @Parameter(description = "Type of the Rule Chain", schema = @Schema(allowableValues = {"CORE", "EDGE"}))
@RequestParam(value = "ruleChainType", required = false) String strRuleChainType) throws ThingsboardException {
checkArrayParameter("componentTypes", strComponentTypes);
Set componentTypes = new HashSet<>();
diff --git a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java
index a46cbe7dba..9ee8ae6cf8 100644
--- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java
@@ -164,7 +164,7 @@ public class CustomerController extends BaseController {
@RequestParam int page,
@Parameter(description = CUSTOMER_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
- @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "title", "email", "country, city"}))
+ @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "title", "email", "country", "city"}))
@RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java
index 8b33aeeb15..783b451a6d 100644
--- a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java
@@ -229,7 +229,7 @@ public class EntityViewController extends BaseController {
@RequestParam(required = false) String type,
@Parameter(description = ENTITY_VIEW_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
- @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name, type"}))
+ @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name", "type"}))
@RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
@@ -293,7 +293,7 @@ public class EntityViewController extends BaseController {
@RequestParam(required = false) String type,
@Parameter(description = ENTITY_VIEW_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
- @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name, type"}))
+ @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name", "type"}))
@RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java b/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java
new file mode 100644
index 0000000000..626f6f3b0f
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java
@@ -0,0 +1,188 @@
+/**
+ * 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.controller;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import io.swagger.v3.oas.annotations.Parameter;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RestController;
+import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.mobile.AndroidConfig;
+import org.thingsboard.server.common.data.mobile.IosConfig;
+import org.thingsboard.server.common.data.mobile.MobileAppSettings;
+import org.thingsboard.server.common.data.security.model.JwtPair;
+import org.thingsboard.server.config.annotations.ApiOperation;
+import org.thingsboard.server.dao.mobile.MobileAppSettingsService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.mobile.secret.MobileAppSecretService;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.security.permission.Operation;
+import org.thingsboard.server.service.security.permission.Resource;
+import org.thingsboard.server.service.security.system.SystemSecurityService;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER;
+import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH;
+
+@RequiredArgsConstructor
+@RestController
+@TbCoreComponent
+public class MobileApplicationController extends BaseController {
+
+ @Value("${cache.specs.mobileSecretKey.timeToLiveInMinutes:2}")
+ private int mobileSecretKeyTtl;
+
+ public static final String ASSET_LINKS_PATTERN = "[{\n" +
+ " \"relation\": [\"delegate_permission/common.handle_all_urls\"],\n" +
+ " \"target\": {\n" +
+ " \"namespace\": \"android_app\",\n" +
+ " \"package_name\": \"%s\",\n" +
+ " \"sha256_cert_fingerprints\":\n" +
+ " [\"%s\"]\n" +
+ " }\n" +
+ "}]";
+
+ public static final String APPLE_APP_SITE_ASSOCIATION_PATTERN = "{\n" +
+ " \"applinks\": {\n" +
+ " \"apps\": [],\n" +
+ " \"details\": [\n" +
+ " {\n" +
+ " \"appID\": \"%s\",\n" +
+ " \"paths\": [ \"/api/noauth/qr\" ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ "}";
+
+ public static final String ANDROID_APPLICATION_STORE_LINK = "https://play.google.com/store/apps/details?id=org.thingsboard.demo.app";
+ public static final String APPLE_APPLICATION_STORE_LINK = "https://apps.apple.com/us/app/thingsboard-live/id1594355695";
+ public static final String SECRET = "secret";
+ public static final String SECRET_PARAM_DESCRIPTION = "A string value representing short-lived secret key";
+ public static final String DEFAULT_APP_DOMAIN = "demo.thingsboard.io";
+ public static final String DEEP_LINK_PATTERN = "https://%s/api/noauth/qr?secret=%s&ttl=%s";
+
+ private final SystemSecurityService systemSecurityService;
+ private final MobileAppSecretService mobileAppSecretService;
+ private final MobileAppSettingsService mobileAppSettingsService;
+
+ @ApiOperation(value = "Get associated android applications (getAssetLinks)")
+ @GetMapping(value = "/.well-known/assetlinks.json")
+ public ResponseEntity getAssetLinks() {
+ MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID);
+ AndroidConfig androidConfig = mobileAppSettings.getAndroidConfig();
+ if (androidConfig != null && androidConfig.isEnabled() && !androidConfig.getAppPackage().isBlank() && !androidConfig.getSha256CertFingerprints().isBlank()) {
+ return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(ASSET_LINKS_PATTERN, androidConfig.getAppPackage(), androidConfig.getSha256CertFingerprints())));
+ } else {
+ return ResponseEntity.notFound().build();
+ }
+ }
+
+ @ApiOperation(value = "Get associated ios applications (getAppleAppSiteAssociation)")
+ @GetMapping(value = "/.well-known/apple-app-site-association")
+ public ResponseEntity getAppleAppSiteAssociation() {
+ MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID);
+ IosConfig iosConfig = mobileAppSettings.getIosConfig();
+ if (iosConfig != null && iosConfig.isEnabled() && !iosConfig.getAppId().isBlank()) {
+ return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(APPLE_APP_SITE_ASSOCIATION_PATTERN, iosConfig.getAppId())));
+ } else {
+ return ResponseEntity.notFound().build();
+ }
+ }
+
+ @ApiOperation(value = "Create Or Update the Mobile application settings (saveMobileAppSettings)",
+ notes = "The request payload contains configuration for android/iOS applications and platform qr code widget settings." + SYSTEM_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
+ @PostMapping(value = "/api/mobile/app/settings")
+ public MobileAppSettings saveMobileAppSettings(@Parameter(description = "A JSON value representing the mobile apps configuration")
+ @RequestBody MobileAppSettings mobileAppSettings) throws ThingsboardException {
+ SecurityUser currentUser = getCurrentUser();
+ accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.WRITE);
+ mobileAppSettings.setTenantId(getTenantId());
+ return mobileAppSettingsService.saveMobileAppSettings(currentUser.getTenantId(), mobileAppSettings);
+ }
+
+ @ApiOperation(value = "Get Mobile application settings (getMobileAppSettings)",
+ notes = "The response payload contains configuration for android/iOS applications and platform qr code widget settings." + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @GetMapping(value = "/api/mobile/app/settings")
+ public MobileAppSettings getMobileAppSettings() throws ThingsboardException {
+ SecurityUser currentUser = getCurrentUser();
+ accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.READ);
+ return mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID);
+ }
+
+ @ApiOperation(value = "Get the deep link to the associated mobile application (getMobileAppDeepLink)",
+ notes = "Fetch the url that takes user to linked mobile application " + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @GetMapping(value = "/api/mobile/deepLink", produces = "text/plain")
+ public String getMobileAppDeepLink(HttpServletRequest request) throws ThingsboardException, URISyntaxException {
+ String secret = mobileAppSecretService.generateMobileAppSecret(getCurrentUser());
+ String baseUrl = systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, null, request);
+ String platformDomain = new URI(baseUrl).getHost();
+ MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID);
+ String appDomain;
+ if (!mobileAppSettings.isUseDefaultApp()) {
+ appDomain = platformDomain;
+ } else {
+ appDomain = DEFAULT_APP_DOMAIN;
+ }
+ String deepLink = String.format(DEEP_LINK_PATTERN, appDomain, secret, mobileSecretKeyTtl);
+ if (!appDomain.equals(platformDomain)) {
+ deepLink = deepLink + "&host=" + baseUrl;
+ }
+ return "\"" + deepLink + "\"";
+ }
+
+ @ApiOperation(value = "Get User Token (getUserTokenByMobileSecret)",
+ notes = "Returns the token of the User based on the provided secret key.")
+ @GetMapping(value = "/api/noauth/qr/{secret}")
+ public JwtPair getUserTokenByMobileSecret(@Parameter(description = SECRET_PARAM_DESCRIPTION)
+ @PathVariable(SECRET) String secret) throws ThingsboardException {
+ checkParameter(SECRET, secret);
+ return mobileAppSecretService.getJwtPair(secret);
+ }
+
+ @GetMapping(value = "/api/noauth/qr")
+ public ResponseEntity> getApplicationRedirect(@RequestHeader(value = "User-Agent") String userAgent) {
+ if (userAgent.contains("Android")) {
+ return ResponseEntity.status(HttpStatus.FOUND)
+ .header("Location", ANDROID_APPLICATION_STORE_LINK)
+ .build();
+ } else if (userAgent.contains("iPhone") || userAgent.contains("iPad")) {
+ return ResponseEntity.status(HttpStatus.FOUND)
+ .header("Location", APPLE_APPLICATION_STORE_LINK)
+ .build();
+ } else {
+ return ResponseEntity.status(HttpStatus.NOT_FOUND)
+ .build();
+ }
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java
index 96f38daf35..fb32ebd30b 100644
--- a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java
@@ -106,8 +106,6 @@ public class NotificationController extends BaseController {
private final NotificationCenter notificationCenter;
private final NotificationSettingsService notificationSettingsService;
- private static final String DELIVERY_METHOD_ALLOWABLE_VALUES = "WEB,MOBILE_APP";
-
@ApiOperation(value = "Get notifications (getNotifications)",
notes = "Returns the page of notifications for current user." + NEW_LINE +
PAGE_DATA_PARAMETERS +
@@ -175,7 +173,7 @@ public class NotificationController extends BaseController {
@RequestParam(required = false) String sortOrder,
@Parameter(description = "To search for unread notifications only")
@RequestParam(defaultValue = "false") boolean unreadOnly,
- @Parameter(description = "Delivery method", schema = @Schema(allowableValues = {DELIVERY_METHOD_ALLOWABLE_VALUES}))
+ @Parameter(description = "Delivery method", schema = @Schema(allowableValues = {"WEB", "MOBILE_APP"}))
@RequestParam(defaultValue = "WEB") NotificationDeliveryMethod deliveryMethod,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
// no permissions
@@ -188,7 +186,7 @@ public class NotificationController extends BaseController {
AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@GetMapping("/notifications/unread/count")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
- public Integer getUnreadNotificationsCount(@Parameter(description = "Delivery method", schema = @Schema(allowableValues = {DELIVERY_METHOD_ALLOWABLE_VALUES}))
+ public Integer getUnreadNotificationsCount(@Parameter(description = "Delivery method", schema = @Schema(allowableValues = {"WEB", "MOBILE_APP"}))
@RequestParam(defaultValue = "MOBILE_APP") NotificationDeliveryMethod deliveryMethod,
@AuthenticationPrincipal SecurityUser user) {
return notificationService.countUnreadNotificationsByRecipientId(user.getTenantId(), deliveryMethod, user.getId());
@@ -211,7 +209,7 @@ public class NotificationController extends BaseController {
AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PutMapping("/notifications/read")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
- public void markAllNotificationsAsRead(@Parameter(description = "Delivery method", schema = @Schema(allowableValues = {DELIVERY_METHOD_ALLOWABLE_VALUES}))
+ public void markAllNotificationsAsRead(@Parameter(description = "Delivery method", schema = @Schema(allowableValues = {"WEB", "MOBILE_APP"}))
@RequestParam(defaultValue = "WEB") NotificationDeliveryMethod deliveryMethod,
@AuthenticationPrincipal SecurityUser user) {
// no permissions
diff --git a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java
index c0a9d6d26f..325cc3b025 100644
--- a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java
+++ b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java
@@ -68,7 +68,7 @@ public class OAuth2Controller extends BaseController {
@Parameter(description = "Platform type to search OAuth2 clients for which " +
"the usage with this platform type is allowed in the settings. " +
"If platform type is not one of allowable values - it will just be ignored",
- schema = @Schema(allowableValues = "WEB, ANDROID, IOS"))
+ schema = @Schema(allowableValues = {"WEB", "ANDROID", "IOS"}))
@RequestParam(required = false) String platform) throws ThingsboardException {
if (log.isDebugEnabled()) {
log.debug("Executing getOAuth2Clients: [{}][{}][{}]", request.getScheme(), request.getServerName(), request.getServerPort());
diff --git a/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java b/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java
index f2b77c3b7b..ee7bfcc0dc 100644
--- a/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java
@@ -198,7 +198,7 @@ public class OtaPackageController extends BaseController {
@ResponseBody
public PageData getOtaPackages(@Parameter(description = DEVICE_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable("deviceProfileId") String strDeviceProfileId,
- @Parameter(description = "OTA Package type.", schema = @Schema(allowableValues = "FIRMWARE, SOFTWARE"))
+ @Parameter(description = "OTA Package type.", schema = @Schema(allowableValues = {"FIRMWARE", "SOFTWARE"}))
@PathVariable("type") String strType,
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
diff --git a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java
index cb2033b29d..bc12171306 100644
--- a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java
@@ -34,10 +34,13 @@ import org.thingsboard.server.common.data.SystemParams;
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.mobile.MobileAppSettings;
+import org.thingsboard.server.common.data.mobile.QRCodeConfig;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.settings.UserSettings;
import org.thingsboard.server.common.data.settings.UserSettingsType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
+import org.thingsboard.server.dao.mobile.MobileAppSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
@@ -45,6 +48,7 @@ import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import java.util.Collections;
import java.util.List;
+import java.util.Optional;
import java.util.stream.Collectors;
@Hidden
@@ -72,6 +76,9 @@ public class SystemInfoController extends BaseController {
@Autowired
private EntitiesVersionControlService versionControlService;
+ @Autowired
+ private MobileAppSettingsService mobileAppSettingsService;
+
@PostConstruct
public void init() {
JsonNode info = buildInfoObject();
@@ -135,6 +142,9 @@ public class SystemInfoController extends BaseController {
DefaultTenantProfileConfiguration tenantProfileConfiguration = tenantProfileCache.get(tenantId).getDefaultProfileConfiguration();
systemParams.setMaxResourceSize(tenantProfileConfiguration.getMaxResourceSize());
}
+ systemParams.setMobileQrEnabled(Optional.ofNullable(mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID))
+ .map(MobileAppSettings::getQrCodeConfig).map(QRCodeConfig::isShowOnHomePage)
+ .orElse(false));
return systemParams;
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
index 8796b8c8a2..097aec6306 100644
--- a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
@@ -138,6 +138,9 @@ public class TenantProfileController extends BaseController {
" \"transportDeviceMsgRateLimit\": \"20:1,600:60\",\n" +
" \"transportDeviceTelemetryMsgRateLimit\": \"20:1,600:60\",\n" +
" \"transportDeviceTelemetryDataPointsRateLimit\": \"20:1,600:60\",\n" +
+ " \"transportGatewayMsgRateLimit\": \"20:1,600:60\",\n" +
+ " \"transportGatewayTelemetryMsgRateLimit\": \"20:1,600:60\",\n" +
+ " \"transportGatewayTelemetryDataPointsRateLimit\": \"20:1,600:60\",\n" +
" \"maxTransportMessages\": 10000000,\n" +
" \"maxTransportDataPoints\": 10000000,\n" +
" \"maxREExecutions\": 4000000,\n" +
diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
index 71350bb1fd..69728b20c3 100644
--- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
+++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
@@ -539,6 +539,18 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
}
}
+ @Override
+ public boolean isOpen(String externalId) {
+ String internalId = externalSessionMap.get(externalId);
+ if (internalId != null) {
+ SessionMetaData sessionMd = getSessionMd(internalId);
+ if (sessionMd != null) {
+ return sessionMd.session.isOpen();
+ }
+ }
+ return false;
+ }
+
private boolean checkLimits(WebSocketSession session, WebSocketSessionRef sessionRef) throws IOException {
var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef);
if (tenantProfileConfiguration == null) {
diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java b/application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java
index 2d5026429a..9eac721dfb 100644
--- a/application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java
+++ b/application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java
@@ -199,6 +199,16 @@ public abstract class BaseApiUsageState {
return getApiUsageState().getEntityId();
}
+ @Override
+ public String toString() {
+ return "BaseApiUsageState{" +
+ "apiUsageState=" + apiUsageState +
+ ", currentCycleTs=" + currentCycleTs +
+ ", nextCycleTs=" + nextCycleTs +
+ ", currentHourTs=" + currentHourTs +
+ '}';
+ }
+
@Data
@Builder
public static class StatsCalculationResult {
diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java
index aef3a4ddcf..60bf529ace 100644
--- a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java
+++ b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java
@@ -17,6 +17,8 @@ package org.thingsboard.server.service.apiusage;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.ListenableFuture;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -67,8 +69,6 @@ import org.thingsboard.server.service.mail.MailExecutorService;
import org.thingsboard.server.service.partition.AbstractPartitionBasedService;
import org.thingsboard.server.service.telemetry.InternalTelemetryService;
-import jakarta.annotation.PostConstruct;
-import jakarta.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -184,6 +184,9 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService
if (newHourTs != hourTs) {
usageState.setHour(newHourTs);
}
+ if (log.isTraceEnabled()) {
+ log.trace("[{}][{}] Processing usage stats from {} (currentCycleTs={}, currentHourTs={}): {}", tenantId, ownerId, serviceId, ts, newHourTs, values);
+ }
updatedEntries = new ArrayList<>(ApiUsageRecordKey.values().length);
Set apiFeatures = new HashSet<>();
for (UsageStatsKVProto statsItem : values) {
@@ -210,6 +213,7 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService
} finally {
updateLock.unlock();
}
+ log.trace("[{}][{}] Saving new stats: {}", tenantId, ownerId, updatedEntries);
tsWsService.saveAndNotifyInternal(tenantId, usageState.getApiUsageState().getId(), updatedEntries, VOID_CALLBACK);
if (!result.isEmpty()) {
persistAndNotify(usageState, result);
@@ -410,6 +414,9 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService
myUsageStates.values().forEach(state -> {
if ((state.getNextCycleTs() < now) && (now - state.getNextCycleTs() < TimeUnit.HOURS.toMillis(1))) {
state.setCycles(state.getNextCycleTs(), SchedulerUtils.getStartOfNextNextMonth());
+ if (log.isTraceEnabled()) {
+ log.trace("[{}][{}] Updating state cycles (currentCycleTs={},nextCycleTs={})", state.getTenantId(), state.getEntityId(), state.getCurrentCycleTs(), state.getNextCycleTs());
+ }
saveNewCounts(state, Arrays.asList(ApiUsageRecordKey.values()));
if (state.getEntityType() == EntityType.TENANT && !state.getEntityId().equals(TenantId.SYS_TENANT_ID)) {
TenantId tenantId = state.getTenantId();
@@ -417,6 +424,8 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService
}
}
});
+ } catch (Throwable e) {
+ log.error("Failed to check start of next cycle", e);
} finally {
updateLock.unlock();
}
@@ -483,7 +492,7 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService
}
}
state.setGaugeReportInterval(gaugeReportInterval);
- log.debug("[{}] Initialized state: {}", ownerId, storedState);
+ log.debug("[{}][{}] Initialized state: {}", tenantId, ownerId, state);
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, ownerId);
if (tpi.isMyPartition()) {
addEntityState(tpi, state);
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..b355ac32e9 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,61 @@ public class DefaultDataUpdateService implements DataUpdateService {
case "3.6.4":
log.info("Updating data from version 3.6.4 to 3.7.0 ...");
updateCustomersWithTheSameTitle();
+ updateMaxRuleNodeExecsPerMessage();
+ updateGatewayRateLimits();
break;
default:
throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion);
}
}
+ private void updateGatewayRateLimits() {
+ 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 -> {
+ boolean updated = false;
+ if (configuration.getTransportDeviceMsgRateLimit() != null && configuration.getTransportGatewayMsgRateLimit() == null) {
+ configuration.setTransportGatewayMsgRateLimit(configuration.getTransportDeviceMsgRateLimit());
+ updated = true;
+ }
+ if (configuration.getTransportDeviceTelemetryMsgRateLimit() != null && configuration.getTransportGatewayTelemetryMsgRateLimit() == null) {
+ configuration.setTransportGatewayTelemetryMsgRateLimit(configuration.getTransportDeviceTelemetryMsgRateLimit());
+ updated = true;
+ }
+ if (configuration.getTransportDeviceTelemetryDataPointsRateLimit() != null && configuration.getTransportGatewayTelemetryDataPointsRateLimit() == null) {
+ configuration.setTransportGatewayTelemetryDataPointsRateLimit(configuration.getTransportDeviceTelemetryDataPointsRateLimit());
+ updated = true;
+ }
+ if (updated) {
+ 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 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/mobile/secret/MobileAppSecretService.java b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileAppSecretService.java
new file mode 100644
index 0000000000..83c1b55c6e
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileAppSecretService.java
@@ -0,0 +1,28 @@
+/**
+ * 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.mobile.secret;
+
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.security.model.JwtPair;
+import org.thingsboard.server.service.security.model.SecurityUser;
+
+public interface MobileAppSecretService {
+
+ String generateMobileAppSecret(SecurityUser securityUser);
+
+ JwtPair getJwtPair(String secret) throws ThingsboardException;
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileAppSecretServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileAppSecretServiceImpl.java
new file mode 100644
index 0000000000..bef31622ff
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileAppSecretServiceImpl.java
@@ -0,0 +1,66 @@
+/**
+ * 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.mobile.secret;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.event.TransactionalEventListener;
+import org.thingsboard.server.cache.TbCacheValueWrapper;
+import org.thingsboard.server.common.data.StringUtils;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.security.model.JwtPair;
+import org.thingsboard.server.dao.entity.AbstractCachedService;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
+import org.thingsboard.server.service.security.system.SystemSecurityService;
+
+import static org.thingsboard.server.service.security.system.DefaultSystemSecurityService.DEFAULT_MOBILE_SECRET_KEY_LENGTH;
+
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class MobileAppSecretServiceImpl extends AbstractCachedService implements MobileAppSecretService {
+
+ private final JwtTokenFactory tokenFactory;
+ private final SystemSecurityService systemSecurityService;
+
+ @Override
+ public String generateMobileAppSecret(SecurityUser securityUser) {
+ log.trace("Executing generateSecret for user [{}]", securityUser.getId());
+ Integer mobileSecretKeyLength = systemSecurityService.getSecuritySettings().getMobileSecretKeyLength();
+ String secret = StringUtils.generateSafeToken(mobileSecretKeyLength == null ? DEFAULT_MOBILE_SECRET_KEY_LENGTH : mobileSecretKeyLength);
+ cache.put(secret, tokenFactory.createTokenPair(securityUser));
+ return secret;
+ }
+
+ @Override
+ public JwtPair getJwtPair(String secret) throws ThingsboardException {
+ TbCacheValueWrapper jwtPair = cache.get(secret);
+ if (jwtPair != null) {
+ return jwtPair.get();
+ } else {
+ throw new ThingsboardException("Jwt token not found or expired!", ThingsboardErrorCode.JWT_TOKEN_EXPIRED);
+ }
+ }
+
+ @TransactionalEventListener(classes = MobileSecretEvictEvent.class)
+ @Override
+ public void handleEvictEvent(MobileSecretEvictEvent event) {
+ cache.evict(event.getSecret());
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretCaffeineCache.java b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretCaffeineCache.java
new file mode 100644
index 0000000000..ce21041d27
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretCaffeineCache.java
@@ -0,0 +1,33 @@
+/**
+ * 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.mobile.secret;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.cache.CacheManager;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.cache.CaffeineTbTransactionalCache;
+import org.thingsboard.server.common.data.CacheConstants;
+import org.thingsboard.server.common.data.security.model.JwtPair;
+
+@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
+@Service("MobileSecretCache")
+public class MobileSecretCaffeineCache extends CaffeineTbTransactionalCache {
+
+ public MobileSecretCaffeineCache(CacheManager cacheManager) {
+ super(cacheManager, CacheConstants.MOBILE_SECRET_KEY_CACHE);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretEvictEvent.java b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretEvictEvent.java
new file mode 100644
index 0000000000..b015291d78
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretEvictEvent.java
@@ -0,0 +1,25 @@
+/**
+ * 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.mobile.secret;
+
+import lombok.Data;
+
+@Data
+public class MobileSecretEvictEvent {
+
+ private final String secret;
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretRedisCache.java b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretRedisCache.java
new file mode 100644
index 0000000000..1ff6be61a3
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretRedisCache.java
@@ -0,0 +1,35 @@
+/**
+ * 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.mobile.secret;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.cache.CacheSpecsMap;
+import org.thingsboard.server.cache.RedisTbTransactionalCache;
+import org.thingsboard.server.cache.TBRedisCacheConfiguration;
+import org.thingsboard.server.cache.TbJsonRedisSerializer;
+import org.thingsboard.server.common.data.CacheConstants;
+import org.thingsboard.server.common.data.security.model.JwtPair;
+
+@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
+@Service("MobileSecretCache")
+public class MobileSecretRedisCache extends RedisTbTransactionalCache {
+
+ public MobileSecretRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
+ super(CacheConstants.MOBILE_SECRET_KEY_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(JwtPair.class));
+ }
+}
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 e696192a43..63be2d89da 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.Objects;
import java.util.Set;
@@ -31,23 +30,23 @@ import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
@Slf4j
-public class TbQueueConsumerTask {
+public class TbQueueConsumerTask {
@Getter
private final Object key;
- private volatile TbQueueConsumer> consumer;
- private volatile Supplier>> consumerSupplier;
+ private volatile TbQueueConsumer consumer;
+ private volatile Supplier> consumerSupplier;
@Setter
private Future> task;
- public TbQueueConsumerTask(Object key, Supplier>> consumerSupplier) {
+ public TbQueueConsumerTask(Object key, Supplier> consumerSupplier) {
this.key = key;
this.consumer = null;
this.consumerSupplier = consumerSupplier;
}
- public TbQueueConsumer> getConsumer() {
+ public TbQueueConsumer getConsumer() {
if (consumer == null) {
synchronized (this) {
if (consumer == null) {
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 0fb005b928..8a96f2c0e1 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,171 +37,52 @@ 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);
+ @Override
+ protected void processTask(TbQueueConsumerManagerTask task) {
+ if (task.getEvent() == QueueEvent.DELETE) {
+ doDelete(task.isDrainQueue());
}
-
- 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
- }
- }
-
- 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) {
@@ -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,7 +242,7 @@ 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);
diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java
index f2e276a0ef..a47d0034a7 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java
@@ -210,11 +210,12 @@ public class JwtTokenFactory {
ZonedDateTime currentTime = ZonedDateTime.now();
+ claimsBuilder.expiration(Date.from(currentTime.plusSeconds(expirationTime).toInstant()));
+
return Jwts.builder()
.claims(claimsBuilder.build())
.issuer(jwtSettingsService.getJwtSettings().getTokenIssuer())
.issuedAt(Date.from(currentTime.toInstant()))
- .expiration(Date.from(currentTime.plusSeconds(expirationTime).toInstant()))
.signWith(getSecretKey(false), Jwts.SIG.HS512);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java
index d26e49e4ab..af1b34ae71 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java
@@ -49,6 +49,7 @@ public class CustomerUserPermissions extends AbstractPermissions {
put(Resource.DEVICE_PROFILE, profilePermissionChecker);
put(Resource.ASSET_PROFILE, profilePermissionChecker);
put(Resource.TB_RESOURCE, customerResourcePermissionChecker);
+ put(Resource.MOBILE_APP_SETTINGS, new PermissionChecker.GenericPermissionChecker(Operation.READ));
}
private static final PermissionChecker customerAlarmPermissionChecker = new PermissionChecker() {
diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java
index 5aa869f2a8..d43663fb97 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java
@@ -46,8 +46,8 @@ public enum Resource {
QUEUE(EntityType.QUEUE),
VERSION_CONTROL,
NOTIFICATION(EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_TEMPLATE,
- EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE);
-
+ EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE),
+ MOBILE_APP_SETTINGS;
private final Set entityTypes;
Resource() {
diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java
index 67cc105c91..d906938e47 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java
@@ -41,6 +41,7 @@ public class SysAdminPermissions extends AbstractPermissions {
put(Resource.TB_RESOURCE, systemEntityPermissionChecker);
put(Resource.QUEUE, systemEntityPermissionChecker);
put(Resource.NOTIFICATION, systemEntityPermissionChecker);
+ put(Resource.MOBILE_APP_SETTINGS, PermissionChecker.allowAllPermissionChecker);
}
private static final PermissionChecker systemEntityPermissionChecker = new PermissionChecker() {
diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
index bfe0af966f..10807e4b5a 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
@@ -50,6 +50,7 @@ public class TenantAdminPermissions extends AbstractPermissions {
put(Resource.QUEUE, queuePermissionChecker);
put(Resource.VERSION_CONTROL, PermissionChecker.allowAllPermissionChecker);
put(Resource.NOTIFICATION, tenantEntityPermissionChecker);
+ put(Resource.MOBILE_APP_SETTINGS, new PermissionChecker.GenericPermissionChecker(Operation.READ));
}
public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() {
diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java
index d3f945cda1..b12e6d4581 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java
@@ -57,7 +57,6 @@ import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.user.UserServiceImpl;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.exception.UserPasswordExpiredException;
-import org.thingsboard.server.service.security.exception.UserPasswordNotValidException;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.utils.MiscUtils;
import ua_parser.Client;
@@ -75,6 +74,8 @@ import static org.thingsboard.server.common.data.CacheConstants.SECURITY_SETTING
@Slf4j
public class DefaultSystemSecurityService implements SystemSecurityService {
+ public static final int DEFAULT_MOBILE_SECRET_KEY_LENGTH = 64;
+
@Autowired
private AdminSettingsService adminSettingsService;
@@ -109,6 +110,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
securitySettings.setPasswordPolicy(new UserPasswordPolicy());
securitySettings.getPasswordPolicy().setMinimumLength(6);
securitySettings.getPasswordPolicy().setMaximumLength(72);
+ securitySettings.setMobileSecretKeyLength(DEFAULT_MOBILE_SECRET_KEY_LENGTH);
}
return securitySettings;
}
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
index 10e342b1e8..984af019b4 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
+++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
@@ -95,7 +95,8 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
private static final int DEFAULT_LIMIT = 100;
private final Map> subscriptionsBySessionId = new ConcurrentHashMap<>();
- @Autowired @Lazy
+ @Autowired
+ @Lazy
private WebSocketService wsService;
@Autowired
@@ -728,7 +729,14 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
public void cancelAllSessionSubscriptions(String sessionId) {
Map sessionSubs = subscriptionsBySessionId.remove(sessionId);
if (sessionSubs != null) {
- sessionSubs.values().forEach(this::cleanupAndCancel);
+ sessionSubs.values().forEach(sub -> {
+ try {
+ cleanupAndCancel(sub);
+ } catch (Exception e) {
+ log.warn("[{}] Failed to remove subscription {} due to ", sub.getTenantId(), sub, e);
+ }
+ }
+ );
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java
index bcb900c3f0..8abb78d9c1 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java
+++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java
@@ -45,6 +45,7 @@ import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.discovery.event.ClusterTopologyChangeEvent;
import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.ws.WebSocketService;
import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpdate;
import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate;
import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate;
@@ -62,6 +63,8 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
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.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@@ -84,18 +87,21 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
private final PartitionService partitionService;
private final TbClusterService clusterService;
private final SubscriptionManagerService subscriptionManagerService;
+ private final WebSocketService webSocketService;
private ExecutorService tsCallBackExecutor;
+ private ScheduledExecutorService staleSessionCleanupExecutor;
public DefaultTbLocalSubscriptionService(AttributesService attrService, TimeseriesService tsService, TbServiceInfoProvider serviceInfoProvider,
PartitionService partitionService, TbClusterService clusterService,
- @Lazy SubscriptionManagerService subscriptionManagerService) {
+ @Lazy SubscriptionManagerService subscriptionManagerService, @Lazy WebSocketService webSocketService) {
this.attrService = attrService;
this.tsService = tsService;
this.serviceInfoProvider = serviceInfoProvider;
this.partitionService = partitionService;
this.clusterService = clusterService;
this.subscriptionManagerService = subscriptionManagerService;
+ this.webSocketService = webSocketService;
}
private String serviceId;
@@ -108,6 +114,8 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
subscriptionUpdateExecutor = ThingsBoardExecutors.newWorkStealingPool(20, getClass());
tsCallBackExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("ts-sub-callback"));
serviceId = serviceInfoProvider.getServiceId();
+ staleSessionCleanupExecutor = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("stale-session-cleanup"));
+ staleSessionCleanupExecutor.scheduleWithFixedDelay(this::cleanupStaleSessions, 60, 60, TimeUnit.SECONDS);
}
@PreDestroy
@@ -118,6 +126,9 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
if (tsCallBackExecutor != null) {
tsCallBackExecutor.shutdownNow();
}
+ if (staleSessionCleanupExecutor != null) {
+ staleSessionCleanupExecutor.shutdownNow();
+ }
}
@Override
@@ -157,9 +168,18 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
TenantId tenantId = subscription.getTenantId();
EntityId entityId = subscription.getEntityId();
log.debug("[{}][{}] Register subscription: {}", tenantId, entityId, subscription);
- Map> sessionSubscriptions = subscriptionsBySessionId.computeIfAbsent(subscription.getSessionId(), k -> new ConcurrentHashMap<>());
- sessionSubscriptions.put(subscription.getSubscriptionId(), subscription);
- modifySubscription(tenantId, entityId, subscription, true);
+ SubscriptionModificationResult result;
+ subsLock.lock();
+ try {
+ Map> sessionSubscriptions = subscriptionsBySessionId.computeIfAbsent(subscription.getSessionId(), k -> new ConcurrentHashMap<>());
+ sessionSubscriptions.put(subscription.getSubscriptionId(), subscription);
+ result = modifySubscription(tenantId, entityId, subscription, true);
+ } finally {
+ subsLock.unlock();
+ }
+ if (result.hasEvent()) {
+ pushSubscriptionEvent(result);
+ }
}
@Override
@@ -195,33 +215,49 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
@Override
public void cancelSubscription(String sessionId, int subscriptionId) {
log.debug("[{}][{}] Going to remove subscription.", sessionId, subscriptionId);
- Map> sessionSubscriptions = subscriptionsBySessionId.get(sessionId);
- if (sessionSubscriptions != null) {
- TbSubscription> subscription = sessionSubscriptions.remove(subscriptionId);
- if (subscription != null) {
- if (sessionSubscriptions.isEmpty()) {
- subscriptionsBySessionId.remove(sessionId);
+ SubscriptionModificationResult result = null;
+ subsLock.lock();
+ try {
+ Map> sessionSubscriptions = subscriptionsBySessionId.get(sessionId);
+ if (sessionSubscriptions != null) {
+ TbSubscription> subscription = sessionSubscriptions.remove(subscriptionId);
+ if (subscription != null) {
+ if (sessionSubscriptions.isEmpty()) {
+ subscriptionsBySessionId.remove(sessionId);
+ }
+ result = modifySubscription(subscription.getTenantId(), subscription.getEntityId(), subscription, false);
+ } else {
+ log.debug("[{}][{}] Subscription not found!", sessionId, subscriptionId);
}
- modifySubscription(subscription.getTenantId(), subscription.getEntityId(), subscription, false);
} else {
- log.debug("[{}][{}] Subscription not found!", sessionId, subscriptionId);
+ log.debug("[{}] No session subscriptions found!", sessionId);
}
- } else {
- log.debug("[{}] No session subscriptions found!", sessionId);
+ } finally {
+ subsLock.unlock();
+ }
+ if (result != null && result.hasEvent()) {
+ pushSubscriptionEvent(result);
}
}
@Override
public void cancelAllSessionSubscriptions(String sessionId) {
log.debug("[{}] Going to remove session subscriptions.", sessionId);
- Map> sessionSubscriptions = subscriptionsBySessionId.remove(sessionId);
- if (sessionSubscriptions != null) {
- for (TbSubscription> subscription : sessionSubscriptions.values()) {
- modifySubscription(subscription.getTenantId(), subscription.getEntityId(), subscription, false);
+ List results = new ArrayList<>();
+ subsLock.lock();
+ try {
+ Map> sessionSubscriptions = subscriptionsBySessionId.remove(sessionId);
+ if (sessionSubscriptions != null) {
+ for (TbSubscription> subscription : sessionSubscriptions.values()) {
+ results.add(modifySubscription(subscription.getTenantId(), subscription.getEntityId(), subscription, false));
+ }
+ } else {
+ log.debug("[{}] No session subscriptions found!", sessionId);
}
- } else {
- log.debug("[{}] No session subscriptions found!", sessionId);
+ } finally {
+ subsLock.unlock();
}
+ results.stream().filter(SubscriptionModificationResult::hasEvent).forEach(this::pushSubscriptionEvent);
}
@Override
@@ -384,10 +420,9 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
callback.onSuccess();
}
- private void modifySubscription(TenantId tenantId, EntityId entityId, TbSubscription> subscription, boolean add) {
+ private SubscriptionModificationResult modifySubscription(TenantId tenantId, EntityId entityId, TbSubscription> subscription, boolean add) {
TbSubscription> missedUpdatesCandidate = null;
- TbEntitySubEvent event;
- subsLock.lock();
+ TbEntitySubEvent event = null;
try {
TbEntityLocalSubsInfo entitySubs = subscriptionsByEntityId.computeIfAbsent(entityId.getId(), id -> new TbEntityLocalSubsInfo(tenantId, entityId));
event = add ? entitySubs.add(subscription) : entitySubs.remove(subscription);
@@ -397,17 +432,23 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
} else if (add) {
missedUpdatesCandidate = entitySubs.registerPendingSubscription(subscription, event);
}
- } finally {
- subsLock.unlock();
+ } catch (Exception e) {
+ log.warn("[{}][{}] Failed to {} subscription {} due to ", tenantId, entityId, add ? "add" : "remove", subscription, e);
}
- if (event != null) {
- log.trace("[{}][{}][{}] Event: {}", tenantId, entityId, subscription.getSubscriptionId(), event);
- pushSubEventToManagerService(tenantId, entityId, event);
+ return new SubscriptionModificationResult(tenantId, entityId, subscription, missedUpdatesCandidate, event);
+ }
+
+ private void pushSubscriptionEvent(SubscriptionModificationResult modificationResult) {
+ try {
+ TbEntitySubEvent event = modificationResult.getEvent();
+ log.trace("[{}][{}][{}] Event: {}", modificationResult.getTenantId(), modificationResult.getEntityId(), modificationResult.getSubscription().getSubscriptionId(), event);
+ pushSubEventToManagerService(modificationResult.getTenantId(), modificationResult.getEntityId(), event);
+ TbSubscription> missedUpdatesCandidate = modificationResult.getMissedUpdatesCandidate();
if (missedUpdatesCandidate != null) {
checkMissedUpdates(missedUpdatesCandidate);
}
- } else {
- log.trace("[{}][{}][{}] No changes detected.", tenantId, entityId, subscription.getSubscriptionId());
+ } catch (Exception e) {
+ log.warn("[{}][{}] Failed to push subscription event {} due to ", modificationResult.getTenantId(), modificationResult.getEntityId(), modificationResult.getEvent(), e);
}
}
@@ -518,4 +559,8 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
}
}
+ private void cleanupStaleSessions() {
+ subscriptionsBySessionId.keySet().forEach(webSocketService::cleanupIfStale);
+ }
+
}
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionModificationResult.java b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionModificationResult.java
new file mode 100644
index 0000000000..a31ecedb8f
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionModificationResult.java
@@ -0,0 +1,39 @@
+/**
+ * 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.subscription;
+
+import lombok.Builder;
+import lombok.Data;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+/**
+ * The modification result of entity subscription
+ */
+@Builder
+@Data
+public class SubscriptionModificationResult {
+
+ private TenantId tenantId;
+ private EntityId entityId;
+ private TbSubscription> subscription;
+ private TbSubscription> missedUpdatesCandidate;
+ private TbEntitySubEvent event;
+
+ public boolean hasEvent() {
+ return event != null;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
index e41d1821c1..2a53e9a772 100644
--- a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
+++ b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
@@ -197,7 +197,8 @@ public class DefaultWebSocketService implements WebSocketService {
wsSessionsMap.put(sessionId, new WsSessionMetaData(sessionRef));
break;
case ERROR:
- log.debug("[{}] Unknown websocket session error: {}. ", sessionId, event.getError().orElse(null));
+ log.debug("[{}] Unknown websocket session error: ", sessionId,
+ event.getError().orElse(new RuntimeException("No error specified")));
break;
case CLOSED:
wsSessionsMap.remove(sessionId);
@@ -294,6 +295,16 @@ public class DefaultWebSocketService implements WebSocketService {
}
}
+ @Override
+ public void cleanupIfStale(String sessionId) {
+ if (!msgEndpoint.isOpen(sessionId)) {
+ log.info("[{}] Cleaning up stale session ", sessionId);
+ wsSessionsMap.remove(sessionId);
+ oldSubService.cancelAllSessionSubscriptions(sessionId);
+ entityDataSubService.cancelAllSessionSubscriptions(sessionId);
+ }
+ }
+
private void processSessionClose(WebSocketSessionRef sessionRef) {
var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef);
if (tenantProfileConfiguration != null) {
diff --git a/application/src/main/java/org/thingsboard/server/service/ws/WebSocketMsgEndpoint.java b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketMsgEndpoint.java
index 41a68c98ca..416af080f8 100644
--- a/application/src/main/java/org/thingsboard/server/service/ws/WebSocketMsgEndpoint.java
+++ b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketMsgEndpoint.java
@@ -29,4 +29,6 @@ public interface WebSocketMsgEndpoint {
void sendPing(WebSocketSessionRef sessionRef, long currentTime) throws IOException;
void close(WebSocketSessionRef sessionRef, CloseStatus withReason) throws IOException;
+
+ boolean isOpen(String sessionId);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java
index 8f586f264e..284431ff84 100644
--- a/application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java
+++ b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java
@@ -36,4 +36,7 @@ public interface WebSocketService {
void sendError(WebSocketSessionRef sessionRef, int subId, SubscriptionErrorCode errorCode, String errorMsg);
void close(String sessionId, CloseStatus status);
+
+ void cleanupIfStale(String sessionId);
+
}
diff --git a/application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionRef.java b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionRef.java
index 190183cce2..44919b138c 100644
--- a/application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionRef.java
+++ b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionRef.java
@@ -33,7 +33,7 @@ public class WebSocketSessionRef {
private static final long serialVersionUID = 1L;
private final String sessionId;
- private SecurityUser securityCtx;
+ private volatile SecurityUser securityCtx;
private final InetSocketAddress localAddress;
private final InetSocketAddress remoteAddress;
private final WebSocketSessionType sessionType;
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index 191d810c64..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:
@@ -585,6 +587,12 @@ cache:
alarmTypes:
timeToLiveInMinutes: "${CACHE_SPECS_ALARM_TYPES_TTL:60}" # Alarm types cache TTL
maxSize: "${CACHE_SPECS_ALARM_TYPES_MAX_SIZE:10000}" # 0 means the cache is disabled
+ mobileAppSettings:
+ timeToLiveInMinutes: "${CACHE_SPECS_MOBILE_APP_SETTINGS_TTL:1440}" # Mobile application cache TTL
+ maxSize: "${CACHE_SPECS_MOBILE_APP_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled
+ mobileSecretKey:
+ timeToLiveInMinutes: "${CACHE_MOBILE_SECRET_KEY_TTL:2}" # QR secret key cache TTL
+ maxSize: "${CACHE_MOBILE_SECRET_KEY_MAX_SIZE:10000}" # 0 means the cache is disabled
# Deliberately placed outside the 'specs' group above
notificationRules:
@@ -1627,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/AuditLogControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java
index 31f398dc03..5005e6ad34 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java
@@ -122,7 +122,7 @@ public class AuditLogControllerTest extends AbstractControllerTest {
}
} while (pageData.hasNext());
- Assert.assertEquals(11, loadedAuditLogs.size());
+ Assert.assertEquals(11 + 1, loadedAuditLogs.size());
loadedAuditLogs = new ArrayList<>();
pageLink = new TimePageLink(5);
@@ -136,7 +136,7 @@ public class AuditLogControllerTest extends AbstractControllerTest {
}
} while (pageData.hasNext());
- Assert.assertEquals(11, loadedAuditLogs.size());
+ Assert.assertEquals(11 + 1, loadedAuditLogs.size());
loadedAuditLogs = new ArrayList<>();
pageLink = new TimePageLink(5);
@@ -150,7 +150,7 @@ public class AuditLogControllerTest extends AbstractControllerTest {
}
} while (pageData.hasNext());
- Assert.assertEquals(11, loadedAuditLogs.size());
+ Assert.assertEquals(11 + 1, loadedAuditLogs.size());
}
@Test
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/controller/MobileApplicationControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java
new file mode 100644
index 0000000000..39e2ef6a40
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java
@@ -0,0 +1,253 @@
+/**
+ * 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.controller;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Value;
+import org.thingsboard.server.common.data.mobile.AndroidConfig;
+import org.thingsboard.server.common.data.mobile.IosConfig;
+import org.thingsboard.server.common.data.mobile.MobileAppSettings;
+import org.thingsboard.server.common.data.mobile.QRCodeConfig;
+import org.thingsboard.server.common.data.security.model.JwtPair;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@Slf4j
+@DaoSqlTest
+public class MobileApplicationControllerTest extends AbstractControllerTest {
+
+ @Value("${cache.specs.mobileSecretKey.timeToLiveInMinutes:2}")
+ private int mobileSecretKeyTtl;
+ private static final String ANDROID_PACKAGE_NAME = "testAppPackage";
+ private static final String ANDROID_APP_SHA256 = "DF:28:32:66:8B:A7:D3:EC:7D:73:CF:CC";
+ private static final String APPLE_APP_ID = "testId";
+ private static final String TEST_LABEL = "Test label";
+
+ @Before
+ public void setUp() throws Exception {
+ loginSysAdmin();
+
+ MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
+ QRCodeConfig qrCodeConfig = new QRCodeConfig();
+ qrCodeConfig.setQrCodeLabel(TEST_LABEL);
+
+ mobileAppSettings.setUseDefaultApp(true);
+ AndroidConfig androidConfig = AndroidConfig.builder()
+ .appPackage(ANDROID_PACKAGE_NAME)
+ .sha256CertFingerprints(ANDROID_APP_SHA256)
+ .enabled(true)
+ .build();
+
+ IosConfig iosConfig = IosConfig.builder()
+ .appId(APPLE_APP_ID)
+ .enabled(true)
+ .build();
+ mobileAppSettings.setAndroidConfig(androidConfig);
+ mobileAppSettings.setIosConfig(iosConfig);
+ mobileAppSettings.setQrCodeConfig(qrCodeConfig);
+
+ doPost("/api/mobile/app/settings", mobileAppSettings)
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testSaveMobileAppSettings() throws Exception {
+ loginSysAdmin();
+ MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
+ assertThat(mobileAppSettings.getQrCodeConfig().getQrCodeLabel()).isEqualTo(TEST_LABEL);
+ assertThat(mobileAppSettings.isUseDefaultApp()).isTrue();
+
+ mobileAppSettings.setUseDefaultApp(false);
+
+ doPost("/api/mobile/app/settings", mobileAppSettings)
+ .andExpect(status().isOk());
+
+ MobileAppSettings updatedMobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
+ assertThat(updatedMobileAppSettings.isUseDefaultApp()).isFalse();
+ }
+
+ @Test
+ public void testShouldNotSaveMobileAppSettingsWithoutRequiredConfig() throws Exception {
+ loginSysAdmin();
+ MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
+
+ mobileAppSettings.setUseDefaultApp(false);
+ mobileAppSettings.setAndroidConfig(null);
+ mobileAppSettings.setIosConfig(null);
+ mobileAppSettings.setQrCodeConfig(null);
+
+ doPost("/api/mobile/app/settings", mobileAppSettings)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Android/ios settings are required to use custom application!")));
+
+ mobileAppSettings.setAndroidConfig(AndroidConfig.builder().enabled(false).build());
+ doPost("/api/mobile/app/settings", mobileAppSettings)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Android/ios settings are required to use custom application!")));
+
+ mobileAppSettings.setIosConfig(IosConfig.builder().enabled(false).build());
+ doPost("/api/mobile/app/settings", mobileAppSettings)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Qr code configuration is required!")));
+
+ mobileAppSettings.setQrCodeConfig(QRCodeConfig.builder().showOnHomePage(false).build());
+ doPost("/api/mobile/app/settings", mobileAppSettings)
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testShouldNotSaveMobileAppSettingsWithoutRequiredAndroidConf() throws Exception {
+ loginSysAdmin();
+ MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
+ mobileAppSettings.setUseDefaultApp(false);
+ AndroidConfig androidConfig = AndroidConfig.builder()
+ .enabled(true)
+ .appPackage(null)
+ .sha256CertFingerprints(null)
+ .build();
+ mobileAppSettings.setAndroidConfig(androidConfig);
+
+ doPost("/api/mobile/app/settings", mobileAppSettings)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Application package and sha256 cert fingerprints are required for custom android application!")));
+
+ androidConfig.setAppPackage("test_app_package");
+ doPost("/api/mobile/app/settings", mobileAppSettings)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Application package and sha256 cert fingerprints are required for custom android application!")));
+
+ androidConfig.setSha256CertFingerprints("test_sha_256");
+ doPost("/api/mobile/app/settings", mobileAppSettings)
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testShouldNotSaveMobileAppSettingsWithoutRequiredIosConf() throws Exception {
+ loginSysAdmin();
+ MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
+ mobileAppSettings.setUseDefaultApp(false);
+ IosConfig iosConfig = IosConfig.builder()
+ .enabled(true)
+ .appId(null)
+ .build();
+ mobileAppSettings.setIosConfig(iosConfig);
+
+ doPost("/api/mobile/app/settings", mobileAppSettings)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Application id is required for custom ios application!")));
+
+ iosConfig.setAppId("test_app_id");
+ doPost("/api/mobile/app/settings", mobileAppSettings)
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testShouldSaveMobileAppSettingsForDefaultApp() throws Exception {
+ loginSysAdmin();
+ MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
+ mobileAppSettings.setUseDefaultApp(true);
+ mobileAppSettings.setIosConfig(null);
+ mobileAppSettings.setAndroidConfig(null);
+
+ doPost("/api/mobile/app/settings", mobileAppSettings)
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testGetApplicationAssociations() throws Exception {
+ loginSysAdmin();
+ MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
+ mobileAppSettings.setUseDefaultApp(false);
+ doPost("/api/mobile/app/settings", mobileAppSettings)
+ .andExpect(status().isOk());
+
+ JsonNode assetLinks = doGet("/.well-known/assetlinks.json", JsonNode.class);
+ assertThat(assetLinks.get(0).get("target").get("package_name").asText()).isEqualTo(ANDROID_PACKAGE_NAME);
+ assertThat(assetLinks.get(0).get("target").get("sha256_cert_fingerprints").get(0).asText()).isEqualTo(ANDROID_APP_SHA256);
+
+ JsonNode appleAssociation = doGet("/.well-known/apple-app-site-association", JsonNode.class);
+ assertThat(appleAssociation.get("applinks").get("details").get(0).get("appID").asText()).isEqualTo(APPLE_APP_ID);
+ }
+
+ @Test
+ public void testGetMobileDeepLink() throws Exception {
+ loginSysAdmin();
+ String deepLink = doGet("/api/mobile/deepLink", String.class);
+
+ Pattern expectedPattern = Pattern.compile("\"https://([^/]+)/api/noauth/qr\\?secret=([^&]+)&ttl=([^&]+)&host=([^&]+)\"");
+ Matcher parsedDeepLink = expectedPattern.matcher(deepLink);
+ assertThat(parsedDeepLink.matches()).isTrue();
+ String appHost = parsedDeepLink.group(1);
+ String secret = parsedDeepLink.group(2);
+ String ttl = parsedDeepLink.group(3);
+ assertThat(appHost).isEqualTo("demo.thingsboard.io");
+ assertThat(ttl).isEqualTo(String.valueOf(mobileSecretKeyTtl));
+
+ JwtPair jwtPair = doGet("/api/noauth/qr/" + secret, JwtPair.class);
+ assertThat(jwtPair).isNotNull();
+
+ loginTenantAdmin();
+ String tenantDeepLink = doGet("/api/mobile/deepLink", String.class);
+ Matcher tenantParsedDeepLink = expectedPattern.matcher(tenantDeepLink);
+ assertThat(tenantParsedDeepLink.matches()).isTrue();
+ String tenantSecret = tenantParsedDeepLink.group(2);
+
+ JwtPair tenantJwtPair = doGet("/api/noauth/qr/" + tenantSecret, JwtPair.class);
+ assertThat(tenantJwtPair).isNotNull();
+
+ loginCustomerUser();
+ String customerDeepLink = doGet("/api/mobile/deepLink", String.class);
+ Matcher customerParsedDeepLink = expectedPattern.matcher(customerDeepLink);
+ assertThat(customerParsedDeepLink.matches()).isTrue();
+ String customerSecret = customerParsedDeepLink.group(2);
+
+ JwtPair customerJwtPair = doGet("/api/noauth/qr/" + customerSecret, JwtPair.class);
+ assertThat(customerJwtPair).isNotNull();
+
+ // update mobile setting to use custom one
+ loginSysAdmin();
+ MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
+ mobileAppSettings.setUseDefaultApp(false);
+ doPost("/api/mobile/app/settings", mobileAppSettings);
+
+ String customAppDeepLink = doGet("/api/mobile/deepLink", String.class);
+ Pattern customAppExpectedPattern = Pattern.compile("\"https://([^/]+)/api/noauth/qr\\?secret=([^&]+)&ttl=([^&]+)\"");
+ Matcher customAppParsedDeepLink = customAppExpectedPattern.matcher(customAppDeepLink);
+ assertThat(customAppParsedDeepLink.matches()).isTrue();
+ assertThat(customAppParsedDeepLink.group(1)).isEqualTo("localhost");
+
+ loginTenantAdmin();
+ String tenantCustomAppDeepLink = doGet("/api/mobile/deepLink", String.class);
+ Matcher tenantCustomAppParsedDeepLink = customAppExpectedPattern.matcher(tenantCustomAppDeepLink);
+ assertThat(tenantCustomAppParsedDeepLink.matches()).isTrue();
+ assertThat(tenantCustomAppParsedDeepLink.group(1)).isEqualTo("localhost");
+
+ loginCustomerUser();
+ String customerCustomAppDeepLink = doGet("/api/mobile/deepLink", String.class);
+ Matcher customerCustomAppParsedDeepLink = customAppExpectedPattern.matcher(customerCustomAppDeepLink);
+ assertThat(customerCustomAppParsedDeepLink.matches()).isTrue();
+ assertThat(customerCustomAppParsedDeepLink.group(1)).isEqualTo("localhost");
+ }
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java
index 38286589bd..63703ae27b 100644
--- a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java
@@ -298,15 +298,13 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
logInWithPreVerificationToken(username, password);
await("async audit log saving").during(1, TimeUnit.SECONDS);
- assertThat(getLogInAuditLogs()).isEmpty();
- assertThat(userService.findUserById(tenantId, user.getId()).getAdditionalInfo()
- .get("lastLoginTs")).isNull();
doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=incorrect")
.andExpect(status().isBadRequest());
+ // there is the first login audit log after user activation
await("async audit log saving").atMost(1, TimeUnit.SECONDS)
- .until(() -> getLogInAuditLogs().size() == 1);
+ .until(() -> getLogInAuditLogs().size() == 2);
assertThat(getLogInAuditLogs().get(0)).satisfies(failedLogInAuditLog -> {
assertThat(failedLogInAuditLog.getActionStatus()).isEqualTo(ActionStatus.FAILURE);
assertThat(failedLogInAuditLog.getActionFailureDetails()).containsIgnoringCase("verification code is incorrect");
@@ -316,7 +314,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + getCorrectTotp(totpTwoFaAccountConfig))
.andExpect(status().isOk());
await("async audit log saving").atMost(1, TimeUnit.SECONDS)
- .until(() -> getLogInAuditLogs().size() == 2);
+ .until(() -> getLogInAuditLogs().size() == 3);
assertThat(getLogInAuditLogs().get(0)).satisfies(successfulLogInAuditLog -> {
assertThat(successfulLogInAuditLog.getActionStatus()).isEqualTo(ActionStatus.SUCCESS);
assertThat(successfulLogInAuditLog.getUserName()).isEqualTo(username);
diff --git a/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java b/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java
index 4d389b4210..dbef55539f 100644
--- a/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java
+++ b/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java
@@ -20,12 +20,12 @@ import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.ListUtils;
import org.apache.commons.lang3.RandomStringUtils;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.BeforeEach;
import org.mockito.Mockito;
-import org.mockito.junit.MockitoJUnitRunner;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.server.common.data.DataConstants;
@@ -67,7 +67,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@Slf4j
-@RunWith(MockitoJUnitRunner.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
public class HashPartitionServiceTest {
public static final int ITERATIONS = 1000000;
@@ -82,7 +82,7 @@ public class HashPartitionServiceTest {
private String hashFunctionName = "murmur3_128";
- @Before
+ @BeforeEach
public void setup() throws Exception {
serviceInfoProvider = mock(TbServiceInfoProvider.class);
applicationEventPublisher = mock(ApplicationEventPublisher.class);
@@ -173,7 +173,7 @@ public class HashPartitionServiceTest {
for (Map.Entry entry : data) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
- Assert.assertTrue(diffPercent < maxDiffPercent);
+ Assertions.assertTrue(diffPercent < maxDiffPercent);
}
@Test
diff --git a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java
index e32e3aaa57..051214c771 100644
--- a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java
@@ -15,24 +15,16 @@
*/
package org.thingsboard.server.service.apiusage;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
-import org.mockito.junit.MockitoJUnitRunner;
-import org.thingsboard.rule.engine.api.MailService;
-import org.thingsboard.server.cluster.TbClusterService;
+import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.id.TenantId;
-import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
-import org.thingsboard.server.dao.tenant.TenantService;
-import org.thingsboard.server.dao.timeseries.TimeseriesService;
-import org.thingsboard.server.dao.usagerecord.ApiUsageStateService;
-import org.thingsboard.server.queue.discovery.PartitionService;
-import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import java.util.UUID;
@@ -40,27 +32,11 @@ import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.Mockito.never;
-@RunWith(MockitoJUnitRunner.class)
+@ExtendWith(MockitoExtension.class)
public class DefaultTbApiUsageStateServiceTest {
- @Mock
- TenantService tenantService;
- @Mock
- TimeseriesService tsService;
- @Mock
- TbClusterService clusterService;
- @Mock
- PartitionService partitionService;
@Mock
TenantApiUsageState tenantUsageStateMock;
- @Mock
- ApiUsageStateService apiUsageStateService;
- @Mock
- TbTenantProfileCache tenantProfileCache;
- @Mock
- MailService mailService;
- @Mock
- DbCallbackExecutorService dbExecutor;
TenantId tenantId = TenantId.fromUUID(UUID.fromString("00797a3b-7aeb-4b5b-b57a-c2a810d0f112"));
@@ -68,7 +44,7 @@ public class DefaultTbApiUsageStateServiceTest {
@InjectMocks
DefaultTbApiUsageStateService service;
- @Before
+ @BeforeEach
public void setUp() {
}
diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java
index d6bfbf1036..ec43753596 100644
--- a/application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java
@@ -17,11 +17,11 @@ package org.thingsboard.server.service.edge.rpc.constructor;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.junit.MockitoJUnitRunner;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
@@ -44,7 +44,7 @@ import java.util.Optional;
import java.util.UUID;
@Slf4j
-@RunWith(MockitoJUnitRunner.class)
+@ExtendWith(MockitoExtension.class)
public class RuleChainMsgConstructorTest {
private static final String RPC_CONNECTION_TYPE = "RPC";
@@ -53,7 +53,7 @@ public class RuleChainMsgConstructorTest {
private TenantId tenantId;
- @Before
+ @BeforeEach
public void setup() {
ruleChainMsgConstructorV1 = new RuleChainMsgConstructorV1();
tenantId = new TenantId(UUID.randomUUID());
@@ -98,10 +98,10 @@ public class RuleChainMsgConstructorTest {
}
private void assetV_3_3_3_and_V_3_4_0(RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg) {
- Assert.assertEquals("First rule node index incorrect!", 3, ruleChainMetadataUpdateMsg.getFirstNodeIndex());
- Assert.assertEquals("Nodes count incorrect!", 12, ruleChainMetadataUpdateMsg.getNodesCount());
- Assert.assertEquals("Connections count incorrect!", 13, ruleChainMetadataUpdateMsg.getConnectionsCount());
- Assert.assertEquals("Rule chain connections count incorrect!", 0, ruleChainMetadataUpdateMsg.getRuleChainConnectionsCount());
+ Assertions.assertEquals(3, ruleChainMetadataUpdateMsg.getFirstNodeIndex(), "First rule node index incorrect!");
+ Assertions.assertEquals(12, ruleChainMetadataUpdateMsg.getNodesCount(), "Nodes count incorrect!");
+ Assertions.assertEquals(13, ruleChainMetadataUpdateMsg.getConnectionsCount(), "Connections count incorrect!");
+ Assertions.assertEquals(0, ruleChainMetadataUpdateMsg.getRuleChainConnectionsCount(), "Rule chain connections count incorrect!");
compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 6, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(0));
compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 10, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(1));
@@ -129,10 +129,10 @@ public class RuleChainMsgConstructorTest {
ruleChainMetaData,
EdgeVersion.V_3_3_0);
- Assert.assertEquals("First rule node index incorrect!", 2, ruleChainMetadataUpdateMsg.getFirstNodeIndex());
- Assert.assertEquals("Nodes count incorrect!", 10, ruleChainMetadataUpdateMsg.getNodesCount());
- Assert.assertEquals("Connections count incorrect!", 10, ruleChainMetadataUpdateMsg.getConnectionsCount());
- Assert.assertEquals("Rule chain connections count incorrect!", 1, ruleChainMetadataUpdateMsg.getRuleChainConnectionsCount());
+ Assertions.assertEquals(2, ruleChainMetadataUpdateMsg.getFirstNodeIndex(),"First rule node index incorrect!");
+ Assertions.assertEquals(10, ruleChainMetadataUpdateMsg.getNodesCount(), "Nodes count incorrect!");
+ Assertions.assertEquals(10, ruleChainMetadataUpdateMsg.getConnectionsCount(),"Connections count incorrect!");
+ Assertions.assertEquals(1, ruleChainMetadataUpdateMsg.getRuleChainConnectionsCount(), "Rule chain connections count incorrect!");
compareNodeConnectionInfoAndProto(createNodeConnectionInfo(2, 5, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(0));
compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 9, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(1));
@@ -146,13 +146,12 @@ public class RuleChainMsgConstructorTest {
compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 9, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(9));
RuleChainConnectionInfoProto ruleChainConnection = ruleChainMetadataUpdateMsg.getRuleChainConnections(0);
- Assert.assertEquals("From index incorrect!", 2, ruleChainConnection.getFromIndex());
- Assert.assertEquals("Type index incorrect!", TbNodeConnectionType.SUCCESS, ruleChainConnection.getType());
- Assert.assertEquals("Additional info incorrect!",
- "{\"description\":\"\",\"layoutX\":477,\"layoutY\":560,\"ruleChainNodeId\":\"rule-chain-node-UNDEFINED\"}",
- ruleChainConnection.getAdditionalInfo());
- Assert.assertTrue("Target rule chain id MSB incorrect!", ruleChainConnection.getTargetRuleChainIdMSB() != 0);
- Assert.assertTrue("Target rule chain id LSB incorrect!", ruleChainConnection.getTargetRuleChainIdLSB() != 0);
+ Assertions.assertEquals(2, ruleChainConnection.getFromIndex(), "From index incorrect!");
+ Assertions.assertEquals(TbNodeConnectionType.SUCCESS, ruleChainConnection.getType(), "Type index incorrect!");
+ Assertions.assertEquals("{\"description\":\"\",\"layoutX\":477,\"layoutY\":560,\"ruleChainNodeId\":\"rule-chain-node-UNDEFINED\"}",
+ ruleChainConnection.getAdditionalInfo(),"Additional info incorrect!");
+ Assertions.assertTrue(ruleChainConnection.getTargetRuleChainIdMSB() != 0,"Target rule chain id MSB incorrect!");
+ Assertions.assertTrue(ruleChainConnection.getTargetRuleChainIdLSB() != 0,"Target rule chain id LSB incorrect!");
assertCheckpointRuleNodeConfiguration(
ruleChainMetadataUpdateMsg.getNodesList(),
@@ -171,10 +170,10 @@ public class RuleChainMsgConstructorTest {
ruleChainMetaData1,
EdgeVersion.V_3_3_0);
- Assert.assertEquals("First rule node index incorrect!", 7, ruleChainMetadataUpdateMsg.getFirstNodeIndex());
- Assert.assertEquals("Nodes count incorrect!", 10, ruleChainMetadataUpdateMsg.getNodesCount());
- Assert.assertEquals("Connections count incorrect!", 10, ruleChainMetadataUpdateMsg.getConnectionsCount());
- Assert.assertEquals("Rule chain connections count incorrect!", 1, ruleChainMetadataUpdateMsg.getRuleChainConnectionsCount());
+ Assertions.assertEquals(7, ruleChainMetadataUpdateMsg.getFirstNodeIndex(), "First rule node index incorrect!");
+ Assertions.assertEquals(10, ruleChainMetadataUpdateMsg.getNodesCount(),"Nodes count incorrect!");
+ Assertions.assertEquals(10, ruleChainMetadataUpdateMsg.getConnectionsCount(),"Connections count incorrect!");
+ Assertions.assertEquals(1, ruleChainMetadataUpdateMsg.getRuleChainConnectionsCount(),"Rule chain connections count incorrect!");
compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 0, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(0));
compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 0, TbMsgType.ATTRIBUTES_UPDATED.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(1));
@@ -188,13 +187,12 @@ public class RuleChainMsgConstructorTest {
compareNodeConnectionInfoAndProto(createNodeConnectionInfo(7, 4, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(9));
RuleChainConnectionInfoProto ruleChainConnection = ruleChainMetadataUpdateMsg.getRuleChainConnections(0);
- Assert.assertEquals("From index incorrect!", 7, ruleChainConnection.getFromIndex());
- Assert.assertEquals("Type index incorrect!", TbNodeConnectionType.SUCCESS, ruleChainConnection.getType());
- Assert.assertEquals("Additional info incorrect!",
- "{\"description\":\"\",\"layoutX\":477,\"layoutY\":560,\"ruleChainNodeId\":\"rule-chain-node-UNDEFINED\"}",
- ruleChainConnection.getAdditionalInfo());
- Assert.assertTrue("Target rule chain id MSB incorrect!", ruleChainConnection.getTargetRuleChainIdMSB() != 0);
- Assert.assertTrue("Target rule chain id LSB incorrect!", ruleChainConnection.getTargetRuleChainIdLSB() != 0);
+ Assertions.assertEquals(7, ruleChainConnection.getFromIndex(),"From index incorrect!");
+ Assertions.assertEquals(TbNodeConnectionType.SUCCESS, ruleChainConnection.getType(), "Type index incorrect!");
+ Assertions.assertEquals( "{\"description\":\"\",\"layoutX\":477,\"layoutY\":560,\"ruleChainNodeId\":\"rule-chain-node-UNDEFINED\"}",
+ ruleChainConnection.getAdditionalInfo(), "Additional info incorrect!");
+ Assertions.assertTrue(ruleChainConnection.getTargetRuleChainIdMSB() != 0, "Target rule chain id MSB incorrect!");
+ Assertions.assertTrue(ruleChainConnection.getTargetRuleChainIdLSB() != 0, "Target rule chain id LSB incorrect!");
assertCheckpointRuleNodeConfiguration(
ruleChainMetadataUpdateMsg.getNodesList(),
@@ -206,15 +204,15 @@ public class RuleChainMsgConstructorTest {
Optional checkpointRuleNodeOpt = nodesList.stream()
.filter(rn -> "org.thingsboard.rule.engine.flow.TbCheckpointNode".equals(rn.getType()))
.findFirst();
- Assert.assertTrue(checkpointRuleNodeOpt.isPresent());
+ Assertions.assertTrue(checkpointRuleNodeOpt.isPresent());
RuleNodeProto checkpointRuleNode = checkpointRuleNodeOpt.get();
- Assert.assertEquals(expectedConfiguration, checkpointRuleNode.getConfiguration());
+ Assertions.assertEquals(expectedConfiguration, checkpointRuleNode.getConfiguration());
}
private void compareNodeConnectionInfoAndProto(NodeConnectionInfo expected, org.thingsboard.server.gen.edge.v1.NodeConnectionInfoProto actual) {
- Assert.assertEquals(expected.getFromIndex(), actual.getFromIndex());
- Assert.assertEquals(expected.getToIndex(), actual.getToIndex());
- Assert.assertEquals(expected.getType(), actual.getType());
+ Assertions.assertEquals(expected.getFromIndex(), actual.getFromIndex());
+ Assertions.assertEquals(expected.getToIndex(), actual.getToIndex());
+ Assertions.assertEquals(expected.getType(), actual.getType());
}
private RuleChainMetaData createRuleChainMetaData(RuleChainId ruleChainId, Integer firstNodeIndex, List nodes, List connections) {
diff --git a/application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java b/application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java
index 95a2c63f52..2e3dd8ee26 100644
--- a/application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java
@@ -15,11 +15,12 @@
*/
package org.thingsboard.server.service.limits;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
-import org.mockito.junit.MockitoJUnitRunner;
+import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.server.cache.limits.DefaultRateLimitService;
import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.common.data.TenantProfile;
@@ -35,21 +36,19 @@ import org.thingsboard.server.dao.tenant.DefaultTbTenantProfileCache;
import java.util.List;
import java.util.UUID;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.when;
-@RunWith(MockitoJUnitRunner.class)
+@ExtendWith(MockitoExtension.class)
public class RateLimitServiceTest {
private RateLimitService rateLimitService;
private DefaultTbTenantProfileCache tenantProfileCache;
private TenantId tenantId;
- @Before
+ @BeforeEach
public void beforeEach() {
tenantProfileCache = Mockito.mock(DefaultTbTenantProfileCache.class);
rateLimitService = new DefaultRateLimitService(tenantProfileCache, mock(NotificationRuleProcessor.class), 60, 100);
@@ -102,10 +101,10 @@ public class RateLimitServiceTest {
private void testRateLimits(LimitedApi limitedApi, int max, Object level) {
for (int i = 1; i <= max; i++) {
boolean success = rateLimitService.checkRateLimit(limitedApi, tenantId, level);
- assertTrue(success);
+ Assertions.assertTrue(success);
}
boolean success = rateLimitService.checkRateLimit(limitedApi, tenantId, level);
- assertFalse(success);
+ Assertions.assertFalse(success);
}
private void updateTenantProfileConfiguration(DefaultTenantProfileConfiguration profileConfiguration) {
diff --git a/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java
index ae9dbe92c4..15650ff2e6 100644
--- a/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java
@@ -21,6 +21,7 @@ import org.junit.After;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.data.util.Pair;
+import org.springframework.jdbc.core.JdbcTemplate;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.notification.SlackService;
import org.thingsboard.server.common.data.User;
@@ -102,6 +103,8 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
protected SqlPartitioningRepository partitioningRepository;
@Autowired
protected DefaultNotifications defaultNotifications;
+ @Autowired
+ private JdbcTemplate jdbcTemplate;
public static final String DEFAULT_NOTIFICATION_SUBJECT = "Just a test";
public static final NotificationType DEFAULT_NOTIFICATION_TYPE = NotificationType.GENERAL;
@@ -112,7 +115,8 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
notificationRuleService.deleteNotificationRulesByTenantId(TenantId.SYS_TENANT_ID);
notificationTemplateService.deleteNotificationTemplatesByTenantId(TenantId.SYS_TENANT_ID);
notificationTargetService.deleteNotificationTargetsByTenantId(TenantId.SYS_TENANT_ID);
- partitioningRepository.dropPartitionsBefore("notification", Long.MAX_VALUE, 1);
+ jdbcTemplate.execute("TRUNCATE TABLE notification");
+ partitioningRepository.cleanupPartitionsCache("notification", Long.MAX_VALUE, 0);
notificationSettingsService.deleteNotificationSettings(TenantId.SYS_TENANT_ID);
}
diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java
index 1a788d437b..883a173edb 100644
--- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java
@@ -331,7 +331,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
@Test
public void testNotificationUpdatesForSeveralUsers() throws Exception {
- int usersCount = 150;
+ int usersCount = 50;
Map sessions = new HashMap<>();
List targets = new ArrayList<>();
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 33b79cc82c..35b5eaeb51 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
@@ -21,14 +21,16 @@ import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.ValueSource;
+import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
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;
@@ -69,6 +71,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;
@@ -92,7 +97,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;
@@ -119,6 +123,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;
@@ -148,10 +155,10 @@ public class TbRuleEngineQueueConsumerManagerTest {
}).when(actorContext).tell(any());
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");
@@ -181,14 +188,23 @@ public class TbRuleEngineQueueConsumerManagerTest {
}).when(queueFactory).createToRuleEngineMsgConsumer(any(), 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();
}
@AfterEach
public void afterEach() {
consumerManager.stop();
consumerManager.awaitStop();
- ruleEngineConsumerContext.stop();
+
+ consumersExecutor.shutdownNow();
+ scheduler.shutdownNow();
+ mgmtExecutor.shutdownNow();
if (generateQueueMsgs) {
await().atMost(10, TimeUnit.SECONDS)
@@ -220,14 +236,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) {
@@ -243,14 +252,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();
@@ -261,7 +263,6 @@ public class TbRuleEngineQueueConsumerManagerTest {
public void testPartitionsUpdate_singleConsumer() {
queue.setConsumerPerPartition(false);
consumerManager.init(queue);
- ruleEngineConsumerContext.setReady(true);
Set partitions = Collections.emptySet();
consumerManager.update(partitions);
@@ -295,7 +296,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(), any());
@@ -339,7 +339,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();
@@ -365,7 +364,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);
@@ -398,7 +396,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();
@@ -418,7 +415,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);
@@ -442,7 +438,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);
@@ -461,7 +456,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);
@@ -483,7 +477,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)
@@ -505,7 +499,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();
@@ -523,7 +516,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)
@@ -544,10 +537,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));
@@ -595,7 +587,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/application/src/test/java/org/thingsboard/server/service/sms/smpp/SmppSmsSenderTest.java b/application/src/test/java/org/thingsboard/server/service/sms/smpp/SmppSmsSenderTest.java
index 96a3daa06d..7db9e01bc8 100644
--- a/application/src/test/java/org/thingsboard/server/service/sms/smpp/SmppSmsSenderTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/sms/smpp/SmppSmsSenderTest.java
@@ -15,10 +15,10 @@
*/
package org.thingsboard.server.service.sms.smpp;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.junit.MockitoJUnitRunner;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
import org.smpp.Session;
import org.smpp.pdu.SubmitSMResp;
import org.thingsboard.server.common.data.StringUtils;
@@ -35,14 +35,14 @@ import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-@RunWith(MockitoJUnitRunner.class)
+@ExtendWith(MockitoExtension.class)
public class SmppSmsSenderTest {
SmppSmsSender smppSmsSender;
SmppSmsProviderConfiguration smppConfig;
Session smppSession;
- @Before
+ @BeforeEach
public void beforeEach() throws Exception {
Constructor constructor = SmppSmsSender.class.getDeclaredConstructor();
constructor.setAccessible(true);
diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttGatewayRateLimitsTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttGatewayRateLimitsTest.java
new file mode 100644
index 0000000000..794d93a44a
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttGatewayRateLimitsTest.java
@@ -0,0 +1,171 @@
+/**
+ * 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.transport.mqtt;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.springframework.boot.test.mock.mockito.SpyBean;
+import org.springframework.test.context.TestPropertySource;
+import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.TenantProfile;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger;
+import org.thingsboard.server.common.data.security.DeviceCredentials;
+import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
+import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
+import org.thingsboard.server.controller.AbstractControllerTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestClient;
+
+import java.util.function.Consumer;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.thingsboard.server.common.data.limit.LimitedApi.TRANSPORT_MESSAGES_PER_GATEWAY;
+
+@DaoSqlTest
+@TestPropertySource(properties = {
+ "service.integrations.supported=ALL",
+ "transport.mqtt.enabled=true",
+})
+public class MqttGatewayRateLimitsTest extends AbstractControllerTest {
+
+ private static final String TOPIC = "v1/gateway/telemetry";
+ private static final String DEVICE_A = "DeviceA";
+ private static final String DEVICE_B = "DeviceB";
+
+ private DeviceId gatewayId;
+ private String gatewayAccessToken;
+
+ @SpyBean
+ private NotificationRuleProcessor notificationRuleProcessor;
+
+ @Before
+ public void beforeTest() throws Exception {
+ loginSysAdmin();
+
+ TenantProfile tenantProfile = doGet("/api/tenantProfile/" + tenantProfileId, TenantProfile.class);
+ Assert.assertNotNull(tenantProfile);
+
+ DefaultTenantProfileConfiguration profileConfiguration = (DefaultTenantProfileConfiguration) tenantProfile.getProfileData().getConfiguration();
+
+ profileConfiguration.setTransportGatewayMsgRateLimit(null);
+ profileConfiguration.setTransportGatewayTelemetryMsgRateLimit(null);
+ profileConfiguration.setTransportGatewayTelemetryDataPointsRateLimit(null);
+
+ doPost("/api/tenantProfile", tenantProfile);
+
+ loginTenantAdmin();
+ createGateway();
+
+ Mockito.reset(notificationRuleProcessor);
+ }
+
+ @Test
+ public void transportGatewayMsgRateLimitTest() throws Exception {
+ transportGatewayRateLimitTest(profileConfiguration -> profileConfiguration.setTransportGatewayMsgRateLimit("1:600"));
+ }
+
+ @Test
+ public void transportGatewayTelemetryMsgRateLimitTest() throws Exception {
+ transportGatewayRateLimitTest(profileConfiguration -> profileConfiguration.setTransportGatewayTelemetryMsgRateLimit("1:600"));
+ }
+
+ @Test
+ public void transportGatewayTelemetryDataPointsRateLimitTest() throws Exception {
+ transportGatewayRateLimitTest(profileConfiguration -> profileConfiguration.setTransportGatewayTelemetryDataPointsRateLimit("1:600"));
+ }
+
+ private void transportGatewayRateLimitTest(Consumer profileConfiguration) throws Exception {
+ loginSysAdmin();
+
+ TenantProfile tenantProfile = doGet("/api/tenantProfile/" + tenantProfileId, TenantProfile.class);
+ Assert.assertNotNull(tenantProfile);
+
+ profileConfiguration.accept((DefaultTenantProfileConfiguration) tenantProfile.getProfileData().getConfiguration());
+
+ doPost("/api/tenantProfile", tenantProfile);
+
+ MqttTestClient client = new MqttTestClient();
+ client.connectAndWait(gatewayAccessToken);
+ client.publishAndWait(TOPIC, getGatewayPayload(DEVICE_A));
+
+ loginTenantAdmin();
+
+ Device deviceA = getDeviceByName(DEVICE_A);
+
+ var deviceATrigger = createRateLimitsTrigger(deviceA);
+
+ Mockito.verify(notificationRuleProcessor, Mockito.never()).process(eq(deviceATrigger));
+
+ try {
+ client.publishAndWait(TOPIC, getGatewayPayload(DEVICE_B));
+ } catch (Exception t) {
+ }
+
+ Device deviceB = getDeviceByName(DEVICE_B);
+
+ var deviceBTrigger = createRateLimitsTrigger(deviceB);
+
+ Mockito.verify(notificationRuleProcessor, Mockito.times(1)).process(deviceBTrigger);
+
+ if (client.isConnected()) {
+ client.disconnect();
+ }
+ }
+
+ private void createGateway() throws Exception {
+ Device device = new Device();
+ device.setName("gateway");
+ ObjectNode additionalInfo = JacksonUtil.newObjectNode();
+ additionalInfo.put("gateway", true);
+ device.setAdditionalInfo(additionalInfo);
+ device = doPost("/api/device", device, Device.class);
+ assertNotNull(device);
+ gatewayId = device.getId();
+ assertNotNull(gatewayId);
+
+ DeviceCredentials deviceCredentials = doGet("/api/device/" + gatewayId + "/credentials", DeviceCredentials.class);
+ assertNotNull(deviceCredentials);
+ assertEquals(gatewayId, deviceCredentials.getDeviceId());
+ gatewayAccessToken = deviceCredentials.getCredentialsId();
+ assertNotNull(gatewayAccessToken);
+ }
+
+ private Device getDeviceByName(String deviceName) throws Exception {
+ Device device = doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class);
+ assertNotNull(device);
+ return device;
+ }
+
+ private byte[] getGatewayPayload(String deviceName) {
+ return String.format("{\"%s\": [{\"values\": {\"temperature\": 42}}]}", deviceName).getBytes();
+ }
+
+ private RateLimitsTrigger createRateLimitsTrigger(Device device) {
+ return RateLimitsTrigger.builder()
+ .tenantId(tenantId)
+ .api(TRANSPORT_MESSAGES_PER_GATEWAY)
+ .limitLevel(device.getId())
+ .limitLevelEntityName(device.getName())
+ .build();
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
index e004dd218d..c95259aec5 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
@@ -48,4 +48,6 @@ public class CacheConstants {
public static final String ENTITY_COUNT_CACHE = "entityCount";
public static final String RESOURCE_INFO_CACHE = "resourceInfo";
public static final String ALARM_TYPES_CACHE = "alarmTypes";
+ public static final String MOBILE_APP_SETTINGS_CACHE = "mobileAppSettings";
+ public static final String MOBILE_SECRET_KEY_CACHE = "mobileSecretKey";
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java
index e022e56c5b..5b4312f7b0 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java
@@ -31,4 +31,5 @@ public class SystemParams {
JsonNode userSettings;
long maxDatapointsLimit;
long maxResourceSize;
+ boolean mobileQrEnabled;
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/AuditLog.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/AuditLog.java
index bc7ac85755..fdbbe4436c 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/audit/AuditLog.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/AuditLog.java
@@ -49,7 +49,7 @@ public class AuditLog extends BaseData {
private ActionType actionType;
@Schema(description = "JsonNode represented action data", accessMode = Schema.AccessMode.READ_ONLY)
private JsonNode actionData;
- @Schema(description = "String represented Action status", example = "SUCCESS", allowableValues = "SUCCESS,FAILURE", accessMode = Schema.AccessMode.READ_ONLY)
+ @Schema(description = "String represented Action status", example = "SUCCESS", allowableValues = {"SUCCESS", "FAILURE"}, accessMode = Schema.AccessMode.READ_ONLY)
private ActionStatus actionStatus;
@Schema(description = "Failure action details info. An empty string in case of action status type 'SUCCESS', otherwise includes stack trace of the caused exception.", accessMode = Schema.AccessMode.READ_ONLY)
private String actionFailureDetails;
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/event/DebugEventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEventFilter.java
index c810dfdbb2..17c94cc4e1 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEventFilter.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEventFilter.java
@@ -25,7 +25,7 @@ public abstract class DebugEventFilter implements EventFilter {
@Schema(description = "String value representing the server name, identifier or ip address where the platform is running", example = "ip-172-31-24-152")
protected String server;
- @Schema(description = "Boolean value to filter the errors", allowableValues = "false, true")
+ @Schema(description = "Boolean value to filter the errors", allowableValues = {"false", "true"})
protected boolean isError;
@Schema(description = "The case insensitive 'contains' filter based on error message", example = "not present in the DB")
protected String errorStr;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/LifeCycleEventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/LifeCycleEventFilter.java
index 643dd0bef2..3c07a699c0 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/event/LifeCycleEventFilter.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/LifeCycleEventFilter.java
@@ -27,7 +27,7 @@ public class LifeCycleEventFilter implements EventFilter {
protected String server;
@Schema(description = "String value representing the lifecycle event type", example = "STARTED")
protected String event;
- @Schema(description = "String value representing status of the lifecycle event", allowableValues = "Success, Failure")
+ @Schema(description = "String value representing status of the lifecycle event", allowableValues = {"Success", "Failure"})
protected String status;
@Schema(description = "The case insensitive 'contains' filter based on error message", example = "not present in the DB")
protected String errorStr;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/RuleNodeDebugEventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/RuleNodeDebugEventFilter.java
index 6f99de5986..3d43b86a6b 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/event/RuleNodeDebugEventFilter.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/RuleNodeDebugEventFilter.java
@@ -25,7 +25,7 @@ import org.thingsboard.server.common.data.StringUtils;
@Schema
public class RuleNodeDebugEventFilter extends DebugEventFilter {
- @Schema(description = "String value representing msg direction type (incoming to entity or outcoming from entity)", allowableValues = "IN, OUT")
+ @Schema(description = "String value representing msg direction type (incoming to entity or outcoming from entity)", allowableValues = {"IN", "OUT"})
protected String msgDirectionType;
@Schema(description = "String value representing the entity id in the event body (originator of the message)", example = "de9d54a0-2b7a-11ec-a3cc-23386423d98f")
protected String entityId;
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/id/MobileAppSettingsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppSettingsId.java
new file mode 100644
index 0000000000..d152701fc4
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppSettingsId.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.common.data.id;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.util.UUID;
+
+@Schema
+public class MobileAppSettingsId extends UUIDBased {
+
+ private static final long serialVersionUID = 1L;
+
+ @JsonCreator
+ public MobileAppSettingsId(@JsonProperty("id") UUID id) {
+ super(id);
+ }
+
+ public static MobileAppSettingsId fromString(String mobileAppSettingsId) {
+ return new MobileAppSettingsId(UUID.fromString(mobileAppSettingsId));
+ }
+}
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..11702100d5 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,9 @@ 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),
+ TRANSPORT_MESSAGES_PER_GATEWAY("transport messages per gateway", false),
+ EMAILS("emails sending", true);
private Function configExtractor;
@Getter
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidConfig.java
new file mode 100644
index 0000000000..e1fb5056fc
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidConfig.java
@@ -0,0 +1,38 @@
+/**
+ * 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.mobile;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import org.thingsboard.server.common.data.validation.NoXss;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode
+public class AndroidConfig {
+
+ private boolean enabled;
+ @NoXss
+ private String appPackage;
+ @NoXss
+ private String sha256CertFingerprints;
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgePosition.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgePosition.java
new file mode 100644
index 0000000000..89c46a5c12
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgePosition.java
@@ -0,0 +1,23 @@
+/**
+ * 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.mobile;
+
+public enum BadgePosition {
+
+ RIGHT,
+ LEFT;
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgeStyle.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgeStyle.java
new file mode 100644
index 0000000000..51606a2340
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgeStyle.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.mobile;
+
+
+public enum BadgeStyle {
+
+ ORIGINAL,
+ WHITE;
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/IosConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/IosConfig.java
new file mode 100644
index 0000000000..f03b446b3b
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/IosConfig.java
@@ -0,0 +1,36 @@
+/**
+ * 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.mobile;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import org.thingsboard.server.common.data.validation.NoXss;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode
+public class IosConfig {
+
+ private boolean enabled;
+ @NoXss
+ private String appId;
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppSettings.java
new file mode 100644
index 0000000000..9a7b88f22c
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppSettings.java
@@ -0,0 +1,45 @@
+/**
+ * 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.mobile;
+
+import jakarta.validation.Valid;
+import lombok.Data;
+import org.thingsboard.server.common.data.BaseData;
+import org.thingsboard.server.common.data.HasTenantId;
+import org.thingsboard.server.common.data.id.MobileAppSettingsId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+@Data
+public class MobileAppSettings extends BaseData implements HasTenantId {
+
+ private static final long serialVersionUID = 2628323657987010348L;
+
+ private TenantId tenantId;
+ private boolean useDefaultApp;
+ @Valid
+ private AndroidConfig androidConfig;
+ @Valid
+ private IosConfig iosConfig;
+ @Valid
+ private QRCodeConfig qrCodeConfig;
+
+ public MobileAppSettings() {
+ }
+ public MobileAppSettings(MobileAppSettingsId id) {
+ super(id);
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/QRCodeConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/QRCodeConfig.java
new file mode 100644
index 0000000000..b3f975870e
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/QRCodeConfig.java
@@ -0,0 +1,40 @@
+/**
+ * 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.mobile;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import org.thingsboard.server.common.data.validation.NoXss;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode
+public class QRCodeConfig {
+
+ private boolean showOnHomePage;
+ private boolean badgeEnabled;
+ private boolean qrCodeLabelEnabled;
+ private BadgePosition badgePosition;
+ private BadgeStyle badgeStyle;
+ @NoXss
+ private String qrCodeLabel;
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java
index d58ca2d62d..4275180ede 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java
@@ -39,7 +39,7 @@ public class ComponentDescriptor extends BaseData {
@Getter @Setter private ComponentType type;
@Schema(description = "Scope of the Rule Node. Always set to 'TENANT', since no rule chains on the 'SYSTEM' level yet.", accessMode = Schema.AccessMode.READ_ONLY, allowableValues = "TENANT", example = "TENANT")
@Getter @Setter private ComponentScope scope;
- @Schema(description = "Clustering mode of the RuleNode. This mode represents the ability to start Rule Node in multiple microservices.", accessMode = Schema.AccessMode.READ_ONLY, allowableValues = "USER_PREFERENCE, ENABLED, SINGLETON", example = "ENABLED")
+ @Schema(description = "Clustering mode of the RuleNode. This mode represents the ability to start Rule Node in multiple microservices.", accessMode = Schema.AccessMode.READ_ONLY, allowableValues = {"USER_PREFERENCE", "ENABLED", "SINGLETON"}, example = "ENABLED")
@Getter @Setter private ComponentClusteringMode clusteringMode;
@Length(fieldName = "name")
@Schema(description = "Name of the Rule Node. Taken from the @RuleNode annotation.", accessMode = Schema.AccessMode.READ_ONLY, example = "Custom Rule Node")
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/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java
index 24d8cc1175..e269f0438d 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java
@@ -70,7 +70,7 @@ public class DeviceCredentials extends BaseData implements
this.deviceId = deviceId;
}
- @Schema(description = "Type of the credentials", allowableValues ="ACCESS_TOKEN, X509_CERTIFICATE, MQTT_BASIC, LWM2M_CREDENTIALS")
+ @Schema(description = "Type of the credentials", allowableValues = {"ACCESS_TOKEN", "X509_CERTIFICATE", "MQTT_BASIC", "LWM2M_CREDENTIALS"})
@Override
public DeviceCredentialsType getCredentialsType() {
return credentialsType;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtPair.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtPair.java
index 459aa7b26f..ae484136ce 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtPair.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtPair.java
@@ -20,10 +20,12 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.security.Authority;
+import java.io.Serializable;
+
@Schema(description = "JWT Pair")
@Data
@NoArgsConstructor
-public class JwtPair {
+public class JwtPair implements Serializable {
@Schema(description = "The JWT Access Token. Used to perform API calls.", example = "AAB254FF67D..")
private String token;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java
index 07262c7eb4..aa4d303440 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java
@@ -32,4 +32,6 @@ public class SecuritySettings implements Serializable {
private Integer maxFailedLoginAttempts;
@Schema(description = "Email to use for notifications about locked users." )
private String userLockoutNotificationEmail;
+ @Schema(description = "Mobile secret key length" )
+ private Integer mobileSecretKeyLength;
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmppSmsProviderConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmppSmsProviderConfiguration.java
index 5fc0c14d61..549770c539 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmppSmsProviderConfiguration.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmppSmsProviderConfiguration.java
@@ -89,7 +89,7 @@ public class SmppSmsProviderConfiguration implements SmsProviderConfiguration {
@Schema(description = "Address range", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String addressRange;
- @Schema(allowableValues = "0-10,13-14",
+ @Schema(allowableValues = {"0-10" ,"13-14"},
description = "0 - SMSC Default Alphabet (ASCII for short and long code and to GSM for toll-free, used as default)\n" +
"1 - IA5 (ASCII for short and long code, Latin 9 for toll-free (ISO-8859-9))\n" +
"2 - Octet Unspecified (8-bit binary)\n" +
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java
index a0cfbfe1e1..d990c300da 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java
@@ -47,6 +47,9 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
private String transportDeviceMsgRateLimit;
private String transportDeviceTelemetryMsgRateLimit;
private String transportDeviceTelemetryDataPointsRateLimit;
+ private String transportGatewayMsgRateLimit;
+ private String transportGatewayTelemetryMsgRateLimit;
+ private String transportGatewayTelemetryDataPointsRateLimit;
private String tenantEntityExportRateLimit;
private String tenantEntityImportRateLimit;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/util/TbPair.java b/common/data/src/main/java/org/thingsboard/server/common/data/util/TbPair.java
index bdca8b2e77..6e8b2e5696 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/util/TbPair.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/util/TbPair.java
@@ -23,4 +23,8 @@ import lombok.Data;
public class TbPair {
private S first;
private T second;
+
+ public static TbPair of(S first, T second) {
+ return new TbPair<>(first, second);
+ }
}
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/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto
index 7b2aa2fe1b..53a3026424 100644
--- a/common/proto/src/main/proto/queue.proto
+++ b/common/proto/src/main/proto/queue.proto
@@ -96,6 +96,8 @@ message SessionInfoProto {
int64 deviceProfileIdLSB = 13;
int64 customerIdMSB = 14;
int64 customerIdLSB = 15;
+ optional int64 gatewayIdMSB = 16;
+ optional int64 gatewayIdLSB = 17;
}
enum SessionEvent {
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/kafka/TbKafkaAdmin.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java
index 9c8f63594b..0cb1194167 100644
--- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java
+++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java
@@ -17,7 +17,6 @@ package org.thingsboard.server.queue.kafka;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
-import org.apache.kafka.clients.admin.AdminClient;
import org.apache.kafka.clients.admin.CreateTopicsResult;
import org.apache.kafka.clients.admin.ListConsumerGroupOffsetsResult;
import org.apache.kafka.clients.admin.NewTopic;
@@ -41,24 +40,17 @@ import java.util.concurrent.TimeoutException;
@Slf4j
public class TbKafkaAdmin implements TbQueueAdmin {
- @Getter
- private final AdminClient client;
+ private final TbKafkaSettings settings;
private final Map topicConfigs;
- private final Set topics = ConcurrentHashMap.newKeySet();
private final int numPartitions;
+ private volatile Set topics;
private final short replicationFactor;
public TbKafkaAdmin(TbKafkaSettings settings, Map topicConfigs) {
- client = AdminClient.create(settings.toAdminProps());
+ this.settings = settings;
this.topicConfigs = topicConfigs;
- try {
- topics.addAll(client.listTopics().names().get());
- } catch (InterruptedException | ExecutionException e) {
- log.error("Failed to get all topics.", e);
- }
-
String numPartitionsStr = topicConfigs.get(TbKafkaTopicConfigs.NUM_PARTITIONS_SETTING);
if (numPartitionsStr != null) {
numPartitions = Integer.parseInt(numPartitionsStr);
@@ -71,6 +63,7 @@ public class TbKafkaAdmin implements TbQueueAdmin {
@Override
public void createTopicIfNotExists(String topic, String properties) {
+ Set topics = getTopics();
if (topics.contains(topic)) {
return;
}
@@ -93,12 +86,13 @@ public class TbKafkaAdmin implements TbQueueAdmin {
@Override
public void deleteTopic(String topic) {
+ Set topics = getTopics();
if (topics.contains(topic)) {
- client.deleteTopics(Collections.singletonList(topic));
+ settings.getAdminClient().deleteTopics(Collections.singletonList(topic));
} else {
try {
- if (client.listTopics().names().get().contains(topic)) {
- client.deleteTopics(Collections.singletonList(topic));
+ if (settings.getAdminClient().listTopics().names().get().contains(topic)) {
+ settings.getAdminClient().deleteTopics(Collections.singletonList(topic));
} else {
log.warn("Kafka topic [{}] does not exist.", topic);
}
@@ -108,15 +102,28 @@ public class TbKafkaAdmin implements TbQueueAdmin {
}
}
- @Override
- public void destroy() {
- if (client != null) {
- client.close();
+ private Set getTopics() {
+ if (topics == null) {
+ synchronized (this) {
+ if (topics == null) {
+ topics = ConcurrentHashMap.newKeySet();
+ try {
+ topics.addAll(settings.getAdminClient().listTopics().names().get());
+ } catch (InterruptedException | ExecutionException e) {
+ log.error("Failed to get all topics.", e);
+ }
+ }
+ }
}
+ return topics;
}
public CreateTopicsResult createTopic(NewTopic topic) {
- return client.createTopics(Collections.singletonList(topic));
+ return settings.getAdminClient().createTopics(Collections.singletonList(topic));
+ }
+
+ @Override
+ public void destroy() {
}
/**
@@ -138,7 +145,7 @@ public class TbKafkaAdmin implements TbQueueAdmin {
return;
}
Map oldOffsets =
- client.listConsumerGroupOffsets(fatGroupId).partitionsToOffsetAndMetadata().get(10, TimeUnit.SECONDS);
+ settings.getAdminClient().listConsumerGroupOffsets(fatGroupId).partitionsToOffsetAndMetadata().get(10, TimeUnit.SECONDS);
if (oldOffsets.isEmpty()) {
return;
}
@@ -150,7 +157,7 @@ public class TbKafkaAdmin implements TbQueueAdmin {
}
var om = consumerOffset.getValue();
Map newOffsets =
- client.listConsumerGroupOffsets(newGroupId).partitionsToOffsetAndMetadata().get(10, TimeUnit.SECONDS);
+ settings.getAdminClient().listConsumerGroupOffsets(newGroupId).partitionsToOffsetAndMetadata().get(10, TimeUnit.SECONDS);
var existingOffset = newOffsets.get(tp);
if (existingOffset == null) {
@@ -161,7 +168,7 @@ public class TbKafkaAdmin implements TbQueueAdmin {
} else {
log.info("[{}] SHOULD alter topic offset [{}] less than old node group offset [{}]", tp, existingOffset.offset(), om.offset());
}
- client.alterConsumerGroupOffsets(newGroupId, Map.of(tp, om)).all().get(10, TimeUnit.SECONDS);
+ settings.getAdminClient().alterConsumerGroupOffsets(newGroupId, Map.of(tp, om)).all().get(10, TimeUnit.SECONDS);
log.info("[{}] altered new consumer groupId {}", tp, newGroupId);
break;
}
diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatsService.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatsService.java
index dbc1b7b7c4..56440cf4a5 100644
--- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatsService.java
+++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatsService.java
@@ -15,11 +15,12 @@
*/
package org.thingsboard.server.queue.kafka;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
import lombok.Builder;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import org.apache.kafka.clients.admin.AdminClient;
import org.apache.kafka.clients.consumer.Consumer;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.KafkaConsumer;
@@ -35,8 +36,6 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.queue.discovery.PartitionService;
-import jakarta.annotation.PostConstruct;
-import jakarta.annotation.PreDestroy;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
@@ -62,7 +61,6 @@ public class TbKafkaConsumerStatsService {
@Autowired
private PartitionService partitionService;
- private AdminClient adminClient;
private Consumer consumer;
private ScheduledExecutorService statsPrintScheduler;
@@ -71,7 +69,6 @@ public class TbKafkaConsumerStatsService {
if (!statsConfig.getEnabled()) {
return;
}
- this.adminClient = AdminClient.create(kafkaSettings.toAdminProps());
this.statsPrintScheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("kafka-consumer-stats"));
Properties consumerProps = kafkaSettings.toConsumerProps(null);
@@ -90,7 +87,7 @@ public class TbKafkaConsumerStatsService {
}
for (String groupId : monitoredGroups) {
try {
- Map groupOffsets = adminClient.listConsumerGroupOffsets(groupId).partitionsToOffsetAndMetadata()
+ Map groupOffsets = kafkaSettings.getAdminClient().listConsumerGroupOffsets(groupId).partitionsToOffsetAndMetadata()
.get(statsConfig.getKafkaResponseTimeoutMs(), TimeUnit.MILLISECONDS);
Map endOffsets = consumer.endOffsets(groupOffsets.keySet(), timeoutDuration);
@@ -157,9 +154,6 @@ public class TbKafkaConsumerStatsService {
if (statsPrintScheduler != null) {
statsPrintScheduler.shutdownNow();
}
- if (adminClient != null) {
- adminClient.close();
- }
if (consumer != null) {
consumer.close();
}
diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java
index 96fa7afb7f..760487c61e 100644
--- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java
+++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java
@@ -19,6 +19,7 @@ import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.CommonClientConfigs;
+import org.apache.kafka.clients.admin.AdminClient;
import org.apache.kafka.clients.admin.AdminClientConfig;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.producer.ProducerConfig;
@@ -34,6 +35,7 @@ import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.TbProperty;
import org.thingsboard.server.queue.util.PropertyUtils;
+import javax.annotation.PreDestroy;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -143,13 +145,7 @@ public class TbKafkaSettings {
@Setter
private Map> consumerPropertiesPerTopic = Collections.emptyMap();
- public Properties toAdminProps() {
- Properties props = toProps();
- props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, servers);
- props.put(AdminClientConfig.RETRIES_CONFIG, retries);
-
- return props;
- }
+ private volatile AdminClient adminClient;
public Properties toConsumerProps(String topic) {
Properties props = toProps();
@@ -221,4 +217,29 @@ public class TbKafkaSettings {
}
}
+ public AdminClient getAdminClient() {
+ if (adminClient == null) {
+ synchronized (this) {
+ if (adminClient == null) {
+ adminClient = AdminClient.create(toAdminProps());
+ }
+ }
+ }
+ return adminClient;
+ }
+
+ protected Properties toAdminProps() {
+ Properties props = toProps();
+ props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, servers);
+ props.put(AdminClientConfig.RETRIES_CONFIG, retries);
+ return props;
+ }
+
+ @PreDestroy
+ private void destroy() {
+ if (adminClient != null) {
+ adminClient.close();
+ }
+ }
+
}
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 30dac37851..9728642b8d 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
@@ -228,7 +228,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..141aa83b7e 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 = new LwM2m.Version(link.getAttributes().get("ver").getValue().toString());
+ } 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/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java
index f803f0fe3a..e174ad86bd 100644
--- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java
+++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java
@@ -59,6 +59,8 @@ public abstract class AbstractGatewayDeviceSessionContext tenantAllowed = new ConcurrentHashMap<>();
private final ConcurrentMap> tenantDevices = new ConcurrentHashMap<>();
+ private final ConcurrentMap> tenantGateways = new ConcurrentHashMap<>();
private final ConcurrentMap perTenantLimits = new ConcurrentHashMap<>();
private final ConcurrentMap perDeviceLimits = new ConcurrentHashMap<>();
+ private final ConcurrentMap perGatewayLimits = new ConcurrentHashMap<>();
private final Map ipMap = new ConcurrentHashMap<>();
private final TransportTenantProfileCache tenantProfileCache;
@@ -65,15 +72,20 @@ public class DefaultTransportRateLimitService implements TransportRateLimitServi
}
@Override
- public EntityType checkLimits(TenantId tenantId, DeviceId deviceId, int dataPoints) {
+ public TbPair checkLimits(TenantId tenantId, DeviceId gatewayId, DeviceId deviceId, int dataPoints) {
if (!tenantAllowed.getOrDefault(tenantId, Boolean.TRUE)) {
- return EntityType.API_USAGE_STATE;
+ return TbPair.of(EntityType.API_USAGE_STATE, false);
}
if (!checkEntityRateLimit(dataPoints, getTenantRateLimits(tenantId))) {
- return EntityType.TENANT;
+ return TbPair.of(EntityType.TENANT, false);
+ }
+
+ if (gatewayId != null && !checkEntityRateLimit(dataPoints, getGatewayRateLimits(tenantId, gatewayId))) {
+ return TbPair.of(EntityType.DEVICE, true);
}
+
if (!checkEntityRateLimit(dataPoints, getDeviceRateLimits(tenantId, deviceId))) {
- return EntityType.DEVICE;
+ return TbPair.of(EntityType.DEVICE, false);
}
return null;
}
@@ -89,24 +101,27 @@ public class DefaultTransportRateLimitService implements TransportRateLimitServi
@Override
public void update(TenantProfileUpdateResult update) {
log.info("Received tenant profile update: {}", update.getProfile());
- EntityTransportRateLimits tenantRateLimitPrototype = createRateLimits(update.getProfile(), true);
- EntityTransportRateLimits deviceRateLimitPrototype = createRateLimits(update.getProfile(), false);
+ EntityTransportRateLimits tenantRateLimitPrototype = createRateLimits(update.getProfile(), TENANT_LIMITS);
+ EntityTransportRateLimits deviceRateLimitPrototype = createRateLimits(update.getProfile(), DEVICE_LIMITS);
+ EntityTransportRateLimits gatewayRateLimitPrototype = createRateLimits(update.getProfile(), GATEWAY_LIMITS);
for (TenantId tenantId : update.getAffectedTenants()) {
- mergeLimits(tenantId, tenantRateLimitPrototype, perTenantLimits::get, perTenantLimits::put);
- getTenantDevices(tenantId).forEach(deviceId -> {
- mergeLimits(deviceId, deviceRateLimitPrototype, perDeviceLimits::get, perDeviceLimits::put);
- });
+ update(tenantId, tenantRateLimitPrototype, deviceRateLimitPrototype, gatewayRateLimitPrototype);
}
}
@Override
public void update(TenantId tenantId) {
- EntityTransportRateLimits tenantRateLimitPrototype = createRateLimits(tenantProfileCache.get(tenantId), true);
- EntityTransportRateLimits deviceRateLimitPrototype = createRateLimits(tenantProfileCache.get(tenantId), false);
+ EntityTransportRateLimits tenantRateLimitPrototype = createRateLimits(tenantProfileCache.get(tenantId), TENANT_LIMITS);
+ EntityTransportRateLimits deviceRateLimitPrototype = createRateLimits(tenantProfileCache.get(tenantId), DEVICE_LIMITS);
+ EntityTransportRateLimits gatewayRateLimitPrototype = createRateLimits(tenantProfileCache.get(tenantId), GATEWAY_LIMITS);
+ update(tenantId, tenantRateLimitPrototype, deviceRateLimitPrototype, gatewayRateLimitPrototype);
+ }
+
+ private void update(TenantId tenantId, EntityTransportRateLimits tenantRateLimitPrototype,
+ EntityTransportRateLimits deviceRateLimitPrototype, EntityTransportRateLimits gatewayRateLimitPrototype) {
mergeLimits(tenantId, tenantRateLimitPrototype, perTenantLimits::get, perTenantLimits::put);
- getTenantDevices(tenantId).forEach(deviceId -> {
- mergeLimits(deviceId, deviceRateLimitPrototype, perDeviceLimits::get, perDeviceLimits::put);
- });
+ getTenantDevices(tenantId).forEach(deviceId -> mergeLimits(deviceId, deviceRateLimitPrototype, perDeviceLimits::get, perDeviceLimits::put));
+ getTenantGateways(tenantId).forEach(deviceId -> mergeLimits(deviceId, gatewayRateLimitPrototype, perGatewayLimits::get, perGatewayLimits::put));
}
@Override
@@ -233,15 +248,34 @@ public class DefaultTransportRateLimitService implements TransportRateLimitServi
}
}
- private EntityTransportRateLimits createRateLimits(TenantProfile tenantProfile, boolean tenant) {
+ private EntityTransportRateLimits createRateLimits(TenantProfile tenantProfile, TransportLimitsType limitsType) {
TenantProfileData profileData = tenantProfile.getProfileData();
DefaultTenantProfileConfiguration profile = (DefaultTenantProfileConfiguration) profileData.getConfiguration();
if (profile == null) {
return new EntityTransportRateLimits(ALLOW, ALLOW, ALLOW);
} else {
- TransportRateLimit regularMsgRateLimit = newLimit(tenant ? profile.getTransportTenantMsgRateLimit() : profile.getTransportDeviceMsgRateLimit());
- TransportRateLimit telemetryMsgRateLimit = newLimit(tenant ? profile.getTransportTenantTelemetryMsgRateLimit() : profile.getTransportDeviceTelemetryMsgRateLimit());
- TransportRateLimit telemetryDpRateLimit = newLimit(tenant ? profile.getTransportTenantTelemetryDataPointsRateLimit() : profile.getTransportDeviceTelemetryDataPointsRateLimit());
+ TransportRateLimit regularMsgRateLimit;
+ TransportRateLimit telemetryMsgRateLimit;
+ TransportRateLimit telemetryDpRateLimit;
+ switch (limitsType) {
+ case TENANT_LIMITS -> {
+ regularMsgRateLimit = newLimit(profile.getTransportTenantMsgRateLimit());
+ telemetryMsgRateLimit = newLimit(profile.getTransportTenantTelemetryMsgRateLimit());
+ telemetryDpRateLimit = newLimit(profile.getTransportTenantTelemetryDataPointsRateLimit());
+ }
+ case DEVICE_LIMITS -> {
+ regularMsgRateLimit = newLimit(profile.getTransportDeviceMsgRateLimit());
+ telemetryMsgRateLimit = newLimit(profile.getTransportDeviceTelemetryMsgRateLimit());
+ telemetryDpRateLimit = newLimit(profile.getTransportDeviceTelemetryDataPointsRateLimit());
+ }
+ case GATEWAY_LIMITS -> {
+ regularMsgRateLimit = newLimit(profile.getTransportGatewayMsgRateLimit());
+ telemetryMsgRateLimit = newLimit(profile.getTransportGatewayTelemetryMsgRateLimit());
+ telemetryDpRateLimit = newLimit(profile.getTransportGatewayTelemetryDataPointsRateLimit());
+ }
+ default -> throw new IllegalStateException("Unknown limits type: " + limitsType);
+ }
+
return new EntityTransportRateLimits(regularMsgRateLimit, telemetryMsgRateLimit, telemetryDpRateLimit);
}
}
@@ -251,21 +285,31 @@ public class DefaultTransportRateLimitService implements TransportRateLimitServi
}
private EntityTransportRateLimits getTenantRateLimits(TenantId tenantId) {
- return perTenantLimits.computeIfAbsent(tenantId, k -> {
- return createRateLimits(tenantProfileCache.get(tenantId), true);
- });
+ return perTenantLimits.computeIfAbsent(tenantId, k -> createRateLimits(tenantProfileCache.get(tenantId), TENANT_LIMITS));
}
private EntityTransportRateLimits getDeviceRateLimits(TenantId tenantId, DeviceId deviceId) {
return perDeviceLimits.computeIfAbsent(deviceId, k -> {
- EntityTransportRateLimits limits = createRateLimits(tenantProfileCache.get(tenantId), false);
+ EntityTransportRateLimits limits = createRateLimits(tenantProfileCache.get(tenantId), DEVICE_LIMITS);
getTenantDevices(tenantId).add(deviceId);
return limits;
});
}
+ private EntityTransportRateLimits getGatewayRateLimits(TenantId tenantId, DeviceId deviceId) {
+ return perGatewayLimits.computeIfAbsent(deviceId, k -> {
+ EntityTransportRateLimits limits = createRateLimits(tenantProfileCache.get(tenantId), GATEWAY_LIMITS);
+ getTenantGateways(tenantId).add(deviceId);
+ return limits;
+ });
+ }
+
private Set getTenantDevices(TenantId tenantId) {
return tenantDevices.computeIfAbsent(tenantId, id -> ConcurrentHashMap.newKeySet());
}
+ private Set getTenantGateways(TenantId tenantId) {
+ return tenantGateways.computeIfAbsent(tenantId, id -> ConcurrentHashMap.newKeySet());
+ }
+
}
diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportLimitsType.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportLimitsType.java
new file mode 100644
index 0000000000..ab65b07414
--- /dev/null
+++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportLimitsType.java
@@ -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.
+ */
+package org.thingsboard.server.common.transport.limits;
+
+public enum TransportLimitsType {
+ TENANT_LIMITS, DEVICE_LIMITS, GATEWAY_LIMITS
+}
diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitService.java
index 975602c5da..e33f9db240 100644
--- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitService.java
+++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitService.java
@@ -18,13 +18,14 @@ package org.thingsboard.server.common.transport.limits;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.transport.profile.TenantProfileUpdateResult;
import java.net.InetSocketAddress;
public interface TransportRateLimitService {
- EntityType checkLimits(TenantId tenantId, DeviceId deviceId, int dataPoints);
+ TbPair checkLimits(TenantId tenantId, DeviceId gatewayId, DeviceId deviceId, int dataPoints);
void update(TenantProfileUpdateResult update);
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..9312370c34 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();
@@ -864,18 +852,27 @@ public class DefaultTransportService extends TransportActivityManager implements
}
TenantId tenantId = TenantId.fromUUID(new UUID(sessionInfo.getTenantIdMSB(), sessionInfo.getTenantIdLSB()));
DeviceId deviceId = new DeviceId(new UUID(sessionInfo.getDeviceIdMSB(), sessionInfo.getDeviceIdLSB()));
+ DeviceId gatewayId = null;
+ if (sessionInfo.hasGatewayIdMSB() && sessionInfo.hasGatewayIdLSB()) {
+ gatewayId = new DeviceId(new UUID(sessionInfo.getGatewayIdMSB(), sessionInfo.getGatewayIdLSB()));
+ }
- EntityType rateLimitedEntityType = rateLimitService.checkLimits(tenantId, deviceId, dataPoints);
- if (rateLimitedEntityType == null) {
+ var rateLimitedPair = rateLimitService.checkLimits(tenantId, gatewayId, deviceId, dataPoints);
+ if (rateLimitedPair == null) {
return true;
} else {
+ var rateLimitedEntityType = rateLimitedPair.getFirst();
if (callback != null) {
callback.onError(new TbRateLimitsException(rateLimitedEntityType));
}
+
if (rateLimitedEntityType == EntityType.DEVICE || rateLimitedEntityType == EntityType.TENANT) {
+ LimitedApi limitedApi =
+ rateLimitedEntityType == EntityType.TENANT ? LimitedApi.TRANSPORT_MESSAGES_PER_TENANT :
+ rateLimitedPair.getSecond() ? LimitedApi.TRANSPORT_MESSAGES_PER_GATEWAY : LimitedApi.TRANSPORT_MESSAGES_PER_DEVICE;
notificationRuleProcessor.process(RateLimitsTrigger.builder()
.tenantId(tenantId)
- .api(rateLimitedEntityType == EntityType.DEVICE ? LimitedApi.TRANSPORT_MESSAGES_PER_DEVICE : LimitedApi.TRANSPORT_MESSAGES_PER_TENANT)
+ .api(limitedApi)
.limitLevel(rateLimitedEntityType == EntityType.DEVICE ? deviceId : tenantId)
.limitLevelEntityName(rateLimitedEntityType == EntityType.DEVICE ? sessionInfo.getDeviceName() : null)
.build());
@@ -945,7 +942,6 @@ public class DefaultTransportService extends TransportActivityManager implements
log.trace("ResourceUpdate - [{}] [{}]", id, mdRez);
transportCallbackExecutor.submit(() -> mdRez.getListener().onResourceUpdate(msg));
});
-
} else if (toSessionMsg.hasResourceDeleteMsg()) {
TransportProtos.ResourceDeleteMsg msg = toSessionMsg.getResourceDeleteMsg();
TenantId tenantId = TenantId.fromUUID(new UUID(msg.getTenantIdMSB(), msg.getTenantIdLSB()));
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/JpaDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/JpaDaoConfig.java
index 05becc8bde..4dd0633420 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/JpaDaoConfig.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/JpaDaoConfig.java
@@ -19,6 +19,7 @@ import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.data.repository.config.BootstrapMode;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.thingsboard.server.dao.util.TbAutoConfiguration;
@@ -28,7 +29,7 @@ import org.thingsboard.server.dao.util.TbAutoConfiguration;
@Configuration
@TbAutoConfiguration
@ComponentScan({"org.thingsboard.server.dao.sql", "org.thingsboard.server.dao.attributes", "org.thingsboard.server.dao.cache", "org.thingsboard.server.cache"})
-@EnableJpaRepositories("org.thingsboard.server.dao.sql")
+@EnableJpaRepositories(value = "org.thingsboard.server.dao.sql", bootstrapMode = BootstrapMode.LAZY)
@EntityScan("org.thingsboard.server.dao.model.sql")
@EnableTransactionManagement
public class JpaDaoConfig {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/SqlTimeseriesDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/SqlTimeseriesDaoConfig.java
index 9cf9532aea..09553fb973 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/SqlTimeseriesDaoConfig.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/SqlTimeseriesDaoConfig.java
@@ -19,14 +19,14 @@ import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.data.repository.config.BootstrapMode;
import org.springframework.transaction.annotation.EnableTransactionManagement;
-import org.thingsboard.server.dao.util.SqlTsOrTsLatestAnyDao;
import org.thingsboard.server.dao.util.TbAutoConfiguration;
@Configuration
@TbAutoConfiguration
@ComponentScan({"org.thingsboard.server.dao.sqlts.dictionary"})
-@EnableJpaRepositories({"org.thingsboard.server.dao.sqlts.dictionary"})
+@EnableJpaRepositories(value = {"org.thingsboard.server.dao.sqlts.dictionary"}, bootstrapMode = BootstrapMode.LAZY)
@EntityScan({"org.thingsboard.server.dao.model.sqlts.dictionary"})
@EnableTransactionManagement
public class SqlTimeseriesDaoConfig {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/SqlTsDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/SqlTsDaoConfig.java
index 6bdf7c2170..bbd31846db 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/SqlTsDaoConfig.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/SqlTsDaoConfig.java
@@ -19,6 +19,7 @@ import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.data.repository.config.BootstrapMode;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.thingsboard.server.dao.util.SqlTsDao;
import org.thingsboard.server.dao.util.TbAutoConfiguration;
@@ -26,7 +27,7 @@ import org.thingsboard.server.dao.util.TbAutoConfiguration;
@Configuration
@TbAutoConfiguration
@ComponentScan({"org.thingsboard.server.dao.sqlts.sql", "org.thingsboard.server.dao.sqlts.insert.sql"})
-@EnableJpaRepositories({"org.thingsboard.server.dao.sqlts.ts", "org.thingsboard.server.dao.sqlts.insert.sql"})
+@EnableJpaRepositories(value = {"org.thingsboard.server.dao.sqlts.ts", "org.thingsboard.server.dao.sqlts.insert.sql"}, bootstrapMode = BootstrapMode.LAZY)
@EntityScan({"org.thingsboard.server.dao.model.sqlts.ts"})
@EnableTransactionManagement
@SqlTsDao
diff --git a/dao/src/main/java/org/thingsboard/server/dao/SqlTsLatestDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/SqlTsLatestDaoConfig.java
index 1793c4ed76..6fcf21a0a8 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/SqlTsLatestDaoConfig.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/SqlTsLatestDaoConfig.java
@@ -19,6 +19,7 @@ import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.data.repository.config.BootstrapMode;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.thingsboard.server.dao.util.SqlTsLatestDao;
import org.thingsboard.server.dao.util.TbAutoConfiguration;
@@ -26,7 +27,7 @@ import org.thingsboard.server.dao.util.TbAutoConfiguration;
@Configuration
@TbAutoConfiguration
@ComponentScan({"org.thingsboard.server.dao.sqlts.sql"})
-@EnableJpaRepositories({"org.thingsboard.server.dao.sqlts.insert.latest.sql", "org.thingsboard.server.dao.sqlts.latest"})
+@EnableJpaRepositories(value = {"org.thingsboard.server.dao.sqlts.insert.latest.sql", "org.thingsboard.server.dao.sqlts.latest"}, bootstrapMode = BootstrapMode.LAZY)
@EntityScan({"org.thingsboard.server.dao.model.sqlts.latest"})
@EnableTransactionManagement
@SqlTsLatestDao
diff --git a/dao/src/main/java/org/thingsboard/server/dao/TimescaleDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/TimescaleDaoConfig.java
index 16617c12a8..c134b88897 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/TimescaleDaoConfig.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/TimescaleDaoConfig.java
@@ -19,6 +19,7 @@ import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.data.repository.config.BootstrapMode;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.thingsboard.server.dao.util.TbAutoConfiguration;
import org.thingsboard.server.dao.util.TimescaleDBTsDao;
@@ -26,7 +27,7 @@ import org.thingsboard.server.dao.util.TimescaleDBTsDao;
@Configuration
@TbAutoConfiguration
@ComponentScan({"org.thingsboard.server.dao.sqlts.timescale"})
-@EnableJpaRepositories({"org.thingsboard.server.dao.sqlts.timescale", "org.thingsboard.server.dao.sqlts.insert.timescale"})
+@EnableJpaRepositories(value = {"org.thingsboard.server.dao.sqlts.timescale", "org.thingsboard.server.dao.sqlts.insert.timescale"}, bootstrapMode = BootstrapMode.LAZY)
@EntityScan({"org.thingsboard.server.dao.model.sqlts.timescale"})
@EnableTransactionManagement
@TimescaleDBTsDao
diff --git a/dao/src/main/java/org/thingsboard/server/dao/TimescaleTsLatestDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/TimescaleTsLatestDaoConfig.java
index f0e6fc362b..f0e74f1c3d 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/TimescaleTsLatestDaoConfig.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/TimescaleTsLatestDaoConfig.java
@@ -19,6 +19,7 @@ import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.data.repository.config.BootstrapMode;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.thingsboard.server.dao.util.TbAutoConfiguration;
import org.thingsboard.server.dao.util.TimescaleDBTsLatestDao;
@@ -26,7 +27,7 @@ import org.thingsboard.server.dao.util.TimescaleDBTsLatestDao;
@Configuration
@TbAutoConfiguration
@ComponentScan({"org.thingsboard.server.dao.sqlts.timescale"})
-@EnableJpaRepositories({"org.thingsboard.server.dao.sqlts.insert.latest.sql", "org.thingsboard.server.dao.sqlts.latest"})
+@EnableJpaRepositories(value = {"org.thingsboard.server.dao.sqlts.insert.latest.sql", "org.thingsboard.server.dao.sqlts.latest"}, bootstrapMode = BootstrapMode.LAZY)
@EntityScan({"org.thingsboard.server.dao.model.sqlts.latest"})
@EnableTransactionManagement
@TimescaleDBTsLatestDao
diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java
new file mode 100644
index 0000000000..4e8a2cd503
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java
@@ -0,0 +1,112 @@
+/**
+ * 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.dao.mobile;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.event.TransactionalEventListener;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.mobile.AndroidConfig;
+import org.thingsboard.server.common.data.mobile.BadgePosition;
+import org.thingsboard.server.common.data.mobile.BadgeStyle;
+import org.thingsboard.server.common.data.mobile.IosConfig;
+import org.thingsboard.server.common.data.mobile.MobileAppSettings;
+import org.thingsboard.server.common.data.mobile.QRCodeConfig;
+import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
+import org.thingsboard.server.dao.service.DataValidator;
+
+import java.util.Map;
+
+import static org.thingsboard.server.dao.service.Validator.validateId;
+
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class BaseMobileAppSettingsService extends AbstractCachedEntityService implements MobileAppSettingsService {
+
+ public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
+ private static final String DEFAULT_QR_CODE_LABEL = "Scan to connect or download mobile app";
+
+ private final MobileAppSettingsDao mobileAppSettingsDao;
+ private final DataValidator mobileAppSettingsDataValidator;
+
+ @Override
+ public MobileAppSettings saveMobileAppSettings(TenantId tenantId, MobileAppSettings mobileAppSettings) {
+ mobileAppSettingsDataValidator.validate(mobileAppSettings, s -> tenantId);
+ try {
+ MobileAppSettings savedMobileAppSettings = mobileAppSettingsDao.save(tenantId, mobileAppSettings);
+ publishEvictEvent(new MobileAppSettingsEvictEvent(tenantId));
+ return savedMobileAppSettings;
+ } catch (Exception e) {
+ handleEvictEvent(new MobileAppSettingsEvictEvent(tenantId));
+ checkConstraintViolation(e, Map.of(
+ "mobile_app_settings_tenant_id_unq_key", "Mobile application for specified tenant already exists!"
+ ));
+ throw e;
+ }
+ }
+
+ @Override
+ public MobileAppSettings getMobileAppSettings(TenantId tenantId) {
+ log.trace("Executing getMobileAppSettings for tenant [{}] ", tenantId);
+ MobileAppSettings mobileAppSettings = cache.getAndPutInTransaction(tenantId,
+ () -> mobileAppSettingsDao.findByTenantId(tenantId), true);
+ return constructMobileAppSettings(mobileAppSettings);
+ }
+
+ @Override
+ public void deleteByTenantId(TenantId tenantId) {
+ log.trace("Executing deleteByTenantId, tenantId [{}]", tenantId);
+ validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
+ mobileAppSettingsDao.removeByTenantId(tenantId);
+ }
+
+ @TransactionalEventListener(classes = MobileAppSettingsEvictEvent.class)
+ @Override
+ public void handleEvictEvent(MobileAppSettingsEvictEvent event) {
+ cache.evict(event.getTenantId());
+ }
+
+ private MobileAppSettings constructMobileAppSettings(MobileAppSettings mobileAppSettings) {
+ if (mobileAppSettings == null) {
+ mobileAppSettings = new MobileAppSettings();
+ mobileAppSettings.setUseDefaultApp(true);
+
+ AndroidConfig androidConfig = AndroidConfig.builder()
+ .enabled(true)
+ .build();
+ IosConfig iosConfig = IosConfig.builder()
+ .enabled(true)
+ .build();
+ QRCodeConfig qrCodeConfig = QRCodeConfig.builder()
+ .showOnHomePage(true)
+ .qrCodeLabelEnabled(true)
+ .qrCodeLabel(DEFAULT_QR_CODE_LABEL)
+ .badgeEnabled(true)
+ .badgePosition(BadgePosition.RIGHT)
+ .badgeStyle(BadgeStyle.ORIGINAL)
+ .badgeEnabled(true)
+ .build();
+
+ mobileAppSettings.setQrCodeConfig(qrCodeConfig);
+ mobileAppSettings.setAndroidConfig(androidConfig);
+ mobileAppSettings.setIosConfig(iosConfig);
+ }
+ return mobileAppSettings;
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsCaffeineCache.java
new file mode 100644
index 0000000000..9e15ae8572
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsCaffeineCache.java
@@ -0,0 +1,34 @@
+/**
+ * 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.dao.mobile;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.cache.CacheManager;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.cache.CaffeineTbTransactionalCache;
+import org.thingsboard.server.common.data.CacheConstants;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.mobile.MobileAppSettings;
+
+@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
+@Service("MobileAppCache")
+public class MobileAppSettingsCaffeineCache extends CaffeineTbTransactionalCache {
+
+ public MobileAppSettingsCaffeineCache(CacheManager cacheManager) {
+ super(cacheManager, CacheConstants.MOBILE_APP_SETTINGS_CACHE);
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsDao.java
new file mode 100644
index 0000000000..094ead1a5b
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsDao.java
@@ -0,0 +1,28 @@
+/**
+ * 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.dao.mobile;
+
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.mobile.MobileAppSettings;
+import org.thingsboard.server.dao.Dao;
+
+
+public interface MobileAppSettingsDao extends Dao {
+
+ MobileAppSettings findByTenantId(TenantId tenantId);
+
+ void removeByTenantId(TenantId tenantId);
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsEvictEvent.java
new file mode 100644
index 0000000000..459e61ef6d
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsEvictEvent.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.dao.mobile;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.id.TenantId;
+
+@Data
+public class MobileAppSettingsEvictEvent {
+ private final TenantId tenantId;
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsRedisCache.java
new file mode 100644
index 0000000000..aac802c1ec
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsRedisCache.java
@@ -0,0 +1,36 @@
+/**
+ * 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.dao.mobile;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.cache.CacheSpecsMap;
+import org.thingsboard.server.cache.RedisTbTransactionalCache;
+import org.thingsboard.server.cache.TBRedisCacheConfiguration;
+import org.thingsboard.server.cache.TbJsonRedisSerializer;
+import org.thingsboard.server.common.data.CacheConstants;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.mobile.MobileAppSettings;
+
+@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
+@Service("MobileAppCache")
+public class MobileAppSettingsRedisCache extends RedisTbTransactionalCache {
+
+ public MobileAppSettingsRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
+ super(CacheConstants.MOBILE_APP_SETTINGS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(MobileAppSettings.class));
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsService.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsService.java
new file mode 100644
index 0000000000..4645947382
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsService.java
@@ -0,0 +1,29 @@
+/**
+ * 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.dao.mobile;
+
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.mobile.MobileAppSettings;
+
+public interface MobileAppSettingsService {
+
+ MobileAppSettings saveMobileAppSettings(TenantId tenantId, MobileAppSettings settings);
+
+ MobileAppSettings getMobileAppSettings(TenantId tenantId);
+
+ void deleteByTenantId(TenantId tenantId);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
index 8f0502efbf..fb77ad6987 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
@@ -660,6 +660,15 @@ public class ModelConstants {
public static final String NOTIFICATION_TEMPLATE_NOTIFICATION_TYPE_PROPERTY = "notification_type";
public static final String NOTIFICATION_TEMPLATE_CONFIGURATION_PROPERTY = "configuration";
+ /**
+ * Mobile application settings constants.
+ */
+ public static final String MOBILE_APP_SETTINGS_TABLE_NAME = "mobile_app_settings";
+ public static final String MOBILE_APP_SETTINGS_USE_DEFAULT_APP_PROPERTY = "use_default_app";
+ public static final String MOBILE_APP_SETTINGS_ANDROID_CONFIG_PROPERTY = "android_config";
+ public static final String MOBILE_APP_SETTINGS_IOS_CONFIG_PROPERTY = "ios_config";
+ public static final String MOBILE_APP_SETTINGS_QR_CODE_CONFIG_PROPERTY = "qr_code_config";
+
protected static final String[] NONE_AGGREGATION_COLUMNS = new String[]{LONG_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BOOLEAN_VALUE_COLUMN, STRING_VALUE_COLUMN, JSON_VALUE_COLUMN, KEY_COLUMN, TS_COLUMN};
protected static final String[] COUNT_AGGREGATION_COLUMNS = new String[]{count(LONG_VALUE_COLUMN), count(DOUBLE_VALUE_COLUMN), count(BOOLEAN_VALUE_COLUMN), count(STRING_VALUE_COLUMN), count(JSON_VALUE_COLUMN), max(TS_COLUMN)};
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppSettingsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppSettingsEntity.java
new file mode 100644
index 0000000000..cd38edd9cb
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppSettingsEntity.java
@@ -0,0 +1,85 @@
+/**
+ * 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.dao.model.sql;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import jakarta.persistence.Column;
+import jakarta.persistence.Convert;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Table;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import org.thingsboard.server.common.data.id.MobileAppSettingsId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.mobile.AndroidConfig;
+import org.thingsboard.server.common.data.mobile.IosConfig;
+import org.thingsboard.server.common.data.mobile.MobileAppSettings;
+import org.thingsboard.server.common.data.mobile.QRCodeConfig;
+import org.thingsboard.server.dao.model.BaseSqlEntity;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.dao.util.mapping.JsonConverter;
+
+import java.util.UUID;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@NoArgsConstructor
+@Entity
+@Table(name = ModelConstants.MOBILE_APP_SETTINGS_TABLE_NAME)
+public class MobileAppSettingsEntity extends BaseSqlEntity {
+
+ @Column(name = ModelConstants.TENANT_ID_COLUMN, columnDefinition = "uuid")
+ protected UUID tenantId;
+
+ @Column(name = ModelConstants.MOBILE_APP_SETTINGS_USE_DEFAULT_APP_PROPERTY)
+ private boolean useDefaultApp;
+
+ @Convert(converter = JsonConverter.class)
+ @Column(name = ModelConstants.MOBILE_APP_SETTINGS_ANDROID_CONFIG_PROPERTY)
+ private JsonNode androidConfig;
+
+ @Convert(converter = JsonConverter.class)
+ @Column(name = ModelConstants.MOBILE_APP_SETTINGS_IOS_CONFIG_PROPERTY)
+ private JsonNode iosConfig;
+
+ @Convert(converter = JsonConverter.class)
+ @Column(name = ModelConstants.MOBILE_APP_SETTINGS_QR_CODE_CONFIG_PROPERTY)
+ private JsonNode qrCodeConfig;
+
+ public MobileAppSettingsEntity(MobileAppSettings mobileAppSettings) {
+ this.setId(mobileAppSettings.getUuidId());
+ this.setCreatedTime(mobileAppSettings.getCreatedTime());
+ this.tenantId = mobileAppSettings.getTenantId().getId();
+ this.useDefaultApp = mobileAppSettings.isUseDefaultApp();
+ this.androidConfig = toJson(mobileAppSettings.getAndroidConfig());
+ this.iosConfig = toJson(mobileAppSettings.getIosConfig());
+ this.qrCodeConfig = toJson(mobileAppSettings.getQrCodeConfig());
+ }
+
+ @Override
+ public MobileAppSettings toData() {
+ MobileAppSettings mobileAppSettings = new MobileAppSettings(new MobileAppSettingsId(getUuid()));
+ mobileAppSettings.setCreatedTime(createdTime);
+ mobileAppSettings.setTenantId(TenantId.fromUUID(tenantId));
+ mobileAppSettings.setUseDefaultApp(useDefaultApp);
+ mobileAppSettings.setAndroidConfig(fromJson(androidConfig, AndroidConfig.class));
+ mobileAppSettings.setIosConfig(fromJson(iosConfig, IosConfig.class));
+ mobileAppSettings.setQrCodeConfig(fromJson(qrCodeConfig, QRCodeConfig.class));
+ return mobileAppSettings;
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppSettingsDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppSettingsDataValidator.java
new file mode 100644
index 0000000000..c867b08cc4
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppSettingsDataValidator.java
@@ -0,0 +1,51 @@
+/**
+ * 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.dao.service.validator;
+
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.mobile.AndroidConfig;
+import org.thingsboard.server.common.data.mobile.IosConfig;
+import org.thingsboard.server.common.data.mobile.MobileAppSettings;
+import org.thingsboard.server.common.data.mobile.QRCodeConfig;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.service.DataValidator;
+
+@Component
+@AllArgsConstructor
+public class MobileAppSettingsDataValidator extends DataValidator {
+
+ @Override
+ protected void validateDataImpl(TenantId tenantId, MobileAppSettings mobileAppSettings) {
+ AndroidConfig androidConfig = mobileAppSettings.getAndroidConfig();
+ IosConfig iosConfig = mobileAppSettings.getIosConfig();
+ QRCodeConfig qrCodeConfig = mobileAppSettings.getQrCodeConfig();
+ if (!mobileAppSettings.isUseDefaultApp() && (androidConfig == null || iosConfig == null)) {
+ throw new DataValidationException("Android/ios settings are required to use custom application!");
+ }
+ if (qrCodeConfig == null) {
+ throw new DataValidationException("Qr code configuration is required!");
+ }
+ if (androidConfig != null && androidConfig.isEnabled() && !mobileAppSettings.isUseDefaultApp() &&
+ (androidConfig.getAppPackage() == null || androidConfig.getSha256CertFingerprints() == null)) {
+ throw new DataValidationException("Application package and sha256 cert fingerprints are required for custom android application!");
+ }
+ if (iosConfig != null && iosConfig.isEnabled() && !mobileAppSettings.isUseDefaultApp() && iosConfig.getAppId() == null) {
+ throw new DataValidationException("Application id is required for custom ios application!");
+ }
+ }
+}
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