Browse Source

Merge remote-tracking branch 'upstream/master' into develop/3.3-edge

pull/3811/head
Volodymyr Babak 6 years ago
parent
commit
d79d00aa87
  1. 4
      .gitignore
  2. 12
      application/pom.xml
  3. 2
      application/src/main/data/json/demo/dashboards/gateways.json
  4. 700
      application/src/main/data/json/demo/dashboards/raspberry_pi_gpio_demo_dashboard.json
  5. 264
      application/src/main/data/json/demo/dashboards/temperature___humidity_demo_dashboard.json
  6. 108
      application/src/main/data/json/demo/dashboards/thermostats.json
  7. 190
      application/src/main/data/json/demo/rule_chains/root_rule_chain.json
  8. 142
      application/src/main/data/json/demo/rule_chains/thermostat_alarms.json
  9. 23
      application/src/main/data/json/system/oauth2_config_templates/facebook_config.json
  10. 21
      application/src/main/data/json/system/oauth2_config_templates/github_config.json
  11. 24
      application/src/main/data/json/system/oauth2_config_templates/google_config.json
  12. 10
      application/src/main/data/json/system/widget_bundles/charts.json
  13. 12
      application/src/main/data/json/system/widget_bundles/input_widgets.json
  14. 3
      application/src/main/data/json/tenant/device_profile/rule_chain_template.json
  15. 73
      application/src/main/data/upgrade/3.1.1/schema_update_before.sql
  16. 40
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  17. 29
      application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
  18. 86
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  19. 7
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
  20. 6
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java
  21. 51
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
  22. 2
      application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
  23. 6
      application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java
  24. 68
      application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
  25. 267
      application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java
  26. 8
      application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
  27. 18
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  28. 2
      application/src/main/java/org/thingsboard/server/controller/AlarmController.java
  29. 7
      application/src/main/java/org/thingsboard/server/controller/AssetController.java
  30. 22
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  31. 18
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  32. 7
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  33. 7
      application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java
  34. 77
      application/src/main/java/org/thingsboard/server/controller/OAuth2ConfigTemplateController.java
  35. 91
      application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java
  36. 6
      application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
  37. 18
      application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
  38. 8
      application/src/main/java/org/thingsboard/server/controller/TenantController.java
  39. 12
      application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
  40. 10
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  41. 2
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  42. 450
      application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java
  43. 38
      application/src/main/java/org/thingsboard/server/service/apiusage/TbApiUsageStateService.java
  44. 196
      application/src/main/java/org/thingsboard/server/service/apiusage/TenantApiUsageState.java
  45. 268
      application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java
  46. 215
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  47. 75
      application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
  48. 26
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  49. 2
      application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java
  50. 2
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
  51. 177
      application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
  52. 89
      application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java
  53. 8
      application/src/main/java/org/thingsboard/server/service/profile/TbDeviceProfileCache.java
  54. 97
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
  55. 180
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  56. 48
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
  57. 11
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTenantRoutingInfoService.java
  58. 21
      application/src/main/java/org/thingsboard/server/service/queue/TbClusterService.java
  59. 33
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
  60. 64
      application/src/main/java/org/thingsboard/server/service/script/AbstractJsInvokeService.java
  61. 13
      application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsInvokeService.java
  62. 2
      application/src/main/java/org/thingsboard/server/service/script/JsExecutorService.java
  63. 5
      application/src/main/java/org/thingsboard/server/service/script/JsInvokeService.java
  64. 6
      application/src/main/java/org/thingsboard/server/service/script/NashornJsInvokeService.java
  65. 6
      application/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java
  66. 11
      application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java
  67. 41
      application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java
  68. 38
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java
  69. 78
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicMapperUtils.java
  70. 64
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java
  71. 18
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java
  72. 93
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java
  73. 4
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java
  74. 17
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java
  75. 16
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java
  76. 39
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java
  77. 5
      application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java
  78. 6
      application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java
  79. 4
      application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
  80. 27
      application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java
  81. 5
      application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java
  82. 53
      application/src/main/java/org/thingsboard/server/service/sms/AbstractSmsSender.java
  83. 42
      application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsSenderFactory.java
  84. 145
      application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java
  85. 33
      application/src/main/java/org/thingsboard/server/service/sms/SmsExecutorService.java
  86. 73
      application/src/main/java/org/thingsboard/server/service/sms/aws/AwsSmsSender.java
  87. 56
      application/src/main/java/org/thingsboard/server/service/sms/twilio/TwilioSmsSender.java
  88. 19
      application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
  89. 4
      application/src/main/java/org/thingsboard/server/service/state/DeviceStateData.java
  90. 8
      application/src/main/java/org/thingsboard/server/service/stats/DefaultRuleEngineStatisticsService.java
  91. 25
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java
  92. 6
      application/src/main/java/org/thingsboard/server/service/telemetry/AbstractSubscriptionService.java
  93. 112
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
  94. 46
      application/src/main/java/org/thingsboard/server/service/telemetry/InternalTelemetryService.java
  95. 2
      application/src/main/java/org/thingsboard/server/service/telemetry/TelemetrySubscriptionService.java
  96. 167
      application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java
  97. 2
      application/src/main/java/org/thingsboard/server/service/transport/TbCoreTransportApiService.java
  98. 40
      application/src/main/java/org/thingsboard/server/utils/MiscUtils.java
  99. 3
      application/src/main/resources/i18n/messages.properties
  100. 145
      application/src/main/resources/templates/state.disabled.ftl

4
.gitignore

@ -33,4 +33,6 @@ pom.xml.versionsBackup
**/.env
.instance_id
rebuild-docker.sh
.run/
.run/
.run/**
.run

12
application/pom.xml

@ -149,10 +149,6 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
@ -201,6 +197,14 @@
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
</dependency>
<dependency>
<groupId>com.twilio.sdk</groupId>
<artifactId>twilio</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-sns</artifactId>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>

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

@ -955,7 +955,7 @@
},
"methodName": "gateway_restart",
"methodParams": "{}",
"buttonText": "gateway restart"
"buttonText": "GATEWAY RESTART"
},
"title": "New RPC Button",
"dropShadow": true,

700
application/src/main/data/json/demo/dashboards/raspberry_pi_gpio_demo_dashboard.json

@ -1,700 +0,0 @@
{
"title": "Raspberry PI GPIO Demo Dashboard",
"configuration": {
"description": "Demo dashboard for Raspberry PI GPIO Demo",
"widgets": {
"602177f6-267b-cb87-4e8f-e23d7fb2f61c": {
"isSystemType": true,
"bundleAlias": "gpio_widgets",
"typeAlias": "raspberry_pi_gpio_control",
"type": "rpc",
"title": "New widget",
"sizeX": 6,
"sizeY": 10,
"config": {
"targetDeviceAliases": [],
"showTitle": true,
"backgroundColor": "#fff",
"color": "rgba(0, 0, 0, 0.87)",
"padding": "0px",
"settings": {
"parseGpioStatusFunction": "return body[pin] === true;",
"gpioStatusChangeRequest": {
"method": "setGpioStatus",
"paramsBody": "{\n \"pin\": \"{$pin}\",\n \"enabled\": \"{$enabled}\"\n}"
},
"requestTimeout": 500,
"switchPanelBackgroundColor": "#008a00",
"gpioStatusRequest": {
"method": "getGpioStatus",
"paramsBody": "{}"
},
"gpioList": [
{
"pin": 7,
"label": "GPIO 4 (GPCLK0)",
"row": 3,
"col": 0,
"_uniqueKey": 0
},
{
"pin": 11,
"label": "GPIO 17",
"row": 5,
"col": 0,
"_uniqueKey": 1
},
{
"pin": 12,
"label": "GPIO 18",
"row": 5,
"col": 1,
"_uniqueKey": 2
},
{
"_uniqueKey": 3,
"pin": 13,
"label": "GPIO 27",
"row": 6,
"col": 0
},
{
"_uniqueKey": 4,
"pin": 15,
"label": "GPIO 22",
"row": 7,
"col": 0
},
{
"_uniqueKey": 5,
"pin": 16,
"label": "GPIO 23",
"row": 7,
"col": 1
},
{
"_uniqueKey": 6,
"pin": 18,
"label": "GPIO 24",
"row": 8,
"col": 1
},
{
"_uniqueKey": 7,
"pin": 22,
"label": "GPIO 25",
"row": 10,
"col": 1
},
{
"_uniqueKey": 8,
"pin": 29,
"label": "GPIO 5",
"row": 14,
"col": 0
},
{
"_uniqueKey": 9,
"pin": 31,
"label": "GPIO 6",
"row": 15,
"col": 0
},
{
"_uniqueKey": 10,
"pin": 32,
"label": "GPIO 12",
"row": 15,
"col": 1
},
{
"_uniqueKey": 11,
"pin": 33,
"label": "GPIO 13",
"row": 16,
"col": 0
},
{
"_uniqueKey": 12,
"pin": 35,
"label": "GPIO 19",
"row": 17,
"col": 0
},
{
"_uniqueKey": 13,
"pin": 36,
"label": "GPIO 16",
"row": 17,
"col": 1
},
{
"_uniqueKey": 14,
"pin": 37,
"label": "GPIO 26",
"row": 18,
"col": 0
},
{
"_uniqueKey": 15,
"pin": 38,
"label": "GPIO 20",
"row": 18,
"col": 1
},
{
"_uniqueKey": 16,
"pin": 40,
"label": "GPIO 21",
"row": 19,
"col": 1
}
]
},
"title": "Raspberry Pi GPIO Control Panel",
"datasources": [],
"targetDeviceAliasIds": [
"f26b12b6-6938-e1a0-85ec-d88a1f23e382"
]
},
"row": 0,
"col": 0,
"id": "602177f6-267b-cb87-4e8f-e23d7fb2f61c"
},
"3cca52a5-e874-eb43-b444-8efa01e663c8": {
"isSystemType": true,
"bundleAlias": "gpio_widgets",
"typeAlias": "raspberry_pi_gpio_panel",
"type": "latest",
"title": "New widget",
"sizeX": 7,
"sizeY": 10,
"config": {
"showTitle": true,
"backgroundColor": "#fff",
"color": "rgba(0, 0, 0, 0.87)",
"padding": "0px",
"settings": {
"gpioList": [
{
"pin": 1,
"label": "3.3V",
"row": 0,
"col": 0,
"color": "#fc9700",
"_uniqueKey": 0
},
{
"pin": 2,
"label": "5V",
"row": 0,
"col": 1,
"color": "#fb0000",
"_uniqueKey": 1
},
{
"pin": 3,
"label": "GPIO 2 (I2C1_SDA)",
"row": 1,
"col": 0,
"color": "#02fefb",
"_uniqueKey": 2
},
{
"color": "#fb0000",
"pin": 4,
"label": "5V",
"row": 1,
"col": 1
},
{
"color": "#02fefb",
"pin": 5,
"label": "GPIO 3 (I2C1_SCL)",
"row": 2,
"col": 0
},
{
"color": "#000000",
"pin": 6,
"label": "GND",
"row": 2,
"col": 1
},
{
"color": "#00fd00",
"pin": 7,
"label": "GPIO 4 (GPCLK0)",
"row": 3,
"col": 0
},
{
"color": "#fdfb00",
"pin": 8,
"label": "GPIO 14 (UART_TXD)",
"row": 3,
"col": 1
},
{
"color": "#000000",
"pin": 9,
"label": "GND",
"row": 4,
"col": 0
},
{
"color": "#fdfb00",
"pin": 10,
"label": "GPIO 15 (UART_RXD)",
"row": 4,
"col": 1
},
{
"color": "#00fd00",
"pin": 11,
"label": "GPIO 17",
"row": 5,
"col": 0
},
{
"color": "#00fd00",
"pin": 12,
"label": "GPIO 18",
"row": 5,
"col": 1
},
{
"color": "#00fd00",
"pin": 13,
"label": "GPIO 27",
"row": 6,
"col": 0
},
{
"color": "#000000",
"pin": 14,
"label": "GND",
"row": 6,
"col": 1
},
{
"color": "#00fd00",
"pin": 15,
"label": "GPIO 22",
"row": 7,
"col": 0
},
{
"color": "#00fd00",
"pin": 16,
"label": "GPIO 23",
"row": 7,
"col": 1
},
{
"color": "#fc9700",
"pin": 17,
"label": "3.3V",
"row": 8,
"col": 0
},
{
"color": "#00fd00",
"pin": 18,
"label": "GPIO 24",
"row": 8,
"col": 1
},
{
"color": "#fd01fd",
"pin": 19,
"label": "GPIO 10 (SPI_MOSI)",
"row": 9,
"col": 0
},
{
"color": "#000000",
"pin": 20,
"label": "GND",
"row": 9,
"col": 1
},
{
"color": "#fd01fd",
"pin": 21,
"label": "GPIO 9 (SPI_MISO)",
"row": 10,
"col": 0
},
{
"color": "#00fd00",
"pin": 22,
"label": "GPIO 25",
"row": 10,
"col": 1
},
{
"color": "#fd01fd",
"pin": 23,
"label": "GPIO 11 (SPI_SCLK)",
"row": 11,
"col": 0
},
{
"color": "#fd01fd",
"pin": 24,
"label": "GPIO 8 (SPI_CE0)",
"row": 11,
"col": 1
},
{
"color": "#000000",
"pin": 25,
"label": "GND",
"row": 12,
"col": 0
},
{
"color": "#fd01fd",
"pin": 26,
"label": "GPIO 7 (SPI_CE1)",
"row": 12,
"col": 1
},
{
"color": "#ffffff",
"pin": 27,
"label": "ID_SD",
"row": 13,
"col": 0
},
{
"color": "#ffffff",
"pin": 28,
"label": "ID_SC",
"row": 13,
"col": 1
},
{
"color": "#00fd00",
"pin": 29,
"label": "GPIO 5",
"row": 14,
"col": 0
},
{
"color": "#000000",
"pin": 30,
"label": "GND",
"row": 14,
"col": 1
},
{
"color": "#00fd00",
"pin": 31,
"label": "GPIO 6",
"row": 15,
"col": 0
},
{
"color": "#00fd00",
"pin": 32,
"label": "GPIO 12",
"row": 15,
"col": 1
},
{
"color": "#00fd00",
"pin": 33,
"label": "GPIO 13",
"row": 16,
"col": 0
},
{
"color": "#000000",
"pin": 34,
"label": "GND",
"row": 16,
"col": 1
},
{
"color": "#00fd00",
"pin": 35,
"label": "GPIO 19",
"row": 17,
"col": 0
},
{
"color": "#00fd00",
"pin": 36,
"label": "GPIO 16",
"row": 17,
"col": 1
},
{
"color": "#00fd00",
"pin": 37,
"label": "GPIO 26",
"row": 18,
"col": 0
},
{
"color": "#00fd00",
"pin": 38,
"label": "GPIO 20",
"row": 18,
"col": 1
},
{
"color": "#000000",
"pin": 39,
"label": "GND",
"row": 19,
"col": 0
},
{
"color": "#00fd00",
"pin": 40,
"label": "GPIO 21",
"row": 19,
"col": 1
}
],
"ledPanelBackgroundColor": "#008a00"
},
"title": "Raspberry Pi GPIO Status Panel",
"datasources": [
{
"type": "entity",
"dataKeys": [
{
"name": "7",
"type": "attribute",
"label": "7",
"color": "#2196f3",
"settings": {},
"_hash": 0.20925966435886978
},
{
"name": "11",
"type": "attribute",
"label": "11",
"color": "#4caf50",
"settings": {},
"_hash": 0.330267349594344
},
{
"name": "12",
"type": "attribute",
"label": "12",
"color": "#f44336",
"settings": {},
"_hash": 0.5040578704481748
},
{
"name": "13",
"type": "attribute",
"label": "13",
"color": "#ffc107",
"settings": {},
"_hash": 0.588956328191639
},
{
"name": "15",
"type": "attribute",
"label": "15",
"color": "#607d8b",
"settings": {},
"_hash": 0.9229040530336119
},
{
"name": "16",
"type": "attribute",
"label": "16",
"color": "#9c27b0",
"settings": {},
"_hash": 0.8692315253041654
},
{
"name": "18",
"type": "attribute",
"label": "18",
"color": "#8bc34a",
"settings": {},
"_hash": 0.41465562857521543
},
{
"name": "22",
"type": "attribute",
"label": "22",
"color": "#3f51b5",
"settings": {},
"_hash": 0.36135260043112827
},
{
"name": "29",
"type": "attribute",
"label": "29",
"color": "#e91e63",
"settings": {},
"_hash": 0.9904592276182183
},
{
"name": "31",
"type": "attribute",
"label": "31",
"color": "#ffeb3b",
"settings": {},
"_hash": 0.038330985429919195
},
{
"name": "32",
"type": "attribute",
"label": "32",
"color": "#03a9f4",
"settings": {},
"_hash": 0.4334683890135089
},
{
"name": "33",
"type": "attribute",
"label": "33",
"color": "#ff9800",
"settings": {},
"_hash": 0.6487255992492305
},
{
"name": "35",
"type": "attribute",
"label": "35",
"color": "#673ab7",
"settings": {},
"_hash": 0.971555321150732
},
{
"name": "36",
"type": "attribute",
"label": "36",
"color": "#cddc39",
"settings": {},
"_hash": 0.7826129728424382
},
{
"name": "37",
"type": "attribute",
"label": "37",
"color": "#009688",
"settings": {},
"_hash": 0.44925676517537627
},
{
"name": "38",
"type": "attribute",
"label": "38",
"color": "#795548",
"settings": {},
"_hash": 0.051518155759787465
},
{
"name": "40",
"type": "attribute",
"label": "40",
"color": "#00bcd4",
"settings": {},
"_hash": 0.8733296686871144
}
],
"name": "RPi",
"entityAliasId": "f26b12b6-6938-e1a0-85ec-d88a1f23e382"
}
],
"timewindow": {
"realtime": {
"timewindowMs": 60000
}
}
},
"row": 0,
"col": 6,
"id": "3cca52a5-e874-eb43-b444-8efa01e663c8"
}
},
"states": {
"default": {
"name": "Default",
"root": true,
"layouts": {
"main": {
"widgets": {
"602177f6-267b-cb87-4e8f-e23d7fb2f61c": {
"sizeX": 6,
"sizeY": 10,
"row": 0,
"col": 0
},
"3cca52a5-e874-eb43-b444-8efa01e663c8": {
"sizeX": 7,
"sizeY": 10,
"row": 0,
"col": 6
}
},
"gridSettings": {
"backgroundColor": "#eeeeee",
"color": "rgba(0,0,0,0.870588)",
"columns": 24,
"margins": [
10,
10
],
"backgroundSizeMode": "100%"
}
}
}
}
},
"entityAliases": {
"f26b12b6-6938-e1a0-85ec-d88a1f23e382": {
"id": "f26b12b6-6938-e1a0-85ec-d88a1f23e382",
"alias": "RPi",
"filter": {
"type": "entityName",
"resolveMultiple": false,
"entityType": "DEVICE",
"entityNameFilter": "Raspberry Pi Demo Device"
}
}
},
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"interval": 1000,
"timewindowMs": 60000
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1498653734150,
"endTimeMs": 1498740134150
}
},
"aggregation": {
"type": "AVG",
"limit": 200
}
},
"settings": {
"stateControllerId": "default",
"showTitle": true,
"showDashboardsSelect": true,
"showEntitiesSelect": true,
"showDashboardTimewindow": true,
"showDashboardExport": true,
"toolbarAlwaysOpen": false
}
},
"name": "Raspberry PI GPIO Demo Dashboard"
}

264
application/src/main/data/json/demo/dashboards/temperature___humidity_demo_dashboard.json

@ -1,264 +0,0 @@
{
"title": "Temperature & Humidity Demo Dashboard",
"configuration": {
"description": "Demo dashboard for sample applications that upload temperature and humidity received from DHT11 or DHT22 sensors",
"widgets": {
"03e06986-1c50-e9e4-267c-2bae930ad9a2": {
"isSystemType": true,
"bundleAlias": "digital_gauges",
"typeAlias": "digital_thermometer",
"type": "latest",
"title": "New widget",
"sizeX": 5,
"sizeY": 5,
"config": {
"datasources": [
{
"type": "entity",
"dataKeys": [
{
"name": "temperature",
"type": "timeseries",
"label": "temperature",
"color": "#2196f3",
"settings": {},
"_hash": 0.3720839051412099
}
],
"name": "DHT11",
"entityAliasId": "63a93238-c13f-4403-4bcc-9ccc86bd6a62"
}
],
"timewindow": {
"realtime": {
"timewindowMs": 60000
}
},
"showTitle": false,
"backgroundColor": "#000000",
"color": "rgba(0, 0, 0, 0.87)",
"padding": "0px",
"settings": {
"maxValue": 50,
"donutStartAngle": 90,
"showValue": true,
"showMinMax": true,
"gaugeWidthScale": 1,
"levelColors": [
"#304ffe",
"#7e57c2",
"#ff4081",
"#d32f2f"
],
"refreshAnimationType": "<>",
"refreshAnimationTime": 700,
"startAnimationType": "<>",
"startAnimationTime": 700,
"titleFont": {
"family": "RobotoDraft",
"size": 12,
"style": "normal",
"weight": "500"
},
"labelFont": {
"family": "RobotoDraft",
"size": 8,
"style": "normal",
"weight": "500"
},
"valueFont": {
"family": "Segment7Standard",
"style": "normal",
"weight": "500",
"size": 18
},
"minMaxFont": {
"family": "Segment7Standard",
"size": 12,
"style": "normal",
"weight": "500"
},
"dashThickness": 1.5,
"decimals": 0,
"minValue": 0,
"units": "°C",
"gaugeColor": "#333333",
"neonGlowBrightness": 35,
"gaugeType": "donut",
"showTitle": false
},
"title": "Temperature"
},
"row": 0,
"col": 0,
"id": "03e06986-1c50-e9e4-267c-2bae930ad9a2"
},
"88808eb1-d381-9970-c852-e3499df68bd8": {
"isSystemType": true,
"bundleAlias": "digital_gauges",
"typeAlias": "digital_vertical_bar",
"type": "latest",
"title": "New widget",
"sizeX": 3,
"sizeY": 5,
"config": {
"datasources": [
{
"type": "entity",
"dataKeys": [
{
"name": "humidity",
"type": "timeseries",
"label": "humidity",
"color": "#2196f3",
"settings": {},
"_hash": 0.9492802776509441
}
],
"name": "DHT11",
"entityAliasId": "63a93238-c13f-4403-4bcc-9ccc86bd6a62"
}
],
"timewindow": {
"realtime": {
"timewindowMs": 60000
}
},
"showTitle": false,
"backgroundColor": "#000000",
"color": "rgba(0, 0, 0, 0.87)",
"padding": "0px",
"settings": {
"maxValue": 100,
"donutStartAngle": 90,
"showValue": true,
"showMinMax": true,
"gaugeWidthScale": 0.75,
"levelColors": [
"#3d5afe",
"#f44336"
],
"refreshAnimationType": "<>",
"refreshAnimationTime": 700,
"startAnimationType": "<>",
"startAnimationTime": 700,
"titleFont": {
"family": "RobotoDraft",
"size": 12,
"style": "normal",
"weight": "500"
},
"labelFont": {
"family": "RobotoDraft",
"size": 8,
"style": "normal",
"weight": "500"
},
"valueFont": {
"family": "Segment7Standard",
"style": "normal",
"weight": "500",
"size": 14
},
"minMaxFont": {
"family": "Segment7Standard",
"size": 8,
"style": "normal",
"weight": "normal",
"color": "#cccccc"
},
"neonGlowBrightness": 20,
"decimals": 0,
"showUnitTitle": true,
"gaugeColor": "#171a1c",
"gaugeType": "verticalBar",
"showTitle": false,
"minValue": 0,
"dashThickness": 1.2
},
"title": "Humidity"
},
"row": 0,
"col": 5,
"id": "88808eb1-d381-9970-c852-e3499df68bd8"
}
},
"states": {
"default": {
"name": "Default",
"root": true,
"layouts": {
"main": {
"widgets": {
"03e06986-1c50-e9e4-267c-2bae930ad9a2": {
"sizeX": 5,
"sizeY": 5,
"row": 0,
"col": 0
},
"88808eb1-d381-9970-c852-e3499df68bd8": {
"sizeX": 3,
"sizeY": 5,
"row": 0,
"col": 5
}
},
"gridSettings": {
"backgroundColor": "#eeeeee",
"color": "rgba(0,0,0,0.870588)",
"columns": 24,
"margins": [
10,
10
],
"backgroundSizeMode": "100%"
}
}
}
}
},
"entityAliases": {
"63a93238-c13f-4403-4bcc-9ccc86bd6a62": {
"id": "63a93238-c13f-4403-4bcc-9ccc86bd6a62",
"alias": "DHT11",
"filter": {
"type": "entityName",
"resolveMultiple": false,
"entityType": "DEVICE",
"entityNameFilter": "DHT11 Demo Device"
}
}
},
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"interval": 1000,
"timewindowMs": 60000
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1498653790019,
"endTimeMs": 1498740190019
}
},
"aggregation": {
"type": "AVG",
"limit": 200
}
},
"settings": {
"stateControllerId": "default",
"showTitle": true,
"showDashboardsSelect": true,
"showEntitiesSelect": true,
"showDashboardTimewindow": true,
"showDashboardExport": true,
"toolbarAlwaysOpen": false
}
},
"name": "Temperature & Humidity Demo Dashboard"
}

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

@ -147,9 +147,9 @@
"name": "Add",
"icon": "add",
"type": "customPretty",
"customHtml": "<form #addEntityForm=\"ngForm\" [formGroup]=\"addEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"add-entity-form\">\n <mat-toolbar color=\"primary\">\n <h2>Add thermostat</h2>\n <span fxFlex></span>\n <button mat-icon-button (click)=\"cancel()\" 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 fxLayout=\"column\">\n <mat-form-field fxFlex class=\"mat-block\">\n <mat-label>Thermostat name</mat-label>\n <input matInput formControlName=\"entityName\" required>\n <mat-error *ngIf=\"addEntityFormGroup.get('entityName').hasError('required')\">\n Thermostat name is required.\n </mat-error>\n </mat-form-field>\n <div formGroupName=\"attributes\" fxLayout=\"column\">\n <mat-slide-toggle formControlName=\"alarmTemperature\">\n High temperature alarm\n </mat-slide-toggle>\n <mat-form-field fxFlex class=\"mat-block\">\n <mat-label>High temperature threshold, °C</mat-label>\n <input type=\"number\" step=\"any\" matInput\n [required] = \"addEntityFormGroup.get('attributes').get('alarmTemperature').value\"\n formControlName=\"thresholdTemperature\">\n <mat-error *ngIf=\"addEntityFormGroup.get('attributes').get('thresholdTemperature').hasError('required')\">\n High temperature threshold is required.\n </mat-error>\n </mat-form-field>\n \n <mat-slide-toggle formControlName=\"alarmHumidity\">\n Low humidity alarm\n </mat-slide-toggle>\n \n <mat-form-field fxFlex class=\"mat-block\">\n <mat-label>Low humidity threshold, %</mat-label>\n <input type=\"number\" step=\"any\" matInput\n [required] = \"addEntityFormGroup.get('attributes').get('alarmHumidity').value\"\n formControlName=\"thresholdHumidity\">\n <mat-error *ngIf=\"addEntityFormGroup.get('attributes').get('thresholdHumidity').hasError('required')\">\n Low humidity threshold is required.\n </mat-error>\n </mat-form-field>\n </div>\n </div>\n <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || addEntityForm.invalid || !addEntityForm.dirty\">\n Create\n </button>\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n </div>\n</form>",
"customHtml": "<form #addEntityForm=\"ngForm\" [formGroup]=\"addEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"add-entity-form\">\n <mat-toolbar color=\"primary\">\n <h2>Add thermostat</h2>\n <span fxFlex></span>\n <button mat-icon-button (click)=\"cancel()\" 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 fxLayout=\"column\">\n <mat-form-field fxFlex class=\"mat-block\">\n <mat-label>Thermostat name</mat-label>\n <input matInput formControlName=\"entityName\" required>\n <mat-error *ngIf=\"addEntityFormGroup.get('entityName').hasError('required')\">\n Thermostat name is required.\n </mat-error>\n </mat-form-field>\n <div formGroupName=\"attributes\" fxLayout=\"column\">\n <mat-slide-toggle formControlName=\"temperatureAlarmFlag\">\n High temperature alarm\n </mat-slide-toggle>\n <mat-form-field fxFlex class=\"mat-block\">\n <mat-label>High temperature threshold, °C</mat-label>\n <input type=\"number\" step=\"any\" matInput\n [required] = \"addEntityFormGroup.get('attributes').get('temperatureAlarmFlag').value\"\n formControlName=\"temperatureAlarmThreshold\">\n <mat-error *ngIf=\"addEntityFormGroup.get('attributes').get('temperatureAlarmThreshold').hasError('required')\">\n High temperature threshold is required.\n </mat-error>\n </mat-form-field>\n \n <mat-slide-toggle formControlName=\"humidityAlarmFlag\">\n Low humidity alarm\n </mat-slide-toggle>\n \n <mat-form-field fxFlex class=\"mat-block\">\n <mat-label>Low humidity threshold, %</mat-label>\n <input type=\"number\" step=\"any\" matInput\n [required] = \"addEntityFormGroup.get('attributes').get('humidityAlarmFlag').value\"\n formControlName=\"humidityAlarmThreshold\">\n <mat-error *ngIf=\"addEntityFormGroup.get('attributes').get('humidityAlarmThreshold').hasError('required')\">\n Low humidity threshold is required.\n </mat-error>\n </mat-form-field>\n </div>\n </div>\n <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || addEntityForm.invalid || !addEntityForm.dirty\">\n Create\n </button>\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n </div>\n</form>",
"customCss": ".add-entity-form{\n width: 300px;\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\nopenAddEntityDialog();\n\nfunction openAddEntityDialog() {\n customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();\n}\n\nfunction AddEntityDialogController(instance) {\n let vm = instance;\n \n vm.addEntityFormGroup = vm.fb.group({\n entityName: ['', [vm.validators.required]],\n attributes: vm.fb.group({\n alarmTemperature: [false],\n thresholdTemperature: [{value: null, disabled: true}],\n alarmHumidity: [false],\n thresholdHumidity: [{value: null, disabled: true}]\n })\n });\n \n vm.addEntityFormGroup.get('attributes').get('alarmTemperature').valueChanges\n .subscribe(activate => {\n if (activate) {\n vm.addEntityFormGroup.get('attributes').get('thresholdTemperature').enable();\n } else {\n vm.addEntityFormGroup.get('attributes').get('thresholdTemperature').disable();\n }\n });\n \n vm.addEntityFormGroup.get('attributes').get('alarmHumidity').valueChanges\n .subscribe(activate => {\n if (activate) {\n vm.addEntityFormGroup.get('attributes').get('thresholdHumidity').enable();\n } else {\n vm.addEntityFormGroup.get('attributes').get('thresholdHumidity').disable();\n }\n });\n\n vm.save = function() {\n vm.addEntityFormGroup.markAsPristine();\n saveEntityObservable().subscribe(\n function (entity) {\n saveAttributes(entity.id).subscribe(\n function () {\n widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n }\n );\n };\n \n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n \n function saveEntityObservable() {\n const formValues = vm.addEntityFormGroup.value;\n let entity = {\n name: formValues.entityName,\n type: \"thermostat\"\n };\n return deviceService.saveDevice(entity);\n }\n \n function saveAttributes(entityId) {\n let attributes = vm.addEntityFormGroup.get('attributes').value;\n let attributesArray = [];\n for (let key in attributes) {\n if(attributes[key] !== null) {\n attributesArray.push({key: key, value: attributes[key]});\n }\n }\n if (attributesArray.length > 0) {\n return attributeService.saveEntityAttributes(entityId, \"SERVER_SCOPE\", attributesArray);\n } else {\n return widgetContext.rxjs.of([]);\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\nopenAddEntityDialog();\n\nfunction openAddEntityDialog() {\n customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();\n}\n\nfunction AddEntityDialogController(instance) {\n let vm = instance;\n \n vm.addEntityFormGroup = vm.fb.group({\n entityName: ['', [vm.validators.required]],\n attributes: vm.fb.group({\n temperatureAlarmFlag: [false],\n temperatureAlarmThreshold: [{value: null, disabled: true}],\n humidityAlarmFlag: [false],\n humidityAlarmThreshold: [{value: null, disabled: true}]\n })\n });\n \n vm.addEntityFormGroup.get('attributes').get('temperatureAlarmFlag').valueChanges\n .subscribe(activate => {\n if (activate) {\n vm.addEntityFormGroup.get('attributes').get('temperatureAlarmThreshold').enable();\n } else {\n vm.addEntityFormGroup.get('attributes').get('temperatureAlarmThreshold').disable();\n }\n });\n \n vm.addEntityFormGroup.get('attributes').get('humidityAlarmFlag').valueChanges\n .subscribe(activate => {\n if (activate) {\n vm.addEntityFormGroup.get('attributes').get('humidityAlarmThreshold').enable();\n } else {\n vm.addEntityFormGroup.get('attributes').get('humidityAlarmThreshold').disable();\n }\n });\n\n vm.save = function() {\n vm.addEntityFormGroup.markAsPristine();\n saveEntityObservable().subscribe(\n function (entity) {\n saveAttributes(entity.id).subscribe(\n function () {\n widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n }\n );\n };\n \n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n \n function saveEntityObservable() {\n const formValues = vm.addEntityFormGroup.value;\n let entity = {\n name: formValues.entityName,\n type: \"thermostat\"\n };\n return deviceService.saveDevice(entity);\n }\n \n function saveAttributes(entityId) {\n let attributes = vm.addEntityFormGroup.get('attributes').value;\n let attributesArray = [];\n for (let key in attributes) {\n if(attributes[key] !== null) {\n attributesArray.push({key: key, value: attributes[key]});\n }\n }\n if (attributesArray.length > 0) {\n return attributeService.saveEntityAttributes(entityId, \"SERVER_SCOPE\", attributesArray);\n } else {\n return widgetContext.rxjs.of([]);\n }\n }\n}",
"customResources": [],
"id": "8ab5a518-67d2-b6a2-956d-81fd512294b2"
}
@ -167,9 +167,9 @@
"name": "Edit",
"icon": "edit",
"type": "customPretty",
"customHtml": "<form #editEntityForm=\"ngForm\" [formGroup]=\"editEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"edit-entity-form\">\n <mat-toolbar color=\"primary\">\n <h2>Edit thermostat {{entityName}}</h2>\n <span fxFlex></span>\n <button mat-icon-button (click)=\"cancel()\" 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 fxLayout=\"column\">\n <mat-form-field fxFlex class=\"mat-block\">\n <mat-label>Thermostat name</mat-label>\n <input matInput formControlName=\"entityName\" readonly>\n </mat-form-field>\n <div formGroupName=\"attributes\" fxLayout=\"column\">\n <mat-slide-toggle formControlName=\"alarmTemperature\">\n High temperature alarm\n </mat-slide-toggle>\n <mat-form-field fxFlex class=\"mat-block\">\n <mat-label>High temperature threshold, °C</mat-label>\n <input type=\"number\" step=\"any\" matInput\n [required] = \"editEntityFormGroup.get('attributes').get('alarmTemperature').value\"\n formControlName=\"thresholdTemperature\">\n <mat-error *ngIf=\"editEntityFormGroup.get('attributes').get('thresholdTemperature').hasError('required')\">\n High temperature threshold is required.\n </mat-error>\n </mat-form-field>\n\n <mat-slide-toggle formControlName=\"alarmHumidity\">\n Low humidity alarm\n </mat-slide-toggle>\n\n <mat-form-field fxFlex class=\"mat-block\">\n <mat-label>Low humidity threshold, %</mat-label>\n <input type=\"number\" step=\"any\" matInput\n [required] = \"editEntityFormGroup.get('attributes').get('alarmHumidity').value\"\n formControlName=\"thresholdHumidity\">\n <mat-error *ngIf=\"editEntityFormGroup.get('attributes').get('thresholdHumidity').hasError('required')\">\n Low humidity threshold is required.\n </mat-error>\n </mat-form-field>\n </div>\n </div>\n <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\n <button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty\">\n Save\n </button>\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n </div>\n</form>",
"customHtml": "<form #editEntityForm=\"ngForm\" [formGroup]=\"editEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"edit-entity-form\">\n <mat-toolbar color=\"primary\">\n <h2>Edit thermostat {{entityName}}</h2>\n <span fxFlex></span>\n <button mat-icon-button (click)=\"cancel()\" 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 fxLayout=\"column\">\n <mat-form-field fxFlex class=\"mat-block\">\n <mat-label>Thermostat name</mat-label>\n <input matInput formControlName=\"entityName\" readonly>\n </mat-form-field>\n <div formGroupName=\"attributes\" fxLayout=\"column\">\n <mat-slide-toggle formControlName=\"temperatureAlarmFlag\">\n High temperature alarm\n </mat-slide-toggle>\n <mat-form-field fxFlex class=\"mat-block\">\n <mat-label>High temperature threshold, °C</mat-label>\n <input type=\"number\" step=\"any\" matInput\n [required] = \"editEntityFormGroup.get('attributes').get('temperatureAlarmFlag').value\"\n formControlName=\"temperatureAlarmThreshold\">\n <mat-error *ngIf=\"editEntityFormGroup.get('attributes').get('temperatureAlarmThreshold').hasError('required')\">\n High temperature threshold is required.\n </mat-error>\n </mat-form-field>\n\n <mat-slide-toggle formControlName=\"humidityAlarmFlag\">\n Low humidity alarm\n </mat-slide-toggle>\n\n <mat-form-field fxFlex class=\"mat-block\">\n <mat-label>Low humidity threshold, %</mat-label>\n <input type=\"number\" step=\"any\" matInput\n [required] = \"editEntityFormGroup.get('attributes').get('humidityAlarmFlag').value\"\n formControlName=\"humidityAlarmThreshold\">\n <mat-error *ngIf=\"editEntityFormGroup.get('attributes').get('humidityAlarmThreshold').hasError('required')\">\n Low humidity threshold is required.\n </mat-error>\n </mat-form-field>\n </div>\n </div>\n <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\n <button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty\">\n Save\n </button>\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n </div>\n</form>",
"customCss": ".edit-entity-form{\n width: 300px;\n}",
"customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n \n vm.entityId = entityId;\n vm.entityName = entityName;\n vm.attributes = {};\n \n vm.editEntityFormGroup = vm.fb.group({\n entityName: [''],\n attributes: vm.fb.group({\n alarmTemperature: [false],\n thresholdTemperature: [{value: null, disabled: true}],\n alarmHumidity: [false],\n thresholdHumidity: [{value: null, disabled: true}]\n })\n });\n \n vm.editEntityFormGroup.get('attributes').get('alarmTemperature').valueChanges\n .subscribe(activate => {\n if (activate) {\n vm.editEntityFormGroup.get('attributes').get('thresholdTemperature').enable();\n } else {\n vm.editEntityFormGroup.get('attributes').get('thresholdTemperature').disable();\n }\n });\n \n vm.editEntityFormGroup.get('attributes').get('alarmHumidity').valueChanges\n .subscribe(activate => {\n if (activate) {\n vm.editEntityFormGroup.get('attributes').get('thresholdHumidity').enable();\n } else {\n vm.editEntityFormGroup.get('attributes').get('thresholdHumidity').disable();\n }\n });\n \n \n getEntityInfo();\n \n \n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveAttributes(entityId).subscribe(\n function () {\n vm.dialogRef.close(null);\n }\n );\n };\n \n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n \n function getEntityAttributes(attributes) {\n for (var i = 0; i < attributes.length; i++) {\n vm.attributes[attributes[i].key] = attributes[i].value;\n }\n }\n \n function getEntityInfo() {\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE').subscribe(\n function (attributes) {\n getEntityAttributes(attributes);\n vm.editEntityFormGroup.patchValue({\n entityName: vm.entityName,\n attributes: vm.attributes\n });\n // if(vm.attributes.alarmTemperature) {\n // vm.editEntityFormGroup.get('attributes').get('thresholdTemperature').enable();\n // }\n // if(vm.attributes.alarmHumidity) {\n // vm.editEntityFormGroup.get('attributes').get('thresholdHumidity').enable();\n // }\n }\n );\n }\n \n function saveAttributes(entityId) {\n let attributes = vm.editEntityFormGroup.get('attributes').value;\n let attributesArray = [];\n for (let key in attributes) {\n if (attributes[key] !== vm.attributes[key]) {\n attributesArray.push({key: key, value: attributes[key]});\n }\n }\n if (attributesArray.length > 0) {\n return attributeService.saveEntityAttributes(entityId, \"SERVER_SCOPE\", attributesArray);\n } else {\n return widgetContext.rxjs.of([]);\n }\n }\n}",
"customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n \n vm.entityId = entityId;\n vm.entityName = entityName;\n vm.attributes = {};\n \n vm.editEntityFormGroup = vm.fb.group({\n entityName: [''],\n attributes: vm.fb.group({\n temperatureAlarmFlag: [false],\n temperatureAlarmThreshold: [{value: null, disabled: true}],\n humidityAlarmFlag: [false],\n humidityAlarmThreshold: [{value: null, disabled: true}]\n })\n });\n \n vm.editEntityFormGroup.get('attributes').get('temperatureAlarmFlag').valueChanges\n .subscribe(activate => {\n if (activate) {\n vm.editEntityFormGroup.get('attributes').get('temperatureAlarmThreshold').enable();\n } else {\n vm.editEntityFormGroup.get('attributes').get('temperatureAlarmThreshold').disable();\n }\n });\n \n vm.editEntityFormGroup.get('attributes').get('humidityAlarmFlag').valueChanges\n .subscribe(activate => {\n if (activate) {\n vm.editEntityFormGroup.get('attributes').get('humidityAlarmThreshold').enable();\n } else {\n vm.editEntityFormGroup.get('attributes').get('humidityAlarmThreshold').disable();\n }\n });\n \n \n getEntityInfo();\n \n \n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveAttributes(entityId).subscribe(\n function () {\n vm.dialogRef.close(null);\n }\n );\n };\n \n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n \n function getEntityAttributes(attributes) {\n for (var i = 0; i < attributes.length; i++) {\n vm.attributes[attributes[i].key] = attributes[i].value;\n }\n }\n \n function getEntityInfo() {\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE').subscribe(\n function (attributes) {\n getEntityAttributes(attributes);\n vm.editEntityFormGroup.patchValue({\n entityName: vm.entityName,\n attributes: vm.attributes\n });\n // if(vm.attributes.temperatureAlarmFlag) {\n // vm.editEntityFormGroup.get('attributes').get('temperatureAlarmThreshold').enable();\n // }\n // if(vm.attributes.humidityAlarmFlag) {\n // vm.editEntityFormGroup.get('attributes').get('humidityAlarmThreshold').enable();\n // }\n }\n );\n }\n \n function saveAttributes(entityId) {\n let attributes = vm.editEntityFormGroup.get('attributes').value;\n let attributesArray = [];\n for (let key in attributes) {\n if (attributes[key] !== vm.attributes[key]) {\n attributesArray.push({key: key, value: attributes[key]});\n }\n }\n if (attributesArray.length > 0) {\n return attributeService.saveEntityAttributes(entityId, \"SERVER_SCOPE\", attributesArray);\n } else {\n return widgetContext.rxjs.of([]);\n }\n }\n}",
"customResources": [],
"id": "7506576f-87ba-d3a0-88fb-e304d451776d"
},
@ -219,8 +219,8 @@
"defaultPageSize": 10,
"defaultSortOrder": "-createdTime",
"enableSelectColumnDisplay": false,
"enableStatusFilter": true,
"alarmsTitle": "Alarms"
"alarmsTitle": "Alarms",
"enableFilter": true
},
"title": "New Alarms table",
"dropShadow": true,
@ -234,6 +234,9 @@
"showLegend": false,
"alarmSource": {
"type": "entity",
"name": "alarms",
"entityAliasId": "68a058e1-fdda-8482-715b-3ae4a488568e",
"filterId": null,
"dataKeys": [
{
"name": "createdTime",
@ -275,9 +278,7 @@
"settings": {},
"_hash": 0.7977920750136249
}
],
"entityAliasId": "ce27a9d0-93bf-b7a4-054d-d0369a8cf813",
"name": "alarms"
]
},
"alarmSearchStatus": "ANY",
"alarmsPollingInterval": 5,
@ -549,7 +550,7 @@
"type": "entity",
"dataKeys": [
{
"name": "alarmTemperature",
"name": "temperatureAlarmFlag",
"type": "attribute",
"label": "High temperature alarm",
"color": "#4caf50",
@ -564,7 +565,7 @@
"_hash": 0.8725278440159361
},
{
"name": "thresholdTemperature",
"name": "temperatureAlarmThreshold",
"type": "attribute",
"label": "High temperature threshold, °C",
"color": "#f44336",
@ -575,12 +576,12 @@
"isEditable": "editable",
"dataKeyHidden": false,
"step": 1,
"disabledOnDataKey": "alarmTemperature"
"disabledOnDataKey": "temperatureAlarmFlag"
},
"_hash": 0.7316078472857874
},
{
"name": "alarmHumidity",
"name": "humidityAlarmFlag",
"type": "attribute",
"label": "Low humidity alarm",
"color": "#ffc107",
@ -595,7 +596,7 @@
"_hash": 0.5339673667431057
},
{
"name": "thresholdHumidity",
"name": "humidityAlarmThreshold",
"type": "attribute",
"label": "Low humidity threshold, %",
"color": "#607d8b",
@ -606,7 +607,7 @@
"isEditable": "editable",
"dataKeyHidden": false,
"step": 1,
"disabledOnDataKey": "alarmHumidity"
"disabledOnDataKey": "humidityAlarmFlag"
},
"_hash": 0.2687091190358901
}
@ -1031,7 +1032,8 @@
"markerImageFunction": "var res;\nif(dsData[dsIndex].active !== \"true\"){\n\tvar res = {\n\t url: images[0],\n\t size: 48\n\t}\n} else {\n var res = {\n\t url: images[1],\n\t size: 48\n\t}\n}\nreturn res;",
"useLabelFunction": true,
"provider": "openstreet-map",
"draggableMarker": true
"draggableMarker": true,
"editablePolygon": true
},
"title": "New Markers Placement - OpenStreetMap",
"dropShadow": true,
@ -1062,61 +1064,6 @@
"displayTimewindow": true
},
"id": "0a430429-9078-9ae6-2b67-e4a15a2bf8bf"
},
"f4bb2f2d-0164-60bc-f3e8-9b1e7b5a59b3": {
"isSystemType": true,
"bundleAlias": "input_widgets",
"typeAlias": "update_double_timeseries",
"type": "latest",
"title": "New widget",
"sizeX": 7.5,
"sizeY": 3,
"config": {
"datasources": [
{
"type": "entity",
"name": null,
"entityAliasId": "12ae98c7-1ea2-52cf-64d5-763e9d993547",
"dataKeys": [
{
"name": "temperature",
"type": "timeseries",
"label": "temperature",
"color": "#2196f3",
"settings": {},
"_hash": 0.4164505192982848
}
]
}
],
"timewindow": {
"realtime": {
"timewindowMs": 60000
}
},
"showTitle": true,
"backgroundColor": "#fff",
"color": "rgba(0, 0, 0, 0.87)",
"padding": "8px",
"settings": {
"showResultMessage": true,
"showLabel": true
},
"title": "New Update double timeseries",
"dropShadow": true,
"enableFullscreen": false,
"widgetStyle": {},
"titleStyle": {
"fontSize": "16px",
"fontWeight": 400
},
"useDashboardTimewindow": true,
"showLegend": false,
"actions": {}
},
"row": 0,
"col": 0,
"id": "f4bb2f2d-0164-60bc-f3e8-9b1e7b5a59b3"
}
},
"states": {
@ -1215,12 +1162,6 @@
"sizeY": 6,
"row": 6,
"col": 0
},
"f4bb2f2d-0164-60bc-f3e8-9b1e7b5a59b3": {
"sizeX": 7.5,
"sizeY": 3,
"row": 12,
"col": 0
}
},
"gridSettings": {
@ -1257,16 +1198,6 @@
"stateEntityParamName": null,
"defaultStateEntity": null
}
},
"ce27a9d0-93bf-b7a4-054d-d0369a8cf813": {
"id": "ce27a9d0-93bf-b7a4-054d-d0369a8cf813",
"alias": "Thermostat-alarm",
"filter": {
"type": "entityName",
"resolveMultiple": false,
"entityType": "ASSET",
"entityNameFilter": "Thermostat Alarms"
}
}
},
"timewindow": {
@ -1301,7 +1232,8 @@
"showDashboardTimewindow": true,
"showDashboardExport": true,
"toolbarAlwaysOpen": true
}
},
"filters": {}
},
"name": "Thermostats"
}

190
application/src/main/data/json/demo/rule_chains/root_rule_chain.json

@ -1,190 +0,0 @@
{
"ruleChain": {
"additionalInfo": null,
"name": "Root Rule Chain",
"type": "CORE",
"firstRuleNodeId": null,
"root": true,
"debugMode": false,
"configuration": null
},
"metadata": {
"firstNodeIndex": 3,
"nodes": [
{
"additionalInfo": {
"layoutX": 1069,
"layoutY": 267
},
"type": "org.thingsboard.rule.engine.filter.TbJsFilterNode",
"name": "Is Thermostat?",
"debugMode": false,
"configuration": {
"jsScript": "return msg.id.entityType === \"DEVICE\" && msg.type === \"thermostat\";"
}
},
{
"additionalInfo": {
"layoutX": 824,
"layoutY": 156
},
"type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode",
"name": "Save Timeseries",
"debugMode": false,
"configuration": {
"defaultTTL": 0
}
},
{
"additionalInfo": {
"layoutX": 825,
"layoutY": 52
},
"type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode",
"name": "Save Client Attributes",
"debugMode": false,
"configuration": {
"scope": "CLIENT_SCOPE",
"notifyDevice": "false"
}
},
{
"additionalInfo": {
"layoutX": 347,
"layoutY": 149
},
"type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode",
"name": "Message Type Switch",
"debugMode": false,
"configuration": {
"version": 0
}
},
{
"additionalInfo": {
"layoutX": 839,
"layoutY": 345
},
"type": "org.thingsboard.rule.engine.action.TbLogNode",
"name": "Log RPC from Device",
"debugMode": false,
"configuration": {
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
}
},
{
"additionalInfo": {
"layoutX": 832,
"layoutY": 407
},
"type": "org.thingsboard.rule.engine.action.TbLogNode",
"name": "Log Other",
"debugMode": false,
"configuration": {
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
}
},
{
"additionalInfo": {
"layoutX": 825,
"layoutY": 468
},
"type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode",
"name": "RPC Call Request",
"debugMode": false,
"configuration": {
"timeoutInSeconds": 60
}
},
{
"additionalInfo": {
"layoutX": 1069,
"layoutY": 90
},
"type": "org.thingsboard.rule.engine.filter.TbJsFilterNode",
"name": "Is Thermostat?",
"debugMode": false,
"configuration": {
"jsScript": "return metadata[\"deviceType\"] === \"thermostat\";"
}
},
{
"additionalInfo": {
"layoutX": 1090,
"layoutY": 360
},
"type": "org.thingsboard.rule.engine.action.TbCreateRelationNode",
"name": "Relate to Asset",
"debugMode": false,
"configuration": {
"direction": "FROM",
"relationType": "ToAlarmPropagationAsset",
"entityType": "ASSET",
"entityNamePattern": "Thermostat Alarms",
"entityTypePattern": "AlarmPropagationAsset",
"entityCacheExpiration": 300,
"createEntityIfNotExists": true,
"changeOriginatorToRelatedEntity": false,
"removeCurrentRelations": false
}
}
],
"connections": [
{
"fromIndex": 0,
"toIndex": 8,
"type": "True"
},
{
"fromIndex": 1,
"toIndex": 7,
"type": "Success"
},
{
"fromIndex": 3,
"toIndex": 5,
"type": "Other"
},
{
"fromIndex": 3,
"toIndex": 2,
"type": "Post attributes"
},
{
"fromIndex": 3,
"toIndex": 1,
"type": "Post telemetry"
},
{
"fromIndex": 3,
"toIndex": 4,
"type": "RPC Request from Device"
},
{
"fromIndex": 3,
"toIndex": 6,
"type": "RPC Request to Device"
},
{
"fromIndex": 3,
"toIndex": 0,
"type": "Entity Created"
}
],
"ruleChainConnections": [
{
"fromIndex": 7,
"targetRuleChainId": {
"entityType": "RULE_CHAIN",
"id": "25e26570-89ed-11ea-a650-cd6e14e633bd"
},
"additionalInfo": {
"layoutX": 1109,
"layoutY": 182,
"ruleChainNodeId": "rule-chain-node-10"
},
"type": "True"
}
]
}
}

142
application/src/main/data/json/demo/rule_chains/thermostat_alarms.json

@ -1,142 +0,0 @@
{
"ruleChain": {
"additionalInfo": null,
"name": "Thermostat Alarms",
"type": "CORE",
"firstRuleNodeId": null,
"root": false,
"debugMode": false,
"configuration": null
},
"metadata": {
"firstNodeIndex": 5,
"nodes": [
{
"additionalInfo": {
"layoutX": 929,
"layoutY": 67
},
"type": "org.thingsboard.rule.engine.action.TbCreateAlarmNode",
"name": "Create Temp Alarm",
"debugMode": false,
"configuration": {
"alarmType": "High Temperature",
"alarmDetailsBuildJs": "var details = {};\nif (metadata.prevAlarmDetails) {\n details = JSON.parse(metadata.prevAlarmDetails);\n}\ndetails.triggerValue = msg.temperature;\nreturn details;",
"severity": "MAJOR",
"propagate": true,
"useMessageAlarmData": false,
"relationTypes": [
"ToAlarmPropagationAsset"
]
}
},
{
"additionalInfo": {
"layoutX": 930,
"layoutY": 201
},
"type": "org.thingsboard.rule.engine.action.TbClearAlarmNode",
"name": "Clear Temp Alarm",
"debugMode": false,
"configuration": {
"alarmType": "High Temperature",
"alarmDetailsBuildJs": "var details = {};\nif (metadata.prevAlarmDetails) {\n details = JSON.parse(metadata.prevAlarmDetails);\n}\nreturn details;"
}
},
{
"additionalInfo": {
"layoutX": 930,
"layoutY": 131
},
"type": "org.thingsboard.rule.engine.action.TbCreateAlarmNode",
"name": "Create Humidity Alarm",
"debugMode": false,
"configuration": {
"alarmType": "Low Humidity",
"alarmDetailsBuildJs": "var details = {};\nif (metadata.prevAlarmDetails) {\n details = JSON.parse(metadata.prevAlarmDetails);\n}\ndetails.triggerValue = msg.humidity;\nreturn details;",
"severity": "MINOR",
"propagate": true,
"useMessageAlarmData": false,
"relationTypes": [
"ToAlarmPropagationAsset"
]
}
},
{
"additionalInfo": {
"layoutX": 929,
"layoutY": 275
},
"type": "org.thingsboard.rule.engine.action.TbClearAlarmNode",
"name": "Clear Humidity Alarm",
"debugMode": false,
"configuration": {
"alarmType": "Low Humidity",
"alarmDetailsBuildJs": "var details = {};\nif (metadata.prevAlarmDetails) {\n details = JSON.parse(metadata.prevAlarmDetails);\n}\nreturn details;"
}
},
{
"additionalInfo": {
"layoutX": 586,
"layoutY": 148
},
"type": "org.thingsboard.rule.engine.filter.TbJsSwitchNode",
"name": "Check Alarms",
"debugMode": false,
"configuration": {
"jsScript": "var relations = [];\nif(metadata[\"ss_alarmTemperature\"] === \"true\"){\n if(msg.temperature > metadata[\"ss_thresholdTemperature\"]){\n relations.push(\"NewTempAlarm\");\n } else {\n relations.push(\"ClearTempAlarm\");\n }\n}\nif(metadata[\"ss_alarmHumidity\"] === \"true\"){\n if(msg.humidity < metadata[\"ss_thresholdHumidity\"]){\n relations.push(\"NewHumidityAlarm\");\n } else {\n relations.push(\"ClearHumidityAlarm\");\n }\n}\n\nreturn relations;"
}
},
{
"additionalInfo": {
"layoutX": 321,
"layoutY": 149
},
"type": "org.thingsboard.rule.engine.metadata.TbGetAttributesNode",
"name": "Fetch Configuration",
"debugMode": false,
"configuration": {
"clientAttributeNames": [],
"sharedAttributeNames": [],
"serverAttributeNames": [
"alarmTemperature",
"thresholdTemperature",
"alarmHumidity",
"thresholdHumidity"
],
"latestTsKeyNames": [],
"tellFailureIfAbsent": false,
"getLatestValueWithTs": false
}
}
],
"connections": [
{
"fromIndex": 4,
"toIndex": 0,
"type": "NewTempAlarm"
},
{
"fromIndex": 4,
"toIndex": 1,
"type": "ClearTempAlarm"
},
{
"fromIndex": 4,
"toIndex": 2,
"type": "NewHumidityAlarm"
},
{
"fromIndex": 4,
"toIndex": 3,
"type": "ClearHumidityAlarm"
},
{
"fromIndex": 5,
"toIndex": 4,
"type": "Success"
}
],
"ruleChainConnections": null
}
}

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

@ -0,0 +1,23 @@
{
"providerId": "Facebook",
"accessTokenUri": "https://graph.facebook.com/v2.8/oauth/access_token",
"authorizationUri": "https://www.facebook.com/v2.8/dialog/oauth",
"scope": ["email","public_profile"],
"jwkSetUri": null,
"userInfoUri": "https://graph.facebook.com/me?fields=id,name,first_name,last_name,email",
"clientAuthenticationMethod": "BASIC",
"userNameAttributeName": "email",
"mapperConfig": {
"type": "BASIC",
"basic": {
"emailAttributeKey": "email",
"firstNameAttributeKey": "first_name",
"lastNameAttributeKey": "last_name",
"tenantNameStrategy": "DOMAIN"
}
},
"comment": null,
"loginButtonIcon": "facebook-logo",
"loginButtonLabel": "Facebook",
"helpLink": "https://developers.facebook.com/docs/facebook-login/web#logindialog"
}

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

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

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

@ -0,0 +1,24 @@
{
"providerId": "Google",
"additionalInfo": null,
"accessTokenUri": "https://oauth2.googleapis.com/token",
"authorizationUri": "https://accounts.google.com/o/oauth2/v2/auth",
"scope": ["email","openid","profile"],
"jwkSetUri": "https://www.googleapis.com/oauth2/v3/certs",
"userInfoUri": "https://openidconnect.googleapis.com/v1/userinfo",
"clientAuthenticationMethod": "BASIC",
"userNameAttributeName": "email",
"mapperConfig": {
"type": "BASIC",
"basic": {
"emailAttributeKey": "email",
"firstNameAttributeKey": "given_name",
"lastNameAttributeKey": "family_name",
"tenantNameStrategy": "DOMAIN"
}
},
"comment": null,
"loginButtonIcon": "google-logo",
"loginButtonLabel": "Google",
"helpLink": "https://developers.google.com/adwords/api/docs/guides/authentication"
}

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

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

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

File diff suppressed because one or more lines are too long

3
application/src/main/data/json/tenant/device_profile/rule_chain_template.json

@ -94,7 +94,8 @@
"name": "Device Profile Node",
"debugMode": false,
"configuration": {
"persistAlarmRulesState": false
"persistAlarmRulesState": false,
"fetchAlarmRulesStateOnStart": false
}
}
],

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

@ -14,19 +14,92 @@
-- limitations under the License.
--
CREATE TABLE IF NOT EXISTS oauth2_client_registration_info (
id uuid NOT NULL CONSTRAINT oauth2_client_registration_info_pkey PRIMARY KEY,
enabled boolean,
created_time bigint NOT NULL,
additional_info varchar,
client_id varchar(255),
client_secret varchar(255),
authorization_uri varchar(255),
token_uri varchar(255),
scope varchar(255),
user_info_uri varchar(255),
user_name_attribute_name varchar(255),
jwk_set_uri varchar(255),
client_authentication_method varchar(255),
login_button_label varchar(255),
login_button_icon varchar(255),
allow_user_creation boolean,
activate_user boolean,
type varchar(31),
basic_email_attribute_key varchar(31),
basic_first_name_attribute_key varchar(31),
basic_last_name_attribute_key varchar(31),
basic_tenant_name_strategy varchar(31),
basic_tenant_name_pattern varchar(255),
basic_customer_name_pattern varchar(255),
basic_default_dashboard_name varchar(255),
basic_always_full_screen boolean,
custom_url varchar(255),
custom_username varchar(255),
custom_password varchar(255),
custom_send_token boolean
);
CREATE TABLE IF NOT EXISTS oauth2_client_registration (
id uuid NOT NULL CONSTRAINT oauth2_client_registration_pkey PRIMARY KEY,
created_time bigint NOT NULL,
domain_name varchar(255),
domain_scheme varchar(31),
client_registration_info_id uuid
);
CREATE TABLE IF NOT EXISTS oauth2_client_registration_template (
id uuid NOT NULL CONSTRAINT oauth2_client_registration_template_pkey PRIMARY KEY,
created_time bigint NOT NULL,
additional_info varchar,
provider_id varchar(255),
authorization_uri varchar(255),
token_uri varchar(255),
scope varchar(255),
user_info_uri varchar(255),
user_name_attribute_name varchar(255),
jwk_set_uri varchar(255),
client_authentication_method varchar(255),
type varchar(31),
basic_email_attribute_key varchar(31),
basic_first_name_attribute_key varchar(31),
basic_last_name_attribute_key varchar(31),
basic_tenant_name_strategy varchar(31),
basic_tenant_name_pattern varchar(255),
basic_customer_name_pattern varchar(255),
basic_default_dashboard_name varchar(255),
basic_always_full_screen boolean,
comment varchar,
login_button_icon varchar(255),
login_button_label varchar(255),
help_link varchar(255),
CONSTRAINT oauth2_template_provider_id_unq_key UNIQUE (provider_id)
);
CREATE TABLE IF NOT EXISTS device_profile (
id uuid NOT NULL CONSTRAINT device_profile_pkey PRIMARY KEY,
created_time bigint NOT NULL,
name varchar(255),
type varchar(255),
transport_type varchar(255),
provision_type varchar(255),
profile_data jsonb,
description varchar,
search_text varchar(255),
is_default boolean,
tenant_id uuid,
default_rule_chain_id uuid,
default_queue_name varchar(255),
provision_device_key varchar,
CONSTRAINT device_profile_name_unq_key UNIQUE (tenant_id, name),
CONSTRAINT device_provision_key_unq_key UNIQUE (provision_device_key),
CONSTRAINT fk_default_rule_chain_device_profile FOREIGN KEY (default_rule_chain_id) REFERENCES rule_chain(id)
);

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

@ -32,6 +32,8 @@ import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
import org.thingsboard.server.actors.service.ActorService;
import org.thingsboard.server.actors.tenant.DebugTbRateLimits;
import org.thingsboard.server.common.data.DataConstants;
@ -67,6 +69,8 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.edge.rpc.EdgeRpcService;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
@ -74,12 +78,14 @@ import org.thingsboard.server.service.executors.ExternalCallExecutorService;
import org.thingsboard.server.service.executors.SharedEventLoopGroupService;
import org.thingsboard.server.service.mail.MailExecutorService;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.service.queue.TbClusterService;
import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService;
import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService;
import org.thingsboard.server.service.script.JsExecutorService;
import org.thingsboard.server.service.script.JsInvokeService;
import org.thingsboard.server.service.session.DeviceSessionCacheService;
import org.thingsboard.server.service.sms.SmsExecutorService;
import org.thingsboard.server.service.state.DeviceStateService;
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
@ -107,6 +113,14 @@ public class ActorSystemContext {
return debugPerTenantLimits;
}
@Autowired
@Getter
private TbApiUsageStateService apiUsageStateService;
@Autowired
@Getter
private TbApiUsageClient apiUsageClient;
@Autowired
@Getter
@Setter
@ -129,6 +143,10 @@ public class ActorSystemContext {
@Getter
private DeviceService deviceService;
@Autowired
@Getter
private TbTenantProfileCache tenantProfileCache;
@Autowired
@Getter
private TbDeviceProfileCache deviceProfileCache;
@ -216,6 +234,10 @@ public class ActorSystemContext {
@Getter
private MailExecutorService mailExecutor;
@Autowired
@Getter
private SmsExecutorService smsExecutor;
@Autowired
@Getter
private DbCallbackExecutorService dbCallbackExecutor;
@ -232,6 +254,14 @@ public class ActorSystemContext {
@Getter
private MailService mailService;
@Autowired
@Getter
private SmsService smsService;
@Autowired
@Getter
private SmsSenderFactory smsSenderFactory;
@Autowired
@Getter
private ClaimDevicesService claimDevicesService;
@ -325,6 +355,10 @@ public class ActorSystemContext {
@Getter
private boolean allowSystemMailService;
@Value("${actors.rule.allow_system_sms_service}")
@Getter
private boolean allowSystemSmsService;
@Value("${transport.sessions.inactivity_timeout}")
@Getter
private long sessionInactivityTimeout;
@ -553,7 +587,11 @@ public class ActorSystemContext {
public void scheduleMsgWithDelay(TbActorRef ctx, TbActorMsg msg, long delayInMs) {
log.debug("Scheduling msg {} with delay {} ms", msg, delayInMs);
getScheduler().schedule(() -> ctx.tell(msg), delayInMs, TimeUnit.MILLISECONDS);
if (delayInMs > 0) {
getScheduler().schedule(() -> ctx.tell(msg), delayInMs, TimeUnit.MILLISECONDS);
} else {
ctx.tell(msg);
}
}
}

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

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

86
application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java

@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.channel.EventLoopGroup;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.StringUtils;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.RuleEngineAlarmService;
@ -27,15 +28,21 @@ import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache;
import org.thingsboard.rule.engine.api.RuleEngineRpcService;
import org.thingsboard.rule.engine.api.RuleEngineTelemetryService;
import org.thingsboard.rule.engine.api.ScriptEngine;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbRelationTypes;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.TbActorRef;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
@ -47,6 +54,7 @@ import org.thingsboard.server.common.data.rule.RuleNodeState;
import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.queue.ServiceQueue;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.dao.asset.AssetService;
@ -72,6 +80,7 @@ import org.thingsboard.server.service.script.RuleNodeJsScriptEngine;
import java.util.Collections;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
/**
@ -185,6 +194,9 @@ class DefaultTbContext implements TbContext {
}
private TopicPartitionInfo resolvePartition(TbMsg tbMsg, String queueName) {
if (StringUtils.isEmpty(queueName)) {
queueName = ServiceQueue.MAIN;
}
return mainCtx.resolve(ServiceType.TB_RULE_ENGINE, queueName, getTenantId(), tbMsg.getOriginator());
}
@ -253,26 +265,26 @@ class DefaultTbContext implements TbContext {
}
public TbMsg customerCreatedMsg(Customer customer, RuleNodeId ruleNodeId) {
return entityCreatedMsg(customer, customer.getId(), ruleNodeId);
return entityActionMsg(customer, customer.getId(), ruleNodeId, DataConstants.ENTITY_CREATED);
}
public TbMsg deviceCreatedMsg(Device device, RuleNodeId ruleNodeId) {
return entityCreatedMsg(device, device.getId(), ruleNodeId);
return entityActionMsg(device, device.getId(), ruleNodeId, DataConstants.ENTITY_CREATED);
}
public TbMsg assetCreatedMsg(Asset asset, RuleNodeId ruleNodeId) {
return entityCreatedMsg(asset, asset.getId(), ruleNodeId);
return entityActionMsg(asset, asset.getId(), ruleNodeId, DataConstants.ENTITY_CREATED);
}
public TbMsg alarmCreatedMsg(Alarm alarm, RuleNodeId ruleNodeId) {
return entityCreatedMsg(alarm, alarm.getId(), ruleNodeId);
public TbMsg alarmActionMsg(Alarm alarm, RuleNodeId ruleNodeId, String action) {
return entityActionMsg(alarm, alarm.getId(), ruleNodeId, action);
}
public <E, I extends EntityId> TbMsg entityCreatedMsg(E entity, I id, RuleNodeId ruleNodeId) {
public <E, I extends EntityId> TbMsg entityActionMsg(E entity, I id, RuleNodeId ruleNodeId, String action) {
try {
return TbMsg.newMsg(DataConstants.ENTITY_CREATED, id, getActionMetaData(ruleNodeId), mapper.writeValueAsString(mapper.valueToTree(entity)));
return TbMsg.newMsg(action, id, getActionMetaData(ruleNodeId), mapper.writeValueAsString(mapper.valueToTree(entity)));
} catch (JsonProcessingException | IllegalArgumentException e) {
throw new RuntimeException("Failed to process " + id.getEntityType().name().toLowerCase() + " created msg: " + e);
throw new RuntimeException("Failed to process " + id.getEntityType().name().toLowerCase() + " " + action + " msg: " + e);
}
}
@ -296,6 +308,11 @@ class DefaultTbContext implements TbContext {
return mainCtx.getMailExecutor();
}
@Override
public ListeningExecutor getSmsExecutor() {
return mainCtx.getSmsExecutor();
}
@Override
public ListeningExecutor getDbCallbackExecutor() {
return mainCtx.getDbCallbackExecutor();
@ -308,7 +325,7 @@ class DefaultTbContext implements TbContext {
@Override
public ScriptEngine createJsScriptEngine(String script, String... argNames) {
return new RuleNodeJsScriptEngine(mainCtx.getJsSandbox(), nodeCtx.getSelf().getId(), script, argNames);
return new RuleNodeJsScriptEngine(getTenantId(), mainCtx.getJsSandbox(), nodeCtx.getSelf().getId(), script, argNames);
}
@Override
@ -431,6 +448,20 @@ class DefaultTbContext implements TbContext {
}
}
@Override
public SmsService getSmsService() {
if (mainCtx.isAllowSystemSmsService()) {
return mainCtx.getSmsService();
} else {
throw new RuntimeException("Access to System SMS Service is forbidden!");
}
}
@Override
public SmsSenderFactory getSmsSenderFactory() {
return mainCtx.getSmsSenderFactory();
}
@Override
public RuleEngineRpcService getRpcService() {
return mainCtx.getTbRuleEngineDeviceRpcService();
@ -476,6 +507,43 @@ class DefaultTbContext implements TbContext {
return mainCtx.getRuleNodeStateService().save(getTenantId(), state);
}
@Override
public void clearRuleNodeStates() {
if (log.isDebugEnabled()) {
log.debug("[{}][{}] Going to clear rule node states", getTenantId(), getSelfId());
}
mainCtx.getRuleNodeStateService().removeByRuleNodeId(getTenantId(), getSelfId());
}
@Override
public void removeRuleNodeStateForEntity(EntityId entityId) {
if (log.isDebugEnabled()) {
log.debug("[{}][{}][{}] Remove Rule Node State for entity.", getTenantId(), getSelfId(), entityId);
}
mainCtx.getRuleNodeStateService().removeByRuleNodeIdAndEntityId(getTenantId(), getSelfId(), entityId);
}
@Override
public void addTenantProfileListener(Consumer<TenantProfile> listener) {
mainCtx.getTenantProfileCache().addListener(getTenantId(), getSelfId(), listener);
}
@Override
public void addDeviceProfileListeners(Consumer<DeviceProfile> profileListener, BiConsumer<DeviceId, DeviceProfile> deviceListener) {
mainCtx.getDeviceProfileCache().addListener(getTenantId(), getSelfId(), profileListener, deviceListener);
}
@Override
public void removeListeners() {
mainCtx.getDeviceProfileCache().removeListener(getTenantId(), getSelfId());
mainCtx.getTenantProfileCache().removeListener(getTenantId(), getSelfId());
}
@Override
public TenantProfile getTenantProfile() {
return mainCtx.getTenantProfileCache().get(getTenantId());
}
private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) {
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("ruleNodeId", ruleNodeId.toString());

7
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java

@ -23,6 +23,7 @@ import org.thingsboard.server.actors.TbActorRef;
import org.thingsboard.server.actors.TbEntityActorId;
import org.thingsboard.server.actors.service.DefaultActorService;
import org.thingsboard.server.actors.shared.ComponentMsgProcessor;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
@ -47,6 +48,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg;
import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.common.MultipleTbQueueTbMsgCallbackWrapper;
import org.thingsboard.server.queue.common.TbQueueTbMsgCallbackWrapper;
import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
import org.thingsboard.server.service.queue.TbClusterService;
import java.util.ArrayList;
@ -69,15 +71,16 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
private final Map<RuleNodeId, List<RuleNodeRelation>> nodeRoutes;
private final RuleChainService service;
private final TbClusterService clusterService;
private final TbApiUsageClient apiUsageClient;
private String ruleChainName;
private RuleNodeId firstId;
private RuleNodeCtx firstNode;
private boolean started;
RuleChainActorMessageProcessor(TenantId tenantId, RuleChain ruleChain, ActorSystemContext systemContext
, TbActorRef parent, TbActorRef self) {
RuleChainActorMessageProcessor(TenantId tenantId, RuleChain ruleChain, ActorSystemContext systemContext, TbActorRef parent, TbActorRef self) {
super(systemContext, tenantId, ruleChain.getId());
this.apiUsageClient = systemContext.getApiUsageClient();
this.ruleChainName = ruleChain.getName();
this.parent = parent;
this.self = self;

6
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java

@ -64,6 +64,12 @@ public abstract class RuleChainManagerActor extends ContextAwareActor {
}
}
protected void destroyRuleChains() {
for (RuleChain ruleChain : new PageDataIterable<>(link -> ruleChainService.findTenantRuleChainsByType(tenantId, RuleChainType.CORE, link), ContextAwareActor.ENTITY_PACK_LIMIT)) {
ctx.stop(new TbEntityActorId(ruleChain.getId()));
}
}
protected void visit(RuleChain entity, TbActorRef actorRef) {
if (entity != null && entity.isRoot() && entity.getType().equals(RuleChainType.CORE)) {
rootChain = entity;

51
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java

@ -21,13 +21,18 @@ import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.TbActorCtx;
import org.thingsboard.server.actors.TbActorRef;
import org.thingsboard.server.actors.shared.ComponentMsgProcessor;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileConfiguration;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
import org.thingsboard.server.common.msg.queue.RuleNodeException;
import org.thingsboard.server.common.msg.queue.RuleNodeInfo;
import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
/**
* @author Andrew Shvayka
@ -36,6 +41,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
private final String ruleChainName;
private final TbActorRef self;
private final TbApiUsageClient apiUsageClient;
private RuleNode ruleNode;
private TbNode tbNode;
private DefaultTbContext defaultCtx;
@ -44,6 +50,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
RuleNodeActorMessageProcessor(TenantId tenantId, String ruleChainName, RuleNodeId ruleNodeId, ActorSystemContext systemContext
, TbActorRef parent, TbActorRef self) {
super(systemContext, tenantId, ruleNodeId);
this.apiUsageClient = systemContext.getApiUsageClient();
this.ruleChainName = ruleChainName;
this.self = self;
this.ruleNode = systemContext.getRuleChainService().findRuleNodeById(tenantId, entityId);
@ -92,26 +99,42 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
public void onRuleToSelfMsg(RuleNodeToSelfMsg msg) throws Exception {
checkActive(msg.getMsg());
if (ruleNode.isDebugMode()) {
systemContext.persistDebugInput(tenantId, entityId, msg.getMsg(), "Self");
}
try {
tbNode.onMsg(defaultCtx, msg.getMsg());
} catch (Exception e) {
defaultCtx.tellFailure(msg.getMsg(), e);
TbMsg tbMsg = msg.getMsg();
int ruleNodeCount = tbMsg.getAndIncrementRuleNodeCounter();
int maxRuleNodeExecutionsPerMessage = getTenantProfileConfiguration().getMaxRuleNodeExecsPerMessage();
if (maxRuleNodeExecutionsPerMessage == 0 || ruleNodeCount < maxRuleNodeExecutionsPerMessage) {
apiUsageClient.report(tenantId, ApiUsageRecordKey.RE_EXEC_COUNT);
if (ruleNode.isDebugMode()) {
systemContext.persistDebugInput(tenantId, entityId, msg.getMsg(), "Self");
}
try {
tbNode.onMsg(defaultCtx, msg.getMsg());
} catch (Exception e) {
defaultCtx.tellFailure(msg.getMsg(), e);
}
} else {
tbMsg.getCallback().onFailure(new RuleNodeException("Message is processed by more then " + maxRuleNodeExecutionsPerMessage + " rule nodes!", ruleChainName, ruleNode));
}
}
void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) throws Exception {
msg.getMsg().getCallback().onProcessingStart(info);
checkActive(msg.getMsg());
if (ruleNode.isDebugMode()) {
systemContext.persistDebugInput(tenantId, entityId, msg.getMsg(), msg.getFromRelationType());
}
try {
tbNode.onMsg(msg.getCtx(), msg.getMsg());
} catch (Exception e) {
msg.getCtx().tellFailure(msg.getMsg(), e);
TbMsg tbMsg = msg.getMsg();
int ruleNodeCount = tbMsg.getAndIncrementRuleNodeCounter();
int maxRuleNodeExecutionsPerMessage = getTenantProfileConfiguration().getMaxRuleNodeExecsPerMessage();
if (maxRuleNodeExecutionsPerMessage == 0 || ruleNodeCount < maxRuleNodeExecutionsPerMessage) {
apiUsageClient.report(tenantId, ApiUsageRecordKey.RE_EXEC_COUNT);
if (ruleNode.isDebugMode()) {
systemContext.persistDebugInput(tenantId, entityId, msg.getMsg(), msg.getFromRelationType());
}
try {
tbNode.onMsg(msg.getCtx(), msg.getMsg());
} catch (Exception e) {
msg.getCtx().tellFailure(msg.getMsg(), e);
}
} else {
tbMsg.getCallback().onFailure(new RuleNodeException("Message is processed by more then " + maxRuleNodeExecutionsPerMessage + " rule nodes!", ruleChainName, ruleNode));
}
}

2
application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java

@ -20,6 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.actors.ActorSystemContext;
@ -113,6 +114,7 @@ public class DefaultActorService implements ActorService {
}
@EventListener(ApplicationReadyEvent.class)
@Order(value = 2)
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
log.info("Received application ready event. Sending application init message to actor system");
appActor.tellWithHighPriority(new AppInitMsg());

6
application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java

@ -19,9 +19,11 @@ import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.TbActorCtx;
import org.thingsboard.server.actors.stats.StatsPersistTick;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileConfiguration;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
import org.thingsboard.server.common.msg.queue.RuleNodeException;
@ -39,6 +41,10 @@ public abstract class ComponentMsgProcessor<T extends EntityId> extends Abstract
this.entityId = id;
}
protected TenantProfileConfiguration getTenantProfileConfiguration() {
return systemContext.getTenantProfileCache().get(tenantId).getProfileData().getConfiguration();
}
public abstract String getComponentName();
public abstract void start(TbActorCtx context) throws Exception;

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

@ -29,6 +29,7 @@ import org.thingsboard.server.actors.device.DeviceActorCreator;
import org.thingsboard.server.actors.ruleChain.RuleChainManagerActor;
import org.thingsboard.server.actors.service.ContextBasedCreator;
import org.thingsboard.server.actors.service.DefaultActorService;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
@ -62,6 +63,7 @@ public class TenantActor extends RuleChainManagerActor {
private boolean isRuleEngineForCurrentTenant;
private boolean isCore;
private ApiUsageState apiUsageState;
private TenantActor(ActorSystemContext systemContext, TenantId tenantId) {
super(systemContext, tenantId);
@ -79,21 +81,24 @@ public class TenantActor extends RuleChainManagerActor {
cantFindTenant = true;
log.info("[{}] Started tenant actor for missing tenant.", tenantId);
} else {
apiUsageState = new ApiUsageState(systemContext.getApiUsageStateService().getApiUsageState(tenant.getId()));
// This Service may be started for specific tenant only.
Optional<TenantId> isolatedTenantId = systemContext.getServiceInfoProvider().getIsolatedTenant();
// TODO: Tenant Profile from cache
TenantProfile tenantProfile = systemContext.getTenantProfileService().findTenantProfileById(tenantId, tenant.getTenantProfileId());
TenantProfile tenantProfile = systemContext.getTenantProfileCache().get(tenant.getTenantProfileId());
isRuleEngineForCurrentTenant = systemContext.getServiceInfoProvider().isService(ServiceType.TB_RULE_ENGINE);
isCore = systemContext.getServiceInfoProvider().isService(ServiceType.TB_CORE);
isRuleEngineForCurrentTenant = systemContext.getServiceInfoProvider().isService(ServiceType.TB_RULE_ENGINE);
if (isRuleEngineForCurrentTenant) {
try {
if (isolatedTenantId.map(id -> id.equals(tenantId)).orElseGet(() -> !tenantProfile.isIsolatedTbRuleEngine())) {
log.info("[{}] Going to init rule chains", tenantId);
initRuleChains();
if (apiUsageState.isReExecEnabled()) {
log.info("[{}] Going to init rule chains", tenantId);
initRuleChains();
} else {
log.info("[{}] Skip init of the rule chains due to API limits", tenantId);
}
} else {
isRuleEngineForCurrentTenant = false;
}
@ -105,8 +110,6 @@ public class TenantActor extends RuleChainManagerActor {
}
} catch (Exception e) {
log.warn("[{}] Unknown failure", tenantId, e);
// TODO: throw this in 3.1?
// throw new TbActorException("Failed to init actor", e);
}
}
@ -122,7 +125,7 @@ public class TenantActor extends RuleChainManagerActor {
if (msg.getMsgType().equals(MsgType.QUEUE_TO_RULE_ENGINE_MSG)) {
QueueToRuleEngineMsg queueMsg = (QueueToRuleEngineMsg) msg;
queueMsg.getTbMsg().getCallback().onSuccess();
} else if (msg.getMsgType().equals(MsgType.TRANSPORT_TO_DEVICE_ACTOR_MSG)){
} else if (msg.getMsgType().equals(MsgType.TRANSPORT_TO_DEVICE_ACTOR_MSG)) {
TransportToDeviceActorMsgWrapper transportMsg = (TransportToDeviceActorMsgWrapper) msg;
transportMsg.getCallback().onSuccess();
}
@ -180,26 +183,33 @@ public class TenantActor extends RuleChainManagerActor {
return;
}
TbMsg tbMsg = msg.getTbMsg();
if (tbMsg.getRuleChainId() == null) {
if (getRootChainActor() != null) {
getRootChainActor().tell(msg);
if (apiUsageState.isReExecEnabled()) {
if (tbMsg.getRuleChainId() == null) {
if (getRootChainActor() != null) {
getRootChainActor().tell(msg);
} else {
tbMsg.getCallback().onFailure(new RuleEngineException("No Root Rule Chain available!"));
log.info("[{}] No Root Chain: {}", tenantId, msg);
}
} else {
tbMsg.getCallback().onFailure(new RuleEngineException("No Root Rule Chain available!"));
log.info("[{}] No Root Chain: {}", tenantId, msg);
try {
ctx.tell(new TbEntityActorId(tbMsg.getRuleChainId()), msg);
} catch (TbActorNotRegisteredException ex) {
log.trace("Received message for non-existing rule chain: [{}]", tbMsg.getRuleChainId());
//TODO: 3.1 Log it to dead letters queue;
tbMsg.getCallback().onSuccess();
}
}
} else {
try {
ctx.tell(new TbEntityActorId(tbMsg.getRuleChainId()), msg);
} catch (TbActorNotRegisteredException ex) {
log.trace("Received message for non-existing rule chain: [{}]", tbMsg.getRuleChainId());
//TODO: 3.1 Log it to dead letters queue;
tbMsg.getCallback().onSuccess();
}
log.trace("[{}] Ack message because Rule Engine is disabled", tenantId);
tbMsg.getCallback().onSuccess();
}
}
private void onRuleChainMsg(RuleChainAwareMsg msg) {
getOrCreateActor(msg.getRuleChainId()).tell(msg);
if (apiUsageState.isReExecEnabled()) {
getOrCreateActor(msg.getRuleChainId()).tell(msg);
}
}
private void onToDeviceActorMsg(DeviceAwareMsg msg, boolean priority) {
@ -215,7 +225,17 @@ public class TenantActor extends RuleChainManagerActor {
}
private void onComponentLifecycleMsg(ComponentLifecycleMsg msg) {
if (msg.getEntityId().getEntityType() == EntityType.EDGE) {
if (msg.getEntityId().getEntityType().equals(EntityType.API_USAGE_STATE)) {
ApiUsageState old = apiUsageState;
apiUsageState = new ApiUsageState(systemContext.getApiUsageStateService().getApiUsageState(tenantId));
if (old.isReExecEnabled() && !apiUsageState.isReExecEnabled()) {
log.info("[{}] Received API state update. Going to DISABLE Rule Engine execution.", tenantId);
destroyRuleChains();
} else if (!old.isReExecEnabled() && apiUsageState.isReExecEnabled()) {
log.info("[{}] Received API state update. Going to ENABLE Rule Engine execution.", tenantId);
initRuleChains();
}
} else if (msg.getEntityId().getEntityType() == EntityType.EDGE) {
EdgeId edgeId = new EdgeId(msg.getEntityId().getId());
EdgeRpcService edgeRpcService = systemContext.getEdgeRpcService();
if (msg.getEvent() == ComponentLifecycleEvent.DELETED) {

267
application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java

@ -0,0 +1,267 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
import org.springframework.security.crypto.keygen.StringKeyGenerator;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
import org.thingsboard.server.utils.MiscUtils;
import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
@Service
@Slf4j
public class CustomOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
public static final String DEFAULT_AUTHORIZATION_REQUEST_BASE_URI = "/oauth2/authorization";
public static final String DEFAULT_LOGIN_PROCESSING_URI = "/login/oauth2/code/";
private static final String REGISTRATION_ID_URI_VARIABLE_NAME = "registrationId";
private static final char PATH_DELIMITER = '/';
private final AntPathRequestMatcher authorizationRequestMatcher = new AntPathRequestMatcher(
DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/{" + REGISTRATION_ID_URI_VARIABLE_NAME + "}");
private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder());
private final StringKeyGenerator secureKeyGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
@Autowired
private ClientRegistrationRepository clientRegistrationRepository;
@Autowired(required = false)
private OAuth2Configuration oauth2Configuration;
@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
String registrationId = this.resolveRegistrationId(request);
String redirectUriAction = getAction(request, "login");
return resolve(request, registrationId, redirectUriAction);
}
@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId) {
if (registrationId == null) {
return null;
}
String redirectUriAction = getAction(request, "authorize");
return resolve(request, registrationId, redirectUriAction);
}
private String getAction(HttpServletRequest request, String defaultAction) {
String action = request.getParameter("action");
if (action == null) {
return defaultAction;
}
return action;
}
private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction) {
if (registrationId == null) {
return null;
}
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
if (clientRegistration == null) {
throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId);
}
Map<String, Object> attributes = new HashMap<>();
attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId());
OAuth2AuthorizationRequest.Builder builder;
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
builder = OAuth2AuthorizationRequest.authorizationCode();
Map<String, Object> additionalParameters = new HashMap<>();
if (!CollectionUtils.isEmpty(clientRegistration.getScopes()) &&
clientRegistration.getScopes().contains(OidcScopes.OPENID)) {
// Section 3.1.2.1 Authentication Request - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
// scope
// REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.
addNonceParameters(attributes, additionalParameters);
}
if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) {
addPkceParameters(attributes, additionalParameters);
}
builder.additionalParameters(additionalParameters);
} else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) {
builder = OAuth2AuthorizationRequest.implicit();
} else {
throw new IllegalArgumentException("Invalid Authorization Grant Type (" +
clientRegistration.getAuthorizationGrantType().getValue() +
") for Client Registration with Id: " + clientRegistration.getRegistrationId());
}
String redirectUriStr = expandRedirectUri(request, clientRegistration, redirectUriAction);
return builder
.clientId(clientRegistration.getClientId())
.authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri())
.redirectUri(redirectUriStr)
.scopes(clientRegistration.getScopes())
.state(this.stateGenerator.generateKey())
.attributes(attributes)
.build();
}
private String resolveRegistrationId(HttpServletRequest request) {
if (this.authorizationRequestMatcher.matches(request)) {
return this.authorizationRequestMatcher
.matcher(request).getVariables().get(REGISTRATION_ID_URI_VARIABLE_NAME);
}
return null;
}
/**
* Expands the {@link ClientRegistration#getRedirectUriTemplate()} with following provided variables:<br/>
* - baseUrl (e.g. https://localhost/app) <br/>
* - baseScheme (e.g. https) <br/>
* - baseHost (e.g. localhost) <br/>
* - basePort (e.g. :8080) <br/>
* - basePath (e.g. /app) <br/>
* - registrationId (e.g. google) <br/>
* - action (e.g. login) <br/>
* <p/>
* Null variables are provided as empty strings.
* <p/>
* Default redirectUriTemplate is: {@link org.springframework.security.config.oauth2.client}.CommonOAuth2Provider#DEFAULT_REDIRECT_URL
*
* @return expanded URI
*/
private String expandRedirectUri(HttpServletRequest request, ClientRegistration clientRegistration, String action) {
Map<String, String> uriVariables = new HashMap<>();
uriVariables.put("registrationId", clientRegistration.getRegistrationId());
UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
.replacePath(request.getContextPath())
.replaceQuery(null)
.fragment(null)
.build();
String scheme = uriComponents.getScheme();
uriVariables.put("baseScheme", scheme == null ? "" : scheme);
String host = uriComponents.getHost();
uriVariables.put("baseHost", host == null ? "" : host);
// following logic is based on HierarchicalUriComponents#toUriString()
int port = uriComponents.getPort();
uriVariables.put("basePort", port == -1 ? "" : ":" + port);
String path = uriComponents.getPath();
if (StringUtils.hasLength(path)) {
if (path.charAt(0) != PATH_DELIMITER) {
path = PATH_DELIMITER + path;
}
}
uriVariables.put("basePath", path == null ? "" : path);
uriVariables.put("baseUrl", uriComponents.toUriString());
uriVariables.put("action", action == null ? "" : action);
String redirectUri = getRedirectUri(request);
log.trace("Redirect URI - {}.", redirectUri);
return UriComponentsBuilder.fromUriString(redirectUri)
.buildAndExpand(uriVariables)
.toUriString();
}
private String getRedirectUri(HttpServletRequest request) {
String loginProcessingUri = oauth2Configuration != null ? oauth2Configuration.getLoginProcessingUrl() : DEFAULT_LOGIN_PROCESSING_URI;
String scheme = MiscUtils.getScheme(request);
String domainName = MiscUtils.getDomainName(request);
int port = MiscUtils.getPort(request);
String baseUrl = scheme + "://" + domainName;
if (needsPort(scheme, port)){
baseUrl += ":" + port;
}
return baseUrl + loginProcessingUri;
}
private boolean needsPort(String scheme, int port) {
boolean isHttpDefault = "http".equals(scheme.toLowerCase()) && port == 80;
boolean isHttpsDefault = "https".equals(scheme.toLowerCase()) && port == 443;
return !isHttpDefault && !isHttpsDefault;
}
/**
* Creates nonce and its hash for use in OpenID Connect 1.0 Authentication Requests.
*
* @param attributes where the {@link OidcParameterNames#NONCE} is stored for the authentication request
* @param additionalParameters where the {@link OidcParameterNames#NONCE} hash is added for the authentication request
*
* @since 5.2
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest">3.1.2.1. Authentication Request</a>
*/
private void addNonceParameters(Map<String, Object> attributes, Map<String, Object> additionalParameters) {
try {
String nonce = this.secureKeyGenerator.generateKey();
String nonceHash = createHash(nonce);
attributes.put(OidcParameterNames.NONCE, nonce);
additionalParameters.put(OidcParameterNames.NONCE, nonceHash);
} catch (NoSuchAlgorithmException e) { }
}
/**
* Creates and adds additional PKCE parameters for use in the OAuth 2.0 Authorization and Access Token Requests
*
* @param attributes where {@link PkceParameterNames#CODE_VERIFIER} is stored for the token request
* @param additionalParameters where {@link PkceParameterNames#CODE_CHALLENGE} and, usually,
* {@link PkceParameterNames#CODE_CHALLENGE_METHOD} are added to be used in the authorization request.
*
* @since 5.2
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-1.1">1.1. Protocol Flow</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.1">4.1. Client Creates a Code Verifier</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.2">4.2. Client Creates the Code Challenge</a>
*/
private void addPkceParameters(Map<String, Object> attributes, Map<String, Object> additionalParameters) {
String codeVerifier = this.secureKeyGenerator.generateKey();
attributes.put(PkceParameterNames.CODE_VERIFIER, codeVerifier);
try {
String codeChallenge = createHash(codeVerifier);
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge);
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
} catch (NoSuchAlgorithmException e) {
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeVerifier);
}
}
private static String createHash(String value) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(value.getBytes(StandardCharsets.US_ASCII));
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
}
}

8
application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java

@ -32,6 +32,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@ -175,6 +176,9 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
web.ignoring().antMatchers("/*.js","/*.css","/*.ico","/assets/**","/static/**");
}
@Autowired
private OAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().cacheControl().and().frameOptions().disable()
@ -207,8 +211,10 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
.addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class);
if (oauth2Configuration != null && oauth2Configuration.isEnabled()) {
if (oauth2Configuration != null) {
http.oauth2Login()
.authorizationEndpoint().authorizationRequestResolver(oAuth2AuthorizationRequestResolver)
.and()
.loginPage("/oauth2Login")
.loginProcessingUrl(oauth2Configuration.getLoginProcessingUrl())
.successHandler(oauth2AuthenticationSuccessHandler)

18
application/src/main/java/org/thingsboard/server/controller/AdminController.java

@ -25,6 +25,8 @@ import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.server.common.data.sms.config.TestSmsRequest;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.UpdateMessage;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -45,6 +47,9 @@ public class AdminController extends BaseController {
@Autowired
private MailService mailService;
@Autowired
private SmsService smsService;
@Autowired
private AdminSettingsService adminSettingsService;
@ -80,6 +85,8 @@ public class AdminController extends BaseController {
if (adminSettings.getKey().equals("mail")) {
mailService.updateMailConfiguration();
((ObjectNode) adminSettings.getJsonValue()).put("password", "");
} else if (adminSettings.getKey().equals("sms")) {
smsService.updateSmsConfiguration();
}
return adminSettings;
} catch (Exception e) {
@ -127,6 +134,17 @@ public class AdminController extends BaseController {
}
}
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/settings/testSms", method = RequestMethod.POST)
public void sendTestSms(@RequestBody TestSmsRequest testSmsRequest) throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
smsService.sendTestSms(testSmsRequest);
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/updates", method = RequestMethod.GET)
@ResponseBody

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

@ -133,6 +133,7 @@ public class AlarmController extends BaseController {
long ackTs = System.currentTimeMillis();
alarmService.ackAlarm(getCurrentUser().getTenantId(), alarmId, ackTs).get();
alarm.setAckTs(ackTs);
alarm.setStatus(alarm.getStatus().isCleared() ? AlarmStatus.CLEARED_ACK : AlarmStatus.ACTIVE_ACK);
logEntityAction(alarm.getOriginator(), alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_ACK, null);
sendNotificationMsgToEdgeService(getTenantId(), alarmId, ActionType.ALARM_ACK);
@ -152,6 +153,7 @@ public class AlarmController extends BaseController {
long clearTs = System.currentTimeMillis();
alarmService.clearAlarm(getCurrentUser().getTenantId(), alarmId, null, clearTs).get();
alarm.setClearTs(clearTs);
alarm.setStatus(alarm.getStatus().isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK);
logEntityAction(alarm.getOriginator(), alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_CLEAR, null);
sendNotificationMsgToEdgeService(getTenantId(), alarmId, ActionType.ALARM_CLEAR);

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

@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.asset.AssetSearchQuery;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
@ -55,6 +56,8 @@ import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import static org.thingsboard.server.dao.asset.BaseAssetService.TB_SERVICE_QUEUE;
import static org.thingsboard.server.controller.EdgeController.EDGE_ID;
@RestController
@ -95,6 +98,10 @@ public class AssetController extends BaseController {
@ResponseBody
public Asset saveAsset(@RequestBody Asset asset) throws ThingsboardException {
try {
if (TB_SERVICE_QUEUE.equals(asset.getType())) {
throw new ThingsboardException("Unable to save asset with type " + TB_SERVICE_QUEUE, ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
asset.setTenantId(getCurrentUser().getTenantId());
checkEntity(asset.getId(), asset, Resource.ASSET);

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

@ -39,10 +39,8 @@ import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.oauth2.OAuth2Service;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
@ -85,9 +83,6 @@ public class AuthController extends BaseController {
@Autowired
private AuditLogService auditLogService;
@Autowired
private OAuth2Service oauth2Service;
@PreAuthorize("isAuthenticated()")
@RequestMapping(value = "/auth/user", method = RequestMethod.GET)
public @ResponseBody User getUser() throws ThingsboardException {
@ -175,7 +170,8 @@ public class AuthController extends BaseController {
try {
String email = resetPasswordByEmailRequest.get("email").asText();
UserCredentials userCredentials = userService.requestPasswordReset(TenantId.SYS_TENANT_ID, email);
String baseUrl = MiscUtils.constructBaseUrl(request);
User user = userService.findUserById(TenantId.SYS_TENANT_ID, userCredentials.getUserId());
String baseUrl = systemSecurityService.getBaseUrl(user.getTenantId(), user.getCustomerId(), request);
String resetUrl = String.format("%s/api/noauth/resetPassword?resetToken=%s", baseUrl,
userCredentials.getResetToken());
@ -223,7 +219,7 @@ public class AuthController extends BaseController {
User user = userService.findUserById(TenantId.SYS_TENANT_ID, credentials.getUserId());
UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail());
SecurityUser securityUser = new SecurityUser(user, credentials.isEnabled(), principal);
String baseUrl = MiscUtils.constructBaseUrl(request);
String baseUrl = systemSecurityService.getBaseUrl(user.getTenantId(), user.getCustomerId(), request);
String loginUrl = String.format("%s/login", baseUrl);
String email = user.getEmail();
@ -272,7 +268,7 @@ public class AuthController extends BaseController {
User user = userService.findUserById(TenantId.SYS_TENANT_ID, userCredentials.getUserId());
UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail());
SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), principal);
String baseUrl = MiscUtils.constructBaseUrl(request);
String baseUrl = systemSecurityService.getBaseUrl(user.getTenantId(), user.getCustomerId(), request);
String loginUrl = String.format("%s/login", baseUrl);
String email = user.getEmail();
mailService.sendPasswordWasResetEmail(loginUrl, email);
@ -342,14 +338,4 @@ public class AuthController extends BaseController {
throw handleException(e);
}
}
@RequestMapping(value = "/noauth/oauth2Clients", method = RequestMethod.POST)
@ResponseBody
public List<OAuth2ClientInfo> getOAuth2Clients() throws ThingsboardException {
try {
return oauth2Service.getOAuth2Clients();
} catch (Exception e) {
throw handleException(e);
}
}
}

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

@ -100,6 +100,8 @@ import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService;
import org.thingsboard.server.dao.oauth2.OAuth2Service;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantProfileService;
@ -117,6 +119,7 @@ import org.thingsboard.server.service.edge.EdgeNotificationService;
import org.thingsboard.server.service.edge.rpc.EdgeGrpcService;
import org.thingsboard.server.service.edge.rpc.init.SyncEdgeService;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.service.queue.TbClusterService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.AccessControlService;
@ -186,6 +189,12 @@ public abstract class BaseController {
@Autowired
protected DashboardService dashboardService;
@Autowired
protected OAuth2Service oAuth2Service;
@Autowired
protected OAuth2ConfigTemplateService oAuth2ConfigTemplateService;
@Autowired
protected ComponentDiscoveryService componentDescriptorService;
@ -222,6 +231,9 @@ public abstract class BaseController {
@Autowired
protected TbQueueProducerProvider producerProvider;
@Autowired
protected TbTenantProfileCache tenantProfileCache;
@Autowired
protected TbDeviceProfileCache deviceProfileCache;
@ -747,6 +759,12 @@ public abstract class BaseController {
case ASSIGNED_TO_TENANT:
msgType = DataConstants.ENTITY_ASSIGNED_TO_TENANT;
break;
case PROVISION_SUCCESS:
msgType = DataConstants.PROVISION_SUCCESS;
break;
case PROVISION_FAILURE:
msgType = DataConstants.PROVISION_FAILURE;
break;
case ASSIGNED_TO_EDGE:
msgType = DataConstants.ENTITY_ASSIGNED_TO_EDGE;
break;

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

@ -54,6 +54,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
@ -122,8 +123,11 @@ public class DeviceController extends BaseController {
Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken));
tbClusterService.onDeviceChange(savedDevice, null);
tbClusterService.pushMsgToCore(new DeviceNameOrTypeUpdateMsg(savedDevice.getTenantId(),
savedDevice.getId(), savedDevice.getName(), savedDevice.getType()), null);
tbClusterService.onEntityStateChange(savedDevice.getTenantId(), savedDevice.getId(),
device.getId() == null ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
if (device.getId() != null) {
sendNotificationMsgToEdgeService(savedDevice.getTenantId(), savedDevice.getId(), ActionType.UPDATED);
@ -156,6 +160,9 @@ public class DeviceController extends BaseController {
Device device = checkDeviceId(deviceId, Operation.DELETE);
deviceService.deleteDevice(getCurrentUser().getTenantId(), deviceId);
tbClusterService.onDeviceDeleted(device, null);
tbClusterService.onEntityStateChange(device.getTenantId(), deviceId, ComponentLifecycleEvent.DELETED);
logEntityAction(deviceId, device,
device.getCustomerId(),
ActionType.DELETED, null, strDeviceId);

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

@ -94,7 +94,6 @@ public class DeviceProfileController extends BaseController {
DeviceProfile savedDeviceProfile = checkNotNull(deviceProfileService.saveDeviceProfile(deviceProfile));
deviceProfileCache.put(savedDeviceProfile);
tbClusterService.onDeviceProfileChange(savedDeviceProfile, null);
tbClusterService.onEntityStateChange(deviceProfile.getTenantId(), savedDeviceProfile.getId(),
created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
@ -120,7 +119,6 @@ public class DeviceProfileController extends BaseController {
DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId));
DeviceProfile deviceProfile = checkDeviceProfileId(deviceProfileId, Operation.DELETE);
deviceProfileService.deleteDeviceProfile(getTenantId(), deviceProfileId);
deviceProfileCache.evict(deviceProfileId);
tbClusterService.onDeviceProfileDelete(deviceProfile, null);
tbClusterService.onEntityStateChange(deviceProfile.getTenantId(), deviceProfile.getId(), ComponentLifecycleEvent.DELETED);
@ -192,10 +190,11 @@ public class DeviceProfileController extends BaseController {
@RequestParam int page,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String sortProperty,
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
@RequestParam(required = false) String sortOrder,
@RequestParam(required = false) String transportType) throws ThingsboardException {
try {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return checkNotNull(deviceProfileService.findDeviceProfileInfos(getTenantId(), pageLink));
return checkNotNull(deviceProfileService.findDeviceProfileInfos(getTenantId(), pageLink, transportType));
} catch (Exception e) {
throw handleException(e);
}

77
application/src/main/java/org/thingsboard/server/controller/OAuth2ConfigTemplateController.java

@ -0,0 +1,77 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.List;
@RestController
@TbCoreComponent
@RequestMapping("/api/oauth2/config/template")
@Slf4j
public class OAuth2ConfigTemplateController extends BaseController {
private static final String CLIENT_REGISTRATION_TEMPLATE_ID = "clientRegistrationTemplateId";
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@RequestMapping(method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
public OAuth2ClientRegistrationTemplate saveClientRegistrationTemplate(@RequestBody OAuth2ClientRegistrationTemplate clientRegistrationTemplate) throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_TEMPLATE, Operation.WRITE);
return oAuth2ConfigTemplateService.saveClientRegistrationTemplate(clientRegistrationTemplate);
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@RequestMapping(value = "/{clientRegistrationTemplateId}", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK)
public void deleteClientRegistrationTemplate(@PathVariable(CLIENT_REGISTRATION_TEMPLATE_ID) String strClientRegistrationTemplateId) throws ThingsboardException {
checkParameter(CLIENT_REGISTRATION_TEMPLATE_ID, strClientRegistrationTemplateId);
try {
accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_TEMPLATE, Operation.DELETE);
OAuth2ClientRegistrationTemplateId clientRegistrationTemplateId = new OAuth2ClientRegistrationTemplateId(toUUID(strClientRegistrationTemplateId));
oAuth2ConfigTemplateService.deleteClientRegistrationTemplateById(clientRegistrationTemplateId);
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(method = RequestMethod.GET, produces = "application/json")
@ResponseBody
public List<OAuth2ClientRegistrationTemplate> getClientRegistrationTemplates() throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_TEMPLATE, Operation.READ);
return oAuth2ConfigTemplateService.findAllClientRegistrationTemplates();
} catch (Exception e) {
throw handleException(e);
}
}
}

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

@ -0,0 +1,91 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientsParams;
import org.thingsboard.server.common.data.oauth2.SchemeType;
import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.utils.MiscUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
@RestController
@TbCoreComponent
@RequestMapping("/api")
@Slf4j
public class OAuth2Controller extends BaseController {
@Autowired
private OAuth2Configuration oAuth2Configuration;
@RequestMapping(value = "/noauth/oauth2Clients", method = RequestMethod.POST)
@ResponseBody
public List<OAuth2ClientInfo> getOAuth2Clients(HttpServletRequest request) throws ThingsboardException {
try {
return oAuth2Service.getOAuth2Clients(MiscUtils.getScheme(request), MiscUtils.getDomainNameAndPort(request));
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@RequestMapping(value = "/oauth2/config", method = RequestMethod.GET, produces = "application/json")
@ResponseBody
public OAuth2ClientsParams getCurrentOAuth2Params() throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_INFO, Operation.READ);
return oAuth2Service.findOAuth2Params();
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@RequestMapping(value = "/oauth2/config", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
public OAuth2ClientsParams saveOAuth2Params(@RequestBody OAuth2ClientsParams oauth2Params) throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_INFO, Operation.WRITE);
oAuth2Service.saveOAuth2Params(oauth2Params);
return oAuth2Service.findOAuth2Params();
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@RequestMapping(value = "/oauth2/loginProcessingUrl", method = RequestMethod.GET)
@ResponseBody
public String getLoginProcessingUrl() throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_INFO, Operation.READ);
return "\"" + oAuth2Configuration.getLoginProcessingUrl() + "\"";
} catch (Exception e) {
throw handleException(e);
}
}
}

6
application/src/main/java/org/thingsboard/server/controller/RuleChainController.java

@ -173,10 +173,12 @@ public class RuleChainController extends BaseController {
public RuleChain saveRuleChain(@RequestBody DefaultRuleChainCreateRequest request) throws ThingsboardException {
try {
checkNotNull(request);
checkNotNull(request.getName());
checkParameter(request.getName(), "name");
RuleChain savedRuleChain = installScripts.createDefaultRuleChain(getCurrentUser().getTenantId(), request.getName());
tbClusterService.onEntityStateChange(savedRuleChain.getTenantId(), savedRuleChain.getId(), ComponentLifecycleEvent.CREATED);
logEntityAction(savedRuleChain.getId(), savedRuleChain, null, ActionType.ADDED, null);
return savedRuleChain;
@ -377,7 +379,7 @@ public class RuleChainController extends BaseController {
String errorText = "";
ScriptEngine engine = null;
try {
engine = new RuleNodeJsScriptEngine(jsInvokeService, getCurrentUser().getId(), script, argNames);
engine = new RuleNodeJsScriptEngine(getTenantId(), jsInvokeService, getCurrentUser().getId(), script, argNames);
TbMsg inMsg = TbMsg.newMsg(msgType, null, new TbMsgMetaData(metadata), TbMsgDataType.JSON, data);
switch (scriptType) {
case "update":

18
application/src/main/java/org/thingsboard/server/controller/TelemetryController.java

@ -45,6 +45,7 @@ import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.DeviceId;
@ -69,6 +70,7 @@ import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.transport.adaptor.JsonConverter;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -93,6 +95,7 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
@ -205,7 +208,7 @@ public class TelemetryController extends BaseController {
@RequestParam(name = "interval", defaultValue = "0") Long interval,
@RequestParam(name = "limit", defaultValue = "100") Integer limit,
@RequestParam(name = "agg", defaultValue = "NONE") String aggStr,
@RequestParam(name= "orderBy", defaultValue = "DESC") String orderBy,
@RequestParam(name = "orderBy", defaultValue = "DESC") String orderBy,
@RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes) throws ThingsboardException {
return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr,
(result, tenantId, entityId) -> {
@ -392,6 +395,11 @@ public class TelemetryController extends BaseController {
if (attributes.isEmpty()) {
return getImmediateDeferredResult("No attributes data found in request body!", HttpStatus.BAD_REQUEST);
}
for (AttributeKvEntry attributeKvEntry : attributes) {
if (attributeKvEntry.getKey().isEmpty() || attributeKvEntry.getKey().trim().length() == 0) {
return getImmediateDeferredResult("Key cannot be empty or contains only spaces", HttpStatus.BAD_REQUEST);
}
}
SecurityUser user = getCurrentUser();
return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.WRITE_ATTRIBUTES, entityIdSrc, (result, tenantId, entityId) -> {
tsSubService.saveAndNotify(tenantId, entityId, scope, attributes, new FutureCallback<Void>() {
@ -435,9 +443,13 @@ public class TelemetryController extends BaseController {
if (entries.isEmpty()) {
return getImmediateDeferredResult("No timeseries data found in request body!", HttpStatus.BAD_REQUEST);
}
SecurityUser user = getCurrentUser();
return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.WRITE_TELEMETRY, entityIdSrc, (result, tenantId, entityId) -> {
tsSubService.saveAndNotify(tenantId, entityId, entries, ttl, new FutureCallback<Void>() {
long tenantTtl = ttl;
if (!TenantId.SYS_TENANT_ID.equals(tenantId) && tenantTtl == 0) {
TenantProfile tenantProfile = tenantProfileCache.get(tenantId);
tenantTtl = TimeUnit.DAYS.toSeconds(((DefaultTenantProfileConfiguration) tenantProfile.getProfileData().getConfiguration()).getDefaultStorageTtlDays());
}
tsSubService.saveAndNotify(tenantId, entityId, entries, tenantTtl, new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable Void tmp) {
result.setResult(new ResponseEntity(HttpStatus.OK));

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

@ -91,6 +91,10 @@ public class TenantController extends BaseController {
if (newTenant) {
installScripts.createDefaultRuleChains(tenant.getId());
}
tenantProfileCache.evict(tenant.getId());
tbClusterService.onTenantChange(tenant, null);
tbClusterService.onEntityStateChange(tenant.getId(), tenant.getId(),
newTenant ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
return tenant;
} catch (Exception e) {
throw handleException(e);
@ -104,8 +108,10 @@ public class TenantController extends BaseController {
checkParameter("tenantId", strTenantId);
try {
TenantId tenantId = new TenantId(toUUID(strTenantId));
checkTenantId(tenantId, Operation.DELETE);
Tenant tenant = checkTenantId(tenantId, Operation.DELETE);
tenantService.deleteTenant(tenantId);
tenantProfileCache.evict(tenantId);
tbClusterService.onTenantDelete(tenant, null);
tbClusterService.onEntityStateChange(tenantId, tenantId, ComponentLifecycleEvent.DELETED);
} catch (Exception e) {
throw handleException(e);

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

@ -29,9 +29,12 @@ import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
@ -93,8 +96,12 @@ public class TenantProfileController extends BaseController {
}
tenantProfile = checkNotNull(tenantProfileService.saveTenantProfile(getTenantId(), tenantProfile));
tenantProfileCache.put(tenantProfile);
tbClusterService.onTenantProfileChange(tenantProfile, null);
tbClusterService.onEntityStateChange(TenantId.SYS_TENANT_ID, tenantProfile.getId(),
newTenantProfile ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
return tenantProfile;
} catch (Exception e) {
} catch (Exception e) {
throw handleException(e);
}
}
@ -106,8 +113,9 @@ public class TenantProfileController extends BaseController {
checkParameter("tenantProfileId", strTenantProfileId);
try {
TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId));
checkTenantProfileId(tenantProfileId, Operation.DELETE);
TenantProfile profile = checkTenantProfileId(tenantProfileId, Operation.DELETE);
tenantProfileService.deleteTenantProfile(getTenantId(), tenantProfileId);
tbClusterService.onTenantProfileDelete(profile, null);
} catch (Exception e) {
throw handleException(e);
}

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

@ -52,6 +52,7 @@ import org.thingsboard.server.service.security.model.token.JwtToken;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import org.thingsboard.server.utils.MiscUtils;
import javax.servlet.http.HttpServletRequest;
@ -78,6 +79,9 @@ public class UserController extends BaseController {
@Autowired
private RefreshTokenRepository refreshTokenRepository;
@Autowired
private SystemSecurityService systemSecurityService;
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/user/{userId}", method = RequestMethod.GET)
@ -146,7 +150,7 @@ public class UserController extends BaseController {
if (sendEmail) {
SecurityUser authUser = getCurrentUser();
UserCredentials userCredentials = userService.findUserCredentialsByUserId(authUser.getTenantId(), savedUser.getId());
String baseUrl = MiscUtils.constructBaseUrl(request);
String baseUrl = systemSecurityService.getBaseUrl(getTenantId(), getCurrentUser().getCustomerId(), request);
String activateUrl = String.format(ACTIVATE_URL_PATTERN, baseUrl,
userCredentials.getActivateToken());
String email = savedUser.getEmail();
@ -189,7 +193,7 @@ public class UserController extends BaseController {
UserCredentials userCredentials = userService.findUserCredentialsByUserId(getCurrentUser().getTenantId(), user.getId());
if (!userCredentials.isEnabled()) {
String baseUrl = MiscUtils.constructBaseUrl(request);
String baseUrl = systemSecurityService.getBaseUrl(getTenantId(), getCurrentUser().getCustomerId(), request);
String activateUrl = String.format(ACTIVATE_URL_PATTERN, baseUrl,
userCredentials.getActivateToken());
mailService.sendActivationEmail(activateUrl, email);
@ -214,7 +218,7 @@ public class UserController extends BaseController {
SecurityUser authUser = getCurrentUser();
UserCredentials userCredentials = userService.findUserCredentialsByUserId(authUser.getTenantId(), user.getId());
if (!userCredentials.isEnabled()) {
String baseUrl = MiscUtils.constructBaseUrl(request);
String baseUrl = systemSecurityService.getBaseUrl(getTenantId(), getCurrentUser().getCustomerId(), request);
String activateUrl = String.format(ACTIVATE_URL_PATTERN, baseUrl,
userCredentials.getActivateToken());
return activateUrl;

2
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -194,6 +194,7 @@ public class ThingsboardInstallService {
dataUpdateService.updateData("3.1.1");
log.info("Updating system data...");
systemDataLoaderService.updateSystemWidgets();
systemDataLoaderService.createOAuth2Templates();
break;
default:
throw new RuntimeException("Unable to upgrade ThingsBoard, unsupported fromVersion: " + upgradeFromVersion);
@ -226,6 +227,7 @@ public class ThingsboardInstallService {
systemDataLoaderService.createDefaultTenantProfiles();
systemDataLoaderService.createAdminSettings();
systemDataLoaderService.loadSystemWidgets();
systemDataLoaderService.createOAuth2Templates();
// systemDataLoaderService.loadSystemPlugins();
// systemDataLoaderService.loadSystemRules();

450
application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java

@ -0,0 +1,450 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.apiusage;
import com.google.common.util.concurrent.FutureCallback;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.ApiFeature;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.ApiUsageStateMailMessage;
import org.thingsboard.server.common.data.ApiUsageStateValue;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.ApiUsageStateId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
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.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.tools.SchedulerUtils;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.usagerecord.ApiUsageStateService;
import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg;
import org.thingsboard.server.gen.transport.TransportProtos.UsageStatsKVProto;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.PartitionChangeEvent;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.scheduler.SchedulerComponent;
import org.thingsboard.server.service.queue.TbClusterService;
import org.thingsboard.server.service.telemetry.InternalTelemetryService;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
@Slf4j
@Service
public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
public static final String HOURLY = "Hourly";
public static final FutureCallback<Integer> VOID_CALLBACK = new FutureCallback<Integer>() {
@Override
public void onSuccess(@Nullable Integer result) {
}
@Override
public void onFailure(Throwable t) {
}
};
private final TbClusterService clusterService;
private final PartitionService partitionService;
private final TenantService tenantService;
private final TimeseriesService tsService;
private final ApiUsageStateService apiUsageStateService;
private final SchedulerComponent scheduler;
private final TbTenantProfileCache tenantProfileCache;
private final MailService mailService;
@Lazy
@Autowired
private InternalTelemetryService tsWsService;
// Tenants that should be processed on this server
private final Map<TenantId, TenantApiUsageState> myTenantStates = new ConcurrentHashMap<>();
// Tenants that should be processed on other servers
private final Map<TenantId, ApiUsageState> otherTenantStates = new ConcurrentHashMap<>();
@Value("${usage.stats.report.enabled:true}")
private boolean enabled;
@Value("${usage.stats.check.cycle:60000}")
private long nextCycleCheckInterval;
private final Lock updateLock = new ReentrantLock();
private final ExecutorService mailExecutor;
public DefaultTbApiUsageStateService(TbClusterService clusterService,
PartitionService partitionService,
TenantService tenantService,
TimeseriesService tsService,
ApiUsageStateService apiUsageStateService,
SchedulerComponent scheduler,
TbTenantProfileCache tenantProfileCache, MailService mailService) {
this.clusterService = clusterService;
this.partitionService = partitionService;
this.tenantService = tenantService;
this.tsService = tsService;
this.apiUsageStateService = apiUsageStateService;
this.scheduler = scheduler;
this.tenantProfileCache = tenantProfileCache;
this.mailService = mailService;
this.mailExecutor = Executors.newSingleThreadExecutor();
}
@PostConstruct
public void init() {
if (enabled) {
log.info("Starting api usage service.");
scheduler.scheduleAtFixedRate(this::checkStartOfNextCycle, nextCycleCheckInterval, nextCycleCheckInterval, TimeUnit.MILLISECONDS);
log.info("Started api usage service.");
}
}
@Override
public void process(TbProtoQueueMsg<ToUsageStatsServiceMsg> msg, TbCallback callback) {
ToUsageStatsServiceMsg statsMsg = msg.getValue();
TenantId tenantId = new TenantId(new UUID(statsMsg.getTenantIdMSB(), statsMsg.getTenantIdLSB()));
if (tenantProfileCache.get(tenantId) == null) {
return;
}
TenantApiUsageState tenantState;
List<TsKvEntry> updatedEntries;
Map<ApiFeature, ApiUsageStateValue> result;
updateLock.lock();
try {
tenantState = getOrFetchState(tenantId);
long ts = tenantState.getCurrentCycleTs();
long hourTs = tenantState.getCurrentHourTs();
long newHourTs = SchedulerUtils.getStartOfCurrentHour();
if (newHourTs != hourTs) {
tenantState.setHour(newHourTs);
}
updatedEntries = new ArrayList<>(ApiUsageRecordKey.values().length);
Set<ApiFeature> apiFeatures = new HashSet<>();
for (UsageStatsKVProto kvProto : statsMsg.getValuesList()) {
ApiUsageRecordKey recordKey = ApiUsageRecordKey.valueOf(kvProto.getKey());
long newValue = tenantState.add(recordKey, kvProto.getValue());
updatedEntries.add(new BasicTsKvEntry(ts, new LongDataEntry(recordKey.getApiCountKey(), newValue)));
long newHourlyValue = tenantState.addToHourly(recordKey, kvProto.getValue());
updatedEntries.add(new BasicTsKvEntry(newHourTs, new LongDataEntry(recordKey.getApiCountKey() + HOURLY, newHourlyValue)));
apiFeatures.add(recordKey.getApiFeature());
}
result = tenantState.checkStateUpdatedDueToThreshold(apiFeatures);
} finally {
updateLock.unlock();
}
tsWsService.saveAndNotifyInternal(tenantId, tenantState.getApiUsageState().getId(), updatedEntries, VOID_CALLBACK);
if (!result.isEmpty()) {
persistAndNotify(tenantState, result);
}
callback.onSuccess();
}
@Override
public void onApplicationEvent(PartitionChangeEvent partitionChangeEvent) {
if (partitionChangeEvent.getServiceType().equals(ServiceType.TB_CORE)) {
myTenantStates.entrySet().removeIf(entry -> !partitionService.resolve(ServiceType.TB_CORE, entry.getKey(), entry.getKey()).isMyPartition());
otherTenantStates.entrySet().removeIf(entry -> partitionService.resolve(ServiceType.TB_CORE, entry.getKey(), entry.getKey()).isMyPartition());
initStatesFromDataBase();
}
}
@Override
public ApiUsageState getApiUsageState(TenantId tenantId) {
TenantApiUsageState tenantState = myTenantStates.get(tenantId);
if (tenantState != null) {
return tenantState.getApiUsageState();
} else {
ApiUsageState state = otherTenantStates.get(tenantId);
if (state != null) {
return state;
} else {
if (partitionService.resolve(ServiceType.TB_CORE, tenantId, tenantId).isMyPartition()) {
return getOrFetchState(tenantId).getApiUsageState();
} else {
updateLock.lock();
try {
state = otherTenantStates.get(tenantId);
if (state == null) {
state = apiUsageStateService.findTenantApiUsageState(tenantId);
if (state != null) {
otherTenantStates.put(tenantId, state);
}
}
} finally {
updateLock.unlock();
}
return state;
}
}
}
}
@Override
public void onApiUsageStateUpdate(TenantId tenantId) {
otherTenantStates.remove(tenantId);
}
@Override
public void onTenantProfileUpdate(TenantProfileId tenantProfileId) {
log.info("[{}] On Tenant Profile Update", tenantProfileId);
TenantProfile tenantProfile = tenantProfileCache.get(tenantProfileId);
updateLock.lock();
try {
myTenantStates.values().forEach(state -> {
if (tenantProfile.getId().equals(state.getTenantProfileId())) {
updateTenantState(state, tenantProfile);
}
});
} finally {
updateLock.unlock();
}
}
@Override
public void onTenantUpdate(TenantId tenantId) {
log.info("[{}] On Tenant Update.", tenantId);
TenantProfile tenantProfile = tenantProfileCache.get(tenantId);
updateLock.lock();
try {
TenantApiUsageState state = myTenantStates.get(tenantId);
if (state != null && !state.getTenantProfileId().equals(tenantProfile.getId())) {
updateTenantState(state, tenantProfile);
}
} finally {
updateLock.unlock();
}
}
private void updateTenantState(TenantApiUsageState state, TenantProfile profile) {
TenantProfileData oldProfileData = state.getTenantProfileData();
state.setTenantProfileId(profile.getId());
state.setTenantProfileData(profile.getProfileData());
Map<ApiFeature, ApiUsageStateValue> result = state.checkStateUpdatedDueToThresholds();
if (!result.isEmpty()) {
persistAndNotify(state, result);
}
updateProfileThresholds(state.getTenantId(), state.getApiUsageState().getId(),
oldProfileData.getConfiguration(), profile.getProfileData().getConfiguration());
}
private void updateProfileThresholds(TenantId tenantId, ApiUsageStateId id,
TenantProfileConfiguration oldData, TenantProfileConfiguration newData) {
long ts = System.currentTimeMillis();
List<TsKvEntry> profileThresholds = new ArrayList<>();
for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) {
long newProfileThreshold = newData.getProfileThreshold(key);
if (oldData == null || oldData.getProfileThreshold(key) != newProfileThreshold) {
log.info("[{}] Updating profile threshold [{}]:[{}]", tenantId, key, newProfileThreshold);
profileThresholds.add(new BasicTsKvEntry(ts, new LongDataEntry(key.getApiLimitKey(), newProfileThreshold)));
}
}
if (!profileThresholds.isEmpty()) {
tsWsService.saveAndNotifyInternal(tenantId, id, profileThresholds, VOID_CALLBACK);
}
}
private void persistAndNotify(TenantApiUsageState state, Map<ApiFeature, ApiUsageStateValue> result) {
log.info("[{}] Detected update of the API state: {}", state.getTenantId(), result);
apiUsageStateService.update(state.getApiUsageState());
clusterService.onApiStateChange(state.getApiUsageState(), null);
long ts = System.currentTimeMillis();
List<TsKvEntry> stateTelemetry = new ArrayList<>();
result.forEach(((apiFeature, aState) -> stateTelemetry.add(new BasicTsKvEntry(ts, new StringDataEntry(apiFeature.getApiStateKey(), aState.name())))));
tsWsService.saveAndNotifyInternal(state.getTenantId(), state.getApiUsageState().getId(), stateTelemetry, VOID_CALLBACK);
String email = tenantService.findTenantById(state.getTenantId()).getEmail();
if (StringUtils.isNotEmpty(email)) {
result.forEach((apiFeature, stateValue) -> {
mailExecutor.submit(() -> {
try {
mailService.sendApiFeatureStateEmail(apiFeature, stateValue, email, createStateMailMessage(state, apiFeature, stateValue));
} catch (ThingsboardException e) {
log.warn("[{}] Can't send update of the API state to tenant with provided email [{}]", state.getTenantId(), email, e);
}
});
});
} else {
log.warn("[{}] Can't send update of the API state to tenant with empty email!", state.getTenantId());
}
}
private ApiUsageStateMailMessage createStateMailMessage(TenantApiUsageState state, ApiFeature apiFeature, ApiUsageStateValue stateValue) {
StateChecker checker = getStateChecker(stateValue);
for (ApiUsageRecordKey apiUsageRecordKey : ApiUsageRecordKey.getKeys(apiFeature)) {
long threshold = state.getProfileThreshold(apiUsageRecordKey);
long warnThreshold = state.getProfileWarnThreshold(apiUsageRecordKey);
long value = state.get(apiUsageRecordKey);
if (checker.check(threshold, warnThreshold, value)) {
return new ApiUsageStateMailMessage(apiUsageRecordKey, threshold, value);
}
}
return null;
}
private StateChecker getStateChecker(ApiUsageStateValue stateValue) {
if (ApiUsageStateValue.ENABLED.equals(stateValue)) {
return (t, wt, v) -> true;
} else if (ApiUsageStateValue.WARNING.equals(stateValue)) {
return (t, wt, v) -> v < t && v >= wt;
} else {
return (t, wt, v) -> v >= t;
}
}
private interface StateChecker {
boolean check(long threshold, long warnThreshold, long value);
}
private void checkStartOfNextCycle() {
updateLock.lock();
try {
long now = System.currentTimeMillis();
myTenantStates.values().forEach(state -> {
if ((state.getNextCycleTs() < now) && (now - state.getNextCycleTs() < TimeUnit.HOURS.toMillis(1))) {
TenantId tenantId = state.getTenantId();
state.setCycles(state.getNextCycleTs(), SchedulerUtils.getStartOfNextNextMonth());
saveNewCounts(state, Arrays.asList(ApiUsageRecordKey.values()));
updateTenantState(state, tenantProfileCache.get(tenantId));
}
});
} finally {
updateLock.unlock();
}
}
private void saveNewCounts(TenantApiUsageState state, List<ApiUsageRecordKey> keys) {
List<TsKvEntry> counts = keys.stream()
.map(key -> new BasicTsKvEntry(state.getCurrentCycleTs(), new LongDataEntry(key.getApiCountKey(), 0L)))
.collect(Collectors.toList());
tsWsService.saveAndNotifyInternal(state.getTenantId(), state.getApiUsageState().getId(), counts, VOID_CALLBACK);
}
private TenantApiUsageState getOrFetchState(TenantId tenantId) {
TenantApiUsageState tenantState = myTenantStates.get(tenantId);
if (tenantState == null) {
ApiUsageState dbStateEntity = apiUsageStateService.findTenantApiUsageState(tenantId);
if (dbStateEntity == null) {
try {
dbStateEntity = apiUsageStateService.createDefaultApiUsageState(tenantId);
} catch (Exception e) {
dbStateEntity = apiUsageStateService.findTenantApiUsageState(tenantId);
}
}
TenantProfile tenantProfile = tenantProfileCache.get(tenantId);
tenantState = new TenantApiUsageState(tenantProfile, dbStateEntity);
List<ApiUsageRecordKey> newCounts = new ArrayList<>();
try {
List<TsKvEntry> dbValues = tsService.findAllLatest(tenantId, dbStateEntity.getId()).get();
for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) {
boolean cycleEntryFound = false;
boolean hourlyEntryFound = false;
for (TsKvEntry tsKvEntry : dbValues) {
if (tsKvEntry.getKey().equals(key.getApiCountKey())) {
cycleEntryFound = true;
boolean oldCount = tsKvEntry.getTs() == tenantState.getCurrentCycleTs();
tenantState.put(key, oldCount ? tsKvEntry.getLongValue().get() : 0L);
if (!oldCount) {
newCounts.add(key);
}
} else if (tsKvEntry.getKey().equals(key.getApiCountKey() + HOURLY)) {
hourlyEntryFound = true;
tenantState.putHourly(key, tsKvEntry.getTs() == tenantState.getCurrentHourTs() ? tsKvEntry.getLongValue().get() : 0L);
}
if (cycleEntryFound && hourlyEntryFound) {
break;
}
}
}
log.debug("[{}] Initialized state: {}", tenantId, dbStateEntity);
myTenantStates.put(tenantId, tenantState);
saveNewCounts(tenantState, newCounts);
} catch (InterruptedException | ExecutionException e) {
log.warn("[{}] Failed to fetch api usage state from db.", tenantId, e);
}
}
return tenantState;
}
private void initStatesFromDataBase() {
try {
log.info("Initializing tenant states.");
PageDataIterable<Tenant> tenantIterator = new PageDataIterable<>(tenantService::findTenants, 1024);
for (Tenant tenant : tenantIterator) {
if (!myTenantStates.containsKey(tenant.getId()) && partitionService.resolve(ServiceType.TB_CORE, tenant.getId(), tenant.getId()).isMyPartition()) {
log.debug("[{}] Initializing tenant state.", tenant.getId());
updateLock.lock();
try {
updateTenantState(getOrFetchState(tenant.getId()), tenantProfileCache.get(tenant.getTenantProfileId()));
log.debug("[{}] Initialized tenant state.", tenant.getId());
} catch (Exception e) {
log.warn("[{}] Failed to initialize tenant API state", tenant.getId(), e);
} finally {
updateLock.unlock();
}
}
}
log.info("Initialized tenant states.");
} catch (Exception e) {
log.warn("Unknown failure", e);
}
}
@PreDestroy
private void destroy() {
if (mailExecutor != null) {
mailExecutor.shutdownNow();
}
}
}

38
application/src/main/java/org/thingsboard/server/service/apiusage/TbApiUsageStateService.java

@ -0,0 +1,38 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.apiusage;
import org.springframework.context.ApplicationListener;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.PartitionChangeEvent;
public interface TbApiUsageStateService extends ApplicationListener<PartitionChangeEvent> {
void process(TbProtoQueueMsg<ToUsageStatsServiceMsg> msg, TbCallback callback);
ApiUsageState getApiUsageState(TenantId tenantId);
void onTenantProfileUpdate(TenantProfileId tenantProfileId);
void onTenantUpdate(TenantId tenantId);
void onApiUsageStateUpdate(TenantId tenantId);
}

196
application/src/main/java/org/thingsboard/server/service/apiusage/TenantApiUsageState.java

@ -0,0 +1,196 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.apiusage;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.util.Pair;
import org.thingsboard.server.common.data.ApiFeature;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.ApiUsageStateValue;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.msg.tools.SchedulerUtils;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class TenantApiUsageState {
private final Map<ApiUsageRecordKey, Long> currentCycleValues = new ConcurrentHashMap<>();
private final Map<ApiUsageRecordKey, Long> currentHourValues = new ConcurrentHashMap<>();
@Getter
@Setter
private TenantProfileId tenantProfileId;
@Getter
@Setter
private TenantProfileData tenantProfileData;
@Getter
private final ApiUsageState apiUsageState;
@Getter
private volatile long currentCycleTs;
@Getter
private volatile long nextCycleTs;
@Getter
private volatile long currentHourTs;
public TenantApiUsageState(TenantProfile tenantProfile, ApiUsageState apiUsageState) {
this.tenantProfileId = tenantProfile.getId();
this.tenantProfileData = tenantProfile.getProfileData();
this.apiUsageState = apiUsageState;
this.currentCycleTs = SchedulerUtils.getStartOfCurrentMonth();
this.nextCycleTs = SchedulerUtils.getStartOfNextMonth();
this.currentHourTs = SchedulerUtils.getStartOfCurrentHour();
}
public void put(ApiUsageRecordKey key, Long value) {
currentCycleValues.put(key, value);
}
public void putHourly(ApiUsageRecordKey key, Long value) {
currentHourValues.put(key, value);
}
public long add(ApiUsageRecordKey key, long value) {
long result = currentCycleValues.getOrDefault(key, 0L) + value;
currentCycleValues.put(key, result);
return result;
}
public long get(ApiUsageRecordKey key) {
return currentCycleValues.getOrDefault(key, 0L);
}
public long addToHourly(ApiUsageRecordKey key, long value) {
long result = currentHourValues.getOrDefault(key, 0L) + value;
currentHourValues.put(key, result);
return result;
}
public void setHour(long currentHourTs) {
this.currentHourTs = currentHourTs;
for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) {
currentHourValues.put(key, 0L);
}
}
public void setCycles(long currentCycleTs, long nextCycleTs) {
this.currentCycleTs = currentCycleTs;
this.nextCycleTs = nextCycleTs;
for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) {
currentCycleValues.put(key, 0L);
}
}
public long getProfileThreshold(ApiUsageRecordKey key) {
return tenantProfileData.getConfiguration().getProfileThreshold(key);
}
public long getProfileWarnThreshold(ApiUsageRecordKey key) {
return tenantProfileData.getConfiguration().getWarnThreshold(key);
}
public TenantId getTenantId() {
return apiUsageState.getTenantId();
}
public ApiUsageStateValue getFeatureValue(ApiFeature feature) {
switch (feature) {
case TRANSPORT:
return apiUsageState.getTransportState();
case RE:
return apiUsageState.getReExecState();
case DB:
return apiUsageState.getDbStorageState();
case JS:
return apiUsageState.getJsExecState();
case EMAIL:
return apiUsageState.getEmailExecState();
case SMS:
return apiUsageState.getSmsExecState();
default:
return ApiUsageStateValue.ENABLED;
}
}
public boolean setFeatureValue(ApiFeature feature, ApiUsageStateValue value) {
ApiUsageStateValue currentValue = getFeatureValue(feature);
switch (feature) {
case TRANSPORT:
apiUsageState.setTransportState(value);
break;
case RE:
apiUsageState.setReExecState(value);
break;
case DB:
apiUsageState.setDbStorageState(value);
break;
case JS:
apiUsageState.setJsExecState(value);
break;
case EMAIL:
apiUsageState.setEmailExecState(value);
break;
case SMS:
apiUsageState.setSmsExecState(value);
break;
}
return !currentValue.equals(value);
}
public Map<ApiFeature, ApiUsageStateValue> checkStateUpdatedDueToThresholds() {
return checkStateUpdatedDueToThreshold(new HashSet<>(Arrays.asList(ApiFeature.values())));
}
public Map<ApiFeature, ApiUsageStateValue> checkStateUpdatedDueToThreshold(Set<ApiFeature> features) {
Map<ApiFeature, ApiUsageStateValue> result = new HashMap<>();
for (ApiFeature feature : features) {
Pair<ApiFeature, ApiUsageStateValue> tmp = checkStateUpdatedDueToThreshold(feature);
if (tmp != null) {
result.put(tmp.getFirst(), tmp.getSecond());
}
}
return result;
}
public Pair<ApiFeature, ApiUsageStateValue> checkStateUpdatedDueToThreshold(ApiFeature feature) {
ApiUsageStateValue featureValue = ApiUsageStateValue.ENABLED;
for (ApiUsageRecordKey recordKey : ApiUsageRecordKey.getKeys(feature)) {
long value = get(recordKey);
long threshold = getProfileThreshold(recordKey);
long warnThreshold = getProfileWarnThreshold(recordKey);
ApiUsageStateValue tmpValue;
if (threshold == 0 || value < warnThreshold) {
tmpValue = ApiUsageStateValue.ENABLED;
} else if (value < threshold) {
tmpValue = ApiUsageStateValue.WARNING;
} else {
tmpValue = ApiUsageStateValue.DISABLED;
}
featureValue = ApiUsageStateValue.toMoreRestricted(featureValue, tmpValue);
}
return setFeatureValue(feature, featureValue) ? Pair.of(feature, featureValue) : null;
}
}

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

@ -0,0 +1,268 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.device;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceDao;
import org.thingsboard.server.dao.device.DeviceProfileDao;
import org.thingsboard.server.dao.device.DeviceProvisionService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.device.provision.ProvisionFailedException;
import org.thingsboard.server.dao.device.provision.ProvisionRequest;
import org.thingsboard.server.dao.device.provision.ProvisionResponse;
import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus;
import org.thingsboard.server.dao.util.mapping.JacksonUtil;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg;
import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueProducer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.state.DeviceStateService;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.locks.ReentrantLock;
@Service
@Slf4j
@TbCoreComponent
public class DeviceProvisionServiceImpl implements DeviceProvisionService {
protected TbQueueProducer<TbProtoQueueMsg<ToRuleEngineMsg>> ruleEngineMsgProducer;
private static final String DEVICE_PROVISION_STATE = "provisionState";
private static final String PROVISIONED_STATE = "provisioned";
private final ReentrantLock deviceCreationLock = new ReentrantLock();
@Autowired
DeviceDao deviceDao;
@Autowired
DeviceProfileDao deviceProfileDao;
@Autowired
DeviceService deviceService;
@Autowired
DeviceCredentialsService deviceCredentialsService;
@Autowired
AttributesService attributesService;
@Autowired
DeviceStateService deviceStateService;
@Autowired
AuditLogService auditLogService;
@Autowired
PartitionService partitionService;
public DeviceProvisionServiceImpl(TbQueueProducerProvider producerProvider) {
ruleEngineMsgProducer = producerProvider.getRuleEngineMsgProducer();
}
@Override
public ProvisionResponse provisionDevice(ProvisionRequest provisionRequest) {
String provisionRequestKey = provisionRequest.getCredentials().getProvisionDeviceKey();
String provisionRequestSecret = provisionRequest.getCredentials().getProvisionDeviceSecret();
if (!StringUtils.isEmpty(provisionRequest.getDeviceName())) {
provisionRequest.setDeviceName(provisionRequest.getDeviceName().trim());
if (StringUtils.isEmpty(provisionRequest.getDeviceName())) {
log.warn("Provision request contains empty device name!");
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
}
}
if (StringUtils.isEmpty(provisionRequestKey) || StringUtils.isEmpty(provisionRequestSecret)) {
throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name());
}
DeviceProfile targetProfile = deviceProfileDao.findByProvisionDeviceKey(provisionRequestKey);
if (targetProfile == null || targetProfile.getProfileData().getProvisionConfiguration() == null ||
targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret() == null) {
throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name());
}
Device targetDevice = deviceDao.findDeviceByTenantIdAndName(targetProfile.getTenantId().getId(), provisionRequest.getDeviceName()).orElse(null);
switch (targetProfile.getProvisionType()) {
case ALLOW_CREATE_NEW_DEVICES:
if (targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret().equals(provisionRequestSecret)) {
if (targetDevice != null) {
log.warn("[{}] The device is present and could not be provisioned once more!", targetDevice.getName());
notify(targetDevice, provisionRequest, DataConstants.PROVISION_FAILURE, false);
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
} else {
return createDevice(provisionRequest, targetProfile);
}
}
break;
case CHECK_PRE_PROVISIONED_DEVICES:
if (targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret().equals(provisionRequestSecret)) {
if (targetDevice != null && targetDevice.getDeviceProfileId().equals(targetProfile.getId())) {
return processProvision(targetDevice, provisionRequest);
} else {
log.warn("[{}] Failed to find pre provisioned device!", provisionRequest.getDeviceName());
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
}
}
break;
}
throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name());
}
private ProvisionResponse processProvision(Device device, ProvisionRequest provisionRequest) {
try {
Optional<AttributeKvEntry> provisionState = attributesService.find(device.getTenantId(), device.getId(),
DataConstants.SERVER_SCOPE, DEVICE_PROVISION_STATE).get();
if (provisionState != null && provisionState.isPresent() && !provisionState.get().getValueAsString().equals(PROVISIONED_STATE)) {
notify(device, provisionRequest, DataConstants.PROVISION_FAILURE, false);
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
} else {
saveProvisionStateAttribute(device).get();
notify(device, provisionRequest, DataConstants.PROVISION_SUCCESS, true);
}
} catch (InterruptedException | ExecutionException e) {
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
}
return new ProvisionResponse(deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), device.getId()), ProvisionResponseStatus.SUCCESS);
}
private ProvisionResponse createDevice(ProvisionRequest provisionRequest, DeviceProfile profile) {
deviceCreationLock.lock();
try {
return processCreateDevice(provisionRequest, profile);
} finally {
deviceCreationLock.unlock();
}
}
private void notify(Device device, ProvisionRequest provisionRequest, String type, boolean success) {
pushProvisionEventToRuleEngine(provisionRequest, device, type);
logAction(device.getTenantId(), device.getCustomerId(), device, success, provisionRequest);
}
private ProvisionResponse processCreateDevice(ProvisionRequest provisionRequest, DeviceProfile profile) {
Device device = deviceService.findDeviceByTenantIdAndName(profile.getTenantId(), provisionRequest.getDeviceName());
try {
if (device == null) {
if (StringUtils.isEmpty(provisionRequest.getDeviceName())) {
String newDeviceName = RandomStringUtils.randomAlphanumeric(20);
log.info("Device name not found in provision request. Generated name is: {}", newDeviceName);
provisionRequest.setDeviceName(newDeviceName);
}
Device savedDevice = deviceService.saveDevice(provisionRequest, profile);
deviceStateService.onDeviceAdded(savedDevice);
saveProvisionStateAttribute(savedDevice).get();
pushDeviceCreatedEventToRuleEngine(savedDevice);
notify(savedDevice, provisionRequest, DataConstants.PROVISION_SUCCESS, true);
return new ProvisionResponse(getDeviceCredentials(savedDevice), ProvisionResponseStatus.SUCCESS);
} else {
log.warn("[{}] The device is already provisioned!", device.getName());
notify(device, provisionRequest, DataConstants.PROVISION_FAILURE, false);
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
}
} catch (InterruptedException | ExecutionException e) {
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
}
}
private ListenableFuture<List<Void>> saveProvisionStateAttribute(Device device) {
return attributesService.save(device.getTenantId(), device.getId(), DataConstants.SERVER_SCOPE,
Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry(DEVICE_PROVISION_STATE, PROVISIONED_STATE),
System.currentTimeMillis())));
}
private DeviceCredentials getDeviceCredentials(Device device) {
return deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), device.getId());
}
private void pushProvisionEventToRuleEngine(ProvisionRequest request, Device device, String type) {
try {
JsonNode entityNode = JacksonUtil.valueToTree(request);
TbMsg msg = TbMsg.newMsg(type, device.getId(), createTbMsgMetaData(device), JacksonUtil.toString(entityNode));
sendToRuleEngine(device.getTenantId(), msg, null);
} catch (IllegalArgumentException e) {
log.warn("[{}] Failed to push device action to rule engine: {}", device.getId(), type, e);
}
}
private void pushDeviceCreatedEventToRuleEngine(Device device) {
try {
ObjectNode entityNode = JacksonUtil.OBJECT_MAPPER.valueToTree(device);
TbMsg msg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, device.getId(), createTbMsgMetaData(device), JacksonUtil.OBJECT_MAPPER.writeValueAsString(entityNode));
sendToRuleEngine(device.getTenantId(), msg, null);
} catch (JsonProcessingException | IllegalArgumentException e) {
log.warn("[{}] Failed to push device action to rule engine: {}", device.getId(), DataConstants.ENTITY_CREATED, e);
}
}
protected void sendToRuleEngine(TenantId tenantId, TbMsg tbMsg, TbQueueCallback callback) {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tenantId, tbMsg.getOriginator());
TransportProtos.ToRuleEngineMsg msg = TransportProtos.ToRuleEngineMsg.newBuilder().setTbMsg(TbMsg.toByteString(tbMsg))
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()).build();
ruleEngineMsgProducer.send(tpi, new TbProtoQueueMsg<>(tbMsg.getId(), msg), callback);
}
private TbMsgMetaData createTbMsgMetaData(Device device) {
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("tenantId", device.getTenantId().toString());
return metaData;
}
private void logAction(TenantId tenantId, CustomerId customerId, Device device, boolean success, ProvisionRequest provisionRequest) {
ActionType actionType = success ? ActionType.PROVISION_SUCCESS : ActionType.PROVISION_FAILURE;
auditLogService.logEntityAction(tenantId, customerId, new UserId(UserId.NULL_UUID), device.getName(), device.getId(), device, actionType, null, provisionRequest);
}
}

215
application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java

@ -28,11 +28,21 @@ import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.TenantProfileData;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.device.profile.AlarmCondition;
import org.thingsboard.server.common.data.device.profile.AlarmRule;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration;
import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
@ -41,19 +51,30 @@ import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.query.BooleanFilterPredicate;
import org.thingsboard.server.common.data.query.DynamicValue;
import org.thingsboard.server.common.data.query.DynamicValueSourceType;
import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.EntityKeyValueType;
import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.query.KeyFilter;
import org.thingsboard.server.common.data.query.NumericFilterPredicate;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
@ -61,6 +82,8 @@ import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
import java.util.Arrays;
import java.util.Collections;
import java.util.TreeMap;
@Service
@Profile("install")
@ -95,12 +118,6 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
@Autowired
private CustomerService customerService;
@Autowired
private RelationService relationService;
@Autowired
private AssetService assetService;
@Autowired
private DeviceService deviceService;
@ -113,6 +130,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
@Autowired
private DeviceCredentialsService deviceCredentialsService;
@Autowired
private RuleChainService ruleChainService;
@Bean
protected BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
@ -127,13 +147,16 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
public void createDefaultTenantProfiles() throws Exception {
tenantProfileService.findOrCreateDefaultTenantProfile(TenantId.SYS_TENANT_ID);
TenantProfileData tenantProfileData = new TenantProfileData();
tenantProfileData.setConfiguration(new DefaultTenantProfileConfiguration());
TenantProfile isolatedTbCoreProfile = new TenantProfile();
isolatedTbCoreProfile.setDefault(false);
isolatedTbCoreProfile.setName("Isolated TB Core");
isolatedTbCoreProfile.setProfileData(new TenantProfileData());
isolatedTbCoreProfile.setDescription("Isolated TB Core tenant profile");
isolatedTbCoreProfile.setIsolatedTbCore(true);
isolatedTbCoreProfile.setIsolatedTbRuleEngine(false);
isolatedTbCoreProfile.setProfileData(tenantProfileData);
try {
tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, isolatedTbCoreProfile);
} catch (DataValidationException e) {
@ -143,10 +166,11 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
TenantProfile isolatedTbRuleEngineProfile = new TenantProfile();
isolatedTbRuleEngineProfile.setDefault(false);
isolatedTbRuleEngineProfile.setName("Isolated TB Rule Engine");
isolatedTbRuleEngineProfile.setProfileData(new TenantProfileData());
isolatedTbRuleEngineProfile.setDescription("Isolated TB Rule Engine tenant profile");
isolatedTbRuleEngineProfile.setIsolatedTbCore(false);
isolatedTbRuleEngineProfile.setIsolatedTbRuleEngine(true);
isolatedTbRuleEngineProfile.setProfileData(tenantProfileData);
try {
tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, isolatedTbRuleEngineProfile);
} catch (DataValidationException e) {
@ -156,10 +180,11 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
TenantProfile isolatedTbCoreAndTbRuleEngineProfile = new TenantProfile();
isolatedTbCoreAndTbRuleEngineProfile.setDefault(false);
isolatedTbCoreAndTbRuleEngineProfile.setName("Isolated TB Core and TB Rule Engine");
isolatedTbCoreAndTbRuleEngineProfile.setProfileData(new TenantProfileData());
isolatedTbCoreAndTbRuleEngineProfile.setDescription("Isolated TB Core and TB Rule Engine tenant profile");
isolatedTbCoreAndTbRuleEngineProfile.setIsolatedTbCore(true);
isolatedTbCoreAndTbRuleEngineProfile.setIsolatedTbRuleEngine(true);
isolatedTbCoreAndTbRuleEngineProfile.setProfileData(tenantProfileData);
try {
tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, isolatedTbCoreAndTbRuleEngineProfile);
} catch (DataValidationException e) {
@ -173,6 +198,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
generalSettings.setKey("general");
ObjectNode node = objectMapper.createObjectNode();
node.put("baseUrl", "http://localhost:8080");
node.put("prohibitDifferentUrl", true);
generalSettings.setJsonValue(node);
adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, generalSettings);
@ -193,6 +219,11 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, mailSettings);
}
@Override
public void createOAuth2Templates() throws Exception {
installScripts.createOAuth2Templates();
}
@Override
public void loadDemoData() throws Exception {
Tenant demoTenant = new Tenant();
@ -233,35 +264,149 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
createDevice(demoTenant.getId(), null, defaultDeviceProfile.getId(), "Raspberry Pi Demo Device", "RASPBERRY_PI_DEMO_TOKEN", "Demo device that is used in " +
"Raspberry Pi GPIO control sample application");
Asset thermostatAlarms = new Asset();
thermostatAlarms.setTenantId(demoTenant.getId());
thermostatAlarms.setName("Thermostat Alarms");
thermostatAlarms.setType("AlarmPropagationAsset");
thermostatAlarms = assetService.saveAsset(thermostatAlarms);
DeviceProfile thermostatDeviceProfile = this.deviceProfileService.findOrCreateDeviceProfile(demoTenant.getId(), "thermostat");
DeviceId t1Id = createDevice(demoTenant.getId(), null, thermostatDeviceProfile.getId(), "Thermostat T1", "T1_TEST_TOKEN", "Demo device for Thermostats dashboard").getId();
DeviceId t2Id = createDevice(demoTenant.getId(), null, thermostatDeviceProfile.getId(), "Thermostat T2", "T2_TEST_TOKEN", "Demo device for Thermostats dashboard").getId();
relationService.saveRelation(thermostatAlarms.getTenantId(), new EntityRelation(thermostatAlarms.getId(), t1Id, "ToAlarmPropagationAsset"));
relationService.saveRelation(thermostatAlarms.getTenantId(), new EntityRelation(thermostatAlarms.getId(), t2Id, "ToAlarmPropagationAsset"));
DeviceProfile thermostatDeviceProfile = new DeviceProfile();
thermostatDeviceProfile.setTenantId(demoTenant.getId());
thermostatDeviceProfile.setDefault(false);
thermostatDeviceProfile.setName("thermostat");
thermostatDeviceProfile.setType(DeviceProfileType.DEFAULT);
thermostatDeviceProfile.setTransportType(DeviceTransportType.DEFAULT);
thermostatDeviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED);
thermostatDeviceProfile.setDescription("Thermostat device profile");
thermostatDeviceProfile.setDefaultRuleChainId(ruleChainService.findTenantRuleChainsByType(
demoTenant.getId(), RuleChainType.CORE, new PageLink(1, 0, "Thermostat")).getData().get(0).getId());
DeviceProfileData deviceProfileData = new DeviceProfileData();
DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration();
DefaultDeviceProfileTransportConfiguration transportConfiguration = new DefaultDeviceProfileTransportConfiguration();
DisabledDeviceProfileProvisionConfiguration provisionConfiguration = new DisabledDeviceProfileProvisionConfiguration(null);
deviceProfileData.setConfiguration(configuration);
deviceProfileData.setTransportConfiguration(transportConfiguration);
deviceProfileData.setProvisionConfiguration(provisionConfiguration);
thermostatDeviceProfile.setProfileData(deviceProfileData);
DeviceProfileAlarm highTemperature = new DeviceProfileAlarm();
highTemperature.setId("highTemperatureAlarmID");
highTemperature.setAlarmType("High Temperature");
AlarmRule temperatureRule = new AlarmRule();
AlarmCondition temperatureCondition = new AlarmCondition();
temperatureCondition.setSpec(new SimpleAlarmConditionSpec());
KeyFilter temperatureAlarmFlagAttributeFilter = new KeyFilter();
temperatureAlarmFlagAttributeFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperatureAlarmFlag"));
temperatureAlarmFlagAttributeFilter.setValueType(EntityKeyValueType.BOOLEAN);
BooleanFilterPredicate temperatureAlarmFlagAttributePredicate = new BooleanFilterPredicate();
temperatureAlarmFlagAttributePredicate.setOperation(BooleanFilterPredicate.BooleanOperation.EQUAL);
temperatureAlarmFlagAttributePredicate.setValue(new FilterPredicateValue<>(Boolean.TRUE));
temperatureAlarmFlagAttributeFilter.setPredicate(temperatureAlarmFlagAttributePredicate);
KeyFilter temperatureTimeseriesFilter = new KeyFilter();
temperatureTimeseriesFilter.setKey(new EntityKey(EntityKeyType.TIME_SERIES, "temperature"));
temperatureTimeseriesFilter.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate temperatureTimeseriesFilterPredicate = new NumericFilterPredicate();
temperatureTimeseriesFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
FilterPredicateValue<Double> temperatureTimeseriesPredicateValue =
new FilterPredicateValue<>(25.0, null,
new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "temperatureAlarmThreshold"));
temperatureTimeseriesFilterPredicate.setValue(temperatureTimeseriesPredicateValue);
temperatureTimeseriesFilter.setPredicate(temperatureTimeseriesFilterPredicate);
temperatureCondition.setCondition(Arrays.asList(temperatureAlarmFlagAttributeFilter, temperatureTimeseriesFilter));
temperatureRule.setAlarmDetails("Current temperature = ${temperature}");
temperatureRule.setCondition(temperatureCondition);
highTemperature.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.MAJOR, temperatureRule)));
AlarmRule clearTemperatureRule = new AlarmRule();
AlarmCondition clearTemperatureCondition = new AlarmCondition();
clearTemperatureCondition.setSpec(new SimpleAlarmConditionSpec());
KeyFilter clearTemperatureTimeseriesFilter = new KeyFilter();
clearTemperatureTimeseriesFilter.setKey(new EntityKey(EntityKeyType.TIME_SERIES, "temperature"));
clearTemperatureTimeseriesFilter.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate clearTemperatureTimeseriesFilterPredicate = new NumericFilterPredicate();
clearTemperatureTimeseriesFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.LESS_OR_EQUAL);
FilterPredicateValue<Double> clearTemperatureTimeseriesPredicateValue =
new FilterPredicateValue<>(25.0, null,
new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "temperatureAlarmThreshold"));
clearTemperatureTimeseriesFilterPredicate.setValue(clearTemperatureTimeseriesPredicateValue);
clearTemperatureTimeseriesFilter.setPredicate(clearTemperatureTimeseriesFilterPredicate);
clearTemperatureCondition.setCondition(Collections.singletonList(clearTemperatureTimeseriesFilter));
clearTemperatureRule.setCondition(clearTemperatureCondition);
clearTemperatureRule.setAlarmDetails("Current temperature = ${temperature}");
highTemperature.setClearRule(clearTemperatureRule);
DeviceProfileAlarm lowHumidity = new DeviceProfileAlarm();
lowHumidity.setId("lowHumidityAlarmID");
lowHumidity.setAlarmType("Low Humidity");
AlarmRule humidityRule = new AlarmRule();
AlarmCondition humidityCondition = new AlarmCondition();
humidityCondition.setSpec(new SimpleAlarmConditionSpec());
KeyFilter humidityAlarmFlagAttributeFilter = new KeyFilter();
humidityAlarmFlagAttributeFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "humidityAlarmFlag"));
humidityAlarmFlagAttributeFilter.setValueType(EntityKeyValueType.BOOLEAN);
BooleanFilterPredicate humidityAlarmFlagAttributePredicate = new BooleanFilterPredicate();
humidityAlarmFlagAttributePredicate.setOperation(BooleanFilterPredicate.BooleanOperation.EQUAL);
humidityAlarmFlagAttributePredicate.setValue(new FilterPredicateValue<>(Boolean.TRUE));
humidityAlarmFlagAttributeFilter.setPredicate(humidityAlarmFlagAttributePredicate);
KeyFilter humidityTimeseriesFilter = new KeyFilter();
humidityTimeseriesFilter.setKey(new EntityKey(EntityKeyType.TIME_SERIES, "humidity"));
humidityTimeseriesFilter.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate humidityTimeseriesFilterPredicate = new NumericFilterPredicate();
humidityTimeseriesFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.LESS);
FilterPredicateValue<Double> humidityTimeseriesPredicateValue =
new FilterPredicateValue<>(60.0, null,
new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "humidityAlarmThreshold"));
humidityTimeseriesFilterPredicate.setValue(humidityTimeseriesPredicateValue);
humidityTimeseriesFilter.setPredicate(humidityTimeseriesFilterPredicate);
humidityCondition.setCondition(Arrays.asList(humidityAlarmFlagAttributeFilter, humidityTimeseriesFilter));
humidityRule.setCondition(humidityCondition);
humidityRule.setAlarmDetails("Current humidity = ${humidity}");
lowHumidity.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.MINOR, humidityRule)));
AlarmRule clearHumidityRule = new AlarmRule();
AlarmCondition clearHumidityCondition = new AlarmCondition();
clearHumidityCondition.setSpec(new SimpleAlarmConditionSpec());
KeyFilter clearHumidityTimeseriesFilter = new KeyFilter();
clearHumidityTimeseriesFilter.setKey(new EntityKey(EntityKeyType.TIME_SERIES, "humidity"));
clearHumidityTimeseriesFilter.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate clearHumidityTimeseriesFilterPredicate = new NumericFilterPredicate();
clearHumidityTimeseriesFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER_OR_EQUAL);
FilterPredicateValue<Double> clearHumidityTimeseriesPredicateValue =
new FilterPredicateValue<>(60.0, null,
new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "humidityAlarmThreshold"));
clearHumidityTimeseriesFilterPredicate.setValue(clearHumidityTimeseriesPredicateValue);
clearHumidityTimeseriesFilter.setPredicate(clearHumidityTimeseriesFilterPredicate);
clearHumidityCondition.setCondition(Collections.singletonList(clearHumidityTimeseriesFilter));
clearHumidityRule.setCondition(clearHumidityCondition);
clearHumidityRule.setAlarmDetails("Current humidity = ${humidity}");
lowHumidity.setClearRule(clearHumidityRule);
deviceProfileData.setAlarms(Arrays.asList(highTemperature, lowHumidity));
DeviceProfile savedThermostatDeviceProfile = deviceProfileService.saveDeviceProfile(thermostatDeviceProfile);
DeviceId t1Id = createDevice(demoTenant.getId(), null, savedThermostatDeviceProfile.getId(), "Thermostat T1", "T1_TEST_TOKEN", "Demo device for Thermostats dashboard").getId();
DeviceId t2Id = createDevice(demoTenant.getId(), null, savedThermostatDeviceProfile.getId(), "Thermostat T2", "T2_TEST_TOKEN", "Demo device for Thermostats dashboard").getId();
attributesService.save(demoTenant.getId(), t1Id, DataConstants.SERVER_SCOPE,
Arrays.asList(new BaseAttributeKvEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 37.3948)),
new BaseAttributeKvEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", -122.1503)),
new BaseAttributeKvEntry(System.currentTimeMillis(), new BooleanDataEntry("alarmTemperature", true)),
new BaseAttributeKvEntry(System.currentTimeMillis(), new BooleanDataEntry("alarmHumidity", true)),
new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("thresholdTemperature", (long) 20)),
new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("thresholdHumidity", (long) 50))));
new BaseAttributeKvEntry(System.currentTimeMillis(), new BooleanDataEntry("temperatureAlarmFlag", true)),
new BaseAttributeKvEntry(System.currentTimeMillis(), new BooleanDataEntry("humidityAlarmFlag", true)),
new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("temperatureAlarmThreshold", (long) 20)),
new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("humidityAlarmThreshold", (long) 50))));
attributesService.save(demoTenant.getId(), t2Id, DataConstants.SERVER_SCOPE,
Arrays.asList(new BaseAttributeKvEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 37.493801)),
new BaseAttributeKvEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", -121.948769)),
new BaseAttributeKvEntry(System.currentTimeMillis(), new BooleanDataEntry("alarmTemperature", true)),
new BaseAttributeKvEntry(System.currentTimeMillis(), new BooleanDataEntry("alarmHumidity", true)),
new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("thresholdTemperature", (long) 25)),
new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("thresholdHumidity", (long) 30))));
new BaseAttributeKvEntry(System.currentTimeMillis(), new BooleanDataEntry("temperatureAlarmFlag", true)),
new BaseAttributeKvEntry(System.currentTimeMillis(), new BooleanDataEntry("humidityAlarmFlag", true)),
new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("temperatureAlarmThreshold", (long) 25)),
new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("humidityAlarmThreshold", (long) 30))));
installScripts.loadDashboards(demoTenant.getId(), null);
}

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

@ -26,11 +26,13 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
@ -40,6 +42,7 @@ import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import static org.thingsboard.server.service.install.DatabaseHelper.objectMapper;
@ -61,6 +64,7 @@ public class InstallScripts {
public static final String DEMO_DIR = "demo";
public static final String RULE_CHAINS_DIR = "rule_chains";
public static final String WIDGET_BUNDLES_DIR = "widget_bundles";
public static final String OAUTH2_CONFIG_TEMPLATES_DIR = "oauth2_config_templates";
public static final String DASHBOARDS_DIR = "dashboards";
public static final String JSON_EXT = ".json";
@ -80,6 +84,9 @@ public class InstallScripts {
@Autowired
private WidgetsBundleService widgetsBundleService;
@Autowired
private OAuth2ConfigTemplateService oAuth2TemplateService;
public Path getTenantRuleChainsDir() {
return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, RULE_CHAINS_DIR);
}
@ -146,12 +153,6 @@ public class InstallScripts {
return ruleChain;
}
public void createDefaultEdgeRuleChains(TenantId tenantId) {
Path tenantChainsDir = getTenantRuleChainsDir();
loadRuleChainFromFile(tenantId, tenantChainsDir.resolve("edge_root_rule_chain.json"));
}
public void loadSystemWidgets() throws Exception {
Path widgetBundlesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_BUNDLES_DIR);
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(widgetBundlesDir, path -> path.toString().endsWith(JSON_EXT))) {
@ -206,50 +207,42 @@ public class InstallScripts {
}
}
public void loadDemoRuleChains(TenantId tenantId) throws Exception {
Path ruleChainsDir = Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, RULE_CHAINS_DIR);
try {
JsonNode ruleChainJson = objectMapper.readTree(ruleChainsDir.resolve("thermostat_alarms.json").toFile());
RuleChain ruleChain = objectMapper.treeToValue(ruleChainJson.get("ruleChain"), RuleChain.class);
RuleChainMetaData ruleChainMetaData = objectMapper.treeToValue(ruleChainJson.get("metadata"), RuleChainMetaData.class);
ruleChain.setTenantId(tenantId);
ruleChain = ruleChainService.saveRuleChain(ruleChain);
ruleChainMetaData.setRuleChainId(ruleChain.getId());
ruleChainService.saveRuleChainMetaData(new TenantId(EntityId.NULL_UUID), ruleChainMetaData);
JsonNode rootChainJson = objectMapper.readTree(ruleChainsDir.resolve("root_rule_chain.json").toFile());
RuleChain rootChain = objectMapper.treeToValue(rootChainJson.get("ruleChain"), RuleChain.class);
RuleChainMetaData rootChainMetaData = objectMapper.treeToValue(rootChainJson.get("metadata"), RuleChainMetaData.class);
RuleChainId thermostatsRuleChainId = ruleChain.getId();
rootChainMetaData.getRuleChainConnections().forEach(connection -> connection.setTargetRuleChainId(thermostatsRuleChainId));
rootChain.setTenantId(tenantId);
rootChain = ruleChainService.saveRuleChain(rootChain);
rootChainMetaData.setRuleChainId(rootChain.getId());
ruleChainService.saveRuleChainMetaData(new TenantId(EntityId.NULL_UUID), rootChainMetaData);
loadRuleChainFromFile(tenantId, ruleChainsDir.resolve("edge_root_rule_chain.json"));
createDefaultRuleChains(tenantId);
createDefaultRuleChain(tenantId, "Thermostat");
createDefaultEdgeRuleChains(tenantId);
} catch (Exception e) {
log.error("Unable to load dashboard from json", e);
throw new RuntimeException("Unable to load dashboard from json", e);
}
}
private void loadRuleChainFromFile(TenantId tenantId, Path ruleChainPath) {
try {
JsonNode ruleChainJson = objectMapper.readTree(ruleChainPath.toFile());
RuleChain ruleChain = objectMapper.treeToValue(ruleChainJson.get("ruleChain"), RuleChain.class);
RuleChainMetaData ruleChainMetaData = objectMapper.treeToValue(ruleChainJson.get("metadata"), RuleChainMetaData.class);
ruleChain.setTenantId(tenantId);
ruleChain = ruleChainService.saveRuleChain(ruleChain);
public void createDefaultEdgeRuleChains(TenantId tenantId) throws IOException {
Path tenantChainsDir = getTenantRuleChainsDir();
createRuleChainFromFile(tenantId, tenantChainsDir.resolve("edge_root_rule_chain.json"), null);
}
ruleChainMetaData.setRuleChainId(ruleChain.getId());
ruleChainService.saveRuleChainMetaData(new TenantId(EntityId.NULL_UUID), ruleChainMetaData);
} catch (Exception e) {
log.error("Unable to load rule chain from json: [{}]", ruleChainPath.toString());
throw new RuntimeException("Unable to load rule chain from json", e);
public void createOAuth2Templates() throws Exception {
Path oauth2ConfigTemplatesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, OAUTH2_CONFIG_TEMPLATES_DIR);
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(oauth2ConfigTemplatesDir, path -> path.toString().endsWith(JSON_EXT))) {
dirStream.forEach(
path -> {
try {
JsonNode oauth2ConfigTemplateJson = objectMapper.readTree(path.toFile());
OAuth2ClientRegistrationTemplate clientRegistrationTemplate = objectMapper.treeToValue(oauth2ConfigTemplateJson, OAuth2ClientRegistrationTemplate.class);
Optional<OAuth2ClientRegistrationTemplate> existingClientRegistrationTemplate =
oAuth2TemplateService.findClientRegistrationTemplateByProviderId(clientRegistrationTemplate.getProviderId());
if (existingClientRegistrationTemplate.isPresent()) {
clientRegistrationTemplate.setId(existingClientRegistrationTemplate.get().getId());
}
oAuth2TemplateService.saveClientRegistrationTemplate(clientRegistrationTemplate);
} catch (Exception e) {
log.error("Unable to load oauth2 config templates from json: [{}]", path.toString());
throw new RuntimeException("Unable to load oauth2 config templates from json", e);
}
}
);
}
}
}

26
application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java

@ -28,6 +28,7 @@ import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.usagerecord.ApiUsageStateService;
import org.thingsboard.server.service.install.sql.SqlDbHelper;
import java.nio.charset.Charset;
@ -96,6 +97,9 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
@Autowired
private DeviceProfileService deviceProfileService;
@Autowired
private ApiUsageStateService apiUsageStateService;
@Override
public void upgradeDatabase(String fromVersion) throws Exception {
@ -364,6 +368,24 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
} catch (Exception e) {
}
try {
conn.createStatement().execute("CREATE TABLE IF NOT EXISTS api_usage_state (" +
" id uuid NOT NULL CONSTRAINT usage_record_pkey PRIMARY KEY," +
" created_time bigint NOT NULL," +
" tenant_id uuid," +
" entity_type varchar(32)," +
" entity_id uuid," +
" transport varchar(32)," +
" db_storage varchar(32)," +
" re_exec varchar(32)," +
" js_exec varchar(32)," +
" email_exec varchar(32)," +
" sms_exec varchar(32)," +
" CONSTRAINT api_usage_state_unq_key UNIQUE (tenant_id, entity_id)\n" +
");");
} catch (Exception e) {
}
schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.1.1", "schema_update_before.sql");
loadSql(schemaUpdateFile, conn);
@ -379,6 +401,10 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
do {
pageData = tenantService.findTenants(pageLink);
for (Tenant tenant : pageData.getData()) {
try {
apiUsageStateService.createDefaultApiUsageState(tenant.getId());
} catch (Exception e) {
}
List<EntitySubtype> deviceTypes = deviceService.findDeviceTypesByTenantId(tenant.getId()).get();
try {
deviceProfileService.createDefaultDeviceProfile(tenant.getId());

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

@ -23,6 +23,8 @@ public interface SystemDataLoaderService {
void createAdminSettings() throws Exception;
void createOAuth2Templates() throws Exception;
void loadSystemWidgets() throws Exception;
void updateSystemWidgets() throws Exception;

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

@ -187,7 +187,7 @@ public class DefaultDataUpdateService implements DataUpdateService {
ruleChainService.saveRuleChainMetaData(tenant.getId(), md);
}
} catch (Exception e) {
log.error("Unable to update Tenant", e);
log.error("[{}] Unable to update Tenant: {}", tenant.getId(), tenant.getName(), e);
}
}
};

177
application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java

@ -20,8 +20,10 @@ import freemarker.template.Configuration;
import freemarker.template.Template;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.NestedRuntimeException;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
@ -29,12 +31,18 @@ import org.springframework.stereotype.Service;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.ApiFeature;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.ApiUsageStateMailMessage;
import org.thingsboard.server.common.data.ApiUsageStateValue;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import javax.annotation.PostConstruct;
import javax.mail.MessagingException;
@ -51,18 +59,28 @@ public class DefaultMailService implements MailService {
public static final String MAIL_PROP = "mail.";
public static final String TARGET_EMAIL = "targetEmail";
public static final String UTF_8 = "UTF-8";
@Autowired
private MessageSource messages;
public static final int _10K = 10000;
public static final int _1M = 1000000;
private final MessageSource messages;
private final Configuration freemarkerConfig;
private final AdminSettingsService adminSettingsService;
private final TbApiUsageClient apiUsageClient;
@Lazy
@Autowired
private Configuration freemarkerConfig;
private TbApiUsageStateService apiUsageStateService;
private JavaMailSenderImpl mailSender;
private String mailFrom;
@Autowired
private AdminSettingsService adminSettingsService;
public DefaultMailService(MessageSource messages, Configuration freemarkerConfig, AdminSettingsService adminSettingsService, TbApiUsageClient apiUsageClient) {
this.messages = messages;
this.freemarkerConfig = freemarkerConfig;
this.adminSettingsService = adminSettingsService;
this.apiUsageClient = apiUsageClient;
}
@PostConstruct
private void init() {
@ -141,7 +159,7 @@ public class DefaultMailService implements MailService {
}
@Override
public void sendEmail(String email, String subject, String message) throws ThingsboardException {
public void sendEmail(TenantId tenantId, String email, String subject, String message) throws ThingsboardException {
sendMail(mailSender, mailFrom, email, subject, message);
}
@ -216,20 +234,25 @@ public class DefaultMailService implements MailService {
}
@Override
public void send(String from, String to, String cc, String bcc, String subject, String body) throws MessagingException {
MimeMessage mailMsg = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mailMsg, "UTF-8");
helper.setFrom(StringUtils.isBlank(from) ? mailFrom : from);
helper.setTo(to.split("\\s*,\\s*"));
if (!StringUtils.isBlank(cc)) {
helper.setCc(cc.split("\\s*,\\s*"));
}
if (!StringUtils.isBlank(bcc)) {
helper.setBcc(bcc.split("\\s*,\\s*"));
public void send(TenantId tenantId, String from, String to, String cc, String bcc, String subject, String body) throws MessagingException {
if (apiUsageStateService.getApiUsageState(tenantId).isEmailSendEnabled()) {
MimeMessage mailMsg = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mailMsg, "UTF-8");
helper.setFrom(StringUtils.isBlank(from) ? mailFrom : from);
helper.setTo(to.split("\\s*,\\s*"));
if (!StringUtils.isBlank(cc)) {
helper.setCc(cc.split("\\s*,\\s*"));
}
if (!StringUtils.isBlank(bcc)) {
helper.setBcc(bcc.split("\\s*,\\s*"));
}
helper.setSubject(subject);
helper.setText(body);
mailSender.send(helper.getMimeMessage());
apiUsageClient.report(tenantId, ApiUsageRecordKey.EMAIL_EXEC_COUNT, 1);
} else {
throw new RuntimeException("Email sending is disabled due to API limits!");
}
helper.setSubject(subject);
helper.setText(body);
mailSender.send(helper.getMimeMessage());
}
@Override
@ -246,6 +269,122 @@ public class DefaultMailService implements MailService {
sendMail(mailSender, mailFrom, email, subject, message);
}
@Override
public void sendApiFeatureStateEmail(ApiFeature apiFeature, ApiUsageStateValue stateValue, String email, ApiUsageStateMailMessage msg) throws ThingsboardException {
String subject = messages.getMessage("api.usage.state", null, Locale.US);
Map<String, Object> model = new HashMap<>();
model.put("apiFeature", apiFeature.getLabel());
model.put(TARGET_EMAIL, email);
String message = null;
switch (stateValue) {
case ENABLED:
model.put("apiLabel", toEnabledValueLabel(apiFeature));
message = mergeTemplateIntoString("state.enabled.ftl", model);
break;
case WARNING:
model.put("apiValueLabel", toDisabledValueLabel(apiFeature) + " " + toWarningValueLabel(msg.getKey(), msg.getValue(), msg.getThreshold()));
message = mergeTemplateIntoString("state.warning.ftl", model);
break;
case DISABLED:
model.put("apiLimitValueLabel", toDisabledValueLabel(apiFeature) + " " + toDisabledValueLabel(msg.getKey(), msg.getThreshold()));
message = mergeTemplateIntoString("state.disabled.ftl", model);
break;
}
sendMail(mailSender, mailFrom, email, subject, message);
}
private String toEnabledValueLabel(ApiFeature apiFeature) {
switch (apiFeature) {
case DB:
return "save";
case TRANSPORT:
return "receive";
case JS:
return "invoke";
case RE:
return "process";
case EMAIL:
case SMS:
return "send";
default:
throw new RuntimeException("Not implemented!");
}
}
private String toDisabledValueLabel(ApiFeature apiFeature) {
switch (apiFeature) {
case DB:
return "saved";
case TRANSPORT:
return "received";
case JS:
return "invoked";
case RE:
return "processed";
case EMAIL:
case SMS:
return "sent";
default:
throw new RuntimeException("Not implemented!");
}
}
private String toWarningValueLabel(ApiUsageRecordKey key, long value, long threshold) {
String valueInM = getValueAsString(value);
String thresholdInM = getValueAsString(threshold);
switch (key) {
case STORAGE_DP_COUNT:
case TRANSPORT_DP_COUNT:
return valueInM + " out of " + thresholdInM + " allowed data points";
case TRANSPORT_MSG_COUNT:
return valueInM + " out of " + thresholdInM + " allowed messages";
case JS_EXEC_COUNT:
return valueInM + " out of " + thresholdInM + " allowed JavaScript functions";
case RE_EXEC_COUNT:
return valueInM + " out of " + thresholdInM + " allowed Rule Engine messages";
case EMAIL_EXEC_COUNT:
return valueInM + " out of " + thresholdInM + " allowed Email messages";
case SMS_EXEC_COUNT:
return valueInM + " out of " + thresholdInM + " allowed SMS messages";
default:
throw new RuntimeException("Not implemented!");
}
}
private String toDisabledValueLabel(ApiUsageRecordKey key, long value) {
switch (key) {
case STORAGE_DP_COUNT:
case TRANSPORT_DP_COUNT:
return getValueAsString(value) + " data points";
case TRANSPORT_MSG_COUNT:
return getValueAsString(value) + " messages";
case JS_EXEC_COUNT:
return "JavaScript functions " + getValueAsString(value) + " times";
case RE_EXEC_COUNT:
return getValueAsString(value) + " Rule Engine messages";
case EMAIL_EXEC_COUNT:
return getValueAsString(value) + " Email messages";
case SMS_EXEC_COUNT:
return getValueAsString(value) + " SMS messages";
default:
throw new RuntimeException("Not implemented!");
}
}
@NotNull
private String getValueAsString(long value) {
if (value > _1M && value % _1M < _10K) {
return value / _1M + "M";
} else if (value > _10K) {
return String.format("%.2fM", ((double) value) / 1000000);
} else {
return value + "";
}
}
private void sendMail(JavaMailSenderImpl mailSender,
String mailFrom, String email,
String subject, String message) throws ThingsboardException {

89
application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java

@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
@ -29,6 +30,8 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
@Service
@Slf4j
@ -40,6 +43,8 @@ public class DefaultTbDeviceProfileCache implements TbDeviceProfileCache {
private final ConcurrentMap<DeviceProfileId, DeviceProfile> deviceProfilesMap = new ConcurrentHashMap<>();
private final ConcurrentMap<DeviceId, DeviceProfileId> devicesMap = new ConcurrentHashMap<>();
private final ConcurrentMap<TenantId, ConcurrentMap<EntityId, Consumer<DeviceProfile>>> profileListeners = new ConcurrentHashMap<>();
private final ConcurrentMap<TenantId, ConcurrentMap<EntityId, BiConsumer<DeviceId, DeviceProfile>>> deviceProfileListeners = new ConcurrentHashMap<>();
public DefaultTbDeviceProfileCache(DeviceProfileService deviceProfileService, DeviceService deviceService) {
this.deviceProfileService = deviceProfileService;
@ -50,19 +55,21 @@ public class DefaultTbDeviceProfileCache implements TbDeviceProfileCache {
public DeviceProfile get(TenantId tenantId, DeviceProfileId deviceProfileId) {
DeviceProfile profile = deviceProfilesMap.get(deviceProfileId);
if (profile == null) {
profile = deviceProfilesMap.get(deviceProfileId);
if (profile == null) {
deviceProfileFetchLock.lock();
try {
deviceProfileFetchLock.lock();
try {
profile = deviceProfilesMap.get(deviceProfileId);
if (profile == null) {
profile = deviceProfileService.findDeviceProfileById(tenantId, deviceProfileId);
if (profile != null) {
deviceProfilesMap.put(deviceProfileId, profile);
log.debug("[{}] Fetch device profile into cache: {}", profile.getId(), profile);
}
} finally {
deviceProfileFetchLock.unlock();
}
} finally {
deviceProfileFetchLock.unlock();
}
}
log.trace("[{}] Found device profile in cache: {}", deviceProfileId, profile);
return profile;
}
@ -74,26 +81,82 @@ public class DefaultTbDeviceProfileCache implements TbDeviceProfileCache {
if (device != null) {
profileId = device.getDeviceProfileId();
devicesMap.put(deviceId, profileId);
} else {
return null;
}
}
return get(tenantId, profileId);
}
@Override
public void put(DeviceProfile profile) {
if (profile.getId() != null) {
deviceProfilesMap.put(profile.getId(), profile);
public void evict(TenantId tenantId, DeviceProfileId profileId) {
DeviceProfile oldProfile = deviceProfilesMap.remove(profileId);
log.debug("[{}] evict device profile from cache: {}", profileId, oldProfile);
DeviceProfile newProfile = get(tenantId, profileId);
if (newProfile != null) {
notifyProfileListeners(newProfile);
}
}
@Override
public void evict(DeviceProfileId profileId) {
deviceProfilesMap.remove(profileId);
public void evict(TenantId tenantId, DeviceId deviceId) {
DeviceProfileId old = devicesMap.remove(deviceId);
if (old != null) {
DeviceProfile newProfile = get(tenantId, deviceId);
if (newProfile == null || !old.equals(newProfile.getId())) {
notifyDeviceListeners(tenantId, deviceId, newProfile);
}
}
}
@Override
public void addListener(TenantId tenantId, EntityId listenerId,
Consumer<DeviceProfile> profileListener,
BiConsumer<DeviceId, DeviceProfile> deviceListener) {
if (profileListener != null) {
profileListeners.computeIfAbsent(tenantId, id -> new ConcurrentHashMap<>()).put(listenerId, profileListener);
}
if (deviceListener != null) {
deviceProfileListeners.computeIfAbsent(tenantId, id -> new ConcurrentHashMap<>()).put(listenerId, deviceListener);
}
}
@Override
public DeviceProfile find(DeviceProfileId deviceProfileId) {
return deviceProfileService.findDeviceProfileById(TenantId.SYS_TENANT_ID, deviceProfileId);
}
@Override
public DeviceProfile findOrCreateDeviceProfile(TenantId tenantId, String profileName) {
return deviceProfileService.findOrCreateDeviceProfile(tenantId, profileName);
}
@Override
public void evict(DeviceId deviceId) {
devicesMap.remove(deviceId);
public void removeListener(TenantId tenantId, EntityId listenerId) {
ConcurrentMap<EntityId, Consumer<DeviceProfile>> tenantListeners = profileListeners.get(tenantId);
if (tenantListeners != null) {
tenantListeners.remove(listenerId);
}
ConcurrentMap<EntityId, BiConsumer<DeviceId, DeviceProfile>> deviceListeners = deviceProfileListeners.get(tenantId);
if (deviceListeners != null) {
deviceListeners.remove(listenerId);
}
}
private void notifyProfileListeners(DeviceProfile profile) {
ConcurrentMap<EntityId, Consumer<DeviceProfile>> tenantListeners = profileListeners.get(profile.getTenantId());
if (tenantListeners != null) {
tenantListeners.forEach((id, listener) -> listener.accept(profile));
}
}
private void notifyDeviceListeners(TenantId tenantId, DeviceId deviceId, DeviceProfile profile) {
if (profile != null) {
ConcurrentMap<EntityId, BiConsumer<DeviceId, DeviceProfile>> tenantListeners = deviceProfileListeners.get(tenantId);
if (tenantListeners != null) {
tenantListeners.forEach((id, listener) -> listener.accept(deviceId, profile));
}
}
}
}

8
application/src/main/java/org/thingsboard/server/service/profile/TbDeviceProfileCache.java

@ -19,13 +19,15 @@ import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
public interface TbDeviceProfileCache extends RuleEngineDeviceProfileCache {
void put(DeviceProfile profile);
void evict(TenantId tenantId, DeviceProfileId id);
void evict(DeviceProfileId id);
void evict(TenantId tenantId, DeviceId id);
void evict(DeviceId id);
DeviceProfile find(DeviceProfileId deviceProfileId);
DeviceProfile findOrCreateDeviceProfile(TenantId tenantId, String deviceType);
}

97
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java

@ -21,8 +21,13 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.msg.ToDeviceActorNotificationMsg;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
@ -43,6 +48,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotifica
import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg;
import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueProducer;
import org.thingsboard.server.queue.common.MultipleTbQueueCallbackWrapper;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
@ -152,8 +158,16 @@ public class DefaultTbClusterService implements TbClusterService {
private TbMsg transformMsg(TbMsg tbMsg, DeviceProfile deviceProfile) {
if (deviceProfile != null) {
RuleChainId targetRuleChainId = deviceProfile.getDefaultRuleChainId();
if (targetRuleChainId != null && !targetRuleChainId.equals(tbMsg.getRuleChainId())) {
String targetQueueName = deviceProfile.getDefaultQueueName();
boolean isRuleChainTransform = targetRuleChainId != null && !targetRuleChainId.equals(tbMsg.getRuleChainId());
boolean isQueueTransform = targetQueueName != null && !targetQueueName.equals(tbMsg.getQueueName());
if (isRuleChainTransform && isQueueTransform) {
tbMsg = TbMsg.transformMsg(tbMsg, targetRuleChainId, targetQueueName);
} else if (isRuleChainTransform) {
tbMsg = TbMsg.transformMsg(tbMsg, targetRuleChainId);
} else if (isQueueTransform) {
tbMsg = TbMsg.transformMsg(tbMsg, targetQueueName);
}
}
return tbMsg;
@ -189,30 +203,78 @@ public class DefaultTbClusterService implements TbClusterService {
@Override
public void onDeviceProfileChange(DeviceProfile deviceProfile, TbQueueCallback callback) {
log.trace("[{}][{}] Processing device profile [{}] change event", deviceProfile.getTenantId(), deviceProfile.getId(), deviceProfile.getName());
TransportProtos.DeviceProfileUpdateMsg profileUpdateMsg = TransportProtos.DeviceProfileUpdateMsg.newBuilder()
.setData(ByteString.copyFrom(encodingService.encode(deviceProfile))).build();
ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setDeviceProfileUpdateMsg(profileUpdateMsg).build();
broadcast(transportMsg);
onEntityChange(deviceProfile.getTenantId(), deviceProfile.getId(), deviceProfile, callback);
}
@Override
public void onTenantProfileChange(TenantProfile tenantProfile, TbQueueCallback callback) {
onEntityChange(TenantId.SYS_TENANT_ID, tenantProfile.getId(), tenantProfile, callback);
}
@Override
public void onTenantChange(Tenant tenant, TbQueueCallback callback) {
onEntityChange(TenantId.SYS_TENANT_ID, tenant.getId(), tenant, callback);
}
@Override
public void onApiStateChange(ApiUsageState apiUsageState, TbQueueCallback callback) {
onEntityChange(apiUsageState.getTenantId(), apiUsageState.getId(), apiUsageState, callback);
broadcast(new ComponentLifecycleMsg(apiUsageState.getTenantId(), apiUsageState.getId(), ComponentLifecycleEvent.UPDATED));
}
@Override
public void onDeviceProfileDelete(DeviceProfile entity, TbQueueCallback callback) {
onEntityDelete(entity.getTenantId(), entity.getId(), entity.getName(), callback);
}
@Override
public void onDeviceProfileDelete(DeviceProfile deviceProfile, TbQueueCallback callback) {
log.trace("[{}][{}] Processing device profile [{}] delete event", deviceProfile.getTenantId(), deviceProfile.getId(), deviceProfile.getName());
TransportProtos.DeviceProfileDeleteMsg profileDeleteMsg = TransportProtos.DeviceProfileDeleteMsg.newBuilder()
.setProfileIdMSB(deviceProfile.getId().getId().getMostSignificantBits())
.setProfileIdLSB(deviceProfile.getId().getId().getLeastSignificantBits())
public void onTenantProfileDelete(TenantProfile entity, TbQueueCallback callback) {
onEntityDelete(TenantId.SYS_TENANT_ID, entity.getId(), entity.getName(), callback);
}
@Override
public void onTenantDelete(Tenant entity, TbQueueCallback callback) {
onEntityDelete(TenantId.SYS_TENANT_ID, entity.getId(), entity.getName(), callback);
}
@Override
public void onDeviceChange(Device entity, TbQueueCallback callback) {
onEntityChange(entity.getTenantId(), entity.getId(), entity, callback);
}
@Override
public void onDeviceDeleted(Device entity, TbQueueCallback callback) {
onEntityDelete(entity.getTenantId(), entity.getId(), entity.getName(), callback);
}
public <T> void onEntityChange(TenantId tenantId, EntityId entityid, T entity, TbQueueCallback callback) {
String entityName = (entity instanceof HasName) ? ((HasName) entity).getName() : entity.getClass().getName();
log.trace("[{}][{}][{}] Processing [{}] change event", tenantId, entityid.getEntityType(), entityid.getId(), entityName);
TransportProtos.EntityUpdateMsg entityUpdateMsg = TransportProtos.EntityUpdateMsg.newBuilder()
.setEntityType(entityid.getEntityType().name())
.setData(ByteString.copyFrom(encodingService.encode(entity))).build();
ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setEntityUpdateMsg(entityUpdateMsg).build();
broadcast(transportMsg, callback);
}
private void onEntityDelete(TenantId tenantId, EntityId entityId, String name, TbQueueCallback callback) {
log.trace("[{}][{}][{}] Processing [{}] delete event", tenantId, entityId.getEntityType(), entityId.getId(), name);
TransportProtos.EntityDeleteMsg entityDeleteMsg = TransportProtos.EntityDeleteMsg.newBuilder()
.setEntityType(entityId.getEntityType().name())
.setEntityIdMSB(entityId.getId().getMostSignificantBits())
.setEntityIdLSB(entityId.getId().getLeastSignificantBits())
.build();
ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setDeviceProfileDeleteMsg(profileDeleteMsg).build();
broadcast(transportMsg);
ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setEntityDeleteMsg(entityDeleteMsg).build();
broadcast(transportMsg, callback);
}
private void broadcast(ToTransportMsg transportMsg) {
private void broadcast(ToTransportMsg transportMsg, TbQueueCallback callback) {
TbQueueProducer<TbProtoQueueMsg<ToTransportMsg>> toTransportNfProducer = producerProvider.getTransportNotificationsMsgProducer();
Set<String> tbTransportServices = partitionService.getAllServiceIds(ServiceType.TB_TRANSPORT);
TbQueueCallback proxyCallback = callback != null ? new MultipleTbQueueCallbackWrapper(tbTransportServices.size(), callback) : null;
for (String transportServiceId : tbTransportServices) {
TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_TRANSPORT, transportServiceId);
toTransportNfProducer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), transportMsg), null);
toTransportNfProducer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), transportMsg), proxyCallback);
toTransportNfs.incrementAndGet();
}
}
@ -221,7 +283,10 @@ public class DefaultTbClusterService implements TbClusterService {
byte[] msgBytes = encodingService.encode(msg);
TbQueueProducer<TbProtoQueueMsg<ToRuleEngineNotificationMsg>> toRuleEngineProducer = producerProvider.getRuleEngineNotificationsMsgProducer();
Set<String> tbRuleEngineServices = new HashSet<>(partitionService.getAllServiceIds(ServiceType.TB_RULE_ENGINE));
if (msg.getEntityId().getEntityType().equals(EntityType.TENANT)) {
if (msg.getEntityId().getEntityType().equals(EntityType.TENANT)
|| msg.getEntityId().getEntityType().equals(EntityType.TENANT_PROFILE)
|| msg.getEntityId().getEntityType().equals(EntityType.DEVICE_PROFILE)
|| msg.getEntityId().getEntityType().equals(EntityType.API_USAGE_STATE)) {
TbQueueProducer<TbProtoQueueMsg<ToCoreNotificationMsg>> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer();
Set<String> tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE);
for (String serviceId : tbCoreServices) {

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

@ -15,10 +15,16 @@
*/
package org.thingsboard.server.service.queue;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.rule.engine.api.RpcError;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.alarm.Alarm;
@ -43,14 +49,17 @@ import org.thingsboard.server.gen.transport.TransportProtos.TbSubscriptionCloseP
import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesUpdateProto;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg;
import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.PartitionChangeEvent;
import org.thingsboard.server.queue.provider.TbCoreQueueFactory;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.edge.EdgeNotificationService;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.service.queue.processing.AbstractConsumerService;
import org.thingsboard.server.service.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService;
@ -69,6 +78,9 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
@ -87,35 +99,60 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
private final TbQueueConsumer<TbProtoQueueMsg<ToCoreMsg>> mainConsumer;
private final DeviceStateService stateService;
private final TbApiUsageStateService statsService;
private final TbLocalSubscriptionService localSubscriptionService;
private final SubscriptionManagerService subscriptionManagerService;
private final TbCoreDeviceRpcService tbCoreDeviceRpcService;
private final EdgeNotificationService edgeNotificationService;
private final TbCoreConsumerStats stats;
protected final TbQueueConsumer<TbProtoQueueMsg<ToUsageStatsServiceMsg>> usageStatsConsumer;
public DefaultTbCoreConsumerService(TbCoreQueueFactory tbCoreQueueFactory, ActorSystemContext actorContext,
DeviceStateService stateService, TbLocalSubscriptionService localSubscriptionService,
SubscriptionManagerService subscriptionManagerService, DataDecodingEncodingService encodingService,
TbCoreDeviceRpcService tbCoreDeviceRpcService, StatsFactory statsFactory, TbDeviceProfileCache deviceProfileCache,
protected volatile ExecutorService usageStatsExecutor;
public DefaultTbCoreConsumerService(TbCoreQueueFactory tbCoreQueueFactory,
ActorSystemContext actorContext,
DeviceStateService stateService,
TbLocalSubscriptionService localSubscriptionService,
SubscriptionManagerService subscriptionManagerService,
DataDecodingEncodingService encodingService,
TbCoreDeviceRpcService tbCoreDeviceRpcService,
StatsFactory statsFactory,
TbDeviceProfileCache deviceProfileCache,
TbApiUsageStateService statsService,
TbTenantProfileCache tenantProfileCache,
TbApiUsageStateService apiUsageStateService,
EdgeNotificationService edgeNotificationService) {
super(actorContext, encodingService, deviceProfileCache, tbCoreQueueFactory.createToCoreNotificationsMsgConsumer());
super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, apiUsageStateService, tbCoreQueueFactory.createToCoreNotificationsMsgConsumer());
this.mainConsumer = tbCoreQueueFactory.createToCoreMsgConsumer();
this.usageStatsConsumer = tbCoreQueueFactory.createToUsageStatsServiceMsgConsumer();
this.stateService = stateService;
this.localSubscriptionService = localSubscriptionService;
this.subscriptionManagerService = subscriptionManagerService;
this.tbCoreDeviceRpcService = tbCoreDeviceRpcService;
this.edgeNotificationService = edgeNotificationService;
this.stats = new TbCoreConsumerStats(statsFactory);
this.statsService = statsService;
}
@PostConstruct
public void init() {
super.init("tb-core-consumer", "tb-core-notifications-consumer");
this.usageStatsExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("tb-core-usage-stats-consumer"));
}
@PreDestroy
public void destroy() {
super.destroy();
if (usageStatsExecutor != null) {
usageStatsExecutor.shutdownNow();
}
}
@EventListener(ApplicationReadyEvent.class)
@Order(value = 2)
public void onApplicationEvent(ApplicationReadyEvent event) {
super.onApplicationEvent(event);
launchUsageStatsConsumer();
}
@Override
@ -123,6 +160,12 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
if (partitionChangeEvent.getServiceType().equals(getServiceType())) {
log.info("Subscribing to partitions: {}", partitionChangeEvent.getPartitions());
this.mainConsumer.subscribe(partitionChangeEvent.getPartitions());
this.usageStatsConsumer.subscribe(
partitionChangeEvent
.getPartitions()
.stream()
.map(tpi -> tpi.newByTopic(usageStatsConsumer.getTopic()))
.collect(Collectors.toSet()));
}
}
@ -140,42 +183,51 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
CountDownLatch processingTimeoutLatch = new CountDownLatch(1);
TbPackProcessingContext<TbProtoQueueMsg<ToCoreMsg>> ctx = new TbPackProcessingContext<>(
processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>());
pendingMap.forEach((id, msg) -> {
log.trace("[{}] Creating main callback for message: {}", id, msg.getValue());
TbCallback callback = new TbPackCallback<>(id, ctx);
try {
ToCoreMsg toCoreMsg = msg.getValue();
if (toCoreMsg.hasToSubscriptionMgrMsg()) {
log.trace("[{}] Forwarding message to subscription manager service {}", id, toCoreMsg.getToSubscriptionMgrMsg());
forwardToSubMgrService(toCoreMsg.getToSubscriptionMgrMsg(), callback);
} else if (toCoreMsg.hasToDeviceActorMsg()) {
log.trace("[{}] Forwarding message to device actor {}", id, toCoreMsg.getToDeviceActorMsg());
forwardToDeviceActor(toCoreMsg.getToDeviceActorMsg(), callback);
} else if (toCoreMsg.hasDeviceStateServiceMsg()) {
log.trace("[{}] Forwarding message to state service {}", id, toCoreMsg.getDeviceStateServiceMsg());
forwardToStateService(toCoreMsg.getDeviceStateServiceMsg(), callback);
} else if (toCoreMsg.hasEdgeNotificationMsg()) {
log.trace("[{}] Forwarding message to edge service {}", id, toCoreMsg.getEdgeNotificationMsg());
forwardToEdgeNotificationService(toCoreMsg.getEdgeNotificationMsg(), callback);
} else if (toCoreMsg.getToDeviceActorNotificationMsg() != null && !toCoreMsg.getToDeviceActorNotificationMsg().isEmpty()) {
Optional<TbActorMsg> actorMsg = encodingService.decode(toCoreMsg.getToDeviceActorNotificationMsg().toByteArray());
if (actorMsg.isPresent()) {
TbActorMsg tbActorMsg = actorMsg.get();
if (tbActorMsg.getMsgType().equals(MsgType.DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG)) {
tbCoreDeviceRpcService.forwardRpcRequestToDeviceActor((ToDeviceRpcRequestActorMsg) tbActorMsg);
} else {
log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg.get());
actorContext.tell(actorMsg.get());
PendingMsgHolder pendingMsgHolder = new PendingMsgHolder();
Future<?> packSubmitFuture = consumersExecutor.submit(() -> {
pendingMap.forEach((id, msg) -> {
log.trace("[{}] Creating main callback for message: {}", id, msg.getValue());
TbCallback callback = new TbPackCallback<>(id, ctx);
try {
ToCoreMsg toCoreMsg = msg.getValue();
pendingMsgHolder.setToCoreMsg(toCoreMsg);
if (toCoreMsg.hasToSubscriptionMgrMsg()) {
log.trace("[{}] Forwarding message to subscription manager service {}", id, toCoreMsg.getToSubscriptionMgrMsg());
forwardToSubMgrService(toCoreMsg.getToSubscriptionMgrMsg(), callback);
} else if (toCoreMsg.hasToDeviceActorMsg()) {
log.trace("[{}] Forwarding message to device actor {}", id, toCoreMsg.getToDeviceActorMsg());
forwardToDeviceActor(toCoreMsg.getToDeviceActorMsg(), callback);
} else if (toCoreMsg.hasDeviceStateServiceMsg()) {
log.trace("[{}] Forwarding message to state service {}", id, toCoreMsg.getDeviceStateServiceMsg());
forwardToStateService(toCoreMsg.getDeviceStateServiceMsg(), callback);
} else if (toCoreMsg.hasEdgeNotificationMsg()) {
log.trace("[{}] Forwarding message to edge service {}", id, toCoreMsg.getEdgeNotificationMsg());
forwardToEdgeNotificationService(toCoreMsg.getEdgeNotificationMsg(), callback);
} else if (toCoreMsg.getToDeviceActorNotificationMsg() != null && !toCoreMsg.getToDeviceActorNotificationMsg().isEmpty()) {
Optional<TbActorMsg> actorMsg = encodingService.decode(toCoreMsg.getToDeviceActorNotificationMsg().toByteArray());
if (actorMsg.isPresent()) {
TbActorMsg tbActorMsg = actorMsg.get();
if (tbActorMsg.getMsgType().equals(MsgType.DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG)) {
tbCoreDeviceRpcService.forwardRpcRequestToDeviceActor((ToDeviceRpcRequestActorMsg) tbActorMsg);
} else {
log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg.get());
actorContext.tell(actorMsg.get());
}
}
callback.onSuccess();
}
callback.onSuccess();
} catch (Throwable e) {
log.warn("[{}] Failed to process message: {}", id, msg, e);
callback.onFailure(e);
}
} catch (Throwable e) {
log.warn("[{}] Failed to process message: {}", id, msg, e);
callback.onFailure(e);
}
});
});
if (!processingTimeoutLatch.await(packProcessingTimeout, TimeUnit.MILLISECONDS)) {
if (!packSubmitFuture.isDone()) {
packSubmitFuture.cancel(true);
ToCoreMsg lastSubmitMsg = pendingMsgHolder.getToCoreMsg();
log.info("Timeout to process message: {}", lastSubmitMsg);
}
ctx.getAckMap().forEach((id, msg) -> log.debug("[{}] Timeout to process message: {}", id, msg.getValue()));
ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process message: {}", id, msg.getValue()));
}
@ -195,6 +247,12 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
});
}
private static class PendingMsgHolder {
@Getter
@Setter
private volatile ToCoreMsg toCoreMsg;
}
@Override
protected ServiceType getServiceType() {
return ServiceType.TB_CORE;
@ -228,6 +286,53 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
}
}
private void launchUsageStatsConsumer() {
usageStatsExecutor.submit(() -> {
while (!stopped) {
try {
List<TbProtoQueueMsg<ToUsageStatsServiceMsg>> msgs = usageStatsConsumer.poll(getNotificationPollDuration());
if (msgs.isEmpty()) {
continue;
}
ConcurrentMap<UUID, TbProtoQueueMsg<ToUsageStatsServiceMsg>> pendingMap = msgs.stream().collect(
Collectors.toConcurrentMap(s -> UUID.randomUUID(), Function.identity()));
CountDownLatch processingTimeoutLatch = new CountDownLatch(1);
TbPackProcessingContext<TbProtoQueueMsg<ToUsageStatsServiceMsg>> ctx = new TbPackProcessingContext<>(
processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>());
pendingMap.forEach((id, msg) -> {
log.trace("[{}] Creating usage stats callback for message: {}", id, msg.getValue());
TbCallback callback = new TbPackCallback<>(id, ctx);
try {
handleUsageStats(msg, callback);
} catch (Throwable e) {
log.warn("[{}] Failed to process usage stats: {}", id, msg, e);
callback.onFailure(e);
}
});
if (!processingTimeoutLatch.await(getNotificationPackProcessingTimeout(), TimeUnit.MILLISECONDS)) {
ctx.getAckMap().forEach((id, msg) -> log.warn("[{}] Timeout to process usage stats: {}", id, msg.getValue()));
ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process usage stats: {}", id, msg.getValue()));
}
usageStatsConsumer.commit();
} catch (Exception e) {
if (!stopped) {
log.warn("Failed to obtain usage stats from queue.", e);
try {
Thread.sleep(getNotificationPollDuration());
} catch (InterruptedException e2) {
log.trace("Failed to wait until the server has capacity to handle new usage stats", e2);
}
}
}
}
log.info("TB Usage Stats Consumer stopped.");
});
}
private void handleUsageStats(TbProtoQueueMsg<ToUsageStatsServiceMsg> msg, TbCallback callback) {
statsService.process(msg, callback);
}
private void forwardToCoreRpcService(FromDeviceRPCResponseProto proto, TbCallback callback) {
RpcError error = proto.getError() > 0 ? RpcError.values()[proto.getError()] : null;
FromDeviceRpcResponse response = new FromDeviceRpcResponse(new UUID(proto.getRequestIdMSB(), proto.getRequestIdLSB())
@ -333,6 +438,9 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
if (mainConsumer != null) {
mainConsumer.unsubscribe();
}
if (usageStatsConsumer != null) {
usageStatsConsumer.unsubscribe();
}
}
}

48
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.service.queue;
import com.google.protobuf.ByteString;
import com.google.protobuf.ProtocolStringList;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
@ -24,10 +23,16 @@ import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.RpcError;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.queue.*;
import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg;
import org.thingsboard.server.common.msg.queue.RuleEngineException;
import org.thingsboard.server.common.msg.queue.RuleNodeInfo;
import org.thingsboard.server.common.msg.queue.ServiceQueue;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TbMsgCallback;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.common.transport.util.DataDecodingEncodingService;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg;
@ -38,17 +43,33 @@ import org.thingsboard.server.queue.provider.TbRuleEngineQueueFactory;
import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings;
import org.thingsboard.server.queue.settings.TbRuleEngineQueueConfiguration;
import org.thingsboard.server.queue.util.TbRuleEngineComponent;
import org.thingsboard.server.common.transport.util.DataDecodingEncodingService;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.queue.processing.*;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.service.queue.processing.AbstractConsumerService;
import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingDecision;
import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingResult;
import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingStrategy;
import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingStrategyFactory;
import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategy;
import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategyFactory;
import org.thingsboard.server.service.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService;
import org.thingsboard.server.service.stats.RuleEngineStatisticsService;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.*;
import java.util.concurrent.*;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@Service
@TbRuleEngineComponent
@ -79,11 +100,16 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
public DefaultTbRuleEngineConsumerService(TbRuleEngineProcessingStrategyFactory processingStrategyFactory,
TbRuleEngineSubmitStrategyFactory submitStrategyFactory,
TbQueueRuleEngineSettings ruleEngineSettings,
TbRuleEngineQueueFactory tbRuleEngineQueueFactory, RuleEngineStatisticsService statisticsService,
ActorSystemContext actorContext, DataDecodingEncodingService encodingService,
TbRuleEngineQueueFactory tbRuleEngineQueueFactory,
RuleEngineStatisticsService statisticsService,
ActorSystemContext actorContext,
DataDecodingEncodingService encodingService,
TbRuleEngineDeviceRpcService tbDeviceRpcService,
StatsFactory statsFactory, TbDeviceProfileCache deviceProfileCache) {
super(actorContext, encodingService, deviceProfileCache, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer());
StatsFactory statsFactory,
TbDeviceProfileCache deviceProfileCache,
TbTenantProfileCache tenantProfileCache,
TbApiUsageStateService apiUsageStateService) {
super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, apiUsageStateService, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer());
this.statisticsService = statisticsService;
this.ruleEngineSettings = ruleEngineSettings;
this.tbRuleEngineQueueFactory = tbRuleEngineQueueFactory;

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

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

21
application/src/main/java/org/thingsboard/server/service/queue/TbClusterService.java

@ -16,8 +16,11 @@
package org.thingsboard.server.service.queue;
import org.thingsboard.rule.engine.api.msg.ToDeviceActorNotificationMsg;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
@ -53,5 +56,19 @@ public interface TbClusterService {
void onDeviceProfileChange(DeviceProfile deviceProfile, TbQueueCallback callback);
void onDeviceProfileDelete(DeviceProfile deviceProfileId, TbQueueCallback callback);
void onDeviceProfileDelete(DeviceProfile deviceProfile, TbQueueCallback callback);
void onTenantProfileChange(TenantProfile tenantProfile, TbQueueCallback callback);
void onTenantProfileDelete(TenantProfile tenantProfile, TbQueueCallback callback);
void onTenantChange(Tenant tenant, TbQueueCallback callback);
void onTenantDelete(Tenant tenant, TbQueueCallback callback);
void onApiStateChange(ApiUsageState apiUsageState, TbQueueCallback callback);
void onDeviceChange(Device device, TbQueueCallback callback);
void onDeviceDeleted(Device device, TbQueueCallback callback);
}

33
application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java

@ -20,11 +20,14 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.ServiceType;
@ -33,7 +36,9 @@ import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.PartitionChangeEvent;
import org.thingsboard.server.common.transport.util.DataDecodingEncodingService;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.service.queue.TbPackCallback;
import org.thingsboard.server.service.queue.TbPackProcessingContext;
@ -59,15 +64,19 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
protected final ActorSystemContext actorContext;
protected final DataDecodingEncodingService encodingService;
protected final TbTenantProfileCache tenantProfileCache;
protected final TbDeviceProfileCache deviceProfileCache;
protected final TbApiUsageStateService apiUsageStateService;
protected final TbQueueConsumer<TbProtoQueueMsg<N>> nfConsumer;
public AbstractConsumerService(ActorSystemContext actorContext, DataDecodingEncodingService encodingService,
TbDeviceProfileCache deviceProfileCache, TbQueueConsumer<TbProtoQueueMsg<N>> nfConsumer) {
TbTenantProfileCache tenantProfileCache, TbDeviceProfileCache deviceProfileCache, TbApiUsageStateService apiUsageStateService, TbQueueConsumer<TbProtoQueueMsg<N>> nfConsumer) {
this.actorContext = actorContext;
this.encodingService = encodingService;
this.tenantProfileCache = tenantProfileCache;
this.deviceProfileCache = deviceProfileCache;
this.apiUsageStateService = apiUsageStateService;
this.nfConsumer = nfConsumer;
}
@ -77,6 +86,7 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
}
@EventListener(ApplicationReadyEvent.class)
@Order(value = 2)
public void onApplicationEvent(ApplicationReadyEvent event) {
log.info("Subscribing to notifications: {}", nfConsumer.getTopic());
this.nfConsumer.subscribe();
@ -143,10 +153,25 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
TbActorMsg actorMsg = actorMsgOpt.get();
if (actorMsg instanceof ComponentLifecycleMsg) {
ComponentLifecycleMsg componentLifecycleMsg = (ComponentLifecycleMsg) actorMsg;
if (EntityType.DEVICE_PROFILE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
deviceProfileCache.evict(new DeviceProfileId(componentLifecycleMsg.getEntityId().getId()));
log.info("[{}][{}][{}] Received Lifecycle event: {}", componentLifecycleMsg.getTenantId(), componentLifecycleMsg.getEntityId().getEntityType(),
componentLifecycleMsg.getEntityId(), componentLifecycleMsg.getEvent());
if (EntityType.TENANT_PROFILE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
TenantProfileId tenantProfileId = new TenantProfileId(componentLifecycleMsg.getEntityId().getId());
tenantProfileCache.evict(tenantProfileId);
if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.UPDATED)) {
apiUsageStateService.onTenantProfileUpdate(tenantProfileId);
}
} else if (EntityType.TENANT.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
tenantProfileCache.evict(componentLifecycleMsg.getTenantId());
if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.UPDATED)) {
apiUsageStateService.onTenantUpdate(componentLifecycleMsg.getTenantId());
}
} else if (EntityType.DEVICE_PROFILE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
deviceProfileCache.evict(componentLifecycleMsg.getTenantId(), new DeviceProfileId(componentLifecycleMsg.getEntityId().getId()));
} else if (EntityType.DEVICE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
deviceProfileCache.evict(new DeviceId(componentLifecycleMsg.getEntityId().getId()));
deviceProfileCache.evict(componentLifecycleMsg.getTenantId(), new DeviceId(componentLifecycleMsg.getEntityId().getId()));
} else if (EntityType.API_USAGE_STATE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
apiUsageStateService.onApiUsageStateUpdate(componentLifecycleMsg.getTenantId());
}
}
log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg);

64
application/src/main/java/org/thingsboard/server/service/script/AbstractJsInvokeService.java

@ -19,6 +19,10 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import java.util.Map;
import java.util.UUID;
@ -33,9 +37,16 @@ import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public abstract class AbstractJsInvokeService implements JsInvokeService {
private final TbApiUsageStateService apiUsageStateService;
private final TbApiUsageClient apiUsageClient;
protected ScheduledExecutorService timeoutExecutorService;
protected Map<UUID, String> scriptIdToNameMap = new ConcurrentHashMap<>();
protected Map<UUID, BlackListInfo> blackListedFunctions = new ConcurrentHashMap<>();
protected Map<UUID, DisableListInfo> disabledFunctions = new ConcurrentHashMap<>();
protected AbstractJsInvokeService(TbApiUsageStateService apiUsageStateService, TbApiUsageClient apiUsageClient) {
this.apiUsageStateService = apiUsageStateService;
this.apiUsageClient = apiUsageClient;
}
public void init(long maxRequestsTimeout) {
if (maxRequestsTimeout > 0) {
@ -50,24 +61,33 @@ public abstract class AbstractJsInvokeService implements JsInvokeService {
}
@Override
public ListenableFuture<UUID> eval(JsScriptType scriptType, String scriptBody, String... argNames) {
UUID scriptId = UUID.randomUUID();
String functionName = "invokeInternal_" + scriptId.toString().replace('-', '_');
String jsScript = generateJsScript(scriptType, functionName, scriptBody, argNames);
return doEval(scriptId, functionName, jsScript);
public ListenableFuture<UUID> eval(TenantId tenantId, JsScriptType scriptType, String scriptBody, String... argNames) {
if (apiUsageStateService.getApiUsageState(tenantId).isJsExecEnabled()) {
UUID scriptId = UUID.randomUUID();
String functionName = "invokeInternal_" + scriptId.toString().replace('-', '_');
String jsScript = generateJsScript(scriptType, functionName, scriptBody, argNames);
return doEval(scriptId, functionName, jsScript);
} else {
return Futures.immediateFailedFuture(new RuntimeException("JS Execution is disabled due to API limits!"));
}
}
@Override
public ListenableFuture<Object> invokeFunction(UUID scriptId, Object... args) {
String functionName = scriptIdToNameMap.get(scriptId);
if (functionName == null) {
return Futures.immediateFailedFuture(new RuntimeException("No compiled script found for scriptId: [" + scriptId + "]!"));
}
if (!isBlackListed(scriptId)) {
return doInvokeFunction(scriptId, functionName, args);
public ListenableFuture<Object> invokeFunction(TenantId tenantId, UUID scriptId, Object... args) {
if (apiUsageStateService.getApiUsageState(tenantId).isJsExecEnabled()) {
String functionName = scriptIdToNameMap.get(scriptId);
if (functionName == null) {
return Futures.immediateFailedFuture(new RuntimeException("No compiled script found for scriptId: [" + scriptId + "]!"));
}
if (!isDisabled(scriptId)) {
apiUsageClient.report(tenantId, ApiUsageRecordKey.JS_EXEC_COUNT, 1);
return doInvokeFunction(scriptId, functionName, args);
} else {
return Futures.immediateFailedFuture(
new RuntimeException("Script invocation is blocked due to maximum error count " + getMaxErrors() + "!"));
}
} else {
return Futures.immediateFailedFuture(
new RuntimeException("Script is blacklisted due to maximum error count " + getMaxErrors() + "!"));
return Futures.immediateFailedFuture(new RuntimeException("JS Execution is disabled due to API limits!"));
}
}
@ -77,7 +97,7 @@ public abstract class AbstractJsInvokeService implements JsInvokeService {
if (functionName != null) {
try {
scriptIdToNameMap.remove(scriptId);
blackListedFunctions.remove(scriptId);
disabledFunctions.remove(scriptId);
doRelease(scriptId, functionName);
} catch (Exception e) {
return Futures.immediateFailedFuture(e);
@ -97,7 +117,7 @@ public abstract class AbstractJsInvokeService implements JsInvokeService {
protected abstract long getMaxBlacklistDuration();
protected void onScriptExecutionError(UUID scriptId) {
blackListedFunctions.computeIfAbsent(scriptId, key -> new BlackListInfo()).incrementAndGet();
disabledFunctions.computeIfAbsent(scriptId, key -> new DisableListInfo()).incrementAndGet();
}
private String generateJsScript(JsScriptType scriptType, String functionName, String scriptBody, String... argNames) {
@ -107,11 +127,11 @@ public abstract class AbstractJsInvokeService implements JsInvokeService {
throw new RuntimeException("No script factory implemented for scriptType: " + scriptType);
}
private boolean isBlackListed(UUID scriptId) {
BlackListInfo errorCount = blackListedFunctions.get(scriptId);
private boolean isDisabled(UUID scriptId) {
DisableListInfo errorCount = disabledFunctions.get(scriptId);
if (errorCount != null) {
if (errorCount.getExpirationTime() <= System.currentTimeMillis()) {
blackListedFunctions.remove(scriptId);
disabledFunctions.remove(scriptId);
return false;
} else {
return errorCount.get() >= getMaxErrors();
@ -121,11 +141,11 @@ public abstract class AbstractJsInvokeService implements JsInvokeService {
}
}
private class BlackListInfo {
private class DisableListInfo {
private final AtomicInteger counter;
private long expirationTime;
private BlackListInfo() {
private DisableListInfo() {
this.counter = new AtomicInteger(0);
}

13
application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsInvokeService.java

@ -24,10 +24,10 @@ import delight.nashornsandbox.NashornSandboxes;
import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
@ -38,7 +38,6 @@ import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@ -57,9 +56,8 @@ public abstract class AbstractNashornJsInvokeService extends AbstractJsInvokeSer
private final FutureCallback<UUID> evalCallback = new JsStatCallback<>(jsEvalMsgs, jsTimeoutMsgs, jsFailedMsgs);
private final FutureCallback<Object> invokeCallback = new JsStatCallback<>(jsInvokeMsgs, jsTimeoutMsgs, jsFailedMsgs);
@Autowired
@Getter
private JsExecutorService jsExecutor;
private final JsExecutorService jsExecutor;
@Value("${js.local.max_requests_timeout:0}")
private long maxRequestsTimeout;
@ -67,6 +65,11 @@ public abstract class AbstractNashornJsInvokeService extends AbstractJsInvokeSer
@Value("${js.local.stats.enabled:false}")
private boolean statsEnabled;
public AbstractNashornJsInvokeService(TbApiUsageStateService apiUsageStateService, TbApiUsageClient apiUsageClient, JsExecutorService jsExecutor) {
super(apiUsageStateService, apiUsageClient);
this.jsExecutor = jsExecutor;
}
@Scheduled(fixedDelayString = "${js.local.stats.print_interval_ms:10000}")
public void printStats() {
if (statsEnabled) {

2
application/src/main/java/org/thingsboard/server/service/script/JsExecutorService.java

@ -27,7 +27,7 @@ public class JsExecutorService extends AbstractListeningExecutor {
@Override
protected int getThreadPollSize() {
return jsExecutorThreadPoolSize;
return Math.max(jsExecutorThreadPoolSize, 1);
}
}

5
application/src/main/java/org/thingsboard/server/service/script/JsInvokeService.java

@ -17,14 +17,15 @@ package org.thingsboard.server.service.script;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import java.util.UUID;
public interface JsInvokeService {
ListenableFuture<UUID> eval(JsScriptType scriptType, String scriptBody, String... argNames);
ListenableFuture<UUID> eval(TenantId tenantId, JsScriptType scriptType, String scriptBody, String... argNames);
ListenableFuture<Object> invokeFunction(UUID scriptId, Object... args);
ListenableFuture<Object> invokeFunction(TenantId tenantId, UUID scriptId, Object... args);
ListenableFuture<Void> release(UUID scriptId);

6
application/src/main/java/org/thingsboard/server/service/script/NashornJsInvokeService.java

@ -19,6 +19,8 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import java.util.concurrent.TimeUnit;
@ -42,6 +44,10 @@ public class NashornJsInvokeService extends AbstractNashornJsInvokeService {
@Value("${js.local.max_black_list_duration_sec:60}")
private int maxBlackListDurationSec;
public NashornJsInvokeService(TbApiUsageStateService apiUsageStateService, TbApiUsageClient apiUsageClient, JsExecutorService jsExecutor) {
super(apiUsageStateService, apiUsageClient, jsExecutor);
}
@Override
protected boolean useJsSandbox() {
return useJsSandbox;

6
application/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java

@ -30,6 +30,8 @@ import org.thingsboard.server.gen.js.JsInvokeProtos;
import org.thingsboard.server.queue.TbQueueRequestTemplate;
import org.thingsboard.server.queue.common.TbProtoJsQueueMsg;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
@ -68,6 +70,10 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
private final AtomicInteger queueFailedMsgs = new AtomicInteger(0);
private final AtomicInteger queueTimeoutMsgs = new AtomicInteger(0);
public RemoteJsInvokeService(TbApiUsageStateService apiUsageStateService, TbApiUsageClient apiUsageClient) {
super(apiUsageStateService, apiUsageClient);
}
@Scheduled(fixedDelayString = "${js.remote.stats.print_interval_ms}")
public void printStats() {
if (statsEnabled) {

11
application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java

@ -25,6 +25,7 @@ import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
@ -43,13 +44,15 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
private final JsInvokeService sandboxService;
private final UUID scriptId;
private final TenantId tenantId;
private final EntityId entityId;
public RuleNodeJsScriptEngine(JsInvokeService sandboxService, EntityId entityId, String script, String... argNames) {
public RuleNodeJsScriptEngine(TenantId tenantId, JsInvokeService sandboxService, EntityId entityId, String script, String... argNames) {
this.tenantId = tenantId;
this.sandboxService = sandboxService;
this.entityId = entityId;
try {
this.scriptId = this.sandboxService.eval(JsScriptType.RULE_NODE_SCRIPT, script, argNames).get();
this.scriptId = this.sandboxService.eval(tenantId, JsScriptType.RULE_NODE_SCRIPT, script, argNames).get();
} catch (Exception e) {
Throwable t = e;
if (e instanceof ExecutionException) {
@ -203,7 +206,7 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
private JsonNode executeScript(TbMsg msg) throws ScriptException {
try {
String[] inArgs = prepareArgs(msg);
String eval = sandboxService.invokeFunction(this.scriptId, inArgs[0], inArgs[1], inArgs[2]).get().toString();
String eval = sandboxService.invokeFunction(tenantId, this.scriptId, inArgs[0], inArgs[1], inArgs[2]).get().toString();
return mapper.readTree(eval);
} catch (ExecutionException e) {
if (e.getCause() instanceof ScriptException) {
@ -220,7 +223,7 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
private ListenableFuture<JsonNode> executeScriptAsync(TbMsg msg) {
String[] inArgs = prepareArgs(msg);
return Futures.transformAsync(sandboxService.invokeFunction(this.scriptId, inArgs[0], inArgs[1], inArgs[2]),
return Futures.transformAsync(sandboxService.invokeFunction(tenantId, this.scriptId, inArgs[0], inArgs[1], inArgs[2]),
o -> {
try {
return Futures.immediateFuture(mapper.readTree(o.toString()));

41
application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java

@ -25,6 +25,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
@ -34,6 +35,7 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.ApiUsageStateId;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
@ -56,8 +58,10 @@ import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.usagerecord.ApiUsageStateService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.AccessControlService;
@ -118,6 +122,9 @@ public class AccessValidator {
@Autowired
protected AccessControlService accessControlService;
@Autowired
protected ApiUsageStateService apiUsageStateService;
private ExecutorService executor;
@PostConstruct
@ -159,7 +166,11 @@ public class AccessValidator {
new FutureCallback<DeferredResult<ResponseEntity>>() {
@Override
public void onSuccess(@Nullable DeferredResult<ResponseEntity> result) {
onSuccess.accept(response, currentUser.getTenantId(), entityId);
try {
onSuccess.accept(response, currentUser.getTenantId(), entityId);
} catch (Exception e) {
onFailure(e);
}
}
@Override
@ -203,6 +214,9 @@ public class AccessValidator {
case EDGE:
validateEdge(currentUser, operation, entityId, callback);
return;
case API_USAGE_STATE:
validateApiUsageState(currentUser, operation, entityId, callback);
return;
default:
//TODO: add support of other entities
throw new IllegalStateException("Not Implemented!");
@ -247,6 +261,27 @@ public class AccessValidator {
}
}
private void validateApiUsageState(final SecurityUser currentUser, Operation operation, EntityId entityId, FutureCallback<ValidationResult> callback) {
if (currentUser.isSystemAdmin()) {
callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
} else {
if (!operation.equals(Operation.READ_TELEMETRY)) {
callback.onSuccess(ValidationResult.accessDenied("Allowed only READ_TELEMETRY operation!"));
}
ApiUsageState apiUsageState = apiUsageStateService.findApiUsageStateById(currentUser.getTenantId(), new ApiUsageStateId(entityId.getId()));
if (apiUsageState == null) {
callback.onSuccess(ValidationResult.entityNotFound("Api Usage State with requested id wasn't found!"));
} else {
try {
accessControlService.checkPermission(currentUser, Resource.API_USAGE_STATE, operation, entityId, apiUsageState);
} catch (ThingsboardException e) {
callback.onSuccess(ValidationResult.accessDenied(e.getMessage()));
}
callback.onSuccess(ValidationResult.ok(apiUsageState));
}
}
}
private void validateAsset(final SecurityUser currentUser, Operation operation, EntityId entityId, FutureCallback<ValidationResult> callback) {
if (currentUser.isSystemAdmin()) {
callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
@ -434,9 +469,9 @@ public class AccessValidator {
public static void handleError(Throwable e, final DeferredResult<ResponseEntity> response, HttpStatus defaultErrorStatus) {
ResponseEntity responseEntity;
if (e != null && e instanceof ToErrorResponseEntity) {
if (e instanceof ToErrorResponseEntity) {
responseEntity = ((ToErrorResponseEntity) e).toErrorResponseEntity();
} else if (e != null && e instanceof IllegalArgumentException) {
} else if (e instanceof IllegalArgumentException || e instanceof IncorrectParameterException) {
responseEntity = new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
} else {
responseEntity = new ResponseEntity<>(defaultErrorStatus);

38
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java

@ -17,7 +17,6 @@ package org.thingsboard.server.service.security.auth.oauth2;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Strings;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@ -32,24 +31,27 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.IdBased;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.oauth2.OAuth2User;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.install.InstallScripts;
import org.thingsboard.server.service.queue.TbClusterService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@ -77,14 +79,23 @@ public abstract class AbstractOAuth2ClientMapper {
@Autowired
private InstallScripts installScripts;
@Autowired
protected TbTenantProfileCache tenantProfileCache;
@Autowired
protected TbClusterService tbClusterService;
private final Lock userCreationLock = new ReentrantLock();
protected SecurityUser getOrCreateSecurityUserFromOAuth2User(OAuth2User oauth2User, boolean allowUserCreation, boolean activateUser) {
protected SecurityUser getOrCreateSecurityUserFromOAuth2User(OAuth2User oauth2User, OAuth2ClientRegistrationInfo clientRegistration) {
OAuth2MapperConfig config = clientRegistration.getMapperConfig();
UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, oauth2User.getEmail());
User user = userService.findUserByEmail(TenantId.SYS_TENANT_ID, oauth2User.getEmail());
if (user == null && !allowUserCreation) {
if (user == null && !config.isAllowUserCreation()) {
throw new UsernameNotFoundException("User not found: " + oauth2User.getEmail());
}
@ -109,21 +120,28 @@ public abstract class AbstractOAuth2ClientMapper {
user.setFirstName(oauth2User.getFirstName());
user.setLastName(oauth2User.getLastName());
ObjectNode additionalInfo = objectMapper.createObjectNode();
if (!StringUtils.isEmpty(oauth2User.getDefaultDashboardName())) {
Optional<DashboardId> dashboardIdOpt =
user.getAuthority() == Authority.TENANT_ADMIN ?
getDashboardId(tenantId, oauth2User.getDefaultDashboardName())
: getDashboardId(tenantId, customerId, oauth2User.getDefaultDashboardName());
if (dashboardIdOpt.isPresent()) {
ObjectNode additionalInfo = objectMapper.createObjectNode();
additionalInfo.put("defaultDashboardFullscreen", oauth2User.isAlwaysFullScreen());
additionalInfo.put("defaultDashboardId", dashboardIdOpt.get().getId().toString());
user.setAdditionalInfo(additionalInfo);
}
}
if (clientRegistration.getAdditionalInfo() != null &&
clientRegistration.getAdditionalInfo().has("providerName")) {
additionalInfo.put("authProviderName", clientRegistration.getAdditionalInfo().get("providerName").asText());
}
user.setAdditionalInfo(additionalInfo);
user = userService.saveUser(user);
if (activateUser) {
if (config.isActivateUser()) {
UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getTenantId(), user.getId());
userService.activateUserCredentials(user.getTenantId(), userCredentials.getActivateToken(), passwordEncoder.encode(""));
}
@ -153,6 +171,10 @@ public abstract class AbstractOAuth2ClientMapper {
tenant.setTitle(tenantName);
tenant = tenantService.saveTenant(tenant);
installScripts.createDefaultRuleChains(tenant.getId());
tenantProfileCache.evict(tenant.getId());
tbClusterService.onTenantChange(tenant, null);
tbClusterService.onEntityStateChange(tenant.getId(), tenant.getId(),
ComponentLifecycleEvent.CREATED);
} else {
tenant = tenants.get(0);
}

78
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicMapperUtils.java

@ -0,0 +1,78 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.auth.oauth2;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.text.StrSubstitutor;
import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
import org.thingsboard.server.dao.oauth2.OAuth2User;
import java.util.Map;
@Slf4j
public class BasicMapperUtils {
private static final String START_PLACEHOLDER_PREFIX = "%{";
private static final String END_PLACEHOLDER_PREFIX = "}";
public static OAuth2User getOAuth2User(String email, Map<String, Object> attributes, OAuth2MapperConfig config) {
OAuth2User oauth2User = new OAuth2User();
oauth2User.setEmail(email);
oauth2User.setTenantName(getTenantName(email, attributes, config));
if (!StringUtils.isEmpty(config.getBasic().getLastNameAttributeKey())) {
String lastName = getStringAttributeByKey(attributes, config.getBasic().getLastNameAttributeKey());
oauth2User.setLastName(lastName);
}
if (!StringUtils.isEmpty(config.getBasic().getFirstNameAttributeKey())) {
String firstName = getStringAttributeByKey(attributes, config.getBasic().getFirstNameAttributeKey());
oauth2User.setFirstName(firstName);
}
if (!StringUtils.isEmpty(config.getBasic().getCustomerNamePattern())) {
StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX);
String customerName = sub.replace(config.getBasic().getCustomerNamePattern());
oauth2User.setCustomerName(customerName);
}
oauth2User.setAlwaysFullScreen(config.getBasic().isAlwaysFullScreen());
if (!StringUtils.isEmpty(config.getBasic().getDefaultDashboardName())) {
oauth2User.setDefaultDashboardName(config.getBasic().getDefaultDashboardName());
}
return oauth2User;
}
public static String getTenantName(String email, Map<String, Object> attributes, OAuth2MapperConfig config) {
switch (config.getBasic().getTenantNameStrategy()) {
case EMAIL:
return email;
case DOMAIN:
return email.substring(email .indexOf("@") + 1);
case CUSTOM:
StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX);
return sub.replace(config.getBasic().getTenantNamePattern());
default:
throw new RuntimeException("Tenant Name Strategy with type " + config.getBasic().getTenantNameStrategy() + " is not supported!");
}
}
public static String getStringAttributeByKey(Map<String, Object> attributes, String key) {
String result = null;
try {
result = (String) attributes.get(key);
} catch (Exception e) {
log.warn("Can't convert attribute to String by key " + key);
}
return result;
}
}

64
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java

@ -16,11 +16,10 @@
package org.thingsboard.server.service.security.auth.oauth2;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.text.StrSubstitutor;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.thingsboard.server.dao.oauth2.OAuth2ClientMapperConfig;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
import org.thingsboard.server.dao.oauth2.OAuth2User;
import org.thingsboard.server.service.security.model.SecurityUser;
@ -30,62 +29,13 @@ import java.util.Map;
@Slf4j
public class BasicOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper {
private static final String START_PLACEHOLDER_PREFIX = "%{";
private static final String END_PLACEHOLDER_PREFIX = "}";
private static final String EMAIL_TENANT_STRATEGY = "email";
private static final String DOMAIN_TENANT_STRATEGY = "domain";
private static final String CUSTOM_TENANT_STRATEGY = "custom";
@Override
public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig config) {
OAuth2User oauth2User = new OAuth2User();
public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2ClientRegistrationInfo clientRegistration) {
OAuth2MapperConfig config = clientRegistration.getMapperConfig();
Map<String, Object> attributes = token.getPrincipal().getAttributes();
String email = getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
oauth2User.setEmail(email);
oauth2User.setTenantName(getTenantName(attributes, config));
if (!StringUtils.isEmpty(config.getBasic().getLastNameAttributeKey())) {
String lastName = getStringAttributeByKey(attributes, config.getBasic().getLastNameAttributeKey());
oauth2User.setLastName(lastName);
}
if (!StringUtils.isEmpty(config.getBasic().getFirstNameAttributeKey())) {
String firstName = getStringAttributeByKey(attributes, config.getBasic().getFirstNameAttributeKey());
oauth2User.setFirstName(firstName);
}
if (!StringUtils.isEmpty(config.getBasic().getCustomerNamePattern())) {
StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX);
String customerName = sub.replace(config.getBasic().getCustomerNamePattern());
oauth2User.setCustomerName(customerName);
}
oauth2User.setAlwaysFullScreen(config.getBasic().isAlwaysFullScreen());
if (!StringUtils.isEmpty(config.getBasic().getDefaultDashboardName())) {
oauth2User.setDefaultDashboardName(config.getBasic().getDefaultDashboardName());
}
return getOrCreateSecurityUserFromOAuth2User(oauth2User, config.isAllowUserCreation(), config.isActivateUser());
}
private String getTenantName(Map<String, Object> attributes, OAuth2ClientMapperConfig config) {
switch (config.getBasic().getTenantNameStrategy()) {
case EMAIL_TENANT_STRATEGY:
return getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
case DOMAIN_TENANT_STRATEGY:
String email = getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
return email.substring(email .indexOf("@") + 1);
case CUSTOM_TENANT_STRATEGY:
StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX);
return sub.replace(config.getBasic().getTenantNamePattern());
default:
throw new RuntimeException("Tenant Name Strategy with type " + config.getBasic().getTenantNameStrategy() + " is not supported!");
}
}
String email = BasicMapperUtils.getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
OAuth2User oauth2User = BasicMapperUtils.getOAuth2User(email, attributes, config);
private String getStringAttributeByKey(Map<String, Object> attributes, String key) {
String result = null;
try {
result = (String) attributes.get(key);
} catch (Exception e) {
log.warn("Can't convert attribute to String by key " + key);
}
return result;
return getOrCreateSecurityUserFromOAuth2User(oauth2User, clientRegistration);
}
}

18
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java

@ -23,28 +23,36 @@ import org.springframework.security.oauth2.client.authentication.OAuth2Authentic
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import org.thingsboard.server.dao.oauth2.OAuth2ClientMapperConfig;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig;
import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
import org.thingsboard.server.dao.oauth2.OAuth2User;
import org.thingsboard.server.service.security.model.SecurityUser;
@Service(value = "customOAuth2ClientMapper")
@Slf4j
public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper {
private static final String PROVIDER_ACCESS_TOKEN = "provider-access-token";
private static final ObjectMapper json = new ObjectMapper();
private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder();
@Override
public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig config) {
OAuth2User oauth2User = getOAuth2User(token, config.getCustom());
return getOrCreateSecurityUserFromOAuth2User(oauth2User, config.isAllowUserCreation(), config.isActivateUser());
public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2ClientRegistrationInfo clientRegistration) {
OAuth2MapperConfig config = clientRegistration.getMapperConfig();
OAuth2User oauth2User = getOAuth2User(token, providerAccessToken, config.getCustom());
return getOrCreateSecurityUserFromOAuth2User(oauth2User, clientRegistration);
}
private synchronized OAuth2User getOAuth2User(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig.CustomOAuth2ClientMapperConfig custom) {
private synchronized OAuth2User getOAuth2User(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2CustomMapperConfig custom) {
if (!StringUtils.isEmpty(custom.getUsername()) && !StringUtils.isEmpty(custom.getPassword())) {
restTemplateBuilder = restTemplateBuilder.basicAuthentication(custom.getUsername(), custom.getPassword());
}
if (custom.isSendToken() && !StringUtils.isEmpty(providerAccessToken)) {
restTemplateBuilder = restTemplateBuilder.defaultHeader(PROVIDER_ACCESS_TOKEN, providerAccessToken);
}
RestTemplate restTemplate = restTemplateBuilder.build();
String request;
try {

93
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java

@ -0,0 +1,93 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.auth.oauth2;
import lombok.Data;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
import org.thingsboard.server.dao.oauth2.OAuth2User;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.ArrayList;
import java.util.Map;
import java.util.Optional;
@Service(value = "githubOAuth2ClientMapper")
@Slf4j
public class GithubOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper {
private static final String EMAIL_URL_KEY = "emailUrl";
private static final String AUTHORIZATION = "Authorization";
private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder();
@Autowired
private OAuth2Configuration oAuth2Configuration;
@Override
public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2ClientRegistrationInfo clientRegistration) {
OAuth2MapperConfig config = clientRegistration.getMapperConfig();
Map<String, String> githubMapperConfig = oAuth2Configuration.getGithubMapper();
String email = getEmail(githubMapperConfig.get(EMAIL_URL_KEY), providerAccessToken);
Map<String, Object> attributes = token.getPrincipal().getAttributes();
OAuth2User oAuth2User = BasicMapperUtils.getOAuth2User(email, attributes, config);
return getOrCreateSecurityUserFromOAuth2User(oAuth2User, clientRegistration);
}
private synchronized String getEmail(String emailUrl, String oauth2Token) {
restTemplateBuilder = restTemplateBuilder.defaultHeader(AUTHORIZATION, "token " + oauth2Token);
RestTemplate restTemplate = restTemplateBuilder.build();
GithubEmailsResponse githubEmailsResponse;
try {
githubEmailsResponse = restTemplate.getForEntity(emailUrl, GithubEmailsResponse.class).getBody();
if (githubEmailsResponse == null){
throw new RuntimeException("Empty Github response!");
}
} catch (Exception e) {
log.error("There was an error during connection to Github API", e);
throw new RuntimeException("Unable to login. Please contact your Administrator!");
}
Optional<String> emailOpt = githubEmailsResponse.stream()
.filter(GithubEmailResponse::isPrimary)
.map(GithubEmailResponse::getEmail)
.findAny();
if (emailOpt.isPresent()){
return emailOpt.get();
} else {
log.error("Could not find primary email from {}.", githubEmailsResponse);
throw new RuntimeException("Unable to login. Please contact your Administrator!");
}
}
private static class GithubEmailsResponse extends ArrayList<GithubEmailResponse> {}
@Data
@ToString
private static class GithubEmailResponse {
private String email;
private boolean verified;
private boolean primary;
private String visibility;
}
}

4
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java

@ -16,9 +16,9 @@
package org.thingsboard.server.service.security.auth.oauth2;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.thingsboard.server.dao.oauth2.OAuth2ClientMapperConfig;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo;
import org.thingsboard.server.service.security.model.SecurityUser;
public interface OAuth2ClientMapper {
SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig config);
SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2ClientRegistrationInfo clientRegistration);
}

17
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java

@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.oauth2.MapperType;
@Component
@Slf4j
@ -32,14 +33,20 @@ public class OAuth2ClientMapperProvider {
@Qualifier("customOAuth2ClientMapper")
private OAuth2ClientMapper customOAuth2ClientMapper;
public OAuth2ClientMapper getOAuth2ClientMapperByType(String oauth2ClientType) {
switch (oauth2ClientType) {
case "custom":
@Autowired
@Qualifier("githubOAuth2ClientMapper")
private OAuth2ClientMapper githubOAuth2ClientMapper;
public OAuth2ClientMapper getOAuth2ClientMapperByType(MapperType oauth2MapperType) {
switch (oauth2MapperType) {
case CUSTOM:
return customOAuth2ClientMapper;
case "basic":
case BASIC:
return basicOAuth2ClientMapper;
case GITHUB:
return githubOAuth2ClientMapper;
default:
throw new RuntimeException("OAuth2ClientMapper with type " + oauth2ClientType + " is not supported!");
throw new RuntimeException("OAuth2ClientRegistrationMapper with type " + oauth2MapperType + " is not supported!");
}
}
}

16
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java

@ -15,10 +15,15 @@
*/
package org.thingsboard.server.service.security.auth.oauth2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
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.service.security.system.SystemSecurityService;
import org.thingsboard.server.utils.MiscUtils;
import javax.servlet.ServletException;
@ -30,13 +35,20 @@ import java.nio.charset.StandardCharsets;
@Component(value = "oauth2AuthenticationFailureHandler")
@ConditionalOnProperty(prefix = "security.oauth2", value = "enabled", havingValue = "true")
public class Oauth2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
public class Oauth2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
private final SystemSecurityService systemSecurityService;
@Autowired
public Oauth2AuthenticationFailureHandler(final SystemSecurityService systemSecurityService) {
this.systemSecurityService = systemSecurityService;
}
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
String baseUrl = MiscUtils.constructBaseUrl(request);
String baseUrl = this.systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, new CustomerId(EntityId.NULL_UUID), request);
getRedirectStrategy().sendRedirect(request, response, baseUrl + "/login?loginError=" +
URLEncoder.encode(exception.getMessage(), StandardCharsets.UTF_8.toString()));
}

39
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java

@ -16,17 +16,22 @@
package org.thingsboard.server.service.security.auth.oauth2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.thingsboard.server.dao.oauth2.OAuth2Client;
import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
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.oauth2.OAuth2ClientRegistrationInfo;
import org.thingsboard.server.dao.oauth2.OAuth2Service;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtToken;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import org.thingsboard.server.utils.MiscUtils;
import javax.servlet.http.HttpServletRequest;
@ -34,39 +39,47 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
@Component(value = "oauth2AuthenticationSuccessHandler")
@ConditionalOnProperty(prefix = "security.oauth2", value = "enabled", havingValue = "true")
public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final JwtTokenFactory tokenFactory;
private final RefreshTokenRepository refreshTokenRepository;
private final OAuth2ClientMapperProvider oauth2ClientMapperProvider;
private final OAuth2Configuration oauth2Configuration;
private final OAuth2Service oAuth2Service;
private final OAuth2AuthorizedClientService oAuth2AuthorizedClientService;
private final SystemSecurityService systemSecurityService;
@Autowired
public Oauth2AuthenticationSuccessHandler(final JwtTokenFactory tokenFactory,
final RefreshTokenRepository refreshTokenRepository,
final OAuth2ClientMapperProvider oauth2ClientMapperProvider,
final OAuth2Configuration oauth2Configuration) {
final OAuth2Service oAuth2Service,
final OAuth2AuthorizedClientService oAuth2AuthorizedClientService, final SystemSecurityService systemSecurityService) {
this.tokenFactory = tokenFactory;
this.refreshTokenRepository = refreshTokenRepository;
this.oauth2ClientMapperProvider = oauth2ClientMapperProvider;
this.oauth2Configuration = oauth2Configuration;
this.oAuth2Service = oAuth2Service;
this.oAuth2AuthorizedClientService = oAuth2AuthorizedClientService;
this.systemSecurityService = systemSecurityService;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException {
String baseUrl = MiscUtils.constructBaseUrl(request);
String baseUrl = this.systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, new CustomerId(EntityId.NULL_UUID), request);
try {
OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) authentication;
OAuth2Client oauth2Client = oauth2Configuration.getClientByRegistrationId(token.getAuthorizedClientRegistrationId());
OAuth2ClientMapper mapper = oauth2ClientMapperProvider.getOAuth2ClientMapperByType(oauth2Client.getMapperConfig().getType());
SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(token, oauth2Client.getMapperConfig());
OAuth2ClientRegistrationInfo clientRegistration = oAuth2Service.findClientRegistrationInfo(UUID.fromString(token.getAuthorizedClientRegistrationId()));
OAuth2AuthorizedClient oAuth2AuthorizedClient = oAuth2AuthorizedClientService.loadAuthorizedClient(
token.getAuthorizedClientRegistrationId(),
token.getPrincipal().getName());
OAuth2ClientMapper mapper = oauth2ClientMapperProvider.getOAuth2ClientMapperByType(clientRegistration.getMapperConfig().getType());
SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(),
clientRegistration);
JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);
JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser);
@ -77,4 +90,4 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS
URLEncoder.encode(e.getMessage(), StandardCharsets.UTF_8.toString()));
}
}
}
}

5
application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java

@ -33,8 +33,11 @@ public enum Resource {
USER(EntityType.USER),
WIDGETS_BUNDLE(EntityType.WIDGETS_BUNDLE),
WIDGET_TYPE(EntityType.WIDGET_TYPE),
OAUTH2_CONFIGURATION_INFO(),
OAUTH2_CONFIGURATION_TEMPLATE(),
TENANT_PROFILE(EntityType.TENANT_PROFILE),
DEVICE_PROFILE(EntityType.DEVICE_PROFILE);
DEVICE_PROFILE(EntityType.DEVICE_PROFILE),
API_USAGE_STATE(EntityType.API_USAGE_STATE);
private final EntityType entityType;

6
application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java

@ -19,14 +19,10 @@ import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.User;
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.security.Authority;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.HashMap;
import java.util.Optional;
@Component(value="sysAdminPermissions")
public class SysAdminPermissions extends AbstractPermissions {
@ -39,6 +35,8 @@ public class SysAdminPermissions extends AbstractPermissions {
put(Resource.USER, userPermissionChecker);
put(Resource.WIDGETS_BUNDLE, systemEntityPermissionChecker);
put(Resource.WIDGET_TYPE, systemEntityPermissionChecker);
put(Resource.OAUTH2_CONFIGURATION_INFO, PermissionChecker.allowAllPermissionChecker);
put(Resource.OAUTH2_CONFIGURATION_TEMPLATE, PermissionChecker.allowAllPermissionChecker);
put(Resource.TENANT_PROFILE, PermissionChecker.allowAllPermissionChecker);
}

4
application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java

@ -19,13 +19,10 @@ import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.User;
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.security.Authority;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.HashMap;
@Component(value="tenantAdminPermissions")
public class TenantAdminPermissions extends AbstractPermissions {
@ -43,6 +40,7 @@ public class TenantAdminPermissions extends AbstractPermissions {
put(Resource.WIDGETS_BUNDLE, widgetsPermissionChecker);
put(Resource.WIDGET_TYPE, widgetsPermissionChecker);
put(Resource.DEVICE_PROFILE, tenantEntityPermissionChecker);
put(Resource.API_USAGE_STATE, tenantEntityPermissionChecker);
put(Resource.EDGE, tenantEntityPermissionChecker);
}

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

@ -40,17 +40,20 @@ import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.user.UserServiceImpl;
import org.thingsboard.server.service.security.exception.UserPasswordExpiredException;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.utils.MiscUtils;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@ -146,7 +149,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
if (isPositiveInteger(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays())) {
if ((userCredentials.getCreatedTime()
+ TimeUnit.DAYS.toMillis(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays()))
< System.currentTimeMillis()) {
< System.currentTimeMillis()) {
userCredentials = userService.requestExpiredPasswordReset(tenantId, userCredentials.getId());
throw new UserPasswordExpiredException("User password expired!", userCredentials.getResetToken());
}
@ -197,6 +200,24 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
}
}
@Override
public String getBaseUrl(TenantId tenantId, CustomerId customerId, HttpServletRequest httpServletRequest) {
String baseUrl = null;
AdminSettings generalSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "general");
JsonNode prohibitDifferentUrl = generalSettings.getJsonValue().get("prohibitDifferentUrl");
if (prohibitDifferentUrl != null && prohibitDifferentUrl.asBoolean()) {
baseUrl = generalSettings.getJsonValue().get("baseUrl").asText();
}
if (StringUtils.isEmpty(baseUrl)) {
baseUrl = MiscUtils.constructBaseUrl(httpServletRequest);
}
return baseUrl;
}
private static boolean isPositiveInteger(Integer val) {
return val != null && val.intValue() > 0;
}

5
application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java

@ -16,11 +16,14 @@
package org.thingsboard.server.service.security.system;
import org.springframework.security.core.AuthenticationException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import javax.servlet.http.HttpServletRequest;
public interface SystemSecurityService {
SecuritySettings getSecuritySettings(TenantId tenantId);
@ -31,4 +34,6 @@ public interface SystemSecurityService {
void validatePassword(TenantId tenantId, String password, UserCredentials userCredentials) throws DataValidationException;
String getBaseUrl(TenantId tenantId, CustomerId customerId, HttpServletRequest httpServletRequest);
}

53
application/src/main/java/org/thingsboard/server/service/sms/AbstractSmsSender.java

@ -0,0 +1,53 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.sms;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.sms.SmsSender;
import org.thingsboard.rule.engine.api.sms.exception.SmsParseException;
import java.util.regex.Pattern;
@Slf4j
public abstract class AbstractSmsSender implements SmsSender {
private static final Pattern E_164_PHONE_NUMBER_PATTERN = Pattern.compile("^\\+[1-9]\\d{1,14}$");
private static final int MAX_SMS_MESSAGE_LENGTH = 1600;
private static final int MAX_SMS_SEGMENT_LENGTH = 70;
protected String validatePhoneNumber(String phoneNumber) throws SmsParseException {
phoneNumber = phoneNumber.trim();
if (!E_164_PHONE_NUMBER_PATTERN.matcher(phoneNumber).matches()) {
throw new SmsParseException("Invalid phone number format. Phone number must be in E.164 format.");
}
return phoneNumber;
}
protected String prepareMessage(String message) {
message = message.replaceAll("^\"|\"$", "").replaceAll("\\\\n", "\n");
if (message.length() > MAX_SMS_MESSAGE_LENGTH) {
log.warn("SMS message exceeds maximum symbols length and will be truncated");
message = message.substring(0, MAX_SMS_MESSAGE_LENGTH);
}
return message;
}
protected int countMessageSegments(String message) {
return (int)Math.ceil((double) message.length() / (double) MAX_SMS_SEGMENT_LENGTH);
}
}

42
application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsSenderFactory.java

@ -0,0 +1,42 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.sms;
import org.springframework.stereotype.Component;
import org.thingsboard.rule.engine.api.sms.SmsSender;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
import org.thingsboard.server.common.data.sms.config.AwsSnsSmsProviderConfiguration;
import org.thingsboard.server.common.data.sms.config.SmsProviderConfiguration;
import org.thingsboard.server.common.data.sms.config.TwilioSmsProviderConfiguration;
import org.thingsboard.server.service.sms.aws.AwsSmsSender;
import org.thingsboard.server.service.sms.twilio.TwilioSmsSender;
@Component
public class DefaultSmsSenderFactory implements SmsSenderFactory {
@Override
public SmsSender createSmsSender(SmsProviderConfiguration config) {
switch (config.getType()) {
case AWS_SNS:
return new AwsSmsSender((AwsSnsSmsProviderConfiguration)config);
case TWILIO:
return new TwilioSmsSender((TwilioSmsProviderConfiguration)config);
default:
throw new RuntimeException("Unknown SMS provider type " + config.getType());
}
}
}

145
application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java

@ -0,0 +1,145 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.sms;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.NestedRuntimeException;
import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.sms.SmsSender;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
import org.thingsboard.server.common.data.sms.config.SmsProviderConfiguration;
import org.thingsboard.server.common.data.sms.config.TestSmsRequest;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.util.mapping.JacksonUtil;
import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
@Service
@Slf4j
public class DefaultSmsService implements SmsService {
private final SmsSenderFactory smsSenderFactory;
private final AdminSettingsService adminSettingsService;
private final TbApiUsageStateService apiUsageStateService;
private final TbApiUsageClient apiUsageClient;
private SmsSender smsSender;
public DefaultSmsService(SmsSenderFactory smsSenderFactory, AdminSettingsService adminSettingsService, TbApiUsageStateService apiUsageStateService, TbApiUsageClient apiUsageClient) {
this.smsSenderFactory = smsSenderFactory;
this.adminSettingsService = adminSettingsService;
this.apiUsageStateService = apiUsageStateService;
this.apiUsageClient = apiUsageClient;
}
@PostConstruct
private void init() {
updateSmsConfiguration();
}
@PreDestroy
private void destroy() {
if (this.smsSender != null) {
this.smsSender.destroy();
}
}
@Override
public void updateSmsConfiguration() {
AdminSettings settings = adminSettingsService.findAdminSettingsByKey(new TenantId(EntityId.NULL_UUID), "sms");
if (settings != null) {
try {
JsonNode jsonConfig = settings.getJsonValue();
SmsProviderConfiguration configuration = JacksonUtil.convertValue(jsonConfig, SmsProviderConfiguration.class);
SmsSender newSmsSender = this.smsSenderFactory.createSmsSender(configuration);
if (this.smsSender != null) {
this.smsSender.destroy();
}
this.smsSender = newSmsSender;
} catch (Exception e) {
log.error("Failed to create SMS sender", e);
}
}
}
private int sendSms(String numberTo, String message) throws ThingsboardException {
if (this.smsSender == null) {
throw new ThingsboardException("Unable to send SMS: no SMS provider configured!", ThingsboardErrorCode.GENERAL);
}
return this.sendSms(this.smsSender, numberTo, message);
}
@Override
public void sendSms(TenantId tenantId, String[] numbersTo, String message) throws ThingsboardException {
if (apiUsageStateService.getApiUsageState(tenantId).isSmsSendEnabled()) {
int smsCount = 0;
try {
for (String numberTo : numbersTo) {
smsCount += this.sendSms(numberTo, message);
}
} finally {
if (smsCount > 0) {
apiUsageClient.report(tenantId, ApiUsageRecordKey.SMS_EXEC_COUNT, smsCount);
}
}
} else {
throw new RuntimeException("SMS sending is disabled due to API limits!");
}
}
@Override
public void sendTestSms(TestSmsRequest testSmsRequest) throws ThingsboardException {
SmsSender testSmsSender;
try {
testSmsSender = this.smsSenderFactory.createSmsSender(testSmsRequest.getProviderConfiguration());
} catch (Exception e) {
throw handleException(e);
}
this.sendSms(testSmsSender, testSmsRequest.getNumberTo(), testSmsRequest.getMessage());
testSmsSender.destroy();
}
private int sendSms(SmsSender smsSender, String numberTo, String message) throws ThingsboardException {
try {
return smsSender.sendSms(numberTo, message);
} catch (Exception e) {
throw handleException(e);
}
}
private ThingsboardException handleException(Exception exception) {
String message;
if (exception instanceof NestedRuntimeException) {
message = ((NestedRuntimeException) exception).getMostSpecificCause().getMessage();
} else {
message = exception.getMessage();
}
log.warn("Unable to send SMS: {}", message);
return new ThingsboardException(String.format("Unable to send SMS: %s", message),
ThingsboardErrorCode.GENERAL);
}
}

33
application/src/main/java/org/thingsboard/server/service/sms/SmsExecutorService.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.sms;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.AbstractListeningExecutor;
@Component
public class SmsExecutorService extends AbstractListeningExecutor {
@Value("${actors.rule.sms_thread_pool_size}")
private int smsExecutorThreadPoolSize;
@Override
protected int getThreadPollSize() {
return smsExecutorThreadPoolSize;
}
}

73
application/src/main/java/org/thingsboard/server/service/sms/aws/AwsSmsSender.java

@ -0,0 +1,73 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.sms.aws;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.sns.AmazonSNS;
import com.amazonaws.services.sns.AmazonSNSClient;
import com.amazonaws.services.sns.model.PublishRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.sms.config.AwsSnsSmsProviderConfiguration;
import org.thingsboard.rule.engine.api.sms.exception.SmsException;
import org.thingsboard.rule.engine.api.sms.exception.SmsSendException;
import org.thingsboard.server.service.sms.AbstractSmsSender;
@Slf4j
public class AwsSmsSender extends AbstractSmsSender {
private AmazonSNS snsClient;
public AwsSmsSender(AwsSnsSmsProviderConfiguration config) {
if (StringUtils.isEmpty(config.getAccessKeyId()) || StringUtils.isEmpty(config.getSecretAccessKey()) || StringUtils.isEmpty(config.getRegion())) {
throw new IllegalArgumentException("Invalid AWS sms provider configuration: aws accessKeyId, aws secretAccessKey and aws region should be specified!");
}
AWSCredentials awsCredentials = new BasicAWSCredentials(config.getAccessKeyId(), config.getSecretAccessKey());
AWSStaticCredentialsProvider credProvider = new AWSStaticCredentialsProvider(awsCredentials);
this.snsClient = AmazonSNSClient.builder()
.withCredentials(credProvider)
.withRegion(config.getRegion())
.build();
}
@Override
public int sendSms(String numberTo, String message) throws SmsException {
numberTo = this.validatePhoneNumber(numberTo);
message = this.prepareMessage(message);
try {
PublishRequest publishRequest = new PublishRequest()
.withPhoneNumber(numberTo)
.withMessage(message);
this.snsClient.publish(publishRequest);
return this.countMessageSegments(message);
} catch (Exception e) {
throw new SmsSendException("Failed to send SMS message - " + e.getMessage(), e);
}
}
@Override
public void destroy() {
if (this.snsClient != null) {
try {
this.snsClient.shutdown();
} catch (Exception e) {
log.error("Failed to shutdown SNS client during destroy()", e);
}
}
}
}

56
application/src/main/java/org/thingsboard/server/service/sms/twilio/TwilioSmsSender.java

@ -0,0 +1,56 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.sms.twilio;
import com.twilio.http.TwilioRestClient;
import com.twilio.rest.api.v2010.account.Message;
import com.twilio.type.PhoneNumber;
import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.sms.config.TwilioSmsProviderConfiguration;
import org.thingsboard.rule.engine.api.sms.exception.SmsException;
import org.thingsboard.rule.engine.api.sms.exception.SmsSendException;
import org.thingsboard.server.service.sms.AbstractSmsSender;
public class TwilioSmsSender extends AbstractSmsSender {
private TwilioRestClient twilioRestClient;
private String numberFrom;
public TwilioSmsSender(TwilioSmsProviderConfiguration config) {
if (StringUtils.isEmpty(config.getAccountSid()) || StringUtils.isEmpty(config.getAccountToken()) || StringUtils.isEmpty(config.getNumberFrom())) {
throw new IllegalArgumentException("Invalid twilio sms provider configuration: accountSid, accountToken and numberFrom should be specified!");
}
this.numberFrom = this.validatePhoneNumber(config.getNumberFrom());
this.twilioRestClient = new TwilioRestClient.Builder(config.getAccountSid(), config.getAccountToken()).build();
}
@Override
public int sendSms(String numberTo, String message) throws SmsException {
numberTo = this.validatePhoneNumber(numberTo);
message = this.prepareMessage(message);
try {
String numSegments = Message.creator(new PhoneNumber(numberTo), new PhoneNumber(this.numberFrom), message).create(this.twilioRestClient).getNumSegments();
return Integer.valueOf(numSegments);
} catch (Exception e) {
throw new SmsSendException("Failed to send SMS message - " + e.getMessage(), e);
}
}
@Override
public void destroy() {
}
}

19
application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java

@ -392,7 +392,7 @@ public class DefaultDeviceStateService implements DeviceStateService {
if (stateData != null) {
DeviceState state = stateData.getState();
state.setActive(ts < state.getLastActivityTime() + state.getInactivityTimeout());
if (!state.isActive() && (state.getLastInactivityAlarmTime() == 0L || state.getLastInactivityAlarmTime() < state.getLastActivityTime())) {
if (!state.isActive() && (state.getLastInactivityAlarmTime() == 0L || state.getLastInactivityAlarmTime() < state.getLastActivityTime()) && stateData.getDeviceCreationTime() + state.getInactivityTimeout() < ts) {
state.setLastInactivityAlarmTime(ts);
pushRuleEngineMessage(stateData, INACTIVITY_EVENT);
save(deviceId, INACTIVITY_ALARM_TIME, ts);
@ -479,6 +479,7 @@ public class DefaultDeviceStateService implements DeviceStateService {
return DeviceStateData.builder()
.tenantId(device.getTenantId())
.deviceId(device.getId())
.deviceCreationTime(device.getCreatedTime())
.metaData(md)
.state(deviceState).build();
} catch (Exception e) {
@ -520,27 +521,27 @@ public class DefaultDeviceStateService implements DeviceStateService {
private void save(DeviceId deviceId, String key, long value) {
if (persistToTelemetry) {
tsSubService.saveAndNotify(
tsSubService.saveAndNotifyInternal(
TenantId.SYS_TENANT_ID, deviceId,
Collections.singletonList(new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry(key, value))),
new AttributeSaveCallback(deviceId, key, value));
new AttributeSaveCallback<>(deviceId, key, value));
} else {
tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, deviceId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback(deviceId, key, value));
tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, deviceId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback<>(deviceId, key, value));
}
}
private void save(DeviceId deviceId, String key, boolean value) {
if (persistToTelemetry) {
tsSubService.saveAndNotify(
tsSubService.saveAndNotifyInternal(
TenantId.SYS_TENANT_ID, deviceId,
Collections.singletonList(new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry(key, value))),
new AttributeSaveCallback(deviceId, key, value));
new AttributeSaveCallback<>(deviceId, key, value));
} else {
tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, deviceId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback(deviceId, key, value));
tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, deviceId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback<>(deviceId, key, value));
}
}
private static class AttributeSaveCallback implements FutureCallback<Void> {
private static class AttributeSaveCallback<T> implements FutureCallback<T> {
private final DeviceId deviceId;
private final String key;
private final Object value;
@ -552,7 +553,7 @@ public class DefaultDeviceStateService implements DeviceStateService {
}
@Override
public void onSuccess(@Nullable Void result) {
public void onSuccess(@Nullable T result) {
log.trace("[{}] Successfully updated attribute [{}] with value [{}]", deviceId, key, value);
}

4
application/src/main/java/org/thingsboard/server/service/state/DeviceStateData.java

@ -30,8 +30,8 @@ class DeviceStateData {
private final TenantId tenantId;
private final DeviceId deviceId;
private final long deviceCreationTime;
private TbMsgMetaData metaData;
private final DeviceState state;
}

8
application/src/main/java/org/thingsboard/server/service/stats/DefaultRuleEngineStatisticsService.java

@ -48,9 +48,9 @@ import java.util.stream.Collectors;
public class DefaultRuleEngineStatisticsService implements RuleEngineStatisticsService {
public static final String TB_SERVICE_QUEUE = "TbServiceQueue";
public static final FutureCallback<Void> CALLBACK = new FutureCallback<Void>() {
public static final FutureCallback<Integer> CALLBACK = new FutureCallback<Integer>() {
@Override
public void onSuccess(@Nullable Void result) {
public void onSuccess(@Nullable Integer result) {
}
@ -85,7 +85,7 @@ public class DefaultRuleEngineStatisticsService implements RuleEngineStatisticsS
.map(kv -> new BasicTsKvEntry(ts, new LongDataEntry(kv.getKey(), (long) kv.getValue().get())))
.collect(Collectors.toList());
if (!tsList.isEmpty()) {
tsService.saveAndNotify(tenantId, serviceAssetId, tsList, CALLBACK);
tsService.saveAndNotifyInternal(tenantId, serviceAssetId, tsList, CALLBACK);
}
}
} catch (DataValidationException e) {
@ -97,7 +97,7 @@ public class DefaultRuleEngineStatisticsService implements RuleEngineStatisticsS
ruleEngineStats.getTenantExceptions().forEach((tenantId, e) -> {
TsKvEntry tsKv = new BasicTsKvEntry(ts, new JsonDataEntry("ruleEngineException", e.toJsonString()));
try {
tsService.saveAndNotify(tenantId, getServiceAssetId(tenantId, queueName), Collections.singletonList(tsKv), CALLBACK);
tsService.saveAndNotifyInternal(tenantId, getServiceAssetId(tenantId, queueName), Collections.singletonList(tsKv), CALLBACK);
} catch (DataValidationException e2) {
if (!e2.getMessage().equalsIgnoreCase("Asset is referencing to non-existent tenant!")) {
throw e2;

25
application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java

@ -21,12 +21,6 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.discovery.ClusterTopologyChangeEvent;
import org.thingsboard.server.queue.discovery.PartitionChangeEvent;
@ -36,7 +30,6 @@ import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.queue.TbClusterService;
import org.thingsboard.server.service.telemetry.DefaultTelemetryWebSocketService;
import org.thingsboard.server.service.telemetry.sub.AlarmSubscriptionUpdate;
import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate;
@ -49,7 +42,6 @@ import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
@Slf4j
@TbCoreComponent
@ -59,9 +51,6 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
private final Set<TopicPartitionInfo> currentPartitions = ConcurrentHashMap.newKeySet();
private final Map<String, Map<Integer, TbSubscription>> subscriptionsBySessionId = new ConcurrentHashMap<>();
@Autowired
private EntityViewService entityViewService;
@Autowired
private PartitionService partitionService;
@ -72,17 +61,17 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
@Lazy
private SubscriptionManagerService subscriptionManagerService;
private ExecutorService wsCallBackExecutor;
private ExecutorService subscriptionUpdateExecutor;
@PostConstruct
public void initExecutor() {
wsCallBackExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("ws-sub-callback"));
subscriptionUpdateExecutor = Executors.newWorkStealingPool(20);
}
@PreDestroy
public void shutdownExecutor() {
if (wsCallBackExecutor != null) {
wsCallBackExecutor.shutdownNow();
if (subscriptionUpdateExecutor != null) {
subscriptionUpdateExecutor.shutdownNow();
}
}
@ -108,7 +97,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
* Since number of subscriptions is usually much less then number of devices that are pushing data.
*/
subscriptionsBySessionId.values().forEach(map -> map.values()
.forEach(sub -> pushSubscriptionToManagerService(sub, false)));
.forEach(sub -> pushSubscriptionToManagerService(sub, true)));
}
}
@ -148,7 +137,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
update.getLatestValues().forEach((key, value) -> attrSub.getKeyStates().put(key, value));
break;
}
subscription.getUpdateConsumer().accept(sessionId, update);
subscriptionUpdateExecutor.submit(() -> subscription.getUpdateConsumer().accept(sessionId, update));
}
callback.onSuccess();
}
@ -158,7 +147,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
TbSubscription subscription = subscriptionsBySessionId
.getOrDefault(sessionId, Collections.emptyMap()).get(update.getSubscriptionId());
if (subscription != null && subscription.getType() == TbSubscriptionType.ALARMS) {
subscription.getUpdateConsumer().accept(sessionId, update);
subscriptionUpdateExecutor.submit(() -> subscription.getUpdateConsumer().accept(sessionId, update));
}
callback.onSuccess();
}

6
application/src/main/java/org/thingsboard/server/service/telemetry/AbstractSubscriptionService.java

@ -105,10 +105,10 @@ public abstract class AbstractSubscriptionService implements ApplicationListener
}
}
protected void addWsCallback(ListenableFuture<List<Void>> saveFuture, Consumer<Void> callback) {
Futures.addCallback(saveFuture, new FutureCallback<List<Void>>() {
protected <T> void addWsCallback(ListenableFuture<T> saveFuture, Consumer<T> callback) {
Futures.addCallback(saveFuture, new FutureCallback<T>() {
@Override
public void onSuccess(@Nullable List<Void> result) {
public void onSuccess(@Nullable T result) {
callback.accept(null);
}

112
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java

@ -20,12 +20,12 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
@ -35,17 +35,19 @@ import org.thingsboard.server.common.data.kv.DoubleDataEntry;
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.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.discovery.PartitionChangeEvent;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.queue.TbClusterService;
import org.thingsboard.server.service.subscription.SubscriptionManagerService;
import org.thingsboard.server.service.subscription.TbSubscriptionUtils;
import javax.annotation.Nullable;
@ -59,12 +61,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
import java.util.stream.Collectors;
/**
* Created by ashvayka on 27.03.18.
@ -76,6 +74,8 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
private final AttributesService attrService;
private final TimeseriesService tsService;
private final EntityViewService entityViewService;
private final TbApiUsageClient apiUsageClient;
private final TbApiUsageStateService apiUsageStateService;
private ExecutorService tsCallBackExecutor;
@ -83,11 +83,15 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
TimeseriesService tsService,
EntityViewService entityViewService,
TbClusterService clusterService,
PartitionService partitionService) {
PartitionService partitionService,
TbApiUsageClient apiUsageClient,
TbApiUsageStateService apiUsageStateService) {
super(clusterService, partitionService);
this.attrService = attrService;
this.tsService = tsService;
this.entityViewService = entityViewService;
this.apiUsageClient = apiUsageClient;
this.apiUsageStateService = apiUsageStateService;
}
@PostConstruct
@ -116,7 +120,36 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
@Override
public void saveAndNotify(TenantId tenantId, EntityId entityId, List<TsKvEntry> ts, long ttl, FutureCallback<Void> callback) {
ListenableFuture<List<Void>> saveFuture = tsService.save(tenantId, entityId, ts, ttl);
checkInternalEntity(entityId);
boolean sysTenant = TenantId.SYS_TENANT_ID.equals(tenantId) || tenantId == null;
if (sysTenant || apiUsageStateService.getApiUsageState(tenantId).isDbStorageEnabled()) {
saveAndNotifyInternal(tenantId, entityId, ts, ttl, new FutureCallback<Integer>() {
@Override
public void onSuccess(Integer result) {
if (!sysTenant && result != null && result > 0) {
apiUsageClient.report(tenantId, ApiUsageRecordKey.STORAGE_DP_COUNT, result);
}
callback.onSuccess(null);
}
@Override
public void onFailure(Throwable t) {
callback.onFailure(t);
}
});
} else {
callback.onFailure(new RuntimeException("DB storage writes are disabled due to API limits!"));
}
}
@Override
public void saveAndNotifyInternal(TenantId tenantId, EntityId entityId, List<TsKvEntry> ts, FutureCallback<Integer> callback) {
saveAndNotifyInternal(tenantId, entityId, ts, 0L, callback);
}
@Override
public void saveAndNotifyInternal(TenantId tenantId, EntityId entityId, List<TsKvEntry> ts, long ttl, FutureCallback<Integer> callback) {
ListenableFuture<Integer> saveFuture = tsService.save(tenantId, entityId, ts, ttl);
addMainCallback(saveFuture, callback);
addWsCallback(saveFuture, success -> onTimeSeriesUpdate(tenantId, entityId, ts));
if (EntityType.DEVICE.equals(entityId.getEntityType()) || EntityType.ASSET.equals(entityId.getEntityType())) {
@ -141,9 +174,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
Optional<TsKvEntry> tsKvEntry = entries.stream()
.filter(entry -> entry.getTs() > startTs && entry.getTs() <= endTs)
.max(Comparator.comparingLong(TsKvEntry::getTs));
if (tsKvEntry.isPresent()) {
entityViewLatest.add(tsKvEntry.get());
}
tsKvEntry.ifPresent(entityViewLatest::add);
}
}
if (!entityViewLatest.isEmpty()) {
@ -176,29 +207,53 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
@Override
public void saveAndNotify(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes, boolean notifyDevice, FutureCallback<Void> callback) {
checkInternalEntity(entityId);
saveAndNotifyInternal(tenantId, entityId, scope, attributes, notifyDevice, callback);
}
@Override
public void saveAndNotifyInternal(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes, boolean notifyDevice, FutureCallback<Void> callback) {
ListenableFuture<List<Void>> saveFuture = attrService.save(tenantId, entityId, scope, attributes);
addMainCallback(saveFuture, callback);
addVoidCallback(saveFuture, callback);
addWsCallback(saveFuture, success -> onAttributesUpdate(tenantId, entityId, scope, attributes, notifyDevice));
}
@Override
public void saveLatestAndNotify(TenantId tenantId, EntityId entityId, List<TsKvEntry> ts, FutureCallback<Void> callback) {
checkInternalEntity(entityId);
saveLatestAndNotifyInternal(tenantId, entityId, ts, callback);
}
@Override
public void saveLatestAndNotifyInternal(TenantId tenantId, EntityId entityId, List<TsKvEntry> ts, FutureCallback<Void> callback) {
ListenableFuture<List<Void>> saveFuture = tsService.saveLatest(tenantId, entityId, ts);
addMainCallback(saveFuture, callback);
addVoidCallback(saveFuture, callback);
addWsCallback(saveFuture, success -> onTimeSeriesUpdate(tenantId, entityId, ts));
}
@Override
public void deleteAndNotify(TenantId tenantId, EntityId entityId, String scope, List<String> keys, FutureCallback<Void> callback) {
checkInternalEntity(entityId);
deleteAndNotifyInternal(tenantId, entityId, scope, keys, callback);
}
@Override
public void deleteAndNotifyInternal(TenantId tenantId, EntityId entityId, String scope, List<String> keys, FutureCallback<Void> callback) {
ListenableFuture<List<Void>> deleteFuture = attrService.removeAll(tenantId, entityId, scope, keys);
addMainCallback(deleteFuture, callback);
addVoidCallback(deleteFuture, callback);
addWsCallback(deleteFuture, success -> onAttributesDelete(tenantId, entityId, scope, keys));
}
@Override
public void deleteLatest(TenantId tenantId, EntityId entityId, List<String> keys, FutureCallback<Void> callback) {
checkInternalEntity(entityId);
deleteLatestInternal(tenantId, entityId, keys, callback);
}
@Override
public void deleteLatestInternal(TenantId tenantId, EntityId entityId, List<String> keys, FutureCallback<Void> callback) {
ListenableFuture<List<Void>> deleteFuture = tsService.removeLatest(tenantId, entityId, keys);
addMainCallback(deleteFuture, callback);
addVoidCallback(deleteFuture, callback);
}
@Override
@ -283,7 +338,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
}
}
private <S, R> void addMainCallback(ListenableFuture<S> saveFuture, final FutureCallback<R> callback) {
private <S> void addVoidCallback(ListenableFuture<S> saveFuture, final FutureCallback<Void> callback) {
Futures.addCallback(saveFuture, new FutureCallback<S>() {
@Override
public void onSuccess(@Nullable S result) {
@ -296,4 +351,25 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
}
}, tsCallBackExecutor);
}
private <S> void addMainCallback(ListenableFuture<S> saveFuture, final FutureCallback<S> callback) {
Futures.addCallback(saveFuture, new FutureCallback<S>() {
@Override
public void onSuccess(@Nullable S result) {
callback.onSuccess(result);
}
@Override
public void onFailure(Throwable t) {
callback.onFailure(t);
}
}, tsCallBackExecutor);
}
private void checkInternalEntity(EntityId entityId) {
if (EntityType.API_USAGE_STATE.equals(entityId.getEntityType())) {
throw new RuntimeException("Can't update API Usage State!");
}
}
}

46
application/src/main/java/org/thingsboard/server/service/telemetry/InternalTelemetryService.java

@ -0,0 +1,46 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.telemetry;
import com.google.common.util.concurrent.FutureCallback;
import org.thingsboard.rule.engine.api.RuleEngineTelemetryService;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import java.util.List;
/**
* Created by ashvayka on 27.03.18.
*/
public interface InternalTelemetryService extends RuleEngineTelemetryService {
void saveAndNotifyInternal(TenantId tenantId, EntityId entityId, List<TsKvEntry> ts, FutureCallback<Integer> callback);
void saveAndNotifyInternal(TenantId tenantId, EntityId entityId, List<TsKvEntry> ts, long ttl, FutureCallback<Integer> callback);
void saveAndNotifyInternal(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes, boolean notifyDevice, FutureCallback<Void> callback);
void saveLatestAndNotifyInternal(TenantId tenantId, EntityId entityId, List<TsKvEntry> ts, FutureCallback<Void> callback);
void deleteAndNotifyInternal(TenantId tenantId, EntityId entityId, String scope, List<String> keys, FutureCallback<Void> callback);
void deleteLatestInternal(TenantId tenantId, EntityId entityId, List<String> keys, FutureCallback<Void> callback);
}

2
application/src/main/java/org/thingsboard/server/service/telemetry/TelemetrySubscriptionService.java

@ -22,6 +22,6 @@ import org.thingsboard.server.queue.discovery.PartitionChangeEvent;
/**
* Created by ashvayka on 27.03.18.
*/
public interface TelemetrySubscriptionService extends RuleEngineTelemetryService, ApplicationListener<PartitionChangeEvent> {
public interface TelemetrySubscriptionService extends InternalTelemetryService, ApplicationListener<PartitionChangeEvent> {
}

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

@ -23,14 +23,17 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.protobuf.ByteString;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
import org.thingsboard.server.common.data.device.credentials.ProvisionDeviceCredentialsData;
import org.thingsboard.server.common.data.device.profile.ProvisionDeviceProfileCredentials;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
@ -44,18 +47,23 @@ import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.transport.util.DataDecodingEncodingService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceProvisionService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.device.provision.ProvisionRequest;
import org.thingsboard.server.dao.device.provision.ProvisionResponse;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.util.mapping.JacksonUtil;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.DeviceCredentialsProto;
import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto;
import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.GetEntityProfileRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.GetEntityProfileResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsgOrBuilder;
import org.thingsboard.server.gen.transport.TransportProtos.ProvisionResponseStatus;
import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg;
@ -63,7 +71,11 @@ import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenR
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.dao.device.provision.ProvisionFailedException;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.service.queue.TbClusterService;
import org.thingsboard.server.service.state.DeviceStateService;
@ -83,10 +95,9 @@ public class DefaultTransportApiService implements TransportApiService {
private static final ObjectMapper mapper = new ObjectMapper();
//TODO: Constructor dependencies;
private final DeviceProfileService deviceProfileService;
private final TenantService tenantService;
private final TenantProfileService tenantProfileService;
private final TbDeviceProfileCache deviceProfileCache;
private final TbTenantProfileCache tenantProfileCache;
private final TbApiUsageStateService apiUsageStateService;
private final DeviceService deviceService;
private final RelationService relationService;
private final DeviceCredentialsService deviceCredentialsService;
@ -94,18 +105,19 @@ public class DefaultTransportApiService implements TransportApiService {
private final DbCallbackExecutorService dbCallbackExecutorService;
private final TbClusterService tbClusterService;
private final DataDecodingEncodingService dataDecodingEncodingService;
private final DeviceProvisionService deviceProvisionService;
private final ConcurrentMap<String, ReentrantLock> deviceCreationLocks = new ConcurrentHashMap<>();
public DefaultTransportApiService(DeviceProfileService deviceProfileService, TenantService tenantService,
TenantProfileService tenantProfileService, DeviceService deviceService,
public DefaultTransportApiService(TbDeviceProfileCache deviceProfileCache,
TbTenantProfileCache tenantProfileCache, TbApiUsageStateService apiUsageStateService, DeviceService deviceService,
RelationService relationService, DeviceCredentialsService deviceCredentialsService,
DeviceStateService deviceStateService, DbCallbackExecutorService dbCallbackExecutorService,
TbClusterService tbClusterService, DataDecodingEncodingService dataDecodingEncodingService) {
this.deviceProfileService = deviceProfileService;
this.tenantService = tenantService;
this.tenantProfileService = tenantProfileService;
TbClusterService tbClusterService, DataDecodingEncodingService dataDecodingEncodingService,
DeviceProvisionService deviceProvisionService) {
this.deviceProfileCache = deviceProfileCache;
this.tenantProfileCache = tenantProfileCache;
this.apiUsageStateService = apiUsageStateService;
this.deviceService = deviceService;
this.relationService = relationService;
this.deviceCredentialsService = deviceCredentialsService;
@ -113,6 +125,7 @@ public class DefaultTransportApiService implements TransportApiService {
this.dbCallbackExecutorService = dbCallbackExecutorService;
this.tbClusterService = tbClusterService;
this.dataDecodingEncodingService = dataDecodingEncodingService;
this.deviceProvisionService = deviceProvisionService;
}
@Override
@ -133,11 +146,11 @@ public class DefaultTransportApiService implements TransportApiService {
} else if (transportApiRequestMsg.hasGetOrCreateDeviceRequestMsg()) {
return Futures.transform(handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg()),
value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor());
} else if (transportApiRequestMsg.hasGetTenantRoutingInfoRequestMsg()) {
return Futures.transform(handle(transportApiRequestMsg.getGetTenantRoutingInfoRequestMsg()),
} else if (transportApiRequestMsg.hasEntityProfileRequestMsg()) {
return Futures.transform(handle(transportApiRequestMsg.getEntityProfileRequestMsg()),
value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor());
} else if (transportApiRequestMsg.hasGetDeviceProfileRequestMsg()) {
return Futures.transform(handle(transportApiRequestMsg.getGetDeviceProfileRequestMsg()),
} else if (transportApiRequestMsg.hasProvisionDeviceRequestMsg()) {
return Futures.transform(handle(transportApiRequestMsg.getProvisionDeviceRequestMsg()),
value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor());
}
return Futures.transform(getEmptyTransportApiResponseFuture(),
@ -155,22 +168,27 @@ public class DefaultTransportApiService implements TransportApiService {
}
private ListenableFuture<TransportApiResponseMsg> validateCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg mqtt) {
DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(mqtt.getUserName());
if (credentials != null) {
if (credentials.getCredentialsType() == DeviceCredentialsType.ACCESS_TOKEN) {
return getDeviceInfo(credentials.getDeviceId(), credentials);
} else if (credentials.getCredentialsType() == DeviceCredentialsType.MQTT_BASIC) {
if (!checkMqttCredentials(mqtt, credentials)) {
credentials = null;
DeviceCredentials credentials = null;
if (!StringUtils.isEmpty(mqtt.getUserName())) {
credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(mqtt.getUserName());
if (credentials != null) {
if (credentials.getCredentialsType() == DeviceCredentialsType.ACCESS_TOKEN) {
return getDeviceInfo(credentials.getDeviceId(), credentials);
} else if (credentials.getCredentialsType() == DeviceCredentialsType.MQTT_BASIC) {
if (!checkMqttCredentials(mqtt, credentials)) {
credentials = null;
}
} else {
return getEmptyTransportApiResponseFuture();
}
}
}
if (credentials == null) {
credentials = checkMqttCredentials(mqtt, EncryptionUtil.getSha3Hash("|", mqtt.getClientId(), mqtt.getUserName()));
if (credentials == null) {
credentials = checkMqttCredentials(mqtt, EncryptionUtil.getSha3Hash(mqtt.getClientId()));
credentials = checkMqttCredentials(mqtt, EncryptionUtil.getSha3Hash("|", mqtt.getClientId(), mqtt.getUserName()));
}
}
if (credentials == null) {
credentials = checkMqttCredentials(mqtt, EncryptionUtil.getSha3Hash(mqtt.getClientId()));
}
if (credentials != null) {
return getDeviceInfo(credentials.getDeviceId(), credentials);
} else {
@ -225,7 +243,7 @@ public class DefaultTransportApiService implements TransportApiService {
device.setName(requestMsg.getDeviceName());
device.setType(requestMsg.getDeviceType());
device.setCustomerId(gateway.getCustomerId());
DeviceProfile deviceProfile = deviceProfileService.findOrCreateDeviceProfile(gateway.getTenantId(), requestMsg.getDeviceType());
DeviceProfile deviceProfile = deviceProfileCache.findOrCreateDeviceProfile(gateway.getTenantId(), requestMsg.getDeviceType());
device.setDeviceProfileId(deviceProfile.getId());
device = deviceService.saveDevice(device);
relationService.saveRelationAsync(TenantId.SYS_TENANT_ID, new EntityRelation(gateway.getId(), device.getId(), "Created"));
@ -245,7 +263,7 @@ public class DefaultTransportApiService implements TransportApiService {
}
GetOrCreateDeviceFromGatewayResponseMsg.Builder builder = GetOrCreateDeviceFromGatewayResponseMsg.newBuilder()
.setDeviceInfo(getDeviceInfoProto(device));
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(device.getTenantId(), device.getDeviceProfileId());
DeviceProfile deviceProfile = deviceProfileCache.get(device.getTenantId(), device.getDeviceProfileId());
if (deviceProfile != null) {
builder.setProfileBody(ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfile)));
} else {
@ -263,25 +281,70 @@ public class DefaultTransportApiService implements TransportApiService {
}, dbCallbackExecutorService);
}
private ListenableFuture<TransportApiResponseMsg> handle(GetTenantRoutingInfoRequestMsg requestMsg) {
TenantId tenantId = new TenantId(new UUID(requestMsg.getTenantIdMSB(), requestMsg.getTenantIdLSB()));
// TODO: Tenant Profile from cache
ListenableFuture<TenantProfile> tenantProfileFuture =
Futures.transform(tenantService.findTenantByIdAsync(TenantId.SYS_TENANT_ID, tenantId), tenant ->
tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, tenant.getTenantProfileId()), dbCallbackExecutorService);
return Futures.transform(tenantProfileFuture, tenantProfile -> TransportApiResponseMsg.newBuilder()
.setGetTenantRoutingInfoResponseMsg(GetTenantRoutingInfoResponseMsg.newBuilder().setIsolatedTbCore(tenantProfile.isIsolatedTbCore())
.setIsolatedTbRuleEngine(tenantProfile.isIsolatedTbRuleEngine()).build()).build(), dbCallbackExecutorService);
private ListenableFuture<TransportApiResponseMsg> handle(ProvisionDeviceRequestMsg requestMsg) {
ListenableFuture<ProvisionResponse> provisionResponseFuture = null;
try {
provisionResponseFuture = Futures.immediateFuture(deviceProvisionService.provisionDevice(
new ProvisionRequest(
requestMsg.getDeviceName(),
requestMsg.getCredentialsType() != null ? DeviceCredentialsType.valueOf(requestMsg.getCredentialsType().name()) : null,
new ProvisionDeviceCredentialsData(requestMsg.getCredentialsDataProto().getValidateDeviceTokenRequestMsg().getToken(),
requestMsg.getCredentialsDataProto().getValidateBasicMqttCredRequestMsg().getClientId(),
requestMsg.getCredentialsDataProto().getValidateBasicMqttCredRequestMsg().getUserName(),
requestMsg.getCredentialsDataProto().getValidateBasicMqttCredRequestMsg().getPassword(),
requestMsg.getCredentialsDataProto().getValidateDeviceX509CertRequestMsg().getHash()),
new ProvisionDeviceProfileCredentials(
requestMsg.getProvisionDeviceCredentialsMsg().getProvisionDeviceKey(),
requestMsg.getProvisionDeviceCredentialsMsg().getProvisionDeviceSecret()))));
} catch (ProvisionFailedException e) {
return Futures.immediateFuture(getTransportApiResponseMsg(
new DeviceCredentials(),
TransportProtos.ProvisionResponseStatus.valueOf(e.getMessage())));
}
return Futures.transform(provisionResponseFuture, provisionResponse -> getTransportApiResponseMsg(provisionResponse.getDeviceCredentials(), TransportProtos.ProvisionResponseStatus.SUCCESS),
dbCallbackExecutorService);
}
private TransportApiResponseMsg getTransportApiResponseMsg(DeviceCredentials deviceCredentials, TransportProtos.ProvisionResponseStatus status) {
if (!status.equals(ProvisionResponseStatus.SUCCESS)) {
return TransportApiResponseMsg.newBuilder().setProvisionDeviceResponseMsg(TransportProtos.ProvisionDeviceResponseMsg.newBuilder().setStatus(status).build()).build();
}
TransportProtos.ProvisionDeviceResponseMsg.Builder provisionResponse = TransportProtos.ProvisionDeviceResponseMsg.newBuilder()
.setCredentialsType(TransportProtos.CredentialsType.valueOf(deviceCredentials.getCredentialsType().name()))
.setStatus(status);
switch (deviceCredentials.getCredentialsType()){
case ACCESS_TOKEN:
provisionResponse.setCredentialsValue(deviceCredentials.getCredentialsId());
break;
case MQTT_BASIC:
case X509_CERTIFICATE:
provisionResponse.setCredentialsValue(deviceCredentials.getCredentialsValue());
break;
}
return TransportApiResponseMsg.newBuilder()
.setProvisionDeviceResponseMsg(provisionResponse.build())
.build();
}
private ListenableFuture<TransportApiResponseMsg> handle(TransportProtos.GetDeviceProfileRequestMsg requestMsg) {
DeviceProfileId profileId = new DeviceProfileId(new UUID(requestMsg.getProfileIdMSB(), requestMsg.getProfileIdLSB()));
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(TenantId.SYS_TENANT_ID, profileId);
return Futures.immediateFuture(TransportApiResponseMsg.newBuilder()
.setGetDeviceProfileResponseMsg(
TransportProtos.GetDeviceProfileResponseMsg.newBuilder()
.setData(ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfile)))
.build()).build());
private ListenableFuture<TransportApiResponseMsg> handle(GetEntityProfileRequestMsg requestMsg) {
EntityType entityType = EntityType.valueOf(requestMsg.getEntityType());
UUID entityUuid = new UUID(requestMsg.getEntityIdMSB(), requestMsg.getEntityIdLSB());
GetEntityProfileResponseMsg.Builder builder = GetEntityProfileResponseMsg.newBuilder();
if (entityType.equals(EntityType.DEVICE_PROFILE)) {
DeviceProfileId deviceProfileId = new DeviceProfileId(entityUuid);
DeviceProfile deviceProfile = deviceProfileCache.find(deviceProfileId);
builder.setData(ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfile)));
} else if (entityType.equals(EntityType.TENANT)) {
TenantId tenantId = new TenantId(entityUuid);
TenantProfile tenantProfile = tenantProfileCache.get(tenantId);
ApiUsageState state = apiUsageStateService.getApiUsageState(tenantId);
builder.setData(ByteString.copyFrom(dataDecodingEncodingService.encode(tenantProfile)));
builder.setApiState(ByteString.copyFrom(dataDecodingEncodingService.encode(state)));
} else {
throw new RuntimeException("Invalid entity profile request: " + entityType);
}
return Futures.immediateFuture(TransportApiResponseMsg.newBuilder().setEntityProfileResponseMsg(builder).build());
}
private ListenableFuture<TransportApiResponseMsg> getDeviceInfo(DeviceId deviceId, DeviceCredentials credentials) {
@ -293,7 +356,7 @@ public class DefaultTransportApiService implements TransportApiService {
try {
ValidateDeviceCredentialsResponseMsg.Builder builder = ValidateDeviceCredentialsResponseMsg.newBuilder();
builder.setDeviceInfo(getDeviceInfoProto(device));
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(device.getTenantId(), device.getDeviceProfileId());
DeviceProfile deviceProfile = deviceProfileCache.get(device.getTenantId(), device.getDeviceProfileId());
if (deviceProfile != null) {
builder.setProfileBody(ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfile)));
} else {

2
application/src/main/java/org/thingsboard/server/service/transport/TbCoreTransportApiService.java

@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.stats.MessagesStats;
import org.thingsboard.server.common.stats.StatsFactory;
@ -90,6 +91,7 @@ public class TbCoreTransportApiService {
}
@EventListener(ApplicationReadyEvent.class)
@Order(value = 2)
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
log.info("Received application ready event. Starting polling for events.");
transportApiTemplate.init(transportApiService);

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

@ -49,12 +49,43 @@ public class MiscUtils {
}
public static String constructBaseUrl(HttpServletRequest request) {
String scheme = request.getScheme();
return String.format("%s://%s:%d",
getScheme(request),
getDomainName(request),
getPort(request));
}
public static String getScheme(HttpServletRequest request){
String scheme = request.getScheme();
String forwardedProto = request.getHeader("x-forwarded-proto");
if (forwardedProto != null) {
scheme = forwardedProto;
}
return scheme;
}
public static String getDomainName(HttpServletRequest request){
return request.getServerName();
}
public static String getDomainNameAndPort(HttpServletRequest request){
String domainName = getDomainName(request);
String scheme = getScheme(request);
int port = MiscUtils.getPort(request);
if (needsPort(scheme, port)) {
domainName += ":" + port;
}
return domainName;
}
private static boolean needsPort(String scheme, int port) {
boolean isHttpDefault = "http".equals(scheme.toLowerCase()) && port == 80;
boolean isHttpsDefault = "https".equals(scheme.toLowerCase()) && port == 443;
return !isHttpDefault && !isHttpsDefault;
}
public static int getPort(HttpServletRequest request){
String forwardedProto = request.getHeader("x-forwarded-proto");
int serverPort = request.getServerPort();
if (request.getHeader("x-forwarded-port") != null) {
@ -72,11 +103,6 @@ public class MiscUtils {
break;
}
}
String baseUrl = String.format("%s://%s:%d",
scheme,
request.getServerName(),
serverPort);
return baseUrl;
return serverPort;
}
}

3
application/src/main/resources/i18n/messages.properties

@ -3,4 +3,5 @@ activation.subject=Your account activation on Thingsboard
account.activated.subject=Thingsboard - your account has been activated
reset.password.subject=Thingsboard - Password reset has been requested
password.was.reset.subject=Thingsboard - your account password has been reset
account.lockout.subject=Thingsboard - User account has been lockout
account.lockout.subject=Thingsboard - User account has been lockout
api.usage.state=Thingsboard - Account limits

145
application/src/main/resources/templates/state.disabled.ftl

@ -0,0 +1,145 @@
<#--
Copyright © 2016-2020 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<head>
<meta name="viewport" content="width=device-width"/>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>Thingsboard - Api Usage State</title>
<style type="text/css">
img {
max-width: 100%;
}
body {
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
line-height: 1.6em;
}
body {
background-color: #f6f6f6;
}
@media only screen and (max-width: 640px) {
body {
padding: 0 !important;
}
h1 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h2 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h3 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h4 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h1 {
font-size: 22px !important;
}
h2 {
font-size: 18px !important;
}
h3 {
font-size: 16px !important;
}
.container {
padding: 0 !important;
width: 100% !important;
}
.content {
padding: 0 !important;
}
.content-wrap {
padding: 10px !important;
}
.invoice {
width: 100% !important;
}
}
</style>
</head>
<body itemscope itemtype="http://schema.org/EmailMessage"
style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;"
bgcolor="#f6f6f6">
<table class="main" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; box-sizing: border-box; border-radius: 3px; width: 100%; background-color: #f6f6f6; margin: 0px auto;" cellspacing="0" cellpadding="0" bgcolor="#f6f6f6">
<tbody>
<tr style="box-sizing: border-box; margin: 0px;">
<td class="content-wrap" style="box-sizing: border-box; vertical-align: top; margin: 0px; padding: 20px;" align="center" valign="top">
<table style="box-sizing: border-box; border: 1px solid #e9e9e9; border-radius: 3px; margin: 0px; height: 367px; background-color: #ffffff; width: 600px; max-width: 600px !important;" width="600" cellspacing="0" cellpadding="0">
<tbody>
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; color: #348eda; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0px; padding: 0px; height: 110px;" valign="top"><img src="https://media.thingsboard.io/email/head.png" alt="" width="598" height="91" /></td>
</tr>
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 16px; margin: 0;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; color: #000000; box-sizing: border-box; font-size: 16px; margin: 0px; padding: 0px 32px; height: 66px; vertical-align: middle;" valign="middle">Your ThingsBoard account feature was <strong>disabled</strong></td>
</tr>
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; line-height: 24px; margin: 0;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0px; padding: 0px 32px; height: 93px; vertical-align: top;" valign="top">
<div style="padding: 16px; margin-bottom: 24px; border: solid 2px #EB5757; border-radius: 6px; background: rgba(235, 87, 87, 0.05);"><img style="vertical-align: middle; padding-right: 6px;" src="https://media.thingsboard.io/email/alarm.png" alt="" width="20" height="20" />
<div style="display: inline; vertical-align: middle;">We have <strong>disabled</strong> the ${apiFeature} for your account because ThingsBoard has already <strong>${apiLimitValueLabel}.</strong></div>
</div>
</td>
</tr>
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0px; padding: 0px 32px; height: 59px;" valign="top">Please contact your system administrator to resolve the issue.</td>
</tr>
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0px; padding: 0px 32px; height: 40px;" valign="top">&mdash; The ThingsBoard</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table style="color: #999999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; box-sizing: border-box; margin: 0px auto; height: 64px; background-color: #f6f6f6; width: 100%;" cellpadding="0px 0px 20px">
<tbody>
<tr style="box-sizing: border-box; margin: 0px;">
<td class="aligncenter content-block" style="box-sizing: border-box; font-size: 12px; margin: 0px; padding: 0px 0px 20px; width: 600px; text-align: center; vertical-align: middle;" align="center" valign="top">This email was sent to&nbsp;<a style="box-sizing: border-box; color: #999999; margin: 0px;" href="mailto:${targetEmail}">${targetEmail}</a>&nbsp;by ThingsBoard.</td>
</tr>
</tbody>
</table>
</body>
</html>

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

Loading…
Cancel
Save