Browse Source

merge with master

pull/3740/head
ShvaykaD 6 years ago
parent
commit
e85daa81e0
  1. 15
      application/src/main/data/json/system/oauth2_config_templates/facebook_config.json
  2. 9
      application/src/main/data/json/system/oauth2_config_templates/github_config.json
  3. 15
      application/src/main/data/json/system/oauth2_config_templates/google_config.json
  4. 10
      application/src/main/data/json/system/widget_bundles/charts.json
  5. 3
      application/src/main/data/upgrade/3.1.1/schema_update_before.sql
  6. 5
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  7. 33
      application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
  8. 4
      application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
  9. 10
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  10. 2
      application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java
  11. 2
      application/src/main/java/org/thingsboard/server/controller/TenantController.java
  12. 7
      application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
  13. 257
      application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java
  14. 6
      application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
  15. 106
      application/src/main/java/org/thingsboard/server/service/profile/DefaultTbTenantProfileCache.java
  16. 38
      application/src/main/java/org/thingsboard/server/service/profile/TbTenantProfileCache.java
  17. 10
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTenantRoutingInfoService.java
  18. 73
      application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java
  19. 16
      application/src/main/java/org/thingsboard/server/utils/MiscUtils.java
  20. 1
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  21. 12
      application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java
  22. 41
      application/src/test/java/org/thingsboard/server/mqtt/AbstractMqttIntegrationTest.java
  23. 3
      application/src/test/java/org/thingsboard/server/mqtt/MqttSqlTestSuite.java
  24. 285
      application/src/test/java/org/thingsboard/server/mqtt/provision/AbstractMqttProvisionJsonDeviceTest.java
  25. 305
      application/src/test/java/org/thingsboard/server/mqtt/provision/AbstractMqttProvisionProtoDeviceTest.java
  26. 23
      application/src/test/java/org/thingsboard/server/mqtt/provision/sql/MqttProvisionDeviceJsonSqlTest.java
  27. 23
      application/src/test/java/org/thingsboard/server/mqtt/provision/sql/MqttProvisionDeviceProtoSqlTest.java
  28. 28
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProvisionService.java
  29. 4
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
  30. 22
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionFailedException.java
  31. 31
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionRequest.java
  32. 25
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionResponse.java
  33. 23
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionResponseStatus.java
  34. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateService.java
  35. 16
      common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
  36. 3
      common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java
  37. 22
      common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileProvisionType.java
  38. 4
      common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java
  39. 27
      common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/ProvisionDeviceCredentialsData.java
  40. 31
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AllowCreateNewDevicesDeviceProfileProvisionConfiguration.java
  41. 31
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.java
  42. 1
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java
  43. 41
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java
  44. 31
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DisabledDeviceProfileProvisionConfiguration.java
  45. 14
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttTopics.java
  46. 24
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/ProvisionDeviceProfileCredentials.java
  47. 6
      common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java
  48. 2
      common/message/src/main/java/org/thingsboard/server/common/msg/session/FeatureType.java
  49. 47
      common/queue/src/main/proto/queue.proto
  50. 39
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java
  51. 3
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapTransportAdaptor.java
  52. 10
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java
  53. 28
      common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java
  54. 113
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
  55. 15
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java
  56. 6
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java
  57. 17
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java
  58. 9
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java
  59. 10
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java
  60. 95
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java
  61. 3
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java
  62. 22
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
  63. 8
      dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
  64. 1
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java
  65. 2
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java
  66. 7
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java
  67. 49
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
  68. 2
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  69. 12
      dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java
  70. 50
      dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationTemplateEntity.java
  71. 4
      dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationTemplateDao.java
  72. 17
      dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateServiceImpl.java
  73. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java
  74. 1
      dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java
  75. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java
  76. 7
      dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationTemplateDao.java
  77. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationTemplateRepository.java
  78. 3
      dao/src/main/resources/sql/schema-entities-hsql.sql
  79. 3
      dao/src/main/resources/sql/schema-entities.sql
  80. 10
      dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ConfigTemplateServiceTest.java
  81. 18
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java
  82. 8
      ui-ngx/proxy.conf.js
  83. 21
      ui-ngx/src/app/app.component.ts
  84. 18
      ui-ngx/src/app/core/auth/auth.service.ts
  85. 22
      ui-ngx/src/app/core/http/admin.service.ts
  86. 44
      ui-ngx/src/app/core/http/oauth2.service.ts
  87. 1
      ui-ngx/src/app/core/http/public-api.ts
  88. 12
      ui-ngx/src/app/core/utils.ts
  89. 6
      ui-ngx/src/app/modules/home/components/device/device-credentials.component.html
  90. 7
      ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts
  91. 21
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  92. 8
      ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html
  93. 29
      ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts
  94. 124
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html
  95. 20
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.scss
  96. 150
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts
  97. 8
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html
  98. 11
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.scss
  99. 85
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts
  100. 118
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html

15
application/src/main/data/json/system/oauth2_config_templates/facebook_config.json

@ -7,14 +7,17 @@
"userInfoUri": "https://graph.facebook.com/me?fields=id,name,first_name,last_name,email",
"clientAuthenticationMethod": "BASIC",
"userNameAttributeName": "email",
"basic": {
"emailAttributeKey": "email",
"firstNameAttributeKey": "first_name",
"lastNameAttributeKey": "last_name",
"tenantNameStrategy": "DOMAIN"
"mapperConfig": {
"type": "BASIC",
"basic": {
"emailAttributeKey": "email",
"firstNameAttributeKey": "first_name",
"lastNameAttributeKey": "last_name",
"tenantNameStrategy": "DOMAIN"
}
},
"comment": null,
"loginButtonIcon": "mdi:facebook",
"loginButtonIcon": "facebook-logo",
"loginButtonLabel": "Facebook",
"helpLink": "https://developers.facebook.com/docs/facebook-login/web#logindialog"
}

9
application/src/main/data/json/system/oauth2_config_templates/github_config.json

@ -7,11 +7,14 @@
"userInfoUri": "https://api.github.com/user",
"clientAuthenticationMethod": "BASIC",
"userNameAttributeName": "login",
"basic": {
"tenantNameStrategy": "DOMAIN"
"mapperConfig": {
"type": "GITHUB",
"basic": {
"tenantNameStrategy": "DOMAIN"
}
},
"comment": "In order to log into ThingsBoard you need to have user's email. You may configure and use Custom OAuth2 Mapper to get email information. Please refer to <a href=\"https://docs.github.com/en/rest/reference/users#list-email-addresses-for-the-authenticated-user\">Github Documentation</a>",
"loginButtonIcon": "mdi:github",
"loginButtonIcon": "github-logo",
"loginButtonLabel": "Github",
"helpLink": "https://docs.github.com/en/developers/apps/creating-an-oauth-app"
}

15
application/src/main/data/json/system/oauth2_config_templates/google_config.json

@ -8,14 +8,17 @@
"userInfoUri": "https://openidconnect.googleapis.com/v1/userinfo",
"clientAuthenticationMethod": "BASIC",
"userNameAttributeName": "email",
"basic": {
"emailAttributeKey": "email",
"firstNameAttributeKey": "given_name",
"lastNameAttributeKey": "family_name",
"tenantNameStrategy": "DOMAIN"
"mapperConfig": {
"type": "BASIC",
"basic": {
"emailAttributeKey": "email",
"firstNameAttributeKey": "given_name",
"lastNameAttributeKey": "family_name",
"tenantNameStrategy": "DOMAIN"
}
},
"comment": null,
"loginButtonIcon": "mdi:google",
"loginButtonIcon": "google-logo",
"loginButtonLabel": "Google",
"helpLink": "https://developers.google.com/adwords/api/docs/guides/authentication"
}

10
application/src/main/data/json/system/widget_bundles/charts.json

@ -19,7 +19,7 @@
],
"templateHtml": "<canvas id=\"barChart\"></canvas>\n",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n var barData = {\n labels: [],\n datasets: []\n };\n \n for (var i = 0; i < self.ctx.datasources.length; i++) {\n var datasource = self.ctx.datasources[i];\n for (var d = 0; d < datasource.dataKeys.length; d++) {\n var dataset = {\n label: datasource.dataKeys[d].label,\n data: [0],\n backgroundColor: [datasource.dataKeys[d].color],\n borderColor: [datasource.dataKeys[d].color],\n borderWidth: 1\n }\n barData.datasets.push(dataset);\n }\n }\n\n var ctx = $('#barChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'bar',\n data: barData,\n options: {\n responsive: false,\n maintainAspectRatio: false,\n scales: {\n yAxes: [{\n ticks: {\n beginAtZero:true\n }\n }]\n }\n }\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n var c = 0;\n for (var i = 0; i < self.ctx.chart.data.datasets.length; i++) {\n var dataset = self.ctx.chart.data.datasets[i];\n var cellData = self.ctx.data[i]; \n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = tvPair[1];\n dataset.data[0] = parseFloat(value);\n }\n }\n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n self.ctx.chart.resize();\n}\n\n",
"controllerScript": "self.onInit = function() {\n var barData = {\n labels: [],\n datasets: []\n };\n \n for (var i = 0; i < self.ctx.datasources.length; i++) {\n var datasource = self.ctx.datasources[i];\n for (var d = 0; d < datasource.dataKeys.length; d++) {\n var dataKey = datasource.dataKeys[d];\n var units = dataKey.units && dataKey.units.length ? dataKey.units : self.ctx.units;\n units = units ? (' (' + units + ')') : '';\n var dataset = {\n label: dataKey.label + units,\n data: [0],\n backgroundColor: [dataKey.color],\n borderColor: [dataKey.color],\n borderWidth: 1\n }\n barData.datasets.push(dataset);\n }\n }\n\n var ctx = $('#barChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'bar',\n data: barData,\n options: {\n responsive: false,\n maintainAspectRatio: false,\n scales: {\n yAxes: [{\n ticks: {\n beginAtZero:true\n }\n }]\n }\n }\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n var c = 0;\n for (var i = 0; i < self.ctx.chart.data.datasets.length; i++) {\n var dataset = self.ctx.chart.data.datasets[i];\n var cellData = self.ctx.data[i]; \n if (cellData.data.length > 0) {\n var decimals;\n if (typeof cellData.dataKey.decimals !== 'undefined' \n && cellData.dataKey.decimals !== null ) {\n decimals = cellData.dataKey.decimals; \n } else {\n decimals = self.ctx.decimals;\n }\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = self.ctx.utils.formatValue(tvPair[1], decimals);\n dataset.data[0] = parseFloat(value);\n }\n }\n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n self.ctx.chart.resize();\n}\n\n",
"settingsSchema": "{}",
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Bars - Chart.js\"}"
@ -55,7 +55,7 @@
],
"templateHtml": "<canvas id=\"pieChart\"></canvas>\n",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n \n var borderColor = self.ctx.settings.borderColor || '#fff';\n var borderWidth = typeof self.ctx.settings.borderWidth !== 'undefined' ? self.ctx.settings.borderWidth : 5;\n \n pieData.datasets.push(dataset);\n \n for (var i=0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n pieData.labels.push(dataKey.label);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push(borderColor);\n dataset.borderWidth.push(borderWidth);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n\n var options = {\n responsive: false,\n maintainAspectRatio: false,\n legend: {\n display: true,\n labels: {\n fontColor: '#666'\n }\n },\n tooltips: {\n callbacks: {\n label: function(tooltipItem, data) {\n var label = data.labels[tooltipItem.index];\n var value = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];\n var content = label + ': ' + value;\n var units = self.ctx.settings.units ? self.ctx.settings.units : self.ctx.units;\n if (units) {\n content += ' ' + units;\n } \n return content;\n }\n }\n }\n };\n\n if (self.ctx.settings.legend) {\n options.legend.display = self.ctx.settings.legend.display !== false;\n options.legend.labels.fontColor = self.ctx.settings.legend.labelsFontColor || '#666';\n }\n\n var ctx = $('#pieChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'doughnut',\n data: pieData,\n options: options\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = tvPair[1];\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n self.ctx.chart.resize();\n}\n\n",
"controllerScript": "self.onInit = function() {\n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n \n var borderColor = self.ctx.settings.borderColor || '#fff';\n var borderWidth = typeof self.ctx.settings.borderWidth !== 'undefined' ? self.ctx.settings.borderWidth : 5;\n \n pieData.datasets.push(dataset);\n \n for (var i=0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n var units = dataKey.units && dataKey.units.length ? dataKey.units : self.ctx.units;\n units = units ? (' (' + units + ')') : '';\n pieData.labels.push(dataKey.label + units);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push(borderColor);\n dataset.borderWidth.push(borderWidth);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n\n var options = {\n responsive: false,\n maintainAspectRatio: false,\n legend: {\n display: true,\n labels: {\n fontColor: '#666'\n }\n },\n tooltips: {\n callbacks: {\n label: function(tooltipItem, data) {\n var label = data.labels[tooltipItem.index];\n var value = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];\n var content = label + ': ' + value;\n var units = self.ctx.settings.units ? self.ctx.settings.units : self.ctx.units;\n if (units) {\n content += ' ' + units;\n } \n return content;\n }\n }\n }\n };\n\n if (self.ctx.settings.legend) {\n options.legend.display = self.ctx.settings.legend.display !== false;\n options.legend.labels.fontColor = self.ctx.settings.legend.labelsFontColor || '#666';\n }\n\n var ctx = $('#pieChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'doughnut',\n data: pieData,\n options: options\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var decimals;\n if (typeof cellData.dataKey.decimals !== 'undefined' \n && cellData.dataKey.decimals !== null ) {\n decimals = cellData.dataKey.decimals; \n } else {\n decimals = self.ctx.decimals;\n }\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = self.ctx.utils.formatValue(tvPair[1], decimals);\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n self.ctx.chart.resize();\n}\n\n",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"borderWidth\": {\n \"title\": \"Border width\",\n \"type\": \"number\",\n \"default\": 5\n },\n \"borderColor\": {\n \"title\": \"Border color\",\n \"type\": \"string\",\n \"default\": \"#fff\"\n },\n \"legend\": {\n \"title\": \"Legend settings\",\n \"type\": \"object\",\n \"properties\": {\n \"display\": {\n \"title\": \"Display legend\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"labelsFontColor\": {\n \"title\": \"Labels font color\",\n \"type\": \"string\",\n \"default\": \"#666\"\n }\n }\n }\n },\n \"required\": []\n },\n \"form\": [\n \"borderWidth\", \n {\n \"key\": \"borderColor\",\n \"type\": \"color\"\n }, \n {\n \"key\": \"legend\",\n \"items\": [\n \"legend.display\",\n {\n \"key\": \"legend.labelsFontColor\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}",
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#26a69a\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#f57c00\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#afb42b\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#673ab7\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"borderWidth\":5,\"borderColor\":\"#fff\",\"legend\":{\"display\":true,\"labelsFontColor\":\"#666666\"}},\"title\":\"Doughnut - Chart.js\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}"
@ -91,7 +91,7 @@
],
"templateHtml": "<canvas id=\"pieChart\"></canvas>\n",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n \n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n \n pieData.datasets.push(dataset);\n \n for (var i=0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n pieData.labels.push(dataKey.label);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n var borderColor = tinycolor(dataKey.color).darken();\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push('#fff');\n dataset.borderWidth.push(5);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n\n var ctx = $('#pieChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'pie',\n data: pieData,\n options: {\n responsive: false,\n maintainAspectRatio: false\n }\n }); \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = tvPair[1];\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n self.ctx.chart.resize();\n}\n",
"controllerScript": "self.onInit = function() {\n \n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n \n pieData.datasets.push(dataset);\n \n for (var i=0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n var units = dataKey.units && dataKey.units.length ? dataKey.units : self.ctx.units;\n units = units ? (' (' + units + ')') : '';\n pieData.labels.push(dataKey.label + units);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n var borderColor = tinycolor(dataKey.color).darken();\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push('#fff');\n dataset.borderWidth.push(5);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n\n var ctx = $('#pieChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'pie',\n data: pieData,\n options: {\n responsive: false,\n maintainAspectRatio: false\n }\n }); \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var decimals;\n if (typeof cellData.dataKey.decimals !== 'undefined' \n && cellData.dataKey.decimals !== null ) {\n decimals = cellData.dataKey.decimals; \n } else {\n decimals = self.ctx.decimals;\n }\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = self.ctx.utils.formatValue(tvPair[1], decimals);\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n self.ctx.chart.resize();\n}\n",
"settingsSchema": "{}",
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Pie - Chart.js\"}"
@ -111,7 +111,7 @@
],
"templateHtml": "<canvas id=\"pieChart\"></canvas>\n",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n \n pieData.datasets.push(dataset);\n \n for (var i = 0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n pieData.labels.push(dataKey.label);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n var borderColor = tinycolor(dataKey.color).darken();\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push('#fff');\n dataset.borderWidth.push(5);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n\n var ctx = $('#pieChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'polarArea',\n data: pieData,\n options: {\n responsive: false,\n maintainAspectRatio: false\n }\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = tvPair[1];\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n \n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n if (self.ctx.height >= 70) {\n try {\n self.ctx.chart.resize();\n } catch (e) {}\n }\n}\n",
"controllerScript": "self.onInit = function() {\n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n \n pieData.datasets.push(dataset);\n \n for (var i = 0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n var units = dataKey.units && dataKey.units.length ? dataKey.units : self.ctx.units;\n units = units ? (' (' + units + ')') : '';\n pieData.labels.push(dataKey.label + units);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n var borderColor = tinycolor(dataKey.color).darken();\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push('#fff');\n dataset.borderWidth.push(5);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n\n var ctx = $('#pieChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'polarArea',\n data: pieData,\n options: {\n responsive: false,\n maintainAspectRatio: false\n }\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var decimals;\n if (typeof cellData.dataKey.decimals !== 'undefined' \n && cellData.dataKey.decimals !== null ) {\n decimals = cellData.dataKey.decimals; \n } else {\n decimals = self.ctx.decimals;\n }\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = self.ctx.utils.formatValue(tvPair[1], decimals);\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n \n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n if (self.ctx.height >= 70) {\n try {\n self.ctx.chart.resize();\n } catch (e) {}\n }\n}\n",
"settingsSchema": "{}",
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fifth\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.2074391823443591,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Polar Area - Chart.js\"}"
@ -131,7 +131,7 @@
],
"templateHtml": "<canvas id=\"radarChart\"></canvas>\n",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n var barData = {\n labels: [],\n datasets: []\n };\n\n var backgroundColor = tinycolor(self.ctx.data[0].dataKey.color);\n backgroundColor.setAlpha(0.2);\n var borderColor = tinycolor(self.ctx.data[0].dataKey.color);\n borderColor.setAlpha(1);\n var dataset = {\n label: self.ctx.datasources[0].name,\n data: [],\n backgroundColor: backgroundColor.toRgbString(),\n borderColor: borderColor.toRgbString(),\n pointBackgroundColor: borderColor.toRgbString(),\n pointBorderColor: borderColor.darken().toRgbString(),\n borderWidth: 1\n }\n \n barData.datasets.push(dataset);\n \n for (var i = 0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n barData.labels.push(dataKey.label);\n dataset.data.push(0);\n }\n\n var ctx = $('#radarChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'radar',\n data: barData,\n options: {\n responsive: false,\n maintainAspectRatio: false\n }\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = tvPair[1];\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n } \n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n if (self.ctx.height >= 70) {\n self.ctx.chart.resize();\n }\n}\n",
"controllerScript": "self.onInit = function() {\n var barData = {\n labels: [],\n datasets: []\n };\n\n var backgroundColor = tinycolor(self.ctx.data[0].dataKey.color);\n backgroundColor.setAlpha(0.2);\n var borderColor = tinycolor(self.ctx.data[0].dataKey.color);\n borderColor.setAlpha(1);\n var dataset = {\n label: self.ctx.datasources[0].name,\n data: [],\n backgroundColor: backgroundColor.toRgbString(),\n borderColor: borderColor.toRgbString(),\n pointBackgroundColor: borderColor.toRgbString(),\n pointBorderColor: borderColor.darken().toRgbString(),\n borderWidth: 1\n }\n \n barData.datasets.push(dataset);\n \n for (var i = 0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n var units = dataKey.units && dataKey.units.length ? dataKey.units : self.ctx.units;\n units = units ? (' (' + units + ')') : '';\n barData.labels.push(dataKey.label + units);\n dataset.data.push(0);\n }\n\n var ctx = $('#radarChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'radar',\n data: barData,\n options: {\n responsive: false,\n maintainAspectRatio: false\n }\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var decimals;\n if (typeof cellData.dataKey.decimals !== 'undefined' \n && cellData.dataKey.decimals !== null ) {\n decimals = cellData.dataKey.decimals; \n } else {\n decimals = self.ctx.decimals;\n }\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = self.ctx.utils.formatValue(tvPair[1], decimals);\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n } \n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n if (self.ctx.height >= 70) {\n self.ctx.chart.resize();\n }\n}\n",
"settingsSchema": "{}",
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Radar - Chart.js\"}"

3
application/src/main/data/upgrade/3.1.1/schema_update_before.sql

@ -89,13 +89,16 @@ CREATE TABLE IF NOT EXISTS device_profile (
name varchar(255),
type varchar(255),
transport_type varchar(255),
provision_type varchar(255),
profile_data jsonb,
description varchar,
search_text varchar(255),
is_default boolean,
tenant_id uuid,
default_rule_chain_id uuid,
provision_device_key varchar,
CONSTRAINT device_profile_name_unq_key UNIQUE (tenant_id, name),
CONSTRAINT device_provision_key_unq_key UNIQUE (provision_device_key),
CONSTRAINT fk_default_rule_chain_device_profile FOREIGN KEY (default_rule_chain_id) REFERENCES rule_chain(id)
);

5
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -72,6 +72,7 @@ import org.thingsboard.server.service.executors.ExternalCallExecutorService;
import org.thingsboard.server.service.executors.SharedEventLoopGroupService;
import org.thingsboard.server.service.mail.MailExecutorService;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.profile.TbTenantProfileCache;
import org.thingsboard.server.service.queue.TbClusterService;
import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService;
import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService;
@ -127,6 +128,10 @@ public class ActorSystemContext {
@Getter
private DeviceService deviceService;
@Autowired
@Getter
private TbTenantProfileCache tenantProfileCache;
@Autowired
@Getter
private TbDeviceProfileCache deviceProfileCache;

33
application/src/main/java/org/thingsboard/server/actors/app/AppActor.java

@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.MsgType;
@ -40,7 +41,9 @@ import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg;
import org.thingsboard.server.common.msg.queue.RuleEngineException;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.service.profile.TbTenantProfileCache;
import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper;
import java.util.HashSet;
@ -50,13 +53,14 @@ import java.util.Set;
@Slf4j
public class AppActor extends ContextAwareActor {
private static final TenantId SYSTEM_TENANT = new TenantId(ModelConstants.NULL_UUID);
private final TbTenantProfileCache tenantProfileCache;
private final TenantService tenantService;
private final Set<TenantId> deletedTenants;
private boolean ruleChainsInitialized;
private AppActor(ActorSystemContext systemContext) {
super(systemContext);
this.tenantProfileCache = systemContext.getTenantProfileCache();
this.tenantService = systemContext.getTenantService();
this.deletedTenants = new HashSet<>();
}
@ -117,8 +121,7 @@ public class AppActor extends ContextAwareActor {
boolean isRuleEngine = systemContext.getServiceInfoProvider().isService(ServiceType.TB_RULE_ENGINE);
boolean isCore = systemContext.getServiceInfoProvider().isService(ServiceType.TB_CORE);
for (Tenant tenant : tenantIterator) {
// TODO: Tenant Profile from cache
TenantProfile tenantProfile = systemContext.getTenantProfileService().findTenantProfileById(TenantId.SYS_TENANT_ID, tenant.getTenantProfileId());
TenantProfile tenantProfile = tenantProfileCache.get(tenant.getTenantProfileId());
if (isCore || (isRuleEngine && !tenantProfile.isIsolatedTbRuleEngine())) {
log.debug("[{}] Creating tenant actor", tenant.getId());
getOrCreateTenantActor(tenant.getId());
@ -133,7 +136,7 @@ public class AppActor extends ContextAwareActor {
}
private void onQueueToRuleEngineMsg(QueueToRuleEngineMsg msg) {
if (SYSTEM_TENANT.equals(msg.getTenantId())) {
if (TenantId.SYS_TENANT_ID.equals(msg.getTenantId())) {
msg.getTbMsg().getCallback().onFailure(new RuleEngineException("Message has system tenant id!"));
} else {
if (!deletedTenants.contains(msg.getTenantId())) {
@ -146,15 +149,23 @@ public class AppActor extends ContextAwareActor {
private void onComponentLifecycleMsg(ComponentLifecycleMsg msg) {
TbActorRef target = null;
if (SYSTEM_TENANT.equals(msg.getTenantId())) {
log.warn("Message has system tenant id: {}", msg);
if (TenantId.SYS_TENANT_ID.equals(msg.getTenantId())) {
if (msg.getEntityId().getEntityType() == EntityType.TENANT_PROFILE) {
tenantProfileCache.evict(new TenantProfileId(msg.getEntityId().getId()));
} else {
log.warn("Message has system tenant id: {}", msg);
}
} else {
if (msg.getEntityId().getEntityType() == EntityType.TENANT
&& msg.getEvent() == ComponentLifecycleEvent.DELETED) {
log.info("[{}] Handling tenant deleted notification: {}", msg.getTenantId(), msg);
if (msg.getEntityId().getEntityType() == EntityType.TENANT) {
TenantId tenantId = new TenantId(msg.getEntityId().getId());
deletedTenants.add(tenantId);
ctx.stop(new TbEntityActorId(tenantId));
tenantProfileCache.evict(tenantId);
if (msg.getEvent() == ComponentLifecycleEvent.DELETED) {
log.info("[{}] Handling tenant deleted notification: {}", msg.getTenantId(), msg);
deletedTenants.add(tenantId);
ctx.stop(new TbEntityActorId(tenantId));
} else {
target = getOrCreateTenantActor(msg.getTenantId());
}
} else {
target = getOrCreateTenantActor(msg.getTenantId());
}

4
application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java

@ -77,9 +77,7 @@ public class TenantActor extends RuleChainManagerActor {
// This Service may be started for specific tenant only.
Optional<TenantId> isolatedTenantId = systemContext.getServiceInfoProvider().getIsolatedTenant();
// TODO: Tenant Profile from cache
TenantProfile tenantProfile = systemContext.getTenantProfileService().findTenantProfileById(tenantId, tenant.getTenantProfileId());
TenantProfile tenantProfile = systemContext.getTenantProfileCache().get(tenant.getTenantProfileId());
isRuleEngineForCurrentTenant = systemContext.getServiceInfoProvider().isService(ServiceType.TB_RULE_ENGINE);
isCore = systemContext.getServiceInfoProvider().isService(ServiceType.TB_CORE);

10
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -94,6 +94,7 @@ import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.profile.TbTenantProfileCache;
import org.thingsboard.server.service.queue.TbClusterService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.AccessControlService;
@ -205,6 +206,9 @@ public abstract class BaseController {
@Autowired
protected TbQueueProducerProvider producerProvider;
@Autowired
protected TbTenantProfileCache tenantProfileCache;
@Autowired
protected TbDeviceProfileCache deviceProfileCache;
@ -700,6 +704,12 @@ public abstract class BaseController {
case ASSIGNED_TO_TENANT:
msgType = DataConstants.ENTITY_ASSIGNED_TO_TENANT;
break;
case PROVISION_SUCCESS:
msgType = DataConstants.PROVISION_SUCCESS;
break;
case PROVISION_FAILURE:
msgType = DataConstants.PROVISION_FAILURE;
break;
}
if (!StringUtils.isEmpty(msgType)) {
try {

2
application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java

@ -40,7 +40,7 @@ public class OAuth2Controller extends BaseController {
@ResponseBody
public List<OAuth2ClientInfo> getOAuth2Clients(HttpServletRequest request) throws ThingsboardException {
try {
return oAuth2Service.getOAuth2Clients(MiscUtils.getScheme(request), MiscUtils.getDomainName(request));
return oAuth2Service.getOAuth2Clients(MiscUtils.getScheme(request), MiscUtils.getDomainNameAndPort(request));
} catch (Exception e) {
throw handleException(e);
}

2
application/src/main/java/org/thingsboard/server/controller/TenantController.java

@ -91,6 +91,7 @@ public class TenantController extends BaseController {
if (newTenant) {
installScripts.createDefaultRuleChains(tenant.getId());
}
tenantProfileCache.evict(tenant.getId());
return tenant;
} catch (Exception e) {
throw handleException(e);
@ -106,6 +107,7 @@ public class TenantController extends BaseController {
TenantId tenantId = new TenantId(toUUID(strTenantId));
checkTenantId(tenantId, Operation.DELETE);
tenantService.deleteTenant(tenantId);
tenantProfileCache.evict(tenantId);
tbClusterService.onEntityStateChange(tenantId, tenantId, ComponentLifecycleEvent.DELETED);
} catch (Exception e) {
throw handleException(e);

7
application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java

@ -29,9 +29,11 @@ import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
@ -93,8 +95,11 @@ public class TenantProfileController extends BaseController {
}
tenantProfile = checkNotNull(tenantProfileService.saveTenantProfile(getTenantId(), tenantProfile));
tenantProfileCache.put(tenantProfile);
tbClusterService.onEntityStateChange(TenantId.SYS_TENANT_ID, tenantProfile.getId(),
newTenantProfile ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
return tenantProfile;
} catch (Exception e) {
} catch (Exception e) {
throw handleException(e);
}
}

257
application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java

@ -0,0 +1,257 @@
/**
* Copyright © 2016-2020 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.device;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceDao;
import org.thingsboard.server.dao.device.DeviceProfileDao;
import org.thingsboard.server.dao.device.DeviceProvisionService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.device.provision.ProvisionFailedException;
import org.thingsboard.server.dao.device.provision.ProvisionRequest;
import org.thingsboard.server.dao.device.provision.ProvisionResponse;
import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus;
import org.thingsboard.server.dao.util.mapping.JacksonUtil;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg;
import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueProducer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.state.DeviceStateService;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.locks.ReentrantLock;
@Service
@Slf4j
@TbCoreComponent
public class DeviceProvisionServiceImpl implements DeviceProvisionService {
protected TbQueueProducer<TbProtoQueueMsg<ToRuleEngineMsg>> ruleEngineMsgProducer;
private static final String DEVICE_PROVISION_STATE = "provisionState";
private static final String PROVISIONED_STATE = "provisioned";
private final ReentrantLock deviceCreationLock = new ReentrantLock();
@Autowired
DeviceDao deviceDao;
@Autowired
DeviceProfileDao deviceProfileDao;
@Autowired
DeviceService deviceService;
@Autowired
DeviceCredentialsService deviceCredentialsService;
@Autowired
AttributesService attributesService;
@Autowired
DeviceStateService deviceStateService;
@Autowired
AuditLogService auditLogService;
@Autowired
PartitionService partitionService;
public DeviceProvisionServiceImpl(TbQueueProducerProvider producerProvider) {
ruleEngineMsgProducer = producerProvider.getRuleEngineMsgProducer();
}
@Override
public ProvisionResponse provisionDevice(ProvisionRequest provisionRequest) {
String provisionRequestKey = provisionRequest.getCredentials().getProvisionDeviceKey();
String provisionRequestSecret = provisionRequest.getCredentials().getProvisionDeviceSecret();
if (StringUtils.isEmpty(provisionRequestKey) || StringUtils.isEmpty(provisionRequestSecret)) {
throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name());
}
DeviceProfile targetProfile = deviceProfileDao.findByProvisionDeviceKey(provisionRequestKey);
if (targetProfile == null || targetProfile.getProfileData().getProvisionConfiguration() == null ||
targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret() == null) {
throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name());
}
Device targetDevice = deviceDao.findDeviceByTenantIdAndName(targetProfile.getTenantId().getId(), provisionRequest.getDeviceName()).orElse(null);
switch (targetProfile.getProvisionType()) {
case ALLOW_CREATE_NEW_DEVICES:
if (targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret().equals(provisionRequestSecret)) {
if (targetDevice != null) {
log.warn("[{}] The device is present and could not be provisioned once more!", targetDevice.getName());
notify(targetDevice, provisionRequest, DataConstants.PROVISION_FAILURE, false);
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
} else {
return createDevice(provisionRequest, targetProfile);
}
}
break;
case CHECK_PRE_PROVISIONED_DEVICES:
if (targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret().equals(provisionRequestSecret)) {
if (targetDevice != null && targetDevice.getDeviceProfileId().equals(targetProfile.getId())) {
return processProvision(targetDevice, provisionRequest);
} else {
log.warn("[{}] Failed to find pre provisioned device!", provisionRequest.getDeviceName());
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
}
}
break;
}
throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name());
}
private ProvisionResponse processProvision(Device device, ProvisionRequest provisionRequest) {
try {
Optional<AttributeKvEntry> provisionState = attributesService.find(device.getTenantId(), device.getId(),
DataConstants.SERVER_SCOPE, DEVICE_PROVISION_STATE).get();
if (provisionState != null && provisionState.isPresent() && !provisionState.get().getValueAsString().equals(PROVISIONED_STATE)) {
notify(device, provisionRequest, DataConstants.PROVISION_FAILURE, false);
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
} else {
saveProvisionStateAttribute(device).get();
notify(device, provisionRequest, DataConstants.PROVISION_SUCCESS, true);
}
} catch (InterruptedException | ExecutionException e) {
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
}
return new ProvisionResponse(deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), device.getId()), ProvisionResponseStatus.SUCCESS);
}
private ProvisionResponse createDevice(ProvisionRequest provisionRequest, DeviceProfile profile) {
deviceCreationLock.lock();
try {
return processCreateDevice(provisionRequest, profile);
} finally {
deviceCreationLock.unlock();
}
}
private void notify(Device device, ProvisionRequest provisionRequest, String type, boolean success) {
pushProvisionEventToRuleEngine(provisionRequest, device, type);
logAction(device.getTenantId(), device.getCustomerId(), device, success, provisionRequest);
}
private ProvisionResponse processCreateDevice(ProvisionRequest provisionRequest, DeviceProfile profile) {
Device device = deviceService.findDeviceByTenantIdAndName(profile.getTenantId(), provisionRequest.getDeviceName());
try {
if (device == null) {
Device savedDevice = deviceService.saveDevice(provisionRequest, profile);
deviceStateService.onDeviceAdded(savedDevice);
saveProvisionStateAttribute(savedDevice).get();
pushDeviceCreatedEventToRuleEngine(savedDevice);
notify(savedDevice, provisionRequest, DataConstants.PROVISION_SUCCESS, true);
return new ProvisionResponse(getDeviceCredentials(savedDevice), ProvisionResponseStatus.SUCCESS);
} else {
log.warn("[{}] The device is already provisioned!", device.getName());
notify(device, provisionRequest, DataConstants.PROVISION_FAILURE, false);
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
}
} catch (InterruptedException | ExecutionException e) {
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
}
}
private ListenableFuture<List<Void>> saveProvisionStateAttribute(Device device) {
return attributesService.save(device.getTenantId(), device.getId(), DataConstants.SERVER_SCOPE,
Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry(DEVICE_PROVISION_STATE, PROVISIONED_STATE),
System.currentTimeMillis())));
}
private DeviceCredentials getDeviceCredentials(Device device) {
return deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), device.getId());
}
private void pushProvisionEventToRuleEngine(ProvisionRequest request, Device device, String type) {
try {
JsonNode entityNode = JacksonUtil.valueToTree(request);
TbMsg msg = TbMsg.newMsg(type, device.getId(), createTbMsgMetaData(device), JacksonUtil.toString(entityNode));
sendToRuleEngine(device.getTenantId(), msg, null);
} catch (IllegalArgumentException e) {
log.warn("[{}] Failed to push device action to rule engine: {}", device.getId(), type, e);
}
}
private void pushDeviceCreatedEventToRuleEngine(Device device) {
try {
ObjectNode entityNode = JacksonUtil.OBJECT_MAPPER.valueToTree(device);
TbMsg msg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, device.getId(), createTbMsgMetaData(device), JacksonUtil.OBJECT_MAPPER.writeValueAsString(entityNode));
sendToRuleEngine(device.getTenantId(), msg, null);
} catch (JsonProcessingException | IllegalArgumentException e) {
log.warn("[{}] Failed to push device action to rule engine: {}", device.getId(), DataConstants.ENTITY_CREATED, e);
}
}
protected void sendToRuleEngine(TenantId tenantId, TbMsg tbMsg, TbQueueCallback callback) {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tenantId, tbMsg.getOriginator());
TransportProtos.ToRuleEngineMsg msg = TransportProtos.ToRuleEngineMsg.newBuilder().setTbMsg(TbMsg.toByteString(tbMsg))
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()).build();
ruleEngineMsgProducer.send(tpi, new TbProtoQueueMsg<>(tbMsg.getId(), msg), callback);
}
private TbMsgMetaData createTbMsgMetaData(Device device) {
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("tenantId", device.getTenantId().toString());
return metaData;
}
private void logAction(TenantId tenantId, CustomerId customerId, Device device, boolean success, ProvisionRequest provisionRequest) {
ActionType actionType = success ? ActionType.PROVISION_SUCCESS : ActionType.PROVISION_FAILURE;
auditLogService.logEntityAction(tenantId, customerId, new UserId(UserId.NULL_UUID), device.getName(), device.getId(), device, actionType, null, provisionRequest);
}
}

6
application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java

@ -42,6 +42,7 @@ import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import static org.thingsboard.server.service.install.DatabaseHelper.objectMapper;
@ -243,6 +244,11 @@ public class InstallScripts {
try {
JsonNode oauth2ConfigTemplateJson = objectMapper.readTree(path.toFile());
OAuth2ClientRegistrationTemplate clientRegistrationTemplate = objectMapper.treeToValue(oauth2ConfigTemplateJson, OAuth2ClientRegistrationTemplate.class);
Optional<OAuth2ClientRegistrationTemplate> existingClientRegistrationTemplate =
oAuth2TemplateService.findClientRegistrationTemplateByProviderId(clientRegistrationTemplate.getProviderId());
if (existingClientRegistrationTemplate.isPresent()) {
clientRegistrationTemplate.setId(existingClientRegistrationTemplate.get().getId());
}
oAuth2TemplateService.saveClientRegistrationTemplate(clientRegistrationTemplate);
} catch (Exception e) {
log.error("Unable to load oauth2 config templates from json: [{}]", path.toString());

106
application/src/main/java/org/thingsboard/server/service/profile/DefaultTbTenantProfileCache.java

@ -0,0 +1,106 @@
/**
* Copyright © 2016-2020 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.profile;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Service
@Slf4j
public class DefaultTbTenantProfileCache implements TbTenantProfileCache {
private final Lock tenantProfileFetchLock = new ReentrantLock();
private final TenantProfileService tenantProfileService;
private final TenantService tenantService;
private final ConcurrentMap<TenantProfileId, TenantProfile> tenantProfilesMap = new ConcurrentHashMap<>();
private final ConcurrentMap<TenantId, TenantProfileId> tenantsMap = new ConcurrentHashMap<>();
public DefaultTbTenantProfileCache(TenantProfileService tenantProfileService, TenantService tenantService) {
this.tenantProfileService = tenantProfileService;
this.tenantService = tenantService;
}
@Override
public TenantProfile get(TenantProfileId tenantProfileId) {
TenantProfile profile = tenantProfilesMap.get(tenantProfileId);
if (profile == null) {
profile = tenantProfilesMap.get(tenantProfileId);
if (profile == null) {
tenantProfileFetchLock.lock();
try {
profile = tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, tenantProfileId);
if (profile != null) {
tenantProfilesMap.put(tenantProfileId, profile);
}
} finally {
tenantProfileFetchLock.unlock();
}
}
}
return profile;
}
@Override
public TenantProfile get(TenantId tenantId) {
TenantProfileId profileId = tenantsMap.get(tenantId);
if (profileId == null) {
Tenant tenant = tenantService.findTenantById(tenantId);
if (tenant != null) {
profileId = tenant.getTenantProfileId();
tenantsMap.put(tenantId, profileId);
} else {
return null;
}
}
return get(profileId);
}
@Override
public void put(TenantProfile profile) {
if (profile.getId() != null) {
tenantProfilesMap.put(profile.getId(), profile);
}
}
@Override
public void evict(TenantProfileId profileId) {
tenantProfilesMap.remove(profileId);
}
@Override
public void evict(TenantId tenantId) {
tenantsMap.remove(tenantId);
}
}

38
application/src/main/java/org/thingsboard/server/service/profile/TbTenantProfileCache.java

@ -0,0 +1,38 @@
/**
* Copyright © 2016-2020 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.profile;
import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
public interface TbTenantProfileCache {
TenantProfile get(TenantId tenantId);
TenantProfile get(TenantProfileId tenantProfileId);
void put(TenantProfile profile);
void evict(TenantProfileId id);
void evict(TenantId id);
}

10
application/src/main/java/org/thingsboard/server/service/queue/DefaultTenantRoutingInfoService.java

@ -25,6 +25,7 @@ import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.queue.discovery.TenantRoutingInfo;
import org.thingsboard.server.queue.discovery.TenantRoutingInfoService;
import org.thingsboard.server.service.profile.TbTenantProfileCache;
@Slf4j
@Service
@ -33,19 +34,18 @@ public class DefaultTenantRoutingInfoService implements TenantRoutingInfoService
private final TenantService tenantService;
private final TenantProfileService tenantProfileService;
private final TbTenantProfileCache tenantProfileCache;
public DefaultTenantRoutingInfoService(TenantService tenantService, TenantProfileService tenantProfileService) {
public DefaultTenantRoutingInfoService(TenantService tenantService, TbTenantProfileCache tenantProfileCache) {
this.tenantService = tenantService;
this.tenantProfileService = tenantProfileService;
this.tenantProfileCache = tenantProfileCache;
}
@Override
public TenantRoutingInfo getRoutingInfo(TenantId tenantId) {
Tenant tenant = tenantService.findTenantById(tenantId);
if (tenant != null) {
// TODO: Tenant Profile from cache
TenantProfile tenantProfile = tenantProfileService.findTenantProfileById(tenantId, tenant.getTenantProfileId());
TenantProfile tenantProfile = tenantProfileCache.get(tenant.getTenantProfileId());
return new TenantRoutingInfo(tenantId, tenantProfile.isIsolatedTbCore(), tenantProfile.isIsolatedTbRuleEngine());
} else {
throw new RuntimeException("Tenant not found!");

73
application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java

@ -23,7 +23,6 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.protobuf.ByteString;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.DataConstants;
@ -31,6 +30,8 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
import org.thingsboard.server.common.data.device.credentials.ProvisionDeviceCredentialsData;
import org.thingsboard.server.common.data.device.profile.ProvisionDeviceProfileCredentials;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
@ -45,7 +46,10 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.transport.util.DataDecodingEncodingService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceProvisionService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.device.provision.ProvisionRequest;
import org.thingsboard.server.dao.device.provision.ProvisionResponse;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
@ -56,6 +60,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFro
import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg;
@ -63,7 +68,9 @@ import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenR
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.dao.device.provision.ProvisionFailedException;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.profile.TbTenantProfileCache;
import org.thingsboard.server.service.queue.TbClusterService;
import org.thingsboard.server.service.state.DeviceStateService;
@ -86,7 +93,7 @@ public class DefaultTransportApiService implements TransportApiService {
//TODO: Constructor dependencies;
private final DeviceProfileService deviceProfileService;
private final TenantService tenantService;
private final TenantProfileService tenantProfileService;
private final TbTenantProfileCache tenantProfileCache;
private final DeviceService deviceService;
private final RelationService relationService;
private final DeviceCredentialsService deviceCredentialsService;
@ -94,18 +101,20 @@ public class DefaultTransportApiService implements TransportApiService {
private final DbCallbackExecutorService dbCallbackExecutorService;
private final TbClusterService tbClusterService;
private final DataDecodingEncodingService dataDecodingEncodingService;
private final DeviceProvisionService deviceProvisionService;
private final ConcurrentMap<String, ReentrantLock> deviceCreationLocks = new ConcurrentHashMap<>();
public DefaultTransportApiService(DeviceProfileService deviceProfileService, TenantService tenantService,
TenantProfileService tenantProfileService, DeviceService deviceService,
TbTenantProfileCache tenantProfileCache, DeviceService deviceService,
RelationService relationService, DeviceCredentialsService deviceCredentialsService,
DeviceStateService deviceStateService, DbCallbackExecutorService dbCallbackExecutorService,
TbClusterService tbClusterService, DataDecodingEncodingService dataDecodingEncodingService) {
TbClusterService tbClusterService, DataDecodingEncodingService dataDecodingEncodingService,
DeviceProvisionService deviceProvisionService) {
this.deviceProfileService = deviceProfileService;
this.tenantService = tenantService;
this.tenantProfileService = tenantProfileService;
this.tenantProfileCache = tenantProfileCache;
this.deviceService = deviceService;
this.relationService = relationService;
this.deviceCredentialsService = deviceCredentialsService;
@ -113,6 +122,7 @@ public class DefaultTransportApiService implements TransportApiService {
this.dbCallbackExecutorService = dbCallbackExecutorService;
this.tbClusterService = tbClusterService;
this.dataDecodingEncodingService = dataDecodingEncodingService;
this.deviceProvisionService = deviceProvisionService;
}
@Override
@ -139,6 +149,9 @@ public class DefaultTransportApiService implements TransportApiService {
} else if (transportApiRequestMsg.hasGetDeviceProfileRequestMsg()) {
return Futures.transform(handle(transportApiRequestMsg.getGetDeviceProfileRequestMsg()),
value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor());
} else if (transportApiRequestMsg.hasProvisionDeviceRequestMsg()) {
return Futures.transform(handle(transportApiRequestMsg.getProvisionDeviceRequestMsg()),
value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor());
}
return Futures.transform(getEmptyTransportApiResponseFuture(),
value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor());
@ -263,12 +276,54 @@ public class DefaultTransportApiService implements TransportApiService {
}, dbCallbackExecutorService);
}
private ListenableFuture<TransportApiResponseMsg> handle(ProvisionDeviceRequestMsg requestMsg) {
ListenableFuture<ProvisionResponse> provisionResponseFuture = null;
try {
provisionResponseFuture = Futures.immediateFuture(deviceProvisionService.provisionDevice(
new ProvisionRequest(
requestMsg.getDeviceName(),
requestMsg.getCredentialsType() != null ? DeviceCredentialsType.valueOf(requestMsg.getCredentialsType().name()) : null,
new ProvisionDeviceCredentialsData(requestMsg.getCredentialsDataProto().getValidateDeviceTokenRequestMsg().getToken(),
requestMsg.getCredentialsDataProto().getValidateBasicMqttCredRequestMsg().getClientId(),
requestMsg.getCredentialsDataProto().getValidateBasicMqttCredRequestMsg().getUserName(),
requestMsg.getCredentialsDataProto().getValidateBasicMqttCredRequestMsg().getPassword(),
requestMsg.getCredentialsDataProto().getValidateDeviceX509CertRequestMsg().getHash()),
new ProvisionDeviceProfileCredentials(
requestMsg.getProvisionDeviceCredentialsMsg().getProvisionDeviceKey(),
requestMsg.getProvisionDeviceCredentialsMsg().getProvisionDeviceSecret()))));
} catch (ProvisionFailedException e) {
return Futures.immediateFuture(getTransportApiResponseMsg(
TransportProtos.DeviceCredentialsProto.getDefaultInstance(),
TransportProtos.ProvisionResponseStatus.valueOf(e.getMessage())));
}
return Futures.transform(provisionResponseFuture, provisionResponse -> getTransportApiResponseMsg(
getDeviceCredentials(provisionResponse.getDeviceCredentials()), TransportProtos.ProvisionResponseStatus.SUCCESS),
dbCallbackExecutorService);
}
private TransportApiResponseMsg getTransportApiResponseMsg(TransportProtos.DeviceCredentialsProto deviceCredentials, TransportProtos.ProvisionResponseStatus status) {
return TransportApiResponseMsg.newBuilder()
.setProvisionDeviceResponseMsg(TransportProtos.ProvisionDeviceResponseMsg.newBuilder()
.setDeviceCredentials(deviceCredentials)
.setProvisionResponseStatus(status)
.build())
.build();
}
private TransportProtos.DeviceCredentialsProto getDeviceCredentials(DeviceCredentials deviceCredentials) {
return TransportProtos.DeviceCredentialsProto.newBuilder()
.setDeviceIdMSB(deviceCredentials.getDeviceId().getId().getMostSignificantBits())
.setDeviceIdLSB(deviceCredentials.getDeviceId().getId().getLeastSignificantBits())
.setCredentialsType(TransportProtos.CredentialsType.valueOf(deviceCredentials.getCredentialsType().name()))
.setCredentialsId(deviceCredentials.getCredentialsId())
.setCredentialsValue(deviceCredentials.getCredentialsValue() != null ? deviceCredentials.getCredentialsValue() : "")
.build();
}
private ListenableFuture<TransportApiResponseMsg> handle(GetTenantRoutingInfoRequestMsg requestMsg) {
TenantId tenantId = new TenantId(new UUID(requestMsg.getTenantIdMSB(), requestMsg.getTenantIdLSB()));
// TODO: Tenant Profile from cache
ListenableFuture<TenantProfile> tenantProfileFuture =
Futures.transform(tenantService.findTenantByIdAsync(TenantId.SYS_TENANT_ID, tenantId), tenant ->
tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, tenant.getTenantProfileId()), dbCallbackExecutorService);
ListenableFuture<TenantProfile> tenantProfileFuture = Futures.immediateFuture(tenantProfileCache.get(tenantId));
return Futures.transform(tenantProfileFuture, tenantProfile -> TransportApiResponseMsg.newBuilder()
.setGetTenantRoutingInfoResponseMsg(GetTenantRoutingInfoResponseMsg.newBuilder().setIsolatedTbCore(tenantProfile.isIsolatedTbCore())
.setIsolatedTbRuleEngine(tenantProfile.isIsolatedTbRuleEngine()).build()).build(), dbCallbackExecutorService);

16
application/src/main/java/org/thingsboard/server/utils/MiscUtils.java

@ -68,6 +68,22 @@ public class MiscUtils {
return request.getServerName();
}
public static String getDomainNameAndPort(HttpServletRequest request){
String domainName = getDomainName(request);
String scheme = getScheme(request);
int port = MiscUtils.getPort(request);
if (needsPort(scheme, port)) {
domainName += ":" + port;
}
return domainName;
}
private static boolean needsPort(String scheme, int port) {
boolean isHttpDefault = "http".equals(scheme.toLowerCase()) && port == 80;
boolean isHttpsDefault = "https".equals(scheme.toLowerCase()) && port == 443;
return !isHttpDefault && !isHttpsDefault;
}
public static int getPort(HttpServletRequest request){
String forwardedProto = request.getHeader("x-forwarded-proto");

1
application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java

@ -68,6 +68,7 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.device.profile.ProvisionDeviceProfileCredentials;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;

12
application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java

@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.device.profile.ProvisionDeviceProfileCredentials;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
@ -153,6 +154,17 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController
.andExpect(statusReason(containsString("Device profile with such name already exists")));
}
@Test
public void testSaveDeviceProfileWithSameProvisionDeviceKey() throws Exception {
DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile");
deviceProfile.setProvisionDeviceKey("testProvisionDeviceKey");
doPost("/api/deviceProfile", deviceProfile).andExpect(status().isOk());
DeviceProfile deviceProfile2 = this.createDeviceProfile("Device Profile 2");
deviceProfile2.setProvisionDeviceKey("testProvisionDeviceKey");
doPost("/api/deviceProfile", deviceProfile2).andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Device profile with such provision device key already exists")));
}
@Ignore
@Test
public void testChangeDeviceProfileTypeWithExistingDevices() throws Exception {

41
application/src/test/java/org/thingsboard/server/mqtt/AbstractMqttIntegrationTest.java

@ -26,13 +26,18 @@ import org.junit.Assert;
import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TransportPayloadType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.device.profile.AllowCreateNewDevicesDeviceProfileProvisionConfiguration;
import org.thingsboard.server.common.data.device.profile.CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.device.profile.DeviceProfileProvisionConfiguration;
import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration;
import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.MqttJsonDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.MqttProtoDeviceProfileTransportConfiguration;
@ -66,7 +71,18 @@ public abstract class AbstractMqttIntegrationTest extends AbstractControllerTest
protected Device savedGateway;
protected String gatewayAccessToken;
protected void processBeforeTest(String deviceName, String gatewayName, TransportPayloadType payloadType, String telemetryTopic, String attributesTopic) throws Exception {
protected void processBeforeTest (String deviceName, String gatewayName, TransportPayloadType payloadType, String telemetryTopic, String attributesTopic) throws Exception {
this.processBeforeTest(deviceName, gatewayName, payloadType, telemetryTopic, attributesTopic, DeviceProfileProvisionType.DISABLED, null, null);
}
protected void processBeforeTest(String deviceName,
String gatewayName,
TransportPayloadType payloadType,
String telemetryTopic,
String attributesTopic,
DeviceProfileProvisionType provisionType,
String provisionKey, String provisionSecret
) throws Exception {
loginSysAdmin();
Tenant tenant = new Tenant();
@ -95,7 +111,7 @@ public abstract class AbstractMqttIntegrationTest extends AbstractControllerTest
gateway.setAdditionalInfo(additionalInfo);
if (payloadType != null) {
DeviceProfile mqttDeviceProfile = createMqttDeviceProfile(payloadType, telemetryTopic, attributesTopic);
DeviceProfile mqttDeviceProfile = createMqttDeviceProfile(payloadType, telemetryTopic, attributesTopic, provisionType, provisionKey, provisionSecret);
DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", mqttDeviceProfile, DeviceProfile.class);
device.setType(savedDeviceProfile.getName());
device.setDeviceProfileId(savedDeviceProfile.getId());
@ -185,11 +201,17 @@ public abstract class AbstractMqttIntegrationTest extends AbstractControllerTest
return keyValueProtoBuilder.build();
}
protected DeviceProfile createMqttDeviceProfile(TransportPayloadType transportPayloadType, String telemetryTopic, String attributesTopic) {
protected DeviceProfile createMqttDeviceProfile(TransportPayloadType transportPayloadType,
String telemetryTopic, String attributesTopic,
DeviceProfileProvisionType provisionType,
String provisionKey, String provisionSecret
) {
DeviceProfile deviceProfile = new DeviceProfile();
deviceProfile.setName(transportPayloadType.name());
deviceProfile.setType(DeviceProfileType.DEFAULT);
deviceProfile.setTransportType(DeviceTransportType.MQTT);
deviceProfile.setProvisionType(provisionType);
deviceProfile.setProvisionDeviceKey(provisionKey);
deviceProfile.setDescription(transportPayloadType.name() + " Test");
DeviceProfileData deviceProfileData = new DeviceProfileData();
DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration();
@ -206,6 +228,19 @@ public abstract class AbstractMqttIntegrationTest extends AbstractControllerTest
transportConfiguration.setDeviceAttributesTopic(attributesTopic);
}
deviceProfileData.setTransportConfiguration(transportConfiguration);
DeviceProfileProvisionConfiguration provisionConfiguration;
switch (provisionType) {
case ALLOW_CREATE_NEW_DEVICES:
provisionConfiguration = new AllowCreateNewDevicesDeviceProfileProvisionConfiguration(provisionSecret);
break;
case CHECK_PRE_PROVISIONED_DEVICES:
provisionConfiguration = new CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration(provisionSecret);
break;
case DISABLED:
default:
provisionConfiguration = new DisabledDeviceProfileProvisionConfiguration(provisionSecret);
}
deviceProfileData.setProvisionConfiguration(provisionConfiguration);
deviceProfileData.setConfiguration(configuration);
deviceProfile.setProfileData(deviceProfileData);
deviceProfile.setDefault(false);

3
application/src/test/java/org/thingsboard/server/mqtt/MqttSqlTestSuite.java

@ -31,7 +31,8 @@ import java.util.Arrays;
"org.thingsboard.server.mqtt.telemetry.attributes.sql.*Test",
"org.thingsboard.server.mqtt.attributes.updates.sql.*Test",
"org.thingsboard.server.mqtt.attributes.request.sql.*Test",
"org.thingsboard.server.mqtt.claim.sql.*Test"
"org.thingsboard.server.mqtt.claim.sql.*Test",
"org.thingsboard.server.mqtt.provision.sql.*Test"
})
public class MqttSqlTestSuite {

285
application/src/test/java/org/thingsboard/server/mqtt/provision/AbstractMqttProvisionJsonDeviceTest.java

@ -0,0 +1,285 @@
/**
* Copyright © 2016-2020 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.mqtt.provision;
import com.google.gson.JsonObject;
import io.netty.handler.codec.mqtt.MqttQoS;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.junit.After;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.TransportPayloadType;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
import org.thingsboard.server.common.data.device.profile.MqttTopics;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.msg.EncryptionUtil;
import org.thingsboard.server.common.transport.util.JsonUtils;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus;
import org.thingsboard.server.dao.util.mapping.JacksonUtil;
import org.thingsboard.server.mqtt.AbstractMqttIntegrationTest;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@Slf4j
public abstract class AbstractMqttProvisionJsonDeviceTest extends AbstractMqttIntegrationTest {
@Autowired
DeviceCredentialsService deviceCredentialsService;
@Autowired
DeviceService deviceService;
@After
public void afterTest() throws Exception {
super.processAfterTest();
}
@Test
public void testProvisioningDisabledDevice() throws Exception {
processTestProvisioningDisabledDevice();
}
@Test
public void testProvisioningCheckPreProvisionedDevice() throws Exception {
processTestProvisioningCheckPreProvisionedDevice();
}
@Test
public void testProvisioningCreateNewDeviceWithoutCredentials() throws Exception {
processTestProvisioningCreateNewDeviceWithoutCredentials();
}
@Test
public void testProvisioningCreateNewDeviceWithAccessToken() throws Exception {
processTestProvisioningCreateNewDeviceWithAccessToken();
}
@Test
public void testProvisioningCreateNewDeviceWithCert() throws Exception {
processTestProvisioningCreateNewDeviceWithCert();
}
@Test
public void testProvisioningCreateNewDeviceWithMqttBasic() throws Exception {
processTestProvisioningCreateNewDeviceWithMqttBasic();
}
@Test
public void testProvisioningWithBadKeyDevice() throws Exception {
processTestProvisioningWithBadKeyDevice();
}
protected void processTestProvisioningDisabledDevice() throws Exception {
super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.DISABLED, null, null);
byte[] result = createMqttClientAndPublish().getPayloadBytes();
JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject();
Assert.assertEquals("Provision data was not found!", response.get("errorMsg").getAsString());
Assert.assertEquals(ProvisionResponseStatus.NOT_FOUND.name(), response.get("provisionDeviceStatus").getAsString());
}
protected void processTestProvisioningCreateNewDeviceWithoutCredentials() throws Exception {
super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret");
byte[] result = createMqttClientAndPublish().getPayloadBytes();
JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject();
Device createdDevice = deviceService.findDeviceByTenantIdAndName(savedTenant.getTenantId(), "Test Provision device");
Assert.assertNotNull(createdDevice);
Assert.assertEquals(createdDevice.getId().toString(), response.get("deviceId").getAsString());
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedTenant.getTenantId(), createdDevice.getId());
Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.get("credentialsType").getAsString());
Assert.assertEquals(deviceCredentials.getCredentialsId(), response.get("credentialsId").getAsString());
Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.get("provisionDeviceStatus").getAsString());
}
protected void processTestProvisioningCreateNewDeviceWithAccessToken() throws Exception {
super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret");
String requestCredentials = ",\"credentialsType\": \"ACCESS_TOKEN\",\"token\": \"test_token\"";
byte[] result = createMqttClientAndPublish(requestCredentials).getPayloadBytes();
JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject();
Device createdDevice = deviceService.findDeviceByTenantIdAndName(savedTenant.getTenantId(), "Test Provision device");
Assert.assertNotNull(createdDevice);
Assert.assertEquals(createdDevice.getId().toString(), response.get("deviceId").getAsString());
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedTenant.getTenantId(), createdDevice.getId());
Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.get("credentialsType").getAsString());
Assert.assertEquals(deviceCredentials.getCredentialsId(), response.get("credentialsId").getAsString());
Assert.assertEquals(deviceCredentials.getCredentialsType().name(), "ACCESS_TOKEN");
Assert.assertEquals(deviceCredentials.getCredentialsId(), "test_token");
Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.get("provisionDeviceStatus").getAsString());
}
protected void processTestProvisioningCreateNewDeviceWithCert() throws Exception {
super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret");
String requestCredentials = ",\"credentialsType\": \"X509_CERTIFICATE\",\"hash\": \"testHash\"";
byte[] result = createMqttClientAndPublish(requestCredentials).getPayloadBytes();
JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject();
Device createdDevice = deviceService.findDeviceByTenantIdAndName(savedTenant.getTenantId(), "Test Provision device");
Assert.assertNotNull(createdDevice);
Assert.assertEquals(createdDevice.getId().toString(), response.get("deviceId").getAsString());
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedTenant.getTenantId(), createdDevice.getId());
Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.get("credentialsType").getAsString());
Assert.assertEquals(deviceCredentials.getCredentialsId(), response.get("credentialsId").getAsString());
Assert.assertEquals(deviceCredentials.getCredentialsType().name(), "X509_CERTIFICATE");
String cert = EncryptionUtil.trimNewLines(deviceCredentials.getCredentialsValue());
String sha3Hash = EncryptionUtil.getSha3Hash(cert);
Assert.assertEquals(deviceCredentials.getCredentialsId(), sha3Hash);
Assert.assertEquals(deviceCredentials.getCredentialsValue(), "testHash");
Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.get("provisionDeviceStatus").getAsString());
}
protected void processTestProvisioningCreateNewDeviceWithMqttBasic() throws Exception {
super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret");
String requestCredentials = ",\"credentialsType\": \"MQTT_BASIC\",\"clientId\": \"test_clientId\",\"username\": \"test_username\",\"password\": \"test_password\"";
byte[] result = createMqttClientAndPublish(requestCredentials).getPayloadBytes();
JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject();
Device createdDevice = deviceService.findDeviceByTenantIdAndName(savedTenant.getTenantId(), "Test Provision device");
Assert.assertNotNull(createdDevice);
Assert.assertEquals(createdDevice.getId().toString(), response.get("deviceId").getAsString());
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedTenant.getTenantId(), createdDevice.getId());
Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.get("credentialsType").getAsString());
Assert.assertEquals(deviceCredentials.getCredentialsId(), response.get("credentialsId").getAsString());
Assert.assertEquals(deviceCredentials.getCredentialsType().name(), "MQTT_BASIC");
Assert.assertEquals(deviceCredentials.getCredentialsId(), EncryptionUtil.getSha3Hash("|", "test_clientId", "test_username"));
BasicMqttCredentials mqttCredentials = new BasicMqttCredentials();
mqttCredentials.setClientId("test_clientId");
mqttCredentials.setUserName("test_username");
mqttCredentials.setPassword("test_password");
Assert.assertEquals(deviceCredentials.getCredentialsValue(), JacksonUtil.toString(mqttCredentials));
Assert.assertEquals(deviceCredentials.getCredentialsId(), response.get("credentialsId").getAsString());
Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.get("provisionDeviceStatus").getAsString());
}
protected void processTestProvisioningCheckPreProvisionedDevice() throws Exception {
super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, "testProvisionKey", "testProvisionSecret");
byte[] result = createMqttClientAndPublish().getPayloadBytes();
JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject();
Assert.assertEquals(savedDevice.getId().toString(), response.get("deviceId").getAsString());
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedTenant.getTenantId(), savedDevice.getId());
Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.get("credentialsType").getAsString());
Assert.assertEquals(deviceCredentials.getCredentialsId(), response.get("credentialsId").getAsString());
Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.get("provisionDeviceStatus").getAsString());
}
protected void processTestProvisioningWithBadKeyDevice() throws Exception {
super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, "testProvisionKeyOrig", "testProvisionSecret");
byte[] result = createMqttClientAndPublish().getPayloadBytes();
JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject();
Assert.assertEquals("Provision data was not found!", response.get("errorMsg").getAsString());
Assert.assertEquals(ProvisionResponseStatus.NOT_FOUND.name(), response.get("provisionDeviceStatus").getAsString());
}
protected TestMqttCallback createMqttClientAndPublish() throws Exception {
return createMqttClientAndPublish("");
}
protected TestMqttCallback createMqttClientAndPublish(String deviceCredentials) throws Exception {
String provisionRequestMsg = createTestProvisionMessage(deviceCredentials);
MqttAsyncClient client = getMqttAsyncClient("provision");
TestMqttCallback onProvisionCallback = getTestMqttCallback();
client.setCallback(onProvisionCallback);
client.subscribe(MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC, MqttQoS.AT_MOST_ONCE.value());
Thread.sleep(2000);
client.publish(MqttTopics.DEVICE_PROVISION_REQUEST_TOPIC, new MqttMessage(provisionRequestMsg.getBytes()));
onProvisionCallback.getLatch().await(3, TimeUnit.SECONDS);
return onProvisionCallback;
}
protected TestMqttCallback getTestMqttCallback() {
CountDownLatch latch = new CountDownLatch(1);
return new TestMqttCallback(latch);
}
protected static class TestMqttCallback implements MqttCallback {
private final CountDownLatch latch;
private Integer qoS;
private byte[] payloadBytes;
TestMqttCallback(CountDownLatch latch) {
this.latch = latch;
}
public int getQoS() {
return qoS;
}
public byte[] getPayloadBytes() {
return payloadBytes;
}
public CountDownLatch getLatch() {
return latch;
}
@Override
public void connectionLost(Throwable throwable) {
}
@Override
public void messageArrived(String requestTopic, MqttMessage mqttMessage) throws Exception {
qoS = mqttMessage.getQos();
payloadBytes = mqttMessage.getPayload();
latch.countDown();
}
@Override
public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
}
}
protected String createTestProvisionMessage(String deviceCredentials) {
return "{\"deviceName\":\"Test Provision device\",\"provisionDeviceKey\":\"testProvisionKey\", \"provisionDeviceSecret\":\"testProvisionSecret\"" + deviceCredentials + "}";
}
}

305
application/src/test/java/org/thingsboard/server/mqtt/provision/AbstractMqttProvisionProtoDeviceTest.java

@ -0,0 +1,305 @@
/**
* Copyright © 2016-2020 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.mqtt.provision;
import io.netty.handler.codec.mqtt.MqttQoS;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.junit.After;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.TransportPayloadType;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
import org.thingsboard.server.common.data.device.profile.MqttTopics;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.msg.EncryptionUtil;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus;
import org.thingsboard.server.dao.util.mapping.JacksonUtil;
import org.thingsboard.server.gen.transport.TransportProtos.CredentialsDataProto;
import org.thingsboard.server.gen.transport.TransportProtos.CredentialsType;
import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceCredentialsMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ValidateBasicMqttCredRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg;
import org.thingsboard.server.mqtt.AbstractMqttIntegrationTest;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@Slf4j
public abstract class AbstractMqttProvisionProtoDeviceTest extends AbstractMqttIntegrationTest {
@Autowired
DeviceCredentialsService deviceCredentialsService;
@Autowired
DeviceService deviceService;
@After
public void afterTest() throws Exception {
super.processAfterTest();
}
@Test
public void testProvisioningDisabledDevice() throws Exception {
processTestProvisioningDisabledDevice();
}
@Test
public void testProvisioningCheckPreProvisionedDevice() throws Exception {
processTestProvisioningCheckPreProvisionedDevice();
}
@Test
public void testProvisioningCreateNewDeviceWithoutCredentials() throws Exception {
processTestProvisioningCreateNewDeviceWithoutCredentials();
}
@Test
public void testProvisioningCreateNewDeviceWithAccessToken() throws Exception {
processTestProvisioningCreateNewDeviceWithAccessToken();
}
@Test
public void testProvisioningCreateNewDeviceWithCert() throws Exception {
processTestProvisioningCreateNewDeviceWithCert();
}
@Test
public void testProvisioningCreateNewDeviceWithMqttBasic() throws Exception {
processTestProvisioningCreateNewDeviceWithMqttBasic();
}
@Test
public void testProvisioningWithBadKeyDevice() throws Exception {
processTestProvisioningWithBadKeyDevice();
}
protected void processTestProvisioningDisabledDevice() throws Exception {
super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, DeviceProfileProvisionType.DISABLED, null, null);
ProvisionDeviceResponseMsg result = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish().getPayloadBytes());
Assert.assertNotNull(result);
Assert.assertEquals(ProvisionResponseStatus.NOT_FOUND.name(), result.getProvisionResponseStatus().toString());
}
protected void processTestProvisioningCreateNewDeviceWithoutCredentials() throws Exception {
super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret");
ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish().getPayloadBytes());
Device createdDevice = deviceService.findDeviceByTenantIdAndName(savedTenant.getTenantId(), "Test Provision device");
Assert.assertNotNull(createdDevice);
Assert.assertEquals(createdDevice.getId().getId(), new UUID(response.getDeviceCredentials().getDeviceIdMSB(), response.getDeviceCredentials().getDeviceIdLSB()));
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedTenant.getTenantId(), createdDevice.getId());
Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.getDeviceCredentials().getCredentialsType().toString());
Assert.assertEquals(deviceCredentials.getCredentialsId(), response.getDeviceCredentials().getCredentialsId());
Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.getProvisionResponseStatus().toString());
}
protected void processTestProvisioningCreateNewDeviceWithAccessToken() throws Exception {
super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret");
CredentialsDataProto requestCredentials = CredentialsDataProto.newBuilder().setValidateDeviceTokenRequestMsg(ValidateDeviceTokenRequestMsg.newBuilder().setToken("test_token").build()).build();
ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish(createTestsProvisionMessage(CredentialsType.ACCESS_TOKEN, requestCredentials)).getPayloadBytes());
Device createdDevice = deviceService.findDeviceByTenantIdAndName(savedTenant.getTenantId(), "Test Provision device");
Assert.assertNotNull(createdDevice);
Assert.assertEquals(createdDevice.getId().getId(), new UUID(response.getDeviceCredentials().getDeviceIdMSB(), response.getDeviceCredentials().getDeviceIdLSB()));
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedTenant.getTenantId(), createdDevice.getId());
Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.getDeviceCredentials().getCredentialsType().toString());
Assert.assertEquals(deviceCredentials.getCredentialsId(), response.getDeviceCredentials().getCredentialsId());
Assert.assertEquals(deviceCredentials.getCredentialsType(), DeviceCredentialsType.ACCESS_TOKEN);
Assert.assertEquals(deviceCredentials.getCredentialsId(), "test_token");
Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.getProvisionResponseStatus().toString());
}
protected void processTestProvisioningCreateNewDeviceWithCert() throws Exception {
super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret");
CredentialsDataProto requestCredentials = CredentialsDataProto.newBuilder().setValidateDeviceX509CertRequestMsg(ValidateDeviceX509CertRequestMsg.newBuilder().setHash("testHash").build()).build();
ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish(createTestsProvisionMessage(CredentialsType.X509_CERTIFICATE, requestCredentials)).getPayloadBytes());
Device createdDevice = deviceService.findDeviceByTenantIdAndName(savedTenant.getTenantId(), "Test Provision device");
Assert.assertNotNull(createdDevice);
Assert.assertEquals(createdDevice.getId().getId(), new UUID(response.getDeviceCredentials().getDeviceIdMSB(), response.getDeviceCredentials().getDeviceIdLSB()));
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedTenant.getTenantId(), createdDevice.getId());
Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.getDeviceCredentials().getCredentialsType().toString());
Assert.assertEquals(deviceCredentials.getCredentialsId(), response.getDeviceCredentials().getCredentialsId());
Assert.assertEquals(deviceCredentials.getCredentialsType(), DeviceCredentialsType.X509_CERTIFICATE);
String cert = EncryptionUtil.trimNewLines(deviceCredentials.getCredentialsValue());
String sha3Hash = EncryptionUtil.getSha3Hash(cert);
Assert.assertEquals(deviceCredentials.getCredentialsId(), sha3Hash);
Assert.assertEquals(deviceCredentials.getCredentialsValue(), "testHash");
Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.getProvisionResponseStatus().toString());
}
protected void processTestProvisioningCreateNewDeviceWithMqttBasic() throws Exception {
super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret");
CredentialsDataProto requestCredentials = CredentialsDataProto.newBuilder().setValidateBasicMqttCredRequestMsg(
ValidateBasicMqttCredRequestMsg.newBuilder()
.setClientId("test_clientId")
.setUserName("test_username")
.setPassword("test_password")
.build()
).build();
ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish(createTestsProvisionMessage(CredentialsType.MQTT_BASIC, requestCredentials)).getPayloadBytes());
Device createdDevice = deviceService.findDeviceByTenantIdAndName(savedTenant.getTenantId(), "Test Provision device");
Assert.assertNotNull(createdDevice);
Assert.assertEquals(createdDevice.getId().getId(), new UUID(response.getDeviceCredentials().getDeviceIdMSB(), response.getDeviceCredentials().getDeviceIdLSB()));
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedTenant.getTenantId(), createdDevice.getId());
Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.getDeviceCredentials().getCredentialsType().toString());
Assert.assertEquals(deviceCredentials.getCredentialsId(), response.getDeviceCredentials().getCredentialsId());
Assert.assertEquals(deviceCredentials.getCredentialsType(), DeviceCredentialsType.MQTT_BASIC);
Assert.assertEquals(deviceCredentials.getCredentialsId(), EncryptionUtil.getSha3Hash("|", "test_clientId", "test_username"));
BasicMqttCredentials mqttCredentials = new BasicMqttCredentials();
mqttCredentials.setClientId("test_clientId");
mqttCredentials.setUserName("test_username");
mqttCredentials.setPassword("test_password");
Assert.assertEquals(deviceCredentials.getCredentialsValue(), JacksonUtil.toString(mqttCredentials));
Assert.assertEquals(deviceCredentials.getCredentialsId(), response.getDeviceCredentials().getCredentialsId());
Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.getProvisionResponseStatus().toString());
}
protected void processTestProvisioningCheckPreProvisionedDevice() throws Exception {
super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, "testProvisionKey", "testProvisionSecret");
ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish().getPayloadBytes());
Assert.assertEquals(savedDevice.getId().getId(), new UUID(response.getDeviceCredentials().getDeviceIdMSB(), response.getDeviceCredentials().getDeviceIdLSB()));
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedTenant.getTenantId(), savedDevice.getId());
Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.getDeviceCredentials().getCredentialsType().toString());
Assert.assertEquals(deviceCredentials.getCredentialsId(), response.getDeviceCredentials().getCredentialsId());
Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.getProvisionResponseStatus().toString());
}
protected void processTestProvisioningWithBadKeyDevice() throws Exception {
super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, "testProvisionKeyOrig", "testProvisionSecret");
ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish().getPayloadBytes());
Assert.assertEquals(ProvisionResponseStatus.NOT_FOUND.name(), response.getProvisionResponseStatus().toString());
}
protected TestMqttCallback createMqttClientAndPublish() throws Exception {
byte[] provisionRequestMsg = createTestProvisionMessage();
return createMqttClientAndPublish(provisionRequestMsg);
}
protected TestMqttCallback createMqttClientAndPublish(byte[] provisionRequestMsg) throws Exception {
MqttAsyncClient client = getMqttAsyncClient("provision");
TestMqttCallback onProvisionCallback = getTestMqttCallback();
client.setCallback(onProvisionCallback);
client.subscribe(MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC, MqttQoS.AT_MOST_ONCE.value());
Thread.sleep(2000);
client.publish(MqttTopics.DEVICE_PROVISION_REQUEST_TOPIC, new MqttMessage(provisionRequestMsg));
onProvisionCallback.getLatch().await(3, TimeUnit.SECONDS);
return onProvisionCallback;
}
protected TestMqttCallback getTestMqttCallback() {
CountDownLatch latch = new CountDownLatch(1);
return new TestMqttCallback(latch);
}
protected static class TestMqttCallback implements MqttCallback {
private final CountDownLatch latch;
private Integer qoS;
private byte[] payloadBytes;
TestMqttCallback(CountDownLatch latch) {
this.latch = latch;
}
public int getQoS() {
return qoS;
}
public byte[] getPayloadBytes() {
return payloadBytes;
}
public CountDownLatch getLatch() {
return latch;
}
@Override
public void connectionLost(Throwable throwable) {
}
@Override
public void messageArrived(String requestTopic, MqttMessage mqttMessage) throws Exception {
qoS = mqttMessage.getQos();
payloadBytes = mqttMessage.getPayload();
latch.countDown();
}
@Override
public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
}
}
protected byte[] createTestsProvisionMessage(CredentialsType credentialsType, CredentialsDataProto credentialsData) throws Exception {
return ProvisionDeviceRequestMsg.newBuilder()
.setDeviceName("Test Provision device")
.setCredentialsType(credentialsType != null ? credentialsType : CredentialsType.ACCESS_TOKEN)
.setCredentialsDataProto(credentialsData != null ? credentialsData: CredentialsDataProto.newBuilder().build())
.setProvisionDeviceCredentialsMsg(
ProvisionDeviceCredentialsMsg.newBuilder()
.setProvisionDeviceKey("testProvisionKey")
.setProvisionDeviceSecret("testProvisionSecret")
).build()
.toByteArray();
}
protected byte[] createTestProvisionMessage() throws Exception {
return createTestsProvisionMessage(null, null);
}
}

23
application/src/test/java/org/thingsboard/server/mqtt/provision/sql/MqttProvisionDeviceJsonSqlTest.java

@ -0,0 +1,23 @@
/**
* Copyright © 2016-2020 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.mqtt.provision.sql;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.mqtt.provision.AbstractMqttProvisionJsonDeviceTest;
@DaoSqlTest
public class MqttProvisionDeviceJsonSqlTest extends AbstractMqttProvisionJsonDeviceTest {
}

23
application/src/test/java/org/thingsboard/server/mqtt/provision/sql/MqttProvisionDeviceProtoSqlTest.java

@ -0,0 +1,23 @@
/**
* Copyright © 2016-2020 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.mqtt.provision.sql;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.mqtt.provision.AbstractMqttProvisionProtoDeviceTest;
@DaoSqlTest
public class MqttProvisionDeviceProtoSqlTest extends AbstractMqttProvisionProtoDeviceTest {
}

28
common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProvisionService.java

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2020 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.device;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.dao.device.provision.ProvisionFailedException;
import org.thingsboard.server.dao.device.provision.ProvisionRequest;
import org.thingsboard.server.dao.device.provision.ProvisionResponse;
public interface DeviceProvisionService {
ProvisionResponse provisionDevice(ProvisionRequest provisionRequest) throws ProvisionFailedException;
}

4
common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.dao.device;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.device.DeviceSearchQuery;
import org.thingsboard.server.common.data.id.CustomerId;
@ -26,6 +27,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.device.provision.ProvisionRequest;
import java.util.List;
@ -83,4 +85,6 @@ public interface DeviceService {
Device assignDeviceToTenant(TenantId tenantId, Device device);
Device saveDevice(ProvisionRequest provisionRequest, DeviceProfile profile);
}

22
common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionFailedException.java

@ -0,0 +1,22 @@
/**
* Copyright © 2016-2020 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.device.provision;
public class ProvisionFailedException extends RuntimeException {
public ProvisionFailedException(String errorMsg) {
super(errorMsg);
}
}

31
common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionRequest.java

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2020 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.device.provision;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.thingsboard.server.common.data.device.credentials.ProvisionDeviceCredentialsData;
import org.thingsboard.server.common.data.device.profile.ProvisionDeviceProfileCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
@Data
@AllArgsConstructor
public class ProvisionRequest {
private String deviceName;
private DeviceCredentialsType credentialsType;
private ProvisionDeviceCredentialsData credentialsData;
private ProvisionDeviceProfileCredentials credentials;
}

25
common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionResponse.java

@ -0,0 +1,25 @@
/**
* Copyright © 2016-2020 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.device.provision;
import lombok.Data;
import org.thingsboard.server.common.data.security.DeviceCredentials;
@Data
public class ProvisionResponse {
private final DeviceCredentials deviceCredentials;
private final ProvisionResponseStatus responseStatus;
}

23
common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionResponseStatus.java

@ -0,0 +1,23 @@
/**
* Copyright © 2016-2020 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.device.provision;
public enum ProvisionResponseStatus {
UNKNOWN,
SUCCESS,
NOT_FOUND,
FAILURE
}

3
common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateService.java

@ -19,10 +19,13 @@ import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate;
import java.util.List;
import java.util.Optional;
public interface OAuth2ConfigTemplateService {
OAuth2ClientRegistrationTemplate saveClientRegistrationTemplate(OAuth2ClientRegistrationTemplate clientRegistrationTemplate);
Optional<OAuth2ClientRegistrationTemplate> findClientRegistrationTemplateByProviderId(String providerId);
OAuth2ClientRegistrationTemplate findClientRegistrationTemplateById(OAuth2ClientRegistrationTemplateId templateId);
List<OAuth2ClientRegistrationTemplate> findAllClientRegistrationTemplates();

16
common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java

@ -63,6 +63,8 @@ public class DataConstants {
public static final String ALARM_CLEAR = "ALARM_CLEAR";
public static final String ENTITY_ASSIGNED_FROM_TENANT = "ENTITY_ASSIGNED_FROM_TENANT";
public static final String ENTITY_ASSIGNED_TO_TENANT = "ENTITY_ASSIGNED_TO_TENANT";
public static final String PROVISION_SUCCESS = "PROVISION_SUCCESS";
public static final String PROVISION_FAILURE = "PROVISION_FAILURE";
public static final String RPC_CALL_FROM_SERVER_TO_DEVICE = "RPC_CALL_FROM_SERVER_TO_DEVICE";
@ -70,4 +72,18 @@ public class DataConstants {
public static final String SECRET_KEY_FIELD_NAME = "secretKey";
public static final String DURATION_MS_FIELD_NAME = "durationMs";
public static final String PROVISION = "provision";
public static final String PROVISION_KEY = "provisionDeviceKey";
public static final String PROVISION_SECRET = "provisionDeviceSecret";
public static final String DEVICE_NAME = "deviceName";
public static final String DEVICE_TYPE = "deviceType";
public static final String CERT_PUB_KEY = "x509CertPubKey";
public static final String CREDENTIALS_TYPE = "credentialsType";
public static final String TOKEN = "token";
public static final String HASH = "hash";
public static final String CLIENT_ID = "clientId";
public static final String USERNAME = "username";
public static final String PASSWORD = "password";
}

3
common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java

@ -41,10 +41,12 @@ public class DeviceProfile extends SearchTextBased<DeviceProfileId> implements H
private boolean isDefault;
private DeviceProfileType type;
private DeviceTransportType transportType;
private DeviceProfileProvisionType provisionType;
private RuleChainId defaultRuleChainId;
private transient DeviceProfileData profileData;
@JsonIgnore
private byte[] profileDataBytes;
private String provisionDeviceKey;
public DeviceProfile() {
super();
@ -62,6 +64,7 @@ public class DeviceProfile extends SearchTextBased<DeviceProfileId> implements H
this.isDefault = deviceProfile.isDefault();
this.defaultRuleChainId = deviceProfile.getDefaultRuleChainId();
this.setProfileData(deviceProfile.getProfileData());
this.provisionDeviceKey = deviceProfile.getProvisionDeviceKey();
}
@Override

22
common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileProvisionType.java

@ -0,0 +1,22 @@
/**
* Copyright © 2016-2020 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;
public enum DeviceProfileProvisionType {
DISABLED,
ALLOW_CREATE_NEW_DEVICES,
CHECK_PRE_PROVISIONED_DEVICES
}

4
common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java

@ -42,7 +42,9 @@ public enum ActionType {
LOGOUT(false),
LOCKOUT(false),
ASSIGNED_FROM_TENANT(false),
ASSIGNED_TO_TENANT(false);
ASSIGNED_TO_TENANT(false),
PROVISION_SUCCESS(false),
PROVISION_FAILURE(false);
private final boolean isRead;

27
common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/ProvisionDeviceCredentialsData.java

@ -0,0 +1,27 @@
/**
* Copyright © 2016-2020 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.device.credentials;
import lombok.Data;
@Data
public class ProvisionDeviceCredentialsData {
private final String token;
private final String clientId;
private final String username;
private final String password;
private final String x509CertHash;
}

31
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AllowCreateNewDevicesDeviceProfileProvisionConfiguration.java

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2020 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.device.profile;
import lombok.Data;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
@Data
public class AllowCreateNewDevicesDeviceProfileProvisionConfiguration implements DeviceProfileProvisionConfiguration {
private final String provisionDeviceSecret;
@Override
public DeviceProfileProvisionType getType() {
return DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES;
}
}

31
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.java

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2020 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.device.profile;
import lombok.Data;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
@Data
public class CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration implements DeviceProfileProvisionConfiguration {
private final String provisionDeviceSecret;
@Override
public DeviceProfileProvisionType getType() {
return DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES;
}
}

1
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java

@ -24,6 +24,7 @@ public class DeviceProfileData {
private DeviceProfileConfiguration configuration;
private DeviceProfileTransportConfiguration transportConfiguration;
private DeviceProfileProvisionConfiguration provisionConfiguration;
private List<DeviceProfileAlarm> alarms;
}

41
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java

@ -0,0 +1,41 @@
/**
* Copyright © 2016-2020 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.device.profile;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = DisabledDeviceProfileProvisionConfiguration.class, name = "DISABLED"),
@JsonSubTypes.Type(value = AllowCreateNewDevicesDeviceProfileProvisionConfiguration.class, name = "ALLOW_CREATE_NEW_DEVICES"),
@JsonSubTypes.Type(value = CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.class, name = "CHECK_PRE_PROVISIONED_DEVICES")})
public interface DeviceProfileProvisionConfiguration {
String getProvisionDeviceSecret();
@JsonIgnore
DeviceProfileProvisionType getType();
}

31
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DisabledDeviceProfileProvisionConfiguration.java

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2020 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.device.profile;
import lombok.Data;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
@Data
public class DisabledDeviceProfileProvisionConfiguration implements DeviceProfileProvisionConfiguration {
private final String provisionDeviceSecret;
@Override
public DeviceProfileProvisionType getType() {
return DeviceProfileProvisionType.DISABLED;
}
}

14
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttTopics.java

@ -20,6 +20,8 @@ package org.thingsboard.server.common.data.device.profile;
*/
public class MqttTopics {
private static final String REQUEST = "/request";
private static final String RESPONSE = "/response";
private static final String RPC = "/rpc";
private static final String CONNECT = "/connect";
private static final String DISCONNECT = "/disconnect";
@ -27,11 +29,13 @@ public class MqttTopics {
private static final String ATTRIBUTES = "/attributes";
private static final String CLAIM = "/claim";
private static final String SUB_TOPIC = "+";
private static final String ATTRIBUTES_RESPONSE = "/attributes/response";
private static final String ATTRIBUTES_REQUEST = "/attributes/request";
private static final String PROVISION = "/provision";
private static final String DEVICE_RPC_RESPONSE = "/rpc/response/";
private static final String DEVICE_RPC_REQUEST = "/rpc/request/";
private static final String ATTRIBUTES_RESPONSE = ATTRIBUTES + RESPONSE;
private static final String ATTRIBUTES_REQUEST = ATTRIBUTES + REQUEST;
private static final String DEVICE_RPC_RESPONSE = RPC + RESPONSE + "/";
private static final String DEVICE_RPC_REQUEST = RPC + REQUEST + "/";
private static final String DEVICE_ATTRIBUTES_RESPONSE = ATTRIBUTES_RESPONSE + "/";
private static final String DEVICE_ATTRIBUTES_REQUEST = ATTRIBUTES_REQUEST + "/";
@ -50,6 +54,8 @@ public class MqttTopics {
public static final String DEVICE_TELEMETRY_TOPIC = BASE_DEVICE_API_TOPIC + TELEMETRY;
public static final String DEVICE_CLAIM_TOPIC = BASE_DEVICE_API_TOPIC + CLAIM;
public static final String DEVICE_ATTRIBUTES_TOPIC = BASE_DEVICE_API_TOPIC + ATTRIBUTES;
public static final String DEVICE_PROVISION_REQUEST_TOPIC = PROVISION + REQUEST;
public static final String DEVICE_PROVISION_RESPONSE_TOPIC = PROVISION + RESPONSE;
// V1_JSON gateway topics

24
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/ProvisionDeviceProfileCredentials.java

@ -0,0 +1,24 @@
/**
* Copyright © 2016-2020 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.device.profile;
import lombok.Data;
@Data
public class ProvisionDeviceProfileCredentials {
private final String provisionDeviceKey;
private final String provisionDeviceSecret;
}

6
common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java

@ -34,8 +34,7 @@ import java.util.List;
public class OAuth2ClientRegistrationTemplate extends SearchTextBasedWithAdditionalInfo<OAuth2ClientRegistrationTemplateId> implements HasName {
private String providerId;
private MapperType mapperType;
private OAuth2BasicMapperConfig basic;
private OAuth2MapperConfig mapperConfig;
private String authorizationUri;
private String accessTokenUri;
private List<String> scope;
@ -51,8 +50,7 @@ public class OAuth2ClientRegistrationTemplate extends SearchTextBasedWithAdditio
public OAuth2ClientRegistrationTemplate(OAuth2ClientRegistrationTemplate clientRegistrationTemplate) {
super(clientRegistrationTemplate);
this.providerId = clientRegistrationTemplate.providerId;
this.mapperType = clientRegistrationTemplate.mapperType;
this.basic = clientRegistrationTemplate.basic;
this.mapperConfig = clientRegistrationTemplate.mapperConfig;
this.authorizationUri = clientRegistrationTemplate.authorizationUri;
this.accessTokenUri = clientRegistrationTemplate.accessTokenUri;
this.scope = clientRegistrationTemplate.scope;

2
common/message/src/main/java/org/thingsboard/server/common/msg/session/FeatureType.java

@ -16,5 +16,5 @@
package org.thingsboard.server.common.msg.session;
public enum FeatureType {
ATTRIBUTES, TELEMETRY, RPC, CLAIM
ATTRIBUTES, TELEMETRY, RPC, CLAIM, PROVISION
}

47
common/queue/src/main/proto/queue.proto

@ -73,6 +73,12 @@ enum KeyValueType {
JSON_V = 4;
}
enum CredentialsType {
ACCESS_TOKEN = 0;
X509_CERTIFICATE = 1;
MQTT_BASIC = 2;
}
message KeyValueProto {
string key = 1;
KeyValueType type = 2;
@ -241,6 +247,43 @@ message ClaimDeviceMsg {
int64 durationMs = 4;
}
message DeviceCredentialsProto {
int64 deviceIdMSB = 1;
int64 deviceIdLSB = 2;
CredentialsType credentialsType = 3;
string credentialsId = 4;
string credentialsValue = 5;
}
message CredentialsDataProto {
ValidateDeviceTokenRequestMsg validateDeviceTokenRequestMsg = 1;
ValidateDeviceX509CertRequestMsg validateDeviceX509CertRequestMsg = 2;
ValidateBasicMqttCredRequestMsg validateBasicMqttCredRequestMsg = 3;
}
message ProvisionDeviceRequestMsg {
string deviceName = 1;
CredentialsType credentialsType = 2;
ProvisionDeviceCredentialsMsg provisionDeviceCredentialsMsg = 3;
CredentialsDataProto credentialsDataProto = 4;
}
message ProvisionDeviceCredentialsMsg {
string provisionDeviceKey = 1;
string provisionDeviceSecret = 2;
}
message ProvisionDeviceResponseMsg {
DeviceCredentialsProto deviceCredentials = 1;
ProvisionResponseStatus provisionResponseStatus = 2;
}
enum ProvisionResponseStatus {
UNKNOWN = 0;
SUCCESS = 1;
NOT_FOUND = 2;
FAILURE = 3;
}
//Used to report session state to tb-Service and persist this state in the cache on the tb-Service level.
message SubscriptionInfoProto {
int64 lastActivityTime = 1;
@ -266,6 +309,7 @@ message TransportToDeviceActorMsg {
ToDeviceRpcResponseMsg toDeviceRPCCallResponse = 6;
SubscriptionInfoProto subscriptionInfo = 7;
ClaimDeviceMsg claimDevice = 8;
ProvisionDeviceRequestMsg provisionDevice = 9;
}
message TransportToRuleEngineMsg {
@ -441,6 +485,7 @@ message TransportApiRequestMsg {
GetTenantRoutingInfoRequestMsg getTenantRoutingInfoRequestMsg = 4;
GetDeviceProfileRequestMsg getDeviceProfileRequestMsg = 5;
ValidateBasicMqttCredRequestMsg validateBasicMqttCredRequestMsg = 6;
ProvisionDeviceRequestMsg provisionDeviceRequestMsg = 7;
}
/* Response from ThingsBoard Core Service to Transport Service */
@ -449,6 +494,7 @@ message TransportApiResponseMsg {
GetOrCreateDeviceFromGatewayResponseMsg getOrCreateDeviceResponseMsg = 2;
GetTenantRoutingInfoResponseMsg getTenantRoutingInfoResponseMsg = 4;
GetDeviceProfileResponseMsg getDeviceProfileResponseMsg = 5;
ProvisionDeviceResponseMsg provisionDeviceResponseMsg = 6;
}
/* Messages that are handled by ThingsBoard Core Service */
@ -491,4 +537,5 @@ message ToTransportMsg {
ToServerRpcResponseMsg toServerResponse = 7;
DeviceProfileUpdateMsg deviceProfileUpdateMsg = 8;
DeviceProfileDeleteMsg deviceProfileDeleteMsg = 9;
ProvisionDeviceResponseMsg provisionResponse = 10;
}

39
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java

@ -24,6 +24,7 @@ import org.eclipse.californium.core.network.ExchangeObserver;
import org.eclipse.californium.core.server.resources.CoapExchange;
import org.eclipse.californium.core.server.resources.Resource;
import org.springframework.util.ReflectionUtils;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.security.DeviceTokenCredentials;
import org.thingsboard.server.common.msg.session.FeatureType;
@ -33,9 +34,11 @@ import org.thingsboard.server.common.transport.TransportContext;
import org.thingsboard.server.common.transport.TransportService;
import org.thingsboard.server.common.transport.TransportServiceCallback;
import org.thingsboard.server.common.transport.adaptor.AdaptorException;
import org.thingsboard.server.common.transport.adaptor.JsonConverter;
import org.thingsboard.server.common.transport.auth.SessionInfoCreator;
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg;
import java.lang.reflect.Field;
import java.util.List;
@ -130,10 +133,25 @@ public class CoapTransportResource extends CoapResource {
case CLAIM:
processRequest(exchange, SessionMsgType.CLAIM_REQUEST);
break;
case PROVISION:
processProvision(exchange);
break;
}
}
}
private void processProvision(CoapExchange exchange) {
log.trace("Processing {}", exchange.advanced().getRequest());
exchange.accept();
try {
transportService.process(transportContext.getAdaptor().convertToProvisionRequestMsg(UUID.randomUUID(), exchange.advanced().getRequest()),
new DeviceProvisionCallback(exchange));
} catch (AdaptorException e) {
log.trace("Failed to decode message: ", e);
exchange.respond(ResponseCode.BAD_REQUEST);
}
}
private void processRequest(CoapExchange exchange, SessionMsgType type) {
log.trace("Processing {}", exchange.advanced().getRequest());
exchange.accept();
@ -274,6 +292,8 @@ public class CoapTransportResource extends CoapResource {
try {
if (uriPath.size() >= FEATURE_TYPE_POSITION) {
return Optional.of(FeatureType.valueOf(uriPath.get(FEATURE_TYPE_POSITION - 1).toUpperCase()));
} else if (uriPath.size() == 3 && uriPath.contains(DataConstants.PROVISION)) {
return Optional.of(FeatureType.valueOf(DataConstants.PROVISION.toUpperCase()));
}
} catch (RuntimeException e) {
log.warn("Failed to decode feature type: {}", uriPath);
@ -325,6 +345,25 @@ public class CoapTransportResource extends CoapResource {
}
}
private static class DeviceProvisionCallback implements TransportServiceCallback<ProvisionDeviceResponseMsg> {
private final CoapExchange exchange;
DeviceProvisionCallback(CoapExchange exchange) {
this.exchange = exchange;
}
@Override
public void onSuccess(TransportProtos.ProvisionDeviceResponseMsg msg) {
exchange.respond(JsonConverter.toJson(msg).toString());
}
@Override
public void onError(Throwable e) {
log.warn("Failed to process request", e);
exchange.respond(ResponseCode.INTERNAL_SERVER_ERROR);
}
}
private static class CoapOkCallback implements TransportServiceCallback<Void> {
private final CoapExchange exchange;

3
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapTransportAdaptor.java

@ -19,6 +19,7 @@ import org.eclipse.californium.core.coap.Request;
import org.eclipse.californium.core.coap.Response;
import org.thingsboard.server.common.transport.adaptor.AdaptorException;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg;
import org.thingsboard.server.transport.coap.CoapTransportResource;
import java.util.UUID;
@ -45,4 +46,6 @@ public interface CoapTransportAdaptor {
Response convertToPublish(CoapTransportResource.CoapSessionListener coapSessionListener, TransportProtos.ToServerRpcResponseMsg msg) throws AdaptorException;
ProvisionDeviceRequestMsg convertToProvisionRequestMsg(UUID sessionId, Request inbound) throws AdaptorException;
}

10
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java

@ -123,6 +123,16 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor {
return response;
}
@Override
public TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(UUID sessionId, Request inbound) throws AdaptorException {
String payload = validatePayload(sessionId, inbound, false);
try {
return JsonConverter.convertToProvisionRequestMsg(payload);
} catch (IllegalStateException | JsonSyntaxException ex) {
throw new AdaptorException(ex);
}
}
@Override
public Response convertToPublish(CoapTransportResource.CoapSessionListener session, TransportProtos.GetAttributeResponseMsg msg) throws AdaptorException {
if (msg.getClientAttributeListCount() == 0 && msg.getSharedAttributeListCount() == 0) {

28
common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java

@ -44,6 +44,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotif
import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto;
import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.SessionCloseNotificationProto;
import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto;
import org.thingsboard.server.gen.transport.TransportProtos.SubscribeToAttributeUpdatesMsg;
@ -203,6 +204,14 @@ public class DeviceApiController {
return responseWriter;
}
@RequestMapping(value = "/provision", method = RequestMethod.POST)
public DeferredResult<ResponseEntity> provisionDevice(@RequestBody String json, HttpServletRequest httpRequest) {
DeferredResult<ResponseEntity> responseWriter = new DeferredResult<>();
transportContext.getTransportService().process(JsonConverter.convertToProvisionRequestMsg(json),
new DeviceProvisionCallback(responseWriter));
return responseWriter;
}
private static class DeviceAuthCallback implements TransportServiceCallback<ValidateDeviceCredentialsResponse> {
private final TransportContext transportContext;
private final DeferredResult<ResponseEntity> responseWriter;
@ -230,6 +239,25 @@ public class DeviceApiController {
}
}
private static class DeviceProvisionCallback implements TransportServiceCallback<ProvisionDeviceResponseMsg> {
private final DeferredResult<ResponseEntity> responseWriter;
DeviceProvisionCallback(DeferredResult<ResponseEntity> responseWriter) {
this.responseWriter = responseWriter;
}
@Override
public void onSuccess(ProvisionDeviceResponseMsg msg) {
responseWriter.setResult(new ResponseEntity<>(JsonConverter.toJson(msg).toString(), HttpStatus.OK));
}
@Override
public void onError(Throwable e) {
log.warn("Failed to process request", e);
responseWriter.setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
}
}
private static class SessionCloseOnErrorCallback implements TransportServiceCallback<Void> {
private final TransportService transportService;
private final SessionInfoProto sessionInfo;

113
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java

@ -16,6 +16,7 @@
package org.thingsboard.server.transport.mqtt;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.gson.JsonParseException;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.mqtt.MqttConnAckMessage;
@ -38,8 +39,10 @@ import io.netty.util.ReferenceCountUtil;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.TransportPayloadType;
import org.thingsboard.server.common.data.device.profile.MqttTopics;
import org.thingsboard.server.common.msg.EncryptionUtil;
import org.thingsboard.server.common.transport.SessionMsgListener;
@ -51,6 +54,7 @@ import org.thingsboard.server.common.transport.auth.TransportDeviceInfo;
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
import org.thingsboard.server.common.transport.service.DefaultTransportService;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.SessionEvent;
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg;
import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor;
@ -68,11 +72,12 @@ import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import static io.netty.handler.codec.mqtt.MqttConnectReturnCode.CONNECTION_ACCEPTED;
import static io.netty.handler.codec.mqtt.MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED;
import static io.netty.handler.codec.mqtt.MqttMessageType.CONNACK;
import static io.netty.handler.codec.mqtt.MqttMessageType.CONNECT;
import static io.netty.handler.codec.mqtt.MqttMessageType.PINGRESP;
import static io.netty.handler.codec.mqtt.MqttMessageType.PUBACK;
import static io.netty.handler.codec.mqtt.MqttMessageType.SUBACK;
@ -130,10 +135,58 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
return;
}
deviceSessionCtx.setChannel(ctx);
if (CONNECT.equals(msg.fixedHeader().messageType())) {
processConnect(ctx, (MqttConnectMessage) msg);
} else if (deviceSessionCtx.isProvisionOnly()) {
processProvisionSessionMsg(ctx, msg);
} else {
processRegularSessionMsg(ctx, msg);
}
}
private void processProvisionSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) {
switch (msg.fixedHeader().messageType()) {
case CONNECT:
processConnect(ctx, (MqttConnectMessage) msg);
case PUBLISH:
MqttPublishMessage mqttMsg = (MqttPublishMessage) msg;
String topicName = mqttMsg.variableHeader().topicName();
int msgId = mqttMsg.variableHeader().packetId();
try {
if (topicName.equals(MqttTopics.DEVICE_PROVISION_REQUEST_TOPIC)) {
try {
TransportProtos.ProvisionDeviceRequestMsg provisionRequestMsg = deviceSessionCtx.getContext().getJsonMqttAdaptor().convertToProvisionRequestMsg(deviceSessionCtx, mqttMsg);
transportService.process(provisionRequestMsg, new DeviceProvisionCallback(ctx, msgId, provisionRequestMsg));
log.trace("[{}][{}] Processing provision publish msg [{}][{}]!", sessionId, deviceSessionCtx.getDeviceId(), topicName, msgId);
} catch (Exception e) {
if (e instanceof JsonParseException || (e.getCause() != null && e.getCause() instanceof JsonParseException)) {
TransportProtos.ProvisionDeviceRequestMsg provisionRequestMsg = deviceSessionCtx.getContext().getProtoMqttAdaptor().convertToProvisionRequestMsg(deviceSessionCtx, mqttMsg);
transportService.process(provisionRequestMsg, new DeviceProvisionCallback(ctx, msgId, provisionRequestMsg));
deviceSessionCtx.setProvisionPayloadType(TransportPayloadType.PROTOBUF);
log.trace("[{}][{}] Processing provision publish msg [{}][{}]!", sessionId, deviceSessionCtx.getDeviceId(), topicName, msgId);
} else {
throw e;
}
}
} else {
throw new RuntimeException("Unsupported topic for provisioning requests!");
}
} catch (RuntimeException | AdaptorException e) {
log.warn("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e);
ctx.close();
}
break;
case PINGREQ:
ctx.writeAndFlush(new MqttMessage(new MqttFixedHeader(PINGRESP, false, AT_MOST_ONCE, false, 0)));
break;
case DISCONNECT:
if (checkConnected(ctx, msg)) {
processDisconnect(ctx);
}
break;
}
}
private void processRegularSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) {
switch (msg.fixedHeader().messageType()) {
case PUBLISH:
processPublish(ctx, (MqttPublishMessage) msg);
break;
@ -257,6 +310,42 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
};
}
private class DeviceProvisionCallback implements TransportServiceCallback<ProvisionDeviceResponseMsg> {
private final ChannelHandlerContext ctx;
private final int msgId;
private final TransportProtos.ProvisionDeviceRequestMsg msg;
DeviceProvisionCallback(ChannelHandlerContext ctx, int msgId, TransportProtos.ProvisionDeviceRequestMsg msg) {
this.ctx = ctx;
this.msgId = msgId;
this.msg = msg;
}
@Override
public void onSuccess(TransportProtos.ProvisionDeviceResponseMsg provisionResponseMsg) {
log.trace("[{}] Published msg: {}", sessionId, msg);
if (msgId > 0) {
ctx.writeAndFlush(createMqttPubAckMsg(msgId));
}
try {
if (deviceSessionCtx.getProvisionPayloadType().equals(TransportPayloadType.JSON)) {
deviceSessionCtx.getContext().getJsonMqttAdaptor().convertToPublish(deviceSessionCtx, provisionResponseMsg).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush);
} else {
deviceSessionCtx.getContext().getProtoMqttAdaptor().convertToPublish(deviceSessionCtx, provisionResponseMsg).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush);
}
transportService.getSchedulerExecutor().schedule(() -> processDisconnect(ctx), 60, TimeUnit.SECONDS);
} catch (Exception e) {
log.trace("[{}] Failed to convert device attributes response to MQTT msg", sessionId, e);
}
}
@Override
public void onError(Throwable e) {
log.trace("[{}] Failed to publish msg: {}", sessionId, msg, e);
processDisconnect(ctx);
}
}
private void processSubscribe(ChannelHandlerContext ctx, MqttSubscribeMessage mqttMsg) {
if (!checkConnected(ctx, mqttMsg)) {
return;
@ -286,6 +375,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
case MqttTopics.GATEWAY_ATTRIBUTES_TOPIC:
case MqttTopics.GATEWAY_RPC_TOPIC:
case MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC:
case MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC:
registerSubQoS(topic, grantedQoSList, reqQoS);
break;
default:
@ -351,11 +441,18 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
private void processConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) {
log.info("[{}] Processing connect msg for client: {}!", sessionId, msg.payload().clientIdentifier());
X509Certificate cert;
if (sslHandler != null && (cert = getX509Certificate()) != null) {
processX509CertConnect(ctx, cert);
String userName = msg.payload().userName();
String clientId = msg.payload().clientIdentifier();
if (DataConstants.PROVISION.equals(userName) || DataConstants.PROVISION.equals(clientId)) {
deviceSessionCtx.setProvisionOnly(true);
ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED));
} else {
processAuthTokenConnect(ctx, msg);
X509Certificate cert;
if (sslHandler != null && (cert = getX509Certificate()) != null) {
processX509CertConnect(ctx, cert);
} else {
processAuthTokenConnect(ctx, msg);
}
}
}
@ -387,7 +484,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
private void processX509CertConnect(ChannelHandlerContext ctx, X509Certificate cert) {
try {
if(!context.isSkipValidityCheckForClientCert()){
if (!context.isSkipValidityCheckForClientCert()) {
cert.checkValidity();
}
String strCert = SslUtil.getX509CertificateString(cert);

15
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java

@ -87,6 +87,16 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor {
}
}
@Override
public TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException {
String payload = validatePayload(ctx.getSessionId(), inbound.payload(), false);
try {
return JsonConverter.convertToProvisionRequestMsg(payload);
} catch (IllegalStateException | JsonSyntaxException ex) {
throw new AdaptorException(ex);
}
}
@Override
public TransportProtos.GetAttributeRequestMsg convertToGetAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException {
return processGetAttributeRequestMsg(inbound, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX);
@ -138,6 +148,11 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor {
return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_RPC_RESPONSE_TOPIC + rpcResponse.getRequestId(), JsonConverter.toJson(rpcResponse)));
}
@Override
public Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ProvisionDeviceResponseMsg provisionResponse) {
return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC, JsonConverter.toJson(provisionResponse)));
}
public static JsonElement validateJsonPayload(UUID sessionId, ByteBuf payloadData) throws AdaptorException {
String payload = validatePayload(sessionId, payloadData, false);
try {

6
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java

@ -24,6 +24,8 @@ import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestM
import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.PostAttributeMsg;
import org.thingsboard.server.gen.transport.TransportProtos.PostTelemetryMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcRequestMsg;
@ -63,4 +65,8 @@ public interface MqttTransportAdaptor {
Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, ToServerRpcResponseMsg rpcResponse) throws AdaptorException;
ProvisionDeviceRequestMsg convertToProvisionRequestMsg(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException;
Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, ProvisionDeviceResponseMsg provisionResponse) throws AdaptorException;
}

17
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java

@ -38,6 +38,7 @@ import org.thingsboard.server.common.transport.adaptor.ProtoConverter;
import org.thingsboard.server.gen.transport.TransportApiProtos;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.transport.mqtt.session.DeviceSessionCtx;
import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg;
import org.thingsboard.server.transport.mqtt.session.MqttDeviceAwareSessionContext;
import java.util.Optional;
@ -118,6 +119,17 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor {
}
}
@Override
public TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(MqttDeviceAwareSessionContext ctx, MqttPublishMessage mqttMsg) throws AdaptorException {
byte[] bytes = toBytes(mqttMsg.payload());
String topicName = mqttMsg.variableHeader().topicName();
try {
return ProtoConverter.convertToProvisionRequestMsg(bytes);
} catch (InvalidProtocolBufferException ex) {
throw new AdaptorException(ex);
}
}
@Override
public Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException {
if (!StringUtils.isEmpty(responseMsg.getError())) {
@ -149,6 +161,11 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor {
return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_ATTRIBUTES_TOPIC, notificationMsg.toByteArray()));
}
@Override
public Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ProvisionDeviceResponseMsg provisionResponse) {
return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC, provisionResponse.toByteArray()));
}
@Override
public Optional<MqttMessage> convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException {
if (!StringUtils.isEmpty(responseMsg.getError())) {

9
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java

@ -18,6 +18,7 @@ package org.thingsboard.server.transport.mqtt.session;
import com.google.protobuf.Descriptors;
import io.netty.channel.ChannelHandlerContext;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceTransportType;
@ -48,12 +49,20 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext {
private final AtomicInteger msgIdSeq = new AtomicInteger(0);
@Getter
@Setter
private boolean provisionOnly = false;
private volatile MqttTopicFilter telemetryTopicFilter = MqttTopicFilterFactory.getDefaultTelemetryFilter();
private volatile MqttTopicFilter attributesTopicFilter = MqttTopicFilterFactory.getDefaultAttributesFilter();
private volatile TransportPayloadType payloadType = TransportPayloadType.JSON;
private volatile Descriptors.Descriptor attributesDynamicMessageDescriptor;
private volatile Descriptors.Descriptor telemetryDynamicMessageDescriptor;
@Getter
@Setter
private TransportPayloadType provisionPayloadType = payloadType;
public DeviceSessionCtx(UUID sessionId, ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap, MqttTransportContext context) {
super(sessionId, mqttQoSMap);
this.context = context;

10
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java

@ -27,6 +27,8 @@ import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfo
import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.PostAttributeMsg;
import org.thingsboard.server.gen.transport.TransportProtos.PostTelemetryMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.SessionEventMsg;
import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto;
import org.thingsboard.server.gen.transport.TransportProtos.SubscribeToAttributeUpdatesMsg;
@ -38,6 +40,8 @@ import org.thingsboard.server.gen.transport.TransportProtos.ValidateBasicMqttCre
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg;
import java.util.concurrent.ScheduledExecutorService;
/**
* Created by ashvayka on 04.10.18.
*/
@ -57,6 +61,9 @@ public interface TransportService {
void process(GetOrCreateDeviceFromGatewayRequestMsg msg,
TransportServiceCallback<GetOrCreateDeviceFromGatewayResponse> callback);
void process(ProvisionDeviceRequestMsg msg,
TransportServiceCallback<ProvisionDeviceResponseMsg> callback);
void getDeviceProfile(DeviceProfileId deviceProfileId, TransportServiceCallback<DeviceProfile> callback);
void onProfileUpdate(DeviceProfile deviceProfile);
@ -83,6 +90,8 @@ public interface TransportService {
void process(SessionInfoProto sessionInfo, ClaimDeviceMsg msg, TransportServiceCallback<Void> callback);
ScheduledExecutorService getSchedulerExecutor();
void registerAsyncSession(SessionInfoProto sessionInfo, SessionMsgListener listener);
void registerSyncSession(SessionInfoProto sessionInfo, SessionMsgListener listener, long timeout);
@ -90,5 +99,4 @@ public interface TransportService {
void reportActivity(SessionInfoProto sessionInfo);
void deregisterSession(SessionInfoProto sessionInfo);
}

95
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java

@ -37,13 +37,19 @@ import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ClaimDeviceMsg;
import org.thingsboard.server.gen.transport.TransportProtos.CredentialsType;
import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.KeyValueProto;
import org.thingsboard.server.gen.transport.TransportProtos.KeyValueType;
import org.thingsboard.server.gen.transport.TransportProtos.PostAttributeMsg;
import org.thingsboard.server.gen.transport.TransportProtos.PostTelemetryMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ProvisionResponseStatus;
import org.thingsboard.server.gen.transport.TransportProtos.TsKvListProto;
import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto;
import org.thingsboard.server.gen.transport.TransportProtos.ValidateBasicMqttCredRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg;
import java.util.ArrayList;
import java.util.HashMap;
@ -53,6 +59,7 @@ import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@ -397,6 +404,36 @@ public class JsonConverter {
}
}
public static JsonObject toJson(ProvisionDeviceResponseMsg payload) {
return toJson(payload, false, 0);
}
public static JsonObject toJson(ProvisionDeviceResponseMsg payload, int requestId) {
return toJson(payload, true, requestId);
}
private static JsonObject toJson(ProvisionDeviceResponseMsg payload, boolean toGateway, int requestId) {
JsonObject result = new JsonObject();
if (payload.getProvisionResponseStatus() == TransportProtos.ProvisionResponseStatus.NOT_FOUND) {
result.addProperty("errorMsg", "Provision data was not found!");
result.addProperty("provisionDeviceStatus", ProvisionResponseStatus.NOT_FOUND.name());
} else if (payload.getProvisionResponseStatus() == TransportProtos.ProvisionResponseStatus.FAILURE) {
result.addProperty("errorMsg", "Failed to provision device!");
result.addProperty("provisionDeviceStatus", ProvisionResponseStatus.FAILURE.name());
} else {
if (toGateway) {
result.addProperty("id", requestId);
}
result.addProperty("deviceId", new UUID(payload.getDeviceCredentials().getDeviceIdMSB(), payload.getDeviceCredentials().getDeviceIdLSB()).toString());
result.addProperty("credentialsType", payload.getDeviceCredentials().getCredentialsType().name());
result.addProperty("credentialsId", payload.getDeviceCredentials().getCredentialsId());
result.addProperty("credentialsValue",
StringUtils.isEmpty(payload.getDeviceCredentials().getCredentialsValue()) ? null : payload.getDeviceCredentials().getCredentialsValue());
result.addProperty("provisionDeviceStatus", ProvisionResponseStatus.SUCCESS.name());
}
return result;
}
public static JsonElement toErrorJson(String errorMsg) {
JsonObject error = new JsonObject();
error.addProperty("error", errorMsg);
@ -410,6 +447,13 @@ public class JsonConverter {
return result;
}
public static JsonElement toGatewayJson(String deviceName, TransportProtos.ProvisionDeviceResponseMsg responseRequest) {
JsonObject result = new JsonObject();
result.addProperty(DEVICE_PROPERTY, deviceName);
result.add("data", JsonConverter.toJson(responseRequest));
return result;
}
public static Set<AttributeKvEntry> convertToAttributes(JsonElement element) {
Set<AttributeKvEntry> result = new HashSet<>();
long ts = System.currentTimeMillis();
@ -498,4 +542,55 @@ public class JsonConverter {
maxStringValueLength = length;
}
public static TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(String json) {
JsonElement jsonElement = new JsonParser().parse(json);
if (jsonElement.isJsonObject()) {
return buildProvisionRequestMsg(jsonElement.getAsJsonObject());
} else {
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + jsonElement);
}
}
public static TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(JsonObject jo) {
return buildProvisionRequestMsg(jo);
}
private static TransportProtos.ProvisionDeviceRequestMsg buildProvisionRequestMsg(JsonObject jo) {
return TransportProtos.ProvisionDeviceRequestMsg.newBuilder()
.setDeviceName(getStrValue(jo, DataConstants.DEVICE_NAME, true))
.setCredentialsType(jo.get(DataConstants.CREDENTIALS_TYPE) != null ? TransportProtos.CredentialsType.valueOf(getStrValue(jo, DataConstants.CREDENTIALS_TYPE, false)) : CredentialsType.ACCESS_TOKEN)
.setCredentialsDataProto(TransportProtos.CredentialsDataProto.newBuilder()
.setValidateDeviceTokenRequestMsg(ValidateDeviceTokenRequestMsg.newBuilder().setToken(getStrValue(jo, DataConstants.TOKEN, false)).build())
.setValidateBasicMqttCredRequestMsg(ValidateBasicMqttCredRequestMsg.newBuilder()
.setClientId(getStrValue(jo, DataConstants.CLIENT_ID, false))
.setUserName(getStrValue(jo, DataConstants.USERNAME, false))
.setPassword(getStrValue(jo, DataConstants.PASSWORD, false))
.build())
.setValidateDeviceX509CertRequestMsg(ValidateDeviceX509CertRequestMsg.newBuilder()
.setHash(getStrValue(jo, DataConstants.HASH, false)).build())
.build())
.setProvisionDeviceCredentialsMsg(buildProvisionDeviceCredentialsMsg(
getStrValue(jo, DataConstants.PROVISION_KEY, true),
getStrValue(jo, DataConstants.PROVISION_SECRET, true)))
.build();
}
private static TransportProtos.ProvisionDeviceCredentialsMsg buildProvisionDeviceCredentialsMsg(String provisionKey, String provisionSecret) {
return TransportProtos.ProvisionDeviceCredentialsMsg.newBuilder()
.setProvisionDeviceKey(provisionKey)
.setProvisionDeviceSecret(provisionSecret)
.build();
}
private static String getStrValue(JsonObject jo, String field, boolean requiredField) {
if (jo.has(field)) {
return jo.get(field).getAsString();
} else {
if (requiredField) {
throw new RuntimeException("Failed to find the field " + field + " in JSON body " + jo + "!");
}
return "";
}
}
}

3
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java

@ -130,6 +130,9 @@ public class ProtoConverter {
}
}
public static TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(byte[] bytes) throws InvalidProtocolBufferException {
return TransportProtos.ProvisionDeviceRequestMsg.parseFrom(bytes);
}
private static List<TransportProtos.KeyValueProto> validateKeyValueProtos(List<TransportProtos.KeyValueProto> kvList) {
kvList.forEach(keyValueProto -> {

22
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java

@ -34,7 +34,6 @@ import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.queue.ServiceQueue;
import org.thingsboard.server.common.msg.queue.ServiceType;
@ -49,9 +48,10 @@ import org.thingsboard.server.common.transport.TransportServiceCallback;
import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse;
import org.thingsboard.server.common.transport.auth.TransportDeviceInfo;
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
import org.thingsboard.server.common.transport.util.DataDecodingEncodingService;
import org.thingsboard.server.common.transport.util.JsonUtils;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg;
@ -75,11 +75,9 @@ import org.thingsboard.server.common.stats.StatsType;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@ -91,7 +89,6 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
/**
* Created by ashvayka on 17.10.18.
@ -234,6 +231,11 @@ public class DefaultTransportService implements TransportService {
}
}
@Override
public ScheduledExecutorService getSchedulerExecutor(){
return this.schedulerExecutor;
}
@Override
public void registerAsyncSession(TransportProtos.SessionInfoProto sessionInfo, SessionMsgListener listener) {
sessions.putIfAbsent(toSessionId(sessionInfo), new SessionMetaData(sessionInfo, TransportProtos.SessionType.ASYNC, listener));
@ -332,6 +334,16 @@ public class DefaultTransportService implements TransportService {
return tdi;
}
@Override
public void process(ProvisionDeviceRequestMsg requestMsg, TransportServiceCallback<ProvisionDeviceResponseMsg> callback) {
log.trace("Processing msg: {}", requestMsg);
TbProtoQueueMsg<TransportApiRequestMsg> protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setProvisionDeviceRequestMsg(requestMsg).build());
ListenableFuture<ProvisionDeviceResponseMsg> response = Futures.transform(transportApiRequestTemplate.send(protoMsg), tmp ->
tmp.getValue().getProvisionDeviceResponseMsg()
, MoreExecutors.directExecutor());
AsyncCallbackTemplate.withCallback(response, callback::onSuccess, callback::onError, transportCallbackExecutor);
}
@Override
public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.SubscriptionInfoProto msg, TransportServiceCallback<Void> callback) {
if (log.isTraceEnabled()) {

8
dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java

@ -47,6 +47,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.dao.audit.sink.AuditLogSink;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.device.provision.ProvisionRequest;
import org.thingsboard.server.dao.service.DataValidator;
import java.io.PrintWriter;
@ -257,6 +258,13 @@ public class AuditLogServiceImpl implements AuditLogService {
actionData.put("os", os);
actionData.put("device", device);
break;
case PROVISION_SUCCESS:
case PROVISION_FAILURE:
ProvisionRequest request = extractParameter(ProvisionRequest.class, additionalInfo);
if (request != null) {
actionData.set("provisionRequest", objectMapper.valueToTree(request));
}
break;
}
return actionData;
}

1
dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java

@ -214,5 +214,4 @@ public interface DeviceDao extends Dao<Device> {
* @return the list of device objects
*/
PageData<Device> findDevicesByTenantIdAndProfileId(UUID tenantId, UUID profileId, PageLink pageLink);
}

2
dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java

@ -38,5 +38,7 @@ public interface DeviceProfileDao extends Dao<DeviceProfile> {
DeviceProfileInfo findDefaultDeviceProfileInfo(TenantId tenantId);
DeviceProfile findByProvisionDeviceKey(String provisionDeviceKey);
DeviceProfile findByName(TenantId tenantId, String profileName);
}

7
dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java

@ -26,6 +26,7 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileInfo;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.EntitySubtype;
@ -33,6 +34,7 @@ import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
@ -112,6 +114,8 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_profile_name_unq_key")) {
throw new DataValidationException("Device profile with such name already exists!");
} else if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_provision_key_unq_key")) {
throw new DataValidationException("Device profile with such provision device key already exists!");
} else {
throw t;
}
@ -210,12 +214,15 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D
deviceProfile.setName(profileName);
deviceProfile.setType(DeviceProfileType.DEFAULT);
deviceProfile.setTransportType(DeviceTransportType.DEFAULT);
deviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED);
deviceProfile.setDescription("Default device profile");
DeviceProfileData deviceProfileData = new DeviceProfileData();
DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration();
DefaultDeviceProfileTransportConfiguration transportConfiguration = new DefaultDeviceProfileTransportConfiguration();
DisabledDeviceProfileProvisionConfiguration provisionConfiguration = new DisabledDeviceProfileProvisionConfiguration(null);
deviceProfileData.setConfiguration(configuration);
deviceProfileData.setTransportConfiguration(transportConfiguration);
deviceProfileData.setProvisionConfiguration(provisionConfiguration);
deviceProfile.setProfileData(deviceProfileData);
return saveDeviceProfile(deviceProfile);
}

49
dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java

@ -40,6 +40,7 @@ import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.device.DeviceSearchQuery;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration;
import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.data.DeviceData;
@ -57,6 +58,9 @@ import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.dao.customer.CustomerDao;
import org.thingsboard.server.dao.device.provision.ProvisionFailedException;
import org.thingsboard.server.dao.device.provision.ProvisionRequest;
import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.event.EventService;
@ -64,6 +68,7 @@ import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.tenant.TenantDao;
import org.thingsboard.server.dao.util.mapping.JacksonUtil;
import javax.annotation.Nullable;
import java.util.ArrayList;
@ -466,6 +471,50 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
return doSaveDevice(device, null);
}
@Override
@CacheEvict(cacheNames = DEVICE_CACHE, key = "{#profile.tenantId, #provisionRequest.deviceName}")
@Transactional
public Device saveDevice(ProvisionRequest provisionRequest, DeviceProfile profile) {
Device device = new Device();
device.setName(provisionRequest.getDeviceName());
device.setType(profile.getName());
device.setTenantId(profile.getTenantId());
Device savedDevice = saveDevice(device);
if (!StringUtils.isEmpty(provisionRequest.getCredentialsData().getToken()) ||
!StringUtils.isEmpty(provisionRequest.getCredentialsData().getX509CertHash()) ||
!StringUtils.isEmpty(provisionRequest.getCredentialsData().getUsername()) ||
!StringUtils.isEmpty(provisionRequest.getCredentialsData().getPassword()) ||
!StringUtils.isEmpty(provisionRequest.getCredentialsData().getClientId())) {
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedDevice.getTenantId(), savedDevice.getId());
if (deviceCredentials == null) {
deviceCredentials = new DeviceCredentials();
}
deviceCredentials.setDeviceId(savedDevice.getId());
deviceCredentials.setCredentialsType(provisionRequest.getCredentialsType());
switch (provisionRequest.getCredentialsType()) {
case ACCESS_TOKEN:
deviceCredentials.setCredentialsId(provisionRequest.getCredentialsData().getToken());
break;
case MQTT_BASIC:
BasicMqttCredentials mqttCredentials = new BasicMqttCredentials();
mqttCredentials.setClientId(provisionRequest.getCredentialsData().getClientId());
mqttCredentials.setUserName(provisionRequest.getCredentialsData().getUsername());
mqttCredentials.setPassword(provisionRequest.getCredentialsData().getPassword());
deviceCredentials.setCredentialsValue(JacksonUtil.toString(mqttCredentials));
break;
case X509_CERTIFICATE:
deviceCredentials.setCredentialsValue(provisionRequest.getCredentialsData().getX509CertHash());
break;
}
try {
deviceCredentialsService.updateDeviceCredentials(savedDevice.getTenantId(), deviceCredentials);
} catch (Exception e) {
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
}
}
return savedDevice;
}
private DataValidator<Device> deviceValidator =
new DataValidator<Device>() {

2
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -169,10 +169,12 @@ public class ModelConstants {
public static final String DEVICE_PROFILE_NAME_PROPERTY = "name";
public static final String DEVICE_PROFILE_TYPE_PROPERTY = "type";
public static final String DEVICE_PROFILE_TRANSPORT_TYPE_PROPERTY = "transport_type";
public static final String DEVICE_PROFILE_PROVISION_TYPE_PROPERTY = "provision_type";
public static final String DEVICE_PROFILE_PROFILE_DATA_PROPERTY = "profile_data";
public static final String DEVICE_PROFILE_DESCRIPTION_PROPERTY = "description";
public static final String DEVICE_PROFILE_IS_DEFAULT_PROPERTY = "is_default";
public static final String DEVICE_PROFILE_DEFAULT_RULE_CHAIN_ID_PROPERTY = "default_rule_chain_id";
public static final String DEVICE_PROFILE_PROVISION_DEVICE_KEY = "provision_device_key";
/**
* Cassandra entityView constants.

12
dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java

@ -23,6 +23,7 @@ import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.id.DeviceProfileId;
@ -62,6 +63,10 @@ public final class DeviceProfileEntity extends BaseSqlEntity<DeviceProfile> impl
@Column(name = ModelConstants.DEVICE_PROFILE_TRANSPORT_TYPE_PROPERTY)
private DeviceTransportType transportType;
@Enumerated(EnumType.STRING)
@Column(name = ModelConstants.DEVICE_PROFILE_PROVISION_TYPE_PROPERTY)
private DeviceProfileProvisionType provisionType;
@Column(name = ModelConstants.DEVICE_PROFILE_DESCRIPTION_PROPERTY)
private String description;
@ -78,6 +83,9 @@ public final class DeviceProfileEntity extends BaseSqlEntity<DeviceProfile> impl
@Column(name = ModelConstants.DEVICE_PROFILE_PROFILE_DATA_PROPERTY, columnDefinition = "jsonb")
private JsonNode profileData;
@Column(name=ModelConstants.DEVICE_PROFILE_PROVISION_DEVICE_KEY)
private String provisionDeviceKey;
public DeviceProfileEntity() {
super();
}
@ -93,12 +101,14 @@ public final class DeviceProfileEntity extends BaseSqlEntity<DeviceProfile> impl
this.name = deviceProfile.getName();
this.type = deviceProfile.getType();
this.transportType = deviceProfile.getTransportType();
this.provisionType = deviceProfile.getProvisionType();
this.description = deviceProfile.getDescription();
this.isDefault = deviceProfile.isDefault();
this.profileData = JacksonUtil.convertValue(deviceProfile.getProfileData(), ObjectNode.class);
if (deviceProfile.getDefaultRuleChainId() != null) {
this.defaultRuleChainId = deviceProfile.getDefaultRuleChainId().getId();
}
this.provisionDeviceKey = deviceProfile.getProvisionDeviceKey();
}
@Override
@ -125,12 +135,14 @@ public final class DeviceProfileEntity extends BaseSqlEntity<DeviceProfile> impl
deviceProfile.setName(name);
deviceProfile.setType(type);
deviceProfile.setTransportType(transportType);
deviceProfile.setProvisionType(provisionType);
deviceProfile.setDescription(description);
deviceProfile.setDefault(isDefault);
deviceProfile.setProfileData(JacksonUtil.convertValue(profileData, DeviceProfileData.class));
if (defaultRuleChainId != null) {
deviceProfile.setDefaultRuleChainId(new RuleChainId(defaultRuleChainId));
}
deviceProfile.setProvisionDeviceKey(provisionDeviceKey);
return deviceProfile;
}
}

50
dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationTemplateEntity.java

@ -109,17 +109,20 @@ public class OAuth2ClientRegistrationTemplateEntity extends BaseSqlEntity<OAuth2
this.loginButtonLabel = clientRegistrationTemplate.getLoginButtonLabel();
this.helpLink = clientRegistrationTemplate.getHelpLink();
this.additionalInfo = clientRegistrationTemplate.getAdditionalInfo();
this.type = clientRegistrationTemplate.getMapperType();
OAuth2BasicMapperConfig basicConfig = clientRegistrationTemplate.getBasic();
if (basicConfig != null) {
this.emailAttributeKey = basicConfig.getEmailAttributeKey();
this.firstNameAttributeKey = basicConfig.getFirstNameAttributeKey();
this.lastNameAttributeKey = basicConfig.getLastNameAttributeKey();
this.tenantNameStrategy = basicConfig.getTenantNameStrategy();
this.tenantNamePattern = basicConfig.getTenantNamePattern();
this.customerNamePattern = basicConfig.getCustomerNamePattern();
this.defaultDashboardName = basicConfig.getDefaultDashboardName();
this.alwaysFullScreen = basicConfig.isAlwaysFullScreen();
OAuth2MapperConfig mapperConfig = clientRegistrationTemplate.getMapperConfig();
if (mapperConfig != null){
this.type = mapperConfig.getType();
OAuth2BasicMapperConfig basicConfig = mapperConfig.getBasic();
if (basicConfig != null) {
this.emailAttributeKey = basicConfig.getEmailAttributeKey();
this.firstNameAttributeKey = basicConfig.getFirstNameAttributeKey();
this.lastNameAttributeKey = basicConfig.getLastNameAttributeKey();
this.tenantNameStrategy = basicConfig.getTenantNameStrategy();
this.tenantNamePattern = basicConfig.getTenantNamePattern();
this.customerNamePattern = basicConfig.getCustomerNamePattern();
this.defaultDashboardName = basicConfig.getDefaultDashboardName();
this.alwaysFullScreen = basicConfig.isAlwaysFullScreen();
}
}
}
@ -130,18 +133,21 @@ public class OAuth2ClientRegistrationTemplateEntity extends BaseSqlEntity<OAuth2
clientRegistrationTemplate.setCreatedTime(createdTime);
clientRegistrationTemplate.setAdditionalInfo(additionalInfo);
clientRegistrationTemplate.setMapperType(type);
clientRegistrationTemplate.setProviderId(providerId);
clientRegistrationTemplate.setBasic(
OAuth2BasicMapperConfig.builder()
.emailAttributeKey(emailAttributeKey)
.firstNameAttributeKey(firstNameAttributeKey)
.lastNameAttributeKey(lastNameAttributeKey)
.tenantNameStrategy(tenantNameStrategy)
.tenantNamePattern(tenantNamePattern)
.customerNamePattern(customerNamePattern)
.defaultDashboardName(defaultDashboardName)
.alwaysFullScreen(alwaysFullScreen)
clientRegistrationTemplate.setMapperConfig(
OAuth2MapperConfig.builder()
.type(type)
.basic(OAuth2BasicMapperConfig.builder()
.emailAttributeKey(emailAttributeKey)
.firstNameAttributeKey(firstNameAttributeKey)
.lastNameAttributeKey(lastNameAttributeKey)
.tenantNameStrategy(tenantNameStrategy)
.tenantNamePattern(tenantNamePattern)
.customerNamePattern(customerNamePattern)
.defaultDashboardName(defaultDashboardName)
.alwaysFullScreen(alwaysFullScreen)
.build()
)
.build()
);
clientRegistrationTemplate.setAuthorizationUri(authorizationUri);

4
dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationTemplateDao.java

@ -19,7 +19,11 @@ import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplat
import org.thingsboard.server.dao.Dao;
import java.util.List;
import java.util.Optional;
public interface OAuth2ClientRegistrationTemplateDao extends Dao<OAuth2ClientRegistrationTemplate> {
Optional<OAuth2ClientRegistrationTemplate> findByProviderId(String providerId);
List<OAuth2ClientRegistrationTemplate> findAll();
}

17
dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateServiceImpl.java

@ -29,6 +29,7 @@ import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator;
import java.util.List;
import java.util.Optional;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validateString;
@ -37,6 +38,7 @@ import static org.thingsboard.server.dao.service.Validator.validateString;
@Service
public class OAuth2ConfigTemplateServiceImpl extends AbstractEntityService implements OAuth2ConfigTemplateService {
public static final String INCORRECT_CLIENT_REGISTRATION_TEMPLATE_ID = "Incorrect clientRegistrationTemplateId ";
public static final String INCORRECT_CLIENT_REGISTRATION_PROVIDER_ID = "Incorrect clientRegistrationProviderId ";
@Autowired
private OAuth2ClientRegistrationTemplateDao clientRegistrationTemplateDao;
@ -59,6 +61,13 @@ public class OAuth2ConfigTemplateServiceImpl extends AbstractEntityService imple
return savedClientRegistrationTemplate;
}
@Override
public Optional<OAuth2ClientRegistrationTemplate> findClientRegistrationTemplateByProviderId(String providerId) {
log.trace("Executing findClientRegistrationTemplateByProviderId [{}]", providerId);
validateString(providerId, INCORRECT_CLIENT_REGISTRATION_PROVIDER_ID + providerId);
return clientRegistrationTemplateDao.findByProviderId(providerId);
}
@Override
public OAuth2ClientRegistrationTemplate findClientRegistrationTemplateById(OAuth2ClientRegistrationTemplateId templateId) {
log.trace("Executing findClientRegistrationTemplateById [{}]", templateId);
@ -95,7 +104,13 @@ public class OAuth2ConfigTemplateServiceImpl extends AbstractEntityService imple
if (StringUtils.isEmpty(clientRegistrationTemplate.getProviderId())) {
throw new DataValidationException("Provider ID should be specified!");
}
if (clientRegistrationTemplate.getBasic() == null) {
if (clientRegistrationTemplate.getMapperConfig() == null) {
throw new DataValidationException("Mapper config should be specified!");
}
if (clientRegistrationTemplate.getMapperConfig().getType() == null) {
throw new DataValidationException("Mapper type should be specified!");
}
if (clientRegistrationTemplate.getMapperConfig().getBasic() == null) {
throw new DataValidationException("Basic mapper config should be specified!");
}
}

2
dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java

@ -20,7 +20,6 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileInfo;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.dao.model.sql.DeviceProfileEntity;
@ -66,4 +65,5 @@ public interface DeviceProfileRepository extends PagingAndSortingRepository<Devi
DeviceProfileEntity findByTenantIdAndName(UUID id, String profileName);
DeviceProfileEntity findByProvisionDeviceKey(@Param("provisionDeviceKey") String provisionDeviceKey);
}

1
dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java

@ -168,5 +168,4 @@ public interface DeviceRepository extends PagingAndSortingRepository<DeviceEntit
DeviceEntity findByTenantIdAndId(UUID tenantId, UUID id);
Long countByDeviceProfileId(UUID deviceProfileId);
}

5
dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java

@ -91,6 +91,11 @@ public class JpaDeviceProfileDao extends JpaAbstractSearchTextDao<DeviceProfileE
return deviceProfileRepository.findDefaultDeviceProfileInfo(tenantId.getId());
}
@Override
public DeviceProfile findByProvisionDeviceKey(String provisionDeviceKey) {
return DaoUtil.getData(deviceProfileRepository.findByProvisionDeviceKey(provisionDeviceKey));
}
@Override
public DeviceProfile findByName(TenantId tenantId, String profileName) {
return DaoUtil.getData(deviceProfileRepository.findByTenantIdAndName(tenantId.getId(), profileName));

7
dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationTemplateDao.java

@ -26,6 +26,7 @@ import org.thingsboard.server.dao.sql.JpaAbstractDao;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Component
@ -43,6 +44,12 @@ public class JpaOAuth2ClientRegistrationTemplateDao extends JpaAbstractDao<OAuth
return repository;
}
@Override
public Optional<OAuth2ClientRegistrationTemplate> findByProviderId(String providerId) {
OAuth2ClientRegistrationTemplate oAuth2ClientRegistrationTemplate = DaoUtil.getData(repository.findByProviderId(providerId));
return Optional.ofNullable(oAuth2ClientRegistrationTemplate);
}
@Override
public List<OAuth2ClientRegistrationTemplate> findAll() {
Iterable<OAuth2ClientRegistrationTemplateEntity> entities = repository.findAll();

3
dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationTemplateRepository.java

@ -21,4 +21,7 @@ import org.thingsboard.server.dao.model.sql.OAuth2ClientRegistrationTemplateEnti
import java.util.UUID;
public interface OAuth2ClientRegistrationTemplateRepository extends CrudRepository<OAuth2ClientRegistrationTemplateEntity, UUID> {
OAuth2ClientRegistrationTemplateEntity findByProviderId(String providerId);
}

3
dao/src/main/resources/sql/schema-entities-hsql.sql

@ -163,13 +163,16 @@ CREATE TABLE IF NOT EXISTS device_profile (
name varchar(255),
type varchar(255),
transport_type varchar(255),
provision_type varchar(255),
profile_data jsonb,
description varchar,
search_text varchar(255),
is_default boolean,
tenant_id uuid,
default_rule_chain_id uuid,
provision_device_key varchar,
CONSTRAINT device_profile_name_unq_key UNIQUE (tenant_id, name),
CONSTRAINT device_provision_key_unq_key UNIQUE (provision_device_key),
CONSTRAINT fk_default_rule_chain_device_profile FOREIGN KEY (default_rule_chain_id) REFERENCES rule_chain(id)
);

3
dao/src/main/resources/sql/schema-entities.sql

@ -181,13 +181,16 @@ CREATE TABLE IF NOT EXISTS device_profile (
name varchar(255),
type varchar(255),
transport_type varchar(255),
provision_type varchar(255),
profile_data jsonb,
description varchar,
search_text varchar(255),
is_default boolean,
tenant_id uuid,
default_rule_chain_id uuid,
provision_device_key varchar,
CONSTRAINT device_profile_name_unq_key UNIQUE (tenant_id, name),
CONSTRAINT device_provision_key_unq_key UNIQUE (provision_device_key),
CONSTRAINT fk_default_rule_chain_device_profile FOREIGN KEY (default_rule_chain_id) REFERENCES rule_chain(id)
);

10
dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ConfigTemplateServiceTest.java

@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.oauth2.MapperType;
import org.thingsboard.server.common.data.oauth2.OAuth2BasicMapperConfig;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate;
import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService;
@ -106,9 +107,9 @@ public class BaseOAuth2ConfigTemplateServiceTest extends AbstractServiceTest {
OAuth2ClientRegistrationTemplate clientRegistrationTemplate = new OAuth2ClientRegistrationTemplate();
clientRegistrationTemplate.setProviderId(providerId);
clientRegistrationTemplate.setAdditionalInfo(mapper.createObjectNode().put(UUID.randomUUID().toString(), UUID.randomUUID().toString()));
clientRegistrationTemplate.setMapperType(MapperType.BASIC);
clientRegistrationTemplate.setBasic(
OAuth2BasicMapperConfig.builder()
clientRegistrationTemplate.setMapperConfig(OAuth2MapperConfig.builder()
.type(MapperType.BASIC)
.basic(OAuth2BasicMapperConfig.builder()
.firstNameAttributeKey("firstName")
.lastNameAttributeKey("lastName")
.emailAttributeKey("email")
@ -116,7 +117,8 @@ public class BaseOAuth2ConfigTemplateServiceTest extends AbstractServiceTest {
.defaultDashboardName("Test")
.alwaysFullScreen(true)
.build()
);
)
.build());
clientRegistrationTemplate.setAuthorizationUri("authorizationUri");
clientRegistrationTemplate.setAccessTokenUri("tokenUri");
clientRegistrationTemplate.setScope(Arrays.asList("scope1", "scope2"));

18
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java

@ -165,9 +165,7 @@ class AlarmRuleState {
return false;
}
}
long startOfDay = zdt.toLocalDate().atStartOfDay(zoneId).toInstant().toEpochMilli();
long msFromStartOfDay = eventTs - startOfDay;
return schedule.getStartsOn() <= msFromStartOfDay && schedule.getEndsOn() > msFromStartOfDay;
return isActive(eventTs, zoneId, zdt, schedule.getStartsOn(), schedule.getEndsOn());
}
private boolean isActiveCustom(CustomTimeSchedule schedule, long eventTs) {
@ -177,9 +175,7 @@ class AlarmRuleState {
for (CustomTimeScheduleItem item : schedule.getItems()) {
if (item.getDayOfWeek() == dayOfWeek) {
if (item.isEnabled()) {
long startOfDay = zdt.toLocalDate().atStartOfDay(zoneId).toInstant().toEpochMilli();
long msFromStartOfDay = eventTs - startOfDay;
return item.getStartsOn() <= msFromStartOfDay && item.getEndsOn() > msFromStartOfDay;
return isActive(eventTs, zoneId, zdt, item.getStartsOn(), item.getEndsOn());
} else {
return false;
}
@ -188,6 +184,16 @@ class AlarmRuleState {
return false;
}
private boolean isActive(long eventTs, ZoneId zoneId, ZonedDateTime zdt, long startsOn, long endsOn) {
long startOfDay = zdt.toLocalDate().atStartOfDay(zoneId).toInstant().toEpochMilli();
long msFromStartOfDay = eventTs - startOfDay;
if (startsOn <= endsOn) {
return startsOn <= msFromStartOfDay && endsOn > msFromStartOfDay;
} else {
return startsOn < msFromStartOfDay || (0 < msFromStartOfDay && msFromStartOfDay < endsOn);
}
}
public void clear() {
if (state.getEventCount() > 0 || state.getLastEventTs() > 0 || state.getDuration() > 0) {
state.setEventCount(0L);

8
ui-ngx/proxy.conf.js

@ -27,6 +27,14 @@ const PROXY_CONFIG = {
"target": ruleNodeUiforwardUrl,
"secure": false,
},
"/oauth2": {
"target": forwardUrl,
"secure": false,
},
"/login/oauth2": {
"target": forwardUrl,
"secure": false,
},
"/static": {
"target": forwardUrl,
"secure": false,

21
ui-ngx/src/app/app.component.ts

@ -68,6 +68,27 @@ export class AppComponent implements OnInit {
)
);
this.matIconRegistry.addSvgIconLiteral(
'google-logo',
this.domSanitizer.bypassSecurityTrustHtml(
'<svg viewBox="0 0 48 48"><path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/><path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/><path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/><path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/><path fill="none" d="M0 0h48v48H0z"/></svg>'
)
);
this.matIconRegistry.addSvgIconLiteral(
'github-logo',
this.domSanitizer.bypassSecurityTrustHtml(
'<svg viewBox="0 0 32.7 32.7"><path d="M16.3 0C7.3 0 0 7.3 0 16.3c0 7.2 4.7 13.3 11.1 15.5.8.1 1.1-.4 1.1-.8v-2.8c-4.5 1-5.5-2.2-5.5-2.2-.7-1.9-1.8-2.4-1.8-2.4-1.5-1 .1-1 .1-1 1.6.1 2.5 1.7 2.5 1.7 1.5 2.5 3.8 1.8 4.7 1.4.1-1.1.6-1.8 1-2.2-3.6-.4-7.4-1.8-7.4-8.1 0-1.8.6-3.2 1.7-4.4-.2-.4-.7-2.1.2-4.3 0 0 1.4-.4 4.5 1.7 1.3-.4 2.7-.5 4.1-.5s2.8.2 4.1.5c3.1-2.1 4.5-1.7 4.5-1.7.9 2.2.3 3.9.2 4.3 1 1.1 1.7 2.6 1.7 4.4 0 6.3-3.8 7.6-7.4 8 .6.5 1.1 1.5 1.1 3v4.5c0 .4.3.9 1.1.8 6.5-2.2 11.1-8.3 11.1-15.5C32.6 7.3 25.3 0 16.3 0z" fill="#211c19"/></svg>'
)
);
this.matIconRegistry.addSvgIconLiteral(
'facebook-logo',
this.domSanitizer.bypassSecurityTrustHtml(
'<svg viewBox="0 0 263 263"><path d="M263 131.5C263 58.9 204.1 0 131.5 0S0 58.9 0 131.5c0 65.6 48.1 120 110.9 129.9v-91.9H77.5v-38h33.4v-29c0-33 19.6-51.2 49.7-51.2 14.4 0 29.4 2.6 29.4 2.6v32.4h-16.5c-16.3 0-21.4 10.1-21.4 20.5v24.7h36.4l-5.8 38h-30.6v91.9c62.8-9.9 110.9-64.3 110.9-129.9z" fill="#1877f2"/><path d="M182.7 169.5l5.8-38H152v-24.7c0-10.4 5.1-20.5 21.4-20.5H190V53.9s-15-2.6-29.4-2.6c-30 0-49.7 18.2-49.7 51.2v29H77.5v38h33.4v91.9c6.7 1.1 13.6 1.6 20.5 1.6s13.9-.5 20.5-1.6v-91.9h30.8z" fill="#fff"/></svg>'
)
);
this.storageService.testLocalStorage();
this.setupTranslate();

18
ui-ngx/src/app/core/auth/auth.service.ts

@ -21,7 +21,7 @@ import { HttpClient } from '@angular/common/http';
import { forkJoin, Observable, of, ReplaySubject, throwError } from 'rxjs';
import { catchError, map, mergeMap, tap } from 'rxjs/operators';
import { LoginRequest, LoginResponse, OAuth2Client, PublicLoginRequest } from '@shared/models/login.models';
import { LoginRequest, LoginResponse, PublicLoginRequest } from '@shared/models/login.models';
import { ActivatedRoute, Router, UrlTree } from '@angular/router';
import { defaultHttpOptions } from '../http/http-utils';
import { UserService } from '../http/user.service';
@ -44,6 +44,7 @@ import { AdminService } from '@core/http/admin.service';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { MatDialog, MatDialogConfig } from '@angular/material/dialog';
import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component';
import { OAuth2ClientInfo } from '@shared/models/oauth2.models';
@Injectable({
providedIn: 'root'
@ -67,7 +68,7 @@ export class AuthService {
}
redirectUrl: string;
oauth2Clients: Array<OAuth2Client> = null;
oauth2Clients: Array<OAuth2ClientInfo> = null;
private refreshTokenSubject: ReplaySubject<LoginResponse> = null;
private jwtHelper = new JwtHelperService();
@ -197,13 +198,14 @@ export class AuthService {
});
}
public loadOAuth2Clients(): Observable<Array<OAuth2Client>> {
return this.http.post<Array<OAuth2Client>>(`/api/noauth/oauth2Clients`,
public loadOAuth2Clients(): Observable<Array<OAuth2ClientInfo>> {
return this.http.post<Array<OAuth2ClientInfo>>(`/api/noauth/oauth2Clients`,
null, defaultHttpOptions()).pipe(
tap((OAuth2Clients) => {
this.oauth2Clients = OAuth2Clients;
})
);
catchError(err => of([])),
tap((OAuth2Clients) => {
this.oauth2Clients = OAuth2Clients;
})
);
}
private forceDefaultPlace(authState?: AuthState, path?: string, params?: any): boolean {

22
ui-ngx/src/app/core/http/admin.service.ts

@ -18,14 +18,7 @@ import { Injectable } from '@angular/core';
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import {
AdminSettings,
ClientProviderTemplated,
MailServerSettings,
OAuth2Settings,
SecuritySettings,
UpdateMessage
} from '@shared/models/settings.models';
import { AdminSettings, MailServerSettings, SecuritySettings, UpdateMessage } from '@shared/models/settings.models';
@Injectable({
providedIn: 'root'
@ -60,19 +53,6 @@ export class AdminService {
defaultHttpOptionsFromConfig(config));
}
public getOAuth2Settings(config?: RequestConfig): Observable<OAuth2Settings> {
return this.http.get<OAuth2Settings>(`/api/oauth2/config`, defaultHttpOptionsFromConfig(config));
}
public getOAuth2Template(config?: RequestConfig): Observable<Array<ClientProviderTemplated>> {
return this.http.get<Array<ClientProviderTemplated>>(`/api/oauth2/config/template`, defaultHttpOptionsFromConfig(config));
}
public saveOAuth2Settings(OAuth2Setting: OAuth2Settings, config?: RequestConfig): Observable<OAuth2Settings> {
return this.http.post<OAuth2Settings>('/api/oauth2/config', OAuth2Setting,
defaultHttpOptionsFromConfig(config));
}
public checkUpdates(config?: RequestConfig): Observable<UpdateMessage> {
return this.http.get<UpdateMessage>(`/api/admin/updates`, defaultHttpOptionsFromConfig(config));
}

44
ui-ngx/src/app/core/http/oauth2.service.ts

@ -0,0 +1,44 @@
///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
import { Observable } from 'rxjs';
import { OAuth2ClientRegistrationTemplate, OAuth2ClientsParams } from '@shared/models/oauth2.models';
@Injectable({
providedIn: 'root'
})
export class OAuth2Service {
constructor(
private http: HttpClient
) { }
public getOAuth2Settings(config?: RequestConfig): Observable<OAuth2ClientsParams> {
return this.http.get<OAuth2ClientsParams>(`/api/oauth2/config`, defaultHttpOptionsFromConfig(config));
}
public getOAuth2Template(config?: RequestConfig): Observable<Array<OAuth2ClientRegistrationTemplate>> {
return this.http.get<Array<OAuth2ClientRegistrationTemplate>>(`/api/oauth2/config/template`, defaultHttpOptionsFromConfig(config));
}
public saveOAuth2Settings(OAuth2Setting: OAuth2ClientsParams, config?: RequestConfig): Observable<OAuth2ClientsParams> {
return this.http.post<OAuth2ClientsParams>('/api/oauth2/config', OAuth2Setting,
defaultHttpOptionsFromConfig(config));
}
}

1
ui-ngx/src/app/core/http/public-api.ts

@ -28,6 +28,7 @@ export * from './entity-relation.service';
export * from './entity-view.service';
export * from './event.service';
export * from './http-utils';
export * from './oauth2.service';
export * from './queue.service';
export * from './rule-chain.service';
export * from './tenant.service';

12
ui-ngx/src/app/core/utils.ts

@ -403,3 +403,15 @@ export function deepTrim<T>(obj: T): T {
return acc;
}, (Array.isArray(obj) ? [] : {}) as T);
}
export function generateSecret(length?: number): string {
if (isUndefined(length) || length == null) {
length = 1;
}
const l = length > 10 ? 10 : length;
const str = Math.random().toString(36).substr(2, l);
if (str.length >= length) {
return str;
}
return str.concat(generateSecret(length - str.length));
}

6
ui-ngx/src/app/modules/home/components/device/device-credentials.component.html

@ -70,8 +70,8 @@
<mat-icon>{{hidePassword ? 'visibility_off' : 'visibility'}}</mat-icon>
</button>
</mat-form-field>
<mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsBasic').hasError('atLeastOne')">
{{ 'device.client-id-or-user-name-necessary' | translate }}
</mat-error>
<tb-error style="margin-top: -12px; display: block;"
[error]="deviceCredentialsFormGroup.get('credentialsBasic').hasError('atLeastOne') ?
('device.client-id-or-user-name-necessary' | translate) : ''"></tb-error>
</section>
</section>

7
ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts

@ -174,8 +174,8 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
updateValidators(): void {
this.hidePassword = true;
const crendetialsType = this.deviceCredentialsFormGroup.get('credentialsType').value as DeviceCredentialsType;
switch (crendetialsType) {
const credentialsType = this.deviceCredentialsFormGroup.get('credentialsType').value as DeviceCredentialsType;
switch (credentialsType) {
case DeviceCredentialsType.ACCESS_TOKEN:
this.deviceCredentialsFormGroup.get('credentialsId').setValidators([Validators.required, Validators.pattern(/^.{1,20}$/)]);
this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity({emitEvent: false});
@ -215,9 +215,6 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
const value = this.deviceCredentialsFormGroup.get('credentialsBasic.password').value;
if (value !== '') {
this.deviceCredentialsFormGroup.get('credentialsBasic.userName').setValidators([Validators.required]);
if (this.deviceCredentialsFormGroup.get('credentialsBasic.userName').untouched) {
this.deviceCredentialsFormGroup.get('credentialsBasic.userName').markAsTouched({onlySelf: true});
}
} else {
this.deviceCredentialsFormGroup.get('credentialsBasic.userName').setValidators([]);
}

21
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -102,13 +102,17 @@ import { DeviceProfileAlarmComponent } from './profile/alarm/device-profile-alar
import { CreateAlarmRulesComponent } from './profile/alarm/create-alarm-rules.component';
import { AlarmRuleComponent } from './profile/alarm/alarm-rule.component';
import { AlarmRuleConditionComponent } from './profile/alarm/alarm-rule-condition.component';
import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-key-filters-dialog.component';
import { FilterTextComponent } from './filter/filter-text.component';
import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-dialog.component';
import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomplete.component';
import { DeviceProfileProvisionConfigurationComponent } from "./profile/device-profile-provision-configuration.component";
import { AlarmScheduleComponent } from './profile/alarm/alarm-schedule.component';
import { DeviceWizardDialogComponent } from './wizard/device-wizard-dialog.component';
import { DeviceCredentialsComponent } from './device/device-credentials.component';
import { AlarmScheduleInfoComponent } from './profile/alarm/alarm-schedule-info.component';
import { AlarmScheduleDialogComponent } from '@home/components/profile/alarm/alarm-schedule-dialog.component';
import { EditAlarmDetailsDialogComponent } from './profile/alarm/edit-alarm-details-dialog.component';
import { AlarmRuleConditionDialogComponent } from '@home/components/profile/alarm/alarm-rule-condition-dialog.component';
@NgModule({
declarations:
@ -190,7 +194,7 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen
DeviceProfileTransportConfigurationComponent,
CreateAlarmRulesComponent,
AlarmRuleComponent,
AlarmRuleKeyFiltersDialogComponent,
AlarmRuleConditionDialogComponent,
AlarmRuleConditionComponent,
DeviceProfileAlarmComponent,
DeviceProfileAlarmsComponent,
@ -198,9 +202,13 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen
DeviceProfileDialogComponent,
AddDeviceProfileDialogComponent,
RuleChainAutocompleteComponent,
AlarmScheduleInfoComponent,
DeviceProfileProvisionConfigurationComponent,
AlarmScheduleComponent,
DeviceWizardDialogComponent,
DeviceCredentialsComponent
DeviceCredentialsComponent,
AlarmScheduleDialogComponent,
EditAlarmDetailsDialogComponent
],
imports: [
CommonModule,
@ -271,7 +279,7 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen
DeviceProfileTransportConfigurationComponent,
CreateAlarmRulesComponent,
AlarmRuleComponent,
AlarmRuleKeyFiltersDialogComponent,
AlarmRuleConditionDialogComponent,
AlarmRuleConditionComponent,
DeviceProfileAlarmComponent,
DeviceProfileAlarmsComponent,
@ -281,6 +289,11 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen
RuleChainAutocompleteComponent,
DeviceWizardDialogComponent,
DeviceCredentialsComponent,
AlarmScheduleInfoComponent,
AlarmScheduleComponent,
AlarmScheduleDialogComponent,
EditAlarmDetailsDialogComponent,
DeviceProfileProvisionConfigurationComponent,
AlarmScheduleComponent
],
providers: [

8
ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html

@ -96,6 +96,14 @@
</tb-device-profile-alarms>
</form>
</mat-step>
<mat-step [stepControl]="provisionConfigFormGroup" [optional]="true">
<form [formGroup]="provisionConfigFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{ 'device-profile.device-provisioning' | translate }}</ng-template>
<tb-device-profile-provision-configuration
formControlName="provisionConfiguration">
</tb-device-profile-provision-configuration>
</form>
</mat-step>
</mat-horizontal-stepper>
</div>
<div mat-dialog-actions fxLayout="column" fxLayoutAlign="start wrap" fxLayoutGap="8px" style="height: 100px;">

29
ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts

@ -36,7 +36,10 @@ import {
DeviceProfile,
DeviceProfileType,
deviceProfileTypeTranslationMap,
DeviceTransportType, deviceTransportTypeHintMap,
DeviceProvisionConfiguration,
DeviceProvisionType,
DeviceTransportType,
deviceTransportTypeHintMap,
deviceTransportTypeTranslationMap
} from '@shared/models/device.models';
import { DeviceProfileService } from '@core/http/device-profile.service';
@ -84,6 +87,8 @@ export class AddDeviceProfileDialogComponent extends
alarmRulesFormGroup: FormGroup;
provisionConfigFormGroup: FormGroup;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AddDeviceProfileDialogData,
@ -118,6 +123,14 @@ export class AddDeviceProfileDialogComponent extends
alarms: [null]
}
);
this.provisionConfigFormGroup = this.fb.group(
{
provisionConfiguration: [{
type: DeviceProvisionType.DISABLED
} as DeviceProvisionConfiguration, [Validators.required]]
}
);
}
private deviceProfileTransportTypeChanged() {
@ -138,7 +151,7 @@ export class AddDeviceProfileDialogComponent extends
}
nextStep() {
if (this.selectedIndex < 2) {
if (this.selectedIndex < 3) {
this.addDeviceProfileStepper.next();
} else {
this.add();
@ -153,20 +166,28 @@ export class AddDeviceProfileDialogComponent extends
return this.transportConfigFormGroup;
case 2:
return this.alarmRulesFormGroup;
case 3:
return this.provisionConfigFormGroup;
}
}
add(): void {
if (this.allValid()) {
const deviceProvisionConfiguration: DeviceProvisionConfiguration = this.provisionConfigFormGroup.get('provisionConfiguration').value;
const provisionDeviceKey = deviceProvisionConfiguration.provisionDeviceKey;
delete deviceProvisionConfiguration.provisionDeviceKey;
const deviceProfile: DeviceProfile = {
name: this.deviceProfileDetailsFormGroup.get('name').value,
type: this.deviceProfileDetailsFormGroup.get('type').value,
transportType: this.transportConfigFormGroup.get('transportType').value,
provisionType: deviceProvisionConfiguration.type,
provisionDeviceKey,
description: this.deviceProfileDetailsFormGroup.get('description').value,
profileData: {
configuration: createDeviceProfileConfiguration(DeviceProfileType.DEFAULT),
transportConfiguration: this.transportConfigFormGroup.get('transportConfiguration').value,
alarms: this.alarmRulesFormGroup.get('alarms').value
alarms: this.alarmRulesFormGroup.get('alarms').value,
provisionConfiguration: deviceProvisionConfiguration
}
};
if (this.deviceProfileDetailsFormGroup.get('defaultRuleChainId').value) {
@ -188,6 +209,8 @@ export class AddDeviceProfileDialogComponent extends
return 'device-profile.transport-configuration';
case 2:
return 'device-profile.alarm-rules';
case 3:
return 'device-profile.device-provisioning';
}
}

124
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html

@ -0,0 +1,124 @@
<!--
Copyright © 2016-2020 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.
-->
<form [formGroup]="conditionFormGroup" (ngSubmit)="save()" style="width: 700px;">
<mat-toolbar color="primary">
<h2>{{ (readonly ? 'device-profile.alarm-rule-condition' : 'device-profile.edit-alarm-rule-condition') | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<div fxFlex fxLayout="column">
<tb-key-filter-list
[displayUserParameters]="false"
[allowUserDynamicSource]="false"
[telemetryKeysOnly]="true"
formControlName="keyFilters">
</tb-key-filter-list>
<section formGroupName="spec" class="row">
<mat-form-field class="mat-block" hideRequiredMarker>
<mat-label translate>device-profile.condition-type</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let alarmConditionType of alarmConditionTypes" [value]="alarmConditionType">
{{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="conditionFormGroup.get('spec.type').hasError('required')">
{{ 'device-profile.condition-type-required' | translate }}
</mat-error>
</mat-form-field>
<div fxLayout="row" fxLayoutGap="8px" *ngIf="conditionFormGroup.get('spec.type').value == AlarmConditionType.DURATION">
<mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
<mat-label></mat-label>
<input type="number" required
step="1" min="1" max="2147483647" matInput
placeholder="{{ 'device-profile.condition-duration-value' | translate }}"
formControlName="value">
<mat-error *ngIf="conditionFormGroup.get('spec.value').hasError('required')">
{{ 'device-profile.condition-duration-value-required' | translate }}
</mat-error>
<mat-error *ngIf="conditionFormGroup.get('spec.value').hasError('min')">
{{ 'device-profile.condition-duration-value-range' | translate }}
</mat-error>
<mat-error *ngIf="conditionFormGroup.get('spec.value').hasError('max')">
{{ 'device-profile.condition-duration-value-range' | translate }}
</mat-error>
<mat-error *ngIf="conditionFormGroup.get('spec.value').hasError('pattern')">
{{ 'device-profile.condition-duration-value-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
<mat-label></mat-label>
<mat-select formControlName="unit"
required
placeholder="{{ 'device-profile.condition-duration-time-unit' | translate }}">
<mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">
{{ timeUnitTranslations.get(timeUnit) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="conditionFormGroup.get('spec.unit').hasError('required')">
{{ 'device-profile.condition-duration-time-unit-required' | translate }}
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutGap="8px" *ngIf="conditionFormGroup.get('spec.type').value == AlarmConditionType.REPEATING">
<mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
<mat-label></mat-label>
<input type="number" required
step="1" min="1" max="2147483647" matInput
placeholder="{{ 'device-profile.condition-repeating-value' | translate }}"
formControlName="count">
<mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('required')">
{{ 'device-profile.condition-repeating-value-required' | translate }}
</mat-error>
<mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('min')">
{{ 'device-profile.condition-repeating-value-range' | translate }}
</mat-error>
<mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('max')">
{{ 'device-profile.condition-repeating-value-range' | translate }}
</mat-error>
<mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('pattern')">
{{ 'device-profile.condition-repeating-value-pattern' | translate }}
</mat-error>
</mat-form-field>
</div>
</section>
</div>
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
*ngIf="!readonly"
type="submit"
[disabled]="(isLoading$ | async) || conditionFormGroup.invalid || !conditionFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ (readonly ? 'action.close' : 'action.cancel') | translate }}
</button>
</div>
</form>

20
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.scss

@ -0,0 +1,20 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.row {
margin-top: 1em;
}
}

150
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts

@ -0,0 +1,150 @@
///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { KeyFilter, keyFilterInfosToKeyFilters, keyFiltersToKeyFilterInfos } from '@shared/models/query/query.models';
import { AlarmCondition, AlarmConditionType, AlarmConditionTypeTranslationMap } from '@shared/models/device.models';
import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models';
export interface AlarmRuleConditionDialogData {
readonly: boolean;
condition: AlarmCondition;
}
@Component({
selector: 'tb-alarm-rule-condition-dialog',
templateUrl: './alarm-rule-condition-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: AlarmRuleConditionDialogComponent}],
styleUrls: ['/alarm-rule-condition-dialog.component.scss']
})
export class AlarmRuleConditionDialogComponent extends DialogComponent<AlarmRuleConditionDialogComponent, AlarmCondition>
implements OnInit, ErrorStateMatcher {
timeUnits = Object.keys(TimeUnit);
timeUnitTranslations = timeUnitTranslationMap;
alarmConditionTypes = Object.keys(AlarmConditionType);
AlarmConditionType = AlarmConditionType;
alarmConditionTypeTranslation = AlarmConditionTypeTranslationMap;
readonly = this.data.readonly;
condition = this.data.condition;
conditionFormGroup: FormGroup;
submitted = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleConditionDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<AlarmRuleConditionDialogComponent, AlarmCondition>,
private fb: FormBuilder,
private utils: UtilsService,
public translate: TranslateService) {
super(store, router, dialogRef);
this.conditionFormGroup = this.fb.group({
keyFilters: [keyFiltersToKeyFilterInfos(this.condition?.condition), Validators.required],
spec: this.fb.group({
type: [AlarmConditionType.SIMPLE, Validators.required],
unit: [{value: null, disable: true}, Validators.required],
value: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]],
count: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]]
})
});
this.conditionFormGroup.patchValue({spec: this.condition?.spec});
this.conditionFormGroup.get('spec.type').valueChanges.subscribe((type) => {
this.updateValidators(type, true, true);
});
if (this.readonly) {
this.conditionFormGroup.disable({emitEvent: false});
} else {
this.updateValidators(this.condition?.spec?.type);
}
}
ngOnInit(): void {
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
private updateValidators(type: AlarmConditionType, resetDuration = false, emitEvent = false) {
switch (type) {
case AlarmConditionType.DURATION:
this.conditionFormGroup.get('spec.value').enable();
this.conditionFormGroup.get('spec.unit').enable();
this.conditionFormGroup.get('spec.count').disable();
if (resetDuration) {
this.conditionFormGroup.get('spec').patchValue({
count: null
});
}
break;
case AlarmConditionType.REPEATING:
this.conditionFormGroup.get('spec.count').enable();
this.conditionFormGroup.get('spec.value').disable();
this.conditionFormGroup.get('spec.unit').disable();
if (resetDuration) {
this.conditionFormGroup.get('spec').patchValue({
value: null,
unit: null
});
}
break;
case AlarmConditionType.SIMPLE:
this.conditionFormGroup.get('spec.value').disable();
this.conditionFormGroup.get('spec.unit').disable();
this.conditionFormGroup.get('spec.count').disable();
if (resetDuration) {
this.conditionFormGroup.get('spec').patchValue({
value: null,
unit: null,
count: null
});
}
break;
}
this.conditionFormGroup.get('spec.value').updateValueAndValidity({emitEvent});
this.conditionFormGroup.get('spec.unit').updateValueAndValidity({emitEvent});
this.conditionFormGroup.get('spec.count').updateValueAndValidity({emitEvent});
}
cancel(): void {
this.dialogRef.close(null);
}
save(): void {
this.submitted = true;
this.condition = {
condition: keyFilterInfosToKeyFilters(this.conditionFormGroup.get('keyFilters').value),
spec: this.conditionFormGroup.get('spec').value
};
this.dialogRef.close(this.condition);
}
}

8
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html

@ -15,8 +15,9 @@
limitations under the License.
-->
<div fxLayout="column" fxFlex>
<div fxLayout="column" fxFlex [formGroup]="alarmRuleConditionFormGroup">
<div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">
<label class="tb-title" translate>device-profile.condition</label>
<span fxFlex></span>
<a mat-button color="primary"
type="button"
@ -27,9 +28,12 @@
</a>
</div>
<div class="tb-alarm-rule-condition" fxFlex fxLayout="column" fxLayoutAlign="center" (click)="openFilterDialog($event)">
<tb-filter-text [formControl]="alarmRuleConditionControl"
<tb-filter-text formControlName="condition"
required
addFilterPrompt="{{'device-profile.enter-alarm-rule-condition-prompt' | translate}}">
</tb-filter-text>
<span class="tb-alarm-rule-condition-spec" [ngClass]="{disabled: this.disabled}" [innerHTML]="specText">
</span>
</div>
</div>

11
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.scss

@ -21,10 +21,15 @@
}
}
.tb-alarm-rule-condition {
padding: 8px;
border: 1px groove rgba(0, 0, 0, .25);
border-radius: 4px;
cursor: pointer;
.tb-alarm-rule-condition-spec {
margin-top: 1em;
line-height: 1.8em;
padding: 4px;
&.disabled {
opacity: 0.7;
}
}
}
}

85
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts

@ -18,20 +18,21 @@ import { Component, forwardRef, Input, OnInit } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormControl,
FormControl, FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
Validator
Validator, Validators
} from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { KeyFilter } from '@shared/models/query/query.models';
import { deepClone } from '@core/utils';
import {
AlarmRuleKeyFiltersDialogComponent,
AlarmRuleKeyFiltersDialogData
} from './alarm-rule-key-filters-dialog.component';
import { deepClone, isUndefined } from '@core/utils';
import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common';
import { AlarmCondition, AlarmConditionSpec, AlarmConditionType } from '@shared/models/device.models';
import {
AlarmRuleConditionDialogComponent,
AlarmRuleConditionDialogData
} from '@home/components/profile/alarm/alarm-rule-condition-dialog.component';
import { TimeUnit } from '@shared/models/time/time.models';
@Component({
selector: 'tb-alarm-rule-condition',
@ -55,9 +56,11 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
@Input()
disabled: boolean;
alarmRuleConditionControl: FormControl;
alarmRuleConditionFormGroup: FormGroup;
specText = '';
private modelValue: Array<KeyFilter>;
private modelValue: AlarmCondition;
private propagateChange = (v: any) => { };
@ -75,25 +78,31 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
}
ngOnInit() {
this.alarmRuleConditionControl = this.fb.control(null);
this.alarmRuleConditionFormGroup = this.fb.group({
condition: [null, Validators.required],
spec: [null, Validators.required]
});
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.alarmRuleConditionControl.disable({emitEvent: false});
this.alarmRuleConditionFormGroup.disable({emitEvent: false});
} else {
this.alarmRuleConditionControl.enable({emitEvent: false});
this.alarmRuleConditionFormGroup.enable({emitEvent: false});
}
}
writeValue(value: Array<KeyFilter>): void {
writeValue(value: AlarmCondition): void {
this.modelValue = value;
if (this.modelValue !== null && isUndefined(this.modelValue?.spec)) {
this.modelValue = Object.assign(this.modelValue, {spec: {type: AlarmConditionType.SIMPLE}});
}
this.updateConditionInfo();
}
public conditionSet() {
return this.modelValue && this.modelValue.length;
return this.modelValue && this.modelValue.condition.length;
}
public validate(c: FormControl) {
@ -108,13 +117,13 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
if ($event) {
$event.stopPropagation();
}
this.dialog.open<AlarmRuleKeyFiltersDialogComponent, AlarmRuleKeyFiltersDialogData,
Array<KeyFilter>>(AlarmRuleKeyFiltersDialogComponent, {
this.dialog.open<AlarmRuleConditionDialogComponent, AlarmRuleConditionDialogData,
AlarmCondition>(AlarmRuleConditionDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
readonly: this.disabled,
keyFilters: this.disabled ? this.modelValue : deepClone(this.modelValue)
condition: this.disabled ? this.modelValue : deepClone(this.modelValue)
}
}).afterClosed().subscribe((result) => {
if (result) {
@ -125,7 +134,45 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
}
private updateConditionInfo() {
this.alarmRuleConditionControl.patchValue(this.modelValue);
this.alarmRuleConditionFormGroup.patchValue(
{
condition: this.modelValue?.condition,
spec: this.modelValue?.spec
}
);
this.updateSpecText();
}
private updateSpecText() {
this.specText = '';
if (this.modelValue && this.modelValue.spec) {
const spec = this.modelValue.spec;
switch (spec.type) {
case AlarmConditionType.SIMPLE:
break;
case AlarmConditionType.DURATION:
let duringText = '';
switch (spec.unit) {
case TimeUnit.SECONDS:
duringText = this.translate.instant('timewindow.seconds', {seconds: spec.value});
break;
case TimeUnit.MINUTES:
duringText = this.translate.instant('timewindow.minutes', {minutes: spec.value});
break;
case TimeUnit.HOURS:
duringText = this.translate.instant('timewindow.hours', {hours: spec.value});
break;
case TimeUnit.DAYS:
duringText = this.translate.instant('timewindow.days', {days: spec.value});
break;
}
this.specText = this.translate.instant('device-profile.condition-during', {during: duringText});
break;
case AlarmConditionType.REPEATING:
this.specText = this.translate.instant('device-profile.condition-repeat-times', {count: spec.count});
break;
}
}
}
private updateModel() {

118
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html

@ -16,90 +16,36 @@
-->
<div fxLayout="column" [formGroup]="alarmRuleFormGroup">
<mat-tab-group>
<mat-tab label="{{ 'device-profile.condition' | translate }}" formGroupName="condition">
<tb-alarm-rule-condition fxFlex class="row"
formControlName="condition">
</tb-alarm-rule-condition>
<section formGroupName="spec" class="row">
<mat-form-field class="mat-block" hideRequiredMarker>
<mat-label translate>device-profile.condition-type</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let alarmConditionType of alarmConditionTypes" [value]="alarmConditionType">
{{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.type').hasError('required')">
{{ 'device-profile.condition-type-required' | translate }}
</mat-error>
</mat-form-field>
<div fxLayout="row" fxLayoutGap="8px" *ngIf="alarmRuleFormGroup.get('condition.spec.type').value == AlarmConditionType.DURATION">
<mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
<mat-label></mat-label>
<input type="number" required
step="1" min="1" max="2147483647" matInput
placeholder="{{ 'device-profile.condition-duration-value' | translate }}"
formControlName="value">
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.value').hasError('required')">
{{ 'device-profile.condition-duration-value-required' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.value').hasError('min')">
{{ 'device-profile.condition-duration-value-range' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.value').hasError('max')">
{{ 'device-profile.condition-duration-value-range' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.value').hasError('pattern')">
{{ 'device-profile.condition-duration-value-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
<mat-label></mat-label>
<mat-select formControlName="unit"
required
placeholder="{{ 'device-profile.condition-duration-time-unit' | translate }}">
<mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">
{{ timeUnitTranslations.get(timeUnit) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.unit').hasError('required')">
{{ 'device-profile.condition-duration-time-unit-required' | translate }}
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutGap="8px" *ngIf="alarmRuleFormGroup.get('condition.spec.type').value == AlarmConditionType.REPEATING">
<mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
<mat-label></mat-label>
<input type="number" required
step="1" min="1" max="2147483647" matInput
placeholder="{{ 'device-profile.condition-repeating-value' | translate }}"
formControlName="count">
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.count').hasError('required')">
{{ 'device-profile.condition-repeating-value-required' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.count').hasError('min')">
{{ 'device-profile.condition-repeating-value-range' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.count').hasError('max')">
{{ 'device-profile.condition-repeating-value-range' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.count').hasError('pattern')">
{{ 'device-profile.condition-repeating-value-pattern' | translate }}
</mat-error>
</mat-form-field>
</div>
</section>
</mat-tab>
<mat-tab label="{{ 'device-profile.schedule' | translate }}">
<tb-alarm-schedule fxFlex class="row"
formControlName="schedule">
</tb-alarm-schedule>
</mat-tab>
<mat-tab label="{{ 'device-profile.alarm-rule-details' | translate }}">
<mat-form-field class="mat-block row">
<mat-label translate>device-profile.alarm-details</mat-label>
<textarea matInput formControlName="alarmDetails" rows="5"></textarea>
</mat-form-field>
</mat-tab>
</mat-tab-group>
<tb-alarm-rule-condition fxFlex class="row"
formControlName="condition">
</tb-alarm-rule-condition>
<mat-divider class="row"></mat-divider>
<tb-alarm-schedule-info fxFlex class="row"
formControlName="schedule">
</tb-alarm-schedule-info>
<mat-divider class="row"></mat-divider>
<div fxLayout="column" fxFlex class="tb-alarm-rule-details row">
<div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">
<label class="tb-title" translate>device-profile.alarm-rule-details</label>
<span fxFlex></span>
<a mat-button color="primary"
*ngIf="!disabled"
type="button"
(click)="openEditDetailsDialog($event)"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above">
{{ 'action.edit' | translate }}
</a>
</div>
<div fxLayout="row" fxLayoutAlign="start start">
<div class="tb-alarm-rule-details-content" [ngClass]="{disabled: this.disabled, collapsed: !this.expandAlarmDetails}"
(click)="!disabled ? openEditDetailsDialog($event) : {}"
fxFlex [innerHTML]="alarmRuleFormGroup.get('alarmDetails').value"></div>
<a mat-button color="primary"
type="button"
(click)="expandAlarmDetails = !expandAlarmDetails">
{{ (expandAlarmDetails ? 'action.hide' : 'action.read-more') | translate }}
</a>
</div>
</div>
</div>

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save