Browse Source

Merge remote-tracking branch 'origin/develop/3.5' into feature/work-remove-deprecated-handleException-usage

# Conflicts:
#	application/src/main/java/org/thingsboard/server/controller/AlarmController.java
#	application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java
pull/7527/head
AndriiD 3 years ago
parent
commit
bf1be7bb07
  1. 20
      application/pom.xml
  2. 4
      application/src/main/data/json/demo/dashboards/gateways.json
  3. 10
      application/src/main/data/json/demo/dashboards/thermostats.json
  4. 2
      application/src/main/data/json/system/widget_bundles/alarm_widgets.json
  5. 10
      application/src/main/data/json/system/widget_bundles/control_widgets.json
  6. 4
      application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json
  7. 32
      application/src/main/data/json/system/widget_bundles/input_widgets.json
  8. 317
      application/src/main/data/upgrade/3.4.4/schema_update.sql
  9. 2
      application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
  10. 17
      application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java
  11. 85
      application/src/main/java/org/thingsboard/server/controller/AlarmController.java
  12. 19
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  13. 3
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  14. 2
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  15. 7
      application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java
  16. 16
      application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
  17. 6
      application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java
  18. 12
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java
  19. 2
      application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java
  20. 4
      application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java
  21. 157
      application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java
  22. 15
      application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java
  23. 2
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
  24. 7
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  25. 3
      application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java
  26. 47
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java
  27. 6
      application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java
  28. 27
      application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java
  29. 10
      application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java
  30. 125
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java
  31. 101
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java
  32. 23
      application/src/main/java/org/thingsboard/server/service/telemetry/sub/AlarmSubscriptionUpdate.java
  33. 3
      application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java
  34. 1
      application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java
  35. 85
      application/src/main/java/org/thingsboard/server/utils/EventDeduplicationExecutor.java
  36. 4
      application/src/main/resources/thingsboard.yml
  37. 2
      application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
  38. 4
      application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java
  39. 110
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  40. 22
      application/src/test/java/org/thingsboard/server/controller/BaseAdminControllerTest.java
  41. 2
      application/src/test/java/org/thingsboard/server/controller/BaseAlarmCommentControllerTest.java
  42. 210
      application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java
  43. 17
      application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java
  44. 12
      application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java
  45. 2
      application/src/test/java/org/thingsboard/server/controller/BaseWidgetsBundleControllerTest.java
  46. 21
      application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java
  47. 1
      application/src/test/java/org/thingsboard/server/edge/BaseAlarmEdgeTest.java
  48. 2
      application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java
  49. 30
      application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java
  50. 58
      application/src/test/java/org/thingsboard/server/service/mail/TestMailService.java
  51. 19
      application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java
  52. 1
      application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java
  53. 2
      application/src/test/java/org/thingsboard/server/transport/AbstractTransportIntegrationTest.java
  54. 20
      application/src/test/java/org/thingsboard/server/transport/TransportNoSqlTestSuite.java
  55. 2
      application/src/test/java/org/thingsboard/server/transport/coap/AbstractCoapIntegrationTest.java
  56. 30
      application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java
  57. 4
      application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestConfigProperties.java
  58. 2
      application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/MqttTestClient.java
  59. 83
      application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java
  60. 7
      application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/credentials/BasicMqttCredentialsTest.java
  61. 15
      application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/AbstractMqttServerSideRpcIntegrationTest.java
  62. 4
      application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/MqttV5TestClient.java
  63. 462
      application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java
  64. 432
      application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/AbstractMqttV5ClientSparkplugAttributesTest.java
  65. 55
      application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesInProfileTest.java
  66. 81
      application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesTest.java
  67. 178
      application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/AbstractMqttV5ClientSparkplugConnectionTest.java
  68. 82
      application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/MqttV5ClientSparkplugBConnectionTest.java
  69. 108
      application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/AbstractMqttV5RpcSparkplugTest.java
  70. 61
      application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/MqttV5RpcSparkplugTest.java
  71. 113
      application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/AbstractMqttV5ClientSparkplugTelemetryTest.java
  72. 56
      application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/MqttV5ClientSparkplugBTelemetryTest.java
  73. 173
      application/src/test/java/org/thingsboard/server/util/EventDeduplicationExecutorTest.java
  74. 4
      application/src/test/resources/application-test.properties
  75. 4
      application/src/test/resources/logback-test.xml
  76. 4
      common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java
  77. 1
      common/cluster-api/src/main/proto/queue.proto
  78. 85
      common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmApiCallResult.java
  79. 21
      common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmOperationResult.java
  80. 60
      common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
  81. 6
      common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java
  82. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java
  83. 2
      common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
  84. 2
      common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java
  85. 2
      common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
  86. 2
      common/data/src/main/java/org/thingsboard/server/common/data/Device.java
  87. 22
      common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java
  88. 11
      common/data/src/main/java/org/thingsboard/server/common/data/HasEmail.java
  89. 12
      common/data/src/main/java/org/thingsboard/server/common/data/HasLabel.java
  90. 10
      common/data/src/main/java/org/thingsboard/server/common/data/HasTitle.java
  91. 2
      common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java
  92. 12
      common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java
  93. 2
      common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java
  94. 55
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
  95. 42
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmAssignee.java
  96. 18
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmAssigneeUpdate.java
  97. 87
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCreateOrUpdateActiveRequest.java
  98. 56
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java
  99. 34
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmModificationRequest.java
  100. 46
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmPropagationInfo.java

20
application/pom.xml

@ -144,21 +144,6 @@
<groupId>org.eclipse.paho</groupId> <groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.mqttv5.client</artifactId> <artifactId>org.eclipse.paho.mqttv5.client</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.cassandraunit</groupId>
<artifactId>cassandra-unit</artifactId>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
<exclusion>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</exclusion>
</exclusions>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<artifactId>ui-ngx</artifactId> <artifactId>ui-ngx</artifactId>
@ -329,6 +314,11 @@
<artifactId>spring-test-dbunit</artifactId> <artifactId>spring-test-dbunit</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>cassandra</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.testcontainers</groupId> <groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId> <artifactId>postgresql</artifactId>

4
application/src/main/data/json/demo/dashboards/gateways.json

@ -131,7 +131,7 @@
"name": "Add device", "name": "Add device",
"icon": "add", "icon": "add",
"type": "customPretty", "type": "customPretty",
"customHtml": "<form #addDeviceForm=\"ngForm\" [formGroup]=\"addDeviceFormGroup\"\n (ngSubmit)=\"save()\" style=\"width: 480px;\">\n <mat-toolbar fxLayout=\"row\" color=\"primary\">\n <h2>Add device</h2>\n <span fxFlex></span>\n <button mat-button mat-icon-button\n (click)=\"cancel()\"\n type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div mat-dialog-content>\n <div class=\"mat-padding\" fxLayout=\"column\">\n <mat-form-field class=\"mat-block\">\n <mat-label>Device name</mat-label>\n <input matInput formControlName=\"deviceName\" required>\n <mat-error *ngIf=\"addDeviceFormGroup.get('deviceName').hasError('required')\">\n Device name is required.\n </mat-error>\n </mat-form-field>\n <div formGroupName=\"attributes\" fxFlex fxLayout=\"row\" fxLayoutGap=\"8px\">\n <mat-form-field fxFlex=\"50\" class=\"mat-block\">\n <mat-label>Latitude</mat-label>\n <input type=\"number\" step=\"any\" matInput formControlName=\"latitude\">\n </mat-form-field>\n <mat-form-field fxFlex=\"50\" class=\"mat-block\">\n <mat-label>Longitude</mat-label>\n <input type=\"number\" step=\"any\" matInput formControlName=\"longitude\">\n </mat-form-field>\n </div>\n <mat-form-field class=\"mat-block\">\n <mat-label>Label</mat-label>\n <input matInput formControlName=\"deviceLabel\">\n </mat-form-field>\n </div> \n </div>\n <div mat-dialog-actions fxLayout=\"row\">\n <span fxFlex></span>\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || addDeviceForm.invalid || !addDeviceForm.dirty\">\n Create\n </button>\n <button mat-button color=\"primary\"\n style=\"margin-right: 20px;\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n </div>\n</form>\n", "customHtml": "<form #addDeviceForm=\"ngForm\" [formGroup]=\"addDeviceFormGroup\"\n (ngSubmit)=\"save()\" style=\"width: 480px;\">\n <mat-toolbar fxLayout=\"row\" color=\"primary\">\n <h2>Add device</h2>\n <span fxFlex></span>\n <button mat-icon-button\n (click)=\"cancel()\"\n type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div mat-dialog-content>\n <div class=\"mat-padding\" fxLayout=\"column\">\n <mat-form-field class=\"mat-block\">\n <mat-label>Device name</mat-label>\n <input matInput formControlName=\"deviceName\" required>\n <mat-error *ngIf=\"addDeviceFormGroup.get('deviceName').hasError('required')\">\n Device name is required.\n </mat-error>\n </mat-form-field>\n <div formGroupName=\"attributes\" fxFlex fxLayout=\"row\" fxLayoutGap=\"8px\">\n <mat-form-field fxFlex=\"50\" class=\"mat-block\">\n <mat-label>Latitude</mat-label>\n <input type=\"number\" step=\"any\" matInput formControlName=\"latitude\">\n </mat-form-field>\n <mat-form-field fxFlex=\"50\" class=\"mat-block\">\n <mat-label>Longitude</mat-label>\n <input type=\"number\" step=\"any\" matInput formControlName=\"longitude\">\n </mat-form-field>\n </div>\n <mat-form-field class=\"mat-block\">\n <mat-label>Label</mat-label>\n <input matInput formControlName=\"deviceLabel\">\n </mat-form-field>\n </div> \n </div>\n <div mat-dialog-actions fxLayout=\"row\">\n <span fxFlex></span>\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || addDeviceForm.invalid || !addDeviceForm.dirty\">\n Create\n </button>\n <button mat-button color=\"primary\"\n style=\"margin-right: 20px;\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n </div>\n</form>\n",
"customCss": "", "customCss": "",
"customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\n\nopenAddDeviceDialog();\n\nfunction openAddDeviceDialog() {\n customDialog.customDialog(htmlTemplate, AddDeviceDialogController).subscribe();\n}\n\nfunction AddDeviceDialogController(instance) {\n let vm = instance;\n \n vm.addDeviceFormGroup = vm.fb.group({\n deviceName: ['', [vm.validators.required]],\n deviceLabel: [''],\n attributes: vm.fb.group({\n latitude: [null],\n longitude: [null]\n }) \n });\n \n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n \n vm.save = function() {\n vm.addDeviceFormGroup.markAsPristine();\n let device = {\n additionalInfo: {gateway: true},\n name: vm.addDeviceFormGroup.get('deviceName').value,\n type: 'gateway',\n label: vm.addDeviceFormGroup.get('deviceLabel').value\n };\n deviceService.saveDevice(device).subscribe(\n function (device) {\n saveAttributes(device.id).subscribe(\n function () {\n widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n }\n );\n };\n \n function saveAttributes(entityId) {\n let attributes = vm.addDeviceFormGroup.get('attributes').value;\n let attributesArray = [];\n for (let key in attributes) {\n attributesArray.push({key: key, value: attributes[key]});\n }\n if (attributesArray.length > 0) {\n return attributeService.saveEntityAttributes(entityId, \"SERVER_SCOPE\", attributesArray);\n } else {\n return widgetContext.rxjs.of([]);\n }\n }\n}\n", "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\n\nopenAddDeviceDialog();\n\nfunction openAddDeviceDialog() {\n customDialog.customDialog(htmlTemplate, AddDeviceDialogController).subscribe();\n}\n\nfunction AddDeviceDialogController(instance) {\n let vm = instance;\n \n vm.addDeviceFormGroup = vm.fb.group({\n deviceName: ['', [vm.validators.required]],\n deviceLabel: [''],\n attributes: vm.fb.group({\n latitude: [null],\n longitude: [null]\n }) \n });\n \n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n \n vm.save = function() {\n vm.addDeviceFormGroup.markAsPristine();\n let device = {\n additionalInfo: {gateway: true},\n name: vm.addDeviceFormGroup.get('deviceName').value,\n type: 'gateway',\n label: vm.addDeviceFormGroup.get('deviceLabel').value\n };\n deviceService.saveDevice(device).subscribe(\n function (device) {\n saveAttributes(device.id).subscribe(\n function () {\n widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n }\n );\n };\n \n function saveAttributes(entityId) {\n let attributes = vm.addDeviceFormGroup.get('attributes').value;\n let attributesArray = [];\n for (let key in attributes) {\n attributesArray.push({key: key, value: attributes[key]});\n }\n if (attributesArray.length > 0) {\n return attributeService.saveEntityAttributes(entityId, \"SERVER_SCOPE\", attributesArray);\n } else {\n return widgetContext.rxjs.of([]);\n }\n }\n}\n",
"customResources": [], "customResources": [],
@ -160,7 +160,7 @@
"name": "Edit device", "name": "Edit device",
"icon": "edit", "icon": "edit",
"type": "customPretty", "type": "customPretty",
"customHtml": "<form #editDeviceForm=\"ngForm\" [formGroup]=\"editDeviceFormGroup\"\n (ngSubmit)=\"save()\" style=\"width: 480px;\">\n <mat-toolbar fxLayout=\"row\" color=\"primary\">\n <h2>Edit device</h2>\n <span fxFlex></span>\n <button mat-button mat-icon-button\n (click)=\"cancel()\"\n type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div mat-dialog-content>\n <div class=\"mat-padding\" fxLayout=\"column\">\n <mat-form-field class=\"mat-block\">\n <mat-label>Device name</mat-label>\n <input matInput formControlName=\"deviceName\" required>\n <mat-error *ngIf=\"editDeviceFormGroup.get('deviceName').hasError('required')\">\n Device name is required.\n </mat-error>\n </mat-form-field>\n <div formGroupName=\"attributes\" fxFlex fxLayout=\"row\" fxLayoutGap=\"8px\">\n <mat-form-field fxFlex=\"50\" class=\"mat-block\">\n <mat-label>Latitude</mat-label>\n <input type=\"number\" step=\"any\" matInput formControlName=\"latitude\">\n </mat-form-field>\n <mat-form-field fxFlex=\"50\" class=\"mat-block\">\n <mat-label>Longitude</mat-label>\n <input type=\"number\" step=\"any\" matInput formControlName=\"longitude\">\n </mat-form-field>\n </div>\n <mat-form-field class=\"mat-block\">\n <mat-label>Label</mat-label>\n <input matInput formControlName=\"deviceLabel\">\n </mat-form-field>\n </div> \n </div>\n <div mat-dialog-actions fxLayout=\"row\">\n <span fxFlex></span>\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || editDeviceForm.invalid || !editDeviceForm.dirty\">\n Update\n </button>\n <button mat-button color=\"primary\"\n style=\"margin-right: 20px;\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n </div>\n</form>\n", "customHtml": "<form #editDeviceForm=\"ngForm\" [formGroup]=\"editDeviceFormGroup\"\n (ngSubmit)=\"save()\" style=\"width: 480px;\">\n <mat-toolbar fxLayout=\"row\" color=\"primary\">\n <h2>Edit device</h2>\n <span fxFlex></span>\n <button mat-icon-button\n (click)=\"cancel()\"\n type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div mat-dialog-content>\n <div class=\"mat-padding\" fxLayout=\"column\">\n <mat-form-field class=\"mat-block\">\n <mat-label>Device name</mat-label>\n <input matInput formControlName=\"deviceName\" required>\n <mat-error *ngIf=\"editDeviceFormGroup.get('deviceName').hasError('required')\">\n Device name is required.\n </mat-error>\n </mat-form-field>\n <div formGroupName=\"attributes\" fxFlex fxLayout=\"row\" fxLayoutGap=\"8px\">\n <mat-form-field fxFlex=\"50\" class=\"mat-block\">\n <mat-label>Latitude</mat-label>\n <input type=\"number\" step=\"any\" matInput formControlName=\"latitude\">\n </mat-form-field>\n <mat-form-field fxFlex=\"50\" class=\"mat-block\">\n <mat-label>Longitude</mat-label>\n <input type=\"number\" step=\"any\" matInput formControlName=\"longitude\">\n </mat-form-field>\n </div>\n <mat-form-field class=\"mat-block\">\n <mat-label>Label</mat-label>\n <input matInput formControlName=\"deviceLabel\">\n </mat-form-field>\n </div> \n </div>\n <div mat-dialog-actions fxLayout=\"row\">\n <span fxFlex></span>\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || editDeviceForm.invalid || !editDeviceForm.dirty\">\n Update\n </button>\n <button mat-button color=\"primary\"\n style=\"margin-right: 20px;\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n </div>\n</form>\n",
"customCss": "/*=======================================================================*/\n/*========== There are two examples: for edit and add entity ==========*/\n/*=======================================================================*/\n/*======================== Edit entity example ========================*/\n/*=======================================================================*/\n/*\n.edit-entity-form md-input-container {\n padding-right: 10px;\n}\n\n.edit-entity-form .boolean-value-input {\n padding-left: 5px;\n}\n\n.edit-entity-form .boolean-value-input .checkbox-label {\n margin-bottom: 8px;\n color: rgba(0,0,0,0.54);\n font-size: 12px;\n}\n\n.relations-list .header {\n padding-right: 5px;\n padding-bottom: 5px;\n padding-left: 5px;\n}\n\n.relations-list .header .cell {\n padding-right: 5px;\n padding-left: 5px;\n font-size: 12px;\n font-weight: 700;\n color: rgba(0, 0, 0, .54);\n white-space: nowrap;\n}\n\n.relations-list .body {\n padding-right: 5px;\n padding-bottom: 15px;\n padding-left: 5px;\n}\n\n.relations-list .body .row {\n padding-top: 5px;\n}\n\n.relations-list .body .cell {\n padding-right: 5px;\n padding-left: 5px;\n}\n\n.relations-list .body md-autocomplete-wrap md-input-container {\n height: 30px;\n}\n\n.relations-list .body .md-button {\n margin: 0;\n}\n\n.relations-list.old-relations tb-entity-select tb-entity-autocomplete button {\n display: none;\n} \n*/\n/*========================================================================*/\n/*========================= Add entity example =========================*/\n/*========================================================================*/\n/*\n.add-entity-form md-input-container {\n padding-right: 10px;\n}\n\n.add-entity-form .boolean-value-input {\n padding-left: 5px;\n}\n\n.add-entity-form .boolean-value-input .checkbox-label {\n margin-bottom: 8px;\n color: rgba(0,0,0,0.54);\n font-size: 12px;\n}\n\n.relations-list .header {\n padding-right: 5px;\n padding-bottom: 5px;\n padding-left: 5px;\n}\n\n.relations-list .header .cell {\n padding-right: 5px;\n padding-left: 5px;\n font-size: 12px;\n font-weight: 700;\n color: rgba(0, 0, 0, .54);\n white-space: nowrap;\n}\n\n.relations-list .body {\n padding-right: 5px;\n padding-bottom: 15px;\n padding-left: 5px;\n}\n\n.relations-list .body .row {\n padding-top: 5px;\n}\n\n.relations-list .body .cell {\n padding-right: 5px;\n padding-left: 5px;\n}\n\n.relations-list .body md-autocomplete-wrap md-input-container {\n height: 30px;\n}\n\n.relations-list .body .md-button {\n margin: 0;\n}\n*/\n", "customCss": "/*=======================================================================*/\n/*========== There are two examples: for edit and add entity ==========*/\n/*=======================================================================*/\n/*======================== Edit entity example ========================*/\n/*=======================================================================*/\n/*\n.edit-entity-form md-input-container {\n padding-right: 10px;\n}\n\n.edit-entity-form .boolean-value-input {\n padding-left: 5px;\n}\n\n.edit-entity-form .boolean-value-input .checkbox-label {\n margin-bottom: 8px;\n color: rgba(0,0,0,0.54);\n font-size: 12px;\n}\n\n.relations-list .header {\n padding-right: 5px;\n padding-bottom: 5px;\n padding-left: 5px;\n}\n\n.relations-list .header .cell {\n padding-right: 5px;\n padding-left: 5px;\n font-size: 12px;\n font-weight: 700;\n color: rgba(0, 0, 0, .54);\n white-space: nowrap;\n}\n\n.relations-list .body {\n padding-right: 5px;\n padding-bottom: 15px;\n padding-left: 5px;\n}\n\n.relations-list .body .row {\n padding-top: 5px;\n}\n\n.relations-list .body .cell {\n padding-right: 5px;\n padding-left: 5px;\n}\n\n.relations-list .body md-autocomplete-wrap md-input-container {\n height: 30px;\n}\n\n.relations-list .body .md-button {\n margin: 0;\n}\n\n.relations-list.old-relations tb-entity-select tb-entity-autocomplete button {\n display: none;\n} \n*/\n/*========================================================================*/\n/*========================= Add entity example =========================*/\n/*========================================================================*/\n/*\n.add-entity-form md-input-container {\n padding-right: 10px;\n}\n\n.add-entity-form .boolean-value-input {\n padding-left: 5px;\n}\n\n.add-entity-form .boolean-value-input .checkbox-label {\n margin-bottom: 8px;\n color: rgba(0,0,0,0.54);\n font-size: 12px;\n}\n\n.relations-list .header {\n padding-right: 5px;\n padding-bottom: 5px;\n padding-left: 5px;\n}\n\n.relations-list .header .cell {\n padding-right: 5px;\n padding-left: 5px;\n font-size: 12px;\n font-weight: 700;\n color: rgba(0, 0, 0, .54);\n white-space: nowrap;\n}\n\n.relations-list .body {\n padding-right: 5px;\n padding-bottom: 15px;\n padding-left: 5px;\n}\n\n.relations-list .body .row {\n padding-top: 5px;\n}\n\n.relations-list .body .cell {\n padding-right: 5px;\n padding-left: 5px;\n}\n\n.relations-list .body md-autocomplete-wrap md-input-container {\n height: 30px;\n}\n\n.relations-list .body .md-button {\n margin: 0;\n}\n*/\n",
"customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\n\nopenEditDeviceDialog();\n\nfunction openEditDeviceDialog() {\n customDialog.customDialog(htmlTemplate, EditDeviceDialogController).subscribe();\n}\n\nfunction EditDeviceDialogController(instance) {\n let vm = instance;\n \n vm.device = null;\n vm.attributes = {};\n \n vm.editDeviceFormGroup = vm.fb.group({\n deviceName: ['', [vm.validators.required]],\n deviceLabel: [''],\n attributes: vm.fb.group({\n latitude: [null],\n longitude: [null]\n }) \n });\n \n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n \n vm.save = function() {\n vm.editDeviceFormGroup.markAsPristine();\n vm.device.name = vm.editDeviceFormGroup.get('deviceName').value;\n vm.device.label = vm.editDeviceFormGroup.get('deviceLabel').value;\n deviceService.saveDevice(vm.device).subscribe(\n function () {\n saveAttributes().subscribe(\n function () {\n widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n }\n );\n };\n \n getEntityInfo();\n \n function getEntityInfo() {\n deviceService.getDevice(entityId.id).subscribe(\n function (device) {\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE',\n ['latitude', 'longitude']).subscribe(\n function (attributes) {\n for (let i = 0; i < attributes.length; i++) {\n vm.attributes[attributes[i].key] = attributes[i].value; \n }\n vm.device = device;\n vm.editDeviceFormGroup.patchValue(\n {\n deviceName: vm.device.name,\n deviceLabel: vm.device.label,\n attributes: {\n latitude: vm.attributes.latitude,\n longitude: vm.attributes.longitude\n }\n }, {emitEvent: false}\n );\n } \n );\n }\n ); \n }\n \n function saveAttributes() {\n let attributes = vm.editDeviceFormGroup.get('attributes').value;\n let attributesArray = [];\n for (let key in attributes) {\n attributesArray.push({key: key, value: attributes[key]});\n }\n if (attributesArray.length > 0) {\n return attributeService.saveEntityAttributes(entityId, 'SERVER_SCOPE', attributesArray);\n } else {\n return widgetContext.rxjs.of([]);\n }\n }\n}\n", "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\n\nopenEditDeviceDialog();\n\nfunction openEditDeviceDialog() {\n customDialog.customDialog(htmlTemplate, EditDeviceDialogController).subscribe();\n}\n\nfunction EditDeviceDialogController(instance) {\n let vm = instance;\n \n vm.device = null;\n vm.attributes = {};\n \n vm.editDeviceFormGroup = vm.fb.group({\n deviceName: ['', [vm.validators.required]],\n deviceLabel: [''],\n attributes: vm.fb.group({\n latitude: [null],\n longitude: [null]\n }) \n });\n \n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n \n vm.save = function() {\n vm.editDeviceFormGroup.markAsPristine();\n vm.device.name = vm.editDeviceFormGroup.get('deviceName').value;\n vm.device.label = vm.editDeviceFormGroup.get('deviceLabel').value;\n deviceService.saveDevice(vm.device).subscribe(\n function () {\n saveAttributes().subscribe(\n function () {\n widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n }\n );\n };\n \n getEntityInfo();\n \n function getEntityInfo() {\n deviceService.getDevice(entityId.id).subscribe(\n function (device) {\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE',\n ['latitude', 'longitude']).subscribe(\n function (attributes) {\n for (let i = 0; i < attributes.length; i++) {\n vm.attributes[attributes[i].key] = attributes[i].value; \n }\n vm.device = device;\n vm.editDeviceFormGroup.patchValue(\n {\n deviceName: vm.device.name,\n deviceLabel: vm.device.label,\n attributes: {\n latitude: vm.attributes.latitude,\n longitude: vm.attributes.longitude\n }\n }, {emitEvent: false}\n );\n } \n );\n }\n ); \n }\n \n function saveAttributes() {\n let attributes = vm.editDeviceFormGroup.get('attributes').value;\n let attributesArray = [];\n for (let key in attributes) {\n attributesArray.push({key: key, value: attributes[key]});\n }\n if (attributesArray.length > 0) {\n return attributeService.saveEntityAttributes(entityId, 'SERVER_SCOPE', attributesArray);\n } else {\n return widgetContext.rxjs.of([]);\n }\n }\n}\n",
"customResources": [], "customResources": [],

10
application/src/main/data/json/demo/dashboards/thermostats.json

@ -215,6 +215,8 @@
"displayDetails": true, "displayDetails": true,
"allowAcknowledgment": true, "allowAcknowledgment": true,
"allowClear": true, "allowClear": true,
"allowAssign": true,
"displayComments": true,
"displayPagination": true, "displayPagination": true,
"defaultPageSize": 10, "defaultPageSize": 10,
"defaultSortOrder": "-createdTime", "defaultSortOrder": "-createdTime",
@ -277,6 +279,14 @@
"color": "#607d8b", "color": "#607d8b",
"settings": {}, "settings": {},
"_hash": 0.7977920750136249 "_hash": 0.7977920750136249
},
{
"name": "assignee",
"type": "alarm",
"label": "Assignee",
"color": "#9c27b0",
"settings": {},
"_hash": 0.8678751039018493
} }
] ]
}, },

2
application/src/main/data/json/system/widget_bundles/alarm_widgets.json

@ -23,7 +23,7 @@
"dataKeySettingsSchema": "", "dataKeySettingsSchema": "",
"settingsDirective": "tb-alarms-table-widget-settings", "settingsDirective": "tb-alarms-table-widget-settings",
"dataKeySettingsDirective": "tb-alarms-table-key-settings", "dataKeySettingsDirective": "tb-alarms-table-key-settings",
"defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStickyAction\":false,\"enableFilter\":true},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{},\"alarmStatusList\":[],\"alarmSeverityList\":[],\"alarmTypeList\":[],\"searchPropagatedAlarms\":false}" "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"allowAssign\":true,\"displayComments\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStickyAction\":false,\"enableFilter\":true},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418},{\"name\":\"assignee\",\"type\":\"alarm\",\"label\":\"Assignee\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.5008441077416634}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{},\"alarmStatusList\":[],\"alarmSeverityList\":[],\"alarmTypeList\":[],\"searchPropagatedAlarms\":false}"
} }
} }
] ]

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

@ -130,8 +130,8 @@
"sizeX": 4, "sizeX": 4,
"sizeY": 2, "sizeY": 2,
"resources": [], "resources": [],
"templateHtml": "<div class=\"tb-rpc-button\" fxLayout=\"column\">\n <div fxFlex=\"20\" class=\"title-container\" fxLayout=\"row\"\n fxLayoutAlign=\"center center\" [fxShow]=\"showTitle\">\n <span class=\"button-title\">{{title}}</span>\n </div>\n <div fxFlex=\"{{showTitle ? 80 : 100}}\" [ngStyle]=\"{paddingTop: showTitle ? '5px': '10px'}\"\n class=\"button-container\" fxLayout=\"column\" fxLayoutAlign=\"center center\">\n <div>\n <button mat-button (click)=\"sendCommand()\"\n [class.mat-raised-button]=\"styleButton?.isRaised\"\n [color]=\"styleButton?.isPrimary ? 'primary' : ''\"\n [ngStyle]=\"customStyle\">\n {{buttonLable}}\n </button>\n </div>\n </div>\n <div class=\"error-container\" [ngStyle]=\"{'background': error?.length ? 'rgba(255,255,255,0.25)' : 'none'}\"\n fxLayout=\"row\" fxLayoutAlign=\"center center\">\n <span class=\"button-error\">{{ error }}</span>\n </div>\n</div>", "templateHtml": "<div class=\"tb-rpc-button\" fxLayout=\"column\">\n <div fxFlex=\"20\" class=\"title-container\" fxLayout=\"row\"\n fxLayoutAlign=\"center center\" [fxShow]=\"showTitle\">\n <span class=\"button-title\">{{title}}</span>\n </div>\n <div fxFlex=\"{{showTitle ? 80 : 100}}\" [ngStyle]=\"{paddingTop: showTitle ? '5px': '10px'}\"\n class=\"button-container\" fxLayout=\"column\" fxLayoutAlign=\"center center\">\n <div>\n <button mat-button (click)=\"sendCommand()\"\n [class.mat-mdc-raised-button]=\"styleButton?.isRaised\"\n [color]=\"styleButton?.isPrimary ? 'primary' : ''\"\n [ngStyle]=\"customStyle\">\n {{buttonLable}}\n </button>\n </div>\n </div>\n <div class=\"error-container\" [ngStyle]=\"{'background': error?.length ? 'rgba(255,255,255,0.25)' : 'none'}\"\n fxLayout=\"row\" fxLayoutAlign=\"center center\">\n <span class=\"button-error\">{{ error }}</span>\n </div>\n</div>",
"templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .mat-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}", "templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .mat-mdc-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}",
"controllerScript": "var requestPersistent = false;\nvar persistentPollingInterval = 5000;\n\nself.onInit = function() {\n if (self.ctx.settings.requestPersistent) {\n requestPersistent = self.ctx.settings.requestPersistent;\n }\n if (self.ctx.settings.persistentPollingInterval) {\n persistentPollingInterval = self.ctx.settings.persistentPollingInterval;\n }\n \n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges();\n });\n};\n\nfunction init() {\n let rpcEnabled = self.ctx.defaultSubscription.rpcEnabled;\n\n self.ctx.$scope.buttonLable = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton.bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n if (!rpcEnabled) {\n self.ctx.$scope.error =\n 'Target device is not set!';\n }\n\n self.ctx.$scope.sendCommand = function() {\n var rpcMethod = self.ctx.settings.methodName;\n var rpcParams = self.ctx.settings.methodParams;\n if (rpcParams.length) {\n try {\n rpcParams = JSON.parse(rpcParams);\n } catch (e) {}\n }\n var timeout = self.ctx.settings.requestTimeout;\n var oneWayElseTwoWay = self.ctx.settings.oneWayElseTwoWay ?\n true : false;\n\n var commandPromise;\n if (oneWayElseTwoWay) {\n commandPromise = self.ctx.controlApi.sendOneWayCommand(\n rpcMethod, rpcParams, timeout, requestPersistent, persistentPollingInterval);\n } else {\n commandPromise = self.ctx.controlApi.sendTwoWayCommand(\n rpcMethod, rpcParams, timeout, requestPersistent, persistentPollingInterval);\n }\n commandPromise.subscribe(\n function success() {\n self.ctx.$scope.error = \"\";\n self.ctx.detectChanges();\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n self.ctx.detectChanges();\n }\n }\n );\n };\n}\n\nself.onDestroy = function() {\n self.ctx.controlApi.completedCommand();\n}\n", "controllerScript": "var requestPersistent = false;\nvar persistentPollingInterval = 5000;\n\nself.onInit = function() {\n if (self.ctx.settings.requestPersistent) {\n requestPersistent = self.ctx.settings.requestPersistent;\n }\n if (self.ctx.settings.persistentPollingInterval) {\n persistentPollingInterval = self.ctx.settings.persistentPollingInterval;\n }\n \n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges();\n });\n};\n\nfunction init() {\n let rpcEnabled = self.ctx.defaultSubscription.rpcEnabled;\n\n self.ctx.$scope.buttonLable = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton.bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n if (!rpcEnabled) {\n self.ctx.$scope.error =\n 'Target device is not set!';\n }\n\n self.ctx.$scope.sendCommand = function() {\n var rpcMethod = self.ctx.settings.methodName;\n var rpcParams = self.ctx.settings.methodParams;\n if (rpcParams.length) {\n try {\n rpcParams = JSON.parse(rpcParams);\n } catch (e) {}\n }\n var timeout = self.ctx.settings.requestTimeout;\n var oneWayElseTwoWay = self.ctx.settings.oneWayElseTwoWay ?\n true : false;\n\n var commandPromise;\n if (oneWayElseTwoWay) {\n commandPromise = self.ctx.controlApi.sendOneWayCommand(\n rpcMethod, rpcParams, timeout, requestPersistent, persistentPollingInterval);\n } else {\n commandPromise = self.ctx.controlApi.sendTwoWayCommand(\n rpcMethod, rpcParams, timeout, requestPersistent, persistentPollingInterval);\n }\n commandPromise.subscribe(\n function success() {\n self.ctx.$scope.error = \"\";\n self.ctx.detectChanges();\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n self.ctx.detectChanges();\n }\n }\n );\n };\n}\n\nself.onDestroy = function() {\n self.ctx.controlApi.completedCommand();\n}\n",
"settingsSchema": "", "settingsSchema": "",
"dataKeySettingsSchema": "{}\n", "dataKeySettingsSchema": "{}\n",
@ -149,8 +149,8 @@
"sizeX": 4, "sizeX": 4,
"sizeY": 2, "sizeY": 2,
"resources": [], "resources": [],
"templateHtml": "<div class=\"tb-rpc-button\" fxLayout=\"column\">\n <div fxFlex=\"20\" class=\"title-container\" fxLayout=\"row\"\n fxLayoutAlign=\"center center\" [fxShow]=\"showTitle\">\n <span class=\"button-title\">{{title}}</span>\n </div>\n <div fxFlex=\"{{showTitle ? 80 : 100}}\" [ngStyle]=\"{paddingTop: showTitle ? '5px': '10px'}\"\n class=\"button-container\" fxLayout=\"column\" fxLayoutAlign=\"center center\">\n <div>\n <button mat-button (click)=\"sendUpdate()\"\n [class.mat-raised-button]=\"styleButton?.isRaised\"\n [color]=\"styleButton?.isPrimary ? 'primary' : ''\"\n [ngStyle]=\"customStyle\">\n {{buttonLable}}\n </button>\n </div>\n </div>\n <div class=\"error-container\" [ngStyle]=\"{'background': error?.length ? 'rgba(255,255,255,0.25)' : 'none'}\"\n fxLayout=\"row\" fxLayoutAlign=\"center center\">\n <span class=\"button-error\">{{ error }}</span>\n </div>\n</div>", "templateHtml": "<div class=\"tb-rpc-button\" fxLayout=\"column\">\n <div fxFlex=\"20\" class=\"title-container\" fxLayout=\"row\"\n fxLayoutAlign=\"center center\" [fxShow]=\"showTitle\">\n <span class=\"button-title\">{{title}}</span>\n </div>\n <div fxFlex=\"{{showTitle ? 80 : 100}}\" [ngStyle]=\"{paddingTop: showTitle ? '5px': '10px'}\"\n class=\"button-container\" fxLayout=\"column\" fxLayoutAlign=\"center center\">\n <div>\n <button mat-button (click)=\"sendUpdate()\"\n [class.mat-mdc-raised-button]=\"styleButton?.isRaised\"\n [color]=\"styleButton?.isPrimary ? 'primary' : ''\"\n [ngStyle]=\"customStyle\">\n {{buttonLable}}\n </button>\n </div>\n </div>\n <div class=\"error-container\" [ngStyle]=\"{'background': error?.length ? 'rgba(255,255,255,0.25)' : 'none'}\"\n fxLayout=\"row\" fxLayoutAlign=\"center center\">\n <span class=\"button-error\">{{ error }}</span>\n </div>\n</div>",
"templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .mat-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}", "templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .mat-mdc-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}",
"controllerScript": "self.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges();\n });\n};\n\nfunction init() {\n self.ctx.$scope.buttonLable = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n let entityAttributeType = self.ctx.settings.entityAttributeType;\n let entityParameters = JSON.parse(self.ctx.settings.entityParameters);\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton\n .bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n let attributeService = self.ctx.$scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n\n self.ctx.$scope.sendUpdate = function() {\n let attributes = [];\n for (let key in entityParameters) {\n attributes.push({\n \"key\": key,\n \"value\": entityParameters[key]\n });\n }\n \n let entityId = {\n entityType: \"DEVICE\",\n id: self.ctx.defaultSubscription.targetDeviceId\n };\n attributeService.saveEntityAttributes(entityId,\n entityAttributeType, attributes).subscribe(\n function success() {\n self.ctx.$scope.error = \"\";\n self.ctx.detectChanges();\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n self.ctx.detectChanges();\n }\n }\n\n );\n };\n}\n", "controllerScript": "self.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges();\n });\n};\n\nfunction init() {\n self.ctx.$scope.buttonLable = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n let entityAttributeType = self.ctx.settings.entityAttributeType;\n let entityParameters = JSON.parse(self.ctx.settings.entityParameters);\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton\n .bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n let attributeService = self.ctx.$scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n\n self.ctx.$scope.sendUpdate = function() {\n let attributes = [];\n for (let key in entityParameters) {\n attributes.push({\n \"key\": key,\n \"value\": entityParameters[key]\n });\n }\n \n let entityId = {\n entityType: \"DEVICE\",\n id: self.ctx.defaultSubscription.targetDeviceId\n };\n attributeService.saveEntityAttributes(entityId,\n entityAttributeType, attributes).subscribe(\n function success() {\n self.ctx.$scope.error = \"\";\n self.ctx.detectChanges();\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n self.ctx.detectChanges();\n }\n }\n\n );\n };\n}\n",
"settingsSchema": "", "settingsSchema": "",
"dataKeySettingsSchema": "{}\n", "dataKeySettingsSchema": "{}\n",
@ -197,4 +197,4 @@
} }
} }
] ]
} }

4
application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json

File diff suppressed because one or more lines are too long

32
application/src/main/data/json/system/widget_bundles/input_widgets.json

File diff suppressed because one or more lines are too long

317
application/src/main/data/upgrade/3.4.4/schema_update.sql

@ -14,6 +14,70 @@
-- limitations under the License. -- limitations under the License.
-- --
-- USER CREDENTIALS START
ALTER TABLE user_credentials
ADD COLUMN IF NOT EXISTS additional_info varchar NOT NULL DEFAULT '{}';
UPDATE user_credentials
SET additional_info = json_build_object('userPasswordHistory', (u.additional_info::json -> 'userPasswordHistory'))
FROM tb_user u WHERE user_credentials.user_id = u.id AND u.additional_info::jsonb ? 'userPasswordHistory';
UPDATE tb_user SET additional_info = tb_user.additional_info::jsonb - 'userPasswordHistory' WHERE additional_info::jsonb ? 'userPasswordHistory';
-- USER CREDENTIALS END
-- ALARM ASSIGN TO USER START
ALTER TABLE alarm ADD COLUMN IF NOT EXISTS assign_ts BIGINT DEFAULT 0;
ALTER TABLE alarm ADD COLUMN IF NOT EXISTS assignee_id UUID;
CREATE INDEX IF NOT EXISTS idx_alarm_tenant_assignee_created_time ON alarm(tenant_id, assignee_id, created_time DESC);
-- ALARM ASSIGN TO USER END
-- ALARM STATUS REFACTORING START
ALTER TABLE alarm ADD COLUMN IF NOT EXISTS acknowledged boolean;
ALTER TABLE alarm ADD COLUMN IF NOT EXISTS cleared boolean;
ALTER TABLE alarm ADD COLUMN IF NOT EXISTS status varchar; -- to avoid failure of the subsequent upgrade.
UPDATE alarm SET acknowledged = true, cleared = true WHERE status = 'CLEARED_ACK';
UPDATE alarm SET acknowledged = true, cleared = false WHERE status = 'ACTIVE_ACK';
UPDATE alarm SET acknowledged = false, cleared = true WHERE status = 'CLEARED_UNACK';
UPDATE alarm SET acknowledged = false, cleared = false WHERE status = 'ACTIVE_UNACK';
-- Drop index by 'status' column and replace with new one that has only active alarms;
DROP INDEX IF EXISTS idx_alarm_originator_alarm_type_active;
CREATE INDEX IF NOT EXISTS idx_alarm_originator_alarm_type_active
ON alarm USING btree (originator_id, type) WHERE cleared = false;
-- Cover index by alarm type to optimize propagated alarm queries;
DROP INDEX IF EXISTS idx_entity_alarm_entity_id_alarm_type_created_time_alarm_id;
CREATE INDEX IF NOT EXISTS idx_entity_alarm_entity_id_alarm_type_created_time_alarm_id ON entity_alarm
USING btree (tenant_id, entity_id, alarm_type, created_time DESC) INCLUDE(alarm_id);
DROP INDEX IF EXISTS idx_alarm_tenant_status_created_time;
ALTER TABLE alarm DROP COLUMN IF EXISTS status;
-- Update old alarms and set their state to clear, if there are newer alarms.
UPDATE alarm a
SET cleared = TRUE
WHERE cleared = FALSE
AND id != (SELECT l.id
FROM alarm l
WHERE l.tenant_id = a.tenant_id
AND l.originator_id = a.originator_id
AND l.type = a.type
ORDER BY l.created_time DESC, l.id
LIMIT 1);
VACUUM FULL ANALYZE alarm;
-- ALARM STATUS REFACTORING END
-- ALARM COMMENTS START
CREATE TABLE IF NOT EXISTS alarm_comment ( CREATE TABLE IF NOT EXISTS alarm_comment (
id uuid NOT NULL, id uuid NOT NULL,
created_time bigint NOT NULL, created_time bigint NOT NULL,
@ -30,3 +94,256 @@ CREATE TABLE IF NOT EXISTS user_settings (
settings varchar(100000), settings varchar(100000),
CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES tb_user(id) ON DELETE CASCADE CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES tb_user(id) ON DELETE CASCADE
); );
-- ALARM COMMENTS END
-- ALARM INFO VIEW
DROP VIEW IF EXISTS alarm_info CASCADE;
CREATE VIEW alarm_info AS
SELECT a.*,
(CASE WHEN a.acknowledged AND a.cleared THEN 'CLEARED_ACK'
WHEN NOT a.acknowledged AND a.cleared THEN 'CLEARED_UNACK'
WHEN a.acknowledged AND NOT a.cleared THEN 'ACTIVE_ACK'
WHEN NOT a.acknowledged AND NOT a.cleared THEN 'ACTIVE_UNACK' END) as status,
COALESCE(CASE WHEN a.originator_type = 0 THEN (select title from tenant where id = a.originator_id)
WHEN a.originator_type = 1 THEN (select title from customer where id = a.originator_id)
WHEN a.originator_type = 2 THEN (select email from tb_user where id = a.originator_id)
WHEN a.originator_type = 3 THEN (select title from dashboard where id = a.originator_id)
WHEN a.originator_type = 4 THEN (select name from asset where id = a.originator_id)
WHEN a.originator_type = 5 THEN (select name from device where id = a.originator_id)
WHEN a.originator_type = 9 THEN (select name from entity_view where id = a.originator_id)
WHEN a.originator_type = 13 THEN (select name from device_profile where id = a.originator_id)
WHEN a.originator_type = 14 THEN (select name from asset_profile where id = a.originator_id)
WHEN a.originator_type = 18 THEN (select name from edge where id = a.originator_id) END
, 'Deleted') originator_name,
COALESCE(CASE WHEN a.originator_type = 0 THEN (select title from tenant where id = a.originator_id)
WHEN a.originator_type = 1 THEN (select COALESCE(title, email) from customer where id = a.originator_id)
WHEN a.originator_type = 2 THEN (select email from tb_user where id = a.originator_id)
WHEN a.originator_type = 3 THEN (select title from dashboard where id = a.originator_id)
WHEN a.originator_type = 4 THEN (select COALESCE(label, name) from asset where id = a.originator_id)
WHEN a.originator_type = 5 THEN (select COALESCE(label, name) from device where id = a.originator_id)
WHEN a.originator_type = 9 THEN (select name from entity_view where id = a.originator_id)
WHEN a.originator_type = 13 THEN (select name from device_profile where id = a.originator_id)
WHEN a.originator_type = 14 THEN (select name from asset_profile where id = a.originator_id)
WHEN a.originator_type = 18 THEN (select COALESCE(label, name) from edge where id = a.originator_id) END
, 'Deleted') as originator_label,
u.first_name as assignee_first_name, u.last_name as assignee_last_name, u.email as assignee_email
FROM alarm a
LEFT JOIN tb_user u ON u.id = a.assignee_id;
-- ALARM INFO VIEW END
-- ALARM FUNCTIONS START
DROP FUNCTION IF EXISTS create_or_update_active_alarm;
CREATE OR REPLACE FUNCTION create_or_update_active_alarm(
t_id uuid, c_id uuid, a_id uuid, a_created_ts bigint,
a_o_id uuid, a_o_type integer, a_type varchar,
a_severity varchar, a_start_ts bigint, a_end_ts bigint,
a_details varchar,
a_propagate boolean, a_propagate_to_owner boolean,
a_propagate_to_tenant boolean, a_propagation_types varchar,
a_creation_enabled boolean)
RETURNS varchar
LANGUAGE plpgsql
AS
$$
DECLARE
null_id constant uuid = '13814000-1dd2-11b2-8080-808080808080'::uuid;
existing alarm;
result alarm_info;
row_count integer;
BEGIN
SELECT * INTO existing FROM alarm a WHERE a.originator_id = a_o_id AND a.type = a_type AND a.cleared = false ORDER BY a.start_ts DESC FOR UPDATE;
IF existing.id IS NULL THEN
IF a_creation_enabled = FALSE THEN
RETURN json_build_object('success', false)::text;
END IF;
IF c_id = null_id THEN
c_id = NULL;
end if;
INSERT INTO alarm
(tenant_id, customer_id, id, created_time,
originator_id, originator_type, type,
severity, start_ts, end_ts,
additional_info,
propagate, propagate_to_owner, propagate_to_tenant, propagate_relation_types,
acknowledged, ack_ts,
cleared, clear_ts,
assignee_id, assign_ts)
VALUES
(t_id, c_id, a_id, a_created_ts,
a_o_id, a_o_type, a_type,
a_severity, a_start_ts, a_end_ts,
a_details,
a_propagate, a_propagate_to_owner, a_propagate_to_tenant, a_propagation_types,
false, 0, false, 0, NULL, 0);
SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id;
RETURN json_build_object('success', true, 'created', true, 'modified', true, 'alarm', row_to_json(result))::text;
ELSE
UPDATE alarm a
SET severity = a_severity,
start_ts = a_start_ts,
end_ts = a_end_ts,
additional_info = a_details,
propagate = a_propagate,
propagate_to_owner = a_propagate_to_owner,
propagate_to_tenant = a_propagate_to_tenant,
propagate_relation_types = a_propagation_types
WHERE a.id = existing.id
AND a.tenant_id = t_id
AND (severity != a_severity OR start_ts != a_start_ts OR end_ts != a_end_ts OR additional_info != a_details
OR propagate != a_propagate OR propagate_to_owner != a_propagate_to_owner OR
propagate_to_tenant != a_propagate_to_tenant OR propagate_relation_types != a_propagation_types);
GET DIAGNOSTICS row_count = ROW_COUNT;
SELECT * INTO result FROM alarm_info a WHERE a.id = existing.id AND a.tenant_id = t_id;
IF row_count > 0 THEN
RETURN json_build_object('success', true, 'modified', true, 'alarm', row_to_json(result), 'old', row_to_json(existing))::text;
ELSE
RETURN json_build_object('success', true, 'modified', false, 'alarm', row_to_json(result))::text;
END IF;
END IF;
END
$$;
DROP FUNCTION IF EXISTS update_alarm;
CREATE OR REPLACE FUNCTION update_alarm(t_id uuid, a_id uuid, a_severity varchar, a_start_ts bigint, a_end_ts bigint,
a_details varchar,
a_propagate boolean, a_propagate_to_owner boolean,
a_propagate_to_tenant boolean, a_propagation_types varchar)
RETURNS varchar
LANGUAGE plpgsql
AS
$$
DECLARE
existing alarm;
result alarm_info;
row_count integer;
BEGIN
SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE;
IF existing IS NULL THEN
RETURN json_build_object('success', false)::text;
END IF;
UPDATE alarm a
SET severity = a_severity,
start_ts = a_start_ts,
end_ts = a_end_ts,
additional_info = a_details,
propagate = a_propagate,
propagate_to_owner = a_propagate_to_owner,
propagate_to_tenant = a_propagate_to_tenant,
propagate_relation_types = a_propagation_types
WHERE a.id = a_id
AND a.tenant_id = t_id
AND (severity != a_severity OR start_ts != a_start_ts OR end_ts != a_end_ts OR additional_info != a_details
OR propagate != a_propagate OR propagate_to_owner != a_propagate_to_owner OR
propagate_to_tenant != a_propagate_to_tenant OR propagate_relation_types != a_propagation_types);
GET DIAGNOSTICS row_count = ROW_COUNT;
SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id;
IF row_count > 0 THEN
RETURN json_build_object('success', true, 'modified', row_count > 0, 'alarm', row_to_json(result), 'old', row_to_json(existing))::text;
ELSE
RETURN json_build_object('success', true, 'modified', row_count > 0, 'alarm', row_to_json(result))::text;
END IF;
END
$$;
DROP FUNCTION IF EXISTS acknowledge_alarm;
CREATE OR REPLACE FUNCTION acknowledge_alarm(t_id uuid, a_id uuid, a_ts bigint)
RETURNS varchar
LANGUAGE plpgsql
AS
$$
DECLARE
existing alarm;
result alarm_info;
modified boolean = FALSE;
BEGIN
SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE;
IF existing IS NULL THEN
RETURN json_build_object('success', false)::text;
END IF;
IF NOT (existing.acknowledged) THEN
modified = TRUE;
UPDATE alarm a SET acknowledged = true, ack_ts = a_ts WHERE a.id = a_id AND a.tenant_id = t_id;
END IF;
SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id;
RETURN json_build_object('success', true, 'modified', modified, 'alarm', row_to_json(result))::text;
END
$$;
DROP FUNCTION IF EXISTS clear_alarm;
CREATE OR REPLACE FUNCTION clear_alarm(t_id uuid, a_id uuid, a_ts bigint, a_details varchar)
RETURNS varchar
LANGUAGE plpgsql
AS
$$
DECLARE
existing alarm;
result alarm_info;
cleared boolean = FALSE;
BEGIN
SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE;
IF existing IS NULL THEN
RETURN json_build_object('success', false)::text;
END IF;
IF NOT(existing.cleared) THEN
cleared = TRUE;
UPDATE alarm a SET cleared = true, clear_ts = a_ts, additional_info = a_details WHERE a.id = a_id AND a.tenant_id = t_id;
END IF;
SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id;
RETURN json_build_object('success', true, 'cleared', cleared, 'alarm', row_to_json(result))::text;
END
$$;
DROP FUNCTION IF EXISTS assign_alarm;
CREATE OR REPLACE FUNCTION assign_alarm(t_id uuid, a_id uuid, u_id uuid, a_ts bigint)
RETURNS varchar
LANGUAGE plpgsql
AS
$$
DECLARE
existing alarm;
result alarm_info;
modified boolean = FALSE;
BEGIN
SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE;
IF existing IS NULL THEN
RETURN json_build_object('success', false)::text;
END IF;
IF existing.assignee_id IS NULL OR existing.assignee_id != u_id THEN
modified = TRUE;
UPDATE alarm a SET assignee_id = u_id, assign_ts = a_ts WHERE a.id = a_id AND a.tenant_id = t_id;
END IF;
SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id;
RETURN json_build_object('success', true, 'modified', modified, 'alarm', row_to_json(result))::text;
END
$$;
DROP FUNCTION IF EXISTS unassign_alarm;
CREATE OR REPLACE FUNCTION unassign_alarm(t_id uuid, a_id uuid, a_ts bigint)
RETURNS varchar
LANGUAGE plpgsql
AS
$$
DECLARE
existing alarm;
result alarm_info;
modified boolean = FALSE;
BEGIN
SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE;
IF existing IS NULL THEN
RETURN json_build_object('success', false)::text;
END IF;
IF existing.assignee_id IS NOT NULL THEN
modified = TRUE;
UPDATE alarm a SET assignee_id = NULL, assign_ts = a_ts WHERE a.id = a_id AND a.tenant_id = t_id;
END IF;
SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id;
RETURN json_build_object('success', true, 'modified', modified, 'alarm', row_to_json(result))::text;
END
$$;
-- ALARM FUNCTIONS END

2
application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java

@ -116,7 +116,7 @@ import java.util.stream.Collectors;
* @author Andrew Shvayka * @author Andrew Shvayka
*/ */
@Slf4j @Slf4j
class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
static final String SESSION_TIMEOUT_MESSAGE = "session timeout!"; static final String SESSION_TIMEOUT_MESSAGE = "session timeout!";
final TenantId tenantId; final TenantId tenantId;

17
application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java

@ -15,6 +15,8 @@
*/ */
package org.thingsboard.server.config; package org.thingsboard.server.config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@ -40,11 +42,15 @@ import java.util.Map;
@Configuration @Configuration
@TbCoreComponent @TbCoreComponent
@EnableWebSocket @EnableWebSocket
@RequiredArgsConstructor
@Slf4j
public class WebSocketConfiguration implements WebSocketConfigurer { public class WebSocketConfiguration implements WebSocketConfigurer {
public static final String WS_PLUGIN_PREFIX = "/api/ws/plugins/"; public static final String WS_PLUGIN_PREFIX = "/api/ws/plugins/";
private static final String WS_PLUGIN_MAPPING = WS_PLUGIN_PREFIX + "**"; private static final String WS_PLUGIN_MAPPING = WS_PLUGIN_PREFIX + "**";
private final WebSocketHandler wsHandler;
@Bean @Bean
public ServletServerContainerFactoryBean createWebSocketContainer() { public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean(); ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
@ -55,7 +61,11 @@ public class WebSocketConfiguration implements WebSocketConfigurer {
@Override @Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(wsHandler(), WS_PLUGIN_MAPPING).setAllowedOriginPatterns("*") if (!(wsHandler instanceof TbWebSocketHandler)) {
log.error("TbWebSocketHandler expected but [{}] provided", wsHandler);
throw new RuntimeException("TbWebSocketHandler expected but " + wsHandler + " provided");
}
registry.addHandler(wsHandler, WS_PLUGIN_MAPPING).setAllowedOriginPatterns("*")
.addInterceptors(new HttpSessionHandshakeInterceptor(), new HandshakeInterceptor() { .addInterceptors(new HttpSessionHandshakeInterceptor(), new HandshakeInterceptor() {
@Override @Override
@ -82,11 +92,6 @@ public class WebSocketConfiguration implements WebSocketConfigurer {
}); });
} }
@Bean
public WebSocketHandler wsHandler() {
return new TbWebSocketHandler();
}
protected SecurityUser getCurrentUser() throws ThingsboardException { protected SecurityUser getCurrentUser() throws ThingsboardException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() instanceof SecurityUser) { if (authentication != null && authentication.getPrincipal() instanceof SecurityUser) {

85
application/src/main/java/org/thingsboard/server/controller/AlarmController.java

@ -30,6 +30,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmQuery;
@ -41,18 +42,25 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.alarm.TbAlarmService; import org.thingsboard.server.service.entitiy.alarm.TbAlarmService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.permission.Resource;
import java.util.UUID;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import static org.thingsboard.server.controller.ControllerConstants.ALARM_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ALARM_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ALARM_INFO_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ALARM_INFO_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ALARM_SORT_PROPERTY_ALLOWABLE_VALUES; import static org.thingsboard.server.controller.ControllerConstants.ALARM_SORT_PROPERTY_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.ASSIGNEE_ID;
import static org.thingsboard.server.controller.ControllerConstants.ASSIGN_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE;
@ -81,6 +89,7 @@ public class AlarmController extends BaseController {
private static final String ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES = "ANY, ACTIVE, CLEARED, ACK, UNACK"; private static final String ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES = "ANY, ACTIVE, CLEARED, ACK, UNACK";
private static final String ALARM_QUERY_STATUS_DESCRIPTION = "A string value representing one of the AlarmStatus enumeration value"; private static final String ALARM_QUERY_STATUS_DESCRIPTION = "A string value representing one of the AlarmStatus enumeration value";
private static final String ALARM_QUERY_STATUS_ALLOWABLE_VALUES = "ACTIVE_UNACK, ACTIVE_ACK, CLEARED_UNACK, CLEARED_ACK"; private static final String ALARM_QUERY_STATUS_ALLOWABLE_VALUES = "ACTIVE_UNACK, ACTIVE_ACK, CLEARED_UNACK, CLEARED_ACK";
private static final String ALARM_QUERY_ASSIGNEE_DESCRIPTION = "A string value representing the assignee user id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
private static final String ALARM_QUERY_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on of next alarm fields: type, severity or status"; private static final String ALARM_QUERY_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on of next alarm fields: type, severity or status";
private static final String ALARM_QUERY_START_TIME_DESCRIPTION = "The start timestamp in milliseconds of the search time range over the Alarm class field: 'createdTime'."; private static final String ALARM_QUERY_START_TIME_DESCRIPTION = "The start timestamp in milliseconds of the search time range over the Alarm class field: 'createdTime'.";
private static final String ALARM_QUERY_END_TIME_DESCRIPTION = "The end timestamp in milliseconds of the search time range over the Alarm class field: 'createdTime'."; private static final String ALARM_QUERY_END_TIME_DESCRIPTION = "The end timestamp in milliseconds of the search time range over the Alarm class field: 'createdTime'.";
@ -112,7 +121,7 @@ public class AlarmController extends BaseController {
return checkAlarmInfoId(alarmId, Operation.READ); return checkAlarmInfoId(alarmId, Operation.READ);
} }
@ApiOperation(value = "Create or update Alarm (saveAlarm)", @ApiOperation(value = "Create or Update Alarm (saveAlarm)",
notes = "Creates or Updates the Alarm. " + notes = "Creates or Updates the Alarm. " +
"When creating alarm, platform generates Alarm Id as " + UUID_WIKI_LINK + "When creating alarm, platform generates Alarm Id as " + UUID_WIKI_LINK +
"The newly created Alarm id will be present in the response. Specify existing Alarm id to update the alarm. " + "The newly created Alarm id will be present in the response. Specify existing Alarm id to update the alarm. " +
@ -129,7 +138,9 @@ public class AlarmController extends BaseController {
@ResponseBody @ResponseBody
public Alarm saveAlarm(@ApiParam(value = "A JSON value representing the alarm.") @RequestBody Alarm alarm) throws ThingsboardException { public Alarm saveAlarm(@ApiParam(value = "A JSON value representing the alarm.") @RequestBody Alarm alarm) throws ThingsboardException {
alarm.setTenantId(getTenantId()); alarm.setTenantId(getTenantId());
checkNotNull(alarm.getOriginator());
checkEntity(alarm.getId(), alarm, Resource.ALARM); checkEntity(alarm.getId(), alarm, Resource.ALARM);
checkEntityId(alarm.getOriginator(), Operation.READ);
return tbAlarmService.save(alarm, getCurrentUser()); return tbAlarmService.save(alarm, getCurrentUser());
} }
@ -152,11 +163,12 @@ public class AlarmController extends BaseController {
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}/ack", method = RequestMethod.POST) @RequestMapping(value = "/alarm/{alarmId}/ack", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public void ackAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception { public AlarmInfo ackAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception {
checkParameter(ALARM_ID, strAlarmId); checkParameter(ALARM_ID, strAlarmId);
AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); Alarm alarm = checkAlarmId(alarmId, Operation.WRITE);
tbAlarmService.ack(alarm, getCurrentUser()).get(); //TODO: return correct error code if the alarm is not found or already cleared
return tbAlarmService.ack(alarm, getCurrentUser());
} }
@ApiOperation(value = "Clear Alarm (clearAlarm)", @ApiOperation(value = "Clear Alarm (clearAlarm)",
@ -166,11 +178,50 @@ public class AlarmController extends BaseController {
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}/clear", method = RequestMethod.POST) @RequestMapping(value = "/alarm/{alarmId}/clear", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public void clearAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception { public AlarmInfo clearAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception {
checkParameter(ALARM_ID, strAlarmId);
AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
Alarm alarm = checkAlarmId(alarmId, Operation.WRITE);
//TODO: return correct error code if the alarm is not found or already cleared
return tbAlarmService.clear(alarm, getCurrentUser());
}
@ApiOperation(value = "Assign/Reassign Alarm (assignAlarm)",
notes = "Assign the Alarm. " +
"Once assigned, the 'assign_ts' field will be set to current timestamp and special rule chain event 'ALARM_ASSIGNED' " +
"(or ALARM_REASSIGNED in case of assigning already assigned alarm) will be generated. " +
"Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}/assign/{assigneeId}", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
public Alarm assignAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION)
@PathVariable(ALARM_ID) String strAlarmId,
@ApiParam(value = ASSIGN_ID_PARAM_DESCRIPTION)
@PathVariable(ASSIGNEE_ID) String strAssigneeId
) throws Exception {
checkParameter(ALARM_ID, strAlarmId);
checkParameter(ASSIGNEE_ID, strAssigneeId);
AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
Alarm alarm = checkAlarmId(alarmId, Operation.WRITE);
UserId assigneeId = new UserId(UUID.fromString(strAssigneeId));
checkUserId(assigneeId, Operation.READ);
return tbAlarmService.assign(alarm, assigneeId, System.currentTimeMillis(), getCurrentUser());
}
@ApiOperation(value = "Unassign Alarm (unassignAlarm)",
notes = "Unassign the Alarm. " +
"Once unassigned, the 'assign_ts' field will be set to current timestamp and special rule chain event 'ALARM_UNASSIGNED' will be generated. " +
"Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}/assign", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK)
public Alarm unassignAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION)
@PathVariable(ALARM_ID) String strAlarmId
) throws Exception {
checkParameter(ALARM_ID, strAlarmId); checkParameter(ALARM_ID, strAlarmId);
AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); Alarm alarm = checkAlarmId(alarmId, Operation.WRITE);
tbAlarmService.clear(alarm, getCurrentUser()).get(); return tbAlarmService.unassign(alarm, System.currentTimeMillis(), getCurrentUser());
} }
@ApiOperation(value = "Get Alarms (getAlarms)", @ApiOperation(value = "Get Alarms (getAlarms)",
@ -188,6 +239,8 @@ public class AlarmController extends BaseController {
@RequestParam(required = false) String searchStatus, @RequestParam(required = false) String searchStatus,
@ApiParam(value = ALARM_QUERY_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_STATUS_ALLOWABLE_VALUES) @ApiParam(value = ALARM_QUERY_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_STATUS_ALLOWABLE_VALUES)
@RequestParam(required = false) String status, @RequestParam(required = false) String status,
@ApiParam(value = ALARM_QUERY_ASSIGNEE_DESCRIPTION)
@RequestParam(required = false) String assigneeId,
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@ -215,9 +268,13 @@ public class AlarmController extends BaseController {
"and 'status' can't be specified at the same time!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); "and 'status' can't be specified at the same time!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} }
checkEntityId(entityId, Operation.READ); checkEntityId(entityId, Operation.READ);
UserId assigneeUserId = null;
if (assigneeId != null) {
assigneeUserId = new UserId(UUID.fromString(assigneeId));
}
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(entityId, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator)).get()); return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(entityId, pageLink, alarmSearchStatus, alarmStatus, assigneeUserId, fetchOriginator)).get());
} }
@ApiOperation(value = "Get All Alarms (getAllAlarms)", @ApiOperation(value = "Get All Alarms (getAllAlarms)",
@ -234,6 +291,8 @@ public class AlarmController extends BaseController {
@RequestParam(required = false) String searchStatus, @RequestParam(required = false) String searchStatus,
@ApiParam(value = ALARM_QUERY_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_STATUS_ALLOWABLE_VALUES) @ApiParam(value = ALARM_QUERY_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_STATUS_ALLOWABLE_VALUES)
@RequestParam(required = false) String status, @RequestParam(required = false) String status,
@ApiParam(value = ALARM_QUERY_ASSIGNEE_DESCRIPTION)
@RequestParam(required = false) String assigneeId,
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@ -257,12 +316,16 @@ public class AlarmController extends BaseController {
throw new ThingsboardException("Invalid alarms search query: Both parameters 'searchStatus' " + throw new ThingsboardException("Invalid alarms search query: Both parameters 'searchStatus' " +
"and 'status' can't be specified at the same time!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); "and 'status' can't be specified at the same time!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} }
UserId assigneeUserId = null;
if (assigneeId != null) {
assigneeUserId = new UserId(UUID.fromString(assigneeId));
}
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
if (getCurrentUser().isCustomerUser()) { if (getCurrentUser().isCustomerUser()) {
return checkNotNull(alarmService.findCustomerAlarms(getCurrentUser().getTenantId(), getCurrentUser().getCustomerId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator)).get()); return checkNotNull(alarmService.findCustomerAlarms(getCurrentUser().getTenantId(), getCurrentUser().getCustomerId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, assigneeUserId, fetchOriginator)).get());
} else { } else {
return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator)).get()); return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, assigneeUserId, fetchOriginator)).get());
} }
} }
@ -281,7 +344,9 @@ public class AlarmController extends BaseController {
@ApiParam(value = ALARM_QUERY_SEARCH_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES) @ApiParam(value = ALARM_QUERY_SEARCH_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES)
@RequestParam(required = false) String searchStatus, @RequestParam(required = false) String searchStatus,
@ApiParam(value = ALARM_QUERY_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_STATUS_ALLOWABLE_VALUES) @ApiParam(value = ALARM_QUERY_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_STATUS_ALLOWABLE_VALUES)
@RequestParam(required = false) String status @RequestParam(required = false) String status,
@ApiParam(value = ALARM_QUERY_ASSIGNEE_DESCRIPTION)
@RequestParam(required = false) String assigneeId
) throws ThingsboardException { ) throws ThingsboardException {
checkParameter("EntityId", strEntityId); checkParameter("EntityId", strEntityId);
checkParameter("EntityType", strEntityType); checkParameter("EntityType", strEntityType);
@ -293,7 +358,7 @@ public class AlarmController extends BaseController {
"and 'status' can't be specified at the same time!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); "and 'status' can't be specified at the same time!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} }
checkEntityId(entityId, Operation.READ); checkEntityId(entityId, Operation.READ);
return alarmService.findHighestAlarmSeverity(getCurrentUser().getTenantId(), entityId, alarmSearchStatus, alarmStatus); return alarmService.findHighestAlarmSeverity(getCurrentUser().getTenantId(), entityId, alarmSearchStatus, alarmStatus, assigneeId);
} }
} }

19
application/src/main/java/org/thingsboard/server/controller/AuthController.java

@ -20,6 +20,7 @@ import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam; import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@ -41,11 +42,13 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent;
import org.thingsboard.server.common.data.security.event.UserSessionInvalidationEvent; import org.thingsboard.server.common.data.security.event.UserSessionInvalidationEvent;
import org.thingsboard.server.common.data.security.model.SecuritySettings; import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.common.msg.tools.TbRateLimits;
import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
@ -62,6 +65,8 @@ import org.thingsboard.server.service.security.system.SystemSecurityService;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@RestController @RestController
@TbCoreComponent @TbCoreComponent
@ -69,6 +74,10 @@ import java.net.URISyntaxException;
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
public class AuthController extends BaseController { public class AuthController extends BaseController {
@Value("${server.rest.rate_limits.reset_password_per_user:5:3600}")
private String defaultLimitsConfiguration;
private final ConcurrentMap<UserId, TbRateLimits> resetPasswordRateLimits = new ConcurrentHashMap<>();
private final BCryptPasswordEncoder passwordEncoder; private final BCryptPasswordEncoder passwordEncoder;
private final JwtTokenFactory tokenFactory; private final JwtTokenFactory tokenFactory;
private final MailService mailService; private final MailService mailService;
@ -199,7 +208,12 @@ public class AuthController extends BaseController {
HttpStatus responseStatus; HttpStatus responseStatus;
String resetURI = "/login/resetPassword"; String resetURI = "/login/resetPassword";
UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken); UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken);
if (userCredentials != null) { if (userCredentials != null) {
TbRateLimits tbRateLimits = getTbRateLimits(userCredentials.getUserId());
if (!tbRateLimits.tryConsume()) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).build();
}
try { try {
URI location = new URI(resetURI + "?resetToken=" + resetToken); URI location = new URI(resetURI + "?resetToken=" + resetToken);
headers.setLocation(location); headers.setLocation(location);
@ -299,4 +313,9 @@ public class AuthController extends BaseController {
systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(request), ActionType.LOGOUT, null); systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(request), ActionType.LOGOUT, null);
eventPublisher.publishEvent(new UserSessionInvalidationEvent(user.getSessionId())); eventPublisher.publishEvent(new UserSessionInvalidationEvent(user.getSessionId()));
} }
private TbRateLimits getTbRateLimits(UserId userId) {
return resetPasswordRateLimits.computeIfAbsent(userId,
key -> new TbRateLimits(defaultLimitsConfiguration, true));
}
} }

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

@ -613,7 +613,6 @@ public abstract class BaseController {
throw handleException(e, false); throw handleException(e, false);
} }
} }
Device checkDeviceId(DeviceId deviceId, Operation operation) throws ThingsboardException { Device checkDeviceId(DeviceId deviceId, Operation operation) throws ThingsboardException {
try { try {
validateId(deviceId, "Incorrect deviceId " + deviceId); validateId(deviceId, "Incorrect deviceId " + deviceId);
@ -725,7 +724,7 @@ public abstract class BaseController {
AlarmInfo checkAlarmInfoId(AlarmId alarmId, Operation operation) throws ThingsboardException { AlarmInfo checkAlarmInfoId(AlarmId alarmId, Operation operation) throws ThingsboardException {
try { try {
validateId(alarmId, "Incorrect alarmId " + alarmId); validateId(alarmId, "Incorrect alarmId " + alarmId);
AlarmInfo alarmInfo = alarmService.findAlarmInfoByIdAsync(getCurrentUser().getTenantId(), alarmId).get(); AlarmInfo alarmInfo = alarmService.findAlarmInfoById(getCurrentUser().getTenantId(), alarmId);
checkNotNull(alarmInfo, "Alarm with id [" + alarmId + "] is not found"); checkNotNull(alarmInfo, "Alarm with id [" + alarmId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.ALARM, operation, alarmId, alarmInfo); accessControlService.checkPermission(getCurrentUser(), Resource.ALARM, operation, alarmId, alarmInfo);
return alarmInfo; return alarmInfo;

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

@ -27,6 +27,7 @@ public class ControllerConstants {
protected static final String EDGE_ID = "edgeId"; protected static final String EDGE_ID = "edgeId";
protected static final String RPC_ID = "rpcId"; protected static final String RPC_ID = "rpcId";
protected static final String ENTITY_ID = "entityId"; protected static final String ENTITY_ID = "entityId";
protected static final String ASSIGNEE_ID = "assigneeId";
protected static final String PAGE_DATA_PARAMETERS = "You can specify parameters to filter the results. " + protected static final String PAGE_DATA_PARAMETERS = "You can specify parameters to filter the results. " +
"The result is wrapped with PageData object that allows you to iterate over result set using pagination. " + "The result is wrapped with PageData object that allows you to iterate over result set using pagination. " +
"See the 'Model' tab of the Response Class for more details. "; "See the 'Model' tab of the Response Class for more details. ";
@ -44,6 +45,7 @@ public class ControllerConstants {
protected static final String USER_ID_PARAM_DESCRIPTION = "A string value representing the user id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String USER_ID_PARAM_DESCRIPTION = "A string value representing the user id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String ASSET_ID_PARAM_DESCRIPTION = "A string value representing the asset id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String ASSET_ID_PARAM_DESCRIPTION = "A string value representing the asset id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String ALARM_ID_PARAM_DESCRIPTION = "A string value representing the alarm id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String ALARM_ID_PARAM_DESCRIPTION = "A string value representing the alarm id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String ASSIGN_ID_PARAM_DESCRIPTION = "A string value representing the user id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String ALARM_COMMENT_ID_PARAM_DESCRIPTION = "A string value representing the alarm comment id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String ALARM_COMMENT_ID_PARAM_DESCRIPTION = "A string value representing the alarm comment id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String ENTITY_ID_PARAM_DESCRIPTION = "A string value representing the entity id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String ENTITY_ID_PARAM_DESCRIPTION = "A string value representing the entity id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";

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

@ -29,6 +29,7 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult; import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmData;
import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.query.AlarmDataQuery;
@ -38,6 +39,7 @@ import org.thingsboard.server.common.data.query.EntityDataPageLink;
import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.query.EntityQueryService; import org.thingsboard.server.service.query.EntityQueryService;
import org.thingsboard.server.service.security.permission.Operation;
import static org.thingsboard.server.controller.ControllerConstants.ALARM_DATA_QUERY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ALARM_DATA_QUERY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_COUNT_QUERY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_COUNT_QUERY_DESCRIPTION;
@ -83,6 +85,11 @@ public class EntityQueryController extends BaseController {
@ApiParam(value = "A JSON value representing the alarm data query. See API call notes above for more details.") @ApiParam(value = "A JSON value representing the alarm data query. See API call notes above for more details.")
@RequestBody AlarmDataQuery query) throws ThingsboardException { @RequestBody AlarmDataQuery query) throws ThingsboardException {
checkNotNull(query); checkNotNull(query);
checkNotNull(query.getPageLink());
UserId assigneeId = query.getPageLink().getAssigneeId();
if (assigneeId != null) {
checkUserId(assigneeId, Operation.READ);
}
return this.entityQueryService.findAlarmDataByQuery(getCurrentUser(), query); return this.entityQueryService.findAlarmDataByQuery(getCurrentUser(), query);
} }

16
application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java

@ -67,8 +67,8 @@ import static org.thingsboard.server.service.telemetry.DefaultTelemetryWebSocket
@Slf4j @Slf4j
public class TbWebSocketHandler extends TextWebSocketHandler implements TelemetryWebSocketMsgEndpoint { public class TbWebSocketHandler extends TextWebSocketHandler implements TelemetryWebSocketMsgEndpoint {
private static final ConcurrentMap<String, SessionMetaData> internalSessionMap = new ConcurrentHashMap<>(); private final ConcurrentMap<String, SessionMetaData> internalSessionMap = new ConcurrentHashMap<>();
private static final ConcurrentMap<String, String> externalSessionMap = new ConcurrentHashMap<>(); private final ConcurrentMap<String, String> externalSessionMap = new ConcurrentHashMap<>();
@Autowired @Autowired
@ -82,13 +82,13 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
@Value("${server.ws.ping_timeout:30000}") @Value("${server.ws.ping_timeout:30000}")
private long pingTimeout; private long pingTimeout;
private ConcurrentMap<String, TelemetryWebSocketSessionRef> blacklistedSessions = new ConcurrentHashMap<>(); private final ConcurrentMap<String, TelemetryWebSocketSessionRef> blacklistedSessions = new ConcurrentHashMap<>();
private ConcurrentMap<String, TbRateLimits> perSessionUpdateLimits = new ConcurrentHashMap<>(); private final ConcurrentMap<String, TbRateLimits> perSessionUpdateLimits = new ConcurrentHashMap<>();
private ConcurrentMap<TenantId, Set<String>> tenantSessionsMap = new ConcurrentHashMap<>(); private final ConcurrentMap<TenantId, Set<String>> tenantSessionsMap = new ConcurrentHashMap<>();
private ConcurrentMap<CustomerId, Set<String>> customerSessionsMap = new ConcurrentHashMap<>(); private final ConcurrentMap<CustomerId, Set<String>> customerSessionsMap = new ConcurrentHashMap<>();
private ConcurrentMap<UserId, Set<String>> regularUserSessionsMap = new ConcurrentHashMap<>(); private final ConcurrentMap<UserId, Set<String>> regularUserSessionsMap = new ConcurrentHashMap<>();
private ConcurrentMap<UserId, Set<String>> publicUserSessionsMap = new ConcurrentHashMap<>(); private final ConcurrentMap<UserId, Set<String>> publicUserSessionsMap = new ConcurrentHashMap<>();
@Override @Override
public void handleTextMessage(WebSocketSession session, TextMessage message) { public void handleTextMessage(WebSocketSession session, TextMessage message) {

6
application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java

@ -86,6 +86,12 @@ public class EntityActionService {
case ALARM_CLEAR: case ALARM_CLEAR:
msgType = DataConstants.ALARM_CLEAR; msgType = DataConstants.ALARM_CLEAR;
break; break;
case ALARM_ASSIGN:
msgType = DataConstants.ALARM_ASSIGN;
break;
case ALARM_UNASSIGN:
msgType = DataConstants.ALARM_UNASSIGN;
break;
case ALARM_DELETE: case ALARM_DELETE:
msgType = DataConstants.ALARM_DELETE; msgType = DataConstants.ALARM_DELETE;
break; break;

12
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java

@ -48,7 +48,7 @@ public abstract class BaseAlarmProcessor extends BaseEdgeProcessor {
return Futures.immediateFuture(null); return Futures.immediateFuture(null);
} }
try { try {
Alarm existentAlarm = alarmService.findLatestByOriginatorAndType(tenantId, originatorId, alarmUpdateMsg.getType()).get(); Alarm existentAlarm = alarmService.findLatestActiveByOriginatorAndType(tenantId, originatorId, alarmUpdateMsg.getType());
switch (alarmUpdateMsg.getMsgType()) { switch (alarmUpdateMsg.getMsgType()) {
case ENTITY_CREATED_RPC_MESSAGE: case ENTITY_CREATED_RPC_MESSAGE:
case ENTITY_UPDATED_RPC_MESSAGE: case ENTITY_UPDATED_RPC_MESSAGE:
@ -62,7 +62,9 @@ public abstract class BaseAlarmProcessor extends BaseEdgeProcessor {
existentAlarm.setClearTs(alarmUpdateMsg.getClearTs()); existentAlarm.setClearTs(alarmUpdateMsg.getClearTs());
existentAlarm.setPropagate(alarmUpdateMsg.getPropagate()); existentAlarm.setPropagate(alarmUpdateMsg.getPropagate());
} }
existentAlarm.setStatus(AlarmStatus.valueOf(alarmUpdateMsg.getStatus())); var alarmStatus = AlarmStatus.valueOf(alarmUpdateMsg.getStatus());
existentAlarm.setCleared(alarmStatus.isCleared());
existentAlarm.setAcknowledged(alarmStatus.isAck());
existentAlarm.setAckTs(alarmUpdateMsg.getAckTs()); existentAlarm.setAckTs(alarmUpdateMsg.getAckTs());
existentAlarm.setEndTs(alarmUpdateMsg.getEndTs()); existentAlarm.setEndTs(alarmUpdateMsg.getEndTs());
existentAlarm.setDetails(JacksonUtil.OBJECT_MAPPER.readTree(alarmUpdateMsg.getDetails())); existentAlarm.setDetails(JacksonUtil.OBJECT_MAPPER.readTree(alarmUpdateMsg.getDetails()));
@ -70,18 +72,18 @@ public abstract class BaseAlarmProcessor extends BaseEdgeProcessor {
break; break;
case ALARM_ACK_RPC_MESSAGE: case ALARM_ACK_RPC_MESSAGE:
if (existentAlarm != null) { if (existentAlarm != null) {
alarmService.ackAlarm(tenantId, existentAlarm.getId(), alarmUpdateMsg.getAckTs()); alarmService.acknowledgeAlarm(tenantId, existentAlarm.getId(), alarmUpdateMsg.getAckTs());
} }
break; break;
case ALARM_CLEAR_RPC_MESSAGE: case ALARM_CLEAR_RPC_MESSAGE:
if (existentAlarm != null) { if (existentAlarm != null) {
alarmService.clearAlarm(tenantId, existentAlarm.getId(), alarmService.clearAlarm(tenantId, existentAlarm.getId(),
JacksonUtil.OBJECT_MAPPER.readTree(alarmUpdateMsg.getDetails()), alarmUpdateMsg.getAckTs()); alarmUpdateMsg.getAckTs(), JacksonUtil.OBJECT_MAPPER.readTree(alarmUpdateMsg.getDetails()));
} }
break; break;
case ENTITY_DELETED_RPC_MESSAGE: case ENTITY_DELETED_RPC_MESSAGE:
if (existentAlarm != null) { if (existentAlarm != null) {
alarmService.deleteAlarm(tenantId, existentAlarm.getId()); alarmService.delAlarm(tenantId, existentAlarm.getId());
} }
break; break;
} }

2
application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java

@ -76,7 +76,7 @@ public abstract class AbstractTbEntityService {
protected ListenableFuture<Void> removeAlarmsByEntityId(TenantId tenantId, EntityId entityId) { protected ListenableFuture<Void> removeAlarmsByEntityId(TenantId tenantId, EntityId entityId) {
ListenableFuture<PageData<AlarmInfo>> alarmsFuture = ListenableFuture<PageData<AlarmInfo>> alarmsFuture =
alarmService.findAlarms(tenantId, new AlarmQuery(entityId, new TimePageLink(Integer.MAX_VALUE), null, null, false)); alarmService.findAlarms(tenantId, new AlarmQuery(entityId, new TimePageLink(Integer.MAX_VALUE), null, null, null, false));
ListenableFuture<List<AlarmId>> alarmIdsFuture = Futures.transform(alarmsFuture, page -> ListenableFuture<List<AlarmId>> alarmIdsFuture = Futures.transform(alarmsFuture, page ->
page.getData().stream().map(AlarmInfo::getId).collect(Collectors.toList()), dbExecutor); page.getData().stream().map(AlarmInfo::getId).collect(Collectors.toList()), dbExecutor);

4
application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java

@ -308,6 +308,10 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS
return EdgeEventActionType.ALARM_ACK; return EdgeEventActionType.ALARM_ACK;
case ALARM_CLEAR: case ALARM_CLEAR:
return EdgeEventActionType.ALARM_CLEAR; return EdgeEventActionType.ALARM_CLEAR;
case ALARM_ASSIGN:
return EdgeEventActionType.ALARM_ASSIGN;
case ALARM_UNASSIGN:
return EdgeEventActionType.ALARM_UNASSIGN;
case DELETED: case DELETED:
return EdgeEventActionType.DELETED; return EdgeEventActionType.DELETED;
case RELATION_ADD_OR_UPDATE: case RELATION_ADD_OR_UPDATE:

157
application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java

@ -15,22 +15,25 @@
*/ */
package org.thingsboard.server.service.entitiy.alarm; package org.thingsboard.server.service.entitiy.alarm;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmAssignee;
import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmCommentType; import org.thingsboard.server.common.data.alarm.AlarmCommentType;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest;
import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.dao.alarm.AlarmApiCallResult;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import java.util.List; import java.util.List;
@ -44,9 +47,34 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb
ActionType actionType = alarm.getId() == null ? ActionType.ADDED : ActionType.UPDATED; ActionType actionType = alarm.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = alarm.getTenantId(); TenantId tenantId = alarm.getTenantId();
try { try {
Alarm savedAlarm = checkNotNull(alarmSubscriptionService.createOrUpdateAlarm(alarm)); AlarmApiCallResult result;
notificationEntityService.notifyCreateOrUpdateAlarm(savedAlarm, actionType, user); if (alarm.getId() == null) {
return savedAlarm; result = alarmSubscriptionService.createAlarm(AlarmCreateOrUpdateActiveRequest.fromAlarm(alarm, user.getId()));
} else {
result = alarmSubscriptionService.updateAlarm(AlarmUpdateRequest.fromAlarm(alarm, user.getId()));
}
if (!result.isSuccessful()) {
throw new ThingsboardException(ThingsboardErrorCode.ITEM_NOT_FOUND);
}
actionType = result.isCreated() ? ActionType.ADDED : ActionType.UPDATED;
if (result.isModified()) {
notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), actionType, user);
}
AlarmInfo resultAlarm = result.getAlarm();
if (alarm.isAcknowledged() && !resultAlarm.isAcknowledged()) {
resultAlarm = ack(resultAlarm, alarm.getAckTs(), user);
}
if (alarm.isCleared() && !resultAlarm.isCleared()) {
resultAlarm = clear(resultAlarm, alarm.getClearTs(), user);
}
UserId newAssignee = alarm.getAssigneeId();
UserId curAssignee = resultAlarm.getAssigneeId();
if (newAssignee != null && !newAssignee.equals(curAssignee)) {
resultAlarm = assign(alarm, newAssignee, alarm.getAssignTs(), user);
} else if (newAssignee == null && curAssignee != null) {
resultAlarm = unassign(alarm, alarm.getAssignTs(), user);
}
return new Alarm(resultAlarm);
} catch (Exception e) { } catch (Exception e) {
notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ALARM), alarm, actionType, user, e); notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ALARM), alarm, actionType, user, e);
throw e; throw e;
@ -54,43 +82,110 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb
} }
@Override @Override
public ListenableFuture<Void> ack(Alarm alarm, User user) { public AlarmInfo ack(Alarm alarm, User user) throws ThingsboardException {
long ackTs = System.currentTimeMillis(); return ack(alarm, System.currentTimeMillis(), user);
ListenableFuture<Boolean> future = alarmSubscriptionService.ackAlarm(alarm.getTenantId(), alarm.getId(), ackTs); }
return Futures.transform(future, result -> {
@Override
public AlarmInfo ack(Alarm alarm, long ackTs, User user) throws ThingsboardException {
AlarmApiCallResult result = alarmSubscriptionService.acknowledgeAlarm(alarm.getTenantId(), alarm.getId(), getOrDefault(ackTs));
if (!result.isSuccessful()) {
throw new ThingsboardException(ThingsboardErrorCode.ITEM_NOT_FOUND);
}
if (result.isModified()) {
AlarmComment alarmComment = AlarmComment.builder() AlarmComment alarmComment = AlarmComment.builder()
.alarmId(alarm.getId()) .alarmId(alarm.getId())
.type(AlarmCommentType.SYSTEM) .type(AlarmCommentType.SYSTEM)
.comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was acknowledged by user %s", .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was acknowledged by user %s",
(user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName())) (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName()))
.put("userId", user.getId().toString())) .put("userId", user.getId().toString())
.put("subtype", "ACK"))
.build(); .build();
alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment); alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment);
alarm.setAckTs(ackTs); notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_ACK, user);
alarm.setStatus(alarm.getStatus().isCleared() ? AlarmStatus.CLEARED_ACK : AlarmStatus.ACTIVE_ACK); } else {
notificationEntityService.notifyCreateOrUpdateAlarm(alarm, ActionType.ALARM_ACK, user); throw new ThingsboardException("Alarm was already acknowledged!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
return null; }
}, MoreExecutors.directExecutor()); return result.getAlarm();
}
@Override
public AlarmInfo clear(Alarm alarm, User user) throws ThingsboardException {
return clear(alarm, System.currentTimeMillis(), user);
} }
@Override @Override
public ListenableFuture<Void> clear(Alarm alarm, User user) { public AlarmInfo clear(Alarm alarm, long clearTs, User user) throws ThingsboardException {
long clearTs = System.currentTimeMillis(); AlarmApiCallResult result = alarmSubscriptionService.clearAlarm(alarm.getTenantId(), alarm.getId(), getOrDefault(clearTs), null);
ListenableFuture<Boolean> future = alarmSubscriptionService.clearAlarm(alarm.getTenantId(), alarm.getId(), null, clearTs); if (!result.isSuccessful()) {
return Futures.transform(future, result -> { throw new ThingsboardException(ThingsboardErrorCode.ITEM_NOT_FOUND);
}
if (result.isCleared()) {
AlarmComment alarmComment = AlarmComment.builder() AlarmComment alarmComment = AlarmComment.builder()
.alarmId(alarm.getId()) .alarmId(alarm.getId())
.type(AlarmCommentType.SYSTEM) .type(AlarmCommentType.SYSTEM)
.comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was cleared by user %s", .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was cleared by user %s",
(user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName())) (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName()))
.put("userId", user.getId().toString())) .put("userId", user.getId().toString())
.put("subtype", "CLEAR"))
.build();
alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment);
notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_CLEAR, user);
} else {
throw new ThingsboardException("Alarm was already cleared!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
return result.getAlarm();
}
@Override
public AlarmInfo assign(Alarm alarm, UserId assigneeId, long assignTs, User user) throws ThingsboardException {
AlarmApiCallResult result = alarmSubscriptionService.assignAlarm(alarm.getTenantId(), alarm.getId(), assigneeId, getOrDefault(assignTs));
if (!result.isSuccessful()) {
throw new ThingsboardException(ThingsboardErrorCode.ITEM_NOT_FOUND);
}
AlarmInfo alarmInfo = result.getAlarm();
if (result.isModified()) {
AlarmAssignee assignee = alarmInfo.getAssignee();
AlarmComment alarmComment = AlarmComment.builder()
.alarmId(alarm.getId())
.type(AlarmCommentType.SYSTEM)
.comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was assigned by user %s to user %s",
(user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName(),
(assignee.getFirstName() == null || assignee.getLastName() == null) ? assignee.getEmail() : assignee.getFirstName() + " " + assignee.getLastName()))
.put("userId", user.getId().toString())
.put("assigneeId", assignee.getId().toString())
.put("subtype", "ASSIGN"))
.build();
alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment);
notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_ASSIGN, user);
} else {
throw new ThingsboardException("Alarm was already assigned to this user!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
return alarmInfo;
}
@Override
public AlarmInfo unassign(Alarm alarm, long unassignTs, User user) throws ThingsboardException {
AlarmApiCallResult result = alarmSubscriptionService.unassignAlarm(alarm.getTenantId(), alarm.getId(), getOrDefault(unassignTs));
if (!result.isSuccessful()) {
throw new ThingsboardException(ThingsboardErrorCode.ITEM_NOT_FOUND);
}
AlarmInfo alarmInfo = result.getAlarm();
if (result.isModified()) {
AlarmComment alarmComment = AlarmComment.builder()
.alarmId(alarm.getId())
.type(AlarmCommentType.SYSTEM)
.comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was unassigned by user %s",
(user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName()))
.put("userId", user.getId().toString())
.put("subtype", "ASSIGN"))
.build(); .build();
alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment); alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment);
alarm.setClearTs(clearTs); notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_UNASSIGN, user);
alarm.setStatus(alarm.getStatus().isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK); } else {
notificationEntityService.notifyCreateOrUpdateAlarm(alarm, ActionType.ALARM_CLEAR, user); throw new ThingsboardException("Alarm was already unassigned!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
return null; }
}, MoreExecutors.directExecutor()); return alarmInfo;
} }
@Override @Override
@ -101,4 +196,8 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb
relatedEdgeIds, user, JacksonUtil.toString(alarm)); relatedEdgeIds, user, JacksonUtil.toString(alarm));
return alarmSubscriptionService.deleteAlarm(tenantId, alarm.getId()); return alarmSubscriptionService.deleteAlarm(tenantId, alarm.getId());
} }
private static long getOrDefault(long ts) {
return ts > 0 ? ts : System.currentTimeMillis();
}
} }

15
application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java

@ -15,18 +15,27 @@
*/ */
package org.thingsboard.server.service.entitiy.alarm; package org.thingsboard.server.service.entitiy.alarm;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.UserId;
public interface TbAlarmService { public interface TbAlarmService {
Alarm save(Alarm entity, User user) throws ThingsboardException; Alarm save(Alarm entity, User user) throws ThingsboardException;
ListenableFuture<Void> ack(Alarm alarm, User user); AlarmInfo ack(Alarm alarm, User user) throws ThingsboardException;
ListenableFuture<Void> clear(Alarm alarm, User user); AlarmInfo ack(Alarm alarm, long ackTs, User user) throws ThingsboardException;
AlarmInfo clear(Alarm alarm, User user) throws ThingsboardException;
AlarmInfo clear(Alarm alarm, long clearTs, User user) throws ThingsboardException;
AlarmInfo assign(Alarm alarm, UserId assigneeId, long assignTs, User user) throws ThingsboardException;
AlarmInfo unassign(Alarm alarm, long unassignTs, User user) throws ThingsboardException;
Boolean delete(Alarm alarm, User user); Boolean delete(Alarm alarm, User user);
} }

2
application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java

@ -556,7 +556,7 @@ public class DefaultDataUpdateService implements DataUpdateService {
}; };
private void updateTenantAlarmsCustomer(TenantId tenantId, String name, AtomicLong processed) { private void updateTenantAlarmsCustomer(TenantId tenantId, String name, AtomicLong processed) {
AlarmQuery alarmQuery = new AlarmQuery(null, new TimePageLink(1000), null, null, false); AlarmQuery alarmQuery = new AlarmQuery(null, new TimePageLink(1000), null, null, null, false);
PageData<AlarmInfo> alarms = alarmDao.findAlarms(tenantId, alarmQuery); PageData<AlarmInfo> alarms = alarmDao.findAlarms(tenantId, alarmQuery);
boolean hasNext = true; boolean hasNext = true;
while (hasNext) { while (hasNext) {

7
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java

@ -26,6 +26,7 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.rpc.RpcError; import org.thingsboard.server.common.data.rpc.RpcError;
@ -35,6 +36,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.common.data.alarm.AlarmAssigneeUpdate;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
@ -502,13 +504,14 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
subscriptionManagerService.onAlarmUpdate( subscriptionManagerService.onAlarmUpdate(
TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())), TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())),
TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()), TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()),
JacksonUtil.fromString(proto.getAlarm(), Alarm.class), callback); JacksonUtil.fromString(proto.getAlarm(), AlarmInfo.class),
callback);
} else if (msg.hasAlarmDelete()) { } else if (msg.hasAlarmDelete()) {
TbAlarmDeleteProto proto = msg.getAlarmDelete(); TbAlarmDeleteProto proto = msg.getAlarmDelete();
subscriptionManagerService.onAlarmDeleted( subscriptionManagerService.onAlarmDeleted(
TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())), TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())),
TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()), TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()),
JacksonUtil.fromString(proto.getAlarm(), Alarm.class), callback); JacksonUtil.fromString(proto.getAlarm(), AlarmInfo.class), callback);
} else { } else {
throwNotHandled(msg, callback); throwNotHandled(msg, callback);
} }

3
application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java

@ -228,8 +228,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
if (userCredentials != null && isPositiveInteger(passwordPolicy.getPasswordReuseFrequencyDays())) { if (userCredentials != null && isPositiveInteger(passwordPolicy.getPasswordReuseFrequencyDays())) {
long passwordReuseFrequencyTs = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(passwordPolicy.getPasswordReuseFrequencyDays()); long passwordReuseFrequencyTs = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(passwordPolicy.getPasswordReuseFrequencyDays());
User user = userService.findUserById(tenantId, userCredentials.getUserId()); JsonNode additionalInfo = userCredentials.getAdditionalInfo();
JsonNode additionalInfo = user.getAdditionalInfo();
if (additionalInfo instanceof ObjectNode && additionalInfo.has(UserServiceImpl.USER_PASSWORD_HISTORY)) { if (additionalInfo instanceof ObjectNode && additionalInfo.has(UserServiceImpl.USER_PASSWORD_HISTORY)) {
JsonNode userPasswordHistoryJson = additionalInfo.get(UserServiceImpl.USER_PASSWORD_HISTORY); JsonNode userPasswordHistoryJson = additionalInfo.get(UserServiceImpl.USER_PASSWORD_HISTORY);
Map<String, String> userPasswordHistoryMap = JacksonUtil.convertValue(userPasswordHistoryJson, new TypeReference<>() {}); Map<String, String> userPasswordHistoryMap = JacksonUtil.convertValue(userPasswordHistoryJson, new TypeReference<>() {});

47
application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java

@ -26,6 +26,7 @@ import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -40,6 +41,7 @@ import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.data.alarm.AlarmAssigneeUpdate;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.gen.transport.TransportProtos.LocalSubscriptionServiceMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.LocalSubscriptionServiceMsgProto;
@ -268,7 +270,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
updateDeviceInactivityTimeout(tenantId, entityId, attributes); updateDeviceInactivityTimeout(tenantId, entityId, attributes);
} else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) { } else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) {
clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onUpdate(tenantId, clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onUpdate(tenantId,
new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, new ArrayList<>(attributes)) new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, new ArrayList<>(attributes))
, null); , null);
} }
} }
@ -292,7 +294,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
} }
@Override @Override
public void onAlarmUpdate(TenantId tenantId, EntityId entityId, Alarm alarm, TbCallback callback) { public void onAlarmUpdate(TenantId tenantId, EntityId entityId, AlarmInfo alarm, TbCallback callback) {
onLocalAlarmSubUpdate(entityId, onLocalAlarmSubUpdate(entityId,
s -> { s -> {
if (TbSubscriptionType.ALARMS.equals(s.getType())) { if (TbSubscriptionType.ALARMS.equals(s.getType())) {
@ -301,15 +303,14 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
return null; return null;
} }
}, },
s -> alarm.getCreatedTime() >= s.getTs(), s -> alarm.getCreatedTime() >= s.getTs() || alarm.getAssignTs() >= s.getTs(),
s -> alarm, alarm, false
false
); );
callback.onSuccess(); callback.onSuccess();
} }
@Override @Override
public void onAlarmDeleted(TenantId tenantId, EntityId entityId, Alarm alarm, TbCallback callback) { public void onAlarmDeleted(TenantId tenantId, EntityId entityId, AlarmInfo alarm, TbCallback callback) {
onLocalAlarmSubUpdate(entityId, onLocalAlarmSubUpdate(entityId,
s -> { s -> {
if (TbSubscriptionType.ALARMS.equals(s.getType())) { if (TbSubscriptionType.ALARMS.equals(s.getType())) {
@ -319,8 +320,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
} }
}, },
s -> alarm.getCreatedTime() >= s.getTs(), s -> alarm.getCreatedTime() >= s.getTs(),
s -> alarm, alarm, true
true
); );
callback.onSuccess(); callback.onSuccess();
} }
@ -354,7 +354,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
deleteDeviceInactivityTimeout(tenantId, entityId, keys); deleteDeviceInactivityTimeout(tenantId, entityId, keys);
} else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) { } else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) {
clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onDelete(tenantId, clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onDelete(tenantId,
new DeviceId(entityId.getId()), scope, keys), null); new DeviceId(entityId.getId()), scope, keys), null);
} }
} }
callback.onSuccess(); callback.onSuccess();
@ -414,19 +414,20 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
private void onLocalAlarmSubUpdate(EntityId entityId, private void onLocalAlarmSubUpdate(EntityId entityId,
Function<TbSubscription, TbAlarmsSubscription> castFunction, Function<TbSubscription, TbAlarmsSubscription> castFunction,
Predicate<TbAlarmsSubscription> filterFunction, Predicate<TbAlarmsSubscription> filterFunction,
Function<TbAlarmsSubscription, Alarm> processFunction, boolean deleted) { AlarmInfo alarm, boolean deleted) {
Set<TbSubscription> entitySubscriptions = subscriptionsByEntityId.get(entityId); Set<TbSubscription> entitySubscriptions = subscriptionsByEntityId.get(entityId);
if (alarm == null) {
log.warn("[{}] empty alarm update!", entityId);
return;
}
if (entitySubscriptions != null) { if (entitySubscriptions != null) {
entitySubscriptions.stream().map(castFunction).filter(Objects::nonNull).filter(filterFunction).forEach(s -> { entitySubscriptions.stream().map(castFunction).filter(Objects::nonNull).filter(filterFunction).forEach(s -> {
Alarm alarm = processFunction.apply(s); if (serviceId.equals(s.getServiceId())) {
if (alarm != null) { AlarmSubscriptionUpdate update = new AlarmSubscriptionUpdate(s.getSubscriptionId(), alarm, deleted);
if (serviceId.equals(s.getServiceId())) { localSubscriptionService.onSubscriptionUpdate(s.getSessionId(), update, TbCallback.EMPTY);
AlarmSubscriptionUpdate update = new AlarmSubscriptionUpdate(s.getSubscriptionId(), alarm, deleted); } else {
localSubscriptionService.onSubscriptionUpdate(s.getSessionId(), update, TbCallback.EMPTY); TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, s.getServiceId());
} else { toCoreNotificationsProducer.send(tpi, toProto(s, alarm, deleted), null);
TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, s.getServiceId());
toCoreNotificationsProducer.send(tpi, toProto(s, alarm, deleted), null);
}
} }
}); });
} else { } else {
@ -557,12 +558,12 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
}); });
ToCoreNotificationMsg toCoreMsg = ToCoreNotificationMsg.newBuilder().setToLocalSubscriptionServiceMsg( ToCoreNotificationMsg toCoreMsg = ToCoreNotificationMsg.newBuilder().setToLocalSubscriptionServiceMsg(
LocalSubscriptionServiceMsgProto.newBuilder().setSubUpdate(builder.build()).build()) LocalSubscriptionServiceMsgProto.newBuilder().setSubUpdate(builder.build()).build())
.build(); .build();
return new TbProtoQueueMsg<>(subscription.getEntityId().getId(), toCoreMsg); return new TbProtoQueueMsg<>(subscription.getEntityId().getId(), toCoreMsg);
} }
private TbProtoQueueMsg<ToCoreNotificationMsg> toProto(TbSubscription subscription, Alarm alarm, boolean deleted) { private TbProtoQueueMsg<ToCoreNotificationMsg> toProto(TbSubscription subscription, AlarmInfo alarm, boolean deleted) {
TbAlarmSubscriptionUpdateProto.Builder builder = TbAlarmSubscriptionUpdateProto.newBuilder(); TbAlarmSubscriptionUpdateProto.Builder builder = TbAlarmSubscriptionUpdateProto.newBuilder();
builder.setSessionId(subscription.getSessionId()); builder.setSessionId(subscription.getSessionId());
@ -571,8 +572,8 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
builder.setDeleted(deleted); builder.setDeleted(deleted);
ToCoreNotificationMsg toCoreMsg = ToCoreNotificationMsg.newBuilder().setToLocalSubscriptionServiceMsg( ToCoreNotificationMsg toCoreMsg = ToCoreNotificationMsg.newBuilder().setToLocalSubscriptionServiceMsg(
LocalSubscriptionServiceMsgProto.newBuilder() LocalSubscriptionServiceMsgProto.newBuilder()
.setAlarmSubUpdate(builder.build()).build()) .setAlarmSubUpdate(builder.build()).build())
.build(); .build();
return new TbProtoQueueMsg<>(subscription.getEntityId().getId(), toCoreMsg); return new TbProtoQueueMsg<>(subscription.getEntityId().getId(), toCoreMsg);
} }

6
application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java

@ -17,11 +17,13 @@ package org.thingsboard.server.service.subscription;
import org.springframework.context.ApplicationListener; import org.springframework.context.ApplicationListener;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.data.alarm.AlarmAssigneeUpdate;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import java.util.List; import java.util.List;
@ -42,9 +44,9 @@ public interface SubscriptionManagerService extends ApplicationListener<Partitio
void onTimeSeriesDelete(TenantId tenantId, EntityId entityId, List<String> keys, TbCallback callback); void onTimeSeriesDelete(TenantId tenantId, EntityId entityId, List<String> keys, TbCallback callback);
void onAlarmUpdate(TenantId tenantId, EntityId entityId, Alarm alarm, TbCallback callback); void onAlarmUpdate(TenantId tenantId, EntityId entityId, AlarmInfo alarm, TbCallback callback);
void onAlarmDeleted(TenantId tenantId, EntityId entityId, Alarm alarm, TbCallback callback); void onAlarmDeleted(TenantId tenantId, EntityId entityId, AlarmInfo alarm, TbCallback callback);
} }

27
application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java

@ -20,7 +20,9 @@ import lombok.Setter;
import lombok.ToString; import lombok.ToString;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmStatusFilter;
import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.Aggregation;
@ -63,10 +65,8 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx<AlarmDataQuery> {
private final AlarmService alarmService; private final AlarmService alarmService;
@Getter @Getter
@Setter
private final LinkedHashMap<EntityId, EntityData> entitiesMap; private final LinkedHashMap<EntityId, EntityData> entitiesMap;
@Getter @Getter
@Setter
private final HashMap<AlarmId, AlarmData> alarmsMap; private final HashMap<AlarmId, AlarmData> alarmsMap;
private final int maxEntitiesPerAlarmSubscription; private final int maxEntitiesPerAlarmSubscription;
@ -225,8 +225,7 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx<AlarmDataQuery> {
boolean matchesFilter = filter(alarm); boolean matchesFilter = filter(alarm);
if (onCurrentPage) { if (onCurrentPage) {
if (matchesFilter) { if (matchesFilter) {
AlarmData updated = new AlarmData(alarm, current.getOriginatorName(), current.getEntityId()); AlarmData updated = new AlarmData(subscriptionUpdate.getAlarm(), current);
updated.getLatest().putAll(current.getLatest());
alarmsMap.put(alarmId, updated); alarmsMap.put(alarmId, updated);
sendWsMsg(new AlarmDataUpdate(cmdId, null, Collections.singletonList(updated), maxEntitiesPerAlarmSubscription, data.getTotalElements())); sendWsMsg(new AlarmDataUpdate(cmdId, null, Collections.singletonList(updated), maxEntitiesPerAlarmSubscription, data.getTotalElements()));
} else { } else {
@ -270,8 +269,24 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx<AlarmDataQuery> {
if (filter.getStatusList() != null && !filter.getStatusList().isEmpty()) { if (filter.getStatusList() != null && !filter.getStatusList().isEmpty()) {
boolean matches = false; boolean matches = false;
for (AlarmSearchStatus status : filter.getStatusList()) { for (AlarmSearchStatus status : filter.getStatusList()) {
if (status.getStatuses().contains(alarm.getStatus())) { switch (status) {
matches = true; case ANY:
matches = true;
break;
case ACK:
matches = alarm.isAcknowledged();
break;
case UNACK:
matches = !alarm.isAcknowledged();
break;
case CLEARED:
matches = alarm.isCleared();
break;
case ACTIVE:
matches = !alarm.isCleared();
break;
}
if (matches) {
break; break;
} }
} }

10
application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java

@ -17,6 +17,8 @@ package org.thingsboard.server.service.subscription;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmAssigneeUpdate;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -189,8 +191,8 @@ public class TbSubscriptionUtils {
if (proto.getErrorCode() > 0) { if (proto.getErrorCode() > 0) {
return new AlarmSubscriptionUpdate(proto.getSubscriptionId(), SubscriptionErrorCode.forCode(proto.getErrorCode()), proto.getErrorMsg()); return new AlarmSubscriptionUpdate(proto.getSubscriptionId(), SubscriptionErrorCode.forCode(proto.getErrorCode()), proto.getErrorMsg());
} else { } else {
Alarm alarm = JacksonUtil.fromString(proto.getAlarm(), Alarm.class); AlarmInfo alarm = JacksonUtil.fromString(proto.getAlarm(), AlarmInfo.class);
return new AlarmSubscriptionUpdate(proto.getSubscriptionId(), alarm); return new AlarmSubscriptionUpdate(proto.getSubscriptionId(), alarm, proto.getDeleted());
} }
} }
@ -316,7 +318,7 @@ public class TbSubscriptionUtils {
return entry; return entry;
} }
public static ToCoreMsg toAlarmUpdateProto(TenantId tenantId, EntityId entityId, Alarm alarm) { public static ToCoreMsg toAlarmUpdateProto(TenantId tenantId, EntityId entityId, AlarmInfo alarm) {
TbAlarmUpdateProto.Builder builder = TbAlarmUpdateProto.newBuilder(); TbAlarmUpdateProto.Builder builder = TbAlarmUpdateProto.newBuilder();
builder.setEntityType(entityId.getEntityType().name()); builder.setEntityType(entityId.getEntityType().name());
builder.setEntityIdMSB(entityId.getId().getMostSignificantBits()); builder.setEntityIdMSB(entityId.getId().getMostSignificantBits());
@ -329,7 +331,7 @@ public class TbSubscriptionUtils {
return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build(); return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build();
} }
public static ToCoreMsg toAlarmDeletedProto(TenantId tenantId, EntityId entityId, Alarm alarm) { public static ToCoreMsg toAlarmDeletedProto(TenantId tenantId, EntityId entityId, AlarmInfo alarm) {
TbAlarmDeleteProto.Builder builder = TbAlarmDeleteProto.newBuilder(); TbAlarmDeleteProto.Builder builder = TbAlarmDeleteProto.newBuilder();
builder.setEntityType(entityId.getEntityType().name()); builder.setEntityType(entityId.getEntityType().name());
builder.setEntityIdMSB(entityId.getId().getMostSignificantBits()); builder.setEntityIdMSB(entityId.getId().getMostSignificantBits());

125
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java

@ -29,14 +29,18 @@ import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmCommentType; import org.thingsboard.server.common.data.alarm.AlarmCommentType;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmModificationRequest;
import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmQuery;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest;
import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest;
import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmData;
import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.query.AlarmDataQuery;
@ -44,6 +48,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.dao.alarm.AlarmApiCallResult;
import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.alarm.AlarmOperationResult; import org.thingsboard.server.dao.alarm.AlarmOperationResult;
import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.alarm.AlarmService;
@ -92,6 +97,41 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
return "alarm"; return "alarm";
} }
@Override
public AlarmApiCallResult createAlarm(AlarmCreateOrUpdateActiveRequest request) {
boolean creationEnabled = apiUsageStateService.getApiUsageState(request.getTenantId()).isAlarmCreationEnabled();
var result = alarmService.createAlarm(request, creationEnabled);
if (result.isCreated()) {
apiUsageClient.report(request.getTenantId(), null, ApiUsageRecordKey.CREATED_ALARMS_COUNT);
}
return withWsCallback(request, result);
}
@Override
public AlarmApiCallResult updateAlarm(AlarmUpdateRequest request) {
return withWsCallback(alarmService.updateAlarm(request));
}
@Override
public AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId alarmId, long ackTs) {
return withWsCallback(alarmService.acknowledgeAlarm(tenantId, alarmId, ackTs));
}
@Override
public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details) {
return withWsCallback(alarmService.clearAlarm(tenantId, alarmId, clearTs, details));
}
@Override
public AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long assignTs) {
return withWsCallback(alarmService.assignAlarm(tenantId, alarmId, assigneeId, assignTs));
}
@Override
public AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long assignTs) {
return withWsCallback(alarmService.unassignAlarm(tenantId, alarmId, assignTs));
}
@Override @Override
public Alarm createOrUpdateAlarm(Alarm alarm) { public Alarm createOrUpdateAlarm(Alarm alarm) {
AlarmOperationResult result = alarmService.createOrUpdateAlarm(alarm, apiUsageStateService.getApiUsageState(alarm.getTenantId()).isAlarmCreationEnabled()); AlarmOperationResult result = alarmService.createOrUpdateAlarm(alarm, apiUsageStateService.getApiUsageState(alarm.getTenantId()).isAlarmCreationEnabled());
@ -115,29 +155,29 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
@Override @Override
public Boolean deleteAlarm(TenantId tenantId, AlarmId alarmId) { public Boolean deleteAlarm(TenantId tenantId, AlarmId alarmId) {
AlarmOperationResult result = alarmService.deleteAlarm(tenantId, alarmId); AlarmApiCallResult result = alarmService.delAlarm(tenantId, alarmId);
onAlarmDeleted(result); onAlarmDeleted(result);
return result.isSuccessful(); return result.isSuccessful();
} }
@Override @Override
public ListenableFuture<Boolean> ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTs) { public ListenableFuture<Boolean> ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTs) {
ListenableFuture<AlarmOperationResult> result = alarmService.ackAlarm(tenantId, alarmId, ackTs); ListenableFuture<AlarmApiCallResult> result = Futures.immediateFuture(alarmService.acknowledgeAlarm(tenantId, alarmId, ackTs));
Futures.addCallback(result, new AlarmUpdateCallback(), wsCallBackExecutor); Futures.addCallback(result, new AlarmUpdateCallback(), wsCallBackExecutor);
return Futures.transform(result, AlarmOperationResult::isSuccessful, wsCallBackExecutor); return Futures.transform(result, AlarmApiCallResult::isSuccessful, wsCallBackExecutor);
} }
@Override @Override
public ListenableFuture<Boolean> clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs) { public ListenableFuture<Boolean> clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs) {
ListenableFuture<AlarmOperationResult> result = clearAlarmForResult(tenantId, alarmId, details, clearTs); AlarmApiCallResult result = alarmService.clearAlarm(tenantId, alarmId, clearTs, details);
return Futures.transform(result, AlarmOperationResult::isSuccessful, wsCallBackExecutor); return Futures.transform(Futures.immediateFuture(result), AlarmApiCallResult::isSuccessful, wsCallBackExecutor);
} }
@Override @Override
public ListenableFuture<AlarmOperationResult> clearAlarmForResult(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs) { public ListenableFuture<AlarmOperationResult> clearAlarmForResult(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs) {
ListenableFuture<AlarmOperationResult> result = alarmService.clearAlarm(tenantId, alarmId, details, clearTs); AlarmApiCallResult result = alarmService.clearAlarm(tenantId, alarmId, clearTs, details);
Futures.addCallback(result, new AlarmUpdateCallback(), wsCallBackExecutor); Futures.addCallback(Futures.immediateFuture(result), new AlarmUpdateCallback(), wsCallBackExecutor);
return result; return Futures.immediateFuture(new AlarmOperationResult(result));
} }
@Override @Override
@ -151,8 +191,8 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
} }
@Override @Override
public ListenableFuture<AlarmInfo> findAlarmInfoByIdAsync(TenantId tenantId, AlarmId alarmId) { public AlarmInfo findAlarmInfoById(TenantId tenantId, AlarmId alarmId) {
return alarmService.findAlarmInfoByIdAsync(tenantId, alarmId); return alarmService.findAlarmInfoById(tenantId, alarmId);
} }
@Override @Override
@ -166,8 +206,8 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
} }
@Override @Override
public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus) { public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, String assigneeId) {
return alarmService.findHighestAlarmSeverity(tenantId, entityId, alarmSearchStatus, alarmStatus); return alarmService.findHighestAlarmSeverity(tenantId, entityId, alarmSearchStatus, alarmStatus, assigneeId);
} }
@Override @Override
@ -175,15 +215,41 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
return alarmService.findAlarmDataByQueryForEntities(tenantId, query, orderedEntityIds); return alarmService.findAlarmDataByQueryForEntities(tenantId, query, orderedEntityIds);
} }
@Override
public Alarm findLatestActiveByOriginatorAndType(TenantId tenantId, EntityId originator, String type) {
return alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, type);
}
@Override @Override
public ListenableFuture<Alarm> findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type) { public ListenableFuture<Alarm> findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type) {
return alarmService.findLatestByOriginatorAndType(tenantId, originator, type); return alarmService.findLatestByOriginatorAndType(tenantId, originator, type);
} }
@Deprecated
private void onAlarmUpdated(AlarmOperationResult result) { private void onAlarmUpdated(AlarmOperationResult result) {
wsCallBackExecutor.submit(() -> { wsCallBackExecutor.submit(() -> {
Alarm alarm = result.getAlarm(); Alarm alarm = result.getAlarm();
TenantId tenantId = result.getAlarm().getTenantId(); TenantId tenantId = alarm.getTenantId();
for (EntityId entityId : result.getPropagatedEntitiesList()) {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId);
if (currentPartitions.contains(tpi)) {
if (subscriptionManagerService.isPresent()) {
subscriptionManagerService.get().onAlarmUpdate(tenantId, entityId, new AlarmInfo(alarm), TbCallback.EMPTY);
} else {
log.warn("Possible misconfiguration because subscriptionManagerService is null!");
}
} else {
TransportProtos.ToCoreMsg toCoreMsg = TbSubscriptionUtils.toAlarmUpdateProto(tenantId, entityId, new AlarmInfo(alarm));
clusterService.pushMsgToCore(tpi, entityId.getId(), toCoreMsg, null);
}
}
});
}
private void onAlarmUpdated(AlarmApiCallResult result) {
wsCallBackExecutor.submit(() -> {
AlarmInfo alarm = result.getAlarm();
TenantId tenantId = alarm.getTenantId();
for (EntityId entityId : result.getPropagatedEntitiesList()) { for (EntityId entityId : result.getPropagatedEntitiesList()) {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId); TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId);
if (currentPartitions.contains(tpi)) { if (currentPartitions.contains(tpi)) {
@ -200,10 +266,10 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
}); });
} }
private void onAlarmDeleted(AlarmOperationResult result) { private void onAlarmDeleted(AlarmApiCallResult result) {
wsCallBackExecutor.submit(() -> { wsCallBackExecutor.submit(() -> {
Alarm alarm = result.getAlarm(); AlarmInfo alarm = result.getAlarm();
TenantId tenantId = result.getAlarm().getTenantId(); TenantId tenantId = alarm.getTenantId();
for (EntityId entityId : result.getPropagatedEntitiesList()) { for (EntityId entityId : result.getPropagatedEntitiesList()) {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId); TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId);
if (currentPartitions.contains(tpi)) { if (currentPartitions.contains(tpi)) {
@ -220,9 +286,9 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
}); });
} }
private class AlarmUpdateCallback implements FutureCallback<AlarmOperationResult> { private class AlarmUpdateCallback implements FutureCallback<AlarmApiCallResult> {
@Override @Override
public void onSuccess(@Nullable AlarmOperationResult result) { public void onSuccess(@Nullable AlarmApiCallResult result) {
onAlarmUpdated(result); onAlarmUpdated(result);
} }
@ -232,4 +298,27 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
} }
} }
private AlarmApiCallResult withWsCallback(AlarmApiCallResult result) {
return withWsCallback(null, result);
}
private AlarmApiCallResult withWsCallback(AlarmModificationRequest request, AlarmApiCallResult result) {
if (result.isSuccessful() && result.isModified()) {
Futures.addCallback(Futures.immediateFuture(result), new AlarmUpdateCallback(), wsCallBackExecutor);
if (result.isSeverityChanged()) {
AlarmInfo alarm = result.getAlarm();
AlarmComment.AlarmCommentBuilder alarmComment = AlarmComment.builder()
.alarmId(alarm.getId())
.type(AlarmCommentType.SYSTEM)
.comment(JacksonUtil.newObjectNode().put("text",
String.format("Alarm severity was updated from %s to %s", result.getOldSeverity(), alarm.getSeverity())));
if (request != null && request.getUserId() != null) {
alarmComment.userId(request.getUserId());
}
alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment.build());
}
}
return result;
}
} }

101
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java

@ -16,7 +16,6 @@
package org.thingsboard.server.service.telemetry; package org.thingsboard.server.service.telemetry;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Function; import com.google.common.base.Function;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
@ -27,6 +26,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.CloseStatus;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
@ -95,6 +95,8 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -112,7 +114,6 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
private static final Aggregation DEFAULT_AGGREGATION = Aggregation.NONE; private static final Aggregation DEFAULT_AGGREGATION = Aggregation.NONE;
private static final int UNKNOWN_SUBSCRIPTION_ID = 0; private static final int UNKNOWN_SUBSCRIPTION_ID = 0;
private static final String PROCESSING_MSG = "[{}] Processing: {}"; private static final String PROCESSING_MSG = "[{}] Processing: {}";
private static final ObjectMapper jsonMapper = new ObjectMapper();
private static final String FAILED_TO_FETCH_DATA = "Failed to fetch data!"; private static final String FAILED_TO_FETCH_DATA = "Failed to fetch data!";
private static final String FAILED_TO_FETCH_ATTRIBUTES = "Failed to fetch attributes!"; private static final String FAILED_TO_FETCH_ATTRIBUTES = "Failed to fetch attributes!";
private static final String SESSION_META_DATA_NOT_FOUND = "Session meta-data not found!"; private static final String SESSION_META_DATA_NOT_FOUND = "Session meta-data not found!";
@ -147,10 +148,10 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
@Value("${server.ws.ping_timeout:30000}") @Value("${server.ws.ping_timeout:30000}")
private long pingTimeout; private long pingTimeout;
private ConcurrentMap<TenantId, Set<String>> tenantSubscriptionsMap = new ConcurrentHashMap<>(); private final ConcurrentMap<TenantId, Set<String>> tenantSubscriptionsMap = new ConcurrentHashMap<>();
private ConcurrentMap<CustomerId, Set<String>> customerSubscriptionsMap = new ConcurrentHashMap<>(); private final ConcurrentMap<CustomerId, Set<String>> customerSubscriptionsMap = new ConcurrentHashMap<>();
private ConcurrentMap<UserId, Set<String>> regularUserSubscriptionsMap = new ConcurrentHashMap<>(); private final ConcurrentMap<UserId, Set<String>> regularUserSubscriptionsMap = new ConcurrentHashMap<>();
private ConcurrentMap<UserId, Set<String>> publicUserSubscriptionsMap = new ConcurrentHashMap<>(); private final ConcurrentMap<UserId, Set<String>> publicUserSubscriptionsMap = new ConcurrentHashMap<>();
private ExecutorService executor; private ExecutorService executor;
private String serviceId; private String serviceId;
@ -204,7 +205,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
} }
try { try {
TelemetryPluginCmdsWrapper cmdsWrapper = jsonMapper.readValue(msg, TelemetryPluginCmdsWrapper.class); TelemetryPluginCmdsWrapper cmdsWrapper = JacksonUtil.OBJECT_MAPPER.readValue(msg, TelemetryPluginCmdsWrapper.class);
if (cmdsWrapper != null) { if (cmdsWrapper != null) {
if (cmdsWrapper.getAttrSubCmds() != null) { if (cmdsWrapper.getAttrSubCmds() != null) {
cmdsWrapper.getAttrSubCmds().forEach(cmd -> { cmdsWrapper.getAttrSubCmds().forEach(cmd -> {
@ -450,7 +451,6 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
@Override @Override
public void onSuccess(List<AttributeKvEntry> data) { public void onSuccess(List<AttributeKvEntry> data) {
List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList()); List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList());
sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData));
Map<String, Long> subState = new HashMap<>(keys.size()); Map<String, Long> subState = new HashMap<>(keys.size());
keys.forEach(key -> subState.put(key, 0L)); keys.forEach(key -> subState.put(key, 0L));
@ -458,6 +458,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
TbAttributeSubscriptionScope scope = StringUtils.isEmpty(cmd.getScope()) ? TbAttributeSubscriptionScope.ANY_SCOPE : TbAttributeSubscriptionScope.valueOf(cmd.getScope()); TbAttributeSubscriptionScope scope = StringUtils.isEmpty(cmd.getScope()) ? TbAttributeSubscriptionScope.ANY_SCOPE : TbAttributeSubscriptionScope.valueOf(cmd.getScope());
Lock subLock = new ReentrantLock();
TbAttributeSubscription sub = TbAttributeSubscription.builder() TbAttributeSubscription sub = TbAttributeSubscription.builder()
.serviceId(serviceId) .serviceId(serviceId)
.sessionId(sessionId) .sessionId(sessionId)
@ -467,9 +468,24 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
.allKeys(false) .allKeys(false)
.keyStates(subState) .keyStates(subState)
.scope(scope) .scope(scope)
.updateConsumer(DefaultTelemetryWebSocketService.this::sendWsMsg) .updateConsumer((sessionId, update) -> {
subLock.lock();
try {
sendWsMsg(sessionId, update);
} finally {
subLock.unlock();
}
})
.build(); .build();
oldSubService.addSubscription(sub);
subLock.lock();
try{
oldSubService.addSubscription(sub);
sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData));
} finally {
subLock.unlock();
}
} }
@Override @Override
@ -550,13 +566,13 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
@Override @Override
public void onSuccess(List<AttributeKvEntry> data) { public void onSuccess(List<AttributeKvEntry> data) {
List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList()); List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList());
sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData));
Map<String, Long> subState = new HashMap<>(attributesData.size()); Map<String, Long> subState = new HashMap<>(attributesData.size());
attributesData.forEach(v -> subState.put(v.getKey(), v.getTs())); attributesData.forEach(v -> subState.put(v.getKey(), v.getTs()));
TbAttributeSubscriptionScope scope = StringUtils.isEmpty(cmd.getScope()) ? TbAttributeSubscriptionScope.ANY_SCOPE : TbAttributeSubscriptionScope.valueOf(cmd.getScope()); TbAttributeSubscriptionScope scope = StringUtils.isEmpty(cmd.getScope()) ? TbAttributeSubscriptionScope.ANY_SCOPE : TbAttributeSubscriptionScope.valueOf(cmd.getScope());
Lock subLock = new ReentrantLock();
TbAttributeSubscription sub = TbAttributeSubscription.builder() TbAttributeSubscription sub = TbAttributeSubscription.builder()
.serviceId(serviceId) .serviceId(serviceId)
.sessionId(sessionId) .sessionId(sessionId)
@ -565,9 +581,24 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
.entityId(entityId) .entityId(entityId)
.allKeys(true) .allKeys(true)
.keyStates(subState) .keyStates(subState)
.updateConsumer(DefaultTelemetryWebSocketService.this::sendWsMsg) .updateConsumer((sessionId, update) -> {
.scope(scope).build(); subLock.lock();
oldSubService.addSubscription(sub); try {
sendWsMsg(sessionId, update);
} finally {
subLock.unlock();
}
})
.scope(scope)
.build();
subLock.lock();
try {
oldSubService.addSubscription(sub);
sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData));
} finally {
subLock.unlock();
}
} }
@Override @Override
@ -636,20 +667,34 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
FutureCallback<List<TsKvEntry>> callback = new FutureCallback<List<TsKvEntry>>() { FutureCallback<List<TsKvEntry>> callback = new FutureCallback<List<TsKvEntry>>() {
@Override @Override
public void onSuccess(List<TsKvEntry> data) { public void onSuccess(List<TsKvEntry> data) {
sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data));
Map<String, Long> subState = new HashMap<>(data.size()); Map<String, Long> subState = new HashMap<>(data.size());
data.forEach(v -> subState.put(v.getKey(), v.getTs())); data.forEach(v -> subState.put(v.getKey(), v.getTs()));
Lock subLock = new ReentrantLock();
TbTimeseriesSubscription sub = TbTimeseriesSubscription.builder() TbTimeseriesSubscription sub = TbTimeseriesSubscription.builder()
.serviceId(serviceId) .serviceId(serviceId)
.sessionId(sessionId) .sessionId(sessionId)
.subscriptionId(cmd.getCmdId()) .subscriptionId(cmd.getCmdId())
.tenantId(sessionRef.getSecurityCtx().getTenantId()) .tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityId) .entityId(entityId)
.updateConsumer(DefaultTelemetryWebSocketService.this::sendWsMsg) .updateConsumer((sessionId, update) -> {
subLock.lock();
try {
sendWsMsg(sessionId, update);
} finally {
subLock.unlock();
}
})
.allKeys(true) .allKeys(true)
.keyStates(subState).build(); .keyStates(subState).build();
oldSubService.addSubscription(sub);
subLock.lock();
try {
oldSubService.addSubscription(sub);
sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data));
} finally {
subLock.unlock();
}
} }
@Override @Override
@ -673,21 +718,35 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
return new FutureCallback<>() { return new FutureCallback<>() {
@Override @Override
public void onSuccess(List<TsKvEntry> data) { public void onSuccess(List<TsKvEntry> data) {
sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data));
Map<String, Long> subState = new HashMap<>(keys.size()); Map<String, Long> subState = new HashMap<>(keys.size());
keys.forEach(key -> subState.put(key, startTs)); keys.forEach(key -> subState.put(key, startTs));
data.forEach(v -> subState.put(v.getKey(), v.getTs())); data.forEach(v -> subState.put(v.getKey(), v.getTs()));
Lock subLock = new ReentrantLock();
TbTimeseriesSubscription sub = TbTimeseriesSubscription.builder() TbTimeseriesSubscription sub = TbTimeseriesSubscription.builder()
.serviceId(serviceId) .serviceId(serviceId)
.sessionId(sessionId) .sessionId(sessionId)
.subscriptionId(cmd.getCmdId()) .subscriptionId(cmd.getCmdId())
.tenantId(sessionRef.getSecurityCtx().getTenantId()) .tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityId) .entityId(entityId)
.updateConsumer(DefaultTelemetryWebSocketService.this::sendWsMsg) .updateConsumer((sessionId, update) -> {
subLock.lock();
try {
sendWsMsg(sessionId, update);
} finally {
subLock.unlock();
}
})
.allKeys(false) .allKeys(false)
.keyStates(subState).build(); .keyStates(subState).build();
oldSubService.addSubscription(sub);
subLock.lock();
try{
oldSubService.addSubscription(sub);
sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data));
} finally {
subLock.unlock();
}
} }
@Override @Override
@ -793,7 +852,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, int cmdId, Object update) { private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, int cmdId, Object update) {
try { try {
String msg = jsonMapper.writeValueAsString(update); String msg = JacksonUtil.OBJECT_MAPPER.writeValueAsString(update);
executor.submit(() -> { executor.submit(() -> {
try { try {
msgEndpoint.send(sessionRef, cmdId, msg); msgEndpoint.send(sessionRef, cmdId, msg);

23
application/src/main/java/org/thingsboard/server/service/telemetry/sub/AlarmSubscriptionUpdate.java

@ -17,15 +17,8 @@ package org.thingsboard.server.service.telemetry.sub;
import lombok.Getter; import lombok.Getter;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.alarm.AlarmAssigneeUpdate;
import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
public class AlarmSubscriptionUpdate { public class AlarmSubscriptionUpdate {
@ -36,15 +29,15 @@ public class AlarmSubscriptionUpdate {
@Getter @Getter
private String errorMsg; private String errorMsg;
@Getter @Getter
private Alarm alarm; private AlarmInfo alarm;
@Getter @Getter
private boolean alarmDeleted; private boolean alarmDeleted;
public AlarmSubscriptionUpdate(int subscriptionId, Alarm alarm) { public AlarmSubscriptionUpdate(int subscriptionId, AlarmInfo alarm) {
this(subscriptionId, alarm, false); this(subscriptionId, alarm, false);
} }
public AlarmSubscriptionUpdate(int subscriptionId, Alarm alarm, boolean alarmDeleted) { public AlarmSubscriptionUpdate(int subscriptionId, AlarmInfo alarm, boolean alarmDeleted) {
super(); super();
this.subscriptionId = subscriptionId; this.subscriptionId = subscriptionId;
this.alarm = alarm; this.alarm = alarm;
@ -64,7 +57,7 @@ public class AlarmSubscriptionUpdate {
@Override @Override
public String toString() { public String toString() {
return "AlarmUpdate [subscriptionId=" + subscriptionId + ", errorCode=" + errorCode + ", errorMsg=" + errorMsg + ", alarm=" return "AlarmUpdate [subscriptionId=" + subscriptionId + ", errorCode=" + errorCode + ", errorMsg=" + errorMsg +
+ alarm + "]"; ", alarm=" + alarm + "]";
} }
} }

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

@ -65,7 +65,6 @@ import org.thingsboard.server.common.msg.EncryptionUtil;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceProvisionService; import org.thingsboard.server.dao.device.DeviceProvisionService;
import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.DeviceService;
@ -95,6 +94,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceLwM2MC
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg;
import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.executors.DbCallbackExecutorService;
@ -290,6 +290,7 @@ public class DefaultTransportApiService implements TransportApiService {
device.setType(requestMsg.getDeviceType()); device.setType(requestMsg.getDeviceType());
device.setCustomerId(gateway.getCustomerId()); device.setCustomerId(gateway.getCustomerId());
DeviceProfile deviceProfile = deviceProfileCache.findOrCreateDeviceProfile(gateway.getTenantId(), requestMsg.getDeviceType()); DeviceProfile deviceProfile = deviceProfileCache.findOrCreateDeviceProfile(gateway.getTenantId(), requestMsg.getDeviceType());
device.setDeviceProfileId(deviceProfile.getId()); device.setDeviceProfileId(deviceProfile.getId());
ObjectNode additionalInfo = JacksonUtil.newObjectNode(); ObjectNode additionalInfo = JacksonUtil.newObjectNode();
additionalInfo.put(DataConstants.LAST_CONNECTED_GATEWAY, gatewayId.toString()); additionalInfo.put(DataConstants.LAST_CONNECTED_GATEWAY, gatewayId.toString());

1
application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java

@ -21,6 +21,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;

85
application/src/main/java/org/thingsboard/server/utils/EventDeduplicationExecutor.java

@ -1,85 +0,0 @@
/**
* Copyright © 2016-2023 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.utils;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.function.Consumer;
/**
* This class deduplicate executions of the specified function.
* Useful in cluster mode, when you get event about partition change multiple times.
* Assuming that the function execution is expensive, we should execute it immediately when first time event occurs and
* later, once the processing of first event is done, process last pending task.
*
* @param <P> parameters of the function
*/
@Slf4j
public class EventDeduplicationExecutor<P> {
private final String name;
private final ExecutorService executor;
private final Consumer<P> function;
private P pendingTask;
private boolean busy;
public EventDeduplicationExecutor(String name, ExecutorService executor, Consumer<P> function) {
this.name = name;
this.executor = executor;
this.function = function;
}
public void submit(P params) {
log.info("[{}] Going to submit: {}", name, params);
synchronized (EventDeduplicationExecutor.this) {
if (!busy) {
busy = true;
pendingTask = null;
try {
log.info("[{}] Submitting task: {}", name, params);
executor.submit(() -> {
try {
log.info("[{}] Executing task: {}", name, params);
function.accept(params);
} catch (Throwable e) {
log.warn("[{}] Failed to process task with parameters: {}", name, params, e);
throw e;
} finally {
unlockAndProcessIfAny();
}
});
} catch (Throwable e) {
log.warn("[{}] Failed to submit task with parameters: {}", name, params, e);
unlockAndProcessIfAny();
throw e;
}
} else {
log.info("[{}] Task is already in progress. {} pending task: {}", name, pendingTask == null ? "adding" : "updating", params);
pendingTask = params;
}
}
}
private void unlockAndProcessIfAny() {
synchronized (EventDeduplicationExecutor.this) {
busy = false;
if (pendingTask != null) {
submit(pendingTask);
}
}
}
}

4
application/src/main/resources/thingsboard.yml

@ -73,6 +73,8 @@ server:
min_timeout: "${MIN_SERVER_SIDE_RPC_TIMEOUT:5000}" min_timeout: "${MIN_SERVER_SIDE_RPC_TIMEOUT:5000}"
# Default value of the server side RPC timeout. # Default value of the server side RPC timeout.
default_timeout: "${DEFAULT_SERVER_SIDE_RPC_TIMEOUT:10000}" default_timeout: "${DEFAULT_SERVER_SIDE_RPC_TIMEOUT:10000}"
rate_limits:
reset_password_per_user: "${RESET_PASSWORD_PER_USER_RATE_LIMIT_CONFIGURATION:5:3600}"
# Application info # Application info
app: app:
@ -571,6 +573,7 @@ spring:
password: "${SPRING_DATASOURCE_PASSWORD:postgres}" password: "${SPRING_DATASOURCE_PASSWORD:postgres}"
hikari: hikari:
maximumPoolSize: "${SPRING_DATASOURCE_MAXIMUM_POOL_SIZE:16}" maximumPoolSize: "${SPRING_DATASOURCE_MAXIMUM_POOL_SIZE:16}"
registerMbeans: "${SPRING_DATASOURCE_HIKARI_REGISTER_MBEANS:false}" # true - enable MBean to diagnose pools state via JMX
# Audit log parameters # Audit log parameters
audit-log: audit-log:
@ -1209,3 +1212,4 @@ management:
exposure: exposure:
# Expose metrics endpoint (use value 'prometheus' to enable prometheus metrics). # Expose metrics endpoint (use value 'prometheus' to enable prometheus metrics).
include: '${METRICS_ENDPOINTS_EXPOSE:info}' include: '${METRICS_ENDPOINTS_EXPOSE:info}'

2
application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java

@ -52,7 +52,7 @@ public abstract class AbstractControllerTest extends AbstractNotifyEntityTest {
@LocalServerPort @LocalServerPort
protected int wsPort; protected int wsPort;
private TbTestWebSocketClient wsClient; // lazy private volatile TbTestWebSocketClient wsClient; // lazy
public TbTestWebSocketClient getWsClient() { public TbTestWebSocketClient getWsClient() {
if (wsClient == null) { if (wsClient == null) {

4
application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java

@ -18,7 +18,10 @@ package org.thingsboard.server.controller;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.mockito.ArgumentMatcher; import org.mockito.ArgumentMatcher;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.server.actors.service.ActorService;
import org.thingsboard.server.actors.service.DefaultActorService;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasName;
@ -38,6 +41,7 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg;
import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.service.session.DeviceSessionCacheService;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;

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

@ -26,17 +26,23 @@ import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jwt; import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.awaitility.Awaitility;
import org.hamcrest.Matcher; import org.hamcrest.Matcher;
import org.hibernate.exception.ConstraintViolationException; import org.hibernate.exception.ConstraintViolationException;
import org.junit.After; import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule; import org.junit.Rule;
import org.junit.rules.TestRule; import org.junit.rules.TestRule;
import org.junit.rules.TestWatcher; import org.junit.rules.TestWatcher;
import org.junit.runner.Description; import org.junit.runner.Description;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter;
@ -44,6 +50,7 @@ import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.mock.http.MockHttpInputMessage; import org.springframework.mock.http.MockHttpInputMessage;
import org.springframework.mock.http.MockHttpOutputMessage; import org.springframework.mock.http.MockHttpOutputMessage;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.ResultActions;
@ -52,6 +59,15 @@ import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilde
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.WebApplicationContext;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.actors.DefaultTbActorSystem;
import org.thingsboard.server.actors.TbActorId;
import org.thingsboard.server.actors.TbActorMailbox;
import org.thingsboard.server.actors.TbEntityActorId;
import org.thingsboard.server.actors.device.DeviceActor;
import org.thingsboard.server.actors.device.DeviceActorMessageProcessor;
import org.thingsboard.server.actors.device.SessionInfo;
import org.thingsboard.server.actors.service.DefaultActorService;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceProfileType;
@ -69,7 +85,9 @@ import org.thingsboard.server.common.data.device.profile.MqttTopics;
import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration;
import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -80,10 +98,11 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.msg.session.FeatureType;
import org.thingsboard.server.config.ThingsboardSecurityConfiguration; import org.thingsboard.server.config.ThingsboardSecurityConfiguration;
import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.service.mail.TestMailService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest;
import org.thingsboard.server.service.security.auth.rest.LoginRequest; import org.thingsboard.server.service.security.auth.rest.LoginRequest;
@ -95,9 +114,14 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
@ -144,6 +168,9 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected MockMvc mockMvc; protected MockMvc mockMvc;
protected String currentActivateToken;
protected String currentResetPasswordToken;
protected String token; protected String token;
protected String refreshToken; protected String refreshToken;
protected String username; protected String username;
@ -155,6 +182,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected TenantId differentTenantId; protected TenantId differentTenantId;
protected CustomerId differentCustomerId; protected CustomerId differentCustomerId;
protected UserId customerUserId; protected UserId customerUserId;
protected UserId differentCustomerUserId;
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
private HttpMessageConverter mappingJackson2HttpMessageConverter; private HttpMessageConverter mappingJackson2HttpMessageConverter;
@ -168,6 +196,15 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
@Autowired @Autowired
private TenantProfileService tenantProfileService; private TenantProfileService tenantProfileService;
@Autowired
public TimeseriesService tsService;
@Autowired
protected DefaultActorService actorService;
@SpyBean
protected MailService mailService;
@Rule @Rule
public TestRule watcher = new TestWatcher() { public TestRule watcher = new TestWatcher() {
protected void starting(Description description) { protected void starting(Description description) {
@ -198,7 +235,9 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
@Before @Before
public void setupWebTest() throws Exception { public void setupWebTest() throws Exception {
log.info("Executing web test setup"); log.debug("Executing web test setup");
setupMailServiceMock();
if (this.mockMvc == null) { if (this.mockMvc == null) {
this.mockMvc = webAppContextSetup(webApplicationContext) this.mockMvc = webAppContextSetup(webApplicationContext)
@ -238,12 +277,33 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
resetTokens(); resetTokens();
log.info("Executed web test setup"); log.debug("Executed web test setup");
}
private void setupMailServiceMock() throws ThingsboardException {
Mockito.doNothing().when(mailService).sendAccountActivatedEmail(anyString(), anyString());
Mockito.doAnswer(new Answer<Void>() {
public Void answer(InvocationOnMock invocation) {
Object[] args = invocation.getArguments();
String activationLink = (String) args[0];
currentActivateToken = activationLink.split("=")[1];
return null;
}
}).when(mailService).sendActivationEmail(anyString(), anyString());
Mockito.doAnswer(new Answer<Void>() {
public Void answer(InvocationOnMock invocation) {
Object[] args = invocation.getArguments();
String passwordResetLink = (String) args[0];
currentResetPasswordToken = passwordResetLink.split("=")[1];
return null;
}
}).when(mailService).sendResetPasswordEmailAsync(anyString(), anyString());
} }
@After @After
public void teardownWebTest() throws Exception { public void teardownWebTest() throws Exception {
log.info("Executing web test teardown"); log.debug("Executing web test teardown");
loginSysAdmin(); loginSysAdmin();
doDelete("/api/tenant/" + tenantId.getId().toString()) doDelete("/api/tenant/" + tenantId.getId().toString())
@ -325,7 +385,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
differentCustomerUser.setCustomerId(savedDifferentCustomer.getId()); differentCustomerUser.setCustomerId(savedDifferentCustomer.getId());
differentCustomerUser.setEmail(DIFFERENT_CUSTOMER_USER_EMAIL); differentCustomerUser.setEmail(DIFFERENT_CUSTOMER_USER_EMAIL);
createUserAndLogin(differentCustomerUser, DIFFERENT_CUSTOMER_USER_PASSWORD); differentCustomerUser = createUserAndLogin(differentCustomerUser, DIFFERENT_CUSTOMER_USER_PASSWORD);
differentCustomerUserId = differentCustomerUser.getId();
} }
} }
@ -368,11 +429,11 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
} }
private JsonNode getActivateRequest(String password) throws Exception { private JsonNode getActivateRequest(String password) throws Exception {
doGet("/api/noauth/activate?activateToken={activateToken}", TestMailService.currentActivateToken) doGet("/api/noauth/activate?activateToken={activateToken}", this.currentActivateToken)
.andExpect(status().isSeeOther()) .andExpect(status().isSeeOther())
.andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken)); .andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + this.currentActivateToken));
return new ObjectMapper().createObjectNode() return new ObjectMapper().createObjectNode()
.put("activateToken", TestMailService.currentActivateToken) .put("activateToken", this.currentActivateToken)
.put("password", password); .put("password", password);
} }
@ -792,4 +853,37 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
return (T) field.get(target); return (T) field.get(target);
} }
protected int getDeviceActorSubscriptionCount(DeviceId deviceId, FeatureType featureType) {
DeviceActorMessageProcessor processor = getDeviceActorProcessor(deviceId);
Map<UUID, SessionInfo> subscriptions = (Map<UUID, SessionInfo>) ReflectionTestUtils.getField(processor, getMapName(featureType));
return subscriptions.size();
}
protected void awaitForDeviceActorToReceiveSubscription(DeviceId deviceId, FeatureType featureType, int subscriptionCount) {
DeviceActorMessageProcessor processor = getDeviceActorProcessor(deviceId);
Map<UUID, SessionInfo> subscriptions = (Map<UUID, SessionInfo>) ReflectionTestUtils.getField(processor, getMapName(featureType));
Awaitility.await("Device actor received subscription command from the transport").atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> subscriptions.size() == subscriptionCount);
}
protected static String getMapName(FeatureType featureType) {
switch (featureType) {
case ATTRIBUTES:
return "attributeSubscriptions";
case RPC:
return "rpcSubscriptions";
default:
throw new RuntimeException("Not supported feature " + featureType + "!");
}
}
protected DeviceActorMessageProcessor getDeviceActorProcessor(DeviceId deviceId) {
DefaultTbActorSystem actorSystem = (DefaultTbActorSystem) ReflectionTestUtils.getField(actorService, "system");
ConcurrentMap<TbActorId, TbActorMailbox> actors = (ConcurrentMap<TbActorId, TbActorMailbox>) ReflectionTestUtils.getField(actorSystem, "actors");
Awaitility.await("Device actor was created").atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> actors.containsKey(new TbEntityActorId(deviceId)));
TbActorMailbox actorMailbox = actors.get(new TbEntityActorId(deviceId));
DeviceActor actor = (DeviceActor) ReflectionTestUtils.getField(actorMailbox, "actor");
return (DeviceActorMessageProcessor) ReflectionTestUtils.getField(actor, "processor");
}
} }

22
application/src/test/java/org/thingsboard/server/controller/BaseAdminControllerTest.java

@ -21,12 +21,9 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomStringUtils;
import org.junit.Test; import org.junit.Test;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.security.model.JwtSettings; import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.service.mail.DefaultMailService;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Base64; import java.util.Base64;
@ -35,6 +32,8 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.notNullValue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -43,12 +42,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
public abstract class BaseAdminControllerTest extends AbstractControllerTest { public abstract class BaseAdminControllerTest extends AbstractControllerTest {
final JwtSettings defaultJwtSettings = new JwtSettings(9000, 604800, "thingsboard.io", "thingsboardDefaultSigningKey"); final JwtSettings defaultJwtSettings = new JwtSettings(9000, 604800, "thingsboard.io", "thingsboardDefaultSigningKey");
@Autowired
MailService mailService;
@Autowired
DefaultMailService defaultMailService;
@Test @Test
public void testFindAdminSettingsByKey() throws Exception { public void testFindAdminSettingsByKey() throws Exception {
loginSysAdmin(); loginSysAdmin();
@ -118,10 +111,12 @@ public abstract class BaseAdminControllerTest extends AbstractControllerTest {
@Test @Test
public void testSendTestMail() throws Exception { public void testSendTestMail() throws Exception {
Mockito.doNothing().when(mailService).sendTestMail(any(), anyString());
loginSysAdmin(); loginSysAdmin();
AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class); AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class);
doPost("/api/admin/settings/testMail", adminSettings) doPost("/api/admin/settings/testMail", adminSettings)
.andExpect(status().isOk()); .andExpect(status().isOk());
Mockito.verify(mailService).sendTestMail(Mockito.any(), Mockito.anyString());
} }
@Test @Test
@ -137,15 +132,8 @@ public abstract class BaseAdminControllerTest extends AbstractControllerTest {
adminSettings.setJsonValue(objectNode); adminSettings.setJsonValue(objectNode);
Mockito.doAnswer((invocations) -> {
var jsonConfig = (JsonNode) invocations.getArgument(0);
var email = (String) invocations.getArgument(1);
defaultMailService.sendTestMail(jsonConfig, email);
return null;
}).when(mailService).sendTestMail(Mockito.any(), Mockito.anyString());
doPost("/api/admin/settings/testMail", adminSettings).andExpect(status().is5xxServerError()); doPost("/api/admin/settings/testMail", adminSettings).andExpect(status().is5xxServerError());
Mockito.doNothing().when(mailService).sendTestMail(Mockito.any(), Mockito.any()); Mockito.verify(mailService).sendTestMail(Mockito.any(), Mockito.anyString());
} }
void resetJwtSettingsToDefault() throws Exception { void resetJwtSettingsToDefault() throws Exception {

2
application/src/test/java/org/thingsboard/server/controller/BaseAlarmCommentControllerTest.java

@ -79,7 +79,6 @@ public abstract class BaseAlarmCommentControllerTest extends AbstractControllerT
.tenantId(tenantId) .tenantId(tenantId)
.customerId(customerId) .customerId(customerId)
.originator(customerDevice.getId()) .originator(customerDevice.getId())
.status(AlarmStatus.ACTIVE_UNACK)
.severity(AlarmSeverity.CRITICAL) .severity(AlarmSeverity.CRITICAL)
.type("test alarm type") .type("test alarm type")
.build(); .build();
@ -316,7 +315,6 @@ public abstract class BaseAlarmCommentControllerTest extends AbstractControllerT
Alarm alarm = Alarm.builder() Alarm alarm = Alarm.builder()
.originator(device.getId()) .originator(device.getId())
.status(AlarmStatus.ACTIVE_UNACK)
.severity(AlarmSeverity.CRITICAL) .severity(AlarmSeverity.CRITICAL)
.type("Test") .type("Test")
.build(); .build();

210
application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java

@ -29,6 +29,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.servlet.ResultActions;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
@ -124,7 +125,8 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
Assert.assertNotNull(updatedAlarm); Assert.assertNotNull(updatedAlarm);
Assert.assertEquals(AlarmSeverity.MAJOR, updatedAlarm.getSeverity()); Assert.assertEquals(AlarmSeverity.MAJOR, updatedAlarm.getSeverity());
testNotifyEntityAllOneTime(updatedAlarm, updatedAlarm.getId(), updatedAlarm.getOriginator(), AlarmInfo foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class);
testNotifyEntityAllOneTime(foundAlarm, updatedAlarm.getId(), updatedAlarm.getOriginator(),
tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.UPDATED); tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.UPDATED);
} }
@ -140,8 +142,57 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
Assert.assertNotNull(updatedAlarm); Assert.assertNotNull(updatedAlarm);
Assert.assertEquals(AlarmSeverity.MAJOR, updatedAlarm.getSeverity()); Assert.assertEquals(AlarmSeverity.MAJOR, updatedAlarm.getSeverity());
testNotifyEntityAllOneTime(updatedAlarm, updatedAlarm.getId(), updatedAlarm.getOriginator(), AlarmInfo foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class);
testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(),
tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.UPDATED); tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.UPDATED);
alarm = updatedAlarm;
alarm.setAcknowledged(true);
alarm.setAckTs(System.currentTimeMillis() - 1000);
updatedAlarm = doPost("/api/alarm", alarm, Alarm.class);
Assert.assertNotNull(updatedAlarm);
Assert.assertTrue(updatedAlarm.isAcknowledged());
Assert.assertEquals(alarm.getAckTs(), updatedAlarm.getAckTs());
foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class);
testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(),
tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ACK);
alarm = updatedAlarm;
alarm.setCleared(true);
alarm.setClearTs(System.currentTimeMillis() - 1000);
updatedAlarm = doPost("/api/alarm", alarm, Alarm.class);
Assert.assertNotNull(updatedAlarm);
Assert.assertTrue(updatedAlarm.isCleared());
Assert.assertEquals(alarm.getClearTs(), updatedAlarm.getClearTs());
foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class);
testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(),
tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_CLEAR);
alarm = updatedAlarm;
alarm.setAssigneeId(tenantAdminUserId);
alarm.setAssignTs(System.currentTimeMillis() - 1000);
updatedAlarm = doPost("/api/alarm", alarm, Alarm.class);
Assert.assertNotNull(updatedAlarm);
Assert.assertEquals(tenantAdminUserId, updatedAlarm.getAssigneeId());
Assert.assertEquals(alarm.getAssignTs(), updatedAlarm.getAssignTs());
foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class);
testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(),
tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN);
alarm = updatedAlarm;
alarm.setAssigneeId(null);
alarm.setAssignTs(System.currentTimeMillis() - 1000);
updatedAlarm = doPost("/api/alarm", alarm, Alarm.class);
Assert.assertNotNull(updatedAlarm);
Assert.assertNull(updatedAlarm.getAssigneeId());
Assert.assertEquals(alarm.getAssignTs(), updatedAlarm.getAssignTs());
foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class);
testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(),
tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_UNASSIGN);
} }
@Test @Test
@ -187,7 +238,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
doDelete("/api/alarm/" + alarm.getId()).andExpect(status().isOk()); doDelete("/api/alarm/" + alarm.getId()).andExpect(status().isOk());
testNotifyEntityOneTimeMsgToEdgeServiceNever(alarm, alarm.getId(), alarm.getOriginator(), testNotifyEntityOneTimeMsgToEdgeServiceNever(new Alarm(alarm), alarm.getId(), alarm.getOriginator(),
tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.DELETED); tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.DELETED);
} }
@ -200,7 +251,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
doDelete("/api/alarm/" + alarm.getId()).andExpect(status().isOk()); doDelete("/api/alarm/" + alarm.getId()).andExpect(status().isOk());
testNotifyEntityOneTimeMsgToEdgeServiceNever(alarm, alarm.getId(), alarm.getOriginator(), testNotifyEntityOneTimeMsgToEdgeServiceNever(new Alarm(alarm), alarm.getId(), alarm.getOriginator(),
tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.DELETED); tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.DELETED);
} }
@ -245,7 +296,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
doPost("/api/alarm/" + alarm.getId() + "/clear").andExpect(status().isOk()); doPost("/api/alarm/" + alarm.getId() + "/clear").andExpect(status().isOk());
Alarm foundAlarm = doGet("/api/alarm/" + alarm.getId(), Alarm.class); AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class);
Assert.assertNotNull(foundAlarm); Assert.assertNotNull(foundAlarm);
Assert.assertEquals(AlarmStatus.CLEARED_UNACK, foundAlarm.getStatus()); Assert.assertEquals(AlarmStatus.CLEARED_UNACK, foundAlarm.getStatus());
@ -261,7 +312,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
Mockito.reset(tbClusterService, auditLogService); Mockito.reset(tbClusterService, auditLogService);
doPost("/api/alarm/" + alarm.getId() + "/clear").andExpect(status().isOk()); doPost("/api/alarm/" + alarm.getId() + "/clear").andExpect(status().isOk());
Alarm foundAlarm = doGet("/api/alarm/" + alarm.getId(), Alarm.class); AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class);
Assert.assertNotNull(foundAlarm); Assert.assertNotNull(foundAlarm);
Assert.assertEquals(AlarmStatus.CLEARED_UNACK, foundAlarm.getStatus()); Assert.assertEquals(AlarmStatus.CLEARED_UNACK, foundAlarm.getStatus());
@ -278,7 +329,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
doPost("/api/alarm/" + alarm.getId() + "/ack").andExpect(status().isOk()); doPost("/api/alarm/" + alarm.getId() + "/ack").andExpect(status().isOk());
Alarm foundAlarm = doGet("/api/alarm/" + alarm.getId(), Alarm.class); AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class);
Assert.assertNotNull(foundAlarm); Assert.assertNotNull(foundAlarm);
Assert.assertEquals(AlarmStatus.ACTIVE_ACK, foundAlarm.getStatus()); Assert.assertEquals(AlarmStatus.ACTIVE_ACK, foundAlarm.getStatus());
@ -348,6 +399,135 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
.andExpect(statusReason(containsString(msgErrorPermission))); .andExpect(statusReason(containsString(msgErrorPermission)));
} }
@Test
public void testAssignAlarm() throws Exception {
loginTenantAdmin();
Alarm alarm = createAlarm(TEST_ALARM_TYPE);
Mockito.reset(tbClusterService, auditLogService);
long beforeAssignmentTs = System.currentTimeMillis();
Thread.sleep(2);
doPost("/api/alarm/" + alarm.getId() + "/assign/" + tenantAdminUserId.getId()).andExpect(status().isOk());
AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class);
Assert.assertNotNull(foundAlarm);
Assert.assertEquals(tenantAdminUserId, foundAlarm.getAssigneeId());
Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis());
testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(),
tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN);
}
@Test
public void testAssignAlarmViaDifferentTenant() throws Exception {
loginTenantAdmin();
Alarm alarm = createAlarm(TEST_ALARM_TYPE);
loginDifferentTenant();
Mockito.reset(tbClusterService, auditLogService);
doPost("/api/alarm/" + alarm.getId() + "/assign/" + tenantAdminUserId.getId()).andExpect(status().isForbidden());
}
@Test
public void testReassignAlarm() throws Exception {
loginTenantAdmin();
Alarm alarm = createAlarm(TEST_ALARM_TYPE);
Mockito.reset(tbClusterService, auditLogService);
long beforeAssignmentTs = System.currentTimeMillis();
Thread.sleep(2);
doPost("/api/alarm/" + alarm.getId() + "/assign/" + tenantAdminUserId.getId()).andExpect(status().isOk());
AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class);
Assert.assertNotNull(foundAlarm);
Assert.assertEquals(tenantAdminUserId, foundAlarm.getAssigneeId());
Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis());
testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(),
tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN);
logout();
loginCustomerUser();
Mockito.reset(tbClusterService, auditLogService);
beforeAssignmentTs = System.currentTimeMillis();
Thread.sleep(2);
doPost("/api/alarm/" + alarm.getId() + "/assign/" + customerUserId.getId()).andExpect(status().isOk());
foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class);
Assert.assertNotNull(foundAlarm);
Assert.assertEquals(customerUserId, foundAlarm.getAssigneeId());
Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis());
testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(),
tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_ASSIGN);
}
@Test
public void testUnassignAlarm() throws Exception {
loginTenantAdmin();
Alarm alarm = createAlarm(TEST_ALARM_TYPE);
Mockito.reset(tbClusterService, auditLogService);
long beforeAssignmentTs = System.currentTimeMillis();
Thread.sleep(2);
doPost("/api/alarm/" + alarm.getId() + "/assign/" + tenantAdminUserId.getId()).andExpect(status().isOk());
AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class);
Assert.assertNotNull(foundAlarm);
Assert.assertEquals(tenantAdminUserId, foundAlarm.getAssigneeId());
Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis());
testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(),
tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN);
beforeAssignmentTs = System.currentTimeMillis();
Thread.sleep(2);
doDelete("/api/alarm/" + alarm.getId() + "/assign").andExpect(status().isOk());
foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class);
Assert.assertNotNull(foundAlarm);
Assert.assertNull(foundAlarm.getAssigneeId());
Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis());
testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(),
tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_UNASSIGN);
}
@Test
public void testUnassignTenantAlarmViaCustomer() throws Exception {
loginTenantAdmin();
Alarm alarm = createAlarm(TEST_ALARM_TYPE);
Mockito.reset(tbClusterService, auditLogService);
long beforeAssignmentTs = System.currentTimeMillis();
Thread.sleep(2);
doPost("/api/alarm/" + alarm.getId() + "/assign/" + tenantAdminUserId.getId()).andExpect(status().isOk());
AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class);
Assert.assertNotNull(foundAlarm);
Assert.assertEquals(tenantAdminUserId, foundAlarm.getAssigneeId());
Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis());
testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(),
tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN);
logout();
loginCustomerUser();
Mockito.reset(tbClusterService, auditLogService);
beforeAssignmentTs = System.currentTimeMillis();
Thread.sleep(2);
doDelete("/api/alarm/" + alarm.getId() + "/assign").andExpect(status().isOk());
foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class);
Assert.assertNotNull(foundAlarm);
Assert.assertNull(foundAlarm.getAssigneeId());
Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis());
testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(),
tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_UNASSIGN);
}
@Test @Test
public void testFindAlarmsViaCustomerUser() throws Exception { public void testFindAlarmsViaCustomerUser() throws Exception {
loginCustomerUser(); loginCustomerUser();
@ -364,7 +544,8 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
var response = doGetTyped( var response = doGetTyped(
"/api/alarm/" + EntityType.DEVICE + "/" "/api/alarm/" + EntityType.DEVICE + "/"
+ customerDevice.getUuidId() + "?page=0&pageSize=" + size, + customerDevice.getUuidId() + "?page=0&pageSize=" + size,
new TypeReference<PageData<AlarmInfo>>() {} new TypeReference<PageData<AlarmInfo>>() {
}
); );
var foundAlarmInfos = response.getData(); var foundAlarmInfos = response.getData();
Assert.assertNotNull("Found pageData is null", foundAlarmInfos); Assert.assertNotNull("Found pageData is null", foundAlarmInfos);
@ -412,7 +593,6 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
Alarm alarm = Alarm.builder() Alarm alarm = Alarm.builder()
.originator(device.getId()) .originator(device.getId())
.status(AlarmStatus.ACTIVE_UNACK)
.severity(AlarmSeverity.CRITICAL) .severity(AlarmSeverity.CRITICAL)
.type("Test") .type("Test")
.build(); .build();
@ -431,7 +611,8 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
this.token = tokens.get("token").asText(); this.token = tokens.get("token").asText();
PageData<AlarmInfo> pageData = doGetTyped( PageData<AlarmInfo> pageData = doGetTyped(
"/api/alarm/DEVICE/" + device.getUuidId() + "?page=0&pageSize=1", new TypeReference<PageData<AlarmInfo>>() {} "/api/alarm/DEVICE/" + device.getUuidId() + "?page=0&pageSize=1", new TypeReference<PageData<AlarmInfo>>() {
}
); );
Assert.assertNotNull("Found pageData is null", pageData); Assert.assertNotNull("Found pageData is null", pageData);
@ -457,12 +638,11 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
testEntityDaoWithRelationsTransactionalException(alarmDao, customerDevice.getId(), alarmId, "/api/alarm/" + alarmId); testEntityDaoWithRelationsTransactionalException(alarmDao, customerDevice.getId(), alarmId, "/api/alarm/" + alarmId);
} }
private Alarm createAlarm(String type) throws Exception { private AlarmInfo createAlarm(String type) throws Exception {
Alarm alarm = Alarm.builder() Alarm alarm = Alarm.builder()
.tenantId(tenantId) .tenantId(tenantId)
.customerId(customerId) .customerId(customerId)
.originator(customerDevice.getId()) .originator(customerDevice.getId())
.status(AlarmStatus.ACTIVE_UNACK)
.severity(AlarmSeverity.CRITICAL) .severity(AlarmSeverity.CRITICAL)
.type(type) .type(type)
.build(); .build();
@ -470,6 +650,10 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
alarm = doPost("/api/alarm", alarm, Alarm.class); alarm = doPost("/api/alarm", alarm, Alarm.class);
Assert.assertNotNull(alarm); Assert.assertNotNull(alarm);
return alarm; AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class);
Assert.assertNotNull(foundAlarm);
Assert.assertEquals(alarm, new Alarm(foundAlarm));
return foundAlarm;
} }
} }

17
application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java

@ -44,7 +44,6 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.user.UserDao; import org.thingsboard.server.dao.user.UserDao;
import org.thingsboard.server.service.mail.TestMailService;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -54,6 +53,8 @@ import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -106,12 +107,12 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
Mockito.reset(tbClusterService, auditLogService); Mockito.reset(tbClusterService, auditLogService);
resetTokens(); resetTokens();
doGet("/api/noauth/activate?activateToken={activateToken}", TestMailService.currentActivateToken) doGet("/api/noauth/activate?activateToken={activateToken}", this.currentActivateToken)
.andExpect(status().isSeeOther()) .andExpect(status().isSeeOther())
.andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken)); .andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + this.currentActivateToken));
JsonNode activateRequest = new ObjectMapper().createObjectNode() JsonNode activateRequest = new ObjectMapper().createObjectNode()
.put("activateToken", TestMailService.currentActivateToken) .put("activateToken", this.currentActivateToken)
.put("password", "testPassword"); .put("password", "testPassword");
JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", activateRequest).andExpect(status().isOk()), JsonNode.class); JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", activateRequest).andExpect(status().isOk()), JsonNode.class);
@ -208,17 +209,19 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
doPost("/api/noauth/resetPasswordByEmail", resetPasswordByEmailRequest) doPost("/api/noauth/resetPasswordByEmail", resetPasswordByEmailRequest)
.andExpect(status().isOk()); .andExpect(status().isOk());
Thread.sleep(1000); Thread.sleep(1000);
doGet("/api/noauth/resetPassword?resetToken={resetToken}", TestMailService.currentResetPasswordToken) doGet("/api/noauth/resetPassword?resetToken={resetToken}", this.currentResetPasswordToken)
.andExpect(status().isSeeOther()) .andExpect(status().isSeeOther())
.andExpect(header().string(HttpHeaders.LOCATION, "/login/resetPassword?resetToken=" + TestMailService.currentResetPasswordToken)); .andExpect(header().string(HttpHeaders.LOCATION, "/login/resetPassword?resetToken=" + this.currentResetPasswordToken));
JsonNode resetPasswordRequest = new ObjectMapper().createObjectNode() JsonNode resetPasswordRequest = new ObjectMapper().createObjectNode()
.put("resetToken", TestMailService.currentResetPasswordToken) .put("resetToken", this.currentResetPasswordToken)
.put("password", "testPassword2"); .put("password", "testPassword2");
Mockito.doNothing().when(mailService).sendPasswordWasResetEmail(anyString(), anyString());
JsonNode tokenInfo = readResponse( JsonNode tokenInfo = readResponse(
doPost("/api/noauth/resetPassword", resetPasswordRequest) doPost("/api/noauth/resetPassword", resetPasswordRequest)
.andExpect(status().isOk()), JsonNode.class); .andExpect(status().isOk()), JsonNode.class);
Mockito.verify(mailService).sendPasswordWasResetEmail(anyString(), anyString());
validateAndSetJwtToken(tokenInfo, email); validateAndSetJwtToken(tokenInfo, email);
doGet("/api/auth/user") doGet("/api/auth/user")

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

@ -548,7 +548,7 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest {
SingleEntityFilter entityFilter = new SingleEntityFilter(); SingleEntityFilter entityFilter = new SingleEntityFilter();
entityFilter.setSingleEntity(tenantId); entityFilter.setSingleEntity(tenantId);
assertThatNoException().isThrownBy(() -> { assertThatNoException().as("subscribeForAttributes").isThrownBy(() -> {
JsonNode update = getWsClient().subscribeForAttributes(tenantId, TbAttributeSubscriptionScope.SERVER_SCOPE.name(), List.of("attr")); JsonNode update = getWsClient().subscribeForAttributes(tenantId, TbAttributeSubscriptionScope.SERVER_SCOPE.name(), List.of("attr"));
assertThat(update.get("errorMsg").isNull()).isTrue(); assertThat(update.get("errorMsg").isNull()).isTrue();
assertThat(update.get("errorCode").asInt()).isEqualTo(SubscriptionErrorCode.NO_ERROR.getCode()); assertThat(update.get("errorCode").asInt()).isEqualTo(SubscriptionErrorCode.NO_ERROR.getCode());
@ -560,7 +560,7 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest {
new BaseAttributeKvEntry(System.currentTimeMillis(), new StringDataEntry("attr", expectedAttrValue)) new BaseAttributeKvEntry(System.currentTimeMillis(), new StringDataEntry("attr", expectedAttrValue))
)); ));
JsonNode update = JacksonUtil.toJsonNode(getWsClient().waitForUpdate()); JsonNode update = JacksonUtil.toJsonNode(getWsClient().waitForUpdate());
assertThat(update).isNotNull(); assertThat(update).as("waitForUpdate").isNotNull();
assertThat(update.get("data").get("attr").get(0).get(1).asText()).isEqualTo(expectedAttrValue); assertThat(update.get("data").get("attr").get(0).get(1).asText()).isEqualTo(expectedAttrValue);
} }
@ -569,15 +569,17 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest {
tsService.saveAndNotify(device.getTenantId(), null, device.getId(), tsData, 0, new FutureCallback<Void>() { tsService.saveAndNotify(device.getTenantId(), null, device.getId(), tsData, 0, new FutureCallback<Void>() {
@Override @Override
public void onSuccess(@Nullable Void result) { public void onSuccess(@Nullable Void result) {
log.debug("sendTelemetry callback onSuccess");
latch.countDown(); latch.countDown();
} }
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
log.error("Failed to send telemetry", t);
latch.countDown(); latch.countDown();
} }
}); });
latch.await(3, TimeUnit.SECONDS); assertThat(latch.await(TIMEOUT, TimeUnit.SECONDS)).as("await sendTelemetry callback");
} }
private void sendAttributes(Device device, TbAttributeSubscriptionScope scope, List<AttributeKvEntry> attrData) throws InterruptedException { private void sendAttributes(Device device, TbAttributeSubscriptionScope scope, List<AttributeKvEntry> attrData) throws InterruptedException {
@ -589,14 +591,16 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest {
tsService.saveAndNotify(tenantId, entityId, scope.name(), attrData, new FutureCallback<Void>() { tsService.saveAndNotify(tenantId, entityId, scope.name(), attrData, new FutureCallback<Void>() {
@Override @Override
public void onSuccess(@Nullable Void result) { public void onSuccess(@Nullable Void result) {
log.debug("sendAttributes callback onSuccess");
latch.countDown(); latch.countDown();
} }
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
log.error("Failed to sendAttributes", t);
latch.countDown(); latch.countDown();
} }
}); });
latch.await(3, TimeUnit.SECONDS); assertThat(latch.await(TIMEOUT, TimeUnit.SECONDS)).as("await sendAttributes callback").isTrue();
} }
} }

2
application/src/test/java/org/thingsboard/server/controller/BaseWidgetsBundleControllerTest.java

@ -192,7 +192,7 @@ public abstract class BaseWidgetsBundleControllerTest extends AbstractController
WidgetsBundle savedWidgetsBundle = doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class); WidgetsBundle savedWidgetsBundle = doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class);
savedWidgetsBundle.setAlias("new_alias"); savedWidgetsBundle.setAlias("new_alias");
Mockito.reset(tbClusterService); Mockito.clearInvocations(tbClusterService);
doPost("/api/widgetsBundle", savedWidgetsBundle) doPost("/api/widgetsBundle", savedWidgetsBundle)
.andExpect(status().isBadRequest()) .andExpect(status().isBadRequest())

21
application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java

@ -47,6 +47,7 @@ import java.util.concurrent.TimeUnit;
@Slf4j @Slf4j
public class TbTestWebSocketClient extends WebSocketClient { public class TbTestWebSocketClient extends WebSocketClient {
private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(30);
private volatile String lastMsg; private volatile String lastMsg;
private volatile CountDownLatch reply; private volatile CountDownLatch reply;
private volatile CountDownLatch update; private volatile CountDownLatch update;
@ -87,12 +88,14 @@ public class TbTestWebSocketClient extends WebSocketClient {
} }
public void registerWaitForUpdate(int count) { public void registerWaitForUpdate(int count) {
log.debug("registerWaitForUpdate [{}]", count);
lastMsg = null; lastMsg = null;
update = new CountDownLatch(count); update = new CountDownLatch(count);
} }
@Override @Override
public void send(String text) throws NotYetConnectedException { public void send(String text) throws NotYetConnectedException {
log.debug("send [{}]", text);
reply = new CountDownLatch(1); reply = new CountDownLatch(1);
super.send(text); super.send(text);
} }
@ -110,21 +113,31 @@ public class TbTestWebSocketClient extends WebSocketClient {
} }
public String waitForUpdate() { public String waitForUpdate() {
return waitForUpdate(TimeUnit.SECONDS.toMillis(3)); return waitForUpdate(TIMEOUT);
} }
public String waitForUpdate(long ms) { public String waitForUpdate(long ms) {
log.debug("waitForUpdate [{}]", ms);
try { try {
update.await(ms, TimeUnit.MILLISECONDS); if (!update.await(ms, TimeUnit.MILLISECONDS)) {
log.warn("Failed to await update (waiting time [{}]ms elapsed)", ms, new RuntimeException("stacktrace"));
}
} catch (InterruptedException e) { } catch (InterruptedException e) {
log.warn("Failed to await reply", e); log.warn("Failed to await update", e);
} }
return lastMsg; return lastMsg;
} }
public String waitForReply() { public String waitForReply() {
return waitForReply(TIMEOUT);
}
public String waitForReply(long ms) {
log.debug("waitForReply [{}]", ms);
try { try {
reply.await(3, TimeUnit.SECONDS); if (!reply.await(ms, TimeUnit.MILLISECONDS)) {
log.warn("Failed to await reply (waiting time [{}]ms elapsed)", ms, new RuntimeException("stacktrace"));
}
} catch (InterruptedException e) { } catch (InterruptedException e) {
log.warn("Failed to await reply", e); log.warn("Failed to await reply", e);
} }

1
application/src/test/java/org/thingsboard/server/edge/BaseAlarmEdgeTest.java

@ -76,7 +76,6 @@ abstract public class BaseAlarmEdgeTest extends AbstractEdgeTest {
Device device = findDeviceByName("Edge Device 1"); Device device = findDeviceByName("Edge Device 1");
Alarm alarm = new Alarm(); Alarm alarm = new Alarm();
alarm.setOriginator(device.getId()); alarm.setOriginator(device.getId());
alarm.setStatus(AlarmStatus.ACTIVE_UNACK);
alarm.setType("alarm"); alarm.setType("alarm");
alarm.setSeverity(AlarmSeverity.CRITICAL); alarm.setSeverity(AlarmSeverity.CRITICAL);
edgeImitator.expectMessageAmount(1); edgeImitator.expectMessageAmount(1);

2
application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java

@ -49,6 +49,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.msg.session.FeatureType;
import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.common.transport.adaptor.JsonConverter;
import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg; import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg;
@ -668,6 +669,7 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest {
client.connectAndWait(deviceCredentials.getCredentialsId()); client.connectAndWait(deviceCredentials.getCredentialsId());
MqttTestCallback onUpdateCallback = new MqttTestCallback(); MqttTestCallback onUpdateCallback = new MqttTestCallback();
client.setCallback(onUpdateCallback); client.setCallback(onUpdateCallback);
client.subscribeAndWait("v1/devices/me/attributes", MqttQoS.AT_MOST_ONCE); client.subscribeAndWait("v1/devices/me/attributes", MqttQoS.AT_MOST_ONCE);
edgeImitator.expectResponsesAmount(1); edgeImitator.expectResponsesAmount(1);

30
application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java

@ -27,9 +27,10 @@ import org.springframework.test.context.junit4.SpringRunner;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.dao.alarm.AlarmApiCallResult;
import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.customer.CustomerService;
@ -81,36 +82,41 @@ public class DefaultTbAlarmServiceTest {
@Test @Test
public void testSave() throws ThingsboardException { public void testSave() throws ThingsboardException {
var alarm = new Alarm(); var alarm = new AlarmInfo();
when(alarmSubscriptionService.createOrUpdateAlarm(alarm)).thenReturn(alarm); when(alarmSubscriptionService.createAlarm(any())).thenReturn(AlarmApiCallResult.builder()
.successful(true)
.modified(true)
.alarm(alarm)
.build());
service.save(alarm, new User()); service.save(alarm, new User());
verify(notificationEntityService, times(1)).notifyCreateOrUpdateAlarm(any(), any(), any()); verify(notificationEntityService, times(1)).notifyCreateOrUpdateAlarm(any(), any(), any());
verify(alarmSubscriptionService, times(1)).createOrUpdateAlarm(eq(alarm)); verify(alarmSubscriptionService, times(1)).createAlarm(any());
} }
@Test @Test
public void testAck() { public void testAck() throws ThingsboardException {
var alarm = new Alarm(); var alarm = new Alarm();
alarm.setStatus(AlarmStatus.ACTIVE_UNACK); when(alarmSubscriptionService.acknowledgeAlarm(any(), any(), anyLong()))
when(alarmSubscriptionService.ackAlarm(any(), any(), anyLong())).thenReturn(Futures.immediateFuture(true)); .thenReturn(AlarmApiCallResult.builder().successful(true).modified(true).build());
service.ack(alarm, new User(new UserId(UUID.randomUUID()))); service.ack(alarm, new User(new UserId(UUID.randomUUID())));
verify(alarmCommentService, times(1)).createOrUpdateAlarmComment(any(), any()); verify(alarmCommentService, times(1)).createOrUpdateAlarmComment(any(), any());
verify(notificationEntityService, times(1)).notifyCreateOrUpdateAlarm(any(), any(), any()); verify(notificationEntityService, times(1)).notifyCreateOrUpdateAlarm(any(), any(), any());
verify(alarmSubscriptionService, times(1)).ackAlarm(any(), any(), anyLong()); verify(alarmSubscriptionService, times(1)).acknowledgeAlarm(any(), any(), anyLong());
} }
@Test @Test
public void testClear() { public void testClear() throws ThingsboardException {
var alarm = new Alarm(); var alarm = new Alarm();
alarm.setStatus(AlarmStatus.ACTIVE_ACK); alarm.setAcknowledged(true);
when(alarmSubscriptionService.clearAlarm(any(), any(), any(), anyLong())).thenReturn(Futures.immediateFuture(true)); when(alarmSubscriptionService.clearAlarm(any(), any(), anyLong(), any()))
.thenReturn(AlarmApiCallResult.builder().successful(true).cleared(true).build());
service.clear(alarm, new User(new UserId(UUID.randomUUID()))); service.clear(alarm, new User(new UserId(UUID.randomUUID())));
verify(alarmCommentService, times(1)).createOrUpdateAlarmComment(any(), any()); verify(alarmCommentService, times(1)).createOrUpdateAlarmComment(any(), any());
verify(notificationEntityService, times(1)).notifyCreateOrUpdateAlarm(any(), any(), any()); verify(notificationEntityService, times(1)).notifyCreateOrUpdateAlarm(any(), any(), any());
verify(alarmSubscriptionService, times(1)).clearAlarm(any(), any(), any(), anyLong()); verify(alarmSubscriptionService, times(1)).clearAlarm(any(), any(), anyLong(), any());
} }
@Test @Test

58
application/src/test/java/org/thingsboard/server/service/mail/TestMailService.java

@ -1,58 +0,0 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.mail;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@Profile("test")
@Configuration
public class TestMailService {
public static String currentActivateToken;
public static String currentResetPasswordToken;
@Bean
@Primary
public MailService mailService() throws ThingsboardException {
MailService mailService = Mockito.mock(MailService.class);
Mockito.doAnswer(new Answer<Void>() {
public Void answer(InvocationOnMock invocation) {
Object[] args = invocation.getArguments();
String activationLink = (String) args[0];
currentActivateToken = activationLink.split("=")[1];
return null;
}
}).when(mailService).sendActivationEmail(Mockito.anyString(), Mockito.anyString());
Mockito.doAnswer(new Answer<Void>() {
public Void answer(InvocationOnMock invocation) {
Object[] args = invocation.getArguments();
String passwordResetLink = (String) args[0];
currentResetPasswordToken = passwordResetLink.split("=")[1];
return null;
}
}).when(mailService).sendResetPasswordEmailAsync(Mockito.anyString(), Mockito.anyString());
return mailService;
}
}

19
application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java

@ -21,6 +21,7 @@ import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityInfo;
@ -237,7 +238,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
resourceService.delete(savedResource, null); resourceService.delete(savedResource, null);
} }
@Test(expected = DataValidationException.class) @Test
public void testSaveTbResourceWithExistsFileName() throws Exception { public void testSaveTbResourceWithExistsFileName() throws Exception {
TbResource resource = new TbResource(); TbResource resource = new TbResource();
resource.setTenantId(tenantId); resource.setTenantId(tenantId);
@ -256,23 +257,27 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
resource.setData("Test Data"); resource.setData("Test Data");
try { try {
resourceService.save(resource2); Assertions.assertThrows(DataValidationException.class, () -> {
resourceService.save(resource2);
});
} finally { } finally {
resourceService.delete(savedResource, null); resourceService.delete(savedResource, null);
} }
} }
@Test(expected = DataValidationException.class) @Test
public void testSaveTbResourceWithEmptyTitle() throws Exception { public void testSaveTbResourceWithEmptyTitle() throws Exception {
TbResource resource = new TbResource(); TbResource resource = new TbResource();
resource.setTenantId(tenantId); resource.setTenantId(tenantId);
resource.setResourceType(ResourceType.JKS); resource.setResourceType(ResourceType.JKS);
resource.setFileName(DEFAULT_FILE_NAME); resource.setFileName(DEFAULT_FILE_NAME);
resource.setData("Test Data"); resource.setData("Test Data");
resourceService.save(resource); Assertions.assertThrows(DataValidationException.class, () -> {
resourceService.save(resource);
});
} }
@Test(expected = DataValidationException.class) @Test
public void testSaveTbResourceWithInvalidTenant() throws Exception { public void testSaveTbResourceWithInvalidTenant() throws Exception {
TbResource resource = new TbResource(); TbResource resource = new TbResource();
resource.setTenantId(TenantId.fromUUID(Uuids.timeBased())); resource.setTenantId(TenantId.fromUUID(Uuids.timeBased()));
@ -280,7 +285,9 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
resource.setTitle("My resource"); resource.setTitle("My resource");
resource.setFileName(DEFAULT_FILE_NAME); resource.setFileName(DEFAULT_FILE_NAME);
resource.setData("Test Data"); resource.setData("Test Data");
resourceService.save(resource); Assertions.assertThrows(DataValidationException.class, () -> {
resourceService.save(resource);
});
} }
@Test @Test

1
application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java

@ -36,6 +36,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
@DaoSqlTest @DaoSqlTest
@TestPropertySource(properties = { @TestPropertySource(properties = {
"js.evaluator=local",
"js.max_script_body_size=50", "js.max_script_body_size=50",
"js.max_total_args_size=50", "js.max_total_args_size=50",
"js.max_result_size=50", "js.max_result_size=50",

2
application/src/test/java/org/thingsboard/server/transport/AbstractTransportIntegrationTest.java

@ -28,7 +28,7 @@ import java.util.concurrent.atomic.AtomicInteger;
@Slf4j @Slf4j
public abstract class AbstractTransportIntegrationTest extends AbstractControllerTest { public abstract class AbstractTransportIntegrationTest extends AbstractControllerTest {
protected static final int DEFAULT_WAIT_TIMEOUT_SECONDS = 10; protected static final int DEFAULT_WAIT_TIMEOUT_SECONDS = 30;
protected static final String MQTT_URL = "tcp://localhost:1883"; protected static final String MQTT_URL = "tcp://localhost:1883";
protected static final String COAP_BASE_URL = "coap://localhost:5683/api/v1/"; protected static final String COAP_BASE_URL = "coap://localhost:5683/api/v1/";

20
application/src/test/java/org/thingsboard/server/transport/TransportNoSqlTestSuite.java

@ -15,30 +15,14 @@
*/ */
package org.thingsboard.server.transport; package org.thingsboard.server.transport;
import org.cassandraunit.dataset.cql.ClassPathCQLDataSet;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.extensions.cpsuite.ClasspathSuite; import org.junit.extensions.cpsuite.ClasspathSuite;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.thingsboard.server.dao.CustomCassandraCQLUnit; import org.thingsboard.server.dao.AbstractNoSqlContainer;
import org.thingsboard.server.queue.memory.InMemoryStorage;
import java.util.Arrays;
@RunWith(ClasspathSuite.class) @RunWith(ClasspathSuite.class)
@ClasspathSuite.ClassnameFilters({ @ClasspathSuite.ClassnameFilters({
"org.thingsboard.server.transport.*.telemetry.timeseries.nosql.*Test", "org.thingsboard.server.transport.*.telemetry.timeseries.nosql.*Test",
}) })
public class TransportNoSqlTestSuite { public class TransportNoSqlTestSuite extends AbstractNoSqlContainer {
@ClassRule
public static CustomCassandraCQLUnit cassandraUnit =
new CustomCassandraCQLUnit(
Arrays.asList(
new ClassPathCQLDataSet("cassandra/schema-keyspace.cql", false, false),
new ClassPathCQLDataSet("cassandra/schema-ts.cql", false, false),
new ClassPathCQLDataSet("cassandra/schema-ts-latest.cql", false, false)
),
"cassandra-test.yaml", 30000l);
} }

2
application/src/test/java/org/thingsboard/server/transport/coap/AbstractCoapIntegrationTest.java

@ -45,6 +45,8 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
@TestPropertySource(properties = { @TestPropertySource(properties = {
"coap.enabled=true",
"service.integrations.supported=ALL",
"transport.coap.enabled=true", "transport.coap.enabled=true",
}) })
@Slf4j @Slf4j

30
application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java

@ -16,7 +16,9 @@
package org.thingsboard.server.transport.mqtt; package org.thingsboard.server.transport.mqtt;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import io.netty.handler.codec.mqtt.MqttQoS;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
@ -36,9 +38,12 @@ import org.thingsboard.server.common.data.device.profile.JsonTransportPayloadCon
import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration;
import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.msg.session.FeatureType;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.transport.AbstractTransportIntegrationTest; import org.thingsboard.server.transport.AbstractTransportIntegrationTest;
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestClient;
import java.util.List; import java.util.List;
@ -46,8 +51,8 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
@TestPropertySource(properties = { @TestPropertySource(properties = {
"service.integrations.supported=ALL",
"transport.mqtt.enabled=true", "transport.mqtt.enabled=true",
"js.evaluator=mock",
}) })
@Slf4j @Slf4j
public abstract class AbstractMqttIntegrationTest extends AbstractTransportIntegrationTest { public abstract class AbstractMqttIntegrationTest extends AbstractTransportIntegrationTest {
@ -103,6 +108,8 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg
if (StringUtils.hasLength(config.getAttributesTopicFilter())) { if (StringUtils.hasLength(config.getAttributesTopicFilter())) {
mqttDeviceProfileTransportConfiguration.setDeviceAttributesTopic(config.getAttributesTopicFilter()); mqttDeviceProfileTransportConfiguration.setDeviceAttributesTopic(config.getAttributesTopicFilter());
} }
mqttDeviceProfileTransportConfiguration.setSparkplug(config.isSparkplug());
mqttDeviceProfileTransportConfiguration.setSparkplugAttributesMetricNames(config.sparkplugAttributesMetricNames);
mqttDeviceProfileTransportConfiguration.setSendAckOnValidationException(config.isSendAckOnValidationException()); mqttDeviceProfileTransportConfiguration.setSendAckOnValidationException(config.isSendAckOnValidationException());
TransportPayloadTypeConfiguration transportPayloadTypeConfiguration; TransportPayloadTypeConfiguration transportPayloadTypeConfiguration;
if (TransportPayloadType.JSON.equals(transportPayloadType)) { if (TransportPayloadType.JSON.equals(transportPayloadType)) {
@ -176,4 +183,25 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg
builder.addAllKv(kvProtos); builder.addAllKv(kvProtos);
return builder.build(); return builder.build();
} }
protected void subscribeAndWait(MqttTestClient client, String attrSubTopic, DeviceId deviceId, FeatureType featureType) throws MqttException {
int subscriptionCount = getDeviceActorSubscriptionCount(deviceId, featureType);
client.subscribeAndWait(attrSubTopic, MqttQoS.AT_MOST_ONCE);
// TODO: This test awaits for the device actor to receive the subscription. Ideally it should not happen. See details below:
// The transport layer acknowledge subscription request once the message about subscription is in the queue.
// Test sends data immediately after acknowledgement.
// But there is a time lag between push to the queue and read from the queue in the tb-core component.
// Ideally, we should reply to device with SUBACK only when the device actor on the tb-core receives the message.
awaitForDeviceActorToReceiveSubscription(deviceId, featureType, subscriptionCount + 1);
}
protected void subscribeAndCheckSubscription(MqttTestClient client, String attrSubTopic, DeviceId deviceId, FeatureType featureType) throws MqttException {
client.subscribeAndWait(attrSubTopic, MqttQoS.AT_MOST_ONCE);
// TODO: This test awaits for the device actor to receive the subscription. Ideally it should not happen. See details below:
// The transport layer acknowledge subscription request once the message about subscription is in the queue.
// Test sends data immediately after acknowledgement.
// But there is a time lag between push to the queue and read from the queue in the tb-core component.
// Ideally, we should reply to device with SUBACK only when the device actor on the tb-core receives the message.
awaitForDeviceActorToReceiveSubscription(deviceId, featureType, 1);
}
} }

4
application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestConfigProperties.java

@ -20,12 +20,16 @@ import lombok.Data;
import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.TransportPayloadType;
import java.util.Set;
@Data @Data
@Builder @Builder
public class MqttTestConfigProperties { public class MqttTestConfigProperties {
String deviceName; String deviceName;
String gatewayName; String gatewayName;
boolean isSparkplug;
Set<String> sparkplugAttributesMetricNames;
TransportPayloadType transportPayloadType; TransportPayloadType transportPayloadType;

2
application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/MqttTestClient.java

@ -31,7 +31,7 @@ public class MqttTestClient {
private static final String MQTT_URL = "tcp://localhost:1883"; private static final String MQTT_URL = "tcp://localhost:1883";
private static final int TIMEOUT = 30; // seconds private static final int TIMEOUT = 30; // seconds
private static final long TIMEOUT_MS = TimeUnit.SECONDS.toMillis(TIMEOUT); public static final long TIMEOUT_MS = TimeUnit.SECONDS.toMillis(TIMEOUT);
private final MqttAsyncClient client; private final MqttAsyncClient client;

83
application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.transport.mqtt.mqttv3.attributes; package org.thingsboard.server.transport.mqtt.mqttv3.attributes;
import com.fasterxml.jackson.core.type.TypeReference;
import com.github.os72.protobuf.dynamic.DynamicSchema; import com.github.os72.protobuf.dynamic.DynamicSchema;
import com.google.protobuf.Descriptors; import com.google.protobuf.Descriptors;
import com.google.protobuf.DynamicMessage; import com.google.protobuf.DynamicMessage;
@ -22,6 +23,7 @@ import com.google.protobuf.InvalidProtocolBufferException;
import com.squareup.wire.schema.internal.parser.ProtoFileElement; import com.squareup.wire.schema.internal.parser.ProtoFileElement;
import io.netty.handler.codec.mqtt.MqttQoS; import io.netty.handler.codec.mqtt.MqttQoS;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.awaitility.Awaitility;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DynamicProtoUtils; import org.thingsboard.server.common.data.DynamicProtoUtils;
@ -30,12 +32,14 @@ import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportC
import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration;
import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.DeviceTypeFilter; import org.thingsboard.server.common.data.query.DeviceTypeFilter;
import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.SingleEntityFilter; import org.thingsboard.server.common.data.query.SingleEntityFilter;
import org.thingsboard.server.common.msg.session.FeatureType;
import org.thingsboard.server.gen.transport.TransportApiProtos; import org.thingsboard.server.gen.transport.TransportApiProtos;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate;
@ -45,6 +49,7 @@ import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestClient;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -118,21 +123,26 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
// subscribe to attributes updates from server methods // subscribe to attributes updates from server methods
protected void processJsonTestSubscribeToAttributesUpdates(String attrSubTopic) throws Exception { protected void processJsonTestSubscribeToAttributesUpdates(String attrSubTopic) throws Exception {
DeviceId deviceId = savedDevice.getId();
MqttTestClient client = new MqttTestClient(); MqttTestClient client = new MqttTestClient();
client.connectAndWait(accessToken); client.connectAndWait(accessToken);
MqttTestCallback onUpdateCallback = new MqttTestCallback(); MqttTestCallback onUpdateCallback = new MqttTestCallback();
client.setCallback(onUpdateCallback); client.setCallback(onUpdateCallback);
client.subscribeAndWait(attrSubTopic, MqttQoS.AT_MOST_ONCE);
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); subscribeAndWait(client, attrSubTopic, deviceId, FeatureType.ATTRIBUTES);
onUpdateCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
doPostAsync("/api/plugins/telemetry/DEVICE/" + deviceId.getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
assertThat(onUpdateCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.as("await onUpdateCallback").isTrue();
validateUpdateAttributesJsonResponse(onUpdateCallback, SHARED_ATTRIBUTES_PAYLOAD); validateUpdateAttributesJsonResponse(onUpdateCallback, SHARED_ATTRIBUTES_PAYLOAD);
MqttTestCallback onDeleteCallback = new MqttTestCallback(); MqttTestCallback onDeleteCallback = new MqttTestCallback();
client.setCallback(onDeleteCallback); client.setCallback(onDeleteCallback);
doDelete("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/SHARED_SCOPE?keys=sharedJson", String.class); doDelete("/api/plugins/telemetry/DEVICE/" + deviceId.getId() + "/SHARED_SCOPE?keys=sharedJson", String.class);
onDeleteCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); assertThat(onDeleteCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.as("await onDeleteCallback").isTrue();
validateUpdateAttributesJsonResponse(onDeleteCallback, SHARED_ATTRIBUTES_DELETED_RESPONSE); validateUpdateAttributesJsonResponse(onDeleteCallback, SHARED_ATTRIBUTES_DELETED_RESPONSE);
client.disconnect(); client.disconnect();
} }
@ -142,16 +152,18 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
client.connectAndWait(accessToken); client.connectAndWait(accessToken);
MqttTestCallback onUpdateCallback = new MqttTestCallback(); MqttTestCallback onUpdateCallback = new MqttTestCallback();
client.setCallback(onUpdateCallback); client.setCallback(onUpdateCallback);
client.subscribeAndWait(attrSubTopic, MqttQoS.AT_MOST_ONCE); subscribeAndWait(client, attrSubTopic, savedDevice.getId(), FeatureType.ATTRIBUTES);
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
onUpdateCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); assertThat(onUpdateCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.as("await onUpdateCallback").isTrue();
validateUpdateAttributesProtoResponse(onUpdateCallback); validateUpdateAttributesProtoResponse(onUpdateCallback);
MqttTestCallback onDeleteCallback = new MqttTestCallback(); MqttTestCallback onDeleteCallback = new MqttTestCallback();
client.setCallback(onDeleteCallback); client.setCallback(onDeleteCallback);
doDelete("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/SHARED_SCOPE?keys=sharedJson", String.class); doDelete("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/SHARED_SCOPE?keys=sharedJson", String.class);
onDeleteCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); assertThat(onDeleteCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.as("await onDeleteCallback").isTrue();
validateDeleteAttributesProtoResponse(onDeleteCallback); validateDeleteAttributesProtoResponse(onDeleteCallback);
client.disconnect(); client.disconnect();
} }
@ -162,7 +174,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
} }
protected void validateUpdateAttributesProtoResponse(MqttTestCallback callback) throws InvalidProtocolBufferException { protected void validateUpdateAttributesProtoResponse(MqttTestCallback callback) throws InvalidProtocolBufferException {
assertNotNull(callback.getPayloadBytes()); assertThat(callback.getPayloadBytes()).as("callback payload non-null").isNotNull();
TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder(); TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder();
List<TransportProtos.TsKvProto> tsKvProtoList = getTsKvProtoList("shared"); List<TransportProtos.TsKvProto> tsKvProtoList = getTsKvProtoList("shared");
attributeUpdateNotificationMsgBuilder.addAllSharedUpdated(tsKvProtoList); attributeUpdateNotificationMsgBuilder.addAllSharedUpdated(tsKvProtoList);
@ -178,7 +190,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
} }
protected void validateDeleteAttributesProtoResponse(MqttTestCallback callback) throws InvalidProtocolBufferException { protected void validateDeleteAttributesProtoResponse(MqttTestCallback callback) throws InvalidProtocolBufferException {
assertNotNull(callback.getPayloadBytes()); assertThat(callback.getPayloadBytes()).as("callback payload non-null").isNotNull();
TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder(); TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder();
attributeUpdateNotificationMsgBuilder.addSharedDeleted("sharedJson"); attributeUpdateNotificationMsgBuilder.addSharedDeleted("sharedJson");
@ -206,10 +218,11 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
assertNotNull(savedDevice); assertNotNull(savedDevice);
client.subscribeAndWait(GATEWAY_ATTRIBUTES_TOPIC, MqttQoS.AT_MOST_ONCE); subscribeAndCheckSubscription(client, GATEWAY_ATTRIBUTES_TOPIC, savedDevice.getId(), FeatureType.ATTRIBUTES);
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
onUpdateCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); assertThat(onUpdateCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.as("await onUpdateCallback").isTrue();
validateJsonGatewayUpdateAttributesResponse(onUpdateCallback, deviceName, SHARED_ATTRIBUTES_PAYLOAD); validateJsonGatewayUpdateAttributesResponse(onUpdateCallback, deviceName, SHARED_ATTRIBUTES_PAYLOAD);
@ -217,7 +230,8 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
client.setCallback(onDeleteCallback); client.setCallback(onDeleteCallback);
doDelete("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/SHARED_SCOPE?keys=sharedJson", String.class); doDelete("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/SHARED_SCOPE?keys=sharedJson", String.class);
onDeleteCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); assertThat(onDeleteCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.as("await onDeleteCallback").isTrue();
validateJsonGatewayUpdateAttributesResponse(onDeleteCallback, deviceName, SHARED_ATTRIBUTES_DELETED_RESPONSE); validateJsonGatewayUpdateAttributesResponse(onDeleteCallback, deviceName, SHARED_ATTRIBUTES_DELETED_RESPONSE);
client.disconnect(); client.disconnect();
@ -235,7 +249,8 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
20, 20,
100); 100);
assertNotNull(device); assertNotNull(device);
client.subscribeAndWait(GATEWAY_ATTRIBUTES_TOPIC, MqttQoS.AT_MOST_ONCE);
subscribeAndCheckSubscription(client, GATEWAY_ATTRIBUTES_TOPIC, device.getId(), FeatureType.ATTRIBUTES);
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
validateProtoGatewayUpdateAttributesResponse(onUpdateCallback, deviceName); validateProtoGatewayUpdateAttributesResponse(onUpdateCallback, deviceName);
MqttTestCallback onDeleteCallback = new MqttTestCallback(); MqttTestCallback onDeleteCallback = new MqttTestCallback();
@ -246,7 +261,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
} }
protected void validateJsonGatewayUpdateAttributesResponse(MqttTestCallback callback, String deviceName, String expectResultData) { protected void validateJsonGatewayUpdateAttributesResponse(MqttTestCallback callback, String deviceName, String expectResultData) {
assertNotNull(callback.getPayloadBytes()); assertThat(callback.getPayloadBytes()).as("callback payload non-null").isNotNull();
assertEquals(JacksonUtil.toJsonNode(getGatewayAttributesResponseJson(deviceName, expectResultData)), JacksonUtil.fromBytes(callback.getPayloadBytes())); assertEquals(JacksonUtil.toJsonNode(getGatewayAttributesResponseJson(deviceName, expectResultData)), JacksonUtil.fromBytes(callback.getPayloadBytes()));
} }
@ -260,8 +275,9 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
} }
protected void validateProtoGatewayUpdateAttributesResponse(MqttTestCallback callback, String deviceName) throws InvalidProtocolBufferException, InterruptedException { protected void validateProtoGatewayUpdateAttributesResponse(MqttTestCallback callback, String deviceName) throws InvalidProtocolBufferException, InterruptedException {
callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); assertThat(callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
assertNotNull(callback.getPayloadBytes()); .as("await callback").isTrue();
assertThat(callback.getPayloadBytes()).as("callback payload non-null").isNotNull();
TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder(); TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder();
List<TransportProtos.TsKvProto> tsKvProtoList = getTsKvProtoList("shared"); List<TransportProtos.TsKvProto> tsKvProtoList = getTsKvProtoList("shared");
@ -285,8 +301,9 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
} }
protected void validateProtoGatewayDeleteAttributesResponse(MqttTestCallback callback, String deviceName) throws InvalidProtocolBufferException, InterruptedException { protected void validateProtoGatewayDeleteAttributesResponse(MqttTestCallback callback, String deviceName) throws InvalidProtocolBufferException, InterruptedException {
callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); assertThat(callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
assertNotNull(callback.getPayloadBytes()); .as("await callback").isTrue();
assertThat(callback.getPayloadBytes()).as("callback payload non-null").isNotNull();
TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder(); TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder();
attributeUpdateNotificationMsgBuilder.addSharedDeleted("sharedJson"); attributeUpdateNotificationMsgBuilder.addSharedDeleted("sharedJson");
TransportProtos.AttributeUpdateNotificationMsg attributeUpdateNotificationMsg = attributeUpdateNotificationMsgBuilder.build(); TransportProtos.AttributeUpdateNotificationMsg attributeUpdateNotificationMsg = attributeUpdateNotificationMsgBuilder.build();
@ -391,9 +408,20 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
100); 100);
assertNotNull(device); assertNotNull(device);
String clientKeysStr = "clientStr,clientBool,clientDbl,clientLong,clientJson";
String attributeValuesUrl = "/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/attributes/CLIENT_SCOPE?keys=" + clientKeysStr;
Awaitility.await()
.atMost(10, TimeUnit.SECONDS)
.until(() -> {
List<Map<String, Object>> attributes = doGetAsyncTyped(attributeValuesUrl, new TypeReference<>() {
});
return attributes.size() == 5;
});
SingleEntityFilter dtf = new SingleEntityFilter(); SingleEntityFilter dtf = new SingleEntityFilter();
dtf.setSingleEntity(device.getId()); dtf.setSingleEntity(device.getId());
String clientKeysStr = "clientStr,clientBool,clientDbl,clientLong,clientJson";
String sharedKeysStr = "sharedStr,sharedBool,sharedDbl,sharedLong,sharedJson"; String sharedKeysStr = "sharedStr,sharedBool,sharedDbl,sharedLong,sharedJson";
List<String> clientKeysList = List.of(clientKeysStr.split(",")); List<String> clientKeysList = List.of(clientKeysStr.split(","));
List<String> sharedKeysList = List.of(sharedKeysStr.split(",")); List<String> sharedKeysList = List.of(sharedKeysStr.split(","));
@ -538,13 +566,15 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
} }
protected void validateJsonResponse(MqttTestCallback callback, String expectedResponse) throws InterruptedException { protected void validateJsonResponse(MqttTestCallback callback, String expectedResponse) throws InterruptedException {
callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); assertThat(callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.as("await callback").isTrue();
assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS());
assertEquals(JacksonUtil.toJsonNode(expectedResponse), JacksonUtil.fromBytes(callback.getPayloadBytes())); assertEquals(JacksonUtil.toJsonNode(expectedResponse), JacksonUtil.fromBytes(callback.getPayloadBytes()));
} }
protected void validateProtoResponse(MqttTestCallback callback, TransportProtos.GetAttributeResponseMsg expectedResponse) throws InterruptedException, InvalidProtocolBufferException { protected void validateProtoResponse(MqttTestCallback callback, TransportProtos.GetAttributeResponseMsg expectedResponse) throws InterruptedException, InvalidProtocolBufferException {
callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); assertThat(callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.as("await callback").isTrue();
assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS());
TransportProtos.GetAttributeResponseMsg actualAttributesResponse = TransportProtos.GetAttributeResponseMsg.parseFrom(callback.getPayloadBytes()); TransportProtos.GetAttributeResponseMsg actualAttributesResponse = TransportProtos.GetAttributeResponseMsg.parseFrom(callback.getPayloadBytes());
assertEquals(expectedResponse.getRequestId(), actualAttributesResponse.getRequestId()); assertEquals(expectedResponse.getRequestId(), actualAttributesResponse.getRequestId());
@ -567,14 +597,16 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
} }
protected void validateJsonResponseGateway(MqttTestCallback callback, String deviceName, String expectedValues) throws InterruptedException { protected void validateJsonResponseGateway(MqttTestCallback callback, String deviceName, String expectedValues) throws InterruptedException {
callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); assertThat(callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.as("await callback").isTrue();
assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS()); assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS());
String expectedRequestPayload = "{\"id\":1,\"device\":\"" + deviceName + "\",\"values\":" + expectedValues + "}"; String expectedRequestPayload = "{\"id\":1,\"device\":\"" + deviceName + "\",\"values\":" + expectedValues + "}";
assertEquals(JacksonUtil.toJsonNode(expectedRequestPayload), JacksonUtil.fromBytes(callback.getPayloadBytes())); assertEquals(JacksonUtil.toJsonNode(expectedRequestPayload), JacksonUtil.fromBytes(callback.getPayloadBytes()));
} }
protected void validateProtoClientResponseGateway(MqttTestCallback callback, String deviceName) throws InterruptedException, InvalidProtocolBufferException { protected void validateProtoClientResponseGateway(MqttTestCallback callback, String deviceName) throws InterruptedException, InvalidProtocolBufferException {
callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); assertThat(callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.as("await callback").isTrue();
assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS()); assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS());
TransportApiProtos.GatewayAttributeResponseMsg expectedGatewayAttributeResponseMsg = getExpectedGatewayAttributeResponseMsg(deviceName, true); TransportApiProtos.GatewayAttributeResponseMsg expectedGatewayAttributeResponseMsg = getExpectedGatewayAttributeResponseMsg(deviceName, true);
TransportApiProtos.GatewayAttributeResponseMsg actualGatewayAttributeResponseMsg = TransportApiProtos.GatewayAttributeResponseMsg.parseFrom(callback.getPayloadBytes()); TransportApiProtos.GatewayAttributeResponseMsg actualGatewayAttributeResponseMsg = TransportApiProtos.GatewayAttributeResponseMsg.parseFrom(callback.getPayloadBytes());
@ -590,7 +622,8 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
} }
protected void validateProtoSharedResponseGateway(MqttTestCallback callback, String deviceName) throws InterruptedException, InvalidProtocolBufferException { protected void validateProtoSharedResponseGateway(MqttTestCallback callback, String deviceName) throws InterruptedException, InvalidProtocolBufferException {
callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); assertThat(callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.as("await callback").isTrue();
assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS()); assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS());
TransportApiProtos.GatewayAttributeResponseMsg expectedGatewayAttributeResponseMsg = getExpectedGatewayAttributeResponseMsg(deviceName, false); TransportApiProtos.GatewayAttributeResponseMsg expectedGatewayAttributeResponseMsg = getExpectedGatewayAttributeResponseMsg(deviceName, false);
TransportApiProtos.GatewayAttributeResponseMsg actualGatewayAttributeResponseMsg = TransportApiProtos.GatewayAttributeResponseMsg.parseFrom(callback.getPayloadBytes()); TransportApiProtos.GatewayAttributeResponseMsg actualGatewayAttributeResponseMsg = TransportApiProtos.GatewayAttributeResponseMsg.parseFrom(callback.getPayloadBytes());

7
application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/credentials/BasicMqttCredentialsTest.java

@ -20,6 +20,7 @@ import org.eclipse.paho.client.mqttv3.MqttException;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
@ -115,7 +116,7 @@ public class BasicMqttCredentialsTest extends AbstractMqttIntegrationTest {
testTelemetryIsDelivered(accessToken2Device, mqttTestClient5); testTelemetryIsDelivered(accessToken2Device, mqttTestClient5);
} }
@Test(expected = MqttException.class) @Test
public void testCorrectClientIdAndUserNameButWrongPassword() throws Exception { public void testCorrectClientIdAndUserNameButWrongPassword() throws Exception {
// Not correct. Correct clientId and username, but wrong password // Not correct. Correct clientId and username, but wrong password
MqttTestClient mqttTestClient = new MqttTestClient(CLIENT_ID); MqttTestClient mqttTestClient = new MqttTestClient(CLIENT_ID);
@ -125,7 +126,9 @@ public class BasicMqttCredentialsTest extends AbstractMqttIntegrationTest {
} catch (MqttException e) { } catch (MqttException e) {
Assert.assertEquals(4, e.getReasonCode()); // 4 - Reason code for bad username or password in MQTT v3 Assert.assertEquals(4, e.getReasonCode()); // 4 - Reason code for bad username or password in MQTT v3
} }
testTelemetryIsNotDelivered(clientIdAndUserNameAndPasswordDevice3, mqttTestClient); Assertions.assertThrows(MqttException.class, () -> {
testTelemetryIsNotDelivered(clientIdAndUserNameAndPasswordDevice3, mqttTestClient);
});
} }
private void testTelemetryIsDelivered(Device device, MqttTestClient client) throws Exception { private void testTelemetryIsDelivered(Device device, MqttTestClient client) throws Exception {

15
application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/AbstractMqttServerSideRpcIntegrationTest.java

@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportC
import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration;
import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration;
import org.thingsboard.server.common.msg.session.FeatureType;
import org.thingsboard.server.gen.transport.TransportApiProtos; import org.thingsboard.server.gen.transport.TransportApiProtos;
import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest; import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest;
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestCallback; import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestCallback;
@ -82,7 +83,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
client.connectAndWait(accessToken); client.connectAndWait(accessToken);
MqttTestCallback callback = new MqttTestCallback(rpcSubTopic.replace("+", "0")); MqttTestCallback callback = new MqttTestCallback(rpcSubTopic.replace("+", "0"));
client.setCallback(callback); client.setCallback(callback);
client.subscribeAndWait(rpcSubTopic, MqttQoS.AT_MOST_ONCE); subscribeAndWait(client, rpcSubTopic, savedDevice.getId(), FeatureType.RPC);
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}"; String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}";
String result = doPostAsync("/api/rpc/oneway/" + savedDevice.getId(), setGpioRequest, String.class, status().isOk()); String result = doPostAsync("/api/rpc/oneway/" + savedDevice.getId(), setGpioRequest, String.class, status().isOk());
@ -119,7 +120,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
protected void processJsonTwoWayRpcTest(String rpcSubTopic) throws Exception { protected void processJsonTwoWayRpcTest(String rpcSubTopic) throws Exception {
MqttTestClient client = new MqttTestClient(); MqttTestClient client = new MqttTestClient();
client.connectAndWait(accessToken); client.connectAndWait(accessToken);
client.subscribeAndWait(rpcSubTopic, MqttQoS.AT_LEAST_ONCE); subscribeAndWait(client, rpcSubTopic, savedDevice.getId(), FeatureType.RPC);
MqttTestRpcJsonCallback callback = new MqttTestRpcJsonCallback(client, rpcSubTopic.replace("+", "0")); MqttTestRpcJsonCallback callback = new MqttTestRpcJsonCallback(client, rpcSubTopic.replace("+", "0"));
client.setCallback(callback); client.setCallback(callback);
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"26\",\"value\": 1}}"; String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"26\",\"value\": 1}}";
@ -133,7 +134,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
protected void processProtoTwoWayRpcTest(String rpcSubTopic) throws Exception { protected void processProtoTwoWayRpcTest(String rpcSubTopic) throws Exception {
MqttTestClient client = new MqttTestClient(); MqttTestClient client = new MqttTestClient();
client.connectAndWait(accessToken); client.connectAndWait(accessToken);
client.subscribeAndWait(rpcSubTopic, MqttQoS.AT_LEAST_ONCE); subscribeAndWait(client, rpcSubTopic, savedDevice.getId(), FeatureType.RPC);
MqttTestRpcProtoCallback callback = new MqttTestRpcProtoCallback(client, rpcSubTopic.replace("+", "0")); MqttTestRpcProtoCallback callback = new MqttTestRpcProtoCallback(client, rpcSubTopic.replace("+", "0"));
client.setCallback(callback); client.setCallback(callback);
@ -194,7 +195,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
client.enableManualAcks(); client.enableManualAcks();
MqttTestSequenceCallback callback = new MqttTestSequenceCallback(client, 10, result); MqttTestSequenceCallback callback = new MqttTestSequenceCallback(client, 10, result);
client.setCallback(callback); client.setCallback(callback);
client.subscribeAndWait(DEVICE_RPC_REQUESTS_SUB_TOPIC, MqttQoS.AT_LEAST_ONCE); subscribeAndWait(client, DEVICE_RPC_REQUESTS_SUB_TOPIC, savedDevice.getId(), FeatureType.RPC);
callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
assertEquals(expected, result); assertEquals(expected, result);
@ -223,6 +224,8 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
MqttTestCallback callback = new MqttTestCallback(GATEWAY_RPC_TOPIC); MqttTestCallback callback = new MqttTestCallback(GATEWAY_RPC_TOPIC);
client.setCallback(callback); client.setCallback(callback);
client.subscribeAndWait(GATEWAY_RPC_TOPIC, MqttQoS.AT_MOST_ONCE); client.subscribeAndWait(GATEWAY_RPC_TOPIC, MqttQoS.AT_MOST_ONCE);
subscribeAndCheckSubscription(client, GATEWAY_RPC_TOPIC, savedDevice.getId(), FeatureType.RPC);
String setGpioRequest = "{\"method\": \"toggle_gpio\", \"params\": {\"pin\":1}}"; String setGpioRequest = "{\"method\": \"toggle_gpio\", \"params\": {\"pin\":1}}";
String deviceId = savedDevice.getId().getId().toString(); String deviceId = savedDevice.getId().getId().toString();
String result = doPostAsync("/api/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().isOk()); String result = doPostAsync("/api/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().isOk());
@ -269,7 +272,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
MqttTestRpcJsonCallback callback = new MqttTestRpcJsonCallback(client, GATEWAY_RPC_TOPIC); MqttTestRpcJsonCallback callback = new MqttTestRpcJsonCallback(client, GATEWAY_RPC_TOPIC);
client.setCallback(callback); client.setCallback(callback);
client.subscribeAndWait(GATEWAY_RPC_TOPIC, MqttQoS.AT_MOST_ONCE); subscribeAndCheckSubscription(client, GATEWAY_RPC_TOPIC, savedDevice.getId(), FeatureType.RPC);
String setGpioRequest = "{\"method\": \"toggle_gpio\", \"params\": {\"pin\":1}}"; String setGpioRequest = "{\"method\": \"toggle_gpio\", \"params\": {\"pin\":1}}";
String deviceId = savedDevice.getId().getId().toString(); String deviceId = savedDevice.getId().getId().toString();
@ -292,7 +295,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
MqttTestRpcProtoCallback callback = new MqttTestRpcProtoCallback(client, GATEWAY_RPC_TOPIC); MqttTestRpcProtoCallback callback = new MqttTestRpcProtoCallback(client, GATEWAY_RPC_TOPIC);
client.setCallback(callback); client.setCallback(callback);
client.subscribeAndWait(GATEWAY_RPC_TOPIC, MqttQoS.AT_MOST_ONCE); subscribeAndCheckSubscription(client, GATEWAY_RPC_TOPIC, savedDevice.getId(), FeatureType.RPC);
String setGpioRequest = "{\"method\": \"toggle_gpio\", \"params\": {\"pin\":1}}"; String setGpioRequest = "{\"method\": \"toggle_gpio\", \"params\": {\"pin\":1}}";
String deviceId = savedDevice.getId().getId().toString(); String deviceId = savedDevice.getId().getId().toString();

4
application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/MqttV5TestClient.java

@ -89,7 +89,9 @@ public class MqttV5TestClient { // We should copy part of MqttV3TestClient, due
if (client == null) { if (client == null) {
throw new RuntimeException("Failed to connect! MqttAsyncClient is not initialized!"); throw new RuntimeException("Failed to connect! MqttAsyncClient is not initialized!");
} }
return client.connect(options); IMqttToken connect = client.connect(options);
connect.waitForCompletion(TIMEOUT_MS);
return connect;
} }
public void disconnectAndWait() throws MqttException { public void disconnectAndWait() throws MqttException {

462
application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java

@ -0,0 +1,462 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.mqtt.sparkplug;
import com.fasterxml.jackson.databind.node.ArrayNode;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.mqttv5.client.IMqttToken;
import org.eclipse.paho.mqttv5.client.MqttCallback;
import org.eclipse.paho.mqttv5.client.MqttConnectionOptions;
import org.eclipse.paho.mqttv5.client.MqttDisconnectResponse;
import org.eclipse.paho.mqttv5.common.MqttException;
import org.eclipse.paho.mqttv5.common.MqttMessage;
import org.eclipse.paho.mqttv5.common.packet.MqttConnAck;
import org.eclipse.paho.mqttv5.common.packet.MqttProperties;
import org.eclipse.paho.mqttv5.common.packet.MqttReturnCode;
import org.eclipse.paho.mqttv5.common.packet.MqttWireMessage;
import org.junit.Assert;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.TransportPayloadType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto;
import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest;
import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties;
import org.thingsboard.server.transport.mqtt.mqttv5.MqttV5TestClient;
import org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType;
import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import static org.awaitility.Awaitility.await;
import static org.eclipse.paho.mqttv5.common.packet.MqttWireMessage.MESSAGE_TYPE_CONNACK;
import static org.thingsboard.common.util.JacksonUtil.newArrayNode;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.Bytes;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.Int16;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.Int32;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.Int64;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.Int8;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt16;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt32;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt64;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt8;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.createMetric;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE;
/**
* Created by nickAS21 on 12.01.23
*/
@Slf4j
public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttIntegrationTest {
protected MqttV5TestClient client;
protected SparkplugMqttCallback mqttCallback;
protected Calendar calendar = Calendar.getInstance();
protected ThreadLocalRandom random = ThreadLocalRandom.current();
protected static final String groupId = "SparkplugBGroupId";
protected static final String edgeNode = "SparkpluBNode";
protected static final String keysBdSeq = "bdSeq";
protected static final String alias = "Failed Telemetry/Attribute proto sparkplug payload. SparkplugMessageType ";
protected String deviceId = "Test Sparkplug B Device";
protected int bdSeq = 0;
protected int seq = 0;
protected static final long PUBLISH_TS_DELTA_MS = 86400000;// Publish start TS <-> 24h
// NBIRTH
protected static final String keyNodeRebirth = "Node Control/Rebirth";
//*BIRTH
protected static final MetricDataType metricBirthDataType_Int32 = Int32;
protected static final String metricBirthName_Int32 = "Device Metric int32";
protected Set<String> sparkplugAttributesMetricNames;
public void beforeSparkplugTest() throws Exception {
MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder()
.gatewayName("Test Connect Sparkplug client node")
.isSparkplug(true)
.sparkplugAttributesMetricNames(sparkplugAttributesMetricNames)
.transportPayloadType(TransportPayloadType.PROTOBUF)
.build();
processBeforeTest(configProperties);
}
public void clientWithCorrectNodeAccessTokenWithNDEATH() throws Exception {
long ts = calendar.getTimeInMillis();
long value = bdSeq = 0;
clientWithCorrectNodeAccessTokenWithNDEATH(ts, value);
}
public void clientWithCorrectNodeAccessTokenWithNDEATH(long ts, long value) throws Exception {
IMqttToken connectionResult = clientConnectWithNDEATH(ts, value);
MqttWireMessage response = connectionResult.getResponse();
Assert.assertEquals(MESSAGE_TYPE_CONNACK, response.getType());
MqttConnAck connAckMsg = (MqttConnAck) response;
Assert.assertEquals(MqttReturnCode.RETURN_CODE_SUCCESS, connAckMsg.getReturnCode());
}
public IMqttToken clientConnectWithNDEATH(long ts, long value, String... nameSpaceBad) throws Exception {
String key = keysBdSeq;
MetricDataType metricDataType = Int64;
SparkplugBProto.Payload.Builder deathPayload = SparkplugBProto.Payload.newBuilder()
.setTimestamp(calendar.getTimeInMillis());
deathPayload.addMetrics(createMetric(value, ts, key, metricDataType));
byte[] deathBytes = deathPayload.build().toByteArray();
this.client = new MqttV5TestClient();
this.mqttCallback = new SparkplugMqttCallback();
this.client.setCallback(this.mqttCallback);
MqttConnectionOptions options = new MqttConnectionOptions();
options.setUserName(gatewayAccessToken);
String nameSpace = nameSpaceBad.length == 0 ? NAMESPACE : nameSpaceBad[0];
String topic = nameSpace + "/" + groupId + "/" + SparkplugMessageType.NDEATH.name() + "/" + edgeNode;
MqttMessage msg = new MqttMessage();
msg.setId(0);
msg.setPayload(deathBytes);
options.setWill(topic, msg);
return client.connect(options);
}
protected List<Device> connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(int cntDevices, long ts) throws Exception {
List<Device> devices = new ArrayList<>();
clientWithCorrectNodeAccessTokenWithNDEATH();
MetricDataType metricDataType = Int32;
String key = "Node Metric int32";
int valueDeviceInt32 = 1024;
SparkplugBProto.Payload.Metric metric = createMetric(valueDeviceInt32, ts, key, metricDataType);
SparkplugBProto.Payload.Builder payloadBirthNode = SparkplugBProto.Payload.newBuilder()
.setTimestamp(ts)
.setSeq(getBdSeqNum());
payloadBirthNode.addMetrics(metric);
payloadBirthNode.setTimestamp(ts);
if (client.isConnected()) {
client.publish(NAMESPACE + "/" + groupId + "/" + SparkplugMessageType.NBIRTH.name() + "/" + edgeNode,
payloadBirthNode.build().toByteArray(), 0, false);
}
valueDeviceInt32 = 4024;
metric = createMetric(valueDeviceInt32, ts, metricBirthName_Int32, metricBirthDataType_Int32);
for (int i = 0; i < cntDevices; i++) {
SparkplugBProto.Payload.Builder payloadBirthDevice = SparkplugBProto.Payload.newBuilder()
.setTimestamp(ts)
.setSeq(getSeqNum());
String deviceName = deviceId + "_" + i;
payloadBirthDevice.addMetrics(metric);
if (client.isConnected()) {
client.publish(NAMESPACE + "/" + groupId + "/" + SparkplugMessageType.DBIRTH.name() + "/" + edgeNode + "/" + deviceName,
payloadBirthDevice.build().toByteArray(), 0, false);
AtomicReference<Device> device = new AtomicReference<>();
await(alias + "find device [" + deviceName + "] after created")
.atMost(200, TimeUnit.SECONDS)
.until(() -> {
device.set(doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class));
return device.get() != null;
});
devices.add(device.get());
}
}
Assert.assertEquals(cntDevices, devices.size());
return devices;
}
protected long getBdSeqNum() throws Exception {
if (bdSeq == 256) {
bdSeq = 0;
}
return bdSeq++;
}
protected long getSeqNum() throws Exception {
if (seq == 256) {
seq = 0;
}
return seq++;
}
protected List<String> connectionWithNBirth(MetricDataType metricDataType, String metricKey, Object metricValue) throws Exception {
List<String> listKeys = new ArrayList<>();
SparkplugBProto.Payload.Builder payloadBirthNode = SparkplugBProto.Payload.newBuilder()
.setTimestamp(calendar.getTimeInMillis());
long ts = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS;
long valueBdSec = getBdSeqNum();
payloadBirthNode.addMetrics(createMetric(valueBdSec, ts, keysBdSeq, Int64));
listKeys.add(SparkplugMessageType.NBIRTH.name() + " " + keysBdSeq);
payloadBirthNode.addMetrics(createMetric(false, ts, keyNodeRebirth, MetricDataType.Boolean));
listKeys.add(keyNodeRebirth);
payloadBirthNode.addMetrics(createMetric(metricValue, ts, metricKey, metricDataType));
listKeys.add(metricKey);
if (client.isConnected()) {
client.publish(NAMESPACE + "/" + groupId + "/" + SparkplugMessageType.NBIRTH.name() + "/" + edgeNode,
payloadBirthNode.build().toByteArray(), 0, false);
}
return listKeys;
}
protected void createdAddMetricValuePrimitiveTsKv(List<TsKvEntry> listTsKvEntry, List<String> listKeys,
SparkplugBProto.Payload.Builder dataPayload, long ts) throws ThingsboardException {
String keys = "MyInt8";
listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextInt8(), ts, Int8));
listKeys.add(keys);
keys = "MyInt16";
listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextInt16(), ts, Int16));
listKeys.add(keys);
keys = "MyInt32";
listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextInt32(), ts, Int32));
listKeys.add(keys);
keys = "MyInt64";
listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextInt64(), ts, Int64));
listKeys.add(keys);
keys = "MyUInt8";
listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextUInt8(), ts, UInt8));
listKeys.add(keys);
keys = "MyUInt16";
listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextUInt16(), ts, UInt16));
listKeys.add(keys);
keys = "MyUInt32";
listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextUInt32(), ts, UInt32));
listKeys.add(keys);
keys = "MyUInt64";
listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextUInt64(), ts, UInt64));
listKeys.add(keys);
keys = "MyFloat";
listTsKvEntry.add(createdAddMetricTsKvFloat(dataPayload, keys, nextFloat(0, 100), ts, MetricDataType.Float));
listKeys.add(keys);
keys = "MyDateTime";
listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextDateTime(), ts, MetricDataType.DateTime));
listKeys.add(keys);
keys = "MyDouble";
listTsKvEntry.add(createdAddMetricTsKvDouble(dataPayload, keys, nextDouble(), ts, MetricDataType.Double));
listKeys.add(keys);
keys = "MyBoolean";
listTsKvEntry.add(createdAddMetricTsKvBoolean(dataPayload, keys, nextBoolean(), ts, MetricDataType.Boolean));
listKeys.add(keys);
keys = "MyString";
listTsKvEntry.add(createdAddMetricTsKvString(dataPayload, keys, nextString(), ts, MetricDataType.String));
listKeys.add(keys);
keys = "MyText";
listTsKvEntry.add(createdAddMetricTsKvString(dataPayload, keys, nextString(), ts, MetricDataType.Text));
listKeys.add(keys);
keys = "MyUUID";
listTsKvEntry.add(createdAddMetricTsKvString(dataPayload, keys, nextString(), ts, MetricDataType.UUID));
listKeys.add(keys);
}
protected void createdAddMetricValueArraysPrimitiveTsKv(List<TsKvEntry> listTsKvEntry, List<String> listKeys,
SparkplugBProto.Payload.Builder dataPayload, long ts) throws ThingsboardException {
String keys = "MyBytesArray";
byte[] bytes = {nextInt8(), nextInt8(), nextInt8()};
createdAddMetricTsKvJson(dataPayload, keys, bytes, ts, Bytes, listTsKvEntry, listKeys);
}
private TsKvEntry createdAddMetricTsKvLong(SparkplugBProto.Payload.Builder dataPayload, String key, Object value,
long ts, MetricDataType metricDataType) throws ThingsboardException {
TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new LongDataEntry(key, Long.valueOf(String.valueOf(value))));
dataPayload.addMetrics(createMetric(value, ts, key, metricDataType));
return tsKvEntry;
}
private TsKvEntry createdAddMetricTsKvFloat(SparkplugBProto.Payload.Builder dataPayload, String key, float value,
long ts, MetricDataType metricDataType) throws ThingsboardException {
Double dd = Double.parseDouble(Float.toString(value));
TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new DoubleDataEntry(key, dd));
dataPayload.addMetrics(createMetric(value, ts, key, metricDataType));
return tsKvEntry;
}
private TsKvEntry createdAddMetricTsKvDouble(SparkplugBProto.Payload.Builder dataPayload, String key, double value,
long ts, MetricDataType metricDataType) throws ThingsboardException {
Long l = Double.valueOf(value).longValue();
TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new LongDataEntry(key, l));
dataPayload.addMetrics(createMetric(value, ts, key, metricDataType));
return tsKvEntry;
}
private TsKvEntry createdAddMetricTsKvBoolean(SparkplugBProto.Payload.Builder dataPayload, String key, boolean value,
long ts, MetricDataType metricDataType) throws ThingsboardException {
TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new BooleanDataEntry(key, value));
dataPayload.addMetrics(createMetric(value, ts, key, metricDataType));
return tsKvEntry;
}
private TsKvEntry createdAddMetricTsKvString(SparkplugBProto.Payload.Builder dataPayload, String key, String value,
long ts, MetricDataType metricDataType) throws ThingsboardException {
TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new StringDataEntry(key, value));
dataPayload.addMetrics(createMetric(value, ts, key, metricDataType));
return tsKvEntry;
}
private void createdAddMetricTsKvJson(SparkplugBProto.Payload.Builder dataPayload, String key,
Object values, long ts, MetricDataType metricDataType,
List<TsKvEntry> listTsKvEntry,
List<String> listKeys) throws ThingsboardException {
ArrayNode nodeArray = newArrayNode();
switch (metricDataType) {
case Bytes:
for (byte b : (byte[]) values) {
nodeArray.add(b);
}
break;
default:
throw new IllegalStateException("Unexpected value: " + metricDataType);
}
if (nodeArray.size() > 0) {
Optional<TsKvEntry> tsKvEntryOptional = Optional.of(new BasicTsKvEntry(ts, new JsonDataEntry(key, nodeArray.toString())));
if (tsKvEntryOptional.isPresent()) {
dataPayload.addMetrics(createMetric(values, ts, key, metricDataType));
listTsKvEntry.add(tsKvEntryOptional.get());
listKeys.add(key);
}
}
}
private byte nextInt8() {
return (byte) random.nextInt(Byte.MIN_VALUE, Byte.MAX_VALUE);
}
private short nextUInt8() {
return (short) random.nextInt(0, Byte.MAX_VALUE * 2 + 1);
}
private short nextInt16() {
return (short) random.nextInt(Short.MIN_VALUE, Short.MAX_VALUE);
}
private int nextUInt16() {
return random.nextInt(0, Short.MAX_VALUE * 2 + 1);
}
protected int nextInt32() {
return random.nextInt(Integer.MIN_VALUE, Integer.MAX_VALUE);
}
protected long nextUInt32() {
long l = Integer.MAX_VALUE;
return random.nextLong(0, l * 2 + 1);
}
private long nextInt64() {
return random.nextLong(Long.MIN_VALUE, Long.MAX_VALUE);
}
private long nextUInt64() {
double d = Long.MAX_VALUE;
return random.nextLong(0, (long) (d * 2 + 1));
}
protected double nextDouble() {
return random.nextDouble(Long.MIN_VALUE, Long.MAX_VALUE);
}
private long nextDateTime() {
long min = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS;
long max = calendar.getTimeInMillis();
return random.nextLong(min, max);
}
protected float nextFloat(float min, float max) {
if (min >= max)
throw new IllegalArgumentException("max must be greater than min");
float result = ThreadLocalRandom.current().nextFloat() * (max - min) + min;
if (result >= max) // correct for rounding
result = Float.intBitsToFloat(Float.floatToIntBits(max) - 1);
return result;
}
protected boolean nextBoolean() {
return random.nextBoolean();
}
protected String nextString() {
return java.util.UUID.randomUUID().toString();
}
public class SparkplugMqttCallback implements MqttCallback {
private final List<SparkplugBProto.Payload.Metric> messageArrivedMetrics = new ArrayList<>();
@Override
public void disconnected(MqttDisconnectResponse mqttDisconnectResponse) {
}
@Override
public void mqttErrorOccurred(MqttException e) {
}
@Override
public void messageArrived(String topic, MqttMessage mqttMsg) throws Exception {
SparkplugBProto.Payload sparkplugBProtoNode = SparkplugBProto.Payload.parseFrom(mqttMsg.getPayload());
messageArrivedMetrics.addAll(sparkplugBProtoNode.getMetricsList());
}
@Override
public void deliveryComplete(IMqttToken iMqttToken) {
}
@Override
public void connectComplete(boolean b, String s) {
}
@Override
public void authPacketArrived(int i, MqttProperties mqttProperties) {
}
public List<SparkplugBProto.Payload.Metric> getMessageArrivedMetrics() {
return messageArrivedMetrics;
}
public void deleteMessageArrivedMetrics(int id) {
messageArrivedMetrics.remove(id);
}
}
}

432
application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/AbstractMqttV5ClientSparkplugAttributesTest.java

@ -0,0 +1,432 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.mqtt.sparkplug.attributes;
import com.fasterxml.jackson.core.type.TypeReference;
import io.netty.handler.codec.mqtt.MqttQoS;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.transport.mqtt.sparkplug.AbstractMqttV5ClientSparkplugTest;
import org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType;
import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.common.data.DataConstants.CLIENT_SCOPE;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt32;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.NCMD;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE;
/**
* Created by nickAS21 on 12.01.23
*/
@Slf4j
public abstract class AbstractMqttV5ClientSparkplugAttributesTest extends AbstractMqttV5ClientSparkplugTest {
protected void processClientWithCorrectAccessTokenPublishNCMDReBirth() throws Exception {
clientWithCorrectNodeAccessTokenWithNDEATH();
List<String> listKeys = connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32());
// Shared attribute "Node Control/Rebirth" = true. type = NCMD.
boolean value = true;
Assert.assertTrue(listKeys.contains(keyNodeRebirth));
String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + keyNodeRebirth + "\":" + value + "}";
Assert.assertTrue("Connection node is failed", client.isConnected());
client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE);
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
await(alias + SparkplugMessageType.NBIRTH.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
return mqttCallback.getMessageArrivedMetrics().size() == 1;
});
Assert.assertEquals(keyNodeRebirth, mqttCallback.getMessageArrivedMetrics().get(0).getName());
Assert.assertTrue(mqttCallback.getMessageArrivedMetrics().get(0).getBooleanValue());
}
/**
* If boolean - send long 0 or 1
* If String - try to parse long
* If double - cast long
* If we can't parse, cast, or JSON there - debug the message with the id of the devise/node, tenant,
* the name and type of the attribute into an error and don't send anything.
*/
protected void processClientWithCorrectAccessTokenPublishNCMD_BooleanType_IfMetricFailedTypeCheck_SendValueOk() throws Exception {
clientWithCorrectNodeAccessTokenWithNDEATH();
MetricDataType metricDataType = MetricDataType.Boolean;
String metricKey = "MyBoolean";
Object metricValue = nextBoolean();
connectionWithNBirth(metricDataType, metricKey, metricValue);
Assert.assertTrue("Connection node is failed", client.isConnected());
client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE);
// Boolean <-> String
boolean expectedValue = true;
String valueStr = "123";
String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueStr + "}";
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
await(alias + SparkplugMessageType.NBIRTH.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
return mqttCallback.getMessageArrivedMetrics().size() == 1;
});
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName());
Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getBooleanValue());
mqttCallback.deleteMessageArrivedMetrics(0);
expectedValue = false;
valueStr = "0";
SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueStr + "}";
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
await(alias + SparkplugMessageType.NBIRTH.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
return mqttCallback.getMessageArrivedMetrics().size() == 1;
});
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName());
Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getBooleanValue());
mqttCallback.deleteMessageArrivedMetrics(0);
// Boolean <-> Integer
expectedValue = true;
Integer valueInt = nextInt32();
SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueInt + "}";
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
await(alias + SparkplugMessageType.NBIRTH.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
return mqttCallback.getMessageArrivedMetrics().size() == 1;
});
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName());
Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getBooleanValue());
mqttCallback.deleteMessageArrivedMetrics(0);
expectedValue = false;
valueInt = 0;
SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueInt + "}";
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
await(alias + SparkplugMessageType.NBIRTH.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
return mqttCallback.getMessageArrivedMetrics().size() == 1;
});
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName());
Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getBooleanValue());
}
protected void processClientWithCorrectAccessTokenPublishNCMD_LongType_IfMetricFailedTypeCheck_SendValueOk() throws Exception {
clientWithCorrectNodeAccessTokenWithNDEATH();
MetricDataType metricDataType = UInt32;
String metricKey = "MyLong";
Object metricValue = nextUInt32();
connectionWithNBirth(metricDataType, metricKey, metricValue);
Assert.assertTrue("Connection node is failed", client.isConnected());
client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE);
// Long <-> String
String valueStr = "123";
long expectedValue = Long.valueOf(valueStr);
String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueStr + "}";
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
await(alias + SparkplugMessageType.NBIRTH.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
return mqttCallback.getMessageArrivedMetrics().size() == 1;
});
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName());
Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getLongValue());
mqttCallback.deleteMessageArrivedMetrics(0);
// Long <-> Boolean
Boolean valueBoolean = true;
expectedValue = 1L;
SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}";
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
await(alias + SparkplugMessageType.NBIRTH.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
return mqttCallback.getMessageArrivedMetrics().size() == 1;
});
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName());
Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getLongValue());
mqttCallback.deleteMessageArrivedMetrics(0);
valueBoolean = false;
expectedValue = 0L;
SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}";
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
await(alias + SparkplugMessageType.NBIRTH.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
return mqttCallback.getMessageArrivedMetrics().size() == 1;
});
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName());
Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getLongValue());
}
protected void processClientWithCorrectAccessTokenPublishNCMD_FloatType_IfMetricFailedTypeCheck_SendValueOk() throws Exception {
clientWithCorrectNodeAccessTokenWithNDEATH();
MetricDataType metricDataType = MetricDataType.Float;
String metricKey = "MyFloat";
Object metricValue = nextFloat(30, 400);
connectionWithNBirth(metricDataType, metricKey, metricValue);
Assert.assertTrue("Connection node is failed", client.isConnected());
client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE);
// Float <-> String
String valueStr = "123.345";
float expectedValue = Float.valueOf(valueStr);
String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueStr + "}";
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
await(alias + SparkplugMessageType.NBIRTH.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
return mqttCallback.getMessageArrivedMetrics().size() == 1;
});
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName());
Assert.assertTrue(expectedValue == mqttCallback.getMessageArrivedMetrics().get(0).getFloatValue());
mqttCallback.deleteMessageArrivedMetrics(0);
// Float <-> Boolean
Boolean valueBoolean = true;
expectedValue = 1f;
SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}";
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
await(alias + SparkplugMessageType.NBIRTH.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
return mqttCallback.getMessageArrivedMetrics().size() == 1;
});
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName());
Assert.assertTrue(expectedValue == mqttCallback.getMessageArrivedMetrics().get(0).getFloatValue());
mqttCallback.deleteMessageArrivedMetrics(0);
valueBoolean = false;
expectedValue = 0f;
SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}";
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
await(alias + SparkplugMessageType.NBIRTH.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
return mqttCallback.getMessageArrivedMetrics().size() == 1;
});
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName());
Assert.assertTrue(expectedValue == mqttCallback.getMessageArrivedMetrics().get(0).getFloatValue());
}
protected void processClientWithCorrectAccessTokenPublishNCMD_DoubleType_IfMetricFailedTypeCheck_SendValueOk() throws Exception {
clientWithCorrectNodeAccessTokenWithNDEATH();
MetricDataType metricDataType = MetricDataType.Double;
String metricKey = "MyDouble";
Object metricValue = nextDouble();
connectionWithNBirth(metricDataType, metricKey, metricValue);
Assert.assertTrue("Connection node is failed", client.isConnected());
client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE);
// Double <-> String
String valueStr = "123345456";
double expectedValue = Double.valueOf(valueStr);
String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueStr + "}";
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
await(alias + SparkplugMessageType.NBIRTH.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
return mqttCallback.getMessageArrivedMetrics().size() == 1;
});
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName());
Assert.assertTrue(expectedValue == mqttCallback.getMessageArrivedMetrics().get(0).getDoubleValue());
mqttCallback.deleteMessageArrivedMetrics(0);
// Double <-> Boolean
Boolean valueBoolean = true;
expectedValue = 1d;
SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}";
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
await(alias + SparkplugMessageType.NBIRTH.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
return mqttCallback.getMessageArrivedMetrics().size() == 1;
});
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName());
Assert.assertTrue(expectedValue == mqttCallback.getMessageArrivedMetrics().get(0).getDoubleValue());
mqttCallback.deleteMessageArrivedMetrics(0);
valueBoolean = false;
expectedValue = 0d;
SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}";
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
await(alias + SparkplugMessageType.NBIRTH.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
return mqttCallback.getMessageArrivedMetrics().size() == 1;
});
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName());
Assert.assertTrue(expectedValue == mqttCallback.getMessageArrivedMetrics().get(0).getDoubleValue());
}
protected void processClientWithCorrectAccessTokenPublishNCMD_StringType_IfMetricFailedTypeCheck_SendValueOk() throws Exception {
clientWithCorrectNodeAccessTokenWithNDEATH();
MetricDataType metricDataType = MetricDataType.String;
String metricKey = "MyString";
Object metricValue = nextString();
connectionWithNBirth(metricDataType, metricKey, metricValue);
Assert.assertTrue("Connection node is failed", client.isConnected());
client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE);
// String <-> Long
long valueLong = 123345456L;
String expectedValue = String.valueOf(valueLong);
String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueLong + "}";
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
await(alias + SparkplugMessageType.NBIRTH.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
return mqttCallback.getMessageArrivedMetrics().size() == 1;
});
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName());
Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getStringValue());
mqttCallback.deleteMessageArrivedMetrics(0);
// String <-> Boolean
Boolean valueBoolean = true;
expectedValue = "true";
SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}";
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
await(alias + SparkplugMessageType.NBIRTH.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
return mqttCallback.getMessageArrivedMetrics().size() == 1;
});
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName());
Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getStringValue());
mqttCallback.deleteMessageArrivedMetrics(0);
valueBoolean = false;
expectedValue = "false";
SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}";
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
await(alias + SparkplugMessageType.NBIRTH.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
return mqttCallback.getMessageArrivedMetrics().size() == 1;
});
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName());
Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getStringValue());
}
protected void processClientDeviceWithCorrectAccessTokenPublishWithBirth_SharedAttribute() throws Exception {
long ts = calendar.getTimeInMillis();
List<Device> devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(1, ts);
// Integer <-> Integer
int expectedValueInt = 123456;
String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricBirthName_Int32 + "\":" + expectedValueInt + "}";
doPostAsync("/api/plugins/telemetry/DEVICE/" + devices.get(0).getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
await(alias + SparkplugMessageType.DBIRTH.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
return mqttCallback.getMessageArrivedMetrics().size() == 1;
});
Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName());
Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName());
Assert.assertEquals(expectedValueInt, mqttCallback.getMessageArrivedMetrics().get(0).getIntValue());
}
protected void processClientDeviceWithCorrectAccessTokenPublishWithBirth_SharedAttributes_LongType_IfMetricFailedTypeCheck_SendValueOk() throws Exception {
long ts = calendar.getTimeInMillis();
List<Device> devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(1, ts);
// Int <-> String
String valueStr = "123";
long expectedValue = Long.valueOf(valueStr);
String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricBirthName_Int32 + "\":" + valueStr + "}";
doPostAsync("/api/plugins/telemetry/DEVICE/" + devices.get(0).getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
await(alias + SparkplugMessageType.DBIRTH.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
return mqttCallback.getMessageArrivedMetrics().size() == 1;
});
Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName());
Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getIntValue());
mqttCallback.deleteMessageArrivedMetrics(0);
// Int <-> Boolean
Boolean valueBoolean = true;
expectedValue = 1;
SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricBirthName_Int32 + "\":" + valueBoolean + "}";
doPostAsync("/api/plugins/telemetry/DEVICE/" + devices.get(0).getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
await(alias + SparkplugMessageType.NBIRTH.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
return mqttCallback.getMessageArrivedMetrics().size() == 1;
});
Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName());
Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getIntValue());
mqttCallback.deleteMessageArrivedMetrics(0);
valueBoolean = false;
expectedValue = 0;
SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricBirthName_Int32 + "\":" + valueBoolean + "}";
doPostAsync("/api/plugins/telemetry/DEVICE/" + devices.get(0).getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
await(alias + SparkplugMessageType.NBIRTH.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
return mqttCallback.getMessageArrivedMetrics().size() == 1;
});
Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName());
Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getIntValue());
}
protected void processClientNodeWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes() throws Exception {
clientWithCorrectNodeAccessTokenWithNDEATH();
connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32());
String urlTemplate = "/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/keys/attributes/" + CLIENT_SCOPE;
AtomicReference<List<String>> actualKeys = new AtomicReference<>();
await(alias + SparkplugMessageType.NBIRTH.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
actualKeys.set(doGetAsyncTyped(urlTemplate, new TypeReference<>() {
}));
return actualKeys.get().size() == 1;
});
Assert.assertEquals(metricBirthName_Int32, actualKeys.get().get(0));
}
protected void processClientDeviceWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes() throws Exception {
long ts = calendar.getTimeInMillis();
List<Device> devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(1, ts);
String urlTemplate = "/api/plugins/telemetry/DEVICE/" + devices.get(0).getId().getId() + "/keys/attributes/" + CLIENT_SCOPE;
AtomicReference<List<String>> actualKeys = new AtomicReference<>();
await(alias + SparkplugMessageType.DBIRTH.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
actualKeys.set(doGetAsyncTyped(urlTemplate, new TypeReference<>() {
}));
return actualKeys.get().size() == 1;
});
Assert.assertEquals(metricBirthName_Int32, actualKeys.get().get(0));
}
}

55
application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesInProfileTest.java

@ -0,0 +1,55 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.mqtt.sparkplug.attributes;
import org.eclipse.paho.mqttv5.common.MqttException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.HashSet;
/**
* Created by nickAS21 on 12.01.23
*/
@DaoSqlTest
public class MqttV5ClientSparkplugBAttributesInProfileTest extends AbstractMqttV5ClientSparkplugAttributesTest {
@Before
public void beforeTest() throws Exception {
sparkplugAttributesMetricNames = new HashSet<>();
sparkplugAttributesMetricNames.add(metricBirthName_Int32);
beforeSparkplugTest();
}
@After
public void afterTest () throws MqttException {
if (client.isConnected()) {
client.disconnect(); }
}
@Test
public void testClientNodeWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes() throws Exception {
processClientNodeWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes();
}
@Test
public void testClientDeviceWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes() throws Exception {
processClientDeviceWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes();
}
}

81
application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesTest.java

@ -0,0 +1,81 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.mqtt.sparkplug.attributes;
import org.eclipse.paho.mqttv5.common.MqttException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.server.dao.service.DaoSqlTest;
/**
* Created by nickAS21 on 12.01.23
*/
@DaoSqlTest
public class MqttV5ClientSparkplugBAttributesTest extends AbstractMqttV5ClientSparkplugAttributesTest {
@Before
public void beforeTest() throws Exception {
beforeSparkplugTest();
}
@After
public void afterTest () throws MqttException {
if (client.isConnected()) {
client.disconnect(); }
}
@Test
public void testClientWithCorrectAccessTokenPublishNCMDReBirth() throws Exception {
processClientWithCorrectAccessTokenPublishNCMDReBirth();
}
@Test
public void testClientWithCorrectAccessTokenPublishNCMD_BooleanType_IfMetricFailedTypeCheck_SendValueOk() throws Exception {
processClientWithCorrectAccessTokenPublishNCMD_BooleanType_IfMetricFailedTypeCheck_SendValueOk();
}
@Test
public void testClientWithCorrectAccessTokenPublishNCMD_LongType_IfMetricFailedTypeCheck_SendValueOk() throws Exception {
processClientWithCorrectAccessTokenPublishNCMD_LongType_IfMetricFailedTypeCheck_SendValueOk();
}
@Test
public void testClientWithCorrectAccessTokenPublishNCMD_FloatType_IfMetricFailedTypeCheck_SendValueOk() throws Exception {
processClientWithCorrectAccessTokenPublishNCMD_FloatType_IfMetricFailedTypeCheck_SendValueOk();
}
@Test
public void testClientWithCorrectAccessTokenPublishNCMD_DoubleType_IfMetricFailedTypeCheck_SendValueOk() throws Exception {
processClientWithCorrectAccessTokenPublishNCMD_DoubleType_IfMetricFailedTypeCheck_SendValueOk();
}
@Test
public void testClientWithCorrectAccessTokenPublishNCMD_StringType_IfMetricFailedTypeCheck_SendValueOk() throws Exception {
processClientWithCorrectAccessTokenPublishNCMD_StringType_IfMetricFailedTypeCheck_SendValueOk();
}
@Test
public void testClientDeviceWithCorrectAccessTokenPublishWithBirth_SharedAttribute() throws Exception {
processClientDeviceWithCorrectAccessTokenPublishWithBirth_SharedAttribute();
}
@Test
public void testClientDeviceWithCorrectAccessTokenPublishWithBirth_SharedAttributes_LongType_IfMetricFailedTypeCheck_SendValueOk() throws Exception {
processClientDeviceWithCorrectAccessTokenPublishWithBirth_SharedAttributes_LongType_IfMetricFailedTypeCheck_SendValueOk();
}
}

178
application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/AbstractMqttV5ClientSparkplugConnectionTest.java

@ -0,0 +1,178 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.mqtt.sparkplug.connection;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.mqttv5.common.MqttException;
import org.junit.Assert;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto;
import org.thingsboard.server.transport.mqtt.mqttv5.MqttV5TestClient;
import org.thingsboard.server.transport.mqtt.sparkplug.AbstractMqttV5ClientSparkplugTest;
import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import static org.awaitility.Awaitility.await;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState.OFFLINE;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState.ONLINE;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.STATE;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.messageName;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE;
/**
* Created by nickAS21 on 12.01.23
*/
@Slf4j
public abstract class AbstractMqttV5ClientSparkplugConnectionTest extends AbstractMqttV5ClientSparkplugTest {
protected void processClientWithCorrectNodeAccessTokenWithNDEATH_Test() throws Exception {
long ts = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS;
long value = bdSeq = 0;
clientWithCorrectNodeAccessTokenWithNDEATH(ts, value);
String keys = SparkplugMessageType.NDEATH.name() + " " + keysBdSeq;
TsKvEntry expectedTsKvEntry = new BasicTsKvEntry(ts, new LongDataEntry(keys, value));
AtomicReference<ListenableFuture<Optional<TsKvEntry>>> finalFuture = new AtomicReference<>();
await(alias + SparkplugMessageType.NDEATH.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
finalFuture.set(tsService.findLatest(tenantId, savedGateway.getId(), keys));
return finalFuture.get().get().isPresent();
});
TsKvEntry actualTsKvEntry = finalFuture.get().get().get();
Assert.assertEquals(expectedTsKvEntry, actualTsKvEntry);
}
protected void processClientWithCorrectNodeAccessTokenWithoutNDEATH_Test() throws Exception {
this.client = new MqttV5TestClient();
MqttException actualException = Assert.assertThrows(MqttException.class, () -> client.connectAndWait(gatewayAccessToken));
String expectedMessage = "Server unavailable.";
int expectedReasonCode = 136;
Assert.assertEquals(expectedMessage, actualException.getMessage());
Assert.assertEquals(expectedReasonCode, actualException.getReasonCode());
}
protected void processClientWithCorrectNodeAccessTokenNameSpaceInvalid_Test() throws Exception {
long ts = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS;
long value = bdSeq = 0;
MqttException actualException = Assert.assertThrows(MqttException.class, () -> clientConnectWithNDEATH(ts, value, "spBv1.2"));
String expectedMessage = "Server unavailable.";
int expectedReasonCode = 136;
Assert.assertEquals(expectedMessage, actualException.getMessage());
Assert.assertEquals(expectedReasonCode, actualException.getReasonCode());
}
protected void processClientWithCorrectAccessTokenWithNDEATHCreatedDevices(int cntDevices) throws Exception {
long ts = calendar.getTimeInMillis();
connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(cntDevices, ts);
}
protected void processConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_ALL(int cntDevices) throws Exception {
long ts = calendar.getTimeInMillis();
List<Device> devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(cntDevices, ts);
TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new StringDataEntry(messageName(STATE), ONLINE.name()));
AtomicReference<ListenableFuture<List<TsKvEntry>>> finalFuture = new AtomicReference<>();
await(alias + messageName(STATE) + ", device: " + savedGateway.getName())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
finalFuture.set(tsService.findAllLatest(tenantId, savedGateway.getId()));
return finalFuture.get().get().contains(tsKvEntry);
});
for (Device device : devices) {
await(alias + messageName(STATE) + ", device: " + device.getName())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
finalFuture.set(tsService.findAllLatest(tenantId, device.getId()));
return finalFuture.get().get().contains(tsKvEntry);
});
}
}
protected void processConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_All_Then_OneDeviceOFFLINE(int cntDevices, int indexDeviceDisconnect) throws Exception {
long ts = calendar.getTimeInMillis();
List<Device> devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(cntDevices, ts);
TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new StringDataEntry(messageName(STATE), OFFLINE.name()));
AtomicReference<ListenableFuture<List<TsKvEntry>>> finalFuture = new AtomicReference<>();
SparkplugBProto.Payload.Builder payloadDeathDevice = SparkplugBProto.Payload.newBuilder()
.setTimestamp(ts)
.setSeq(getSeqNum());
if (client.isConnected()) {
List<Device> devicesList = new ArrayList<>(devices);
Device device = devicesList.get(indexDeviceDisconnect);
client.publish(NAMESPACE + "/" + groupId + "/" + SparkplugMessageType.DDEATH.name() + "/" + edgeNode + "/" + device.getName(),
payloadDeathDevice.build().toByteArray(), 0, false);
await(alias + messageName(STATE) + ", device: " + device.getName())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
finalFuture.set(tsService.findAllLatest(tenantId, device.getId()));
return findEqualsKeyValueInKvEntrys(finalFuture.get().get(), tsKvEntry);
});
}
}
protected void processConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_All_Then_OFFLINE_All(int cntDevices) throws Exception {
long ts = calendar.getTimeInMillis();
List<Device> devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(cntDevices, ts);
TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new StringDataEntry(messageName(STATE), OFFLINE.name()));
AtomicReference<ListenableFuture<List<TsKvEntry>>> finalFuture = new AtomicReference<>();
if (client.isConnected()) {
client.disconnect();
await(alias + messageName(STATE) + ", device: " + savedGateway.getName())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
finalFuture.set(tsService.findAllLatest(tenantId, savedGateway.getId()));
return findEqualsKeyValueInKvEntrys(finalFuture.get().get(), tsKvEntry);
});
List<Device> devicesList = new ArrayList<>(devices);
for (Device device : devicesList) {
await(alias + messageName(STATE) + ", device: " + device.getName())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
finalFuture.set(tsService.findAllLatest(tenantId, device.getId()));
return findEqualsKeyValueInKvEntrys(finalFuture.get().get(), tsKvEntry);
});
}
}
}
private boolean findEqualsKeyValueInKvEntrys(List<TsKvEntry> finalFuture, TsKvEntry tsKvEntry) {
for (TsKvEntry kvEntry : finalFuture) {
if (kvEntry.getKey().equals(tsKvEntry.getKey()) && kvEntry.getValue().equals(tsKvEntry.getValue())) {
return true;
}
}
return false;
}
}

82
application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/MqttV5ClientSparkplugBConnectionTest.java

@ -0,0 +1,82 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.mqtt.sparkplug.connection;
import org.eclipse.paho.mqttv5.common.MqttException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.server.dao.service.DaoSqlTest;
/**
* Created by nickAS21 on 12.01.23
*/
@DaoSqlTest
public class MqttV5ClientSparkplugBConnectionTest extends AbstractMqttV5ClientSparkplugConnectionTest {
@Before
public void beforeTest() throws Exception {
beforeSparkplugTest();
}
@After
public void afterTest() throws MqttException {
if (client.isConnected()) {
client.disconnect();
}
}
@Test
public void testClientWithCorrectAccessTokenWithNDEATH() throws Exception {
processClientWithCorrectNodeAccessTokenWithNDEATH_Test();
}
@Test
public void testClientWithCorrectNodeAccessTokenWithoutNDEATH() throws Exception {
processClientWithCorrectNodeAccessTokenWithoutNDEATH_Test();
}
@Test
public void testClientWithCorrectNodeAccessTokenNameSpaceInvalid() throws Exception {
processClientWithCorrectNodeAccessTokenNameSpaceInvalid_Test();
}
@Test
public void testClientWithCorrectAccessTokenWithNDEATHCreatedOneDevice() throws Exception {
processClientWithCorrectAccessTokenWithNDEATHCreatedDevices(1);
}
@Test
public void testClientWithCorrectAccessTokenWithNDEATHCreatedTwoDevice() throws Exception {
processClientWithCorrectAccessTokenWithNDEATHCreatedDevices(2);
}
@Test
public void testClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_ALL() throws Exception {
processConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_ALL(3);
}
@Test
public void testConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_All_Then_OneDeviceOFFLINE() throws Exception {
processConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_All_Then_OneDeviceOFFLINE(3, 1);
}
@Test
public void testConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_All_Then_OFFLINE_All() throws Exception {
processConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_All_Then_OFFLINE_All(3);
}
}

108
application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/AbstractMqttV5RpcSparkplugTest.java

@ -0,0 +1,108 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.mqtt.sparkplug.rpc;
import io.netty.handler.codec.mqtt.MqttQoS;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Test;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.transport.mqtt.sparkplug.AbstractMqttV5ClientSparkplugTest;
import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.common.data.exception.ThingsboardErrorCode.INVALID_ARGUMENTS;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.DCMD;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.NCMD;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE;
@Slf4j
public abstract class AbstractMqttV5RpcSparkplugTest extends AbstractMqttV5ClientSparkplugTest {
private static final int metricBirthValue_Int32 = 123456;
private static final String sparkplugRpcRequest = "{\"metricName\":\"" + metricBirthName_Int32 + "\",\"value\":" + metricBirthValue_Int32 + "}";
@Test
public void processClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_Success() throws Exception {
clientWithCorrectNodeAccessTokenWithNDEATH();
connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32());
Assert.assertTrue("Connection node is failed", client.isConnected());
client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE);
String expected = "{\"result\":\"Success: " + SparkplugMessageType.NCMD.name() + "\"}";
String actual = sendRPCSparkplug(NCMD.name(), sparkplugRpcRequest, savedGateway);
await(alias + SparkplugMessageType.NCMD.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
return mqttCallback.getMessageArrivedMetrics().size() == 1;
});
Assert.assertEquals(expected, actual);
Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName());
Assert.assertTrue(metricBirthValue_Int32 == mqttCallback.getMessageArrivedMetrics().get(0).getIntValue());
}
@Test
public void processClientDeviceWithCorrectAccessTokenPublish_TwoWayRpc_Success() throws Exception {
long ts = calendar.getTimeInMillis();
List<Device> devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(1, ts);
String expected = "{\"result\":\"Success: " + DCMD.name() + "\"}";
String actual = sendRPCSparkplug(DCMD.name() , sparkplugRpcRequest, devices.get(0));
await(alias + NCMD.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
return mqttCallback.getMessageArrivedMetrics().size() == 1;
});
Assert.assertEquals(expected, actual);
Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName());
Assert.assertTrue(metricBirthValue_Int32 == mqttCallback.getMessageArrivedMetrics().get(0).getIntValue());
}
@Test
public void processClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_InvalidTypeMessage_INVALID_ARGUMENTS() throws Exception {
clientWithCorrectNodeAccessTokenWithNDEATH();
connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32());
Assert.assertTrue("Connection node is failed", client.isConnected());
client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE);
String invalidateTypeMessageName = "RCMD";
String expected = "{\"result\":\"" + INVALID_ARGUMENTS + "\",\"error\":\"Failed to convert device RPC command to MQTT msg: " +
invalidateTypeMessageName + "{\\\"metricName\\\":\\\"" + metricBirthName_Int32 + "\\\",\\\"value\\\":" + metricBirthValue_Int32 + "}\"}";
String actual = sendRPCSparkplug(invalidateTypeMessageName, sparkplugRpcRequest, savedGateway);
Assert.assertEquals(expected, actual);
}
@Test
public void processClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_InBirthNotHaveMetric_BAD_REQUEST_PARAMS() throws Exception {
clientWithCorrectNodeAccessTokenWithNDEATH();
connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32());
Assert.assertTrue("Connection node is failed", client.isConnected());
client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE);
String metricNameBad = metricBirthName_Int32 + "_Bad";
String sparkplugRpcRequestBad = "{\"metricName\":\"" + metricNameBad + "\",\"value\":" + metricBirthValue_Int32 + "}";
String expected = "{\"result\":\"BAD_REQUEST_PARAMS\",\"error\":\"Failed send To Node Rpc Request: " +
DCMD.name() + ". This node does not have a metricName: [" + metricNameBad + "]\"}";
String actual = sendRPCSparkplug(DCMD.name(), sparkplugRpcRequestBad, savedGateway);
Assert.assertEquals(expected, actual);
}
private String sendRPCSparkplug(String nameTypeMessage, String keyValue, Device device) throws Exception {
String setRpcRequest = "{\"method\": \"" + nameTypeMessage + "\", \"params\": " + keyValue + "}";
return doPostAsync("/api/plugins/rpc/twoway/" + device.getId().getId(), setRpcRequest, String.class, status().isOk());
}
}

61
application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/MqttV5RpcSparkplugTest.java

@ -0,0 +1,61 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.mqtt.sparkplug.rpc;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.mqttv5.common.MqttException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.server.dao.service.DaoSqlTest;
@DaoSqlTest
@Slf4j
public class MqttV5RpcSparkplugTest extends AbstractMqttV5RpcSparkplugTest {
@Before
public void beforeTest() throws Exception {
beforeSparkplugTest();
}
@After
public void afterTest() throws MqttException {
if (client.isConnected()) {
client.disconnect();
}
}
@Test
public void testClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_Success() throws Exception {
processClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_Success();
}
@Test
public void testClientDeviceWithCorrectAccessTokenPublish_TwoWayRpc_Success() throws Exception {
processClientDeviceWithCorrectAccessTokenPublish_TwoWayRpc_Success();
}
@Test
public void testClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_InvalidTypeMessage_INVALID_ARGUMENTS() throws Exception {
processClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_InvalidTypeMessage_INVALID_ARGUMENTS();
}
@Test
public void testClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_InBirthNotHaveMetric_BAD_REQUEST_PARAMS() throws Exception {
processClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_InvalidTypeMessage_INVALID_ARGUMENTS();
}
}

113
application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/AbstractMqttV5ClientSparkplugTelemetryTest.java

@ -0,0 +1,113 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.mqtt.sparkplug.timeseries;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto;
import org.thingsboard.server.transport.mqtt.sparkplug.AbstractMqttV5ClientSparkplugTest;
import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import static org.awaitility.Awaitility.await;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE;
/**
* Created by nickAS21 on 12.01.23
*/
@Slf4j
public abstract class AbstractMqttV5ClientSparkplugTelemetryTest extends AbstractMqttV5ClientSparkplugTest {
protected void processClientWithCorrectAccessTokenPublishNBIRTH() throws Exception {
clientWithCorrectNodeAccessTokenWithNDEATH();
List<String> listKeys = connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32());
Assert.assertTrue("Connection node is failed", client.isConnected());
AtomicReference<ListenableFuture<List<TsKvEntry>>> finalFuture = new AtomicReference<>();
await(alias + SparkplugMessageType.NBIRTH.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
finalFuture.set(tsService.findLatest(tenantId, savedGateway.getId(), listKeys));
return !finalFuture.get().get().isEmpty();
});
Assert.assertEquals(listKeys.size(), finalFuture.get().get().size());
}
protected void processClientWithCorrectAccessTokenPushNodeMetricBuildPrimitiveSimple() throws Exception {
List<String> listKeys = new ArrayList<>();
clientWithCorrectNodeAccessTokenWithNDEATH();
String messageTypeName = SparkplugMessageType.NDATA.name();
List<TsKvEntry> listTsKvEntry = new ArrayList<>();
SparkplugBProto.Payload.Builder ndataPayload = SparkplugBProto.Payload.newBuilder()
.setTimestamp(calendar.getTimeInMillis())
.setSeq(getSeqNum());
long ts = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS;
createdAddMetricValuePrimitiveTsKv(listTsKvEntry, listKeys, ndataPayload, ts);
if (client.isConnected()) {
client.publish(NAMESPACE + "/" + groupId + "/" + messageTypeName + "/" + edgeNode,
ndataPayload.build().toByteArray(), 0, false);
}
AtomicReference<ListenableFuture<List<TsKvEntry>>> finalFuture = new AtomicReference<>();
await(alias + SparkplugMessageType.NDATA.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
finalFuture.set(tsService.findAllLatest(tenantId, savedGateway.getId()));
return finalFuture.get().get().size() == (listTsKvEntry.size() + 1);
});
Assert.assertTrue("Actual tsKvEntrys is not containsAll Expected tsKvEntrys", finalFuture.get().get().containsAll(listTsKvEntry));
}
protected void processClientWithCorrectAccessTokenPushNodeMetricBuildArraysPrimitiveSimple() throws Exception {
clientWithCorrectNodeAccessTokenWithNDEATH();
String messageTypeName = SparkplugMessageType.NDATA.name();
List<String> listKeys = new ArrayList<>();
List<TsKvEntry> listTsKvEntry = new ArrayList<>();
SparkplugBProto.Payload.Builder ndataPayload = SparkplugBProto.Payload.newBuilder()
.setTimestamp(calendar.getTimeInMillis())
.setSeq(getSeqNum());
long ts = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS;
createdAddMetricValueArraysPrimitiveTsKv(listTsKvEntry, listKeys, ndataPayload, ts);
if (client.isConnected()) {
client.publish(NAMESPACE + "/" + groupId + "/" + messageTypeName + "/" + edgeNode,
ndataPayload.build().toByteArray(), 0, false);
}
AtomicReference<ListenableFuture<List<TsKvEntry>>> finalFuture = new AtomicReference<>();
await(alias + SparkplugMessageType.NDATA.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
finalFuture.set(tsService.findAllLatest(tenantId, savedGateway.getId()));
return finalFuture.get().get().size() == (listTsKvEntry.size() + 1);
});
Assert.assertTrue("Actual tsKvEntrys is not containsAll Expected tsKvEntrys", finalFuture.get().get().containsAll(listTsKvEntry));
}
}

56
application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/MqttV5ClientSparkplugBTelemetryTest.java

@ -0,0 +1,56 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.mqtt.sparkplug.timeseries;
import org.eclipse.paho.mqttv5.common.MqttException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.server.dao.service.DaoSqlTest;
/**
* Created by nickAS21 on 12.01.23
*/
@DaoSqlTest
public class MqttV5ClientSparkplugBTelemetryTest extends AbstractMqttV5ClientSparkplugTelemetryTest {
@Before
public void beforeTest() throws Exception {
beforeSparkplugTest();
}
@After
public void afterTest () throws MqttException {
if (client.isConnected()) {
client.disconnect(); }
}
@Test
public void testClientWithCorrectAccessTokenPublishNBIRTH() throws Exception {
processClientWithCorrectAccessTokenPublishNBIRTH();
}
@Test
public void testClientWithCorrectAccessTokenPushNodeMetricBuildPrimitiveSimple() throws Exception {
processClientWithCorrectAccessTokenPushNodeMetricBuildPrimitiveSimple();
}
@Test
public void testClientWithCorrectAccessTokenPushNodeMetricBuildPArraysPrimitiveSimple() throws Exception {
processClientWithCorrectAccessTokenPushNodeMetricBuildArraysPrimitiveSimple();
}
}

173
application/src/test/java/org/thingsboard/server/util/EventDeduplicationExecutorTest.java

@ -1,173 +0,0 @@
/**
* Copyright © 2016-2023 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.util;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.utils.EventDeduplicationExecutor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
@Slf4j
@RunWith(MockitoJUnitRunner.class)
public class EventDeduplicationExecutorTest {
ThingsBoardThreadFactory threadFactory = ThingsBoardThreadFactory.forName(getClass().getSimpleName());
ExecutorService executor;
@After
public void tearDown() throws Exception {
if (executor != null) {
executor.shutdownNow();
}
}
@Test
public void testSimpleFlowSameThread() throws InterruptedException {
simpleFlow(MoreExecutors.newDirectExecutorService());
}
@Test
public void testPeriodicFlowSameThread() throws InterruptedException {
periodicFlow(MoreExecutors.newDirectExecutorService());
}
@Test
public void testExceptionFlowSameThread() throws InterruptedException {
exceptionFlow(MoreExecutors.newDirectExecutorService());
}
@Test
public void testSimpleFlowSingleThread() throws InterruptedException {
executor = Executors.newSingleThreadExecutor(threadFactory);
simpleFlow(executor);
}
@Test
public void testPeriodicFlowSingleThread() throws InterruptedException {
executor = Executors.newSingleThreadExecutor(threadFactory);
periodicFlow(executor);
}
@Test
public void testExceptionFlowSingleThread() throws InterruptedException {
executor = Executors.newSingleThreadExecutor(threadFactory);
exceptionFlow(executor);
}
@Test
public void testSimpleFlowMultiThread() throws InterruptedException {
executor = Executors.newFixedThreadPool(3, threadFactory);
simpleFlow(executor);
}
@Test
public void testPeriodicFlowMultiThread() throws InterruptedException {
executor = Executors.newFixedThreadPool(3, threadFactory);
periodicFlow(executor);
}
@Test
public void testExceptionFlowMultiThread() throws InterruptedException {
executor = Executors.newFixedThreadPool(3, threadFactory);
exceptionFlow(executor);
}
private void simpleFlow(ExecutorService executorService) throws InterruptedException {
try {
Consumer<String> function = Mockito.spy(StringConsumer.class);
EventDeduplicationExecutor<String> executor = new EventDeduplicationExecutor<>(EventDeduplicationExecutorTest.class.getSimpleName(), executorService, function);
String params1 = "params1";
String params2 = "params2";
String params3 = "params3";
executor.submit(params1);
executor.submit(params2);
executor.submit(params3);
Thread.sleep(500);
Mockito.verify(function).accept(params1);
Mockito.verify(function).accept(params3);
} finally {
executorService.shutdownNow();
}
}
private void periodicFlow(ExecutorService executorService) throws InterruptedException {
try {
Consumer<String> function = Mockito.spy(StringConsumer.class);
EventDeduplicationExecutor<String> executor = new EventDeduplicationExecutor<>(EventDeduplicationExecutorTest.class.getSimpleName(), executorService, function);
String params1 = "params1";
String params2 = "params2";
String params3 = "params3";
executor.submit(params1);
Thread.sleep(500);
executor.submit(params2);
Thread.sleep(500);
executor.submit(params3);
Thread.sleep(500);
Mockito.verify(function).accept(params1);
Mockito.verify(function).accept(params2);
Mockito.verify(function).accept(params3);
} finally {
executorService.shutdownNow();
}
}
private void exceptionFlow(ExecutorService executorService) throws InterruptedException {
try {
Consumer<String> function = Mockito.spy(StringConsumer.class);
EventDeduplicationExecutor<String> executor = new EventDeduplicationExecutor<>(EventDeduplicationExecutorTest.class.getSimpleName(), executorService, function);
String params1 = "params1";
String params2 = "params2";
String params3 = "params3";
Mockito.doThrow(new RuntimeException()).when(function).accept("params1");
executor.submit(params1);
executor.submit(params2);
Thread.sleep(500);
executor.submit(params3);
Thread.sleep(500);
Mockito.verify(function).accept(params2);
Mockito.verify(function).accept(params3);
} finally {
executorService.shutdownNow();
}
}
public static class StringConsumer implements Consumer<String> {
@Override
public void accept(String s) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}

4
application/src/test/resources/application-test.properties

@ -1,3 +1,4 @@
js.evaluator=mock
transport.lwm2m.server.security.credentials.enabled=true transport.lwm2m.server.security.credentials.enabled=true
transport.lwm2m.server.security.credentials.type=KEYSTORE transport.lwm2m.server.security.credentials.type=KEYSTORE
transport.lwm2m.server.security.credentials.keystore.store_file=lwm2m/credentials/lwm2mserver.jks transport.lwm2m.server.security.credentials.keystore.store_file=lwm2m/credentials/lwm2mserver.jks
@ -20,6 +21,9 @@ transport.mqtt.enabled=false
transport.coap.enabled=false transport.coap.enabled=false
transport.lwm2m.enabled=false transport.lwm2m.enabled=false
transport.snmp.enabled=false transport.snmp.enabled=false
coap.enabled=false
integrations.rpc.enabled=false
service.integrations.supported=NONE
# Low latency settings to perform tests as fast as possible # Low latency settings to perform tests as fast as possible
sql.attributes.batch_max_delay=5 sql.attributes.batch_max_delay=5

4
application/src/test/resources/logback-test.xml

@ -13,8 +13,10 @@
<logger name="org.springframework" level="WARN"/> <logger name="org.springframework" level="WARN"/>
<logger name="org.springframework.boot.test" level="WARN"/> <logger name="org.springframework.boot.test" level="WARN"/>
<logger name="org.apache.cassandra" level="WARN"/> <logger name="org.apache.cassandra" level="WARN"/>
<logger name="org.cassandraunit" level="INFO"/> <logger name="org.testcontainers" level="INFO" />
<logger name="org.eclipse.leshan" level="INFO"/> <logger name="org.eclipse.leshan" level="INFO"/>
<logger name="org.thingsboard.server.controller.AbstractWebTest" level="INFO"/>
<!-- mute TelemetryEdgeSqlTest that causes a lot of randomly generated errors --> <!-- mute TelemetryEdgeSqlTest that causes a lot of randomly generated errors -->
<logger name="org.thingsboard.server.service.edge.rpc.EdgeGrpcSession" level="OFF"/> <logger name="org.thingsboard.server.service.edge.rpc.EdgeGrpcSession" level="OFF"/>

4
common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java

@ -108,6 +108,10 @@ public abstract class RedisTbTransactionalCache<K extends Serializable, V extend
@Override @Override
public void evict(Collection<K> keys) { public void evict(Collection<K> keys) {
//Redis expects at least 1 key to delete. Otherwise - ERR wrong number of arguments for 'del' command
if (keys.isEmpty()) {
return;
}
try (var connection = connectionFactory.getConnection()) { try (var connection = connectionFactory.getConnection()) {
connection.del(keys.stream().map(this::getRawKey).toArray(byte[][]::new)); connection.del(keys.stream().map(this::getRawKey).toArray(byte[][]::new));
} }

1
common/cluster-api/src/main/proto/queue.proto

@ -186,6 +186,7 @@ message GetOrCreateDeviceFromGatewayRequestMsg {
int64 gatewayIdLSB = 2; int64 gatewayIdLSB = 2;
string deviceName = 3; string deviceName = 3;
string deviceType = 4; string deviceType = 4;
bool sparkplug = 5;
} }
message GetOrCreateDeviceFromGatewayResponseMsg { message GetOrCreateDeviceFromGatewayResponseMsg {

85
common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmApiCallResult.java

@ -0,0 +1,85 @@
/**
* Copyright © 2016-2023 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.alarm;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.id.EntityId;
import java.util.List;
@Data
public class AlarmApiCallResult {
private final boolean successful;
private final boolean created;
private final boolean modified;
private final boolean cleared;
private final AlarmInfo alarm;
private final Alarm old;
private final List<EntityId> propagatedEntitiesList;
@Builder
private AlarmApiCallResult(boolean successful, boolean created, boolean modified, boolean cleared, AlarmInfo alarm, Alarm old, List<EntityId> propagatedEntitiesList) {
this.successful = successful;
this.created = created;
this.modified = modified;
this.cleared = cleared;
this.alarm = alarm;
this.old = old;
this.propagatedEntitiesList = propagatedEntitiesList;
}
public AlarmApiCallResult(AlarmApiCallResult other, List<EntityId> propagatedEntitiesList) {
this.successful = other.successful;
this.created = other.created;
this.modified = other.modified;
this.cleared = other.cleared;
this.alarm = other.alarm;
this.old = other.old;
this.propagatedEntitiesList = propagatedEntitiesList;
}
public boolean isSeverityChanged() {
if (alarm == null || old == null) {
return false;
} else {
return !alarm.getSeverity().equals(old.getSeverity());
}
}
public AlarmSeverity getOldSeverity() {
return isSeverityChanged() ? old.getSeverity() : null;
}
public boolean isPropagationChanged() {
if (created) {
return true;
}
if (alarm == null || old == null) {
return false;
}
return (alarm.isPropagate() != old.isPropagate()) ||
(alarm.isPropagateToOwner() != old.isPropagateToOwner()) ||
(alarm.isPropagateToTenant() != old.isPropagateToTenant()) ||
(!alarm.getPropagateRelationTypes().equals(old.getPropagateRelationTypes()));
}
}

21
common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmOperationResult.java

@ -16,16 +16,20 @@
package org.thingsboard.server.dao.alarm; package org.thingsboard.server.dao.alarm;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmAssigneeUpdate;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@Builder
@Data @Data
@AllArgsConstructor @AllArgsConstructor
@Deprecated
public class AlarmOperationResult { public class AlarmOperationResult {
private final Alarm alarm; private final Alarm alarm;
private final boolean successful; private final boolean successful;
@ -40,4 +44,21 @@ public class AlarmOperationResult {
public AlarmOperationResult(Alarm alarm, boolean successful, List<EntityId> propagatedEntitiesList) { public AlarmOperationResult(Alarm alarm, boolean successful, List<EntityId> propagatedEntitiesList) {
this(alarm, successful, false, null, propagatedEntitiesList); this(alarm, successful, false, null, propagatedEntitiesList);
} }
public AlarmOperationResult(Alarm alarm, boolean successful, boolean created, List<EntityId> propagatedEntitiesList) {
this.alarm = alarm;
this.successful = successful;
this.created = created;
this.propagatedEntitiesList = propagatedEntitiesList;
this.oldSeverity = null;
}
//Temporary while we have not removed the AlarmOperationResult.
public AlarmOperationResult(AlarmApiCallResult result) {
this.alarm = result.getAlarm() != null ? new Alarm(result.getAlarm()) : null;
this.successful = result.isSuccessful() && (result.isCreated() || result.isModified());
this.created = result.isCreated();
this.oldSeverity = result.getOldSeverity();
this.propagatedEntitiesList = result.getPropagatedEntitiesList();
}
} }

60
common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java

@ -23,10 +23,13 @@ import org.thingsboard.server.common.data.alarm.AlarmQuery;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest;
import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest;
import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmData;
import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.query.AlarmDataQuery;
@ -34,35 +37,76 @@ import org.thingsboard.server.dao.entity.EntityDaoService;
import java.util.Collection; import java.util.Collection;
/**
* Created by ashvayka on 11.05.17.
*/
public interface AlarmService extends EntityDaoService { public interface AlarmService extends EntityDaoService {
/*
* New API, since 3.5.
*/
/**
* Designed for atomic operations over active alarms.
* Only one active alarm may exist for the pair {originatorId, alarmType}
*/
AlarmApiCallResult createAlarm(AlarmCreateOrUpdateActiveRequest request);
/**
* Designed for atomic operations over active alarms.
* Only one active alarm may exist for the pair {originatorId, alarmType}
*/
AlarmApiCallResult createAlarm(AlarmCreateOrUpdateActiveRequest request, boolean alarmCreationEnabled);
/**
* Designed to update existing alarm. Accepts only part of the alarm fields.
*/
AlarmApiCallResult updateAlarm(AlarmUpdateRequest request);
AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId alarmId, long ackTs);
AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details);
AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long ts);
AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long ts);
AlarmApiCallResult delAlarm(TenantId tenantId, AlarmId alarmId);
/*
* Legacy API, before 3.5.
*/
@Deprecated(since = "3.5.0", forRemoval = true)
AlarmOperationResult createOrUpdateAlarm(Alarm alarm); AlarmOperationResult createOrUpdateAlarm(Alarm alarm);
@Deprecated(since = "3.5.0", forRemoval = true)
AlarmOperationResult createOrUpdateAlarm(Alarm alarm, boolean alarmCreationEnabled); AlarmOperationResult createOrUpdateAlarm(Alarm alarm, boolean alarmCreationEnabled);
AlarmOperationResult deleteAlarm(TenantId tenantId, AlarmId alarmId); @Deprecated(since = "3.5.0", forRemoval = true)
ListenableFuture<AlarmOperationResult> ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTs); ListenableFuture<AlarmOperationResult> ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTs);
@Deprecated(since = "3.5.0", forRemoval = true)
ListenableFuture<AlarmOperationResult> clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs); ListenableFuture<AlarmOperationResult> clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs);
@Deprecated(since = "3.5.0", forRemoval = true)
AlarmOperationResult deleteAlarm(TenantId tenantId, AlarmId alarmId);
@Deprecated(since = "3.5.0", forRemoval = true)
ListenableFuture<Alarm> findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type);
// Other API
Alarm findAlarmById(TenantId tenantId, AlarmId alarmId); Alarm findAlarmById(TenantId tenantId, AlarmId alarmId);
ListenableFuture<Alarm> findAlarmByIdAsync(TenantId tenantId, AlarmId alarmId); ListenableFuture<Alarm> findAlarmByIdAsync(TenantId tenantId, AlarmId alarmId);
ListenableFuture<AlarmInfo> findAlarmInfoByIdAsync(TenantId tenantId, AlarmId alarmId); AlarmInfo findAlarmInfoById(TenantId tenantId, AlarmId alarmId);
ListenableFuture<PageData<AlarmInfo>> findAlarms(TenantId tenantId, AlarmQuery query); ListenableFuture<PageData<AlarmInfo>> findAlarms(TenantId tenantId, AlarmQuery query);
ListenableFuture<PageData<AlarmInfo>> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query); ListenableFuture<PageData<AlarmInfo>> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query);
AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus,
AlarmStatus alarmStatus); AlarmStatus alarmStatus, String assigneeId);
ListenableFuture<Alarm> findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type); Alarm findLatestActiveByOriginatorAndType(TenantId tenantId, EntityId originator, String type);
PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId, PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId,
AlarmDataQuery query, Collection<EntityId> orderedEntityIds); AlarmDataQuery query, Collection<EntityId> orderedEntityIds);

6
common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.dao.entity; package org.thingsboard.server.dao.entity;
import org.thingsboard.server.common.data.id.NameLabelAndCustomerDetails;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -29,10 +30,13 @@ public interface EntityService {
Optional<String> fetchEntityName(TenantId tenantId, EntityId entityId); Optional<String> fetchEntityName(TenantId tenantId, EntityId entityId);
Optional<String> fetchEntityLabel(TenantId tenantId, EntityId entityId);
Optional<CustomerId> fetchEntityCustomerId(TenantId tenantId, EntityId entityId); Optional<CustomerId> fetchEntityCustomerId(TenantId tenantId, EntityId entityId);
Optional<NameLabelAndCustomerDetails> fetchNameLabelAndCustomerDetails(TenantId tenantId, EntityId entityId);
long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query); long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query);
PageData<EntityData> findEntityDataByQuery(TenantId tenantId, CustomerId customerId, EntityDataQuery query); PageData<EntityData> findEntityDataByQuery(TenantId tenantId, CustomerId customerId, EntityDataQuery query);
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java

@ -21,7 +21,7 @@ import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.common.data.validation.NoXss;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public abstract class ContactBased<I extends UUIDBased> extends SearchTextBasedWithAdditionalInfo<I> implements HasName { public abstract class ContactBased<I extends UUIDBased> extends SearchTextBasedWithAdditionalInfo<I> implements HasEmail {
private static final long serialVersionUID = 5047448057830660988L; private static final long serialVersionUID = 5047448057830660988L;

2
common/data/src/main/java/org/thingsboard/server/common/data/Customer.java

@ -29,7 +29,7 @@ import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.common.data.validation.NoXss;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class Customer extends ContactBased<CustomerId> implements HasTenantId, ExportableEntity<CustomerId> { public class Customer extends ContactBased<CustomerId> implements HasTenantId, ExportableEntity<CustomerId>, HasTitle {
private static final long serialVersionUID = -1599722990298929275L; private static final long serialVersionUID = -1599722990298929275L;

2
common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java

@ -31,7 +31,7 @@ import java.util.Objects;
import java.util.Set; import java.util.Set;
@ApiModel @ApiModel
public class DashboardInfo extends SearchTextBased<DashboardId> implements HasName, HasTenantId { public class DashboardInfo extends SearchTextBased<DashboardId> implements HasName, HasTenantId, HasTitle {
private TenantId tenantId; private TenantId tenantId;
@NoXss @NoXss

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

@ -74,6 +74,8 @@ public class DataConstants {
public static final String TIMESERIES_DELETED = "TIMESERIES_DELETED"; public static final String TIMESERIES_DELETED = "TIMESERIES_DELETED";
public static final String ALARM_ACK = "ALARM_ACK"; public static final String ALARM_ACK = "ALARM_ACK";
public static final String ALARM_CLEAR = "ALARM_CLEAR"; public static final String ALARM_CLEAR = "ALARM_CLEAR";
public static final String ALARM_ASSIGN = "ALARM_ASSIGN";
public static final String ALARM_UNASSIGN = "ALARM_UNASSIGN";
public static final String ALARM_DELETE = "ALARM_DELETE"; public static final String ALARM_DELETE = "ALARM_DELETE";
public static final String ENTITY_ASSIGNED_FROM_TENANT = "ENTITY_ASSIGNED_FROM_TENANT"; 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 ENTITY_ASSIGNED_TO_TENANT = "ENTITY_ASSIGNED_TO_TENANT";

2
common/data/src/main/java/org/thingsboard/server/common/data/Device.java

@ -40,7 +40,7 @@ import java.util.Optional;
@ApiModel @ApiModel
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Slf4j @Slf4j
public class Device extends SearchTextBasedWithAdditionalInfo<DeviceId> implements HasName, HasTenantId, HasCustomerId, HasOtaPackage, ExportableEntity<DeviceId> { public class Device extends SearchTextBasedWithAdditionalInfo<DeviceId> implements HasLabel, HasTenantId, HasCustomerId, HasOtaPackage, ExportableEntity<DeviceId> {
private static final long serialVersionUID = 2807343040519543363L; private static final long serialVersionUID = 2807343040519543363L;

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

@ -19,5 +19,25 @@ package org.thingsboard.server.common.data;
* @author Andrew Shvayka * @author Andrew Shvayka
*/ */
public enum EntityType { public enum EntityType {
TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, RULE_CHAIN, RULE_NODE, ENTITY_VIEW, WIDGETS_BUNDLE, WIDGET_TYPE, TENANT_PROFILE, DEVICE_PROFILE, ASSET_PROFILE, API_USAGE_STATE, TB_RESOURCE, OTA_PACKAGE, EDGE, RPC, QUEUE; TENANT,
CUSTOMER,
USER,
DASHBOARD,
ASSET,
DEVICE,
ALARM,
RULE_CHAIN,
RULE_NODE,
ENTITY_VIEW,
WIDGETS_BUNDLE,
WIDGET_TYPE,
TENANT_PROFILE,
DEVICE_PROFILE,
ASSET_PROFILE,
API_USAGE_STATE,
TB_RESOURCE,
OTA_PACKAGE,
EDGE,
RPC,
QUEUE
} }

11
ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.scss → common/data/src/main/java/org/thingsboard/server/common/data/HasEmail.java

@ -13,9 +13,10 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
:host ::ng-deep { package org.thingsboard.server.common.data;
.mat-form-field-infix {
width: auto; public interface HasEmail extends HasName {
min-width: 100px;
} String getEmail();
} }

12
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-duration-predicate-value.component.scss → common/data/src/main/java/org/thingsboard/server/common/data/HasLabel.java

@ -13,10 +13,10 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
:host ::ng-deep { package org.thingsboard.server.common.data;
.source-attribute {
.mat-form-field-infix{ public interface HasLabel extends HasName {
width: 100%;
} String getLabel();
}
} }

10
ui-ngx/src/app/modules/home/components/filter/filter-select.component.scss → common/data/src/main/java/org/thingsboard/server/common/data/HasTitle.java

@ -13,12 +13,10 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
:host { package org.thingsboard.server.common.data;
} public interface HasTitle {
String getTitle();
:host ::ng-deep {
.mat-form-field-infix {
border-top: none;
}
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java

@ -36,7 +36,7 @@ import org.thingsboard.server.common.data.ota.OtaPackageType;
@Slf4j @Slf4j
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class OtaPackageInfo extends SearchTextBasedWithAdditionalInfo<OtaPackageId> implements HasName, HasTenantId { public class OtaPackageInfo extends SearchTextBasedWithAdditionalInfo<OtaPackageId> implements HasName, HasTenantId, HasTitle {
private static final long serialVersionUID = 3168391583570815419L; private static final long serialVersionUID = 3168391583570815419L;

12
common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java

@ -18,9 +18,14 @@ package org.thingsboard.server.common.data;
import com.google.common.base.Splitter; import com.google.common.base.Splitter;
import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomStringUtils;
import java.security.SecureRandom;
import java.util.Base64;
import static org.apache.commons.lang3.StringUtils.repeat; import static org.apache.commons.lang3.StringUtils.repeat;
public class StringUtils { public class StringUtils {
public static final SecureRandom RANDOM = new SecureRandom();
public static final String EMPTY = ""; public static final String EMPTY = "";
public static final int INDEX_NOT_FOUND = -1; public static final int INDEX_NOT_FOUND = -1;
@ -180,4 +185,11 @@ public class StringUtils {
return RandomStringUtils.randomAlphabetic(count); return RandomStringUtils.randomAlphabetic(count);
} }
public static String generateSafeToken(int length) {
byte[] bytes = new byte[length];
RANDOM.nextBytes(bytes);
Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding();
return encoder.encodeToString(bytes);
}
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java

@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.validation.NoXss;
@ApiModel @ApiModel
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class Tenant extends ContactBased<TenantId> implements HasTenantId { public class Tenant extends ContactBased<TenantId> implements HasTenantId, HasTitle {
private static final long serialVersionUID = 8057243243859922101L; private static final long serialVersionUID = 8057243243859922101L;

55
common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.common.data.alarm; package org.thingsboard.server.common.data.alarm;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModel;
@ -22,6 +23,7 @@ import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.HasCustomerId; import org.thingsboard.server.common.data.HasCustomerId;
import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasName;
@ -30,6 +32,7 @@ import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.common.data.validation.NoXss;
@ -40,8 +43,10 @@ import java.util.List;
*/ */
@ApiModel @ApiModel
@Data @Data
@EqualsAndHashCode(callSuper = true)
@Builder @Builder
@AllArgsConstructor @AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class Alarm extends BaseData<AlarmId> implements HasName, HasTenantId, HasCustomerId { public class Alarm extends BaseData<AlarmId> implements HasName, HasTenantId, HasCustomerId {
@ApiModelProperty(position = 3, value = "JSON object with Tenant Id", accessMode = ApiModelProperty.AccessMode.READ_ONLY) @ApiModelProperty(position = 3, value = "JSON object with Tenant Id", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
@ -58,25 +63,31 @@ public class Alarm extends BaseData<AlarmId> implements HasName, HasTenantId, Ha
private EntityId originator; private EntityId originator;
@ApiModelProperty(position = 8, required = true, value = "Alarm severity", example = "CRITICAL") @ApiModelProperty(position = 8, required = true, value = "Alarm severity", example = "CRITICAL")
private AlarmSeverity severity; private AlarmSeverity severity;
@ApiModelProperty(position = 9, required = true, value = "Alarm status", example = "CLEARED_UNACK") @ApiModelProperty(position = 9, required = true, value = "Acknowledged", example = "true")
private AlarmStatus status; private boolean acknowledged;
@ApiModelProperty(position = 10, value = "Timestamp of the alarm start time, in milliseconds", example = "1634058704565") @ApiModelProperty(position = 10, required = true, value = "Cleared", example = "false")
private boolean cleared;
@ApiModelProperty(position = 11, value = "Alarm assignee user id")
private UserId assigneeId;
@ApiModelProperty(position = 12, value = "Timestamp of the alarm start time, in milliseconds", example = "1634058704565")
private long startTs; private long startTs;
@ApiModelProperty(position = 11, value = "Timestamp of the alarm end time(last time update), in milliseconds", example = "1634111163522") @ApiModelProperty(position = 13, value = "Timestamp of the alarm end time(last time update), in milliseconds", example = "1634111163522")
private long endTs; private long endTs;
@ApiModelProperty(position = 12, value = "Timestamp of the alarm acknowledgement, in milliseconds", example = "1634115221948") @ApiModelProperty(position = 14, value = "Timestamp of the alarm acknowledgement, in milliseconds", example = "1634115221948")
private long ackTs; private long ackTs;
@ApiModelProperty(position = 13, value = "Timestamp of the alarm clearing, in milliseconds", example = "1634114528465") @ApiModelProperty(position = 15, value = "Timestamp of the alarm clearing, in milliseconds", example = "1634114528465")
private long clearTs; private long clearTs;
@ApiModelProperty(position = 14, value = "JSON object with alarm details") @ApiModelProperty(position = 16, value = "Timestamp of the alarm assignment, in milliseconds", example = "1634115928465")
private long assignTs;
@ApiModelProperty(position = 17, value = "JSON object with alarm details")
private transient JsonNode details; private transient JsonNode details;
@ApiModelProperty(position = 15, value = "Propagation flag to specify if alarm should be propagated to parent entities of alarm originator", example = "true") @ApiModelProperty(position = 18, value = "Propagation flag to specify if alarm should be propagated to parent entities of alarm originator", example = "true")
private boolean propagate; private boolean propagate;
@ApiModelProperty(position = 16, value = "Propagation flag to specify if alarm should be propagated to the owner (tenant or customer) of alarm originator", example = "true") @ApiModelProperty(position = 19, value = "Propagation flag to specify if alarm should be propagated to the owner (tenant or customer) of alarm originator", example = "true")
private boolean propagateToOwner; private boolean propagateToOwner;
@ApiModelProperty(position = 17, value = "Propagation flag to specify if alarm should be propagated to the tenant entity", example = "true") @ApiModelProperty(position = 20, value = "Propagation flag to specify if alarm should be propagated to the tenant entity", example = "true")
private boolean propagateToTenant; private boolean propagateToTenant;
@ApiModelProperty(position = 18, value = "JSON array of relation types that should be used for propagation. " + @ApiModelProperty(position = 21, value = "JSON array of relation types that should be used for propagation. " +
"By default, 'propagateRelationTypes' array is empty which means that the alarm will be propagated based on any relation type to parent entities. " + "By default, 'propagateRelationTypes' array is empty which means that the alarm will be propagated based on any relation type to parent entities. " +
"This parameter should be used only in case when 'propagate' parameter is set to true, otherwise, 'propagateRelationTypes' array will be ignored.") "This parameter should be used only in case when 'propagate' parameter is set to true, otherwise, 'propagateRelationTypes' array will be ignored.")
private List<String> propagateRelationTypes; private List<String> propagateRelationTypes;
@ -97,11 +108,14 @@ public class Alarm extends BaseData<AlarmId> implements HasName, HasTenantId, Ha
this.type = alarm.getType(); this.type = alarm.getType();
this.originator = alarm.getOriginator(); this.originator = alarm.getOriginator();
this.severity = alarm.getSeverity(); this.severity = alarm.getSeverity();
this.status = alarm.getStatus(); this.assigneeId = alarm.getAssigneeId();
this.startTs = alarm.getStartTs(); this.startTs = alarm.getStartTs();
this.endTs = alarm.getEndTs(); this.endTs = alarm.getEndTs();
this.acknowledged = alarm.isAcknowledged();
this.ackTs = alarm.getAckTs(); this.ackTs = alarm.getAckTs();
this.clearTs = alarm.getClearTs(); this.clearTs = alarm.getClearTs();
this.cleared = alarm.isCleared();
this.assignTs = alarm.getAssignTs();
this.details = alarm.getDetails(); this.details = alarm.getDetails();
this.propagate = alarm.isPropagate(); this.propagate = alarm.isPropagate();
this.propagateToOwner = alarm.isPropagateToOwner(); this.propagateToOwner = alarm.isPropagateToOwner();
@ -119,7 +133,7 @@ public class Alarm extends BaseData<AlarmId> implements HasName, HasTenantId, Ha
@ApiModelProperty(position = 1, value = "JSON object with the alarm Id. " + @ApiModelProperty(position = 1, value = "JSON object with the alarm Id. " +
"Specify this field to update the alarm. " + "Specify this field to update the alarm. " +
"Referencing non-existing alarm Id will cause error. " + "Referencing non-existing alarm Id will cause error. " +
"Omit this field to create new alarm." ) "Omit this field to create new alarm.")
@Override @Override
public AlarmId getId() { public AlarmId getId() {
return super.getId(); return super.getId();
@ -132,4 +146,19 @@ public class Alarm extends BaseData<AlarmId> implements HasName, HasTenantId, Ha
return super.getCreatedTime(); return super.getCreatedTime();
} }
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
@ApiModelProperty(position = 22, required = true, value = "status of the Alarm", example = "ACTIVE_UNACK", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
public AlarmStatus getStatus() {
return toStatus(cleared, acknowledged);
}
public static AlarmStatus toStatus(boolean cleared, boolean acknowledged) {
if (cleared) {
return acknowledged ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK;
} else {
return acknowledged ? AlarmStatus.ACTIVE_ACK : AlarmStatus.ACTIVE_UNACK;
}
}
} }

42
ui-ngx/src/app/modules/home/pages/edge/edge-table-header.component.scss → common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmAssignee.java

@ -13,37 +13,25 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@import '../../../../../scss/constants'; package org.thingsboard.server.common.data.alarm;
:host { import lombok.AllArgsConstructor;
flex: 1; import lombok.Builder;
display: flex; import lombok.Data;
justify-content: flex-start; import org.thingsboard.server.common.data.id.UserId;
min-width: 150px;
}
:host ::ng-deep {
tb-entity-subtype-select {
width: 100%;
mat-form-field { import java.io.Serializable;
font-size: 16px;
.mat-form-field-wrapper { @Builder
padding-bottom: 0; @AllArgsConstructor
} @Data
public class AlarmAssignee implements Serializable {
.mat-form-field-underline { private static final long serialVersionUID = 6628286223963972860L;
bottom: 0;
}
@media #{$mat-xs} { private final UserId id;
width: 100%; private final String firstName;
private final String lastName;
private final String email;
.mat-form-field-infix {
width: auto !important;
}
}
}
}
} }

18
ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.scss → common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmAssigneeUpdate.java

@ -13,8 +13,18 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
:host ::ng-deep { package org.thingsboard.server.common.data.alarm;
.mat-checkbox-label {
white-space: normal; import lombok.Data;
}
import java.io.Serializable;
@Data
public class AlarmAssigneeUpdate implements Serializable {
private static final long serialVersionUID = -2391676304697483808L;
private final boolean deleted;
private final AlarmAssignee assignee;
} }

87
common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCreateOrUpdateActiveRequest.java

@ -0,0 +1,87 @@
/**
* Copyright © 2016-2023 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.alarm;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.annotations.ApiModelProperty;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
@Data
@Builder
public class AlarmCreateOrUpdateActiveRequest implements AlarmModificationRequest {
@NotNull
@ApiModelProperty(position = 1, value = "JSON object with Tenant Id", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private TenantId tenantId;
@ApiModelProperty(position = 2, value = "JSON object with Customer Id", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private CustomerId customerId;
@NotNull
@ApiModelProperty(position = 3, required = true, value = "representing type of the Alarm", example = "High Temperature Alarm")
@Length(fieldName = "type")
private String type;
@NotNull
@ApiModelProperty(position = 4, required = true, value = "JSON object with alarm originator id")
private EntityId originator;
@NotNull
@ApiModelProperty(position = 5, required = true, value = "Alarm severity", example = "CRITICAL")
private AlarmSeverity severity;
@ApiModelProperty(position = 6, value = "Timestamp of the alarm start time, in milliseconds", example = "1634058704565")
private long startTs;
@ApiModelProperty(position = 7, value = "Timestamp of the alarm end time(last time update), in milliseconds", example = "1634111163522")
private long endTs;
@NoXss
@ApiModelProperty(position = 8, value = "JSON object with alarm details")
private JsonNode details;
@Valid
@ApiModelProperty(position = 9, value = "JSON object with propagation details")
private AlarmPropagationInfo propagation;
private UserId userId;
public static AlarmCreateOrUpdateActiveRequest fromAlarm(Alarm a) {
return fromAlarm(a, null);
}
public static AlarmCreateOrUpdateActiveRequest fromAlarm(Alarm a, UserId userId) {
return AlarmCreateOrUpdateActiveRequest.builder()
.tenantId(a.getTenantId())
.customerId(a.getCustomerId())
.type(a.getType())
.originator(a.getOriginator())
.severity((a.getSeverity()))
.startTs(a.getStartTs())
.endTs(a.getEndTs())
.details(a.getDetails())
.propagation(AlarmPropagationInfo.builder()
.propagate(a.isPropagate())
.propagateToOwner(a.isPropagateToOwner())
.propagateToTenant(a.isPropagateToTenant())
.propagateRelationTypes(a.getPropagateRelationTypes()).build())
.userId(userId)
.build();
}
}

56
common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java

@ -17,15 +17,36 @@ package org.thingsboard.server.common.data.alarm;
import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.thingsboard.server.common.data.User;
import java.util.Objects;
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@ApiModel @ApiModel
public class AlarmInfo extends Alarm { public class AlarmInfo extends Alarm {
private static final long serialVersionUID = 2807343093519543363L; private static final long serialVersionUID = 2807343093519543363L;
@Getter
@Setter
@ApiModelProperty(position = 19, value = "Alarm originator name", example = "Thermostat") @ApiModelProperty(position = 19, value = "Alarm originator name", example = "Thermostat")
private String originatorName; private String originatorName;
@Getter
@Setter
@ApiModelProperty(position = 20, value = "Alarm originator label", example = "Thermostat label")
private String originatorLabel;
@Getter
@Setter
@ApiModelProperty(position = 21, value = "Alarm assignee")
private AlarmAssignee assignee;
public AlarmInfo() { public AlarmInfo() {
super(); super();
} }
@ -34,35 +55,18 @@ public class AlarmInfo extends Alarm {
super(alarm); super(alarm);
} }
public AlarmInfo(Alarm alarm, String originatorName) { public AlarmInfo(AlarmInfo alarmInfo) {
super(alarm); super(alarmInfo);
this.originatorName = originatorName; this.originatorName = alarmInfo.originatorName;
} this.originatorLabel = alarmInfo.originatorLabel;
this.assignee = alarmInfo.getAssignee();
public String getOriginatorName() {
return originatorName;
} }
public void setOriginatorName(String originatorName) { public AlarmInfo(Alarm alarm, String originatorName, String originatorLabel, AlarmAssignee assignee) {
super(alarm);
this.originatorName = originatorName; this.originatorName = originatorName;
this.originatorLabel = originatorLabel;
this.assignee = assignee;
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
AlarmInfo alarmInfo = (AlarmInfo) o;
return originatorName != null ? originatorName.equals(alarmInfo.originatorName) : alarmInfo.originatorName == null;
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + (originatorName != null ? originatorName.hashCode() : 0);
return result;
}
} }

34
common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmModificationRequest.java

@ -0,0 +1,34 @@
/**
* Copyright © 2016-2023 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.alarm;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
public interface AlarmModificationRequest {
TenantId getTenantId();
long getStartTs();
long getEndTs();
void setStartTs(long startTs);
void setEndTs(long endTs);
UserId getUserId();
}

46
common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmPropagationInfo.java

@ -0,0 +1,46 @@
/**
* Copyright © 2016-2023 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.alarm;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.validation.NoXss;
import java.util.Collections;
import java.util.List;
@Builder
@Data
public class AlarmPropagationInfo {
public static AlarmPropagationInfo EMPTY = new AlarmPropagationInfo(false, false, false, Collections.emptyList());
@ApiModelProperty(position = 1, value = "Propagation flag to specify if alarm should be propagated to parent entities of alarm originator", example = "true")
private boolean propagate;
@ApiModelProperty(position = 2, value = "Propagation flag to specify if alarm should be propagated to the owner (tenant or customer) of alarm originator", example = "true")
private boolean propagateToOwner;
@ApiModelProperty(position = 3, value = "Propagation flag to specify if alarm should be propagated to the tenant entity", example = "true")
private boolean propagateToTenant;
@NoXss
@ApiModelProperty(position = 4, value = "JSON array of relation types that should be used for propagation. " +
"By default, 'propagateRelationTypes' array is empty which means that the alarm will be propagated based on any relation type to parent entities. " +
"This parameter should be used only in case when 'propagate' parameter is set to true, otherwise, 'propagateRelationTypes' array will be ignored.")
private List<String> propagateRelationTypes;
}

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

Loading…
Cancel
Save