Browse Source

Merge branch 'master' of github.com:thingsboard/thingsboard into feature/user-security-info

pull/11671/head
ViacheslavKlimov 2 years ago
parent
commit
ae812ad3a7
  1. 2
      application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg
  2. 2
      application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg
  3. 16
      application/src/main/data/json/tenant/dashboards/gateways.json
  4. 0
      application/src/main/data/upgrade/3.8.1/schema_update.sql
  5. 2
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java
  6. 14
      application/src/main/java/org/thingsboard/server/actors/shared/RuleChainErrorActor.java
  7. 10
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  8. 8
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  9. 2
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  10. 6
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  11. 6
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  12. 4
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2EdgeProcessor.java
  13. 2
      application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java
  14. 263
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  15. 2
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java
  16. 23
      application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
  17. 6
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/EmailTwoFaProvider.java
  18. 39
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java
  19. 64
      application/src/main/java/org/thingsboard/server/service/subscription/TbEntityLocalSubsInfo.java
  20. 3
      application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionsInfo.java
  21. 2
      application/src/main/resources/thingsboard.yml
  22. 91
      application/src/test/java/org/thingsboard/server/actors/tenant/TenantActorTest.java
  23. 126
      application/src/test/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionServiceTest.java
  24. 186
      application/src/test/java/org/thingsboard/server/service/subscription/TbEntityLocalSubsInfoTest.java
  25. 82
      application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java
  26. 11
      application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java
  27. 4
      application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/LwM2mAttributesTest.java
  28. 27
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java
  29. 86
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mTemperatureSensor.java
  30. 73
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java
  31. 732
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbLwm2mObjectEnabler.java
  32. 71
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbObjectsInitializer.java
  33. 86
      application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java
  34. 96
      application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota5LwM2MIntegrationTest.java
  35. 73
      application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota9LwM2MIntegrationTest.java
  36. 9
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserveTest.java
  37. 131
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java
  38. 109
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2MIntegrationObserveCompositeTest.java
  39. 27
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java
  40. 197
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverWriteAttributesTest.java
  41. 38
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java
  42. 98
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadCollectedValueTest.java
  43. 90
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java
  44. 14
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java
  45. 7
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLengthTest.java
  46. 5
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/diffPort/AbstractLwM2MIntegrationDiffPortTest.java
  47. 5
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java
  48. 15
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java
  49. 14
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java
  50. 2
      common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java
  51. 2
      common/edge-api/src/main/proto/edge.proto
  52. 1
      common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java
  53. 68
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java
  54. 15
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/utils/CoapEfentoUtils.java
  55. 40
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/utils/PulseCounterType.java
  56. 69
      common/transport/coap/src/test/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResourceTest.java
  57. 2
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapSecurityStore.java
  58. 2
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LwM2mCredentialsSecurityInfoValidator.java
  59. 13
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java
  60. 2
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java
  61. 21
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java
  62. 5
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java
  63. 16
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitRepositoryService.java
  64. 8
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java
  65. 3
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepositoryService.java
  66. 8
      dao/src/main/java/org/thingsboard/server/dao/AbstractVersionedInsertRepository.java
  67. 2
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java
  68. 2
      dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java
  69. 115
      dao/src/test/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDaoTest.java
  70. 24
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java
  71. 58
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java
  72. 6
      pom.xml
  73. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/credentials/CertPemCredentials.java
  74. 26
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  75. 7
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/credentials/CertPemCredentialsTest.java
  76. 133
      ui-ngx/.eslintrc.json
  77. 44
      ui-ngx/angular.json
  78. 43
      ui-ngx/e2e/protractor.conf.js
  79. 39
      ui-ngx/e2e/src/app.e2e-spec.ts
  80. 27
      ui-ngx/e2e/src/app.po.ts
  81. 13
      ui-ngx/e2e/tsconfig.e2e.json
  82. 2
      ui-ngx/extra-webpack.config.js
  83. 130
      ui-ngx/generate-icon-metadata.js
  84. 222
      ui-ngx/package.json
  85. 10
      ui-ngx/patches/@angular+core+18.2.6.patch
  86. 21
      ui-ngx/patches/@angular+flex-layout+15.0.0-beta.42.patch
  87. 51
      ui-ngx/patches/@mat-datetimepicker+core+11.0.3.patch
  88. 34
      ui-ngx/patches/@mat-datetimepicker+core+14.0.0.patch
  89. 28
      ui-ngx/patches/angular-gridster2+18.0.1.patch
  90. 2
      ui-ngx/pom.xml
  91. 1
      ui-ngx/src/app/core/api/widget-api.models.ts
  92. 140
      ui-ngx/src/app/core/core.module.ts
  93. 4
      ui-ngx/src/app/core/guards/auth.guard.ts
  94. 4
      ui-ngx/src/app/core/guards/confirm-on-exit.guard.ts
  95. 23
      ui-ngx/src/app/core/http/rule-chain.service.ts
  96. 10
      ui-ngx/src/app/core/interceptors/entity-conflict.interceptor.ts
  97. 49
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  98. 10
      ui-ngx/src/app/core/services/dynamic-component-factory.service.ts
  99. 211
      ui-ngx/src/app/core/services/resources.service.ts
  100. 2
      ui-ngx/src/app/core/settings/settings.utils.ts

2
application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg

@ -37,7 +37,7 @@
},
{
"tag": "icon",
"stateRenderFunction": "var showIcon = ctx.properties.showIcon;\nif (showIcon) {\n element.show();\n var icon = ctx.properties.icon;\n var iconSize = ctx.properties.iconSize;\n var iconColor = ctx.properties.iconColor;\n ctx.api.icon(element, icon, iconSize, iconColor, true);\n} else {\n element.hide()\n}",
"stateRenderFunction": "var showIcon = ctx.properties.showIcon;\nvar showLabel = ctx.properties.label;\nif (showIcon) {\n element.show();\n var icon = ctx.properties.icon;\n var iconSize = ctx.properties.iconSize;\n var iconColor = ctx.properties.iconColor;\n ctx.api.icon(element, icon, iconSize, iconColor, true);\n if (!showLabel) {\n element.transform({translateX: 83,translateY: 137});\n }\n} else {\n element.hide()\n}\n",
"actions": null
},
{

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

2
application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg

@ -37,7 +37,7 @@
},
{
"tag": "icon",
"stateRenderFunction": "var showIcon = ctx.properties.showIcon;\nif (showIcon) {\n element.show();\n var icon = ctx.properties.icon;\n var iconSize = ctx.properties.iconSize;\n var iconColor = ctx.properties.iconColor;\n ctx.api.icon(element, icon, iconSize, iconColor, true);\n} else {\n element.hide()\n}",
"stateRenderFunction": "var showIcon = ctx.properties.showIcon;\nvar showLabel = ctx.properties.label;\nif (showIcon) {\n element.show();\n var icon = ctx.properties.icon;\n var iconSize = ctx.properties.iconSize;\n var iconColor = ctx.properties.iconColor;\n ctx.api.icon(element, icon, iconSize, iconColor, true);\n if (!showLabel) {\n element.transform({translateX: 119, translateY: 137});\n }\n} else {\n element.hide()\n}",
"actions": null
},
{

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

16
application/src/main/data/json/tenant/dashboards/gateways.json

@ -2159,17 +2159,17 @@
}
},
{
"type": "entity",
"type": "entityCount",
"entityAliasId": "a75d9031-ba51-8da4-81be-de65061b72f4",
"filterId": "44038462-1bae-e075-7b31-283341cb2295",
"dataKeys": [
{
"name": "count",
"type": "entityField",
"type": "count",
"label": "modbusCount",
"color": "#4caf50",
"color": "#ff5722",
"settings": {},
"_hash": 0.9300660062254784,
"_hash": 0.46402083951505624,
"aggregationType": null,
"units": null,
"decimals": null,
@ -2192,7 +2192,7 @@
{
"name": "count",
"type": "count",
"label": "grcpCount",
"label": "grpcCount",
"color": "#f44336",
"settings": {},
"_hash": 0.16110429492126088,
@ -2605,9 +2605,9 @@
"padding": "0px",
"settings": {
"useMarkdownTextFunction": false,
"markdownTextPattern": "<div style=\"width: 100%; height: 100%; padding: 0;\" fxFlex fxLayout=\"column\">\r\n <mat-tab-group [(selectedIndex)]=\"selectedTabIndex\">\r\n <mat-tab label=\"All\" value=\"gateway_devices_0\"></mat-tab>\r\n <mat-tab *ngIf=\"${mqttCount}\" label=\"MQTT\" value=\"gateway_devices_1\"></mat-tab>\r\n <mat-tab *ngIf=\"${modbusCount}\" label=\"MODBUS\" value=\"gateway_devices_2\"></mat-tab>\r\n <mat-tab *ngIf=\"${grpcCount}\" label=\"GRPC\" value=\"gateway_devices_3\"></mat-tab>\r\n <mat-tab *ngIf=\"${opcuaCount}\" label=\"OPCUA\" value=\"gateway_devices_4\"> </mat-tab>\r\n <mat-tab *ngIf=\"${bleCount}\" label=\"BLE\" value=\"gateway_devices_6\"></mat-tab>\r\n <mat-tab *ngIf=\"${requestCount}\" label=\"REQUEST\" value=\"gateway_devices_7\"></mat-tab>\r\n <mat-tab *ngIf=\"${canCount}\" label=\"CAN\" value=\"gateway_devices_8\"></mat-tab>\r\n <mat-tab *ngIf=\"${bacnetCount}\" label=\"BACNET\" value=\"gateway_devices_9\"></mat-tab>\r\n <mat-tab *ngIf=\"${odbcCount}\" label=\"ODBC\" value=\"gateway_devices_10\"></mat-tab>\r\n <mat-tab *ngIf=\"${restCount}\" label=\"REST\" value=\"gateway_devices_11\"></mat-tab>\r\n <mat-tab *ngIf=\"${snmpCount}\" label=\"SNMP\" value=\"gateway_devices_12\"></mat-tab>\r\n <mat-tab *ngIf=\"${ftpCount}\" label=\"FTP\" value=\"gateway_devices_13\"></mat-tab>\r\n <mat-tab *ngIf=\"${socketCount}\" label=\"SOCKET\" value=\"gateway_devices_14\"></mat-tab>\r\n <mat-tab *ngIf=\"${xmppCount}\" label=\"XMPP\" value=\"gateway_devices_15\"></mat-tab>\r\n <mat-tab *ngIf=\"${occpCount}\" label=\"OCCP\" value=\"gateway_devices_16\"></mat-tab>\r\n <mat-tab *ngIf=\"${customCount}\" label=\"CUSTOM\" value=\"gateway_devices_17\"></mat-tab>\r\n </mat-tab-group><tb-dashboard-state *ngIf=\"selectedTabIndex == 1\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_1\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 2\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_2\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 3\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_3\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 4\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_4\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 6\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_6\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 7\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_7\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 8\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_8\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 9\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_9\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 10\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_10\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 11\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_11\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 12\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_12\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 13\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_13\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 14\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_14\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 15\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_15\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 16\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_16\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 17\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_17\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"!selectedTabIndex\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_0\"></tb-dashboard-state>\r\n</div>\r\n",
"markdownTextPattern": "<div style=\"width: 100%; height: 100%; padding: 0;\" fxFlex fxLayout=\"column\">\n <mat-tab-group class=devices-tabs>\n <mat-tab label=\"All\" value=\"gateway_devices_0\">\n <tb-dashboard-state [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_0\"></tb-dashboard-state>\n </mat-tab>\n <mat-tab *ngIf=\"${mqttCount}\" label=\"MQTT\" value=\"gateway_devices_1\">\n <tb-dashboard-state [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_1\"></tb-dashboard-state>\n </mat-tab>\n <mat-tab *ngIf=\"${modbusCount}\" label=\"MODBUS\" value=\"gateway_devices_2\">\n <tb-dashboard-state [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_2\"></tb-dashboard-state>\n </mat-tab>\n <mat-tab *ngIf=\"${grpcCount}\" label=\"GRPC\" value=\"gateway_devices_3\">\n <tb-dashboard-state [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_3\"></tb-dashboard-state>\n </mat-tab>\n <mat-tab *ngIf=\"${opcuaCount}\" label=\"OPCUA\" value=\"gateway_devices_4\">\n <tb-dashboard-state [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_4\"></tb-dashboard-state>\n </mat-tab>\n <mat-tab *ngIf=\"${bleCount}\" label=\"BLE\" value=\"gateway_devices_6\">\n <tb-dashboard-state [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_6\"></tb-dashboard-state>\n </mat-tab>\n <mat-tab *ngIf=\"${requestCount}\" label=\"REQUEST\" value=\"gateway_devices_7\">\n <tb-dashboard-state [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_7\"></tb-dashboard-state>\n </mat-tab>\n <mat-tab *ngIf=\"${canCount}\" label=\"CAN\" value=\"gateway_devices_8\">\n <tb-dashboard-state [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_8\"></tb-dashboard-state>\n </mat-tab>\n <mat-tab *ngIf=\"${bacnetCount}\" label=\"BACNET\" value=\"gateway_devices_9\">\n <tb-dashboard-state [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_9\"></tb-dashboard-state>\n </mat-tab>\n <mat-tab *ngIf=\"${odbcCount}\" label=\"ODBC\" value=\"gateway_devices_10\">\n <tb-dashboard-state [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_10\"></tb-dashboard-state>\n </mat-tab>\n <mat-tab *ngIf=\"${restCount}\" label=\"REST\" value=\"gateway_devices_11\">\n <tb-dashboard-state [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_11\"></tb-dashboard-state>\n </mat-tab>\n <mat-tab *ngIf=\"${snmpCount}\" label=\"SNMP\" value=\"gateway_devices_12\">\n <tb-dashboard-state [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_12\"></tb-dashboard-state>\n </mat-tab>\n <mat-tab *ngIf=\"${ftpCount}\" label=\"FTP\" value=\"gateway_devices_13\">\n <tb-dashboard-state [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_13\"></tb-dashboard-state>\n </mat-tab>\n <mat-tab *ngIf=\"${socketCount}\" label=\"SOCKET\" value=\"gateway_devices_14\">\n <tb-dashboard-state [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_14\"></tb-dashboard-state>\n </mat-tab>\n <mat-tab *ngIf=\"${xmppCount}\" label=\"XMPP\" value=\"gateway_devices_15\">\n <tb-dashboard-state [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_15\"></tb-dashboard-state>\n </mat-tab>\n <mat-tab *ngIf=\"${ocppCount}\" label=\"OCPP\" value=\"gateway_devices_16\">\n <tb-dashboard-state [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_16\"></tb-dashboard-state>\n </mat-tab>\n <mat-tab *ngIf=\"${customCount}\" label=\"CUSTOM\" value=\"gateway_devices_17\">\n <tb-dashboard-state [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_17\"></tb-dashboard-state>\n </mat-tab>\n </mat-tab-group>\n</div>\n",
"applyDefaultMarkdownStyle": false,
"markdownCss": ".mat-mdc-form-field-subscript-wrapper {\n display: none !important;\n}"
"markdownCss": ".mat-mdc-form-field-subscript-wrapper {\n display: none !important;\n}\n\n.devices-tabs {\n height: 100%;\n}\n\n::ng-deep .mat-mdc-tab-body-wrapper {\n height: 100%;\n}"
},
"title": "Gateway devices",
"showTitleIcon": false,
@ -6208,7 +6208,7 @@
}
},
"gateway_devices_16": {
"name": "gateway_devices_occp",
"name": "gateway_devices_ocpp",
"root": false,
"layouts": {
"main": {

0
application/src/main/data/upgrade/3.8.0/schema_update.sql → application/src/main/data/upgrade/3.8.1/schema_update.sql

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

@ -95,7 +95,7 @@ public abstract class RuleChainManagerActor extends ContextAwareActor {
() -> {
RuleChain ruleChain = provider.apply(ruleChainId);
if (ruleChain == null) {
return new RuleChainErrorActor.ActorCreator(systemContext, tenantId,
return new RuleChainErrorActor.ActorCreator(systemContext, tenantId, ruleChainId,
new RuleEngineException("Rule Chain with id: " + ruleChainId + " not found!"));
} else {
return new RuleChainActor.ActorCreator(systemContext, tenantId, ruleChain);

14
application/src/main/java/org/thingsboard/server/actors/shared/RuleChainErrorActor.java

@ -19,16 +19,15 @@ import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.TbActor;
import org.thingsboard.server.actors.TbActorId;
import org.thingsboard.server.actors.TbStringActorId;
import org.thingsboard.server.actors.TbEntityActorId;
import org.thingsboard.server.actors.service.ContextAwareActor;
import org.thingsboard.server.actors.service.ContextBasedCreator;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.aware.RuleChainAwareMsg;
import org.thingsboard.server.common.msg.queue.RuleEngineException;
import java.util.UUID;
@Slf4j
public class RuleChainErrorActor extends ContextAwareActor {
@ -43,9 +42,8 @@ public class RuleChainErrorActor extends ContextAwareActor {
@Override
protected boolean doProcess(TbActorMsg msg) {
if (msg instanceof RuleChainAwareMsg) {
if (msg instanceof RuleChainAwareMsg rcMsg) {
log.debug("[{}] Reply with {} for message {}", tenantId, error.getMessage(), msg);
var rcMsg = (RuleChainAwareMsg) msg;
rcMsg.getMsg().getCallback().onFailure(error);
return true;
} else {
@ -56,17 +54,19 @@ public class RuleChainErrorActor extends ContextAwareActor {
public static class ActorCreator extends ContextBasedCreator {
private final TenantId tenantId;
private final RuleChainId ruleChainId;
private final RuleEngineException error;
public ActorCreator(ActorSystemContext context, TenantId tenantId, RuleEngineException error) {
public ActorCreator(ActorSystemContext context, TenantId tenantId, RuleChainId ruleChainId, RuleEngineException error) {
super(context);
this.tenantId = tenantId;
this.ruleChainId = ruleChainId;
this.error = error;
}
@Override
public TbActorId createActorId() {
return new TbStringActorId(UUID.randomUUID().toString());
return new TbEntityActorId(ruleChainId);
}
@Override

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

@ -235,7 +235,15 @@ public class AdminController extends BaseController {
}
}
String email = getCurrentUser().getEmail();
mailService.sendTestMail(adminSettings.getJsonValue(), email);
try {
mailService.sendTestMail(adminSettings.getJsonValue(), email);
} catch (ThingsboardException e) {
String error = e.getMessage();
if (e.getCause() != null) {
error += ": " + e.getCause().getMessage(); // showing actual underlying error for testing purposes
}
throw new ThingsboardException(error, e.getErrorCode());
}
}
}

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

@ -217,7 +217,7 @@ public class AuthController extends BaseController {
try {
mailService.sendAccountActivatedEmail(loginUrl, email);
} catch (Exception e) {
log.info("Unable to send account activation email [{}]", e.getMessage());
log.warn("Unable to send account activation email [{}]", e.getMessage());
}
}
@ -256,7 +256,11 @@ public class AuthController extends BaseController {
String baseUrl = systemSecurityService.getBaseUrl(user.getTenantId(), user.getCustomerId(), request);
String loginUrl = String.format("%s/login", baseUrl);
String email = user.getEmail();
mailService.sendPasswordWasResetEmail(loginUrl, email);
try {
mailService.sendPasswordWasResetEmail(loginUrl, email);
} catch (Exception e) {
log.warn("Couldn't send password was reset email: {}", e.getMessage());
}
eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId()));

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

@ -403,7 +403,7 @@ public abstract class BaseController {
|| exception instanceof DataValidationException || cause instanceof IncorrectParameterException) {
return new ThingsboardException(exception.getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} else if (exception instanceof MessagingException) {
return new ThingsboardException("Unable to send mail: " + exception.getMessage(), ThingsboardErrorCode.GENERAL);
return new ThingsboardException("Unable to send mail", ThingsboardErrorCode.GENERAL);
} else if (exception instanceof AsyncRequestTimeoutException) {
return new ThingsboardException("Request timeout", ThingsboardErrorCode.GENERAL);
} else if (exception instanceof DataAccessException) {

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

@ -217,7 +217,11 @@ public class UserController extends BaseController {
accessControlService.checkPermission(securityUser, Resource.USER, Operation.READ, user.getId(), user);
UserActivationLink activationLink = tbUserService.getActivationLink(securityUser.getTenantId(), securityUser.getCustomerId(), user.getId(), request);
mailService.sendActivationEmail(activationLink.value(), activationLink.ttlMs(), email);
try {
mailService.sendActivationEmail(activationLink.value(), activationLink.ttlMs(), email);
} catch (Exception e) {
throw new ThingsboardException("Couldn't send user activation email", ThingsboardErrorCode.GENERAL);
}
}
@ApiOperation(value = "Get activation link (getActivationLink)",

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

@ -141,8 +141,10 @@ public class ThingsboardInstallService {
log.info("Upgrading ThingsBoard from version 3.7.0 to 3.8.0 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.7.0");
case "3.8.0":
log.info("Upgrading ThingsBoard from version 3.8.0 to 3.9.0 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.8.0");
log.info("Upgrading ThingsBoard from version 3.8.0 to 3.8.1 ...");
case "3.8.1":
log.info("Upgrading ThingsBoard from version 3.8.1 to 3.9.0 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.8.1");
//TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache
break;
default:

4
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2EdgeProcessor.java

@ -38,7 +38,7 @@ import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils;
public class OAuth2EdgeProcessor extends BaseEdgeProcessor {
public DownlinkMsg convertOAuth2DomainEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_7_1)) {
if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_8_0)) {
return null;
}
DomainId domainId = new DomainId(edgeEvent.getEntityId());
@ -73,7 +73,7 @@ public class OAuth2EdgeProcessor extends BaseEdgeProcessor {
}
public DownlinkMsg convertOAuth2ClientEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_7_1)) {
if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_8_0)) {
return null;
}
OAuth2ClientId oAuth2ClientId = new OAuth2ClientId(edgeEvent.getEntityId());

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

@ -60,7 +60,7 @@ public class DefaultUserService extends AbstractTbEntityService implements TbUse
mailService.sendActivationEmail(activationLink.value(), activationLink.ttlMs(), savedUser.getEmail());
} catch (ThingsboardException e) {
userService.deleteUser(tenantId, savedUser);
throw e;
throw new ThingsboardException("Couldn't send user activation email", ThingsboardErrorCode.GENERAL);
}
}
logEntityActionService.logEntityAction(tenantId, savedUser.getId(), savedUser, customerId, actionType, user);

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

@ -16,24 +16,22 @@
package org.thingsboard.server.service.install;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.intellij.lang.annotations.Language;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.StatementCallback;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import org.thingsboard.server.service.install.update.DefaultDataUpdateService;
import java.nio.charset.Charset;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Statement;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
@Service
@Profile("install")
@ -42,140 +40,129 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
private static final String SCHEMA_UPDATE_SQL = "schema_update.sql";
@Value("${spring.datasource.url}")
private String dbUrl;
private final InstallScripts installScripts;
private final JdbcTemplate jdbcTemplate;
private final TransactionTemplate transactionTemplate;
@Value("${spring.datasource.username}")
private String dbUserName;
@Value("${spring.datasource.password}")
private String dbPassword;
@Autowired
private InstallScripts installScripts;
public SqlDatabaseUpgradeService(InstallScripts installScripts, JdbcTemplate jdbcTemplate, PlatformTransactionManager transactionManager) {
this.installScripts = installScripts;
this.jdbcTemplate = jdbcTemplate;
this.transactionTemplate = new TransactionTemplate(transactionManager);
this.transactionTemplate.setTimeout((int) TimeUnit.MINUTES.toSeconds(120));
}
@Override
public void upgradeDatabase(String fromVersion) throws Exception {
public void upgradeDatabase(String fromVersion) {
switch (fromVersion) {
case "3.5.0":
updateSchema("3.5.0", 3005000, "3.5.1", 3005001, null);
break;
case "3.5.1":
updateSchema("3.5.1", 3005001, "3.6.0", 3006000, conn -> {
String[] entityNames = new String[]{"device", "component_descriptor", "customer", "dashboard", "rule_chain", "rule_node", "ota_package",
"asset_profile", "asset", "device_profile", "tb_user", "tenant_profile", "tenant", "widgets_bundle", "entity_view", "edge"};
for (String entityName : entityNames) {
try {
conn.createStatement().execute("ALTER TABLE " + entityName + " DROP COLUMN search_text CASCADE");
} catch (Exception e) {
}
}
try {
conn.createStatement().execute("ALTER TABLE component_descriptor ADD COLUMN IF NOT EXISTS configuration_version int DEFAULT 0;");
} catch (Exception e) {
}
try {
conn.createStatement().execute("ALTER TABLE rule_node ADD COLUMN IF NOT EXISTS configuration_version int DEFAULT 0;");
} catch (Exception e) {
}
try {
conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_rule_node_type_configuration_version ON rule_node(type, configuration_version);");
} catch (Exception e) {
}
try {
conn.createStatement().execute("UPDATE rule_node SET " +
case "3.5.0" -> updateSchema("3.5.0", 3005000, "3.5.1", 3005001);
case "3.5.1" -> {
updateSchema("3.5.1", 3005001, "3.6.0", 3006000);
String[] tables = new String[]{"device", "component_descriptor", "customer", "dashboard", "rule_chain", "rule_node", "ota_package",
"asset_profile", "asset", "device_profile", "tb_user", "tenant_profile", "tenant", "widgets_bundle", "entity_view", "edge"};
for (String table : tables) {
execute("ALTER TABLE " + table + " DROP COLUMN IF EXISTS search_text CASCADE");
}
execute(
"ALTER TABLE component_descriptor ADD COLUMN IF NOT EXISTS configuration_version int DEFAULT 0;",
"ALTER TABLE rule_node ADD COLUMN IF NOT EXISTS configuration_version int DEFAULT 0;",
"CREATE INDEX IF NOT EXISTS idx_rule_node_type_configuration_version ON rule_node(type, configuration_version);",
"UPDATE rule_node SET " +
"configuration = (configuration::jsonb || '{\"updateAttributesOnlyOnValueChange\": \"false\"}'::jsonb)::varchar, " +
"configuration_version = 1 " +
"WHERE type = 'org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode' AND configuration_version < 1;");
} catch (Exception e) {
}
try {
conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_unread ON notification(recipient_id) WHERE status <> 'READ';");
} catch (Exception e) {
}
});
break;
case "3.6.0":
updateSchema("3.6.0", 3006000, "3.6.1", 3006001, null);
break;
case "3.6.1":
updateSchema("3.6.1", 3006001, "3.6.2", 3006002, connection -> {
try {
Path saveAttributesNodeUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.6.1", "save_attributes_node_update.sql");
loadSql(saveAttributesNodeUpdateFile, connection);
} catch (Exception e) {
log.warn("Failed to execute update script for save attributes rule nodes due to: ", e);
}
try {
connection.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_asset_profile_id ON asset(tenant_id, asset_profile_id);");
} catch (Exception e) {
}
});
break;
case "3.6.2":
updateSchema("3.6.2", 3006002, "3.6.3", 3006003, null);
break;
case "3.6.3":
updateSchema("3.6.3", 3006003, "3.6.4", 3006004, null);
break;
case "3.6.4":
updateSchema("3.6.4", 3006004, "3.7.0", 3007000, null);
break;
case "3.7.0":
updateSchema("3.7.0", 3007000, "3.8.0", 3008000, connection -> {
try {
connection.createStatement().execute("UPDATE rule_node SET " +
"configuration = CASE " +
" WHEN (configuration::jsonb ->> 'persistAlarmRulesState') = 'false'" +
" THEN (configuration::jsonb || '{\"fetchAlarmRulesStateOnStart\": \"false\"}'::jsonb)::varchar " +
" ELSE configuration " +
"END, " +
"configuration_version = 1 " +
"WHERE type = 'org.thingsboard.rule.engine.profile.TbDeviceProfileNode' " +
"AND configuration_version < 1;");
} catch (Exception e) {
log.warn("Failed to execute update script for device profile rule nodes due to: ", e);
}
});
break;
case "3.8.0":
updateSchema("3.8.0", 3008000, "3.9.0", 3009000, null);
break;
default:
throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion);
"WHERE type = 'org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode' AND configuration_version < 1;",
"CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_unread ON notification(recipient_id) WHERE status <> 'READ';"
);
}
case "3.6.0" -> updateSchema("3.6.0", 3006000, "3.6.1", 3006001);
case "3.6.1" -> {
updateSchema("3.6.1", 3006001, "3.6.2", 3006002);
try {
Path saveAttributesNodeUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.6.1", "save_attributes_node_update.sql");
loadSql(saveAttributesNodeUpdateFile);
} catch (Exception e) {
log.warn("Failed to execute update script for save attributes rule nodes due to: ", e);
}
execute("CREATE INDEX IF NOT EXISTS idx_asset_profile_id ON asset(tenant_id, asset_profile_id);");
}
case "3.6.2" -> updateSchema("3.6.2", 3006002, "3.6.3", 3006003);
case "3.6.3" -> updateSchema("3.6.3", 3006003, "3.6.4", 3006004);
case "3.6.4" -> updateSchema("3.6.4", 3006004, "3.7.0", 3007000);
case "3.7.0" -> {
updateSchema("3.7.0", 3007000, "3.8.0", 3008000);
try {
execute("UPDATE rule_node SET " +
"configuration = CASE " +
" WHEN (configuration::jsonb ->> 'persistAlarmRulesState') = 'false'" +
" THEN (configuration::jsonb || '{\"fetchAlarmRulesStateOnStart\": \"false\"}'::jsonb)::varchar " +
" ELSE configuration " +
"END, " +
"configuration_version = 1 " +
"WHERE type = 'org.thingsboard.rule.engine.profile.TbDeviceProfileNode' " +
"AND configuration_version < 1;", false);
} catch (Exception e) {
log.warn("Failed to execute update script for device profile rule nodes due to: ", e);
}
}
case "3.8.1" -> updateSchema("3.8.1", 3008001, "3.9.0", 3009000);
default -> throw new RuntimeException("Unsupported fromVersion '" + fromVersion + "'");
}
}
private void updateSchema(String oldVersionStr, int oldVersion, String newVersionStr, int newVersion, Consumer<Connection> additionalAction) {
try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
log.info("Updating schema ...");
if (isOldSchema(conn, oldVersion)) {
Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", oldVersionStr, SCHEMA_UPDATE_SQL);
loadSql(schemaUpdateFile, conn);
if (additionalAction != null) {
additionalAction.accept(conn);
private void updateSchema(String oldVersionStr, int oldVersion, String newVersionStr, int newVersion) {
try {
transactionTemplate.executeWithoutResult(ts -> {
log.info("Updating schema ...");
if (isOldSchema(oldVersion)) {
loadSql(getSchemaUpdateFile(oldVersionStr));
jdbcTemplate.execute("UPDATE tb_schema_settings SET schema_version = " + newVersion);
log.info("Schema updated to version {}", newVersionStr);
} else {
log.info("Skip schema re-update to version {}. Use env flag 'SKIP_SCHEMA_VERSION_CHECK' to force the re-update.", newVersionStr);
}
conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = " + newVersion + ";");
log.info("Schema updated to version {}", newVersionStr);
} else {
log.info("Skip schema re-update to version {}. Use env flag 'SKIP_SCHEMA_VERSION_CHECK' to force the re-update.", newVersionStr);
}
});
} catch (Exception e) {
log.error("Failed updating schema!!!", e);
throw new RuntimeException("Failed to update schema", e);
}
}
private void loadSql(Path sqlFile, Connection conn) throws Exception {
String sql = new String(Files.readAllBytes(sqlFile), Charset.forName("UTF-8"));
Statement st = conn.createStatement();
st.setQueryTimeout((int) TimeUnit.HOURS.toSeconds(3));
st.execute(sql);//NOSONAR, ignoring because method used to execute thingsboard database upgrade script
printWarnings(st);
Thread.sleep(5000);
private Path getSchemaUpdateFile(String version) {
return Paths.get(installScripts.getDataDir(), "upgrade", version, SCHEMA_UPDATE_SQL);
}
protected void printWarnings(Statement statement) throws SQLException {
SQLWarning warnings = statement.getWarnings();
private void loadSql(Path sqlFile) {
String sql;
try {
sql = Files.readString(sqlFile);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
jdbcTemplate.execute((StatementCallback<Object>) stmt -> {
stmt.execute(sql);
printWarnings(stmt.getWarnings());
return null;
});
}
private void execute(@Language("sql") String... statements) {
for (String statement : statements) {
execute(statement, true);
}
}
private void execute(@Language("sql") String statement, boolean ignoreErrors) {
try {
jdbcTemplate.execute(statement);
} catch (Exception e) {
if (!ignoreErrors) {
throw e;
}
}
}
private void printWarnings(SQLWarning warnings) {
if (warnings != null) {
log.info("{}", warnings.getMessage());
SQLWarning nextWarning = warnings.getNextWarning();
@ -186,26 +173,18 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
}
}
protected boolean isOldSchema(Connection conn, long fromVersion) {
private boolean isOldSchema(long fromVersion) {
if (DefaultDataUpdateService.getEnv("SKIP_SCHEMA_VERSION_CHECK", false)) {
log.info("Skipped DB schema version check due to SKIP_SCHEMA_VERSION_CHECK set to true!");
return true;
}
jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS tb_schema_settings (schema_version bigint NOT NULL, CONSTRAINT tb_schema_settings_pkey PRIMARY KEY (schema_version))");
Long schemaVersion = jdbcTemplate.queryForList("SELECT schema_version FROM tb_schema_settings", Long.class).stream().findFirst().orElse(null);
boolean isOldSchema = true;
try {
Statement statement = conn.createStatement();
statement.execute("CREATE TABLE IF NOT EXISTS tb_schema_settings ( schema_version bigint NOT NULL, CONSTRAINT tb_schema_settings_pkey PRIMARY KEY (schema_version));");
Thread.sleep(1000);
ResultSet resultSet = statement.executeQuery("SELECT schema_version FROM tb_schema_settings;");
if (resultSet.next()) {
isOldSchema = resultSet.getLong(1) <= fromVersion;
} else {
resultSet.close();
statement.execute("INSERT INTO tb_schema_settings (schema_version) VALUES (" + fromVersion + ")");
}
statement.close();
} catch (InterruptedException | SQLException e) {
log.info("Failed to check current PostgreSQL schema due to: {}", e.getMessage());
if (schemaVersion != null) {
isOldSchema = schemaVersion <= fromVersion;
} else {
jdbcTemplate.execute("INSERT INTO tb_schema_settings (schema_version) VALUES (" + fromVersion + ")");
}
return isOldSchema;
}

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

@ -62,7 +62,7 @@ public class DefaultCacheCleanupService implements CacheCleanupService {
clearAll();
break;
case "3.7.0":
log.info("Clearing cache to upgrade from version 3.7.0 to 3.7.1");
log.info("Clearing cache to upgrade from version 3.7.0 to 3.8.0");
clearAll();
break;
default:

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

@ -443,17 +443,16 @@ public class DefaultMailService implements MailService {
}
}
private void sendMailWithTimeout(JavaMailSender mailSender, MimeMessage msg, long timeout) {
private void sendMailWithTimeout(JavaMailSender mailSender, MimeMessage msg, long timeout) throws ThingsboardException {
var submittedMail = Futures.withTimeout(
mailExecutorService.submit(() -> mailSender.send(msg)),
timeout, TimeUnit.MILLISECONDS, timeoutScheduler);
try {
submittedMail.get(timeout, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
log.debug("Error during mail submission", e);
throw new RuntimeException("Timeout!");
} catch (Exception e) {
throw new RuntimeException(ExceptionUtils.getRootCause(e));
throw new ThingsboardException("Unable to send mail", ExceptionUtils.getRootCause(e), ThingsboardErrorCode.GENERAL);
}
}
@ -463,20 +462,20 @@ public class DefaultMailService implements MailService {
Template template = freemarkerConfig.getTemplate(templateLocation);
return FreeMarkerTemplateUtils.processTemplateIntoString(template, model);
} catch (Exception e) {
throw handleException(e);
log.warn("Failed to process mail template: {}", ExceptionUtils.getRootCauseMessage(e));
throw new ThingsboardException("Failed to process mail template: " + e.getMessage(), e, ThingsboardErrorCode.GENERAL);
}
}
protected ThingsboardException handleException(Exception exception) {
String message;
protected ThingsboardException handleException(Throwable exception) {
if (exception instanceof ThingsboardException thingsboardException) {
return thingsboardException;
}
if (exception instanceof NestedRuntimeException) {
message = ((NestedRuntimeException) exception).getMostSpecificCause().getMessage();
} else {
message = exception.getMessage();
exception = ((NestedRuntimeException) exception).getMostSpecificCause();
}
log.warn("Unable to send mail: {}", message);
return new ThingsboardException(String.format("Unable to send mail: %s", message),
ThingsboardErrorCode.GENERAL);
log.warn("Unable to send mail: {}", exception.getMessage());
return new ThingsboardException("Unable to send mail: " + exception.getMessage(), ThingsboardErrorCode.GENERAL);
}
}

6
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/EmailTwoFaProvider.java

@ -57,7 +57,11 @@ public class EmailTwoFaProvider extends OtpBasedTwoFaProvider<EmailTwoFaProvider
@Override
protected void sendVerificationCode(SecurityUser user, String verificationCode, EmailTwoFaProviderConfig providerConfig, EmailTwoFaAccountConfig accountConfig) throws ThingsboardException {
mailService.sendTwoFaVerificationEmail(accountConfig.getEmail(), verificationCode, providerConfig.getVerificationCodeLifetime());
try {
mailService.sendTwoFaVerificationEmail(accountConfig.getEmail(), verificationCode, providerConfig.getVerificationCodeLifetime());
} catch (Exception e) {
throw new ThingsboardException("Couldn't send 2FA verification email", ThingsboardErrorCode.GENERAL);
}
}
@Override

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

@ -282,7 +282,6 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
if (sessionSubscriptions != null) {
TbSubscription<?> subscription = sessionSubscriptions.remove(subscriptionId);
if (subscription != null) {
if (sessionSubscriptions.isEmpty()) {
subscriptionsBySessionId.remove(sessionId);
}
@ -304,22 +303,26 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
@Override
public void cancelAllSessionSubscriptions(TenantId tenantId, String sessionId) {
log.debug("[{}][{}] Going to remove session subscriptions.", tenantId, sessionId);
List<SubscriptionModificationResult> results = new ArrayList<>();
Lock subsLock = getSubsLock(tenantId);
subsLock.lock();
try {
Map<Integer, TbSubscription<?>> sessionSubscriptions = subscriptionsBySessionId.remove(sessionId);
if (sessionSubscriptions != null) {
for (TbSubscription<?> subscription : sessionSubscriptions.values()) {
results.add(modifySubscription(tenantId, subscription.getEntityId(), subscription, false));
}
Map<EntityId, List<TbSubscription<?>>> entitySubscriptions =
sessionSubscriptions.values().stream().collect(Collectors.groupingBy(TbSubscription::getEntityId));
entitySubscriptions.forEach((entityId, subscriptions) -> {
TbEntitySubEvent event = removeAllSubscriptions(tenantId, entityId, subscriptions);
if (event != null) {
pushSubscriptionsEvent(tenantId, entityId, event);
}
});
} else {
log.debug("[{}][{}] No session subscriptions found!", tenantId, sessionId);
}
} finally {
subsLock.unlock();
}
results.stream().filter(SubscriptionModificationResult::hasEvent).forEach(this::pushSubscriptionEvent);
}
@Override
@ -500,6 +503,30 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
return new SubscriptionModificationResult(tenantId, entityId, subscription, missedUpdatesCandidate, event);
}
private TbEntitySubEvent removeAllSubscriptions(TenantId tenantId, EntityId entityId, List<TbSubscription<?>> subscriptions) {
TbEntitySubEvent event = null;
try {
TbEntityLocalSubsInfo entitySubs = subscriptionsByEntityId.get(entityId.getId());
event = entitySubs.removeAll(subscriptions);
if (entitySubs.isEmpty()) {
subscriptionsByEntityId.remove(entityId.getId());
entityUpdates.remove(entityId.getId());
}
} catch (Exception e) {
log.warn("[{}][{}] Failed to remove all subscriptions {} due to ", tenantId, entityId, subscriptions, e);
}
return event;
}
private void pushSubscriptionsEvent(TenantId tenantId, EntityId entityId, TbEntitySubEvent event) {
try {
log.trace("[{}][{}] Event: {}", tenantId, entityId, event);
pushSubEventToManagerService(tenantId, entityId, event);
} catch (Exception e) {
log.warn("[{}][{}] Failed to push subscription event {} due to ", tenantId, entityId, event, e);
}
}
private void pushSubscriptionEvent(SubscriptionModificationResult modificationResult) {
try {
TbEntitySubEvent event = modificationResult.getEvent();

64
application/src/main/java/org/thingsboard/server/service/subscription/TbEntityLocalSubsInfo.java

@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@ -129,13 +130,64 @@ public class TbEntityLocalSubsInfo {
if (!subs.remove(sub)) {
return null;
}
if (subs.isEmpty()) {
if (isEmpty()) {
return toEvent(ComponentLifecycleEvent.DELETED);
}
TbSubscriptionsInfo oldState = state.copy();
TbSubscriptionsInfo newState = new TbSubscriptionsInfo();
TbSubscriptionType type = sub.getType();
TbSubscriptionsInfo newState = state.copy();
clearState(newState, type);
return updateState(Set.of(type), newState);
}
public TbEntitySubEvent removeAll(List<? extends TbSubscription<?>> subsToRemove) {
Set<TbSubscriptionType> changedTypes = new HashSet<>();
TbSubscriptionsInfo newState = state.copy();
for (TbSubscription<?> sub : subsToRemove) {
log.trace("[{}][{}][{}] Removing: {}", tenantId, entityId, sub.getSubscriptionId(), sub);
if (!subs.remove(sub)) {
continue;
}
if (isEmpty()) {
return toEvent(ComponentLifecycleEvent.DELETED);
}
TbSubscriptionType type = sub.getType();
if (changedTypes.contains(type)) {
continue;
}
clearState(newState, type);
changedTypes.add(type);
}
return updateState(changedTypes, newState);
}
private void clearState(TbSubscriptionsInfo state, TbSubscriptionType type) {
switch (type) {
case NOTIFICATIONS:
case NOTIFICATIONS_COUNT:
state.notifications = false;
break;
case ALARMS:
state.alarms = false;
break;
case ATTRIBUTES:
state.attrAllKeys = false;
state.attrKeys = null;
break;
case TIMESERIES:
state.tsAllKeys = false;
state.tsKeys = null;
}
}
private TbEntitySubEvent updateState(Set<TbSubscriptionType> updatedTypes, TbSubscriptionsInfo newState) {
for (TbSubscription<?> subscription : subs) {
switch (subscription.getType()) {
TbSubscriptionType type = subscription.getType();
if (!updatedTypes.contains(type)) {
continue;
}
switch (type) {
case NOTIFICATIONS:
case NOTIFICATIONS_COUNT:
if (!newState.notifications) {
@ -173,7 +225,7 @@ public class TbEntityLocalSubsInfo {
break;
}
}
if (newState.equals(oldState)) {
if (newState.equals(state)) {
return null;
} else {
this.state = newState;
@ -196,7 +248,7 @@ public class TbEntityLocalSubsInfo {
public boolean isEmpty() {
return state.isEmpty();
return subs.isEmpty();
}
public TbSubscription<?> registerPendingSubscription(TbSubscription<?> subscription, TbEntitySubEvent event) {

3
application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionsInfo.java

@ -20,6 +20,7 @@ import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import java.util.HashSet;
import java.util.Set;
/**
@ -48,7 +49,7 @@ public class TbSubscriptionsInfo {
}
protected TbSubscriptionsInfo copy(int seqNumber) {
return new TbSubscriptionsInfo(notifications, alarms, tsAllKeys, tsKeys, attrAllKeys, attrKeys, seqNumber);
return new TbSubscriptionsInfo(notifications, alarms, tsAllKeys, tsKeys != null ? new HashSet<>(tsKeys) : null, attrAllKeys, attrKeys != null ? new HashSet<>(attrKeys) : null, seqNumber);
}
}

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

@ -802,7 +802,7 @@ spring:
# This property controls the amount of time that a connection can be out of the pool before a message is logged indicating a possible connection leak for events datasource. A value of 0 means leak detection is disabled
leakDetectionThreshold: "${SPRING_EVENTS_DATASOURCE_HIKARI_LEAK_DETECTION_THRESHOLD:0}"
# This property increases the number of connections in the pool as demand increases for events datasource. At the same time, the property ensures that the pool doesn't grow to the point of exhausting a system's resources, which ultimately affects an application's performance and availability
maximumPoolSize: "${SPRING_EVENTS_DATASOURCE_MAXIMUM_POOL_SIZE:16}"
maximumPoolSize: "${SPRING_EVENTS_DATASOURCE_MAXIMUM_POOL_SIZE:4}"
# Enable MBean to diagnose pools state via JMX for events datasource
registerMbeans: "${SPRING_EVENTS_DATASOURCE_HIKARI_REGISTER_MBEANS:false}"

91
application/src/test/java/org/thingsboard/server/actors/tenant/TenantActorTest.java

@ -18,57 +18,130 @@ package org.thingsboard.server.actors.tenant;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.DefaultTbActorSystem;
import org.thingsboard.server.actors.TbActorCtx;
import org.thingsboard.server.actors.TbActorMailbox;
import org.thingsboard.server.actors.TbActorRef;
import org.thingsboard.server.actors.TbActorSystem;
import org.thingsboard.server.actors.TbActorSystemSettings;
import org.thingsboard.server.actors.TbEntityActorId;
import org.thingsboard.server.actors.ruleChain.RuleChainActor;
import org.thingsboard.server.actors.ruleChain.RuleChainToRuleChainMsg;
import org.thingsboard.server.actors.shared.RuleChainErrorActor;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.msg.rule.engine.DeviceDeleteMsg;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.thingsboard.server.actors.service.DefaultActorService.RULE_DISPATCHER_NAME;
public class TenantActorTest {
TenantActor tenantActor;
TbActorCtx ctx;
ActorSystemContext systemContext;
RuleChainService ruleChainService;
PartitionService partitionService;
TenantId tenantId = TenantId.SYS_TENANT_ID;
DeviceId deviceId = DeviceId.fromString("78bf9b26-74ef-4af2-9cfb-ad6cf24ad2ec");
RuleChainId ruleChainId = new RuleChainId(UUID.fromString("48cfa2b0-3dca-11ef-8d1a-37c2894cc59c"));
@Before
public void setUp() throws Exception {
systemContext = mock(ActorSystemContext.class);
ctx = mock(TbActorCtx.class);
ruleChainService = mock(RuleChainService.class);
partitionService = mock();
TbServiceInfoProvider serviceInfoProvider = mock(TbServiceInfoProvider.class);
TbApiUsageStateService apiUsageService = mock(TbApiUsageStateService.class);
TenantService tenantService = mock(TenantService.class);
when(systemContext.getRuleChainService()).thenReturn(ruleChainService);
tenantActor = (TenantActor) new TenantActor.ActorCreator(systemContext, tenantId).createActor();
when(systemContext.getTenantService()).thenReturn(mock(TenantService.class));
tenantActor.init(ctx);
tenantActor.cantFindTenant = false;
when(tenantService.findTenantById(tenantId)).thenReturn(mock());
when(systemContext.getTenantService()).thenReturn(tenantService);
when(serviceInfoProvider.isService(ServiceType.TB_CORE)).thenReturn(true);
when(serviceInfoProvider.isService(ServiceType.TB_RULE_ENGINE)).thenReturn(true);
when(systemContext.getServiceInfoProvider()).thenReturn(serviceInfoProvider);
when(partitionService.isManagedByCurrentService(tenantId)).thenReturn(true);
when(systemContext.getPartitionService()).thenReturn(partitionService);
when(systemContext.getApiUsageStateService()).thenReturn(apiUsageService);
when(apiUsageService.getApiUsageState(tenantId)).thenReturn(new ApiUsageState());
}
@Test
public void deleteDeviceTest() {
public void deleteDeviceTest() throws Exception {
TbActorCtx ctx = mock(TbActorCtx.class);
tenantActor.init(ctx);
TbActorRef deviceActorRef = mock(TbActorRef.class);
when(systemContext.resolve(ServiceType.TB_CORE, tenantId, deviceId)).thenReturn(new TopicPartitionInfo("Main", tenantId, 0,true));
when(systemContext.resolve(ServiceType.TB_CORE, tenantId, deviceId)).thenReturn(new TopicPartitionInfo("Main", tenantId, 0, true));
when(ctx.getOrCreateChildActor(any(), any(), any(), any())).thenReturn(deviceActorRef);
ComponentLifecycleMsg componentLifecycleMsg = new ComponentLifecycleMsg(tenantId, deviceId, ComponentLifecycleEvent.DELETED);
tenantActor.doProcess(componentLifecycleMsg);
verify(deviceActorRef).tellWithHighPriority(eq(new DeviceDeleteMsg(tenantId, deviceId)));
reset(ctx, deviceActorRef);
when(systemContext.resolve(ServiceType.TB_CORE, tenantId, deviceId)).thenReturn(new TopicPartitionInfo("Main", tenantId, 1,false));
when(systemContext.resolve(ServiceType.TB_CORE, tenantId, deviceId)).thenReturn(new TopicPartitionInfo("Main", tenantId, 1, false));
tenantActor.doProcess(componentLifecycleMsg);
verify(ctx, never()).getOrCreateChildActor(any(), any(), any(), any());
verify(deviceActorRef, never()).tellWithHighPriority(any());
}
@Test
public void ruleChainErrorActorTest() throws Exception {
TbActorSystemSettings settings = new TbActorSystemSettings(0, 0, 0);
TbActorSystem system = spy(new DefaultTbActorSystem(settings));
system.createDispatcher(RULE_DISPATCHER_NAME, mock());
TbActorMailbox tenantCtx = new TbActorMailbox(system, settings, null, mock(), mock(), null);
tenantActor.init(tenantCtx);
TbMsg msg = mock(TbMsg.class);
when(ruleChainService.findRuleChainById(tenantId, ruleChainId)).thenReturn(new RuleChain(ruleChainId));
RuleChainToRuleChainMsg ruleChainMsg = new RuleChainToRuleChainMsg(ruleChainId, null, msg, null);
tenantActor.doProcess(ruleChainMsg);
verify(system).createChildActor(eq(RULE_DISPATCHER_NAME), any(RuleChainActor.ActorCreator.class), any());
reset(system);
tenantActor.doProcess(ruleChainMsg);
verify(system, never()).createChildActor(any(), any(), any());
//Delete rule-chain
TbActorRef ruleChainActor = system.getActor(new TbEntityActorId(ruleChainId));
assertNotNull(ruleChainActor);
system.stop(ruleChainActor);
when(ruleChainService.findRuleChainById(tenantId, ruleChainId)).thenReturn(null);
tenantActor.doProcess(ruleChainMsg);
verify(system).createChildActor(eq(RULE_DISPATCHER_NAME), any(RuleChainErrorActor.ActorCreator.class), any());
reset(system);
tenantActor.doProcess(ruleChainMsg);
verify(system, never()).createChildActor(any(), any(), any());
system.stop();
}
}

126
application/src/test/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionServiceTest.java

@ -0,0 +1,126 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.subscription;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.LoggerFactory;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.service.ws.WebSocketSessionRef;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Executors;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class DefaultTbLocalSubscriptionServiceTest {
ListAppender<ILoggingEvent> testLogAppender;
TbLocalSubscriptionService subscriptionService;
@BeforeEach
public void setUp() throws Exception {
Logger logger = (Logger) LoggerFactory.getLogger(DefaultTbLocalSubscriptionService.class);
testLogAppender = new ListAppender<>();
testLogAppender.start();
logger.addAppender(testLogAppender);
RateLimitService rateLimitService = mock();
when(rateLimitService.checkRateLimit(eq(LimitedApi.WS_SUBSCRIPTIONS), any(Object.class), nullable(String.class))).thenReturn(true);
PartitionService partitionService = mock();
when(partitionService.resolve(any(), any(), any())).thenReturn(TopicPartitionInfo.builder().build());
subscriptionService = new DefaultTbLocalSubscriptionService(mock(), mock(), mock(), partitionService, mock(), mock(), mock(), rateLimitService);
ReflectionTestUtils.setField(subscriptionService, "serviceId", "serviceId");
}
@AfterEach
public void tearDown() {
if (testLogAppender != null) {
testLogAppender.stop();
Logger logger = (Logger) LoggerFactory.getLogger(DefaultTbLocalSubscriptionService.class);
logger.detachAppender(testLogAppender);
}
}
@Test
public void addSubscriptionConcurrentModificationTest() throws Exception {
ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
TenantId tenantId = new TenantId(UUID.randomUUID());
DeviceId deviceId = new DeviceId(UUID.randomUUID());
WebSocketSessionRef sessionRef = mock();
ReflectionTestUtils.setField(subscriptionService, "subscriptionUpdateExecutor", executorService);
List<ListenableFuture<?>> futures = new ArrayList<>();
try {
subscriptionService.onCoreStartupMsg(TransportProtos.CoreStartupMsg.newBuilder().addAllPartitions(List.of(0)).getDefaultInstanceForType());
for (int i = 0; i < 50; i++) {
futures.add(executorService.submit(() -> subscriptionService.addSubscription(createSubscription(tenantId, deviceId), sessionRef)));
}
Futures.allAsList(futures).get();
} finally {
executorService.shutdownNow();
}
List<ILoggingEvent> logs = testLogAppender.list;
boolean exceptionLogged = logs.stream()
.filter(event -> event.getThrowableProxy() != null)
.map(event -> event.getThrowableProxy().getClassName())
.anyMatch(log -> log.equals("java.util.ConcurrentModificationException"));
assertFalse(exceptionLogged, "Detected ConcurrentModificationException!");
}
private TbSubscription<?> createSubscription(TenantId tenantId, EntityId entityId) {
Map<String, Long> keys = new HashMap<>();
for (int i = 0; i < 50; i++) {
keys.put(RandomStringUtils.randomAlphanumeric(5), 1L);
}
return TbAttributeSubscription.builder()
.tenantId(tenantId)
.entityId(entityId)
.subscriptionId(1)
.sessionId(RandomStringUtils.randomAlphanumeric(5))
.keyStates(keys)
.build();
}
}

186
application/src/test/java/org/thingsboard/server/service/subscription/TbEntityLocalSubsInfoTest.java

@ -0,0 +1,186 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.subscription;
import org.junit.jupiter.api.Test;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class TbEntityLocalSubsInfoTest {
@Test
public void addTest() {
Set<TbAttributeSubscription> expectedSubs = new HashSet<>();
TbEntityLocalSubsInfo subsInfo = createSubsInfo();
TenantId tenantId = subsInfo.getTenantId();
EntityId entityId = subsInfo.getEntityId();
TbAttributeSubscription attrSubscription1 = TbAttributeSubscription.builder()
.sessionId("session1")
.tenantId(tenantId)
.entityId(entityId)
.keyStates(Map.of("key1", 1L, "key2", 2L))
.build();
expectedSubs.add(attrSubscription1);
TbEntitySubEvent created = subsInfo.add(attrSubscription1);
assertFalse(subsInfo.isEmpty());
assertNotNull(created);
assertEquals(expectedSubs, subsInfo.getSubs());
checkEvent(created, expectedSubs, ComponentLifecycleEvent.CREATED);
assertNull(subsInfo.add(attrSubscription1));
TbAttributeSubscription attrSubscription2 = TbAttributeSubscription.builder()
.sessionId("session2")
.tenantId(tenantId)
.entityId(entityId)
.keyStates(Map.of("key3", 3L, "key4", 4L))
.build();
expectedSubs.add(attrSubscription2);
TbEntitySubEvent updated = subsInfo.add(attrSubscription2);
assertNotNull(updated);
assertEquals(expectedSubs, subsInfo.getSubs());
checkEvent(updated, expectedSubs, ComponentLifecycleEvent.UPDATED);
}
@Test
public void removeTest() {
Set<TbAttributeSubscription> expectedSubs = new HashSet<>();
TbEntityLocalSubsInfo subsInfo = createSubsInfo();
TenantId tenantId = subsInfo.getTenantId();
EntityId entityId = subsInfo.getEntityId();
TbAttributeSubscription attrSubscription1 = TbAttributeSubscription.builder()
.sessionId("session1")
.tenantId(tenantId)
.entityId(entityId)
.keyStates(Map.of("key1", 1L, "key2", 2L))
.build();
TbAttributeSubscription attrSubscription2 = TbAttributeSubscription.builder()
.sessionId("session2")
.tenantId(tenantId)
.entityId(entityId)
.keyStates(Map.of("key3", 3L, "key4", 4L))
.build();
expectedSubs.add(attrSubscription1);
expectedSubs.add(attrSubscription2);
subsInfo.add(attrSubscription1);
subsInfo.add(attrSubscription2);
assertEquals(expectedSubs, subsInfo.getSubs());
TbEntitySubEvent updatedEvent = subsInfo.remove(attrSubscription1);
expectedSubs.remove(attrSubscription1);
assertNotNull(updatedEvent);
assertEquals(expectedSubs, subsInfo.getSubs());
checkEvent(updatedEvent, expectedSubs, ComponentLifecycleEvent.UPDATED);
TbEntitySubEvent deletedEvent = subsInfo.remove(attrSubscription2);
expectedSubs.remove(attrSubscription2);
assertNotNull(deletedEvent);
assertEquals(expectedSubs, subsInfo.getSubs());
checkEvent(deletedEvent, expectedSubs, ComponentLifecycleEvent.DELETED);
assertTrue(subsInfo.isEmpty());
}
@Test
public void removeAllTest() {
TbEntityLocalSubsInfo subsInfo = createSubsInfo();
TenantId tenantId = subsInfo.getTenantId();
EntityId entityId = subsInfo.getEntityId();
TbAttributeSubscription attrSubscription1 = TbAttributeSubscription.builder()
.sessionId("session1")
.tenantId(tenantId)
.entityId(entityId)
.keyStates(Map.of("key1", 1L, "key2", 2L))
.build();
TbAttributeSubscription attrSubscription2 = TbAttributeSubscription.builder()
.sessionId("session2")
.tenantId(tenantId)
.entityId(entityId)
.keyStates(Map.of("key3", 3L, "key4", 4L))
.build();
TbAttributeSubscription attrSubscription3 = TbAttributeSubscription.builder()
.sessionId("session3")
.tenantId(tenantId)
.entityId(entityId)
.keyStates(Map.of("key5", 5L, "key6", 6L))
.build();
subsInfo.add(attrSubscription1);
subsInfo.add(attrSubscription2);
subsInfo.add(attrSubscription3);
assertFalse(subsInfo.isEmpty());
TbEntitySubEvent updatedEvent = subsInfo.removeAll(List.of(attrSubscription1, attrSubscription2));
assertNotNull(updatedEvent);
checkEvent(updatedEvent, Set.of(attrSubscription3), ComponentLifecycleEvent.UPDATED);
assertFalse(subsInfo.isEmpty());
TbEntitySubEvent deletedEvent = subsInfo.removeAll(List.of(attrSubscription3));
assertNotNull(deletedEvent);
checkEvent(deletedEvent, null, ComponentLifecycleEvent.DELETED);
assertTrue(subsInfo.isEmpty());
}
private TbEntityLocalSubsInfo createSubsInfo() {
return new TbEntityLocalSubsInfo(new TenantId(UUID.randomUUID()), new DeviceId(UUID.randomUUID()));
}
private void checkEvent(TbEntitySubEvent event, Set<TbAttributeSubscription> expectedSubs, ComponentLifecycleEvent expectedType) {
assertEquals(expectedType, event.getType());
TbSubscriptionsInfo info = event.getInfo();
if (event.getType() == ComponentLifecycleEvent.DELETED) {
assertNull(info);
return;
}
assertNotNull(info);
assertFalse(info.notifications);
assertFalse(info.alarms);
assertFalse(info.attrAllKeys);
assertFalse(info.tsAllKeys);
assertNull(info.tsKeys);
assertEquals(getAttrKeys(expectedSubs), info.attrKeys);
}
private Set<String> getAttrKeys(Set<TbAttributeSubscription> attributeSubscriptions) {
return attributeSubscriptions.stream().map(s -> s.getKeyStates().keySet()).flatMap(Collection::stream).collect(Collectors.toSet());
}
}

82
application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java

@ -25,9 +25,9 @@ import org.eclipse.leshan.client.LeshanClient;
import org.eclipse.leshan.client.object.Security;
import org.eclipse.leshan.core.ResponseCode;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.TestPropertySource;
@ -54,6 +54,7 @@ import org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryMappingC
import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.AbstractLwM2MBootstrapServerCredential;
import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.LwM2MBootstrapServerCredential;
import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.NoSecLwM2MBootstrapServerCredential;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataPageLink;
import org.thingsboard.server.common.data.query.EntityDataQuery;
@ -70,9 +71,9 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.LatestValueCmd;
import org.thingsboard.server.transport.AbstractTransportIntegrationTest;
import org.thingsboard.server.transport.lwm2m.client.LwM2MTestClient;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext;
import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler;
import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler;
import java.io.IOException;
import java.net.ServerSocket;
import java.util.ArrayList;
import java.util.Arrays;
@ -107,7 +108,10 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfil
public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportIntegrationTest {
@SpyBean
LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandlerTest;
protected LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandlerTest;
@SpyBean
protected DefaultLwM2mUplinkMsgHandler defaultUplinkMsgHandlerTest;
@Autowired
private LwM2mClientContext clientContextTest;
@ -117,7 +121,6 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
public static final int securityPort = 5686;
public static final int portBs = 5687;
public static final int securityPortBs = 5688;
public static final int[] SERVERS_PORT_NUMBERS = {port, securityPort, portBs, securityPortBs};
public static final String host = "localhost";
public static final String hostBs = "localhost";
@ -172,12 +175,10 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
protected final Set<Lwm2mTestHelper.LwM2MClientState> expectedStatusesRegistrationLwm2mSuccess = new HashSet<>(Arrays.asList(ON_INIT, ON_REGISTRATION_STARTED, ON_REGISTRATION_SUCCESS));
protected final Set<Lwm2mTestHelper.LwM2MClientState> expectedStatusesRegistrationLwm2mSuccessUpdate = new HashSet<>(Arrays.asList(ON_INIT, ON_REGISTRATION_STARTED, ON_REGISTRATION_SUCCESS, ON_UPDATE_STARTED, ON_UPDATE_SUCCESS));
protected final Set<Lwm2mTestHelper.LwM2MClientState> expectedStatusesRegistrationBsSuccess = new HashSet<>(Arrays.asList(ON_BOOTSTRAP_STARTED, ON_BOOTSTRAP_SUCCESS, ON_REGISTRATION_STARTED, ON_REGISTRATION_SUCCESS));
protected DeviceProfile deviceProfile;
protected ScheduledExecutorService executor;
protected LwM2MTestClient lwM2MTestClient;
private String[] resources;
protected String deviceId;
protected boolean isWriteAttribute = false;
protected boolean supportFormatOnly_SenMLJSON_SenMLCBOR = false;
@Before
@ -186,14 +187,11 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
}
@After
public void after() {
public void after() throws Exception {
clientDestroy();
executor.shutdownNow();
}
@AfterClass
public static void afterClass() {
awaitServersDestroy();
if (executor != null && !executor.isShutdown()) {
executor.shutdownNow();
}
}
private void init() throws Exception {
@ -218,8 +216,8 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
String endpoint,
boolean queueMode) throws Exception {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITH_PARAMS, getBootstrapServerCredentialsNoSec(NONE));
createDeviceProfile(transportConfiguration);
Device device = createDevice(deviceCredentials, endpoint);
DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + endpoint, transportConfiguration);
Device device = createLwm2mDevice(deviceCredentials, endpoint, deviceProfile.getId());
SingleEntityFilter sef = new SingleEntityFilter();
sef.setSingleEntity(device.getId());
@ -255,29 +253,30 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
}
protected void createDeviceProfile(Lwm2mDeviceProfileTransportConfiguration transportConfiguration) throws Exception {
deviceProfile = new DeviceProfile();
deviceProfile.setName("LwM2M");
deviceProfile.setType(DeviceProfileType.DEFAULT);
deviceProfile.setTenantId(tenantId);
deviceProfile.setTransportType(DeviceTransportType.LWM2M);
deviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED);
deviceProfile.setDescription(deviceProfile.getName());
protected DeviceProfile createLwm2mDeviceProfile(String name, Lwm2mDeviceProfileTransportConfiguration transportConfiguration) throws Exception {
DeviceProfile lwm2mDeviceProfile = new DeviceProfile();
lwm2mDeviceProfile.setName(name);
lwm2mDeviceProfile.setType(DeviceProfileType.DEFAULT);
lwm2mDeviceProfile.setTenantId(tenantId);
lwm2mDeviceProfile.setTransportType(DeviceTransportType.LWM2M);
lwm2mDeviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED);
lwm2mDeviceProfile.setDescription(name);
DeviceProfileData deviceProfileData = new DeviceProfileData();
deviceProfileData.setConfiguration(new DefaultDeviceProfileConfiguration());
deviceProfileData.setProvisionConfiguration(new DisabledDeviceProfileProvisionConfiguration(null));
deviceProfileData.setTransportConfiguration(transportConfiguration);
deviceProfile.setProfileData(deviceProfileData);
lwm2mDeviceProfile.setProfileData(deviceProfileData);
deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
Assert.assertNotNull(deviceProfile);
lwm2mDeviceProfile = doPost("/api/deviceProfile", lwm2mDeviceProfile, DeviceProfile.class);
Assert.assertNotNull(lwm2mDeviceProfile);
return lwm2mDeviceProfile;
}
protected Device createDevice(LwM2MDeviceCredentials credentials, String endpoint) throws Exception {
protected Device createLwm2mDevice(LwM2MDeviceCredentials credentials, String endpoint, DeviceProfileId deviceProfileId) throws Exception {
Device device = new Device();
device.setName(endpoint);
device.setDeviceProfileId(deviceProfile.getId());
device.setDeviceProfileId(deviceProfileId);
device.setTenantId(tenantId);
device = doPost("/api/device", device, Device.class);
Assert.assertNotNull(device);
@ -319,7 +318,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
try (ServerSocket socket = new ServerSocket(0)) {
int clientPort = socket.getLocalPort();
lwM2MTestClient.init(security, securityBs, clientPort, isRpc,
this.defaultLwM2mUplinkMsgHandlerTest, this.clientContextTest, isWriteAttribute,
this.defaultLwM2mUplinkMsgHandlerTest, this.clientContextTest,
clientDtlsCidLength, queueMode, supportFormatOnly_SenMLJSON_SenMLCBOR);
}
}
@ -385,25 +384,6 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
return credentials;
}
private static void awaitServersDestroy() {
await("One of servers ports number is not free")
.atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.until(() -> isServerPortsAvailable() == null);
}
private static String isServerPortsAvailable() {
for (int port : SERVERS_PORT_NUMBERS) {
try (ServerSocket serverSocket = new ServerSocket(port)) {
serverSocket.close();
Assert.assertEquals(true, serverSocket.isClosed());
} catch (IOException e) {
log.warn(String.format("Port %n still in use", port));
return (String.format("Port %n still in use", port));
}
}
return null;
}
private static void awaitClientDestroy(LeshanClient leshanClient) {
await("Destroy LeshanClient: delete All is registered Servers.")
.atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
@ -456,4 +436,10 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
return JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
}
protected long countUpdateReg() {
return Mockito.mockingDetails(defaultUplinkMsgHandlerTest)
.getInvocations().stream()
.filter(invocation -> invocation.getMethod().getName().equals("updatedReg"))
.count();
}
}

11
application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java

@ -42,6 +42,7 @@ public class Lwm2mTestHelper {
public static final int RESOURCE_ID_11 = 11;
public static final int RESOURCE_ID_14 = 14;
public static final int RESOURCE_ID_15 = 15;
public static final int RESOURCE_ID_5700 = 5700;
public static final int RESOURCE_INSTANCE_ID_0 = 0;
public static final int RESOURCE_INSTANCE_ID_2 = 2;
@ -51,6 +52,12 @@ public class Lwm2mTestHelper {
public static final String RESOURCE_ID_NAME_19_0_2 = "dataCreationTime";
public static final String RESOURCE_ID_NAME_19_1_0 = "dataWrite";
public static final String RESOURCE_ID_NAME_19_0_3 = "dataDescription";
public static final String RESOURCE_ID_NAME_3303_12_5700 = "sensorValue";
public static final double RESOURCE_ID_3303_12_5700_VALUE_0 = 25.05d;
public static final double RESOURCE_ID_3303_12_5700_VALUE_1 = 35.12d;
public static long RESOURCE_ID_3303_12_5700_TS_0 = 0;
public static long RESOURCE_ID_3303_12_5700_TS_1 = 0;
public static final int RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS = 3000;
public enum LwM2MClientState {
@ -72,8 +79,8 @@ public class Lwm2mTestHelper {
ON_DEREGISTRATION_FAILURE(14, "onDeregistrationFailure"),
ON_DEREGISTRATION_TIMEOUT(15, "onDeregistrationTimeout"),
ON_EXPECTED_ERROR(16, "onUnexpectedError"),
ON_READ_CONNECTION_ID (17, "onReadConnection"),
ON_WRITE_CONNECTION_ID (18, "onWriteConnection");
ON_READ_CONNECTION_ID(17, "onReadConnection"),
ON_WRITE_CONNECTION_ID(18, "onWriteConnection");
public int code;
public String type;

4
application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/LwM2mAttributesTest.java

@ -49,13 +49,13 @@ public class LwM2mAttributesTest {
@ParameterizedTest(name = "Tests {index} : {0}")
@MethodSource("doesntSupportAttributesWithoutValue")
public void check_attribute_can_not_be_created_without_value(LwM2mAttributeModel<?> model) {
assertThrows(UnsupportedOperationException.class, () -> LwM2mAttributes.create(model));
assertThrows(IllegalArgumentException.class, () -> LwM2mAttributes.create(model));
}
@ParameterizedTest(name = "Tests {index} : {0}")
@MethodSource("doesntSupportAttributesWithValueNull")
public void check_attribute_can_not_be_created_with_null(LwM2mAttributeModel<?> model) {
assertThrows(NullPointerException.class, () -> LwM2mAttributes.create(model, null));
assertThrows(IllegalArgumentException.class, () -> LwM2mAttributes.create(model, null));
}
private static Stream<Arguments> supportNullAttributes() throws InvalidAttributeException {

27
application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java

@ -137,10 +137,11 @@ public class LwM2MTestClient {
private Map<LwM2MClientState, Integer> clientDtlsCid;
private LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandlerTest;
private LwM2mClientContext clientContext;
private LwM2mTemperatureSensor lwM2mTemperatureSensor12;
public void init(Security security, Security securityBs, int port, boolean isRpc,
LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandler,
LwM2mClientContext clientContext, boolean isWriteAttribute, Integer cIdLength, boolean queueMode,
LwM2mClientContext clientContext, Integer cIdLength, boolean queueMode,
boolean supportFormatOnly_SenMLJSON_SenMLCBOR) throws InvalidDDFFileException, IOException {
Assert.assertNull("client already initialized", leshanClient);
this.defaultLwM2mUplinkMsgHandlerTest = defaultLwM2mUplinkMsgHandler;
@ -150,7 +151,7 @@ public class LwM2MTestClient {
models.addAll(ObjectLoader.loadDdfFile(LwM2MTestClient.class.getClassLoader().getResourceAsStream("lwm2m/" + resourceName), resourceName));
}
LwM2mModel model = new StaticModel(models);
ObjectsInitializer initializer = isWriteAttribute ? new TbObjectsInitializer(model) : new ObjectsInitializer(model);
ObjectsInitializer initializer = new ObjectsInitializer(model);
if (securityBs != null && security != null) {
// SECURITY
security.setId(serverId);
@ -159,11 +160,11 @@ public class LwM2MTestClient {
initializer.setClassForObject(SECURITY, Security.class);
initializer.setInstancesForObject(SECURITY, instances);
// SERVER
Server lwm2mServer = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60));
Server lwm2mServer = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60));
lwm2mServer.setId(serverId);
Server serverBs = new Server(shortServerIdBs0, TimeUnit.MINUTES.toSeconds(60));
Server serverBs = new Server(shortServerIdBs0, TimeUnit.MINUTES.toSeconds(60));
serverBs.setId(serverIdBs);
instances = new LwM2mInstanceEnabler[]{serverBs, lwm2mServer};
instances = new LwM2mInstanceEnabler[]{serverBs, lwm2mServer};
initializer.setClassForObject(SERVER, Server.class);
initializer.setInstancesForObject(SERVER, instances);
} else if (securityBs != null) {
@ -177,7 +178,7 @@ public class LwM2MTestClient {
// SERVER
Server lwm2mServer = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60));
lwm2mServer.setId(serverId);
initializer.setInstancesForObject(SERVER, lwm2mServer );
initializer.setInstancesForObject(SERVER, lwm2mServer);
}
initializer.setInstancesForObject(DEVICE, lwM2MDevice = new SimpleLwM2MDevice(executor));
@ -190,7 +191,7 @@ public class LwM2MTestClient {
locationParams.getPos();
initializer.setInstancesForObject(LOCATION, new LwM2mLocation(locationParams.getLatitude(), locationParams.getLongitude(), locationParams.getScaleFactor(), executor, OBJECT_INSTANCE_ID_0));
LwM2mTemperatureSensor lwM2mTemperatureSensor0 = new LwM2mTemperatureSensor(executor, OBJECT_INSTANCE_ID_0);
LwM2mTemperatureSensor lwM2mTemperatureSensor12 = new LwM2mTemperatureSensor(executor, OBJECT_INSTANCE_ID_12);
lwM2mTemperatureSensor12 = new LwM2mTemperatureSensor(executor, OBJECT_INSTANCE_ID_12);
initializer.setInstancesForObject(TEMPERATURE_SENSOR, lwM2mTemperatureSensor0, lwM2mTemperatureSensor12);
List<LwM2mObjectEnabler> enablers = initializer.createAll();
@ -239,11 +240,11 @@ public class LwM2MTestClient {
boolean supportDeprecatedCiphers = false;
clientCoapConfig.set(DTLS_RECOMMENDED_CIPHER_SUITES_ONLY, !supportDeprecatedCiphers);
if (cIdLength!= null) {
if (cIdLength != null) {
setDtlsConnectorConfigCidLength(clientCoapConfig, cIdLength);
}
if (cIdLength!= null) {
if (cIdLength != null) {
setDtlsConnectorConfigCidLength(clientCoapConfig, cIdLength);
}
@ -262,12 +263,12 @@ public class LwM2MTestClient {
// Configure Registration Engine
DefaultRegistrationEngineFactory engineFactory = new DefaultRegistrationEngineFactory();
// old
// old
/**
* Force reconnection/rehandshake on registration update.
*/
int comPeriodInSec = 5;
if (comPeriodInSec > 0) engineFactory.setCommunicationPeriod(comPeriodInSec * 1000);
if (comPeriodInSec > 0) engineFactory.setCommunicationPeriod(comPeriodInSec * 1000);
// engineFactory.setCommunicationPeriod(5000); // old
/**
* By default client will try to resume DTLS session by using abbreviated Handshake. This option force to always do a full handshake."
@ -288,7 +289,7 @@ public class LwM2MTestClient {
builder.setDataSenders(new ManualDataSender());
builder.setRegistrationEngineFactory(engineFactory);
Map<ContentFormat, NodeDecoder> decoders = new HashMap<>();
Map<ContentFormat, NodeEncoder> encoders = new HashMap<>();
Map<ContentFormat, NodeEncoder> encoders = new HashMap<>();
if (supportFormatOnly_SenMLJSON_SenMLCBOR) {
// decoders.put(ContentFormat.OPAQUE, new LwM2mNodeOpaqueDecoder());
decoders.put(ContentFormat.CBOR, new LwM2mNodeCborDecoder());
@ -316,7 +317,6 @@ public class LwM2MTestClient {
clientDtlsCid = new HashMap<>();
clientStates.add(ON_INIT);
leshanClient = builder.build();
lwM2mTemperatureSensor12.setLeshanClient(leshanClient);
LwM2mClientObserver observer = new LwM2mClientObserver() {
@Override
@ -453,6 +453,7 @@ public class LwM2MTestClient {
if (isStartLw) {
this.awaitClientAfterStartConnectLw();
}
lwM2mTemperatureSensor12.setLeshanClient(leshanClient);
}
}

86
application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mTemperatureSensor.java

@ -26,16 +26,20 @@ import org.eclipse.leshan.core.request.ContentFormat;
import org.eclipse.leshan.core.request.argument.Arguments;
import org.eclipse.leshan.core.response.ExecuteResponse;
import org.eclipse.leshan.core.response.ReadResponse;
import org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper;
import javax.security.auth.Destroyable;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_VALUE_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_VALUE_1;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS;
@Slf4j
public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destroyable {
@ -46,18 +50,23 @@ public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destr
private double maxMeasuredValue = currentTemp;
private LeshanClient leshanClient;
private List<Double> containingValues;
private int cntIdentitySystem;
protected static final Random RANDOM = new Random();
private static final List<Integer> supportedResources = Arrays.asList(5601, 5602, 5700, 5701);
public LwM2mTemperatureSensor() {
private LwM2mServer registeredServer;
private ManualDataSender sender;
private int resourceIdForSendCollected = 5700;
public LwM2mTemperatureSensor() {
}
public LwM2mTemperatureSensor(ScheduledExecutorService executorService, Integer id) {
try {
if (id != null) this.setId(id);
executorService.scheduleWithFixedDelay(this::adjustTemperature, 2000, 2000, TimeUnit.MILLISECONDS);
executorService.scheduleWithFixedDelay(this::adjustTemperature, 2000, 2000, TimeUnit.MILLISECONDS);
} catch (Throwable e) {
log.error("[{}]Throwable", e.toString());
e.printStackTrace();
@ -66,23 +75,33 @@ public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destr
@Override
public synchronized ReadResponse read(LwM2mServer identity, int resourceId) {
log.info("Read on Temperature resource /[{}]/[{}]/[{}]", getModel().id, getId(), resourceId);
log.trace("Read on Temperature resource /[{}]/[{}]/[{}]", getModel().id, getId(), resourceId);
if (this.registeredServer == null && this.leshanClient != null && getId() == 12) {
try {
Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_0 = Instant.now().toEpochMilli();
this.registeredServer = this.leshanClient.getRegisteredServers().values().iterator().next();
this.sender = (ManualDataSender) this.leshanClient.getSendService().getDataSender(ManualDataSender.DEFAULT_NAME);
this.sender.collectData(Arrays.asList(getPathForCollectedValue(resourceIdForSendCollected)));
} catch (Exception e) {
log.error("[{}] Sender for SendCollected", e.toString());
e.printStackTrace();
}
}
switch (resourceId) {
case 5601:
return ReadResponse.success(resourceId, getTwoDigitValue(minMeasuredValue));
case 5602:
return ReadResponse.success(resourceId, getTwoDigitValue(maxMeasuredValue));
case 5700:
if (identity == LwM2mServer.SYSTEM) {
setTemperature();
setData();
return ReadResponse.success(resourceId, getTwoDigitValue(currentTemp));
} else if (this.getId() == 12 && this.leshanClient != null) {
containingValues = new ArrayList<>();
sendCollected(5700);
return ReadResponse.success(resourceId, getData());
if (identity == LwM2mServer.SYSTEM) {
double val5700 = cntIdentitySystem == 0 ? RESOURCE_ID_3303_12_5700_VALUE_0 : RESOURCE_ID_3303_12_5700_VALUE_1;
cntIdentitySystem++;
return ReadResponse.success(resourceId, val5700);
} else {
return ReadResponse.success(resourceId, getTwoDigitValue(currentTemp));
if (cntIdentitySystem == 1 && this.getId() == 12 && this.leshanClient != null) {
sendCollected();
}
return super.read(identity, resourceId);
}
case 5701:
return ReadResponse.success(resourceId, UNIT_CELSIUS);
@ -117,10 +136,11 @@ public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destr
}
}
private void setTemperature(){
private void setTemperature() {
float delta = (RANDOM.nextInt(20) - 10) / 10f;
currentTemp += delta;
}
private synchronized Integer adjustMinMaxMeasuredValue(double newTemperature) {
if (newTemperature > maxMeasuredValue) {
maxMeasuredValue = newTemperature;
@ -143,7 +163,7 @@ public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destr
return supportedResources;
}
protected void setLeshanClient(LeshanClient leshanClient){
protected void setLeshanClient(LeshanClient leshanClient) {
this.leshanClient = leshanClient;
}
@ -151,40 +171,22 @@ public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destr
public void destroy() {
}
private void sendCollected(int resourceId) {
private void sendCollected() {
try {
LwM2mServer registeredServer = this.leshanClient.getRegisteredServers().values().iterator().next();
ManualDataSender sender = this.leshanClient.getSendService().getDataSender(ManualDataSender.DEFAULT_NAME,
ManualDataSender.class);
sender.collectData(Arrays.asList(getPathForCollectedValue(resourceId)));
Thread.sleep(1000);
sender.collectData(Arrays.asList(getPathForCollectedValue(resourceId)));
if ((Instant.now().toEpochMilli() - Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_0) < RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS) {
Thread.sleep(RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS);
}
sender.collectData(Arrays.asList(getPathForCollectedValue(resourceIdForSendCollected)));
Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_1 = Instant.now().toEpochMilli();
sender.sendCollectedData(registeredServer, ContentFormat.SENML_JSON, 1000, false);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
private LwM2mPath getPathForCollectedValue(int resourceId) {
return new LwM2mPath(3303, this.getId(), resourceId);
}
private double getData() {
if (containingValues.size() > 1) {
Integer t0 = Math.toIntExact(Math.round(containingValues.get(0) * 100));
Integer t1 = Math.toIntExact(Math.round(containingValues.get(1) * 100));
long to_t1 = (((long) t0) << 32) | (t1 & 0xffffffffL);
return Double.longBitsToDouble(to_t1);
} else {
return currentTemp;
}
}
private void setData() {
if (containingValues == null){
containingValues = new ArrayList<>();
}
containingValues.add(getTwoDigitValue(currentTemp));
}
}

73
application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java

@ -20,7 +20,7 @@ import org.eclipse.leshan.client.resource.BaseInstanceEnabler;
import org.eclipse.leshan.client.servers.LwM2mServer;
import org.eclipse.leshan.core.Destroyable;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.model.ResourceModel.Type;
import org.eclipse.leshan.core.node.LwM2mResource;
import org.eclipse.leshan.core.request.argument.Arguments;
import org.eclipse.leshan.core.response.ExecuteResponse;
@ -30,7 +30,6 @@ import org.eclipse.leshan.core.response.WriteResponse;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.PrimitiveIterator;
@ -46,9 +45,46 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl
private static final Random RANDOM = new Random();
private static final int min = 5;
private static final int max = 50;
private static final PrimitiveIterator.OfInt randomIterator = new Random().ints(min,max + 1).iterator();
private static final PrimitiveIterator.OfInt randomIterator = new Random().ints(min, max + 1).iterator();
private static final List<Integer> supportedResources = Arrays.asList(0, 1, 2, 3, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21);
/**
* 0: DC power
* 1: Internal Battery
* 2: External Battery
* 3: Fuel Cell
* 4: Power over Ethernet
* 5: USB
* 6: AC (Mains) power
* 7: Solar
*/
private static final Map<Integer, Long> availablePowerSources =
Map.of(0, 0L, 1, 1L, 2, 7L);
private static Map<Integer, Long> powerSourceVoltage =
Map.of(0, 12000L, 1, 12400L, 7, 14600L); //mV
private static Map<Integer, Long> powerSourceCurrent =
Map.of(0, 72000L, 1, 2000L, 7, 25000L); // mA
/**
* 0=No error
* 1=Low battery power
* 2=External power supply off
* 3=GPS module failure
* 4=Low received signal strength
* 5=Out of memory
* 6=SMS failure
* 7=IP connectivity failure
* 8=Peripheral malfunction
* 9..15=Reserved for future use
* 16..32=Device specific error codes
*
* When the single Device Object Instance is initiated, there is only one error code Resource Instance whose value is equal to 0 that means no error.
* When the first error happens, the LwM2M Client changes error code Resource Instance to any non-zero value to indicate the error type.
* When any other error happens, a new error code Resource Instance is created.
* When an error associated with a Resource Instance is no longer present, that Resource Instance is deleted.
* When the single existing error is no longer present, the LwM2M Client returns to the original no error state where Instance 0 has value 0.
*/
private static Map<Integer, Long> errorCode =
Map.of(0, 0L); // 0-32
public SimpleLwM2MDevice() {
}
@ -81,15 +117,17 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl
case 3:
return ReadResponse.success(resourceId, getFirmwareVersion());
case 6:
return ReadResponse.success(resourceId, getAvailablePowerSources(), ResourceModel.Type.INTEGER);
return ReadResponse.success(resourceId, getAvailablePowerSources(), Type.INTEGER);
case 7:
return ReadResponse.success(resourceId, getPowerSourceVoltage(), Type.INTEGER);
case 8:
return ReadResponse.success(resourceId, getPowerSourceCurrent(), Type.INTEGER);
case 9:
return ReadResponse.success(resourceId, getBatteryLevel());
case 10:
return ReadResponse.success(resourceId, getMemoryFree());
case 11:
Map<Integer, Long> errorCodes = new HashMap<>();
errorCodes.put(0, getErrorCode());
return ReadResponse.success(resourceId, errorCodes, ResourceModel.Type.INTEGER);
return ReadResponse.success(resourceId, getErrorCodes(), Type.INTEGER);
case 14:
return ReadResponse.success(resourceId, getUtcOffset());
case 15:
@ -156,16 +194,19 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl
return "1.0.2";
}
private long getErrorCode() {
return 0;
private Map<Integer, ?> getAvailablePowerSources() {
return availablePowerSources;
}
private Map<Integer, Long> getAvailablePowerSources() {
Map<Integer, Long> availablePowerSources = new HashMap<>();
availablePowerSources.put(0, 1L);
availablePowerSources.put(1, 2L);
availablePowerSources.put(2, 5L);
return availablePowerSources;
private Map<Integer, ?> getPowerSourceVoltage() {
return powerSourceVoltage;
}
private Map<Integer, ?> getPowerSourceCurrent() {
return powerSourceCurrent;
}
private Map<Integer, ?> getErrorCodes() {
return errorCode;
}
private int getBatteryLevel() {

732
application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbLwm2mObjectEnabler.java

@ -1,732 +0,0 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.lwm2m.client;
import org.eclipse.leshan.client.LwM2mClient;
import org.eclipse.leshan.client.resource.BaseObjectEnabler;
import org.eclipse.leshan.client.resource.DummyInstanceEnabler;
import org.eclipse.leshan.client.resource.LwM2mInstanceEnabler;
import org.eclipse.leshan.client.resource.LwM2mInstanceEnablerFactory;
import org.eclipse.leshan.client.resource.listener.ResourceListener;
import org.eclipse.leshan.client.servers.LwM2mServer;
import org.eclipse.leshan.client.servers.ServersInfoExtractor;
import org.eclipse.leshan.client.util.LinkFormatHelper;
import org.eclipse.leshan.core.Destroyable;
import org.eclipse.leshan.core.LwM2mId;
import org.eclipse.leshan.core.Startable;
import org.eclipse.leshan.core.Stoppable;
import org.eclipse.leshan.core.link.lwm2m.LwM2mLink;
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttribute;
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet;
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.node.LwM2mMultipleResource;
import org.eclipse.leshan.core.node.LwM2mObject;
import org.eclipse.leshan.core.node.LwM2mObjectInstance;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.node.LwM2mResource;
import org.eclipse.leshan.core.node.LwM2mResourceInstance;
import org.eclipse.leshan.core.request.BootstrapDeleteRequest;
import org.eclipse.leshan.core.request.BootstrapReadRequest;
import org.eclipse.leshan.core.request.BootstrapWriteRequest;
import org.eclipse.leshan.core.request.ContentFormat;
import org.eclipse.leshan.core.request.CreateRequest;
import org.eclipse.leshan.core.request.DeleteRequest;
import org.eclipse.leshan.core.request.DiscoverRequest;
import org.eclipse.leshan.core.request.DownlinkRequest;
import org.eclipse.leshan.core.request.ExecuteRequest;
import org.eclipse.leshan.core.request.ObserveRequest;
import org.eclipse.leshan.core.request.ReadRequest;
import org.eclipse.leshan.core.request.WriteAttributesRequest;
import org.eclipse.leshan.core.request.WriteRequest;
import org.eclipse.leshan.core.request.WriteRequest.Mode;
import org.eclipse.leshan.core.response.BootstrapDeleteResponse;
import org.eclipse.leshan.core.response.BootstrapReadResponse;
import org.eclipse.leshan.core.response.BootstrapWriteResponse;
import org.eclipse.leshan.core.response.CreateResponse;
import org.eclipse.leshan.core.response.DeleteResponse;
import org.eclipse.leshan.core.response.DiscoverResponse;
import org.eclipse.leshan.core.response.ExecuteResponse;
import org.eclipse.leshan.core.response.ObserveResponse;
import org.eclipse.leshan.core.response.ReadResponse;
import org.eclipse.leshan.core.response.WriteAttributesResponse;
import org.eclipse.leshan.core.response.WriteResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
public class TbLwm2mObjectEnabler extends BaseObjectEnabler implements Destroyable, Startable, Stoppable {
private static Logger LOG = LoggerFactory.getLogger(DummyInstanceEnabler.class);
protected Map<Integer, LwM2mInstanceEnabler> instances;
protected LwM2mInstanceEnablerFactory instanceFactory;
protected ContentFormat defaultContentFormat;
private LinkFormatHelper tbLinkFormatHelper;
protected Map<LwM2mPath, LwM2mAttributeSet> lwM2mAttributes;
public TbLwm2mObjectEnabler(int id, ObjectModel objectModel, Map<Integer, LwM2mInstanceEnabler> instances,
LwM2mInstanceEnablerFactory instanceFactory, ContentFormat defaultContentFormat) {
super(id, objectModel);
this.instances = new HashMap<>(instances);
;
this.instanceFactory = instanceFactory;
this.defaultContentFormat = defaultContentFormat;
for (Entry<Integer, LwM2mInstanceEnabler> entry : this.instances.entrySet()) {
instances.put(entry.getKey(), entry.getValue());
listenInstance(entry.getValue(), entry.getKey());
}
this.lwM2mAttributes = new HashMap<>();
}
public TbLwm2mObjectEnabler(int id, ObjectModel objectModel) {
super(id, objectModel);
}
@Override
public synchronized List<Integer> getAvailableInstanceIds() {
List<Integer> ids = new ArrayList<>(instances.keySet());
Collections.sort(ids);
return ids;
}
@Override
public synchronized List<Integer> getAvailableResourceIds(int instanceId) {
LwM2mInstanceEnabler instanceEnabler = instances.get(instanceId);
if (instanceEnabler != null) {
return instanceEnabler.getAvailableResourceIds(getObjectModel());
} else {
return Collections.emptyList();
}
}
public synchronized void addInstance(int instanceId, LwM2mInstanceEnabler newInstance) {
instances.put(instanceId, newInstance);
listenInstance(newInstance, instanceId);
fireInstancesAdded(instanceId);
}
public synchronized LwM2mInstanceEnabler getInstance(int instanceId) {
return instances.get(instanceId);
}
public synchronized LwM2mInstanceEnabler removeInstance(int instanceId) {
LwM2mInstanceEnabler removedInstance = instances.remove(instanceId);
if (removedInstance != null) {
fireInstancesRemoved(removedInstance.getId());
}
return removedInstance;
}
@Override
protected CreateResponse doCreate(LwM2mServer server, CreateRequest request) {
if (!getObjectModel().multiple && instances.size() > 0) {
return CreateResponse.badRequest("an instance already exist for this single instance object");
}
if (request.unknownObjectInstanceId()) {
// create instance
LwM2mInstanceEnabler newInstance = createInstance(server, getObjectModel().multiple ? null : 0,
request.getResources());
// add new instance to this object
instances.put(newInstance.getId(), newInstance);
listenInstance(newInstance, newInstance.getId());
fireInstancesAdded(newInstance.getId());
return CreateResponse
.success(new LwM2mPath(request.getPath().getObjectId(), newInstance.getId()).toString());
} else {
List<LwM2mObjectInstance> instanceNodes = request.getObjectInstances();
// checks single object instances
if (!getObjectModel().multiple) {
if (request.getObjectInstances().size() > 1) {
return CreateResponse.badRequest("can not create several instances on this single instance object");
}
if (request.getObjectInstances().get(0).getId() != 0) {
return CreateResponse.badRequest("single instance object must use 0 as ID");
}
}
// ensure instance does not already exists
for (LwM2mObjectInstance instance : instanceNodes) {
if (instances.containsKey(instance.getId())) {
return CreateResponse.badRequest(String.format("instance %d already exists", instance.getId()));
}
}
// create the new instances
int[] instanceIds = new int[request.getObjectInstances().size()];
int i = 0;
for (LwM2mObjectInstance instance : request.getObjectInstances()) {
// create instance
LwM2mInstanceEnabler newInstance = createInstance(server, instance.getId(),
instance.getResources().values());
// add new instance to this object
instances.put(newInstance.getId(), newInstance);
listenInstance(newInstance, newInstance.getId());
// store instance ids
instanceIds[i] = newInstance.getId();
i++;
}
fireInstancesAdded(instanceIds);
return CreateResponse.success();
}
}
protected LwM2mInstanceEnabler createInstance(LwM2mServer server, Integer instanceId,
Collection<LwM2mResource> resources) {
// create the new instance
LwM2mInstanceEnabler newInstance = instanceFactory.create(getObjectModel(), instanceId, instances.keySet());
newInstance.setLwM2mClient(getLwm2mClient());
// add/write resource
for (LwM2mResource resource : resources) {
newInstance.write(server, true, resource.getId(), resource);
}
return newInstance;
}
@Override
protected ReadResponse doRead(LwM2mServer server, ReadRequest request) {
LwM2mPath path = request.getPath();
// Manage Object case
if (path.isObject()) {
List<LwM2mObjectInstance> lwM2mObjectInstances = new ArrayList<>();
for (LwM2mInstanceEnabler instance : instances.values()) {
ReadResponse response = instance.read(server);
if (response.isSuccess()) {
lwM2mObjectInstances.add((LwM2mObjectInstance) response.getContent());
}
}
return ReadResponse.success(new LwM2mObject(getId(), lwM2mObjectInstances));
}
// Manage Instance case
LwM2mInstanceEnabler instance = instances.get(path.getObjectInstanceId());
if (instance == null)
return ReadResponse.notFound();
if (path.getResourceId() == null) {
return instance.read(server);
}
// Manage Resource case
if (path.getResourceInstanceId() == null) {
return instance.read(server, path.getResourceId());
}
// Manage Resource Instance case
return instance.read(server, path.getResourceId(), path.getResourceInstanceId());
}
@Override
protected BootstrapReadResponse doRead(LwM2mServer server, BootstrapReadRequest request) {
// Basic implementation we delegate to classic Read Request
ReadResponse response = doRead(server,
new ReadRequest(request.getContentFormat(), request.getPath(), request.getCoapRequest()));
return new BootstrapReadResponse(response.getCode(), response.getContent(), response.getErrorMessage());
}
@Override
protected ObserveResponse doObserve(final LwM2mServer server, final ObserveRequest request) {
final LwM2mPath path = request.getPath();
// Manage Object case
if (path.isObject()) {
List<LwM2mObjectInstance> lwM2mObjectInstances = new ArrayList<>();
for (LwM2mInstanceEnabler instance : instances.values()) {
ReadResponse response = instance.observe(server);
if (response.isSuccess()) {
lwM2mObjectInstances.add((LwM2mObjectInstance) response.getContent());
}
}
return ObserveResponse.success(new LwM2mObject(getId(), lwM2mObjectInstances));
}
// Manage Instance case
final LwM2mInstanceEnabler instance = instances.get(path.getObjectInstanceId());
if (instance == null)
return ObserveResponse.notFound();
if (path.getResourceId() == null) {
return instance.observe(server);
}
// Manage Resource case
if (path.getResourceInstanceId() == null) {
return instance.observe(server, path.getResourceId());
}
// Manage Resource Instance case
return instance.observe(server, path.getResourceId(), path.getResourceInstanceId());
}
@Override
protected WriteResponse doWrite(LwM2mServer server, WriteRequest request) {
LwM2mPath path = request.getPath();
// Manage Instance case
LwM2mInstanceEnabler instance = instances.get(path.getObjectInstanceId());
if (instance == null)
return WriteResponse.notFound();
if (path.isObjectInstance()) {
return instance.write(server, request.isReplaceRequest(), (LwM2mObjectInstance) request.getNode());
}
// Manage Resource case
if (path.getResourceInstanceId() == null) {
return instance.write(server, request.isReplaceRequest(), path.getResourceId(),
(LwM2mResource) request.getNode());
}
// Manage Resource Instance case
return instance.write(server, false, path.getResourceId(), path.getResourceInstanceId(),
((LwM2mResourceInstance) request.getNode()));
}
@Override
protected BootstrapWriteResponse doWrite(LwM2mServer server, BootstrapWriteRequest request) {
LwM2mPath path = request.getPath();
// Manage Object case
if (path.isObject()) {
for (LwM2mObjectInstance instanceNode : ((LwM2mObject) request.getNode()).getInstances().values()) {
LwM2mInstanceEnabler instanceEnabler = instances.get(instanceNode.getId());
if (instanceEnabler == null) {
doCreate(server, new CreateRequest(path.getObjectId(), instanceNode));
} else {
doWrite(server, new WriteRequest(Mode.REPLACE, path.getObjectId(), instanceEnabler.getId(),
instanceNode.getResources().values()));
}
}
return BootstrapWriteResponse.success();
}
// Manage Instance case
if (path.isObjectInstance()) {
LwM2mObjectInstance instanceNode = (LwM2mObjectInstance) request.getNode();
LwM2mInstanceEnabler instanceEnabler = instances.get(path.getObjectInstanceId());
if (instanceEnabler == null) {
doCreate(server, new CreateRequest(path.getObjectId(), instanceNode));
} else {
doWrite(server, new WriteRequest(Mode.REPLACE, request.getContentFormat(), path.getObjectId(),
path.getObjectInstanceId(), instanceNode.getResources().values()));
}
return BootstrapWriteResponse.success();
}
// Manage resource case
LwM2mResource resource = (LwM2mResource) request.getNode();
LwM2mInstanceEnabler instanceEnabler = instances.get(path.getObjectInstanceId());
if (instanceEnabler == null) {
doCreate(server, new CreateRequest(path.getObjectId(),
new LwM2mObjectInstance(path.getObjectInstanceId(), resource)));
} else {
instanceEnabler.write(server, true, path.getResourceId(), resource);
}
return BootstrapWriteResponse.success();
}
@Override
protected ExecuteResponse doExecute(LwM2mServer server, ExecuteRequest request) {
LwM2mPath path = request.getPath();
LwM2mInstanceEnabler instance = instances.get(path.getObjectInstanceId());
if (instance == null) {
return ExecuteResponse.notFound();
}
return instance.execute(server, path.getResourceId(), request.getArguments());
}
@Override
protected DeleteResponse doDelete(LwM2mServer server, DeleteRequest request) {
LwM2mInstanceEnabler deletedInstance = instances.remove(request.getPath().getObjectInstanceId());
if (deletedInstance != null) {
deletedInstance.onDelete(server);
fireInstancesRemoved(deletedInstance.getId());
return DeleteResponse.success();
}
return DeleteResponse.notFound();
}
@Override
public BootstrapDeleteResponse doDelete(LwM2mServer server, BootstrapDeleteRequest request) {
if (request.getPath().isRoot() || request.getPath().isObject()) {
if (id == LwM2mId.SECURITY) {
// For security object, we clean everything except bootstrap Server account.
// Get bootstrap account and store removed instances ids
Entry<Integer, LwM2mInstanceEnabler> bootstrapServerAccount = null;
int[] instanceIds = new int[instances.size()];
int i = 0;
for (Entry<Integer, LwM2mInstanceEnabler> instance : instances.entrySet()) {
if (ServersInfoExtractor.isBootstrapServer(instance.getValue())) {
bootstrapServerAccount = instance;
} else {
// Store instance ids
instanceIds[i] = instance.getKey();
i++;
}
}
// Clear everything
instances.clear();
// Put bootstrap account again
if (bootstrapServerAccount != null) {
instances.put(bootstrapServerAccount.getKey(), bootstrapServerAccount.getValue());
}
fireInstancesRemoved(instanceIds);
return BootstrapDeleteResponse.success();
} else if (id == LwM2mId.OSCORE) {
// For OSCORE object, we clean everything except OSCORE object link to bootstrap Server account.
// Get bootstrap account
LwM2mObjectInstance bootstrapInstance = ServersInfoExtractor.getBootstrapSecurityInstance(
getLwm2mClient().getObjectTree().getObjectEnabler(LwM2mId.SECURITY));
// Get OSCORE instance ID associated to it
Integer bootstrapOscoreInstanceId = bootstrapInstance != null
? ServersInfoExtractor.getOscoreSecurityMode(bootstrapInstance)
: null;
// if bootstrap server use OSCORE,
// search the OSCORE instance for this ID and store removed instances ids
if (bootstrapOscoreInstanceId != null) {
Entry<Integer, LwM2mInstanceEnabler> bootstrapServerOscore = null;
int[] instanceIds = new int[instances.size()];
int i = 0;
for (Entry<Integer, LwM2mInstanceEnabler> instance : instances.entrySet()) {
if (bootstrapOscoreInstanceId.equals(instance.getKey())) {
bootstrapServerOscore = instance;
} else {
// Store instance ids
instanceIds[i] = instance.getKey();
i++;
}
}
// Clear everything
instances.clear();
// Put bootstrap OSCORE instance again
if (bootstrapServerOscore != null) {
instances.put(bootstrapServerOscore.getKey(), bootstrapServerOscore.getValue());
}
fireInstancesRemoved(instanceIds);
return BootstrapDeleteResponse.success();
}
// else delete everything.
}
// In all other cases, just delete everything
instances.clear();
// fired instances removed
int[] instanceIds = new int[instances.size()];
int i = 0;
for (Entry<Integer, LwM2mInstanceEnabler> instance : instances.entrySet()) {
instanceIds[i] = instance.getKey();
i++;
}
fireInstancesRemoved(instanceIds);
return BootstrapDeleteResponse.success();
} else if (request.getPath().isObjectInstance()) {
if (id == LwM2mId.SECURITY) {
// For security object, deleting bootstrap Server account is not allowed
LwM2mInstanceEnabler instance = instances.get(request.getPath().getObjectInstanceId());
if (instance == null) {
return BootstrapDeleteResponse
.badRequest(String.format("Instance %s not found", request.getPath()));
} else if (ServersInfoExtractor.isBootstrapServer(instance)) {
return BootstrapDeleteResponse.badRequest("bootstrap server can not be deleted");
}
} else if (id == LwM2mId.OSCORE) {
// For OSCORE object, deleting instance linked to Bootstrap account is not allowed
// Get bootstrap instance
LwM2mObjectInstance bootstrapInstance = ServersInfoExtractor.getBootstrapSecurityInstance(
getLwm2mClient().getObjectTree().getObjectEnabler(LwM2mId.SECURITY));
// Get OSCORE instance ID associated to it
Integer bootstrapOscoreInstanceId = bootstrapInstance != null
? ServersInfoExtractor.getOscoreSecurityMode(bootstrapInstance)
: null;
if (bootstrapOscoreInstanceId != null
&& bootstrapOscoreInstanceId.equals(request.getPath().getObjectInstanceId())) {
return BootstrapDeleteResponse
.badRequest("OSCORE instance linked to bootstrap server can not be deleted");
}
}
if (null != instances.remove(request.getPath().getObjectInstanceId())) {
fireInstancesRemoved(request.getPath().getObjectInstanceId());
return BootstrapDeleteResponse.success();
} else {
return BootstrapDeleteResponse.badRequest(String.format("Instance %s not found", request.getPath()));
}
}
return BootstrapDeleteResponse.badRequest(String.format("unexcepted path %s", request.getPath()));
}
protected void listenInstance(LwM2mInstanceEnabler instance, final int instanceId) {
instance.addResourceListener(new ResourceListener() {
@Override
public void resourceChanged(LwM2mPath... paths) {
for (LwM2mPath path : paths) {
if (!isValid(instanceId, path)) {
LOG.warn("InstanceEnabler ({}) of object ({}) try to raise a change of {} which seems invalid.",
instanceId, getId(), path);
}
}
fireResourcesChanged(paths);
}
});
}
protected boolean isValid(int instanceId, LwM2mPath pathToValidate) {
if (!(pathToValidate.isResource() || pathToValidate.isResourceInstance()))
return false;
if (pathToValidate.getObjectId() != getId()) {
return false;
}
if (pathToValidate.getObjectInstanceId() != instanceId) {
return false;
}
return true;
}
@Override
public ContentFormat getDefaultEncodingFormat(DownlinkRequest<?> request) {
return defaultContentFormat;
}
@Override
public void init(LwM2mClient client, LinkFormatHelper linkFormatHelper) {
super.init(client, linkFormatHelper);
this.tbLinkFormatHelper = linkFormatHelper;
for (LwM2mInstanceEnabler instanceEnabler : instances.values()) {
instanceEnabler.setLwM2mClient(client);
}
}
@Override
public void destroy() {
for (LwM2mInstanceEnabler instanceEnabler : instances.values()) {
if (instanceEnabler instanceof Destroyable) {
((Destroyable) instanceEnabler).destroy();
} else if (instanceEnabler instanceof Stoppable) {
((Stoppable) instanceEnabler).stop();
}
}
}
@Override
public void start() {
for (LwM2mInstanceEnabler instanceEnabler : instances.values()) {
if (instanceEnabler instanceof Startable) {
((Startable) instanceEnabler).start();
}
}
}
@Override
public void stop() {
for (LwM2mInstanceEnabler instanceEnabler : instances.values()) {
if (instanceEnabler instanceof Stoppable) {
((Stoppable) instanceEnabler).stop();
}
}
}
@Override
public synchronized WriteAttributesResponse writeAttributes(LwM2mServer server, WriteAttributesRequest request) {
// execute is not supported for bootstrap
if (server.isLwm2mBootstrapServer()) {
return WriteAttributesResponse.methodNotAllowed();
}
// return WriteAttributesResponse.internalServerError("not implemented");
return doWriteAttributes(server, request);
}
/**
* <NOTIFICATION> Class Attributes
* - pmin (def = 0(sec)) Integer Resource/Object Instance/Object Readable Resource
* - pmax (def = -- ) Integer Resource/Object Instance/Object Readable Resource
* - Greater Than gt (def = -- ) Float Resource Numerical&Readable Resource
* - Less Than lt (def = -- ) Float Resource Numerical&Readable Resource
* - Step st (def = -- ) Float Resource Numerical&Readable Resource
*/
public WriteAttributesResponse doWriteAttributes(LwM2mServer server, WriteAttributesRequest request) {
LwM2mPath lwM2mPath = request.getPath();
LwM2mAttributeSet attributeSet = lwM2mAttributes.get(lwM2mPath);
Map <String, LwM2mAttribute<?>> attributes = new HashMap<>();
for (LwM2mAttribute attr : request.getAttributes().getLwM2mAttributes()) {
if (attr.getName().equals("pmax") || attr.getName().equals("pmin")) {
if (lwM2mPath.isObject() || lwM2mPath.isObjectInstance() || lwM2mPath.isResource()) {
attributes.put(attr.getName(), attr);
} else {
return WriteAttributesResponse.badRequest("Attribute " + attr.getName() + " can be used for only Resource/Object Instance/Object.");
}
} else if (attr.getName().equals("gt") || attr.getName().equals("lt") || attr.getName().equals("st")) {
if (lwM2mPath.isResource()) {
attributes.put(attr.getName(), attr);
} else {
return WriteAttributesResponse.badRequest("Attribute " + attr.getName() + " can be used for only Resource.");
}
}
}
if (attributes.size()>0){
if (attributeSet == null) {
attributeSet = new LwM2mAttributeSet(attributes.values());
} else {
Iterable<LwM2mAttribute<?>> lwM2mAttributeIterable = attributeSet.getLwM2mAttributes();
Map <String, LwM2mAttribute<?>> attributesOld = new HashMap<>();
for (LwM2mAttribute<?> attr : lwM2mAttributeIterable) {
attributesOld.put(attr.getName(), attr);
}
attributesOld.putAll(attributes);
attributeSet = new LwM2mAttributeSet(attributesOld.values());
}
lwM2mAttributes.put(lwM2mPath, attributeSet);
return WriteAttributesResponse.success();
}
return WriteAttributesResponse.internalServerError("not implemented");
}
@Override
public synchronized DiscoverResponse discover(LwM2mServer server, DiscoverRequest request) {
if (server.isLwm2mBootstrapServer()) {
// discover is not supported for bootstrap
return DiscoverResponse.methodNotAllowed();
}
if (id == LwM2mId.SECURITY || id == LwM2mId.OSCORE) {
return DiscoverResponse.notFound();
}
return doDiscover(server, request);
}
protected DiscoverResponse doDiscover(LwM2mServer server, DiscoverRequest request) {
LwM2mPath path = request.getPath();
if (path.isObject()) {
LwM2mLink[] ObjectLinks = linkUpdateAttributes(this.tbLinkFormatHelper.getObjectDescription(this, null), server);
return DiscoverResponse.success(ObjectLinks);
} else if (path.isObjectInstance()) {
// Manage discover on instance
if (!getAvailableInstanceIds().contains(path.getObjectInstanceId()))
return DiscoverResponse.notFound();
LwM2mLink[] instanceLink = linkUpdateAttributes(this.tbLinkFormatHelper.getInstanceDescription(this, path.getObjectInstanceId(), null), server);
return DiscoverResponse.success(instanceLink);
} else if (path.isResource()) {
// Manage discover on resource
if (!getAvailableInstanceIds().contains(path.getObjectInstanceId()))
return DiscoverResponse.notFound();
ResourceModel resourceModel = getObjectModel().resources.get(path.getResourceId());
if (resourceModel == null)
return DiscoverResponse.notFound();
if (!getAvailableResourceIds(path.getObjectInstanceId()).contains(path.getResourceId()))
return DiscoverResponse.notFound();
LwM2mLink resourceLink = linkAddAttribute(
this.tbLinkFormatHelper.getResourceDescription(this, path.getObjectInstanceId(), path.getResourceId(), null),
server);
return DiscoverResponse.success(new LwM2mLink[] { resourceLink });
}
return DiscoverResponse.badRequest(null);
}
private LwM2mLink[] linkUpdateAttributes(LwM2mLink[] links, LwM2mServer server) {
return Arrays.stream(links)
.map(link -> linkAddAttribute(link, server))
.toArray(LwM2mLink[]::new);
}
private LwM2mLink linkAddAttribute(LwM2mLink link, LwM2mServer server) {
LwM2mAttributeSet lwM2mAttributeSetDop = null;
if (this.lwM2mAttributes.get(link.getPath())!= null){
lwM2mAttributeSetDop = this.lwM2mAttributes.get(link.getPath());
}
LwM2mAttribute resourceAttributeDim = getResourceAttributes (server, link.getPath());
Map <String, LwM2mAttribute<?>> attributes = new HashMap<>();
if (link.getAttributes() != null) {
for (LwM2mAttribute attr : link.getAttributes().getLwM2mAttributes()) {
attributes.put(attr.getName(), attr);
}
}
if (lwM2mAttributeSetDop != null) {
for (LwM2mAttribute attr : lwM2mAttributeSetDop.getLwM2mAttributes()) {
attributes.put(attr.getName(), attr);
}
}
if (resourceAttributeDim != null) {
attributes.put(resourceAttributeDim.getName(), resourceAttributeDim);
}
return new LwM2mLink(link.getRootPath(), link.getPath(), attributes.values());
}
protected LwM2mAttribute getResourceAttributes (LwM2mServer server, LwM2mPath path) {
ResourceModel resourceModel = getObjectModel().resources.get(path.getResourceId());
if (path.isResource() && resourceModel.multiple) {
return getResourceAttributeDim(path, server);
}
return null;
}
protected LwM2mAttribute getResourceAttributeDim(LwM2mPath path, LwM2mServer server) {
LwM2mInstanceEnabler instance = instances.get(path.getObjectInstanceId());
try {
ReadResponse readResponse = instance.read(server, path.getResourceId());
if (readResponse.getCode().getCode()==205 && readResponse.getContent() instanceof LwM2mMultipleResource) {
long valueDim = ((LwM2mMultipleResource)readResponse.getContent()).getInstances().size();
return LwM2mAttributes.create(LwM2mAttributes.DIMENSION, valueDim);
} else {
return null;
}
} catch (Exception e ){
return null;
}
}
}

71
application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbObjectsInitializer.java

@ -1,71 +0,0 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.lwm2m.client;
import org.eclipse.leshan.client.resource.BaseInstanceEnablerFactory;
import org.eclipse.leshan.client.resource.LwM2mInstanceEnabler;
import org.eclipse.leshan.client.resource.LwM2mObjectEnabler;
import org.eclipse.leshan.client.resource.ObjectsInitializer;
import org.eclipse.leshan.core.model.LwM2mModel;
import org.eclipse.leshan.core.model.ObjectModel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class TbObjectsInitializer extends ObjectsInitializer {
public TbObjectsInitializer(LwM2mModel model) {
super(model);
}
public List<LwM2mObjectEnabler> create(int... objectId) {
List<LwM2mObjectEnabler> enablers = new ArrayList<>();
for (int anObjectId : objectId) {
LwM2mObjectEnabler objectEnabler = create(anObjectId);
if (objectEnabler != null)
enablers.add(objectEnabler);
}
return enablers;
}
public LwM2mObjectEnabler create(int objectId) {
ObjectModel objectModel = model.getObjectModel(objectId);
if (objectModel == null) {
throw new IllegalArgumentException(
"Cannot create object for id " + objectId + " because no model is defined for this id.");
}
return createNodeEnabler(objectModel);
}
protected LwM2mObjectEnabler createNodeEnabler(ObjectModel objectModel) {
Map<Integer, LwM2mInstanceEnabler> instances = new HashMap<>();
LwM2mInstanceEnabler[] newInstances = createInstances(objectModel);
for (LwM2mInstanceEnabler instance : newInstances) {
// set id if not already set
if (instance.getId() == null) {
int id = BaseInstanceEnablerFactory.generateNewInstanceId(instances.keySet());
instance.setId(id);
}
instance.setModel(objectModel);
instances.put(instance.getId(), instance);
}
return new TbLwm2mObjectEnabler(objectModel.id, objectModel, instances, getFactoryFor(objectModel),
getContentFormat(objectModel.id));
}
}

86
application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java

@ -15,17 +15,31 @@
*/
package org.thingsboard.server.transport.lwm2m.ota;
import com.fasterxml.jackson.core.type.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.rest.client.utils.RestJsonConverter.toTimeseries;
import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE;
import static org.thingsboard.server.common.data.ota.OtaPackageType.SOFTWARE;
@Slf4j
@DaoSqlTest
public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest {
@ -33,9 +47,10 @@ public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MInteg
protected static final String CLIENT_ENDPOINT_WITHOUT_FW_INFO = "WithoutFirmwareInfoDevice";
protected static final String CLIENT_ENDPOINT_OTA5 = "Ota5_Device";
protected static final String CLIENT_ENDPOINT_OTA9 = "Ota9_Device";
protected List<OtaPackageUpdateStatus> expectedStatuses;
protected final String OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA =
protected final String OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA5 =
" {\n" +
" \"keyName\": {\n" +
@ -43,22 +58,14 @@ public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MInteg
" \"/5_1.2/0/5\": \"updateResult\",\n" +
" \"/5_1.2/0/6\": \"pkgname\",\n" +
" \"/5_1.2/0/7\": \"pkgversion\",\n" +
" \"/5_1.2/0/9\": \"firmwareUpdateDeliveryMethod\",\n" +
" \"/9_1.1/0/0\": \"pkgname\",\n" +
" \"/9_1.1/0/1\": \"pkgversion\",\n" +
" \"/9_1.1/0/7\": \"updateState\",\n" +
" \"/9_1.1/0/9\": \"updateResult\"\n" +
" \"/5_1.2/0/9\": \"firmwareUpdateDeliveryMethod\"\n" +
" },\n" +
" \"observe\": [\n" +
" \"/5_1.2/0/3\",\n" +
" \"/5_1.2/0/5\",\n" +
" \"/5_1.2/0/6\",\n" +
" \"/5_1.2/0/7\",\n" +
" \"/5_1.2/0/9\",\n" +
" \"/9_1.1/0/0\",\n" +
" \"/9_1.1/0/1\",\n" +
" \"/9_1.1/0/7\",\n" +
" \"/9_1.1/0/9\"\n" +
" \"/5_1.2/0/9\"\n" +
" ],\n" +
" \"attribute\": [],\n" +
" \"telemetry\": [\n" +
@ -66,7 +73,28 @@ public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MInteg
" \"/5_1.2/0/5\",\n" +
" \"/5_1.2/0/6\",\n" +
" \"/5_1.2/0/7\",\n" +
" \"/5_1.2/0/9\",\n" +
" \"/5_1.2/0/9\"\n" +
" ],\n" +
" \"attributeLwm2m\": {}\n" +
" }";
protected final String OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA9 =
" {\n" +
" \"keyName\": {\n" +
" \"/9_1.1/0/0\": \"pkgname\",\n" +
" \"/9_1.1/0/1\": \"pkgversion\",\n" +
" \"/9_1.1/0/7\": \"updateState\",\n" +
" \"/9_1.1/0/9\": \"updateResult\"\n" +
" },\n" +
" \"observe\": [\n" +
" \"/9_1.1/0/0\",\n" +
" \"/9_1.1/0/1\",\n" +
" \"/9_1.1/0/7\",\n" +
" \"/9_1.1/0/9\"\n" +
" ],\n" +
" \"attribute\": [],\n" +
" \"telemetry\": [\n" +
" \"/9_1.1/0/0\",\n" +
" \"/9_1.1/0/1\",\n" +
" \"/9_1.1/0/7\",\n" +
@ -79,14 +107,14 @@ public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MInteg
setResources(this.RESOURCES_OTA);
}
protected OtaPackageInfo createFirmware() throws Exception {
protected OtaPackageInfo createFirmware(String version, DeviceProfileId deviceProfileId) throws Exception {
String CHECKSUM = "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a";
OtaPackageInfo firmwareInfo = new OtaPackageInfo();
firmwareInfo.setDeviceProfileId(deviceProfile.getId());
firmwareInfo.setDeviceProfileId(deviceProfileId);
firmwareInfo.setType(FIRMWARE);
firmwareInfo.setTitle("My firmware");
firmwareInfo.setVersion("v1.0");
firmwareInfo.setVersion(version);
OtaPackageInfo savedFirmwareInfo = doPost("/api/otaPackage", firmwareInfo, OtaPackageInfo.class);
@ -95,11 +123,11 @@ public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MInteg
return savaData("/api/otaPackage/" + savedFirmwareInfo.getId().getId().toString() + "?checksum={checksum}&checksumAlgorithm={checksumAlgorithm}", testData, CHECKSUM, "SHA256");
}
protected OtaPackageInfo createSoftware() throws Exception {
protected OtaPackageInfo createSoftware(DeviceProfileId deviceProfileId) throws Exception {
String CHECKSUM = "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a";
OtaPackageInfo swInfo = new OtaPackageInfo();
swInfo.setDeviceProfileId(deviceProfile.getId());
swInfo.setDeviceProfileId(deviceProfileId);
swInfo.setType(SOFTWARE);
swInfo.setTitle("My sw");
swInfo.setVersion("v1.0");
@ -117,4 +145,28 @@ public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MInteg
setJwtToken(postRequest);
return readResponse(mockMvc.perform(postRequest).andExpect(status().isOk()), OtaPackageInfo.class);
}
protected Device getDeviceFromAPI(UUID deviceId) throws Exception {
final Device device = doGet("/api/device/" + deviceId, Device.class);
log.trace("Fetched device by API for deviceId {}, device is {}", deviceId, device);
return device;
}
protected List<TsKvEntry> getFwSwStateTelemetryFromAPI(UUID deviceId, String type_state) throws Exception {
final List<TsKvEntry> tsKvEntries = toTimeseries(doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + deviceId + "/values/timeseries?orderBy=ASC&keys=" + type_state + "&startTs=0&endTs=" + System.currentTimeMillis(), new TypeReference<>() {
}));
log.warn("Fetched telemetry by API for deviceId {}, list size {}, tsKvEntries {}", deviceId, tsKvEntries.size(), tsKvEntries);
return tsKvEntries;
}
protected boolean predicateForStatuses(List<TsKvEntry> ts) {
List<OtaPackageUpdateStatus> statuses = ts.stream()
.sorted(Comparator.comparingLong(TsKvEntry::getTs))
.map(KvEntry::getValueAsString)
.map(OtaPackageUpdateStatus::valueOf)
.collect(Collectors.toList());
log.warn("{}", statuses);
return statuses.containsAll(expectedStatuses);
}
}

96
application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java → application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota5LwM2MIntegrationTest.java

@ -20,6 +20,7 @@ import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Test;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials;
import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.kv.KvEntry;
@ -29,9 +30,7 @@ import org.thingsboard.server.transport.lwm2m.ota.AbstractOtaLwM2MIntegrationTes
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@ -45,24 +44,21 @@ import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.INIT
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.QUEUED;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATED;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATING;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.VERIFIED;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE;
@Slf4j
public class OtaLwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest {
private List<OtaPackageUpdateStatus> expectedStatuses;
public class Ota5LwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest {
@Test
public void testFirmwareUpdateWithClientWithoutFirmwareOtaInfoFromProfile() throws Exception {
public void testFirmwareUpdateWithClientWithoutFirmwareOtaInfoFromProfile_IsNotSupported() throws Exception {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITH_PARAMS, getBootstrapServerCredentialsNoSec(NONE));
createDeviceProfile(transportConfiguration);
DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + this.CLIENT_ENDPOINT_WITHOUT_FW_INFO, transportConfiguration);
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(this.CLIENT_ENDPOINT_WITHOUT_FW_INFO));
final Device device = createDevice(deviceCredentials, this.CLIENT_ENDPOINT_WITHOUT_FW_INFO);
final Device device = createLwm2mDevice(deviceCredentials, this.CLIENT_ENDPOINT_WITHOUT_FW_INFO, deviceProfile.getId());
createNewClient(SECURITY_NO_SEC, null, false, this.CLIENT_ENDPOINT_WITHOUT_FW_INFO);
awaitObserveReadAll(0, device.getId().getId().toString());
device.setFirmwareId(createFirmware().getId());
device.setFirmwareId(createFirmware("5.1", deviceProfile.getId()).getId());
final Device savedDevice = doPost("/api/device", device, Device.class);
Thread.sleep(1000);
@ -78,81 +74,37 @@ public class OtaLwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest {
Assert.assertEquals(expectedStatuses, statuses);
}
/**
* /5/0/5 -> Update Result (Res); 5/0/3 -> State;
* => ((Res>=0 && Res<=9) && State=0)
* => Write to Package/Write to Package URI -> DOWNLOADING ((Res>=0 && Res<=9) && State=1)
* => Download Finished -> DOWNLOADED ((Res==0 || Res=8) && State=2)
* => Executable resource Update is triggered / Initiate Firmware Update -> UPDATING (Res=0 && State=3)
* => Update Successful [Res==1]
* => Start / Res=0 -> "IDLE" ....
* @throws Exception
*/
@Test
public void testFirmwareUpdateByObject5() throws Exception {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA, getBootstrapServerCredentialsNoSec(NONE));
createDeviceProfile(transportConfiguration);
public void testFirmwareUpdateByObject5_Ok() throws Exception {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA5, getBootstrapServerCredentialsNoSec(NONE));
DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + this.CLIENT_ENDPOINT_OTA5, transportConfiguration);
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(this.CLIENT_ENDPOINT_OTA5));
final Device device = createDevice(deviceCredentials, this.CLIENT_ENDPOINT_OTA5);
final Device device = createLwm2mDevice(deviceCredentials, this.CLIENT_ENDPOINT_OTA5, deviceProfile.getId());
createNewClient(SECURITY_NO_SEC, null, false, this.CLIENT_ENDPOINT_OTA5);
awaitObserveReadAll(9, device.getId().getId().toString());
awaitObserveReadAll(5, device.getId().getId().toString());
device.setFirmwareId(createFirmware().getId());
device.setFirmwareId(createFirmware("fw.v.1.5.0-update", deviceProfile.getId()).getId());
final Device savedDevice = doPost("/api/device", device, Device.class);
assertThat(savedDevice).as("saved device").isNotNull();
assertThat(getDeviceFromAPI(device.getId().getId())).as("fetched device").isEqualTo(savedDevice);
expectedStatuses = Arrays.asList(QUEUED, INITIATED, DOWNLOADING, DOWNLOADED, UPDATING, UPDATED);
List<TsKvEntry> ts = await("await on timeseries")
List<TsKvEntry> ts = await("await on timeseries for FW")
.atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> toTimeseries(doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" +
savedDevice.getId().getId() + "/values/timeseries?orderBy=ASC&keys=fw_state&startTs=0&endTs=" +
System.currentTimeMillis(), new TypeReference<>() {
})), this::predicateForStatuses);
.until(() -> getFwSwStateTelemetryFromAPI(device.getId().getId(), "fw_state"), this::predicateForStatuses);
log.warn("Object5: Got the ts: {}", ts);
}
/**
* This is the example how to use the AWAITILITY instead Thread.sleep()
* Test will finish as fast as possible, but will await until TIMEOUT if a build machine is busy or slow
* Check the detailed log output to learn how Awaitility polling the API and when exactly expected result appears
* */
@Test
public void testSoftwareUpdateByObject9() throws Exception {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA, getBootstrapServerCredentialsNoSec(NONE));
createDeviceProfile(transportConfiguration);
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(this.CLIENT_ENDPOINT_OTA9));
final Device device = createDevice(deviceCredentials, this.CLIENT_ENDPOINT_OTA9);
createNewClient(SECURITY_NO_SEC, null, false, this.CLIENT_ENDPOINT_OTA9);
awaitObserveReadAll(9, device.getId().getId().toString());
device.setSoftwareId(createSoftware().getId());
final Device savedDevice = doPost("/api/device", device, Device.class); //sync call
assertThat(savedDevice).as("saved device").isNotNull();
assertThat(getDeviceFromAPI(device.getId().getId())).as("fetched device").isEqualTo(savedDevice);
expectedStatuses = List.of(
QUEUED, INITIATED, DOWNLOADING, DOWNLOADING, DOWNLOADING, DOWNLOADED, VERIFIED, UPDATED);
List<TsKvEntry> ts = await("await on timeseries")
.atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> getSwStateTelemetryFromAPI(device.getId().getId()), this::predicateForStatuses);
log.warn("Object9: Got the ts: {}", ts);
}
private Device getDeviceFromAPI(UUID deviceId) throws Exception {
final Device device = doGet("/api/device/" + deviceId, Device.class);
log.trace("Fetched device by API for deviceId {}, device is {}", deviceId, device);
return device;
}
private List<TsKvEntry> getSwStateTelemetryFromAPI(UUID deviceId) throws Exception {
final List<TsKvEntry> tsKvEntries = toTimeseries(doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + deviceId + "/values/timeseries?orderBy=ASC&keys=sw_state&startTs=0&endTs=" + System.currentTimeMillis(), new TypeReference<>() {
}));
log.warn("Fetched telemetry by API for deviceId {}, list size {}, tsKvEntries {}", deviceId, tsKvEntries.size(), tsKvEntries);
return tsKvEntries;
}
private boolean predicateForStatuses(List<TsKvEntry> ts) {
List<OtaPackageUpdateStatus> statuses = ts.stream()
.sorted(Comparator.comparingLong(TsKvEntry::getTs))
.map(KvEntry::getValueAsString)
.map(OtaPackageUpdateStatus::valueOf)
.collect(Collectors.toList());
log.warn("{}", statuses);
return statuses.containsAll(expectedStatuses);
}
}

73
application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota9LwM2MIntegrationTest.java

@ -0,0 +1,73 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.lwm2m.ota.sql;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials;
import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.transport.lwm2m.ota.AbstractOtaLwM2MIntegrationTest;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.DOWNLOADED;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.DOWNLOADING;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.INITIATED;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.QUEUED;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATED;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.VERIFIED;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE;
@Slf4j
public class Ota9LwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest {
/**
* => Start -> INITIAL (State=0) -> DOWNLOAD STARTED;
* => PKG / URI Write -> DOWNLOAD STARTED (Res=1 (Downloading) && State=1) -> DOWNLOADED
* => PKG Written -> DOWNLOADED (Res=1 Initial && State=2) -> DELIVERED;
* => PKG integrity verified -> DELIVERED (Res=3 (Successfully Downloaded and package integrity verified) && State=3) -> INSTALLED;
* => Install -> INSTALLED (Res=2 SW successfully installed) && State=4) -> Start
*
* */
@Test
public void testSoftwareUpdateByObject9() throws Exception {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA9, getBootstrapServerCredentialsNoSec(NONE));
DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + this.CLIENT_ENDPOINT_OTA9, transportConfiguration);
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(this.CLIENT_ENDPOINT_OTA9));
final Device device = createLwm2mDevice(deviceCredentials, this.CLIENT_ENDPOINT_OTA9, deviceProfile.getId());
createNewClient(SECURITY_NO_SEC, null, false, this.CLIENT_ENDPOINT_OTA9);
awaitObserveReadAll(4, device.getId().getId().toString());
device.setSoftwareId(createSoftware(deviceProfile.getId()).getId());
final Device savedDevice = doPost("/api/device", device, Device.class); //sync call
assertThat(savedDevice).as("saved device").isNotNull();
assertThat(getDeviceFromAPI(device.getId().getId())).as("fetched device").isEqualTo(savedDevice);
expectedStatuses = List.of(
QUEUED, INITIATED, DOWNLOADING, DOWNLOADING, DOWNLOADING, DOWNLOADED, VERIFIED, UPDATED);
List<TsKvEntry> ts = await("await on timeseries")
.atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> getFwSwStateTelemetryFromAPI(device.getId().getId(), "sw_state"), this::predicateForStatuses);
log.warn("Object9: Got the ts: {}", ts);
}
}

9
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserveTest.java

@ -15,8 +15,8 @@
*/
package org.thingsboard.server.transport.lwm2m.rpc;
import org.junit.Before;
import org.thingsboard.server.dao.service.DaoSqlTest;
import static org.junit.Assert.assertTrue;
@DaoSqlTest
public abstract class AbstractRpcLwM2MIntegrationObserveTest extends AbstractRpcLwM2MIntegrationTest{
@ -26,9 +26,8 @@ public abstract class AbstractRpcLwM2MIntegrationObserveTest extends AbstractRpc
setResources(this.RESOURCES_RPC_MULTIPLE_19);
}
@Before
public void initTest () throws Exception {
awaitObserveReadAll(4, deviceId);
protected void sendRpcObserveWithContainsLwM2mSingleResource(String params) throws Exception {
String rpcActualResult = sendRpcObserveOkWithResultValue("Observe", params);
assertTrue(rpcActualResult.contains("LwM2mSingleResource") || rpcActualResult.contains("LwM2mMultipleResource"));
}
}

131
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java

@ -15,20 +15,30 @@
*/
package org.thingsboard.server.transport.lwm2m.rpc;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.link.LinkParser;
import org.eclipse.leshan.core.link.lwm2m.DefaultLwM2mLinkParser;
import org.junit.Before;
import org.mockito.Mockito;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials;
import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest;
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServerHelper;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Predicate;
import static org.awaitility.Awaitility.await;
import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL;
import static org.eclipse.leshan.core.LwM2mId.DEVICE;
import static org.eclipse.leshan.core.LwM2mId.FIRMWARE;
@ -40,24 +50,28 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_ID_0
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_ID_1;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_12;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_5700;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_0_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_0_2;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_1_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3303_12_5700;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_14;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.TEMPERATURE_SENSOR;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resources;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId;
@Slf4j
@DaoSqlTest
public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest {
protected final LinkParser linkParser = new DefaultLwM2mLinkParser();
protected String OBSERVE_ATTRIBUTES_WITH_PARAMS_RPC;
protected String CONFIG_PROFILE_WITH_PARAMS_RPC;
public Set expectedObjects;
public Set expectedObjectIdVers;
public Set expectedInstances;
@ -84,22 +98,28 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg
protected String idVer_19_0_0;
@SpyBean
protected LwM2mTransportServerHelper lwM2mTransportServerHelperTest;
public AbstractRpcLwM2MIntegrationTest() {
setResources(resources);
}
@Before
public void startInitRPC() throws Exception {
if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationDiscoverWriteAttributesTest")){
isWriteAttribute = true;
}
if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationWriteCborTest")){
if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationWriteCborTest")) {
supportFormatOnly_SenMLJSON_SenMLCBOR = true;
}
initRpc();
if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationObserveTest")) {
initRpc(0);
} else if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationReadCollectedValueTest")) {
initRpc(3303);
} else {
initRpc(1);
}
}
private void initRpc () throws Exception {
protected void initRpc(int typeConfigProfile) throws Exception {
String endpoint = DEVICE_ENDPOINT_RPC_PREF + endpointSequence.incrementAndGet();
createNewClient(SECURITY_NO_SEC, null, true, endpoint);
expectedObjects = ConcurrentHashMap.newKeySet();
@ -134,10 +154,10 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg
idVer_3_0_0 = objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0;
idVer_3_0_9 = objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_9;
id_3_0_9 = fromVersionedIdToObjectId(idVer_3_0_9);
id_3_0_9 = fromVersionedIdToObjectId(idVer_3_0_9);
idVer_19_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0;
OBSERVE_ATTRIBUTES_WITH_PARAMS_RPC =
String ATTRIBUTES_TELEMETRY_WITH_PARAMS_RPC_WITH_OBSERVE =
" {\n" +
" \"keyName\": {\n" +
" \"" + idVer_3_0_9 + "\": \"" + RESOURCE_ID_NAME_3_9 + "\",\n" +
@ -164,11 +184,54 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg
" \"attributeLwm2m\": {}\n" +
" }";
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITH_PARAMS_RPC, getBootstrapServerCredentialsNoSec(NONE));
createDeviceProfile(transportConfiguration);
String TELEMETRY_WITH_PARAMS_RPC_WITHOUT_OBSERVE =
" {\n" +
" \"keyName\": {\n" +
" \"" + idVer_3_0_9 + "\": \"" + RESOURCE_ID_NAME_3_9 + "\",\n" +
" \"" + objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_14 + "\": \"" + RESOURCE_ID_NAME_3_14 + "\",\n" +
" \"" + idVer_19_0_0 + "\": \"" + RESOURCE_ID_NAME_19_0_0 + "\",\n" +
" \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "\": \"" + RESOURCE_ID_NAME_19_1_0 + "\",\n" +
" \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2 + "\": \"" + RESOURCE_ID_NAME_19_0_2 + "\"\n" +
" },\n" +
" \"observe\": [\n" +
" ],\n" +
" \"attribute\": [\n" +
" \"" + objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_14 + "\",\n" +
" \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2 + "\"\n" +
" ],\n" +
" \"telemetry\": [\n" +
" \"" + idVer_3_0_9 + "\",\n" +
" \"" + idVer_19_0_0 + "\",\n" +
" \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "\"\n" +
" ],\n" +
" \"attributeLwm2m\": {}\n" +
" }";
String TELEMETRY_WITH_PARAMS_RPC_COLLECTED_VALUE =
" {\n" +
" \"keyName\": {\n" +
" \"" + objectIdVer_3303 + "/" + OBJECT_INSTANCE_ID_12 + "/" + RESOURCE_ID_5700 + "\": \"" + RESOURCE_ID_NAME_3303_12_5700 + "\"\n" +
" },\n" +
" \"observe\": [\n" +
" ],\n" +
" \"attribute\": [\n" +
" ],\n" +
" \"telemetry\": [\n" +
" \"" + objectIdVer_3303 + "/" + OBJECT_INSTANCE_ID_12 + "/" + RESOURCE_ID_5700 + "\"\n" +
" ],\n" +
" \"attributeLwm2m\": {}\n" +
" }";
CONFIG_PROFILE_WITH_PARAMS_RPC =
switch (typeConfigProfile) {
case 0 -> ATTRIBUTES_TELEMETRY_WITH_PARAMS_RPC_WITH_OBSERVE;
case 1 -> TELEMETRY_WITH_PARAMS_RPC_WITHOUT_OBSERVE;
case 3303 -> TELEMETRY_WITH_PARAMS_RPC_COLLECTED_VALUE;
default -> throw new IllegalStateException("Unexpected value: " + typeConfigProfile);
};
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(CONFIG_PROFILE_WITH_PARAMS_RPC, getBootstrapServerCredentialsNoSec(NONE));
DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + endpoint, transportConfiguration);
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(endpoint));
final Device device = createDevice(deviceCredentials, endpoint);
final Device device = createLwm2mDevice(deviceCredentials, endpoint, deviceProfile.getId());
deviceId = device.getId().getId().toString();
lwM2MTestClient.start(true);
@ -183,4 +246,48 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg
return pathIdVer;
}
protected long countUpdateAttrTelemetryAll() {
return Mockito.mockingDetails(defaultUplinkMsgHandlerTest)
.getInvocations().stream()
.filter(invocation -> invocation.getMethod().getName().equals("updateAttrTelemetry"))
.count();
}
protected long countUpdateAttrTelemetryResource(String idVerRez) {
return Mockito.mockingDetails(defaultUplinkMsgHandlerTest)
.getInvocations().stream()
.filter(invocation ->
invocation.getMethod().getName().equals("updateAttrTelemetry") &&
invocation.getArguments().length > 1 &&
idVerRez.equals(invocation.getArguments()[1])
)
.count();
}
protected void updateRegAtLeastOnceAfterAction() {
long initialInvocationCount = countUpdateReg();
AtomicLong newInvocationCount = new AtomicLong(initialInvocationCount);
log.trace("updateRegAtLeastOnceAfterAction: initialInvocationCount [{}]", initialInvocationCount);
await("Update Registration at-least-once after action")
.atMost(50, TimeUnit.SECONDS)
.until(() -> {
newInvocationCount.set(countUpdateReg());
return newInvocationCount.get() > initialInvocationCount;
});
log.trace("updateRegAtLeastOnceAfterAction: newInvocationCount [{}]", newInvocationCount.get());
}
protected long countSendParametersOnThingsboardTelemetryResource(String rezName) {
return Mockito.mockingDetails(lwM2mTransportServerHelperTest)
.getInvocations().stream()
.filter(invocation ->
invocation.getMethod().getName().equals("sendParametersOnThingsboardTelemetry") &&
invocation.getArguments().length > 0 &&
invocation.getArguments()[0] instanceof List &&
((List<?>) invocation.getArguments()[0]).stream()
.filter(arg -> arg instanceof TransportProtos.KeyValueProto)
.anyMatch(arg -> rezName.equals(((TransportProtos.KeyValueProto) arg).getKey()))
)
.count();
}
}

109
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2MIntegrationObserveCompositeTest.java

@ -20,11 +20,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.ResponseCode;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserveTest;
import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@ -55,17 +52,12 @@ import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fr
@Slf4j
public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MIntegrationObserveTest {
@SpyBean
DefaultLwM2mUplinkMsgHandler defaultUplinkMsgHandlerTest;
/**
* ObserveComposite {"ids":["5/0/7", "5/0/5", "5/0/3", "3/0/9", "19/1/0/0"]} - Ok
* @throws Exception
*/
@Test
public void testObserveCompositeAnyResources_Result_CONTENT_Value_LwM2mSingleResource_LwM2mResourceInstance() throws Exception {
sendObserveCancelAllWithAwait(deviceId);
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7;
String expectedIdVer5_0_5 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_5;
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3;
@ -88,7 +80,6 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
*/
@Test
public void testObserveComposite_ObjectInstanceWithOtherObjectResourceInstance_Result_CONTENT_Ok() throws Exception {
sendObserveCancelAllWithAwait(deviceId);
String expectedIdVer19_1_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0;
String expectedIdVer5_0 = objectInstanceIdVer_5;
String expectedIds = "[\"" + expectedIdVer19_1_0 + "\", \"" + expectedIdVer5_0 + "\"]";
@ -107,7 +98,6 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
*/
@Test
public void testObserveReadAll_AfterCompositeObservation_WithResourceNotReadable_Result_CONTENT_ObserveResourceNotReadableIsNull() throws Exception {
sendObserveCancelAllWithAwait(deviceId);
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7;
String expectedIdVer5_0_2 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_2;
String expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_2 + "\"]";
@ -127,7 +117,6 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
*/
@Test
public void testObserveComposite_Result_BAD_REQUEST_ONE_PATH_CONTAINCE_OTHER() throws Exception {
sendObserveCancelAllWithAwait(deviceId);
String expectedIdVer5_0 = objectInstanceIdVer_5;
String expectedIdVer5_0_2 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_2;
String expectedIds = "[\"" + expectedIdVer5_0 + "\", \"" + expectedIdVer5_0_2 + "\"]";
@ -140,7 +129,7 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
}
/**
* Previous -> "3/0/9", "19/0/2", "19/1/0", "19/0/0", All only SingleObservation;
* Previous -> "3/0/9" SingleObservation;
* if at least one of the resource objectIds (Composite) in SingleObservation or CompositeObservation is already registered - return BAD REQUEST
* ObserveComposite {"ids":["5/0/7", "5/0/5", "5/0/3", "3/0/9"]}
* @throws Exception
@ -152,13 +141,8 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText());
String actualValues = rpcActualResultReadAll.get("value").asText();
String expectedIdVer19_0_2 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2;
String expectedIdVer19_1_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0;
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(idVer_3_0_9)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer19_0_2)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer19_1_0)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(idVer_19_0_0)));
// Send Observe composite with "/3/0/9"
assertTrue(actualValues.contains("[]"));
sendRpcObserveWithContainsLwM2mSingleResource(idVer_3_0_9);
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7;
String expectedIdVer5_0_5 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_5;
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3;
@ -174,9 +158,6 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText());
actualValues = rpcActualResultReadAll.get("value").asText();
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(idVer_3_0_9)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer19_0_2)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer19_1_0)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(idVer_19_0_0)));
}
/**
* Previous -> ["5/0/7", "5/0/5", "5/0/3"], CompositeObservation *
@ -213,12 +194,6 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText());
actualValues = rpcActualResultReadAll.get("value").asText();
String expectedIdVer19_0_2 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2;
String expectedIdVer19_1_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0;
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(idVer_3_0_9)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer19_0_2)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer19_1_0)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(idVer_19_0_0)));
assertTrue(actualValues.contains("CompositeObservation:"));
assertTrue(actualValues.contains(expectedId5_0_7));
assertTrue(actualValues.contains(expectedId5_0_5));
@ -231,8 +206,6 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
*/
@Test
public void testObserveCompositeAnyResources_Result_CONTENT_Value_LwM2mSingleResource_LwM2mMultipleResource() throws Exception {
sendObserveCancelAllWithAwait(deviceId);
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7;
String expectedIdVer5_0_5 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_5;
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3;
@ -255,8 +228,6 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
*/
@Test
public void testObserveCompositeWithKeyName_Result_CONTENT_Value_SingleResources() throws Exception {
sendObserveCancelAllWithAwait(deviceId);
String expectedKey3_0_9 = RESOURCE_ID_NAME_3_9;
String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14;
String expectedKey19_0_0 = RESOURCE_ID_NAME_19_0_0;
@ -281,6 +252,7 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
*/
@Test
public void testObserveCompositeWithKeyName_IfLeastOneResourceIsAlreadyRegistered_return_BadRequest() throws Exception {
sendRpcObserveWithContainsLwM2mSingleResource(idVer_3_0_9);
String expectedKey3_0_9 = RESOURCE_ID_NAME_3_9;
String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14;
String expectedKey19_0_0 = RESOURCE_ID_NAME_19_0_0;
@ -299,8 +271,6 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
*/
@Test
public void testObserveReadAll_AfterbserveCancelAllAndCompositeObservation_Result_CONTENT_Value_CompositeObservation_Only() throws Exception {
sendObserveCancelAllWithAwait(deviceId);
String expectedKey3_0_9 = RESOURCE_ID_NAME_3_9;
String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14;
String expectedKey19_0_0 = RESOURCE_ID_NAME_19_0_0;
@ -330,7 +300,6 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
*/
@Test
public void testObserveCancelAllThenObserveCompositeAnyResources_Result_CONTENT_CancelObserveComposite_This_Result_Content_Count_1() throws Exception {
sendObserveCancelAllWithAwait(deviceId);
// ObserveComposite
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7;
String expectedIdVer5_0_5 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_5;
@ -356,7 +325,6 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
*/
@Test
public void testObserveCompositeFiveResources_Result_CONTENT_CancelObserveComposite_TwoAnyResource_Result_BadRequest() throws Exception {
sendObserveCancelAllWithAwait(deviceId);
// ObserveComposite five
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7;
String expectedIdVer5_0_5 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_5;
@ -384,7 +352,6 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
*/
@Test
public void testObserveOneObjectAnyResources_Result_CONTENT_Cancel_OneResourceFromObjectAnyResource_Result_BAD_REQUEST_Cancel_OneObject_Result_CONTENT() throws Exception {
sendObserveCancelAllWithAwait(deviceId);
// ObserveComposite
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3;
String expectedIdVer19_1_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + RESOURCE_INSTANCE_ID_0;
@ -419,17 +386,25 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
String idVer_19_0_2 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2;
String id_19_0_2 = fromVersionedIdToObjectId(idVer_19_0_2);
// 1 - "ObserveReadAll": at least one update value of all resources we observe - after connection
// 1 - Verify after start
String actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null);
ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText());
String rpcActualVValuesReadAll = rpcActualResultReadAll.get("value").asText();
ArrayNode rpcactualValues = JacksonUtil.fromString(rpcActualVValuesReadAll, ArrayNode.class);
assertEquals(rpcactualValues.size(), 4);
assertTrue(actualResultReadAll.contains("SingleObservation:" + id_3_0_9));
assertTrue(actualResultReadAll.contains("SingleObservation:" + id_19_1_0));
assertTrue(actualResultReadAll.contains("SingleObservation:" + id_19_0_2));
assertTrue(actualResultReadAll.contains("SingleObservation:" + id_19_0_0));
String actualValues = rpcActualResultReadAll.get("value").asText();
assertTrue(actualValues.contains("[]"));
sendRpcObserveWithContainsLwM2mSingleResource(idVer_3_0_9);
sendRpcObserveWithContainsLwM2mSingleResource(idVer_19_0_0);
sendRpcObserveWithContainsLwM2mSingleResource(idVer_19_1_0);
sendRpcObserveWithContainsLwM2mSingleResource(idVer_19_0_2);
actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null);
rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText());
actualValues = rpcActualResultReadAll.get("value").asText();
assertTrue(actualValues.contains("SingleObservation:" + id_3_0_9));
assertTrue(actualValues.contains("SingleObservation:" + id_19_1_0));
assertTrue(actualValues.contains("SingleObservation:" + id_19_0_2));
assertTrue(actualValues.contains("SingleObservation:" + id_19_0_0));
long initAttrTelemetryAtCount = countUpdateAttrTelemetryAll();
long initAttrTelemetryAtCount_3_0_9 = countUpdateAttrTelemetryResource(idVer_3_0_9);
long initAttrTelemetryAtCount_19_0_0 = countUpdateAttrTelemetryResource(idVer_19_0_0);
@ -448,8 +423,8 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null);
rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText());
rpcActualVValuesReadAll = rpcActualResultReadAll.get("value").asText();
rpcactualValues = JacksonUtil.fromString(rpcActualVValuesReadAll, ArrayNode.class);
String rpcActualVValuesReadAll = rpcActualResultReadAll.get("value").asText();
ArrayNode rpcactualValues = JacksonUtil.fromString(rpcActualVValuesReadAll, ArrayNode.class);
assertEquals(rpcactualValues.size(), 0);
// 2.1 - ObserveComposite: observeCancelAll verify"
initAttrTelemetryAtCount = countUpdateAttrTelemetryAll();
@ -517,13 +492,6 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, sendRpcRequest, String.class, status().isOk());
}
private long countUpdateAttrTelemetryAll() {
return Mockito.mockingDetails(defaultUplinkMsgHandlerTest)
.getInvocations().stream()
.filter(invocation -> invocation.getMethod().getName().equals("updateAttrTelemetry"))
.count();
}
private void updateAttrTelemetryAllAtLeastOnceAfterAction(long initialInvocationCount) {
AtomicLong newInvocationCount = new AtomicLong(initialInvocationCount);
log.warn("countUpdateAttrTelemetryAllAtLeastOnceAfterAction: initialInvocationCount [{}]", initialInvocationCount);
@ -536,19 +504,6 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
log.warn("countUpdateAttrTelemetryAllAtLeastOnceAfterAction: newInvocationCount [{}]", newInvocationCount.get());
}
private long countUpdateAttrTelemetryResource(String idVerRez) {
return Mockito.mockingDetails(defaultUplinkMsgHandlerTest)
.getInvocations().stream()
.filter(invocation ->
invocation.getMethod().getName().equals("updateAttrTelemetry") &&
invocation.getArguments().length > 1 &&
idVerRez.equals(invocation.getArguments()[1])
)
.count();
}
private void updateAttrTelemetryResourceAtLeastOnceAfterAction(long initialInvocationCount, String idVerRez) {
AtomicLong newInvocationCount = new AtomicLong(initialInvocationCount);
log.warn("countUpdateAttrTelemetryResourceAtLeastOnceAfterAction: initialInvocationCount [{}]", initialInvocationCount);
@ -560,24 +515,4 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
});
log.warn("countUpdateAttrTelemetryResourceAtLeastOnceAfterAction: newInvocationCount [{}]", newInvocationCount.get());
}
private long countUpdateReg() {
return Mockito.mockingDetails(defaultUplinkMsgHandlerTest)
.getInvocations().stream()
.filter(invocation -> invocation.getMethod().getName().equals("updatedReg"))
.count();
}
private void updateRegAtLeastOnceAfterAction() {
long initialInvocationCount = countUpdateReg();
AtomicLong newInvocationCount = new AtomicLong(initialInvocationCount);
log.warn("updateRegAtLeastOnceAfterAction: initialInvocationCount [{}]", initialInvocationCount);
await("Update Registration at-least-once after action")
.atMost(50, TimeUnit.SECONDS)
.until(() -> {
newInvocationCount.set(countUpdateReg());
return newInvocationCount.get() > initialInvocationCount;
});
log.warn("updateRegAtLeastOnceAfterAction: newInvocationCount [{}]", newInvocationCount.get());
}
}

27
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java

@ -22,6 +22,8 @@ import org.eclipse.leshan.core.link.Link;
import org.eclipse.leshan.core.link.LinkParseException;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.junit.Test;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.test.context.event.annotation.BeforeTestClass;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.transport.lwm2m.config.TbLwM2mVersion;
@ -30,8 +32,10 @@ import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTes
import java.util.Arrays;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.awaitility.Awaitility.await;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -39,10 +43,16 @@ import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPA
import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_6;
public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegrationTest {
@BeforeEach
public void beforeTest () throws Exception {
testInit();
}
/**
* DiscoverAll
*
@ -171,6 +181,17 @@ public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegration
assertEquals(ResponseCode.NOT_FOUND.getName(), rpcActualResult.get("result").asText());
}
@Test
public void testDiscoverRequestCannotTargetResourceInstance_Return_INTERNAL_SERVER_ERROR() throws Exception {
// ResourceInstanceId
String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6 + "/1";
String actualResult = sendDiscover(expectedPath);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.INTERNAL_SERVER_ERROR.getName(), rpcActualResult.get("result").asText());
String expected = "InvalidRequestException: Discover request cannot target resource instance path: /3/0/6/1";
assertTrue(rpcActualResult.get("error").asText().contains(expected));
}
private String sendDiscover(String path) throws Exception {
String setRpcRequest = "{\"method\": \"Discover\", \"params\": {\"id\": \"" + path + "\"}}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk());
@ -190,4 +211,10 @@ public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegration
return null;
}
}
public void testInit() throws Exception {
await("Update Registration at-least-once after start")
.atMost(50, TimeUnit.SECONDS)
.until(() -> countUpdateReg() > 0);
}
}

197
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverWriteAttributesTest.java

@ -19,12 +19,12 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import org.eclipse.leshan.core.ResponseCode;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.transport.util.JsonUtils;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_6;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_7;
@ -32,68 +32,22 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID
public class RpcLwm2mIntegrationDiscoverWriteAttributesTest extends AbstractRpcLwM2MIntegrationTest {
/**
* WriteAttributes {"id":"/3_1.2/0/6","attributes":{"pmax":100, "pmin":10}}
* if not implemented:
* {"result":"INTERNAL_SERVER_ERROR","error":"not implemented"}
* if implemented:
* {"result":"BAD_REQUEST","error":"Attribute pmax can be used for only Resource/Object Instance/Object."}
* <PROPERTIES> Class Attributes
* - dim (0-65535) Integer: Multiple-Instance Resource; R, Number of instances existing for a Multiple-Instance Resource
* <NOTIFICATION> Class Attributes
* - pmin (def = 0(sec)) Integer: Object; Object Instance; Resource; Resource Instance; RW, Readable Resource
* - pmax (def = -- ) Integer: Object; Object Instance; Resource; Resource Instance; RW, Readable Resource
* - Greater Than gt (def = -- ) Float: Resource; Resource Instance; RW, Numerical&Readable Resource
* - Less Than lt (def = -- ) Float: Resource; Resource Instance; RW, Numerical&Readable Resource
* - Step st (def = -- ) Float: Resource; Resource Instance; RW, Numerical&Readable Resource
*/
@Test
public void testWriteAttributesResourceWithParametersByResourceInstanceId_Result_BAD_REQUEST() throws Exception {
String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6 + "/1";
String expectedValue = "{\"pmax\":100, \"pmin\":10}";
String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
String expected = "Attribute pmax can be used for only Resource/Object Instance/Object.";
String actual = rpcActualResult.get("error").asText();
assertTrue(actual.equals(expected));
}
/**
* WriteAttributes {"id":"/3_1.2/0/6","attributes":{"pmax":100, "pmin":10}}
* if not implemented:
* {"result":"INTERNAL_SERVER_ERROR","error":"not implemented"}
* if implemented:
* {"result":"BAD_REQUEST","error":"Attribute pmax can be used for only Resource/Object Instance/Object."}
*/
@Test
public void testWriteAttributeResourceDimWithParametersByResourceId_Result_BAD_REQUEST() throws Exception {
String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6;
String expectedValue = "{\"dim\":3}";
String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
String expected = "Attribute dim is of class PROPERTIES but only NOTIFICATION attribute can be used in WRITE ATTRIBUTE request.";
String actual = rpcActualResult.get("error").asText();
assertTrue(actual.equals(expected));
}
@Test
public void testWriteAttributesResourceVerWithParametersById_Result_BAD_REQUEST() throws Exception {
String expectedPath = objectIdVer_3;
String expectedValue = "{\"ver\":1.3}";
String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
String expected = "Attribute ver is of class PROPERTIES but only NOTIFICATION attribute can be used in WRITE ATTRIBUTE request.";
String actual = rpcActualResult.get("error").asText();
assertTrue(actual.equals(expected));
}
@Test
public void testWriteAttributesResourceServerUriWithParametersById_Result_BAD_REQUEST() throws Exception {
String expectedPath = objectInstanceIdVer_1;
String actualResult = sendRPCReadById(expectedPath);
String expectedValue = "{\"uri\":\"coaps://localhost:5690\"}";
actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
String expected = "Attribute uri is of class PROPERTIES but only NOTIFICATION attribute can be used in WRITE ATTRIBUTE request.";
String actual = rpcActualResult.get("error").asText();
assertTrue(actual.equals(expected));
}
/**
* <PROPERTIES> Class Attributes
* Object Version ver Object
* Provide the version of the associated Object.
* "ver" only for objectId
* <PROPERTIES> Class Attributes
* Dimension dim Integer [0:255]
* Number of instances existing for a Multiple-Instance Resource
@ -105,131 +59,107 @@ public class RpcLwm2mIntegrationDiscoverWriteAttributesTest extends AbstractRpcL
* <Type>Integer</Type>
* <RangeEnumeration>0..7</RangeEnumeration>
* WriteAttributes implemented: Discover {"id":"3/0/6"} -> 'dim' = 3
* "ver" only for objectId
*/
@Test
public void testReadDIM_3_0_6_Only_R () throws Exception {
String path = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6;
String actualResult = sendDiscover(path);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String expected = "</3/0/6>;dim=3";
assertTrue(rpcActualResult.get("value").asText().equals(expected));
}
/**
* <PROPERTIES> Class Attributes
* Object Version ver Object
* Provide the version of the associated Object.
* "ver" only for objectId
*/
@Test
public void testReadVer () throws Exception {
public void testReadDIM_3_0_6_Only_R() throws Exception {
String path = objectIdVer_3;
String actualResult = sendDiscover(path);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String expected = "</3>;ver=1.2";
assertTrue(rpcActualResult.get("value").asText().contains(expected));
expected = "</3/0/6>;dim=3";
assertTrue(rpcActualResult.get("value").asText().contains(expected));
expected = "</3/0/7>;dim=3";
assertTrue(rpcActualResult.get("value").asText().contains(expected));
expected = "</3/0/8>;dim=3";
assertTrue(rpcActualResult.get("value").asText().contains(expected));
expected = "</3/0/11>;dim=1";
assertTrue(rpcActualResult.get("value").asText().contains(expected));
}
/**
* WriteAttributes {"id":"/3/0/14","attributes":{"pmax":100, "pmin":10}}
* if not implemented:
* {"result":"INTERNAL_SERVER_ERROR","error":"not implemented"}
* if implemented:
* {"result":"CHANGED"}
* result changed:
*
*/
@Test
public void testWriteAttributesResourceWithParametersById_Result_CHANGED() throws Exception {
String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14;
String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6;
String expectedValue = "{\"pmax\":100, \"pmin\":10}";
String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText());
// result changed
// result changed
actualResult = sendDiscover(expectedPath);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String expected = "</3/0/14>;pmax=100;pmin=10";
String expected = "</3/0/6>;pmax=100;pmin=10;dim=3";
assertTrue(rpcActualResult.get("value").asText().contains(expected));
}
/**
* <NOTIFICATION> Class Attributes
* Minimum/Maximum Period pmin/pmax
* Notes: The Minimum Period Attribute:
* -- indicates the minimum time in seconds the LwM2M Client MUST wait between two notifications. If a notification of an observed Resource is supposed to be generated but it is before pmin expiry, notification MUST be sent as soon as pmin expires. In the absence of this parameter, the Minimum Period is defined by the Default Minimum Period set in the LwM2M Server Account.
* Notes: The Maximum Period Attribute:
* -- indicates the maximum time in seconds the LwM2M Client MAY wait between two notifications. When this "Maximum Period" expires after the last notification, a new notification MUST be sent. In the absence of this parameter, the "Maximum Period" is defined by the Default Maximum Period when set in the LwM2M Server Account or considered as 0 otherwise. The value of 0, means pmax MUST be ignored. The maximum period parameter MUST be greater than the minimum period parameter otherwise pmax will be ignored for the Resource to which such inconsistent timing conditions are applied.
* Greater Than gt Resource
* Less Than lt Resource
* Step st Resource
*
* Object Id = 1
* Default Minimum Period Id = 2 300 or 0
* Default Maximum Period Id = 3 6000 or "-"
* </3/0>;pmax=65, </3/0/1>, <3/0/2>, </3/0/3>, </3/0/4>,
* <3/0/6>;dim=8,<3/0/7>;gt=50;lt=42.2;st=0.5,<3/0/8>;...
*/
@Test
public void testWriteAttributesPeriodLtGt () throws Exception {
public void testWriteAttributesResourceVerWithParametersById_Result_BAD_REQUEST() throws Exception {
String expectedPath = objectIdVer_3;
String expectedValue = "{\"ver\":1.3}";
String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
String expected = "Attribute ver is of class PROPERTIES but only NOTIFICATION attribute can be used in WRITE ATTRIBUTE request.";
String actual = rpcActualResult.get("error").asText();
assertTrue(actual.equals(expected));
}
@Test
public void testWriteAttributesObjectInstanceResourcePeriodLtGt_Return_CHANGED() throws Exception {
String expectedPath = objectInstanceIdVer_3;
String expectedValue = "{\"pmax\":60}";
String expectedValue = "{\"pmax\":65, \"pmin\":5}";
String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText());
expectedPath = objectInstanceIdVer_3;
expectedValue = "{\"pmax\":65}";
actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText());
expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_7;
expectedValue ="{\"gt\":50, \"lt\":42.2, \"st\":0.5}";
String expectedValueStr = "gt=50;lt=42.2;st=0.5";
JsonUtils.parse("{" + expectedValueStr + "}").toString();
expectedValue = JsonUtils.parse("{" + expectedValueStr + "}").toString();
actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText());
// ObjectId
// ObjectId
expectedPath = objectIdVer_3;
actualResult = sendDiscover(expectedPath);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
// String expected = "</3>;ver=1.2,</3/0>;pmax=60,</3/0/0>,</3/0/1>,</3/0/2>,</3/0/3>,</3/0/6>;dim=3,</3/0/7>;st=0.5;lt=42.2;gt=50.0,</3/0/8>,</3/0/9>,</3/0/10>,</3/0/11>;dim=1,</3/0/13>,</3/0/14>,</3/0/15>,</3/0/16>,</3/0/17>,</3/0/18>,</3/0/19>,</3/0/20>,</3/0/21>";
String expected = "</3>;ver=1.2,</3/0>;pmax=65";
assertTrue(rpcActualResult.get("value").asText().contains(expected));
expected = "</3/0/6>;dim=3,</3/0/7>;st=0.5;lt=42.2;gt=50.0";
assertTrue(rpcActualResult.get("value").asText().contains(expected));
// ObjectInstanceId
String actualValue = rpcActualResult.get("value").asText();
String expected = "</3>;ver=1.2,</3/0>;pmax=65;pmin=5";
assertTrue(actualValue.contains(expected));
expected = "</3/0/6>;dim=3";
assertTrue(actualValue.contains(expected));
expected = "</3/0/7>;" + expectedValueStr + ";dim=3";
assertTrue(actualValue.contains(expected));
// ObjectInstanceId
expectedPath = objectInstanceIdVer_3;
actualResult = sendDiscover(expectedPath);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
expected = "</3/0>;pmax=65";
assertTrue(rpcActualResult.get("value").asText().contains(expected));
expected = "</3/0/6>;dim=3,</3/0/7>;st=0.5;lt=42.2;gt=50.0";
assertTrue(rpcActualResult.get("value").asText().contains(expected));
// ResourceId
actualValue = rpcActualResult.get("value").asText();
expected = "</3/0>;pmax=65;pmin=5";
assertTrue(actualValue.contains(expected));
expected = "</3/0/7>;" + expectedValueStr + ";dim=3";
assertTrue(actualValue.contains(expected));
// ResourceId
expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6;
actualResult = sendDiscover(expectedPath);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
expected = "</3/0/6>;dim=3";
expected = "</3/0/6>;dim=3,</3/0/6/0>,</3/0/6/1>,</3/0/6/2>";
assertTrue(rpcActualResult.get("value").asText().contains(expected));
expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_7;
actualResult = sendDiscover(expectedPath);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
expected = "</3/0/7>;st=0.5;lt=42.2;gt=50.0";
expected = "</3/0/7>;" + expectedValueStr;
assertTrue(rpcActualResult.get("value").asText().contains(expected));
// ResourceInstanceId
expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6+ "/1";
actualResult = sendDiscover(expectedPath);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.INTERNAL_SERVER_ERROR.getName(), rpcActualResult.get("result").asText());
expected = "InvalidRequestException: Discover request cannot target resource instance path: /3/0/6/1";
assertTrue(rpcActualResult.get("error").asText().contains(expected));
}
private String sendRPCExecuteWithValueById(String path, String value) throws Exception {
@ -237,11 +167,6 @@ public class RpcLwm2mIntegrationDiscoverWriteAttributesTest extends AbstractRpcL
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk());
}
private String sendRPCReadById(String path) throws Exception {
String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"id\": \"" + path + "\"}}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk());
}
private String sendDiscover(String path) throws Exception {
String setRpcRequest = "{\"method\": \"Discover\", \"params\": {\"id\": \"" + path + "\"}}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk());

38
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java

@ -20,15 +20,11 @@ import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.LwM2m.Version;
import org.eclipse.leshan.core.ResponseCode;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.response.ReadResponse;
import org.eclipse.leshan.server.registration.Registration;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserveTest;
import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler;
import java.util.Optional;
import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL;
import static org.junit.Assert.assertEquals;
@ -41,21 +37,21 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INST
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId;
@Slf4j
public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationObserveTest {
@SpyBean
DefaultLwM2mUplinkMsgHandler defaultUplinkMsgHandlerTest;
@Before
public void setupObserveTest() throws Exception {
awaitObserveReadAll(4, deviceId);
}
@Test
public void testObserveReadAll_Count_4_CancelAll_Count_0_Ok() throws Exception {
String actualValuesReadAll = sendRpcObserveOkWithResultValue("ObserveReadAll", null);
assertEquals(4, actualValuesReadAll.split(",").length);
sendObserveCancelAllWithAwait(deviceId);
actualValuesReadAll = sendRpcObserveOkWithResultValue("ObserveReadAll", null);
assertEquals("[]", actualValuesReadAll);
}
/**
@ -64,12 +60,12 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO
*/
@Test
public void testObserveOneResource_Result_CONTENT_Value_Count_3_After_Cancel_Count_2() throws Exception {
long initSendTelemetryAtCount = countSendParametersOnThingsboardTelemetryResource(RESOURCE_ID_NAME_3_9);
sendObserveCancelAllWithAwait(deviceId);
sendRpcObserveWithContainsLwM2mSingleResource(idVer_3_0_9);
int cntUpdate = 3;
verify(defaultUplinkMsgHandlerTest, timeout(10000).times(cntUpdate))
.onUpdateValueAfterReadResponse(Mockito.any(Registration.class), eq(idVer_3_0_9), Mockito.any(ReadResponse.class));
updateRegAtLeastOnceAfterAction();
long lastSendTelemetryAtCount = countSendParametersOnThingsboardTelemetryResource(RESOURCE_ID_NAME_3_9);
assertTrue(lastSendTelemetryAtCount > initSendTelemetryAtCount);
}
/**
@ -84,7 +80,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO
int cntUpdate = 3;
verify(defaultUplinkMsgHandlerTest, timeout(10000).times(cntUpdate))
.updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9));
.updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9), eq(null));
}
/**
@ -99,7 +95,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO
int cntUpdate = 3;
verify(defaultUplinkMsgHandlerTest, timeout(10000).times(cntUpdate))
.updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9));
.updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9), eq(null));
}
/**
@ -334,7 +330,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO
cntUpdate = 10;
verify(defaultUplinkMsgHandlerTest, timeout(50000).atLeast(cntUpdate))
.updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9));
.updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9), eq(null));
}
private void sendRpcObserveWithWithTwoResource(String expectedId_1, String expectedId_2) throws Exception {
@ -348,11 +344,5 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
return rpcActualResult.get("value").asText();
}
private void sendRpcObserveWithContainsLwM2mSingleResource(String params) throws Exception {
String rpcActualResult = sendRpcObserveOkWithResultValue("Observe", params);
assertTrue(rpcActualResult.contains("LwM2mSingleResource"));
assertEquals(Optional.of(1).get(), Optional.ofNullable(getCntObserveAll(deviceId)).get());
}
}

98
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadCollectedValueTest.java

@ -0,0 +1,98 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.lwm2m.rpc.sql;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest;
import java.util.concurrent.atomic.AtomicReference;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_12;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_1;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_VALUE_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_VALUE_1;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3303_12_5700;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS;
@Slf4j
public class RpcLwm2mIntegrationReadCollectedValueTest extends AbstractRpcLwM2MIntegrationTest {
/**
* Read {"id":"/3303/12/5700"}
* Trigger a Send operation from the client with multiple values for the same resource as a payload
* acked "[{"bn":"/3303/12/5700","bt":1724".. 116 bytes]
* 2 values for the resource /3303/12/5700 should be stored with:
* - timestamps1 = Instance.now() + RESOURCE_ID_VALUE_3303_12_5700_1
* - timestamps2 = (timestamps1 + 3 sec) + RESOURCE_ID_VALUE_3303_12_5700_2
* @throws Exception
*/
@Test
public void testReadSingleResource_sendFromClient_CollectedValue() throws Exception {
// init test
int cntValues = 2;
int resourceId = 5700;
String expectedIdVer = objectIdVer_3303 + "/" + OBJECT_INSTANCE_ID_12 + "/" + resourceId;
sendRPCById(expectedIdVer);
// verify time start/end send CollectedValue;
await().atMost(40, SECONDS).until(() -> RESOURCE_ID_3303_12_5700_TS_0 > 0
&& RESOURCE_ID_3303_12_5700_TS_1 > 0);
// verify result read: verify count value: 1-2: send CollectedValue;
AtomicReference<ObjectNode> actualValues = new AtomicReference<>();
await().atMost(40, SECONDS).until(() -> {
actualValues.set(doGetAsync(
"/api/plugins/telemetry/DEVICE/" + deviceId + "/values/timeseries?keys="
+ RESOURCE_ID_NAME_3303_12_5700
+ "&startTs=" + (RESOURCE_ID_3303_12_5700_TS_0 - RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS)
+ "&endTs=" + (RESOURCE_ID_3303_12_5700_TS_1 + RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS)
+ "&interval=0&limit=100&useStrictDataTypes=false",
ObjectNode.class));
return actualValues.get() != null && actualValues.get().size() > 0
&& actualValues.get().get(RESOURCE_ID_NAME_3303_12_5700).size() >= cntValues && verifyTs(actualValues);
});
}
private boolean verifyTs(AtomicReference<ObjectNode> actualValues) {
String expectedVal_0 = String.valueOf(RESOURCE_ID_3303_12_5700_VALUE_0);
String expectedVal_1 = String.valueOf(RESOURCE_ID_3303_12_5700_VALUE_1);
ArrayNode actual = (ArrayNode) actualValues.get().get(RESOURCE_ID_NAME_3303_12_5700);
long actualTS0 = 0;
long actualTS1 = 0;
for (JsonNode tsNode : actual) {
if (tsNode.get("value").asText().equals(expectedVal_0)) {
actualTS0 = tsNode.get("ts").asLong();
} else if (tsNode.get("value").asText().equals(expectedVal_1)) {
actualTS1 = tsNode.get("ts").asLong();
}
}
return actualTS0 >= RESOURCE_ID_3303_12_5700_TS_0
&& actualTS1 <= RESOURCE_ID_3303_12_5700_TS_1
&& (actualTS1 - actualTS0) >= RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS;
}
private String sendRPCById(String path) throws Exception {
String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"id\": \"" + path + "\"}}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk());
}
}

90
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java

@ -18,32 +18,19 @@ package org.thingsboard.server.transport.lwm2m.rpc.sql;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.ResponseCode;
import org.eclipse.leshan.core.node.LwM2mNode;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.node.LwM2mResource;
import org.eclipse.leshan.core.node.TimestampedLwM2mNodes;
import org.eclipse.leshan.server.registration.Registration;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest;
import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler;
import java.time.Instant;
import java.util.Map;
import static org.eclipse.leshan.core.LwM2mId.SERVER;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_12;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_1;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_11;
@ -59,10 +46,6 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID
@Slf4j
public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest {
@SpyBean
DefaultLwM2mUplinkMsgHandler defaultUplinkMsgHandlerTest;
/**
* Read {"id":"/3"}
* Read {"id":"/6"}...
@ -88,7 +71,7 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest
e.printStackTrace();
}
});
} catch (Exception e2){
} catch (Exception e2) {
e2.printStackTrace();
}
}
@ -99,10 +82,10 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest
* @throws Exception
*/
@Test
public void testReadAllInstancesInClientById_Result_CONTENT_Value_IsInstances_IsResources() throws Exception{
public void testReadAllInstancesInClientById_Result_CONTENT_Value_IsInstances_IsResources() throws Exception {
expectedObjectIdVerInstances.forEach(expected -> {
try {
String actualResult = sendRPCById((String) expected);
String actualResult = sendRPCById((String) expected);
String expectedObjectId = pathIdVerToObjectId((String) expected);
LwM2mPath expectedPath = new LwM2mPath(expectedObjectId);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
@ -122,7 +105,7 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest
*/
@Test
public void testReadMultipleResourceById_Result_CONTENT_Value_IsLwM2mMultipleResource() throws Exception {
String expectedIdVer = objectInstanceIdVer_3 +"/" + RESOURCE_ID_11;
String expectedIdVer = objectInstanceIdVer_3 + "/" + RESOURCE_ID_11;
String actualResult = sendRPCById(expectedIdVer);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
@ -135,7 +118,7 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest
*/
@Test
public void testReadSingleResourceById_Result_CONTENT_Value_IsLwM2mSingleResource() throws Exception {
String expectedIdVer = objectInstanceIdVer_3 +"/" + RESOURCE_ID_14;
String expectedIdVer = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14;
String actualResult = sendRPCById(expectedIdVer);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
@ -161,7 +144,7 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest
*/
@Test
public void testReadCompositeSingleResourceByIds_Result_CONTENT_Value_IsObjectIsLwM2mSingleResourceIsLwM2mMultipleResource() throws Exception {
String expectedIdVer_1 = (String) expectedObjectIdVers.stream().filter(path -> (!((String)path).contains("/" + BINARY_APP_DATA_CONTAINER) && ((String)path).contains("/" + SERVER))).findFirst().get();
String expectedIdVer_1 = (String) expectedObjectIdVers.stream().filter(path -> (!((String) path).contains("/" + BINARY_APP_DATA_CONTAINER) && ((String) path).contains("/" + SERVER))).findFirst().get();
String objectId_1 = pathIdVerToObjectId(expectedIdVer_1);
String expectedIdVer3_0_1 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_1;
String expectedIdVer3_0_11 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_11;
@ -221,8 +204,8 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest
String objectId_19 = pathIdVerToObjectId(objectIdVer_19);
String expected3_0_9 = objectInstanceId_3 + "/" + RESOURCE_ID_9 + "=LwM2mSingleResource [id=" + RESOURCE_ID_9 + ", value=";
String expected3_0_14 = objectInstanceId_3 + "/" + RESOURCE_ID_14 + "=LwM2mSingleResource [id=" + RESOURCE_ID_14 + ", value=";
String expected19_0_0 = objectId_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0 + expectedKey19_X_0;
String expected19_1_0 = objectId_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + expectedKey19_X_0;
String expected19_0_0 = objectId_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0 + expectedKey19_X_0;
String expected19_1_0 = objectId_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + expectedKey19_X_0;
String actualValues = rpcActualResult.get("value").asText();
assertTrue(actualValues.contains(expected3_0_9));
assertTrue(actualValues.contains(expected3_0_14));
@ -230,60 +213,6 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest
assertTrue(actualValues.contains(expected19_1_0));
}
/**
* /3303/0/5700
* Read {"id":"/3303/0/5700"}
* Trigger a Send operation from the client with multiple values for the same resource as a payload
* acked "[{"bn":"/3303/12/5700","bt":1724".. 116 bytes]
* 2 values for the resource /3303/12/5700 should be stored with timestamps1 = Instance.now(), timestamps2 = Instance.now()
*
* @throws Exception
*/
@Test
public void testReadSingleResource_sendFromClient_CollectedValue() throws Exception {
TimestampedLwM2mNodes[] tsNodesHolder = new TimestampedLwM2mNodes[1];
doAnswer(inv -> {
tsNodesHolder[0] = inv.getArgument(1);
return null;
}).when(defaultUplinkMsgHandlerTest).onUpdateValueWithSendRequest(
Mockito.any(Registration.class),
Mockito.any(TimestampedLwM2mNodes.class)
);
int resourceId = 5700;
String expectedIdVer = objectIdVer_3303 + "/" + OBJECT_INSTANCE_ID_12 + "/" + resourceId;
String actualResult = sendRPCById(expectedIdVer);
verify(defaultUplinkMsgHandlerTest, timeout(10000).times(1))
.onUpdateValueWithSendRequest(Mockito.any(Registration.class), Mockito.any(TimestampedLwM2mNodes.class));
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String expected = "LwM2mSingleResource [id=" + resourceId + ", value=";
String actual = rpcActualResult.get("value").asText();
assertTrue(actual.contains(expected));
int indStart = actual.indexOf(expected) + expected.length();
int indEnd = actual.indexOf(",", indStart);
String valStr = actual.substring(indStart, indEnd);
double dd = Double.parseDouble(valStr);
long combined = Double.doubleToRawLongBits(dd);
int t0 = (int) (combined >> 32);
int t1 = (int) combined;
double[] expectedValues ={(double)t0/100, (double)t1/100};
int ind = 0;
LwM2mPath expectedPath = new LwM2mPath("/3303/12/5700");
for (Instant ts : tsNodesHolder[0].getTimestamps()) {
Map<LwM2mPath, LwM2mNode> nodesAt = tsNodesHolder[0].getNodesAt(ts);
for (var instant : nodesAt.entrySet()) {
LwM2mPath actualPath = instant.getKey();
LwM2mNode node = instant.getValue();
LwM2mResource lwM2mResource = (LwM2mResource) node;
assertEquals(expectedPath, actualPath);
assertEquals(expectedValues[ind], lwM2mResource.getValue());
ind++;
}
}
}
/**
* ReadComposite {"keys":["batteryLevel", "UtfOffset", "dataDescription"]}
*/
@ -301,7 +230,6 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest
assertEquals(actualValue, expectedValue);
}
private String sendRPCById(String path) throws Exception {
String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"id\": \"" + path + "\"}}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk());

14
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java

@ -25,6 +25,7 @@ import org.junit.Assert;
import org.springframework.test.web.servlet.MvcResult;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.device.credentials.lwm2m.AbstractLwM2MClientSecurityCredential;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MBootstrapClientCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MClientCredential;
@ -41,6 +42,7 @@ import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.LwM2MBo
import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.PSKLwM2MBootstrapServerCredential;
import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.RPKLwM2MBootstrapServerCredential;
import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.X509LwM2MBootstrapServerCredential;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.dao.service.DaoSqlTest;
@ -203,8 +205,8 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M
boolean isAwaitObserveReadAll,
LwM2MClientState finishState,
boolean isStartLw) throws Exception {
createDeviceProfile(transportConfiguration);
final Device device = createDevice(deviceCredentials, endpoint);
DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + endpoint, transportConfiguration);
final Device device = createLwm2mDevice(deviceCredentials, endpoint, deviceProfile.getId());
createNewClient(security, securityBs, true, endpoint);
lwM2MTestClient.start(isStartLw);
if (isAwaitObserveReadAll) {
@ -248,8 +250,8 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M
Set<LwM2MClientState> expectedStatusesLwm2m,
Set<LwM2MClientState> expectedStatusesBs) throws Exception {
createDeviceProfile(transportConfiguration);
final Device device = createDevice(deviceCredentials, endpoint);
DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + endpoint, transportConfiguration);
final Device device = createLwm2mDevice(deviceCredentials, endpoint, deviceProfile.getId());
String deviceIdStr = device.getId().getId().toString();
createNewClient(security, securityBs, true, endpoint);
lwM2MTestClient.start(true);
@ -446,10 +448,10 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M
return bootstrapCredentials;
}
protected MvcResult createDeviceWithMvcResult(LwM2MDeviceCredentials credentials, String endpoint) throws Exception {
protected MvcResult createDeviceWithMvcResult(LwM2MDeviceCredentials credentials, String endpoint, DeviceProfileId deviceProfileId) throws Exception {
Device device = new Device();
device.setName(endpoint);
device.setDeviceProfileId(deviceProfile.getId());
device.setDeviceProfileId(deviceProfileId);
device.setTenantId(tenantId);
device = doPost("/api/device", device, Device.class);
Assert.assertNotNull(device);

7
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLengthTest.java

@ -20,7 +20,7 @@ import org.eclipse.californium.elements.config.Configuration;
import org.eclipse.leshan.client.californium.endpoint.CaliforniumClientEndpoint;
import org.eclipse.leshan.client.californium.endpoint.CaliforniumClientEndpointsProvider;
import org.junit.Assert;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest;
@ -49,9 +49,8 @@ public abstract class AbstractSecurityLwM2MIntegrationDtlsCidLengthTest extends
protected void basicTestConnectionDtlsCidLength(Integer clientDtlsCidLength,
Integer serverDtlsCidLength) throws Exception {
createDeviceProfile(transportConfiguration);
final Device device = createDevice(deviceCredentials, clientEndpoint);
device.getId().getId().toString();
DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + clientEndpoint, transportConfiguration);
createLwm2mDevice(deviceCredentials, clientEndpoint, deviceProfile.getId());
createNewClient(security, null, true, clientEndpoint, clientDtlsCidLength);
lwM2MTestClient.start(true);
await(awaitAlias)

5
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/diffPort/AbstractLwM2MIntegrationDiffPortTest.java

@ -23,6 +23,7 @@ import org.eclipse.leshan.server.registration.RegistrationStore;
import org.eclipse.leshan.server.registration.RegistrationUpdate;
import org.junit.Assert;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest;
@ -59,8 +60,8 @@ public abstract class AbstractLwM2MIntegrationDiffPortTest extends AbstractSecur
return invocation.callRealMethod();
}).when(registrationStoreTest).updateRegistration(any(RegistrationUpdate.class));
createDeviceProfile(transportConfiguration);
createDevice(deviceCredentials, clientEndpoint);
DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + clientEndpoint, transportConfiguration);
createLwm2mDevice(deviceCredentials, clientEndpoint, deviceProfile.getId());
createNewClient(security, null, true, clientEndpoint);
lwM2MTestClient.start(true);
await(awaitAlias)

5
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java

@ -20,6 +20,7 @@ import org.eclipse.leshan.client.object.Security;
import org.eclipse.leshan.core.util.Hex;
import org.junit.Test;
import org.springframework.test.web.servlet.MvcResult;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.PSKClientCredential;
import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration;
@ -76,9 +77,9 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes
clientCredentials.setIdentity(identity);
clientCredentials.setKey(keyPsk);
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, NONE));
createDeviceProfile(transportConfiguration);
DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + clientEndpoint, transportConfiguration);
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, null, null, PSK, false);
MvcResult result = createDeviceWithMvcResult(deviceCredentials, clientEndpoint);
MvcResult result = createDeviceWithMvcResult(deviceCredentials, clientEndpoint, deviceProfile.getId());
assertEquals(HttpServletResponse.SC_BAD_REQUEST, result.getResponse().getStatus());
String msgExpected = "Key must be HexDec format: 32, 64, 128 characters!";
assertTrue(result.getResponse().getContentAsString().contains(msgExpected));

15
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java

@ -21,6 +21,7 @@ import org.eclipse.leshan.client.object.Security;
import org.eclipse.leshan.core.util.Hex;
import org.junit.Test;
import org.springframework.test.web.servlet.MvcResult;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.RPKClientCredential;
import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration;
@ -33,6 +34,7 @@ import static org.eclipse.leshan.client.object.Security.rpk;
import static org.eclipse.leshan.client.object.Security.rpkBootstrap;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode.PSK;
import static org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode.RPK;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_SUCCESS;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOTH;
@ -75,10 +77,11 @@ public class RpkLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTes
RPKClientCredential clientCredentials = new RPKClientCredential();
clientCredentials.setEndpoint(clientEndpoint);
clientCredentials.setKey(Hex.encodeHexString(certificate.getPublicKey().getEncoded()));
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(RPK, NONE));
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, NONE));
DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + clientEndpoint, transportConfiguration);
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, RPK, false);
createDeviceProfile(transportConfiguration);
MvcResult result = createDeviceWithMvcResult(deviceCredentials, clientEndpoint);
MvcResult result = createDeviceWithMvcResult(deviceCredentials, clientEndpoint, deviceProfile.getId());
assertEquals(HttpServletResponse.SC_BAD_REQUEST, result.getResponse().getStatus());
String msgExpected = "LwM2M client RPK key must be in standard [RFC7250] and support only EC algorithm and then encoded to Base64 format!";
assertTrue(result.getResponse().getContentAsString().contains(msgExpected));
@ -92,10 +95,10 @@ public class RpkLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTes
RPKClientCredential clientCredentials = new RPKClientCredential();
clientCredentials.setEndpoint(clientEndpoint);
clientCredentials.setKey(Base64.encodeBase64String(certificate.getPublicKey().getEncoded()));
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(RPK, NONE));
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, NONE));
DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + clientEndpoint, transportConfiguration);
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, RPK, true);
createDeviceProfile(transportConfiguration);
MvcResult result = createDeviceWithMvcResult(deviceCredentials, clientEndpoint);
MvcResult result = createDeviceWithMvcResult(deviceCredentials, clientEndpoint, deviceProfile.getId());
assertEquals(HttpServletResponse.SC_BAD_REQUEST, result.getResponse().getStatus());
String msgExpected = "Bootstrap server client RPK secret key must be in PKCS#8 format (DER encoding, standard [RFC5958]) and then encoded to Base64 format!";
assertTrue(result.getResponse().getContentAsString().contains(msgExpected));

14
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java

@ -20,6 +20,7 @@ import org.eclipse.leshan.client.object.Security;
import org.eclipse.leshan.core.util.Hex;
import org.junit.Test;
import org.springframework.test.web.servlet.MvcResult;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.X509ClientCredential;
import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration;
@ -33,6 +34,7 @@ import static org.eclipse.leshan.client.object.Security.x509;
import static org.eclipse.leshan.client.object.Security.x509Bootstrap;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode.PSK;
import static org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode.X509;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_SUCCESS;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOTH;
@ -76,10 +78,10 @@ public class X509_NoTrustLwM2MIntegrationTest extends AbstractSecurityLwM2MInteg
X509ClientCredential clientCredentials = new X509ClientCredential();
clientCredentials.setEndpoint(clientEndpoint);
clientCredentials.setCert(Hex.encodeHexString(certificate.getEncoded()));
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(X509, NONE));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, X509, false);
createDeviceProfile(transportConfiguration);
MvcResult result = createDeviceWithMvcResult(deviceCredentials, clientEndpoint);
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, NONE));
DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + clientEndpoint, transportConfiguration);
MvcResult result = createDeviceWithMvcResult(deviceCredentials, clientEndpoint, deviceProfile.getId());
assertEquals(HttpServletResponse.SC_BAD_REQUEST, result.getResponse().getStatus());
String msgExpected = "LwM2M client X509 certificate must be in DER-encoded X509v3 format and support only EC algorithm and then encoded to Base64 format!";
assertTrue(result.getResponse().getContentAsString().contains(msgExpected));
@ -93,10 +95,10 @@ public class X509_NoTrustLwM2MIntegrationTest extends AbstractSecurityLwM2MInteg
X509ClientCredential clientCredentials = new X509ClientCredential();
clientCredentials.setEndpoint(clientEndpoint);
clientCredentials.setCert(Base64.getEncoder().encodeToString(certificate.getEncoded()));
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(X509, NONE));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, X509, true);
createDeviceProfile(transportConfiguration);
MvcResult result = createDeviceWithMvcResult(deviceCredentials, clientEndpoint);
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, NONE));
DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + clientEndpoint, transportConfiguration);
MvcResult result = createDeviceWithMvcResult(deviceCredentials, clientEndpoint, deviceProfile.getId());
assertEquals(HttpServletResponse.SC_BAD_REQUEST, result.getResponse().getStatus());
String msgExpected = "Bootstrap server client X509 secret key must be in PKCS#8 format (DER encoding, standard [RFC5958]) and then encoded to Base64 format!";
assertTrue(result.getResponse().getContentAsString().contains(msgExpected));

2
common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java

@ -136,7 +136,7 @@ public class EdgeGrpcClient implements EdgeRpcClient {
.setConnectRequestMsg(ConnectRequestMsg.newBuilder()
.setEdgeRoutingKey(edgeKey)
.setEdgeSecret(edgeSecret)
.setEdgeVersion(EdgeVersion.V_3_7_1)
.setEdgeVersion(EdgeVersion.V_3_8_0)
.setMaxInboundMessageSize(maxInboundMessageSize)
.build())
.build());

2
common/edge-api/src/main/proto/edge.proto

@ -39,7 +39,7 @@ enum EdgeVersion {
V_3_6_2 = 5;
V_3_6_4 = 6;
V_3_7_0 = 7;
V_3_7_1 = 8;
V_3_8_0 = 8;
}
/**

1
common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java

@ -407,6 +407,7 @@ public class ProtoUtils {
.setRequestIdMSB(msg.getMsg().getId().getMostSignificantBits())
.setRequestIdLSB(msg.getMsg().getId().getLeastSignificantBits())
.setOneway(msg.getMsg().isOneway())
.setPersisted(msg.getMsg().isPersisted())
.build();
return TransportProtos.ToDeviceRpcRequestActorMsgProto.newBuilder()

68
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java

@ -46,6 +46,7 @@ import org.thingsboard.server.transport.coap.CoapTransportContext;
import org.thingsboard.server.transport.coap.callback.CoapDeviceAuthCallback;
import org.thingsboard.server.transport.coap.callback.CoapEfentoCallback;
import org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils;
import org.thingsboard.server.transport.coap.efento.utils.PulseCounterType;
import java.nio.ByteBuffer;
import java.text.SimpleDateFormat;
@ -62,8 +63,25 @@ import static org.thingsboard.server.transport.coap.CoapTransportService.CONFIGU
import static org.thingsboard.server.transport.coap.CoapTransportService.CURRENT_TIMESTAMP;
import static org.thingsboard.server.transport.coap.CoapTransportService.DEVICE_INFO;
import static org.thingsboard.server.transport.coap.CoapTransportService.MEASUREMENTS;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.BREATH_VOC_METADATA_FACTOR;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.CO2_EQUIVALENT_METADATA_FACTOR;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.CO2_GAS_METADATA_FACTOR;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.ELEC_METER_ACC_MAJOR_METADATA_FACTOR;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.ELEC_METER_ACC_MINOR_METADATA_FACTOR;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.IAQ_METADATA_FACTOR;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.PULSE_CNT_ACC_MAJOR_METADATA_FACTOR;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.PULSE_CNT_ACC_MINOR_METADATA_FACTOR;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.PULSE_CNT_ACC_WIDE_MAJOR_METADATA_FACTOR;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.PULSE_CNT_ACC_WIDE_MINOR_METADATA_FACTOR;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.STATIC_IAQ_METADATA_FACTOR;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.WATER_METER_ACC_MAJOR_METADATA_FACTOR;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.WATER_METER_ACC_MINOR_METADATA_FACTOR;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.isBinarySensor;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.isSensorError;
import static org.thingsboard.server.transport.coap.efento.utils.PulseCounterType.ELEC_METER_ACC;
import static org.thingsboard.server.transport.coap.efento.utils.PulseCounterType.PULSE_CNT_ACC;
import static org.thingsboard.server.transport.coap.efento.utils.PulseCounterType.PULSE_CNT_ACC_WIDE;
import static org.thingsboard.server.transport.coap.efento.utils.PulseCounterType.WATER_CNT_ACC;
@Slf4j
public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
@ -84,6 +102,7 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
List<String> uriPath = request.getOptions().getUriPath();
boolean validPath = uriPath.size() == CHILD_RESOURCE_POSITION && uriPath.get(1).equals(CURRENT_TIMESTAMP);
if (!validPath) {
log.trace("Invalid path: [{}]", uriPath);
exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
} else {
int dateInSec = (int) (System.currentTimeMillis() / 1000);
@ -98,6 +117,7 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
Request request = advanced.getRequest();
List<String> uriPath = request.getOptions().getUriPath();
if (uriPath.size() != CHILD_RESOURCE_POSITION) {
log.trace("Unexpected uri path size, uri path: [{}]", uriPath);
exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
return;
}
@ -113,6 +133,7 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
processConfigurationRequest(exchange);
break;
default:
log.trace("Unexpected request type: [{}]", requestType);
exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
break;
}
@ -179,6 +200,7 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
log.error("[{}] Failed to decode Efento ProtoConfig: ", sessionId, e);
exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
} catch (InvalidProtocolBufferException e) {
log.error("[{}] Error while processing efento message: ", sessionId, e);
throw new RuntimeException(e);
}
});
@ -312,7 +334,7 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
values.addProperty("pulse_cnt_" + channelNumber, (double) (startPoint + sampleOffset));
break;
case MEASUREMENT_TYPE_IAQ:
values.addProperty("iaq_" + channelNumber, (startPoint + sampleOffset));
addPropertiesForMeasurementTypeWithMetadataFactor(values, "iaq_", channelNumber, startPoint + sampleOffset, IAQ_METADATA_FACTOR);
break;
case MEASUREMENT_TYPE_ELECTRICITY_METER:
values.addProperty("watt_hour_" + channelNumber, (double) (startPoint + sampleOffset));
@ -330,22 +352,25 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
values.addProperty("distance_mm_" + channelNumber, (double) (startPoint + sampleOffset));
break;
case MEASUREMENT_TYPE_WATER_METER_ACC_MINOR:
values.addProperty("acc_counter_water_minor_" + channelNumber, (double) (startPoint + sampleOffset));
calculateAccPulseCounterTotalValue(values, WATER_CNT_ACC , channelNumber, startPoint + sampleOffset, WATER_METER_ACC_MINOR_METADATA_FACTOR);
break;
case MEASUREMENT_TYPE_WATER_METER_ACC_MAJOR:
values.addProperty("acc_counter_water_major_" + channelNumber, (double) (startPoint + sampleOffset));
addPropertiesForMeasurementTypeWithMetadataFactor(values, WATER_CNT_ACC.getPrefix(), channelNumber, startPoint + sampleOffset, WATER_METER_ACC_MAJOR_METADATA_FACTOR);
break;
case MEASUREMENT_TYPE_HUMIDITY_ACCURATE:
values.addProperty("humidity_relative_" + channelNumber, (double) (startPoint + sampleOffset) / 10f);
break;
case MEASUREMENT_TYPE_STATIC_IAQ:
values.addProperty("static_iaq_" + channelNumber, (double) (startPoint + sampleOffset));
addPropertiesForMeasurementTypeWithMetadataFactor(values, "static_iaq_", channelNumber, startPoint + sampleOffset, STATIC_IAQ_METADATA_FACTOR);
break;
case MEASUREMENT_TYPE_CO2_GAS:
addPropertiesForMeasurementTypeWithMetadataFactor(values, "co2_gas_", channelNumber, startPoint + sampleOffset, CO2_GAS_METADATA_FACTOR);
break;
case MEASUREMENT_TYPE_CO2_EQUIVALENT:
values.addProperty("co2_ppm_" + channelNumber, (double) (startPoint + sampleOffset));
addPropertiesForMeasurementTypeWithMetadataFactor(values, "co2_", channelNumber, startPoint + sampleOffset, CO2_EQUIVALENT_METADATA_FACTOR);
break;
case MEASUREMENT_TYPE_BREATH_VOC:
values.addProperty("breath_voc_ppm_" + channelNumber, (double) (startPoint + sampleOffset));
addPropertiesForMeasurementTypeWithMetadataFactor(values, "breath_voc_", channelNumber, startPoint + sampleOffset, BREATH_VOC_METADATA_FACTOR);
break;
case MEASUREMENT_TYPE_PERCENTAGE:
values.addProperty("percentage_" + channelNumber, (double) (startPoint + sampleOffset) / 100f);
@ -357,25 +382,25 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
values.addProperty("current_" + channelNumber, (double) (startPoint + sampleOffset) / 100f);
break;
case MEASUREMENT_TYPE_PULSE_CNT_ACC_MINOR:
values.addProperty("pulse_cnt_minor_" + channelNumber, (double) (startPoint + sampleOffset));
calculateAccPulseCounterTotalValue(values, PULSE_CNT_ACC , channelNumber, startPoint + sampleOffset, PULSE_CNT_ACC_MINOR_METADATA_FACTOR);
break;
case MEASUREMENT_TYPE_PULSE_CNT_ACC_MAJOR:
values.addProperty("pulse_cnt_major_" + channelNumber, (double) (startPoint + sampleOffset));
addPropertiesForMeasurementTypeWithMetadataFactor(values, PULSE_CNT_ACC.getPrefix(), channelNumber, startPoint + sampleOffset, PULSE_CNT_ACC_MAJOR_METADATA_FACTOR);
break;
case MEASUREMENT_TYPE_ELEC_METER_ACC_MINOR:
values.addProperty("elec_meter_minor_" + channelNumber, (double) (startPoint + sampleOffset));
calculateAccPulseCounterTotalValue(values, ELEC_METER_ACC , channelNumber, startPoint + sampleOffset, ELEC_METER_ACC_MINOR_METADATA_FACTOR);
break;
case MEASUREMENT_TYPE_ELEC_METER_ACC_MAJOR:
values.addProperty("elec_meter_major_" + channelNumber, (double) (startPoint + sampleOffset));
addPropertiesForMeasurementTypeWithMetadataFactor(values, ELEC_METER_ACC.getPrefix(), channelNumber, startPoint + sampleOffset, ELEC_METER_ACC_MAJOR_METADATA_FACTOR);
break;
case MEASUREMENT_TYPE_PULSE_CNT_ACC_WIDE_MINOR:
values.addProperty("pulse_cnt_wide_minor_" + channelNumber, (double) (startPoint + sampleOffset));
calculateAccPulseCounterTotalValue(values, PULSE_CNT_ACC_WIDE , channelNumber, startPoint + sampleOffset, PULSE_CNT_ACC_WIDE_MINOR_METADATA_FACTOR);
break;
case MEASUREMENT_TYPE_PULSE_CNT_ACC_WIDE_MAJOR:
values.addProperty("pulse_cnt_wide_major_" + channelNumber, (double) (startPoint + sampleOffset));
addPropertiesForMeasurementTypeWithMetadataFactor(values, PULSE_CNT_ACC_WIDE.getPrefix(), channelNumber, startPoint + sampleOffset, PULSE_CNT_ACC_WIDE_MAJOR_METADATA_FACTOR);
break;
case MEASUREMENT_TYPE_CURRENT_PRECISE:
values.addProperty("current_precise_" + channelNumber, (double) (startPoint + sampleOffset)/1000f);
values.addProperty("current_precise_" + channelNumber, (double) (startPoint + sampleOffset) / 1000f);
break;
case MEASUREMENT_TYPE_NO_SENSOR:
case UNRECOGNIZED:
@ -387,6 +412,23 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
}
}
private void addPropertiesForMeasurementTypeWithMetadataFactor(JsonObject values, String prefix, int channelNumber, int value, int metadataFactor) {
values.addProperty(prefix + channelNumber, value / metadataFactor);
values.addProperty(prefix + "metadata_" + channelNumber, value % metadataFactor);
}
private void calculateAccPulseCounterTotalValue(JsonObject values, PulseCounterType pulseCounterType, int channelNumber, int value, int metadataFactor) {
int minorValue = value / metadataFactor;
int majorChannel = value % metadataFactor + 1;
String majorPropertyKey = pulseCounterType.getPrefix() + majorChannel;
JsonElement majorProperty = values.get(majorPropertyKey);
if (majorProperty != null) {
int totalValue = majorProperty.getAsInt() * pulseCounterType.getMajorResolution() + minorValue;
values.addProperty(pulseCounterType.getPrefix() + "total_" + channelNumber, totalValue);
values.remove(majorPropertyKey);
}
}
private void addBinarySample(ProtoChannel protoChannel, boolean valueIsOk, JsonObject values, int channel, UUID sessionId) {
switch (protoChannel.getType()) {
case MEASUREMENT_TYPE_OK_ALARM:

15
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/utils/CoapEfentoUtils.java

@ -28,6 +28,21 @@ import static org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos.Me
public class CoapEfentoUtils {
public static final int PULSE_CNT_ACC_MINOR_METADATA_FACTOR = 6;
public static final int PULSE_CNT_ACC_MAJOR_METADATA_FACTOR = 4;
public static final int ELEC_METER_ACC_MINOR_METADATA_FACTOR = 6;
public static final int ELEC_METER_ACC_MAJOR_METADATA_FACTOR = 4;
public static final int PULSE_CNT_ACC_WIDE_MINOR_METADATA_FACTOR = 6;
public static final int PULSE_CNT_ACC_WIDE_MAJOR_METADATA_FACTOR = 4;
public static final int WATER_METER_ACC_MINOR_METADATA_FACTOR = 6;
public static final int WATER_METER_ACC_MAJOR_METADATA_FACTOR = 4;
public static final int IAQ_METADATA_FACTOR = 3;
public static final int STATIC_IAQ_METADATA_FACTOR = 3;
public static final int CO2_GAS_METADATA_FACTOR = 3;
public static final int CO2_EQUIVALENT_METADATA_FACTOR = 3;
public static final int BREATH_VOC_METADATA_FACTOR = 3;
public static String convertByteArrayToString(byte[] a) {
StringBuilder out = new StringBuilder();
for (byte b : a) {

40
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/utils/PulseCounterType.java

@ -0,0 +1,40 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.coap.efento.utils;
public enum PulseCounterType {
WATER_CNT_ACC("water_cnt_acc_", 100),
PULSE_CNT_ACC("pulse_cnt_acc_", 1000),
ELEC_METER_ACC("elec_meter_acc_", 1000),
PULSE_CNT_ACC_WIDE("pulse_cnt_acc_wide_", 1000000);
private final String prefix;
private final int majorResolution;
PulseCounterType(String prefix, int majorResolution) {
this.prefix = prefix;
this.majorResolution = majorResolution;
}
public String getPrefix() {
return prefix;
}
public int getMajorResolution() {
return majorResolution;
}
}

69
common/transport/coap/src/test/java/org/thingsboard/server/transport/coap/efento/CoapEfentTransportResourceTest.java → common/transport/coap/src/test/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResourceTest.java

@ -29,6 +29,7 @@ import org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils;
import java.nio.ByteBuffer;
import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@ -70,7 +71,7 @@ import static org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos.Me
import static org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos.MeasurementType.MEASUREMENT_TYPE_WATER_METER_ACC_MINOR;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.convertTimestampToUtcString;
class CoapEfentTransportResourceTest {
class CoapEfentoTransportResourceTest {
private static CoapEfentoTransportResource coapEfentoTransportResource;
@ -152,31 +153,69 @@ class CoapEfentTransportResourceTest {
Arguments.of(MEASUREMENT_TYPE_ATMOSPHERIC_PRESSURE, List.of(1013), "pressure_1", 101.3),
Arguments.of(MEASUREMENT_TYPE_DIFFERENTIAL_PRESSURE, List.of(500), "pressure_diff_1", 500),
Arguments.of(MEASUREMENT_TYPE_PULSE_CNT, List.of(300), "pulse_cnt_1", 300),
Arguments.of(MEASUREMENT_TYPE_IAQ, List.of(150), "iaq_1", 150),
Arguments.of(MEASUREMENT_TYPE_IAQ, List.of(150), "iaq_1", 50.0),
Arguments.of(MEASUREMENT_TYPE_ELECTRICITY_METER, List.of(1200), "watt_hour_1", 1200),
Arguments.of(MEASUREMENT_TYPE_SOIL_MOISTURE, List.of(35), "soil_moisture_1", 35),
Arguments.of(MEASUREMENT_TYPE_AMBIENT_LIGHT, List.of(500), "ambient_light_1", 50),
Arguments.of(MEASUREMENT_TYPE_HIGH_PRESSURE, List.of(200000), "high_pressure_1", 200000),
Arguments.of(MEASUREMENT_TYPE_DISTANCE_MM, List.of(1500), "distance_mm_1", 1500),
Arguments.of(MEASUREMENT_TYPE_WATER_METER_ACC_MINOR, List.of(125), "acc_counter_water_minor_1", 125),
Arguments.of(MEASUREMENT_TYPE_WATER_METER_ACC_MAJOR, List.of(2500), "acc_counter_water_major_1", 2500),
Arguments.of(MEASUREMENT_TYPE_HUMIDITY_ACCURATE, List.of(525), "humidity_relative_1", 52.5),
Arguments.of(MEASUREMENT_TYPE_STATIC_IAQ, List.of(110), "static_iaq_1", 110),
Arguments.of(MEASUREMENT_TYPE_CO2_EQUIVALENT, List.of(450), "co2_ppm_1", 450),
Arguments.of(MEASUREMENT_TYPE_BREATH_VOC, List.of(220), "breath_voc_ppm_1", 220),
Arguments.of(MEASUREMENT_TYPE_PERCENTAGE, List.of(80), "percentage_1", 0.80),
Arguments.of(MEASUREMENT_TYPE_VOLTAGE, List.of(2400), "voltage_1", 240),
Arguments.of(MEASUREMENT_TYPE_STATIC_IAQ, List.of(110), "static_iaq_1", 36),
Arguments.of(MEASUREMENT_TYPE_CO2_EQUIVALENT, List.of(450), "co2_1", 150),
Arguments.of(MEASUREMENT_TYPE_BREATH_VOC, List.of(220), "breath_voc_1", 73),
Arguments.of(MEASUREMENT_TYPE_PERCENTAGE, List.of(80), "percentage_1", 0.8),
Arguments.of(MEASUREMENT_TYPE_VOLTAGE, List.of(2400), "voltage_1", 240.0),
Arguments.of(MEASUREMENT_TYPE_CURRENT, List.of(550), "current_1", 5.5),
Arguments.of(MEASUREMENT_TYPE_PULSE_CNT_ACC_MINOR, List.of(180), "pulse_cnt_minor_1", 180),
Arguments.of(MEASUREMENT_TYPE_PULSE_CNT_ACC_MAJOR, List.of(1200), "pulse_cnt_major_1", 1200),
Arguments.of(MEASUREMENT_TYPE_ELEC_METER_ACC_MINOR, List.of(550), "elec_meter_minor_1", 550),
Arguments.of(MEASUREMENT_TYPE_ELEC_METER_ACC_MAJOR, List.of(5500), "elec_meter_major_1", 5500),
Arguments.of(MEASUREMENT_TYPE_PULSE_CNT_ACC_WIDE_MINOR, List.of(230), "pulse_cnt_wide_minor_1", 230),
Arguments.of(MEASUREMENT_TYPE_PULSE_CNT_ACC_WIDE_MAJOR, List.of(1700), "pulse_cnt_wide_major_1", 1700),
Arguments.of(MEASUREMENT_TYPE_CURRENT_PRECISE, List.of(275), "current_precise_1", 0.275)
);
}
@ParameterizedTest
@MethodSource
void checkPulseCounterSensors(MeasurementType minorType, List<Integer> minorSampleOffsets, MeasurementType majorType, List<Integer> majorSampleOffsets,
String totalPropertyName, double expectedTotalValue) {
long tsInSec = Instant.now().getEpochSecond();
ProtoMeasurements measurements = ProtoMeasurements.newBuilder()
.setSerialNum(integerToByteString(1234))
.setCloudToken("test_token")
.setMeasurementPeriodBase(180)
.setMeasurementPeriodFactor(0)
.setBatteryStatus(true)
.setSignal(0)
.setNextTransmissionAt(1000)
.setTransferReason(0)
.setHash(0)
.addAllChannels(Arrays.asList(MeasurementsProtos.ProtoChannel.newBuilder()
.setType(majorType)
.setTimestamp(Math.toIntExact(tsInSec))
.addAllSampleOffsets(majorSampleOffsets)
.build(),
MeasurementsProtos.ProtoChannel.newBuilder()
.setType(minorType)
.setTimestamp(Math.toIntExact(tsInSec))
.addAllSampleOffsets(minorSampleOffsets)
.build()))
.build();
List<CoapEfentoTransportResource.EfentoTelemetry> efentoMeasurements = coapEfentoTransportResource.getEfentoMeasurements(measurements, UUID.randomUUID());
assertThat(efentoMeasurements).hasSize(1);
assertThat(efentoMeasurements.get(0).getTs()).isEqualTo(tsInSec * 1000);
assertThat(efentoMeasurements.get(0).getValues().getAsJsonObject().get(totalPropertyName + "_2").getAsDouble()).isEqualTo(expectedTotalValue);
checkDefaultMeasurements(measurements, efentoMeasurements, 180, false);
}
private static Stream<Arguments> checkPulseCounterSensors() {
return Stream.of(
Arguments.of(MEASUREMENT_TYPE_WATER_METER_ACC_MINOR, List.of(15*6), MEASUREMENT_TYPE_WATER_METER_ACC_MAJOR,
List.of(625*4), "water_cnt_acc_total", 625.0*100 + 15),
Arguments.of(MEASUREMENT_TYPE_PULSE_CNT_ACC_MINOR, List.of(10*6), MEASUREMENT_TYPE_PULSE_CNT_ACC_MAJOR,
List.of(300*4), "pulse_cnt_acc_total", 300.0*1000 + 10),
Arguments.of(MEASUREMENT_TYPE_ELEC_METER_ACC_MINOR, List.of(12*6), MEASUREMENT_TYPE_ELEC_METER_ACC_MAJOR,
List.of(100*4), "elec_meter_acc_total", 100.0*1000 + 12),
Arguments.of(MEASUREMENT_TYPE_PULSE_CNT_ACC_WIDE_MINOR, List.of(13*6), MEASUREMENT_TYPE_PULSE_CNT_ACC_WIDE_MAJOR,
List.of(440*4), "pulse_cnt_acc_wide_total", 440.0*1000000 + 13));
}
@Test
void checkBinarySensor() {
long tsInSec = Instant.now().getEpochSecond();

2
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapSecurityStore.java

@ -133,7 +133,7 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore {
log.error(" [{}] Different values SecurityMode between of client and profile.", store.getEndpoint());
log.error("{} getParametersBootstrap: [{}] Different values SecurityMode between of client and profile.", LOG_LWM2M_ERROR, store.getEndpoint());
String logMsg = String.format("%s: Different values SecurityMode between of client and profile.", LOG_LWM2M_ERROR);
helper.sendParametersOnThingsboardTelemetry(helper.getKvStringtoThingsboard(LOG_LWM2M_TELEMETRY, logMsg), sessionInfo);
helper.sendParametersOnThingsboardTelemetry(helper.getKvStringtoThingsboard(LOG_LWM2M_TELEMETRY, logMsg), sessionInfo, null);
return null;
}
}

2
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LwM2mCredentialsSecurityInfoValidator.java

@ -18,7 +18,7 @@ package org.thingsboard.server.transport.lwm2m.secure;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.DecoderException;
import org.eclipse.leshan.core.util.SecurityUtil;
import org.eclipse.leshan.core.security.util.SecurityUtil;
import org.eclipse.leshan.server.security.SecurityInfo;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;

13
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java

@ -36,6 +36,7 @@ import org.thingsboard.server.queue.util.TbLwM2mTransportComponent;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@ -58,12 +59,12 @@ public class LwM2mTransportServerHelper {
context.getTransportService().process(sessionInfo, postAttributeMsg, TransportServiceCallback.EMPTY);
}
public void sendParametersOnThingsboardTelemetry(List<TransportProtos.KeyValueProto> kvList, SessionInfoProto sessionInfo) {
sendParametersOnThingsboardTelemetry(kvList, sessionInfo, null);
public void sendParametersOnThingsboardTelemetry(List<TransportProtos.KeyValueProto> kvList, SessionInfoProto sessionInfo, @Nullable Map<String, AtomicLong> keyTsLatestMaps){
sendParametersOnThingsboardTelemetry(kvList, sessionInfo, keyTsLatestMaps, null);
}
public void sendParametersOnThingsboardTelemetry(List<TransportProtos.KeyValueProto> kvList, SessionInfoProto sessionInfo, @Nullable Map<String, AtomicLong> keyTsLatestMap) {
TransportProtos.TsKvListProto tsKvList = toTsKvList(kvList, keyTsLatestMap);
public void sendParametersOnThingsboardTelemetry(List<TransportProtos.KeyValueProto> kvList, SessionInfoProto sessionInfo, @Nullable Map<String, AtomicLong> keyTsLatestMap, @Nullable Instant ts) {
TransportProtos.TsKvListProto tsKvList = toTsKvList(kvList, keyTsLatestMap, ts);
PostTelemetryMsg postTelemetryMsg = PostTelemetryMsg.newBuilder()
.addTsKvList(tsKvList)
@ -72,9 +73,9 @@ public class LwM2mTransportServerHelper {
context.getTransportService().process(sessionInfo, postTelemetryMsg, TransportServiceCallback.EMPTY);
}
TransportProtos.TsKvListProto toTsKvList(List<TransportProtos.KeyValueProto> kvList, Map<String, AtomicLong> keyTsLatestMap) {
TransportProtos.TsKvListProto toTsKvList(List<TransportProtos.KeyValueProto> kvList, Map<String, AtomicLong> keyTsLatestMap, @Nullable Instant ts) {
return TransportProtos.TsKvListProto.newBuilder()
.setTs(getTs(kvList, keyTsLatestMap))
.setTs(ts == null ? getTs(kvList, keyTsLatestMap) : ts.toEpochMilli())
.addAllKv(kvList)
.build();
}

2
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java

@ -449,7 +449,7 @@ public class LwM2mClient {
}
public LwM2m.Version getSupportedObjectVersion(Integer objectid) {
return this.supportedClientObjects.get(objectid);
return this.supportedClientObjects != null ? this.supportedClientObjects.get(objectid) : null;
}
private void setSupportedClientObjects(){

21
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java

@ -42,7 +42,6 @@ import org.eclipse.leshan.core.observation.Observation;
import org.eclipse.leshan.core.request.CreateRequest;
import org.eclipse.leshan.core.request.ObserveRequest;
import org.eclipse.leshan.core.request.ReadRequest;
import org.eclipse.leshan.core.request.SendRequest;
import org.eclipse.leshan.core.request.WriteCompositeRequest;
import org.eclipse.leshan.core.request.WriteRequest;
import org.eclipse.leshan.core.request.WriteRequest.Mode;
@ -117,6 +116,7 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH;
@ -382,7 +382,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
this.updateObjectInstanceResourceValue(lwM2MClient, lwM2mObjectInstance, path.toString(), 0);
} else if (node instanceof LwM2mResource) {
LwM2mResource lwM2mResource = (LwM2mResource) node;
this.updateResourcesValue(lwM2MClient, lwM2mResource, path.toString(), Mode.UPDATE, 0);
this.updateResourcesValueWithTs(lwM2MClient, lwM2mResource, path.toString(), Mode.UPDATE, ts);
}
}
tryAwake(lwM2MClient);
@ -612,12 +612,21 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
otaService.onCurrentSoftwareResultUpdate(lwM2MClient, (Long) lwM2mResource.getValue());
}
if (ResponseCode.BAD_REQUEST.getCode() > code) {
this.updateAttrTelemetry(registration, path);
this.updateAttrTelemetry(registration, path, null);
}
} else {
log.error("Fail update path [{}] Resource [{}]", path, lwM2mResource);
}
}
private void updateResourcesValueWithTs(LwM2mClient lwM2MClient, LwM2mResource lwM2mResource, String stringPath, Mode mode, Instant ts) {
Registration registration = lwM2MClient.getRegistration();
String path = convertObjectIdToVersionedId(stringPath, lwM2MClient);
if (lwM2MClient.saveResourceValue(path, lwM2mResource, modelProvider, mode)) {
this.updateAttrTelemetry(registration, path, ts);
} else {
log.error("Fail update path [{}] Resource [{}] with ts.", path, lwM2mResource);
}
}
/**
@ -629,7 +638,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
*
* @param registration - Registration LwM2M Client
*/
public void updateAttrTelemetry(Registration registration, String path) {
public void updateAttrTelemetry(Registration registration, String path, Instant ts) {
log.trace("UpdateAttrTelemetry paths [{}]", path);
try {
ResultsAddKeyValueProto results = this.getParametersFromProfile(registration, path);
@ -640,8 +649,8 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
this.helper.sendParametersOnThingsboardAttribute(results.getResultAttributes(), sessionInfo);
}
if (results.getResultTelemetries().size() > 0) {
log.trace("UpdateTelemetry paths [{}] value [{}]", path, results.getResultTelemetries().get(0).toString());
this.helper.sendParametersOnThingsboardTelemetry(results.getResultTelemetries(), sessionInfo);
log.trace("UpdateTelemetry paths [{}] value [{}] ts [{}]", path, results.getResultTelemetries().get(0).toString(), ts == null ? "null" : ts.toEpochMilli());
this.helper.sendParametersOnThingsboardTelemetry(results.getResultTelemetries(), sessionInfo, null, ts);
}
}
} catch (Exception e) {

5
common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java

@ -223,7 +223,7 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe
var currentSettings = vcService.getRepositorySettings(ctx.getTenantId());
var newSettings = ctx.getSettings();
if (!newSettings.equals(currentSettings)) {
vcService.initRepository(ctx.getTenantId(), ctx.getSettings());
vcService.initRepository(ctx.getTenantId(), ctx.getSettings(), false);
}
if (msg.hasCommitRequest()) {
handleCommitRequest(ctx, msg.getCommitRequest());
@ -464,7 +464,7 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe
private void handleInitRepositoryCommand(VersionControlRequestCtx ctx) {
try {
vcService.initRepository(ctx.getTenantId(), ctx.getSettings());
vcService.initRepository(ctx.getTenantId(), ctx.getSettings(), false);
reply(ctx, Optional.empty());
} catch (Exception e) {
log.debug("[{}] Failed to connect to the repository: ", ctx, e);
@ -564,4 +564,5 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe
}, MoreExecutors.directExecutor());
}
}
}

16
common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitRepositoryService.java

@ -203,9 +203,9 @@ public class DefaultGitRepositoryService implements GitRepositoryService {
GitRepository gitRepository = Optional.ofNullable(repositories.get(tenantId))
.orElseThrow(() -> new IllegalStateException("Repository is not initialized"));
if (!Files.exists(Path.of(gitRepository.getDirectory()))) {
if (!GitRepository.exists(gitRepository.getDirectory())) {
try {
return cloneRepository(tenantId, gitRepository.getSettings());
return openOrCloneRepository(tenantId, gitRepository.getSettings(), false);
} catch (Exception e) {
throw new IllegalStateException("Could not initialize the repository: " + e.getMessage(), e);
}
@ -239,11 +239,11 @@ public class DefaultGitRepositoryService implements GitRepositoryService {
}
@Override
public void initRepository(TenantId tenantId, RepositorySettings settings) throws Exception {
public void initRepository(TenantId tenantId, RepositorySettings settings, boolean fetch) throws Exception {
if (!settings.isLocalOnly()) {
clearRepository(tenantId);
}
cloneRepository(tenantId, settings);
openOrCloneRepository(tenantId, settings, fetch);
}
@Override
@ -280,14 +280,18 @@ public class DefaultGitRepositoryService implements GitRepositoryService {
return EntityIdFactory.getByTypeAndUuid(entityType, entityId);
}
private GitRepository cloneRepository(TenantId tenantId, RepositorySettings settings) throws Exception {
private GitRepository openOrCloneRepository(TenantId tenantId, RepositorySettings settings, boolean fetch) throws Exception {
log.debug("[{}] Init tenant repository started.", tenantId);
Path repositoryDirectory = Path.of(repositoriesFolder, settings.isLocalOnly() ? "local_" + settings.getRepositoryUri() : tenantId.getId().toString());
GitRepository repository;
if (Files.exists(repositoryDirectory)) {
if (GitRepository.exists(repositoryDirectory.toString())) {
repository = GitRepository.open(repositoryDirectory.toFile(), settings);
if (fetch) {
repository.fetch();
}
} else {
FileUtils.deleteDirectory(repositoryDirectory.toFile());
Files.createDirectories(repositoryDirectory);
if (settings.isLocalOnly()) {
repository = GitRepository.create(settings, repositoryDirectory.toFile());

8
common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java

@ -21,6 +21,7 @@ import com.google.common.collect.Streams;
import lombok.Data;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
@ -75,6 +76,7 @@ import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyPair;
import java.security.PublicKey;
import java.util.ArrayList;
@ -409,6 +411,12 @@ public class GitRepository {
return result;
}
@SneakyThrows
public static boolean exists(String directory) {
File gitDirectory = Path.of(directory, ".git").toFile();
return FileUtils.isDirectory(gitDirectory) && !FileUtils.isEmptyDirectory(gitDirectory);
}
private <C extends GitCommand<T>, T> T execute(C command) throws GitAPIException {
if (command instanceof TransportCommand transportCommand && authHandler != null) {
authHandler.configureCommand(transportCommand);

3
common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepositoryService.java

@ -42,7 +42,7 @@ public interface GitRepositoryService {
void testRepository(TenantId tenantId, RepositorySettings settings) throws Exception;
void initRepository(TenantId tenantId, RepositorySettings settings) throws Exception;
void initRepository(TenantId tenantId, RepositorySettings settings, boolean fetch) throws Exception;
RepositorySettings getRepositorySettings(TenantId tenantId) throws Exception;
@ -67,4 +67,5 @@ public interface GitRepositoryService {
String getContentsDiff(TenantId tenantId, String content1, String content2) throws IOException;
void fetch(TenantId tenantId) throws GitAPIException;
}

8
dao/src/main/java/org/thingsboard/server/dao/AbstractVersionedInsertRepository.java

@ -47,8 +47,7 @@ public abstract class AbstractVersionedInsertRepository<T> extends AbstractInser
List<Integer> toInsertIndexes = new ArrayList<>(notUpdatedCount);
List<T> insertEntities = new ArrayList<>(notUpdatedCount);
int keyHolderIndex = 0;
for (int i = 0; i < updateResult.length; i++) {
for (int i = 0, keyHolderIndex = 0; i < updateResult.length; i++) {
if (updateResult[i] == 0) {
insertEntities.add(entities.get(i));
seqNumbers.add(null);
@ -67,9 +66,10 @@ public abstract class AbstractVersionedInsertRepository<T> extends AbstractInser
seqNumbersList = keyHolder.getKeyList();
for (int i = 0; i < insertResult.length; i++) {
for (int i = 0, keyHolderIndex = 0; i < insertResult.length; i++) {
if (insertResult[i] != 0) {
seqNumbers.set(toInsertIndexes.get(i), (Long) seqNumbersList.get(i).get(VERSION_COLUMN));
seqNumbers.set(toInsertIndexes.get(i), (Long) seqNumbersList.get(keyHolderIndex).get(VERSION_COLUMN));
keyHolderIndex++;
}
}

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

@ -19,7 +19,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.SecurityMode;
import org.eclipse.leshan.core.util.SecurityUtil;
import org.eclipse.leshan.core.security.util.SecurityUtil;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.event.TransactionalEventListener;

2
dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java

@ -18,7 +18,7 @@ package org.thingsboard.server.dao.service.validator;
import com.google.protobuf.Descriptors;
import com.google.protobuf.DynamicMessage;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.util.SecurityUtil;
import org.eclipse.leshan.core.security.util.SecurityUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;

115
dao/src/test/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDaoTest.java

@ -0,0 +1,115 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.sqlts;
import com.google.common.util.concurrent.ListenableFuture;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.dao.service.AbstractServiceTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.timeseries.TimeseriesLatestDao;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@DaoSqlTest
public class SqlTimeseriesLatestDaoTest extends AbstractServiceTest {
@Autowired
private TimeseriesLatestDao timeseriesLatestDao;
@Test
public void saveLatestTest() throws Exception {
DeviceId deviceId = new DeviceId(UUID.randomUUID());
var entry = createEntry("key", 1000);
Long version = timeseriesLatestDao.saveLatest(tenantId, deviceId, entry).get();
assertNotNull(version);
assertTrue(version > 0);
TsKvEntry foundEntry = timeseriesLatestDao.findLatest(tenantId, deviceId, "key").get();
assertNotNull(foundEntry);
equalsIgnoreVersion(entry, foundEntry);
assertEquals(version, foundEntry.getVersion());
var updatedEntry = createEntry("key", 2000);
Long updatedVersion = timeseriesLatestDao.saveLatest(tenantId, deviceId, updatedEntry).get();
assertNotNull(updatedVersion);
assertTrue(updatedVersion > version);
foundEntry = timeseriesLatestDao.findLatest(tenantId, deviceId, "key").get();
assertNotNull(foundEntry);
equalsIgnoreVersion(updatedEntry, foundEntry);
assertEquals(updatedVersion, foundEntry.getVersion());
var oldEntry = createEntry("key", 1);
Long oldVersion = timeseriesLatestDao.saveLatest(tenantId, deviceId, oldEntry).get();
assertNull(oldVersion);
foundEntry = timeseriesLatestDao.findLatest(tenantId, deviceId, "key").get();
assertNotNull(foundEntry);
equalsIgnoreVersion(updatedEntry, foundEntry);
assertEquals(updatedVersion, foundEntry.getVersion());
}
@Test
public void updateWithOldTsTest() throws Exception {
DeviceId deviceId = new DeviceId(UUID.randomUUID());
int n = 50;
for (int i = 0; i < n; i++) {
timeseriesLatestDao.saveLatest(tenantId, deviceId, createEntry("key_" + i, System.currentTimeMillis()));
}
List<ListenableFuture<Long>> futures = new ArrayList<>(n);
for (int i = 0; i < n; i++) {
long ts = i % 2 == 0 ? System.currentTimeMillis() : 1000;
futures.add(timeseriesLatestDao.saveLatest(tenantId, deviceId, createEntry("key_" + i, ts)));
}
for (int i = 0; i < futures.size(); i++) {
Long version = futures.get(i).get();
if (i % 2 == 0) {
assertNotNull(version);
assertTrue(version > 0);
} else {
assertNull(version);
}
}
}
private TsKvEntry createEntry(String key, long ts) {
return new BasicTsKvEntry(ts, new StringDataEntry(key, RandomStringUtils.random(10)));
}
private void equalsIgnoreVersion(TsKvEntry expected, TsKvEntry actual) {
Assert.assertEquals(expected.getKey(), actual.getKey());
Assert.assertEquals(expected.getValue(), actual.getValue());
Assert.assertEquals(expected.getTs(), actual.getTs());
}
}

24
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java

@ -49,6 +49,7 @@ 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.EntityViewId;
import org.thingsboard.server.common.data.id.RpcId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
@ -57,6 +58,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.rpc.Rpc;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.security.DeviceCredentials;
@ -64,6 +66,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static io.restassured.RestAssured.given;
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
@ -320,6 +323,27 @@ public class TestRestClient {
.as(JsonNode.class);
}
public Rpc getPersistedRpc(RpcId rpcId) {
return given().spec(requestSpec)
.get("/api/rpc/persistent/{rpcId}", rpcId.toString())
.then()
.statusCode(HTTP_OK)
.extract()
.as(Rpc.class);
}
public PageData<Rpc> getPersistedRpcByDevice(DeviceId deviceId, PageLink pageLink) {
Map<String, String> params = new HashMap<>();
addPageLinkToParam(params, pageLink);
return given().spec(requestSpec).queryParams(params)
.get("/api/rpc/persistent/device/{deviceId}", deviceId.toString())
.then()
.statusCode(HTTP_OK)
.extract()
.as(new TypeRef<>() {
});
}
public PageData<DeviceProfile> getDeviceProfiles(PageLink pageLink) {
Map<String, String> params = new HashMap<>();
addPageLinkToParam(params, pageLink);

58
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java

@ -46,9 +46,11 @@ 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.StringUtils;
import org.thingsboard.server.common.data.id.RpcId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rpc.Rpc;
import org.thingsboard.server.common.data.rule.NodeConnectionInfo;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
@ -68,6 +70,7 @@ import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutionException;
@ -76,6 +79,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.fail;
import static org.thingsboard.server.common.data.DataConstants.DEVICE;
import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE;
@ -307,6 +311,60 @@ public class MqttClientTest extends AbstractContainerTest {
assertThat(serverResponse).isEqualTo(mapper.readTree(clientResponse.toString()));
}
@Test
public void serverSidePersistedRpc() throws Exception {
DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId());
MqttMessageListener listener = new MqttMessageListener();
MqttClient mqttClient = getMqttClient(deviceCredentials, listener);
mqttClient.on("v1/devices/me/rpc/request/+", listener, MqttQoS.AT_LEAST_ONCE).get();
// Wait until subscription is processed
TimeUnit.SECONDS.sleep(3 * timeoutMultiplier);
// Send an RPC from the server
JsonObject serverRpcPayload = new JsonObject();
serverRpcPayload.addProperty("method", "getValue");
serverRpcPayload.addProperty("params", true);
serverRpcPayload.addProperty("persistent", true);
JsonNode persistentRpcId = testRestClient.postServerSideRpc(device.getId(), mapper.readTree(serverRpcPayload.toString()));
assertNotNull(persistentRpcId);
RpcId rpcId = new RpcId(UUID.fromString(persistentRpcId.get("rpcId").asText()));
// Wait for RPC call from the server and send the response
MqttEvent requestFromServer = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS);
assertThat(Objects.requireNonNull(requestFromServer).getMessage()).isEqualTo("{\"method\":\"getValue\",\"params\":true}");
Integer requestId = Integer.valueOf(Objects.requireNonNull(requestFromServer).getTopic().substring("v1/devices/me/rpc/request/".length()));
JsonObject clientResponse = new JsonObject();
clientResponse.addProperty("response", "someResponse");
// Send a response to the server's RPC request
mqttClient.publish("v1/devices/me/rpc/response/" + requestId, Unpooled.wrappedBuffer(clientResponse.toString().getBytes())).get();
PageLink pageLink = new PageLink(10);
Awaitility.await()
.pollInterval(500, TimeUnit.MILLISECONDS)
.atMost(5 * timeoutMultiplier, TimeUnit.SECONDS)
.until(() -> {
PageData<Rpc> rpcByDevice = testRestClient.getPersistedRpcByDevice(device.getId(), pageLink);
for (Rpc rpc : rpcByDevice.getData()) {
if (rpc.getId().equals(rpcId)) {
return true;
}
}
return false;
});
Rpc persistentRpc = testRestClient.getPersistedRpc(rpcId);
assertThat(persistentRpc.getResponse()).isEqualTo(mapper.readTree(clientResponse.toString()));
}
@Test
public void clientSideRpc() throws Exception {
DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId());

6
pom.xml

@ -74,8 +74,8 @@
<fasterxml-classmate.version>1.7.0</fasterxml-classmate.version>
<auth0-jwt.version>4.4.0</auth0-jwt.version>
<json-schema-validator.version>2.2.14</json-schema-validator.version>
<californium.version>3.11.0</californium.version>
<leshan.version>2.0.0-M14</leshan.version>
<californium.version>3.12.1</californium.version>
<leshan.version>2.0.0-M15</leshan.version>
<gson.version>2.10.1</gson.version>
<freemarker.version>2.3.32</freemarker.version>
<mail.version>2.0.1</mail.version>
@ -83,7 +83,7 @@
<zookeeper.version>3.9.2</zookeeper.version>
<protobuf.version>3.25.3</protobuf.version> <!-- A Major v4 does not support by the pubsub yet-->
<grpc.version>1.63.0</grpc.version>
<tbel.version>1.2.3</tbel.version>
<tbel.version>1.2.4</tbel.version>
<lombok.version>1.18.32</lombok.version>
<paho.client.version>1.2.5</paho.client.version>
<paho.mqttv5.client.version>1.2.5</paho.mqttv5.client.version>

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/credentials/CertPemCredentials.java

@ -47,7 +47,7 @@ public class CertPemCredentials implements ClientCredentials {
protected String caCert;
private String cert;
private String privateKey;
private String password = "";
private String password;
@Override
public CredentialsType getType() {

26
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js

File diff suppressed because one or more lines are too long

7
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/credentials/CertPemCredentialsTest.java

@ -38,7 +38,6 @@ import static org.thingsboard.rule.engine.credentials.CertPemCredentials.PRIVATE
public class CertPemCredentialsTest {
private static final String PASS = "test";
private static final String EMPTY_PASS = "";
private static final String RSA = "RSA";
private static final String EC = "EC";
@ -81,10 +80,10 @@ public class CertPemCredentialsTest {
private static Stream<Arguments> testLoadKeyStore() {
return Stream.of(
Arguments.of("pem/rsa_cert.pem", "pem/rsa_key.pem", EMPTY_PASS, RSA),
Arguments.of("pem/rsa_cert.pem", "pem/rsa_key.pem", null, RSA),
Arguments.of("pem/rsa_encrypted_cert.pem", "pem/rsa_encrypted_key.pem", PASS, RSA),
Arguments.of("pem/rsa_encrypted_traditional_cert.pem", "pem/rsa_encrypted_traditional_key.pem", PASS, RSA),
Arguments.of("pem/ec_cert.pem", "pem/ec_key.pem", EMPTY_PASS, EC)
Arguments.of("pem/ec_cert.pem", "pem/ec_key.pem", null, EC)
);
}
@ -98,7 +97,7 @@ public class CertPemCredentialsTest {
certPemCredentials.setPassword(password);
KeyStore keyStore = certPemCredentials.loadKeyStore();
Assertions.assertNotNull(keyStore);
Key key = keyStore.getKey(PRIVATE_KEY_ALIAS, password.toCharArray());
Key key = keyStore.getKey(PRIVATE_KEY_ALIAS, SslUtil.getPassword(password));
Assertions.assertNotNull(key);
Assertions.assertEquals(algorithm, key.getAlgorithm());

133
ui-ngx/.eslintrc.json

@ -1,68 +1,69 @@
{
"root": true,
"ignorePatterns": [
"projects/**/*"
],
"overrides": [
{
"files": [
"*.ts"
],
"parserOptions": {
"project": [
"tsconfig.json",
"e2e/tsconfig.json"
],
"createDefaultProgram": true
},
"extends": [
"plugin:@angular-eslint/ng-cli-compat",
"plugin:@angular-eslint/ng-cli-compat--formatting-add-on",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@typescript-eslint/explicit-member-accessibility": [
"off",
{
"accessibility": "explicit"
}
],
"arrow-parens": [
"off",
"always"
],
"@angular-eslint/component-selector": [
"error",
{
"prefix": [ "tb" ]
}
],
"id-blacklist": [
"error",
"any",
"Number",
"String",
"string",
"Boolean",
"boolean",
"Undefined",
"undefined"
],
"import/order": "off",
"@typescript-eslint/member-ordering": "off",
"no-underscore-dangle": "off",
"@typescript-eslint/naming-convention": "off",
"jsdoc/newline-after-description": 0
}
},
{
"files": [
"*.html"
],
"extends": [
"plugin:@angular-eslint/template/recommended"
],
"rules": {}
}
]
"root": true,
"ignorePatterns": [
"projects/**/*"
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx"
],
"parserOptions": {
"project": [
"tsconfig.json"
],
"createDefaultProgram": true
},
"extends": [
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@typescript-eslint/explicit-member-accessibility": [
"off",
{
"accessibility": "explicit"
}
],
"arrow-parens": [
"off",
"always"
],
"@angular-eslint/component-selector": [
"error",
{
"prefix": [
"tb"
]
}
],
"id-blacklist": [
"error",
"any",
"Number",
"String",
"string",
"Boolean",
"boolean",
"Undefined",
"undefined"
],
"import/order": "off",
"@typescript-eslint/member-ordering": "off",
"no-underscore-dangle": "off",
"@typescript-eslint/naming-convention": "off",
"jsdoc/newline-after-description": 0
}
},
{
"files": [
"*.html"
],
"extends": [
"plugin:@angular-eslint/template/recommended"
],
"rules": {}
}
]
}

44
ui-ngx/angular.json

@ -217,15 +217,15 @@
"serve": {
"builder": "@angular-builders/custom-webpack:dev-server",
"options": {
"browserTarget": "thingsboard:build",
"buildTarget": "thingsboard:build",
"proxyConfig": "proxy.conf.js"
},
"configurations": {
"production": {
"browserTarget": "thingsboard:build:production"
"buildTarget": "thingsboard:build:production"
},
"development": {
"browserTarget": "thingsboard:build:development"
"buildTarget": "thingsboard:build:development"
}
},
"defaultConfiguration": "development"
@ -233,24 +233,7 @@
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "thingsboard:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.spec.json",
"karmaConfig": "src/karma.conf.js",
"styles": [
"src/styles.scss"
],
"scripts": [],
"assets": [
"src/favicon.ico",
"src/assets"
]
"buildTarget": "thingsboard:build"
}
},
"lint": {
@ -263,25 +246,6 @@
}
}
}
},
"thingsboard-e2e": {
"root": "e2e/",
"projectType": "application",
"prefix": "",
"architect": {
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "thingsboard:serve"
},
"configurations": {
"production": {
"devServerTarget": "thingsboard:serve:production"
}
}
}
}
}
},
"cli": {

43
ui-ngx/e2e/protractor.conf.js

@ -1,43 +0,0 @@
/*
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter } = require("jasmine-spec-reporter");
exports.config = {
allScriptsTimeout: 11000,
specs: [
"./src/**/*.e2e-spec.ts",
],
capabilities: {
"browserName": "chrome",
},
directConnect: true,
baseUrl: "http://localhost:4200/",
framework: "jasmine",
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {},
},
onPrepare() {
require("ts-node").register({
project: require("path").join(__dirname, "./tsconfig.e2e.json"),
});
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
},
};

39
ui-ngx/e2e/src/app.e2e-spec.ts

@ -1,39 +0,0 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { AppPage } from './app.po';
import { browser, logging } from 'protractor';
describe('workspace-project App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display welcome message', () => {
page.navigateTo();
expect(page.getTitleText()).toEqual('Welcome to tb-license-server!');
});
afterEach(async () => {
// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
expect(logs).not.toContain(jasmine.objectContaining({
level: logging.Level.SEVERE,
} as logging.Entry));
});
});

27
ui-ngx/e2e/src/app.po.ts

@ -1,27 +0,0 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo() {
return browser.get(browser.baseUrl) as Promise<any>;
}
getTitleText() {
return element(by.css('tb-root h1')).getText() as Promise<string>;
}
}

13
ui-ngx/e2e/tsconfig.e2e.json

@ -1,13 +0,0 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}

2
ui-ngx/extra-webpack.config.js

@ -14,7 +14,7 @@
* limitations under the License.
*/
const CompressionPlugin = require("compression-webpack-plugin");
const JavaScriptOptimizerPlugin = require("@angular-devkit/build-angular/src/webpack/plugins/javascript-optimizer-plugin").JavaScriptOptimizerPlugin;
const JavaScriptOptimizerPlugin = require("@angular-devkit/build-angular/src/tools/webpack/plugins/javascript-optimizer-plugin").JavaScriptOptimizerPlugin;
const webpack = require("webpack");
const dirTree = require("directory-tree");
const ngWebpack = require('@ngtools/webpack');

130
ui-ngx/generate-icon-metadata.js

@ -0,0 +1,130 @@
/*
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const fs = require('fs');
const path = require('path');
const materialIconDir = path.join('.', 'src', 'assets', 'metadata');
const mdiMetadata = path.join('.', 'node_modules', '@mdi', 'svg', 'meta.json');
async function init() {
const iconsBundle = JSON.parse(await fs.promises.readFile(path.join(materialIconDir, 'material-icons.json')));
await getMaterialIconMetadataAndUpdated(iconsBundle);
await getMDIMetadataAndUpdated(iconsBundle);
await fs.promises.writeFile(path.join(materialIconDir, 'material-icons.json'), JSON.stringify(iconsBundle), 'utf8')
}
async function getMaterialIconMetadataAndUpdated(iconsBundle){
const iconsResponse = await fetch('https://fonts.google.com/metadata/icons?key=material_symbols&incomplete=true');
const iconsText = await iconsResponse.text();
const clearText = iconsText.substring(iconsText.indexOf("\n") + 1);
const icons = JSON.parse(clearText).icons;
let prevItem;
const filterIcons = icons.filter((item) => {
if (prevItem?.name !== item.name && !item.unsupported_families.includes('Material Icons')) {
prevItem = item;
return true;
}
return false;
});
filterIcons.forEach((item, index) => {
const findItem = iconsBundle.find((el) => el.name === item.name);
if (!findItem) {
let prevIndexIcon = 0;
if (index === 0) {
prevIndexIcon = 45;
} else {
let iteration = 0;
while (prevIndexIcon < 45) {
iteration++;
const prevIconName = filterIcons[index - iteration].name;
prevIndexIcon = findPreviousIcon(iconsBundle, prevIconName);
}
}
if (prevIndexIcon >= 0) {
iconsBundle.splice(prevIndexIcon + 1, 0, {name:item.name, tags:item.tags});
}
console.log('Not found icon:', item.name);
console.count('Not found material icon');
return;
}
if (JSON.stringify(item.tags) !== JSON.stringify(findItem.tags)) {
findItem.tags = item.tags;
console.log('Difference tags in', item.name);
console.count('Difference tags in material icon');
}
});
}
async function getMDIMetadataAndUpdated(iconsBundle){
const mdiBundle = JSON.parse(await fs.promises.readFile(mdiMetadata));
iconsBundle
.filter(item => item.name.startsWith('mdi:'))
.forEach(item => {
const iconName = item.name.substring(item.name.indexOf(":") + 1);
const findItem = mdiBundle.find((el) => el.name === iconName);
if (!findItem) {
console.error('Delete icon:', item.name);
}
});
mdiBundle.forEach((item, index) => {
const iconName = `mdi:${item.name}`
let iconTags = item.tags;
const iconAliases = item.aliases.map(item => item.replaceAll('-', ' '));
if (!iconTags.length && item.aliases.length) {
iconTags = iconAliases;
} else if (item.aliases.length) {
iconTags = iconTags.concat(iconAliases);
}
iconTags = iconTags.map(item => item.toLowerCase());
const findItem = iconsBundle.find((el) => el.name === iconName);
if (!findItem) {
let prevIndexIcon;
if (index === 0) {
prevIndexIcon = iconsBundle.findIndex(item => item.name.startsWith('mdi:'))
} else {
const prevIconName = `mdi:${mdiBundle[index - 1].name}`;
prevIndexIcon = findPreviousIcon(iconsBundle, prevIconName);
}
if (prevIndexIcon >= 0) {
iconsBundle.splice(prevIndexIcon + 1, 0, {name:iconName, tags:iconTags});
}
console.log('Not found icon:', iconName);
console.count('Not found mdi icon');
return;
}
if (JSON.stringify(iconTags) !== JSON.stringify(findItem.tags)) {
findItem.tags = iconTags;
console.log('Difference tags in', iconName);
console.count('Difference tags in mdi icon');
}
});
}
function findPreviousIcon(iconsBundle, findName) {
return iconsBundle.findIndex(item => item.name === findName);
}
init();

222
ui-ngx/package.json

@ -7,172 +7,164 @@
"build": "ng build",
"build:prod": "node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --configuration production --vendor-chunk",
"build:types": "node generate-types.js",
"test": "ng test",
"build:icon-metadata": "node generate-icon-metadata.js",
"lint": "ng lint",
"e2e": "ng e2e",
"prepare": "patch-package"
},
"private": true,
"dependencies": {
"@angular/animations": "^15.2.10",
"@angular/cdk": "^15.2.9",
"@angular/common": "^15.2.10",
"@angular/compiler": "^15.2.10",
"@angular/core": "^15.2.10",
"@angular/animations": "18.2.6",
"@angular/cdk": "18.2.6",
"@angular/common": "18.2.6",
"@angular/compiler": "18.2.6",
"@angular/core": "18.2.6",
"@angular/flex-layout": "^15.0.0-beta.42",
"@angular/forms": "^15.2.10",
"@angular/material": "^15.2.9",
"@angular/platform-browser": "^15.2.10",
"@angular/platform-browser-dynamic": "^15.2.10",
"@angular/router": "^15.2.10",
"@auth0/angular-jwt": "^5.1.2",
"@date-io/core": "1.3.7",
"@date-io/date-fns": "1.3.7",
"@angular/forms": "18.2.6",
"@angular/material": "18.2.6",
"@angular/platform-browser": "18.2.6",
"@angular/platform-browser-dynamic": "18.2.6",
"@angular/router": "18.2.6",
"@auth0/angular-jwt": "^5.2.0",
"@emotion/react": "11.13.3",
"@emotion/styled": "11.13.0",
"@flowjs/flow.js": "^2.14.1",
"@flowjs/ngx-flow": "~0.6.0",
"@geoman-io/leaflet-geoman-free": "2.14.2",
"@iplab/ngx-color-picker": "^15.0.2",
"@mat-datetimepicker/core": "~11.0.3",
"@material-ui/core": "4.12.3",
"@material-ui/icons": "4.11.2",
"@material-ui/pickers": "3.3.10",
"@mdi/svg": "^7.2.96",
"@messageformat/core": "^3.1.0",
"@ngrx/effects": "^15.4.0",
"@ngrx/store": "^15.4.0",
"@ngrx/store-devtools": "^15.4.0",
"@ngx-translate/core": "^14.0.0",
"@flowjs/ngx-flow": "~0.8.1",
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@geoman-io/leaflet-geoman-free": "2.17.0",
"@iplab/ngx-color-picker": "^18.0.1",
"@mat-datetimepicker/core": "~14.0.0",
"@mdi/svg": "^7.4.47",
"@messageformat/core": "^3.4.0",
"@mui/icons-material": "6.1.2",
"@mui/lab": "6.0.0-beta.10",
"@mui/material": "6.1.2",
"@mui/styles": "6.1.2",
"@mui/system": "6.1.2",
"@mui/x-date-pickers": "7.18.0",
"@ngrx/effects": "^18.0.2",
"@ngrx/store": "^18.0.2",
"@ngrx/store-devtools": "^18.0.2",
"@ngx-translate/core": "^15.0.0",
"@svgdotjs/svg.filter.js": "^3.0.8",
"@svgdotjs/svg.js": "^3.2.0",
"@svgdotjs/svg.js": "^3.2.4",
"@svgdotjs/svg.panzoom.js": "^2.1.2",
"@tinymce/tinymce-angular": "^7.0.0",
"ace-builds": "1.4.13",
"@tinymce/tinymce-angular": "^8.0.1",
"ace-builds": "1.36.2",
"ace-diff": "^3.0.3",
"angular-gridster2": "~15.0.4",
"angular2-hotkeys": "^13.1.0",
"angular-gridster2": "~18.0.1",
"angular2-hotkeys": "^16.0.1",
"canvas-gauges": "^2.1.7",
"core-js": "^3.29.1",
"date-fns": "2.0.0-alpha.27",
"dayjs": "1.11.4",
"core-js": "^3.38.1",
"dayjs": "1.11.13",
"echarts": "https://github.com/thingsboard/echarts/archive/5.5.0-TB.tar.gz",
"flot": "https://github.com/thingsboard/flot.git#0.9-work",
"flot.curvedlines": "https://github.com/MichaelZinsmaier/CurvedLines.git#master",
"font-awesome": "^4.7.0",
"html2canvas": "^1.4.1",
"jquery": "^3.7.1",
"jquery.terminal": "^2.35.3",
"js-beautify": "1.14.7",
"jquery.terminal": "^2.43.1",
"js-beautify": "1.15.1",
"json-schema-defaults": "^0.4.0",
"jstree": "^3.3.15",
"jstree": "^3.3.17",
"jstree-bootstrap-theme": "^1.0.1",
"jszip": "^3.10.1",
"leaflet": "1.8.0",
"leaflet": "1.9.4",
"leaflet-polylinedecorator": "1.6.0",
"leaflet-providers": "1.13.0",
"leaflet-providers": "2.0.0",
"leaflet.gridlayer.googlemutant": "0.14.1",
"leaflet.markercluster": "1.5.3",
"libphonenumber-js": "^1.10.4",
"marked": "^4.0.17",
"moment": "^2.29.4",
"moment-timezone": "^0.5.42",
"ngx-clipboard": "^15.1.0",
"libphonenumber-js": "^1.11.10",
"marked": "~12.0.2",
"moment": "^2.30.1",
"moment-timezone": "^0.5.45",
"ngx-clipboard": "^16.0.0",
"ngx-daterangepicker-material": "^6.0.4",
"ngx-drag-drop": "^15.0.1",
"ngx-flowchart": "https://github.com/thingsboard/ngx-flowchart.git#release/2.0.0",
"ngx-hm-carousel": "^3.0.0",
"ngx-markdown": "^15.1.2",
"ngx-sharebuttons": "^12.0.0",
"ngx-translate-messageformat-compiler": "^6.2.0",
"ngx-drag-drop": "^18.0.2",
"ngx-flowchart": "https://github.com/thingsboard/ngx-flowchart.git#release/3.0.0",
"ngx-hm-carousel": "^18.0.0",
"ngx-markdown": "^18.1.0",
"ngx-sharebuttons": "^15.0.3",
"ngx-translate-messageformat-compiler": "^7.0.0",
"objectpath": "^2.0.0",
"prettier": "^2.8.3",
"prop-types": "^15.8.1",
"qrcode": "^1.5.1",
"qrcode": "^1.5.4",
"raphael": "^2.3.0",
"rc-select": "13.2.1",
"react": "17.0.2",
"react-ace": "9.5.0",
"react-dom": "17.0.2",
"react-dropzone": "^11.4.2",
"rc-select": "14.15.2",
"react": "18.3.1",
"react-ace": "12.0.0",
"react-dom": "18.3.1",
"react-dropzone": "14.2.9",
"reactcss": "^1.2.3",
"rxjs": "~7.8.0",
"schema-inspector": "^2.0.2",
"rxjs": "~7.8.1",
"schema-inspector": "^2.1.0",
"screenfull": "^6.0.2",
"sorted-btree": "^1.8.1",
"split.js": "^1.6.5",
"systemjs": "6.14.1",
"systemjs": "6.15.1",
"tinycolor2": "^1.6.0",
"tinymce": "~5.10.7",
"tinymce": "~6.8.4",
"tooltipster": "^4.2.8",
"ts-transformer-keys": "^0.4.4",
"tslib": "^2.5.0",
"tslib": "^2.7.0",
"tv4": "^1.3.0",
"typeface-roboto": "^1.1.13",
"zone.js": "~0.13.0"
"zone.js": "~0.14.10"
},
"devDependencies": {
"@angular-builders/custom-webpack": "~15.0.0",
"@angular-devkit/build-angular": "^15.2.10",
"@angular-eslint/builder": "15.2.1",
"@angular-eslint/eslint-plugin": "15.2.1",
"@angular-eslint/eslint-plugin-template": "15.2.1",
"@angular-eslint/schematics": "15.2.1",
"@angular-eslint/template-parser": "15.2.1",
"@angular/cli": "^15.2.10",
"@angular/compiler-cli": "^15.2.10",
"@angular/language-service": "^15.2.10",
"@ngtools/webpack": "15.2.10",
"@types/ace-diff": "^2.1.1",
"@types/canvas-gauges": "^2.1.4",
"@types/flot": "^0.0.32",
"@types/flowjs": "^2.13.9",
"@types/jasmine": "~3.10.2",
"@types/jasminewd2": "^2.0.10",
"@types/jquery": "^3.5.30",
"@types/js-beautify": "^1.13.3",
"@types/leaflet": "1.8.0",
"@angular-builders/custom-webpack": "~18.0.0",
"@angular-devkit/build-angular": "18.2.7",
"@angular-devkit/core": "18.2.7",
"@angular-devkit/schematics": "18.2.7",
"@angular-eslint/builder": "18.3.1",
"@angular-eslint/eslint-plugin": "18.3.1",
"@angular-eslint/eslint-plugin-template": "18.3.1",
"@angular-eslint/schematics": "18.3.1",
"@angular-eslint/template-parser": "18.3.1",
"@angular/cli": "18.2.7",
"@angular/compiler-cli": "18.2.6",
"@angular/language-service": "18.2.6",
"@ngtools/webpack": "18.2.7",
"@types/ace-diff": "^2.1.4",
"@types/canvas-gauges": "^2.1.8",
"@types/flot": "^0.0.36",
"@types/flowjs": "^2.13.14",
"@types/jquery": "^3.5.31",
"@types/js-beautify": "^1.14.3",
"@types/leaflet": "1.9.12",
"@types/leaflet-polylinedecorator": "1.6.4",
"@types/leaflet-providers": "1.2.4",
"@types/leaflet.gridlayer.googlemutant": "0.4.9",
"@types/leaflet.markercluster": "1.5.4",
"@types/lodash": "^4.14.192",
"@types/marked": "^4.0.8",
"@types/lodash": "^4.17.10",
"@types/node": "~18.15.11",
"@types/raphael": "^2.3.2",
"@types/react": "17.0.37",
"@types/react-dom": "17.0.11",
"@types/systemjs": "6.13.1",
"@types/tinycolor2": "^1.4.3",
"@types/tooltipster": "^0.0.31",
"@typescript-eslint/eslint-plugin": "5.57.0",
"@typescript-eslint/parser": "5.57.0",
"compression-webpack-plugin": "^10.0.0",
"directory-tree": "^3.5.1",
"eslint": "^8.37.0",
"@types/raphael": "^2.3.9",
"@types/react": "18.3.10",
"@types/react-dom": "18.3.0",
"@types/systemjs": "6.15.1",
"@types/tinycolor2": "^1.4.6",
"@types/tooltipster": "^0.0.35",
"@typescript-eslint/eslint-plugin": "^8.7.0",
"@typescript-eslint/parser": "^8.7.0",
"@typescript-eslint/utils": "^8.7.0",
"compression-webpack-plugin": "^11.1.0",
"directory-tree": "^3.5.2",
"eslint": "~8.57.1",
"eslint-plugin-import": "latest",
"eslint-plugin-jsdoc": "latest",
"eslint-plugin-prefer-arrow": "latest",
"jasmine-core": "~3.10.1",
"jasmine-spec-reporter": "~7.0.0",
"karma": "~6.3.9",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.3",
"karma-jasmine": "~4.0.1",
"karma-jasmine-html-reporter": "^1.7.0",
"ngrx-store-freeze": "^0.2.4",
"patch-package": "^6.5.1",
"patch-package": "^8.0.0",
"postinstall-prepare": "^2.0.0",
"protractor": "~7.0.0",
"raw-loader": "^4.0.2",
"ts-node": "^10.9.1",
"typescript": "~4.9.5",
"webpack": "5.77.0"
"ts-node": "^10.9.2",
"typescript": "~5.5.4",
"webpack": "5.95.0"
},
"resolutions": {
"@types/react": "17.0.37",
"ace-builds": "1.4.13",
"@date-io/core": "1.3.7",
"rc-virtual-list": "3.4.13",
"read-package-json": "6.0.0",
"cacache": "17.0.4"
"@types/react": "18.3.10",
"rc-virtual-list": "3.5.2",
"ace-builds": "1.36.2",
"tinymce": "6.8.4"
}
}

10
ui-ngx/patches/@angular+core+15.2.10.patch → ui-ngx/patches/@angular+core+18.2.6.patch

@ -1,8 +1,8 @@
diff --git a/node_modules/@angular/core/fesm2020/core.mjs b/node_modules/@angular/core/fesm2020/core.mjs
index e9a9b75..17044d9 100755
--- a/node_modules/@angular/core/fesm2020/core.mjs
+++ b/node_modules/@angular/core/fesm2020/core.mjs
@@ -11053,13 +11053,13 @@ function findDirectiveDefMatches(tView, tNode) {
diff --git a/node_modules/@angular/core/fesm2022/core.mjs b/node_modules/@angular/core/fesm2022/core.mjs
index 0fa881f..b844dfa 100755
--- a/node_modules/@angular/core/fesm2022/core.mjs
+++ b/node_modules/@angular/core/fesm2022/core.mjs
@@ -12868,13 +12868,13 @@ function findDirectiveDefMatches(tView, tNode) {
if (isNodeMatchingSelectorList(tNode, def.selectors, /* isProjectionMode */ false)) {
matches || (matches = []);
if (isComponentDef(def)) {

21
ui-ngx/patches/@angular+flex-layout+15.0.0-beta.42.patch

File diff suppressed because one or more lines are too long

51
ui-ngx/patches/@mat-datetimepicker+core+11.0.3.patch

@ -1,51 +0,0 @@
diff --git a/node_modules/@mat-datetimepicker/core/esm2020/datetimepicker/clock.mjs b/node_modules/@mat-datetimepicker/core/esm2020/datetimepicker/clock.mjs
index e3457ea..a069460 100644
--- a/node_modules/@mat-datetimepicker/core/esm2020/datetimepicker/clock.mjs
+++ b/node_modules/@mat-datetimepicker/core/esm2020/datetimepicker/clock.mjs
@@ -259,9 +259,9 @@ export class MatDatetimepickerClockComponent {
value = 0;
}
// Don't close the minutes view if an invalid minute is clicked.
- if (!this._minutes.find((m) => m?.['value'] === value)?.['enabled']) {
- return;
- }
+ // if (!this._minutes.find((m) => m?.['value'] === value)?.['enabled']) {
+ // return;
+ // }
date = this._adapter.createDatetime(this._adapter.getYear(this.activeDate), this._adapter.getMonth(this.activeDate), this._adapter.getDate(this.activeDate), this._adapter.getHour(this.activeDate), value);
}
this._timeChanged = true;
diff --git a/node_modules/@mat-datetimepicker/core/fesm2015/mat-datetimepicker-core.mjs b/node_modules/@mat-datetimepicker/core/fesm2015/mat-datetimepicker-core.mjs
index 7699ff6..01aad13 100644
--- a/node_modules/@mat-datetimepicker/core/fesm2015/mat-datetimepicker-core.mjs
+++ b/node_modules/@mat-datetimepicker/core/fesm2015/mat-datetimepicker-core.mjs
@@ -951,9 +951,9 @@ class MatDatetimepickerClockComponent {
value = 0;
}
// Don't close the minutes view if an invalid minute is clicked.
- if (!((_b = this._minutes.find((m) => (m === null || m === void 0 ? void 0 : m['value']) === value)) === null || _b === void 0 ? void 0 : _b['enabled'])) {
- return;
- }
+ // if (!((_b = this._minutes.find((m) => (m === null || m === void 0 ? void 0 : m['value']) === value)) === null || _b === void 0 ? void 0 : _b['enabled'])) {
+ // return;
+ // }
date = this._adapter.createDatetime(this._adapter.getYear(this.activeDate), this._adapter.getMonth(this.activeDate), this._adapter.getDate(this.activeDate), this._adapter.getHour(this.activeDate), value);
}
this._timeChanged = true;
diff --git a/node_modules/@mat-datetimepicker/core/fesm2020/mat-datetimepicker-core.mjs b/node_modules/@mat-datetimepicker/core/fesm2020/mat-datetimepicker-core.mjs
index 809a57d..f712b84 100644
--- a/node_modules/@mat-datetimepicker/core/fesm2020/mat-datetimepicker-core.mjs
+++ b/node_modules/@mat-datetimepicker/core/fesm2020/mat-datetimepicker-core.mjs
@@ -946,9 +946,9 @@ class MatDatetimepickerClockComponent {
value = 0;
}
// Don't close the minutes view if an invalid minute is clicked.
- if (!this._minutes.find((m) => m?.['value'] === value)?.['enabled']) {
- return;
- }
+ // if (!this._minutes.find((m) => m?.['value'] === value)?.['enabled']) {
+ // return;
+ // }
date = this._adapter.createDatetime(this._adapter.getYear(this.activeDate), this._adapter.getMonth(this.activeDate), this._adapter.getDate(this.activeDate), this._adapter.getHour(this.activeDate), value);
}
this._timeChanged = true;

34
ui-ngx/patches/@mat-datetimepicker+core+14.0.0.patch

@ -0,0 +1,34 @@
diff --git a/node_modules/@mat-datetimepicker/core/esm2022/datetimepicker/clock.mjs b/node_modules/@mat-datetimepicker/core/esm2022/datetimepicker/clock.mjs
index 7ecfae7..08363d3 100644
--- a/node_modules/@mat-datetimepicker/core/esm2022/datetimepicker/clock.mjs
+++ b/node_modules/@mat-datetimepicker/core/esm2022/datetimepicker/clock.mjs
@@ -259,9 +259,9 @@ export class MatDatetimepickerClockComponent {
value = 0;
}
// Don't close the minutes view if an invalid minute is clicked.
- if (!this._minutes.find((m) => m?.['value'] === value)?.['enabled']) {
- return;
- }
+ // if (!this._minutes.find((m) => m?.['value'] === value)?.['enabled']) {
+ // return;
+ // }
date = this._adapter.createDatetime(this._adapter.getYear(this.activeDate), this._adapter.getMonth(this.activeDate), this._adapter.getDate(this.activeDate), this._adapter.getHour(this.activeDate), value);
}
this._timeChanged = true;
diff --git a/node_modules/@mat-datetimepicker/core/fesm2022/mat-datetimepicker-core.mjs b/node_modules/@mat-datetimepicker/core/fesm2022/mat-datetimepicker-core.mjs
index 00f4a52..df688e3 100644
--- a/node_modules/@mat-datetimepicker/core/fesm2022/mat-datetimepicker-core.mjs
+++ b/node_modules/@mat-datetimepicker/core/fesm2022/mat-datetimepicker-core.mjs
@@ -946,9 +946,9 @@ class MatDatetimepickerClockComponent {
value = 0;
}
// Don't close the minutes view if an invalid minute is clicked.
- if (!this._minutes.find((m) => m?.['value'] === value)?.['enabled']) {
- return;
- }
+ // if (!this._minutes.find((m) => m?.['value'] === value)?.['enabled']) {
+ // return;
+ // }
date = this._adapter.createDatetime(this._adapter.getYear(this.activeDate), this._adapter.getMonth(this.activeDate), this._adapter.getDate(this.activeDate), this._adapter.getHour(this.activeDate), value);
}
this._timeChanged = true;

28
ui-ngx/patches/angular-gridster2+15.0.4.patch → ui-ngx/patches/angular-gridster2+18.0.1.patch

@ -1,15 +1,15 @@
diff --git a/node_modules/angular-gridster2/fesm2020/angular-gridster2.mjs b/node_modules/angular-gridster2/fesm2020/angular-gridster2.mjs
index cf4e220..df51c91 100644
--- a/node_modules/angular-gridster2/fesm2020/angular-gridster2.mjs
+++ b/node_modules/angular-gridster2/fesm2020/angular-gridster2.mjs
diff --git a/node_modules/angular-gridster2/fesm2022/angular-gridster2.mjs b/node_modules/angular-gridster2/fesm2022/angular-gridster2.mjs
index 0dcd873..e99b602 100644
--- a/node_modules/angular-gridster2/fesm2022/angular-gridster2.mjs
+++ b/node_modules/angular-gridster2/fesm2022/angular-gridster2.mjs
@@ -666,8 +666,8 @@ class GridsterRenderer {
renderer.setStyle(el, DirTypes.LTR ? 'margin-right' : 'margin-left', '');
}
else {
- const x = Math.round(this.gridster.curColWidth * item.x);
- const y = Math.round(this.gridster.curRowHeight * item.y);
+ const x = this.gridster.curColWidth * item.x;
+ const y = this.gridster.curRowHeight * item.y;
const width = this.gridster.curColWidth * item.cols - this.gridster.$options.margin;
const height = this.gridster.curRowHeight * item.rows - this.gridster.$options.margin;
// set the cell style
renderer.setStyle(el, DirTypes.LTR ? 'margin-right' : 'margin-left', '');
}
else {
- const x = Math.round(this.gridster.curColWidth * item.x);
- const y = Math.round(this.gridster.curRowHeight * item.y);
+ const x = this.gridster.curColWidth * item.x;
+ const y = this.gridster.curRowHeight * item.y;
const width = this.gridster.curColWidth * item.cols - this.gridster.$options.margin;
const height = this.gridster.curRowHeight * item.rows - this.gridster.$options.margin;
// set the cell style

2
ui-ngx/pom.xml

@ -57,7 +57,7 @@
</goals>
<configuration>
<nodeVersion>v20.11.1</nodeVersion>
<yarnVersion>v1.22.17</yarnVersion>
<yarnVersion>v1.22.22</yarnVersion>
</configuration>
</execution>
<execution>

1
ui-ngx/src/app/core/api/widget-api.models.ts

@ -42,7 +42,6 @@ import { RafService } from '@core/services/raf.service';
import { EntityAliases } from '@shared/models/alias.models';
import { EntityInfo } from '@app/shared/models/entity.models';
import { IDashboardComponent } from '@home/models/dashboard-component.models';
import moment_ from 'moment';
import {
AlarmData,
AlarmDataPageLink,

140
ui-ngx/src/app/core/core.module.ts

@ -15,8 +15,8 @@
///
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { CommonModule, IMAGE_CONFIG } from '@angular/common';
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
@ -43,74 +43,72 @@ import { TranslateDefaultParser } from '@core/translate/translate-default-parser
import { TranslateDefaultLoader } from '@core/translate/translate-default-loader';
import { EntityConflictInterceptor } from '@core/interceptors/entity-conflict.interceptor';
@NgModule({
imports: [
CommonModule,
HttpClientModule,
FlexLayoutModule.withConfig({addFlexToParent: false}),
MatDialogModule,
MatButtonModule,
MatSnackBarModule,
// ngx-translate
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateDefaultLoader
},
missingTranslationHandler: {
provide: MissingTranslationHandler,
useClass: TbMissingTranslationHandler
},
compiler: {
provide: TranslateCompiler,
useClass: TranslateDefaultCompiler
},
parser: {
provide: TranslateParser,
useClass: TranslateDefaultParser
}
}),
HotkeyModule.forRoot(),
// ngrx
StoreModule.forRoot(reducers,
{ metaReducers,
runtimeChecks: {
strictStateImmutability: true,
strictActionImmutability: true,
strictStateSerializability: true,
strictActionSerializability: true
}}
),
EffectsModule.forRoot(effects),
env.production
? []
: StoreDevtoolsModule.instrument({
name: env.appTitle
})
],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: GlobalHttpInterceptor,
multi: true
},
{
provide: HTTP_INTERCEPTORS,
useClass: EntityConflictInterceptor,
multi: true
},
{
provide: MAT_DIALOG_DEFAULT_OPTIONS,
useValue: {
...new MatDialogConfig(),
restoreFocus: false
}
},
WINDOW_PROVIDERS
],
exports: []
})
@NgModule({ exports: [], imports: [CommonModule,
FlexLayoutModule.withConfig({ addFlexToParent: false }),
MatDialogModule,
MatButtonModule,
MatSnackBarModule,
// ngx-translate
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateDefaultLoader
},
missingTranslationHandler: {
provide: MissingTranslationHandler,
useClass: TbMissingTranslationHandler
},
compiler: {
provide: TranslateCompiler,
useClass: TranslateDefaultCompiler
},
parser: {
provide: TranslateParser,
useClass: TranslateDefaultParser
}
}),
HotkeyModule.forRoot(),
// ngrx
StoreModule.forRoot(reducers, { metaReducers,
runtimeChecks: {
strictStateImmutability: true,
strictActionImmutability: true,
strictStateSerializability: true,
strictActionSerializability: true
} }),
EffectsModule.forRoot(effects),
env.production
? []
: StoreDevtoolsModule.instrument({
name: env.appTitle,
connectInZone: true
})], providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: GlobalHttpInterceptor,
multi: true
},
{
provide: HTTP_INTERCEPTORS,
useClass: EntityConflictInterceptor,
multi: true
},
{
provide: MAT_DIALOG_DEFAULT_OPTIONS,
useValue: {
...new MatDialogConfig(),
restoreFocus: false
}
},
WINDOW_PROVIDERS,
provideHttpClient(withInterceptorsFromDi()),
{
provide: IMAGE_CONFIG,
useValue: {
disableImageSizeWarning: true,
disableImageLazyLoadWarning: true
}
}
] })
export class CoreModule {
}

4
ui-ngx/src/app/core/guards/auth.guard.ts

@ -15,7 +15,7 @@
///
import { Injectable, NgZone } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
import { AuthService } from '../auth/auth.service';
import { select, Store } from '@ngrx/store';
import { AppState } from '../core.state';
@ -34,7 +34,7 @@ import { MobileService } from '@core/services/mobile.service';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate, CanActivateChild {
export class AuthGuard {
constructor(private store: Store<AppState>,
private router: Router,

4
ui-ngx/src/app/core/guards/confirm-on-exit.guard.ts

@ -15,7 +15,7 @@
///
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanDeactivate, RouterStateSnapshot } from '@angular/router';
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { UntypedFormGroup } from '@angular/forms';
import { select, Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -39,7 +39,7 @@ export interface HasDirtyFlag {
@Injectable({
providedIn: 'root'
})
export class ConfirmOnExitGuard implements CanDeactivate<HasConfirmForm & HasDirtyFlag> {
export class ConfirmOnExitGuard {
constructor(private store: Store<AppState>,
private dialogService: DialogService,

23
ui-ngx/src/app/core/http/rule-chain.service.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { ComponentFactory, Injectable } from '@angular/core';
import { Injectable, Type } from '@angular/core';
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { forkJoin, Observable, of } from 'rxjs';
import { HttpClient } from '@angular/common/http';
@ -31,11 +31,13 @@ import { ComponentDescriptorService } from './component-descriptor.service';
import {
IRuleNodeConfigurationComponent,
LinkLabel,
RuleNodeComponentDescriptor, RuleNodeConfiguration, ScriptLanguage,
RuleNodeComponentDescriptor,
RuleNodeConfiguration,
ScriptLanguage,
TestScriptInputParams,
TestScriptResult
} from '@app/shared/models/rule-node.models';
import { ResourcesService } from '../services/resources.service';
import { componentTypeBySelector, ResourcesService } from '../services/resources.service';
import { catchError, map, mergeMap } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
import { deepClone, snakeCase } from '@core/utils';
@ -50,7 +52,7 @@ export class RuleChainService {
private ruleNodeComponentsMap: Map<RuleChainType, Array<RuleNodeComponentDescriptor>> =
new Map<RuleChainType, Array<RuleNodeComponentDescriptor>>();
private ruleNodeConfigFactories: {[directive: string]: ComponentFactory<IRuleNodeConfigurationComponent>} = {};
private ruleNodeConfigComponents: {[directive: string]: Type<IRuleNodeConfigurationComponent>} = {};
constructor(
private http: HttpClient,
@ -126,8 +128,8 @@ export class RuleChainService {
}
}
public getRuleNodeConfigFactory(directive: string): ComponentFactory<IRuleNodeConfigurationComponent> {
return this.ruleNodeConfigFactories[directive];
public getRuleNodeConfigComponent(directive: string): Type<IRuleNodeConfigurationComponent> {
return this.ruleNodeConfigComponents[directive];
}
public getRuleNodeComponentByClazz(ruleChainType: RuleChainType = RuleChainType.CORE, clazz: string): RuleNodeComponentDescriptor {
@ -219,14 +221,13 @@ export class RuleChainService {
});
}
if (moduleResource) {
tasks.push(this.resourcesService.loadFactories(moduleResource, modulesMap).pipe(
tasks.push(this.resourcesService.loadModulesWithComponents(moduleResource, modulesMap).pipe(
map((res) => {
if (nodeDefinition.configDirective && nodeDefinition.configDirective.length) {
const selector = snakeCase(nodeDefinition.configDirective, '-');
const componentFactory = res.factories.find((factory) =>
factory.selector === selector);
if (componentFactory) {
this.ruleNodeConfigFactories[nodeDefinition.configDirective] = componentFactory;
const componentType = componentTypeBySelector(res, selector);
if (componentType) {
this.ruleNodeConfigComponents[nodeDefinition.configDirective] = componentType;
} else {
component.configurationDescriptor.nodeDefinition.uiResourceLoadError =
this.translate.instant('rulenode.directive-is-not-loaded',

10
ui-ngx/src/app/core/interceptors/entity-conflict.interceptor.ts

@ -15,15 +15,7 @@
///
import { Injectable } from '@angular/core';
import {
HttpErrorResponse,
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpParams,
HttpRequest,
HttpStatusCode
} from '@angular/common/http';
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpParams, HttpRequest, HttpStatusCode } from '@angular/common/http';
import { Observable, of, throwError } from 'rxjs';
import { catchError, switchMap } from 'rxjs/operators';
import { MatDialog } from '@angular/material/dialog';

49
ui-ngx/src/app/core/services/dashboard-utils.service.ts

@ -749,12 +749,57 @@ export class DashboardUtilsService {
widget.sizeY = 1;
}
}
const widgets: WidgetLayout[] = [];
for (const w of Object.keys(layout.widgets)) {
const widget = layout.widgets[w];
widget.row = Math.round(widget.row * ratio);
widget.col = Math.round(widget.col * ratio);
widget.sizeX = Math.round(widget.sizeX * ratio);
widget.sizeY = Math.round(widget.sizeY * ratio);
widget.sizeX = Math.max(1, Math.round(widget.sizeX * ratio));
widget.sizeY = Math.max(1, Math.round(widget.sizeY * ratio));
widgets.push(widget);
}
widgets.sort((w1, w2) => {
let res = w1.col - w2.col;
if (res === 0) {
res = w1.row - w2.row;
}
return res;
});
for (const widget of widgets) {
for (const widget2 of widgets) {
if (widget !== widget2) {
const left = widget.col;
const right = widget.col + widget.sizeX;
const top = widget.row;
const bottom = widget.row + widget.sizeY;
const left2 = widget2.col;
const right2 = widget2.col + widget2.sizeX;
const top2 = widget2.row;
const bottom2 = widget2.row + widget2.sizeY;
if (left < right2 && right > left2 &&
top < bottom2 && bottom > top2 ) {
let horizontalOverlapFixed = false;
if (right - left2 === 1) {
if (widget.sizeX > 1) {
widget.sizeX--;
horizontalOverlapFixed = true;
} else if (widget2.sizeX > 1) {
widget2.col++;
widget2.sizeX--;
horizontalOverlapFixed = true;
}
}
if (!horizontalOverlapFixed && (bottom - top2) === 1) {
if (widget.sizeY > 1) {
widget.sizeY--;
} else if (widget2.sizeY > 1) {
widget2.row++;
widget2.sizeY--;
}
}
}
}
}
}
}

10
ui-ngx/src/app/core/services/dynamic-component-factory.service.ts

@ -18,6 +18,7 @@ import { Component, Injectable, Type, ɵComponentDef, ɵNG_COMP_DEF } from '@ang
import { from, Observable, of } from 'rxjs';
import { CommonModule } from '@angular/common';
import { mergeMap } from 'rxjs/operators';
import { guid } from '@core/utils';
@Injectable({
providedIn: 'root'
@ -30,14 +31,14 @@ export class DynamicComponentFactoryService {
public createDynamicComponent<T>(
componentType: Type<T>,
template: string,
modules?: Type<any>[],
imports?: Type<any>[],
preserveWhitespaces?: boolean,
styles?: string[]): Observable<Type<T>> {
return from(import('@angular/compiler')).pipe(
mergeMap(() => {
let componentImports: Type<any>[] = [CommonModule];
if (modules) {
componentImports = [...componentImports, ...modules];
if (imports) {
componentImports = [...componentImports, ...imports];
}
const comp = this.createAndCompileDynamicComponent(componentType, template, componentImports, preserveWhitespaces, styles);
return of(comp.type);
@ -60,7 +61,8 @@ export class DynamicComponentFactoryService {
imports,
preserveWhitespaces,
styles,
standalone: true
standalone: true,
selector: 'tb-dynamic-component#' + guid()
})(componentType);
// Trigger component compilation
return comp[ɵNG_COMP_DEF];

211
ui-ngx/src/app/core/services/resources.service.ts

@ -15,16 +15,19 @@
///
import {
Compiler,
ComponentFactory,
createNgModule,
Inject,
Injectable,
Injector,
ModuleWithComponentFactories,
Type, ɵNG_MOD_DEF
Type,
ɵComponentDef,
ɵCssSelectorList,
ɵNG_COMP_DEF,
ɵNG_MOD_DEF,
ɵNgModuleDef
} from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { forkJoin, Observable, ReplaySubject, throwError } from 'rxjs';
import { Observable, ReplaySubject, throwError } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { IModulesMap } from '@modules/common/modules-map.models';
import { TbResourceId } from '@shared/models/id/tb-resource-id';
@ -36,13 +39,56 @@ import { AppState } from '@core/core.state';
import { map, tap } from 'rxjs/operators';
import { RequestConfig } from '@core/http/http-utils';
declare const System;
export interface ModuleInfo {
module: ɵNgModuleDef<any>;
components: ɵComponentDef<any>[];
}
export interface ModulesWithComponents {
modules: ModuleInfo[];
standaloneComponents: ɵComponentDef<any>[];
}
export const flatModulesWithComponents = (modulesWithComponentsList: ModulesWithComponents[]): ModulesWithComponents => {
const modulesWithComponents: ModulesWithComponents = {
modules: [],
standaloneComponents: []
};
for (const m of modulesWithComponentsList) {
for (const module of m.modules) {
if (!modulesWithComponents.modules.some(m1 => m1.module === module.module)) {
modulesWithComponents.modules.push(module);
}
}
for (const comp of m.standaloneComponents) {
if (!modulesWithComponents.standaloneComponents.includes(comp)) {
modulesWithComponents.standaloneComponents.push(comp);
}
}
}
return modulesWithComponents;
}
export interface ModulesWithFactories {
modules: Type<any>[];
factories: ComponentFactory<any>[];
export const modulesWithComponentsToTypes = (modulesWithComponents: ModulesWithComponents): Type<any>[] =>
[...modulesWithComponents.modules.map(m => m.module.type),
...modulesWithComponents.standaloneComponents.map(c => c.type)];
export const componentTypeBySelector = (modulesWithComponents: ModulesWithComponents, selector: string): Type<any> | undefined => {
let found = modulesWithComponents.standaloneComponents.find(c => matchesSelector(c.selectors, selector));
if (!found) {
for (const m of modulesWithComponents.modules) {
found = m.components.find(c => matchesSelector(c.selectors, selector));
if (found) {
break;
}
}
}
return found?.type;
}
const matchesSelector = (selectors: ɵCssSelectorList, selector: string) =>
selectors.some(s => s.some(s1 => typeof s1 === 'string' && s1 === selector));
@Injectable({
providedIn: 'root'
})
@ -50,16 +96,15 @@ export class ResourcesService {
private loadedJsonResources: { [url: string]: ReplaySubject<any> } = {};
private loadedResources: { [url: string]: ReplaySubject<void> } = {};
private loadedModulesAndFactories: { [url: string]: ReplaySubject<ModulesWithFactories> } = {};
private loadedModulesWithComponents: { [url: string]: ReplaySubject<ModulesWithComponents> } = {};
private anchor = this.document.getElementsByTagName('head')[0] || this.document.getElementsByTagName('body')[0];
constructor(@Inject(DOCUMENT) private readonly document: any,
constructor(@Inject(DOCUMENT) private readonly document: Document,
protected store: Store<AppState>,
private compiler: Compiler,
private http: HttpClient,
private injector: Injector) {
this.store.pipe(select(selectIsAuthenticated)).subscribe(() => this.clearModulesCache());
this.store.pipe(select(selectIsAuthenticated)).subscribe(() => this.clearModulesWithComponentsCache());
}
public loadJsonResource<T>(url: string, postProcess?: (data: T) => T): Observable<T> {
@ -93,7 +138,7 @@ export class ResourcesService {
return this.loadedResources[url].asObservable();
}
let fileType;
let fileType: string;
const match = /[./](css|less|html|htm|js)?(([?#]).*)?$/.exec(url);
if (match !== null) {
fileType = match[1];
@ -137,53 +182,38 @@ export class ResourcesService {
);
}
public loadFactories(resourceId: string | TbResourceId, modulesMap: IModulesMap): Observable<ModulesWithFactories> {
public loadModulesWithComponents(resourceId: string | TbResourceId, modulesMap: IModulesMap): Observable<ModulesWithComponents> {
const url = this.getDownloadUrl(resourceId);
if (this.loadedModulesAndFactories[url]) {
return this.loadedModulesAndFactories[url].asObservable();
if (this.loadedModulesWithComponents[url]) {
return this.loadedModulesWithComponents[url].asObservable();
}
modulesMap.init();
const meta = this.getMetaInfo(resourceId);
const subject = new ReplaySubject<ModulesWithFactories>();
this.loadedModulesAndFactories[url] = subject;
const subject = new ReplaySubject<ModulesWithComponents>();
this.loadedModulesWithComponents[url] = subject;
import('@angular/compiler').then(
() => {
// @ts-ignore
System.import(url, undefined, meta).then(
(module) => {
const modules = this.extractNgModules(module);
if (modules.length) {
const tasks: Promise<ModuleWithComponentFactories<any>>[] = [];
for (const m of modules) {
tasks.push(this.compiler.compileModuleAndAllComponentsAsync(m));
}
forkJoin(tasks).subscribe({
next: (compiled) => {
try {
const componentFactories: ComponentFactory<any>[] = [];
for (const c of compiled) {
c.ngModuleFactory.create(this.injector);
componentFactories.push(...c.componentFactories);
}
const modulesWithFactories: ModulesWithFactories = {
modules,
factories: componentFactories
};
this.loadedModulesAndFactories[url].next(modulesWithFactories);
this.loadedModulesAndFactories[url].complete();
} catch (e) {
this.loadedModulesAndFactories[url].error(new Error(`Unable to init module from url: ${url}`));
}
},
error: (e) => {
this.loadedModulesAndFactories[url].error(new Error(`Unable to compile module from url: ${url}`));
(module: any) => {
try {
const modulesWithComponents = this.extractModulesWithComponents(module);
if (modulesWithComponents.modules.length || modulesWithComponents.standaloneComponents.length) {
for (const module of modulesWithComponents.modules) {
createNgModule(module.module.type, this.injector);
}
});
} else {
this.loadedModulesAndFactories[url].error(new Error(`Module '${url}' doesn't have default export!`));
this.loadedModulesWithComponents[url].next(modulesWithComponents);
this.loadedModulesWithComponents[url].complete();
} else {
this.loadedModulesWithComponents[url].error(new Error(`Module '${url}' doesn't have exported modules or components!`));
}
} catch (e) {
console.log(`Unable to parse module from url: ${url}`, e);
this.loadedModulesWithComponents[url].error(new Error(`Unable to parse module from url: ${url}`));
}
},
(e) => {
this.loadedModulesAndFactories[url].error(new Error(`Unable to load module from url: ${url}`));
() => {
this.loadedModulesWithComponents[url].error(new Error(`Unable to load module from url: ${url}`));
}
);
}
@ -192,7 +222,7 @@ export class ResourcesService {
tap({
next: () => System.delete(url),
error: () => {
delete this.loadedModulesAndFactories[url];
delete this.loadedModulesWithComponents[url];
System.delete(url);
},
complete: () => System.delete(url)
@ -200,41 +230,64 @@ export class ResourcesService {
);
}
private extractNgModules(module: any, modules: Type<any>[] = []): Type<any>[] {
try {
let potentialModules = [module];
let currentScanDepth = 0;
while (potentialModules.length && currentScanDepth < 10) {
const newPotentialModules = [];
for (const potentialModule of potentialModules) {
if (potentialModule && (ɵNG_MOD_DEF in potentialModule)) {
modules.push(potentialModule);
} else {
for (const k of Object.keys(potentialModule)) {
if (!this.isPrimitive(potentialModule[k])) {
newPotentialModules.push(potentialModule[k]);
}
private extractModulesWithComponents(module: any,
modulesWithComponents: ModulesWithComponents = {
modules: [],
standaloneComponents: []
},
visitedModules: Set<any> = new Set<any>()): ModulesWithComponents {
if (module && ['object', 'function'].includes(typeof module) && !visitedModules.has(module)) {
visitedModules.add(module);
if (ɵNG_MOD_DEF in module) {
const moduleDef: ɵNgModuleDef<any> = module[ɵNG_MOD_DEF];
const moduleInfo: ModuleInfo = {
module: moduleDef,
components: []
}
modulesWithComponents.modules.push(moduleInfo);
const exportsDecl = moduleDef.exports;
let exports: Type<any>[];
if (Array.isArray(exportsDecl)) {
exports = exportsDecl;
} else {
exports = exportsDecl();
}
for (const element of exports) {
if (ɵNG_COMP_DEF in element) {
const component: ɵComponentDef<any> = element[ɵNG_COMP_DEF];
if (!component.standalone) {
moduleInfo.components.push(component);
} else {
modulesWithComponents.standaloneComponents.push(component);
}
} else {
this.extractModulesWithComponents(module, modulesWithComponents, visitedModules);
}
}
} else if (ɵNG_COMP_DEF in module) {
const component: ɵComponentDef<any> = module[ɵNG_COMP_DEF];
if (component.standalone) {
if (!modulesWithComponents.standaloneComponents.includes(component)) {
modulesWithComponents.standaloneComponents.push(component);
}
}
} else {
for (const k of Object.keys(module)) {
const val = module[k];
if (val && ['object', 'function'].includes(typeof val)) {
this.extractModulesWithComponents(val, modulesWithComponents, visitedModules);
}
}
potentialModules = newPotentialModules;
currentScanDepth++;
}
} catch (e) {
console.log('Could not load NgModule', e);
}
return modules;
}
private isPrimitive(test) {
return test !== Object(test);
return modulesWithComponents;
}
private loadResourceByType(type: 'css' | 'js', url: string): Observable<any> {
const subject = new ReplaySubject<void>();
this.loadedResources[url] = subject;
let el;
let el: any;
let loaded = false;
switch (type) {
case 'js':
@ -250,7 +303,7 @@ export class ResourcesService {
el.href = url;
break;
}
el.onload = el.onreadystatechange = (e) => {
el.onload = el.onreadystatechange = () => {
if (el.readyState && !/^c|loade/.test(el.readyState) || loaded) { return; }
el.onload = el.onreadystatechange = null;
loaded = true;
@ -282,7 +335,7 @@ export class ResourcesService {
}
}
private clearModulesCache() {
this.loadedModulesAndFactories = {};
private clearModulesWithComponentsCache() {
this.loadedModulesWithComponents = {};
}
}

2
ui-ngx/src/app/core/settings/settings.utils.ts

@ -16,7 +16,7 @@
import { environment as env } from '@env/environment';
import { TranslateService } from '@ngx-translate/core';
import * as _moment from 'moment';
import _moment from 'moment';
import { Observable } from 'rxjs';
export function updateUserLang(translate: TranslateService, userLang: string, translations = env.supportedLangs): Observable<any> {

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

Loading…
Cancel
Save