Browse Source

Merge branch 'master' into lwm2m_M15

pull/11784/head
nick 2 years ago
parent
commit
bb945e2540
  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. 16
      application/src/main/data/upgrade/3.8.0/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. 4
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  8. 39
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  9. 7
      application/src/main/java/org/thingsboard/server/controller/CustomerController.java
  10. 5
      application/src/main/java/org/thingsboard/server/controller/TenantController.java
  11. 13
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  12. 1
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  13. 4
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2EdgeProcessor.java
  14. 260
      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. 3
      application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionsInfo.java
  17. 2
      application/src/main/resources/thingsboard.yml
  18. 91
      application/src/test/java/org/thingsboard/server/actors/tenant/TenantActorTest.java
  19. 126
      application/src/test/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionServiceTest.java
  20. 11
      application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java
  21. 19
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java
  22. 62
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mTemperatureSensor.java
  23. 78
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java
  24. 47
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2MIntegrationObserveCompositeTest.java
  25. 20
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java
  26. 122
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java
  27. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java
  28. 2
      common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java
  29. 2
      common/edge-api/src/main/proto/edge.proto
  30. 1
      common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java
  31. 69
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java
  32. 15
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/utils/CoapEfentoUtils.java
  33. 40
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/utils/PulseCounterType.java
  34. 68
      common/transport/coap/src/test/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResourceTest.java
  35. 2
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapSecurityStore.java
  36. 13
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java
  37. 21
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java
  38. 5
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java
  39. 11
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitRepositoryService.java
  40. 3
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepositoryService.java
  41. 5
      dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
  42. 24
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java
  43. 58
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java
  44. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/credentials/CertPemCredentials.java
  45. 50
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  46. 7
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/credentials/CertPemCredentialsTest.java
  47. 130
      ui-ngx/generate-icon-metadata.js
  48. 1
      ui-ngx/package.json
  49. 16
      ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts
  50. 3
      ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts
  51. 22
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/gateway-connector-version-processor.abstract.ts
  52. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.html
  53. 1
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html
  54. 5
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts
  55. 14
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html
  56. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html
  57. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.abstract.ts
  58. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html
  59. 10
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/report-strategy/report-strategy.component.ts
  60. 1
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts
  61. 8
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts
  62. 15
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html
  63. 150
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts
  64. 9
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts
  65. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/gateway-help-link.pipe.ts
  66. 32
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/latest-version-config.pipe.ts
  67. 9
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/gateway-connector-version-mapping.util.ts
  68. 10
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/opc-version-mapping.util.ts
  69. 16
      ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts
  70. 7
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  71. 28
      ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.html
  72. 70
      ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.ts
  73. 2
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts
  74. 15
      ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts
  75. 5
      ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html
  76. 21
      ui-ngx/src/app/shared/components/time/datapoints-limit.component.scss
  77. 36
      ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.html
  78. 2
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.html
  79. 11
      ui-ngx/src/app/shared/models/icon.models.ts
  80. 8
      ui-ngx/src/app/shared/models/widget-settings.models.ts
  81. 74
      ui-ngx/src/assets/help/en_US/scada/symbol_state_render_fn.md
  82. 144
      ui-ngx/src/assets/help/en_US/scada/tag_click_action_fn.md
  83. 59
      ui-ngx/src/assets/help/en_US/scada/tag_state_render_fn.md
  84. 65
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/attributes_timeseries_expressions_fn.md
  85. 42
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  86. 16
      ui-ngx/src/assets/metadata/connector-default-configs/modbus.json
  87. 2
      ui-ngx/src/assets/metadata/material-icons.json
  88. 2
      ui-ngx/yarn.lock

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": {

16
application/src/main/data/upgrade/3.8.0/schema_update.sql

@ -0,0 +1,16 @@
--
-- 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.
--

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

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

@ -82,7 +82,9 @@ public class AuthController extends BaseController {
@GetMapping(value = "/auth/user")
public User getUser() throws ThingsboardException {
SecurityUser securityUser = getCurrentUser();
return userService.findUserById(securityUser.getTenantId(), securityUser.getId());
User user = userService.findUserById(securityUser.getTenantId(), securityUser.getId());
checkDashboardInfo(user.getAdditionalInfo());
return user;
}
@ApiOperation(value = "Logout (logout)",

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

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.ListenableFuture;
import jakarta.mail.MessagingException;
@ -113,6 +114,7 @@ import org.thingsboard.server.common.data.rpc.Rpc;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.util.ThrowingBiFunction;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
@ -187,6 +189,8 @@ import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.StringUtils.isNotEmpty;
import static org.thingsboard.server.common.data.query.EntityKeyType.ENTITY_FIELD;
import static org.thingsboard.server.controller.ControllerConstants.DEFAULT_DASHBOARD;
import static org.thingsboard.server.controller.ControllerConstants.HOME_DASHBOARD;
import static org.thingsboard.server.controller.UserController.YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION;
import static org.thingsboard.server.dao.service.Validator.validateId;
@ -872,11 +876,36 @@ public abstract class BaseController {
}
}
protected void processDashboardIdFromAdditionalInfo(ObjectNode additionalInfo, String requiredFields) throws ThingsboardException {
String dashboardId = additionalInfo.has(requiredFields) ? additionalInfo.get(requiredFields).asText() : null;
if (dashboardId != null && !dashboardId.equals("null")) {
if (dashboardService.findDashboardById(getTenantId(), new DashboardId(UUID.fromString(dashboardId))) == null) {
additionalInfo.remove(requiredFields);
protected void checkUserInfo(User user) throws ThingsboardException {
if (user.getAdditionalInfo() instanceof ObjectNode additionalInfo) {
checkDashboardInfo(additionalInfo);
UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getTenantId(), user.getId());
if (userCredentials.isEnabled() && !additionalInfo.has("userCredentialsEnabled")) {
additionalInfo.put("userCredentialsEnabled", true);
}
}
}
protected void checkDashboardInfo(JsonNode additionalInfo) throws ThingsboardException {
checkDashboardInfo(additionalInfo, DEFAULT_DASHBOARD);
checkDashboardInfo(additionalInfo, HOME_DASHBOARD);
}
protected void checkDashboardInfo(JsonNode node, String dashboardField) throws ThingsboardException {
if (node instanceof ObjectNode additionalInfo) {
DashboardId dashboardId = Optional.ofNullable(additionalInfo.get(dashboardField))
.filter(JsonNode::isTextual).map(JsonNode::asText)
.map(id -> {
try {
return new DashboardId(UUID.fromString(id));
} catch (IllegalArgumentException e) {
return null;
}
}).orElse(null);
if (dashboardId != null && !dashboardService.existsById(getTenantId(), dashboardId)) {
additionalInfo.remove(dashboardField);
}
}
}

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

@ -80,9 +80,7 @@ public class CustomerController extends BaseController {
checkParameter(CUSTOMER_ID, strCustomerId);
CustomerId customerId = new CustomerId(toUUID(strCustomerId));
Customer customer = checkCustomerId(customerId, Operation.READ);
if (!customer.getAdditionalInfo().isNull()) {
processDashboardIdFromAdditionalInfo((ObjectNode) customer.getAdditionalInfo(), HOME_DASHBOARD);
}
checkDashboardInfo(customer.getAdditionalInfo(), HOME_DASHBOARD);
return customer;
}
@ -181,7 +179,8 @@ public class CustomerController extends BaseController {
public Customer getTenantCustomer(
@Parameter(description = "A string value representing the Customer title.")
@RequestParam String customerTitle) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
TenantId tenantId = getCurrentUser().getTenantId();
return checkNotNull(customerService.findCustomerByTenantIdAndTitle(tenantId, customerTitle), "Customer with title [" + customerTitle + "] is not found");
}
}

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

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor;
@ -79,9 +78,7 @@ public class TenantController extends BaseController {
checkParameter(TENANT_ID, strTenantId);
TenantId tenantId = TenantId.fromUUID(toUUID(strTenantId));
Tenant tenant = checkTenantId(tenantId, Operation.READ);
if (!tenant.getAdditionalInfo().isNull()) {
processDashboardIdFromAdditionalInfo((ObjectNode) tenant.getAdditionalInfo(), HOME_DASHBOARD);
}
checkDashboardInfo(tenant.getAdditionalInfo(), HOME_DASHBOARD);
return tenant;
}

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

@ -16,7 +16,6 @@
package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.servlet.http.HttpServletRequest;
@ -92,8 +91,6 @@ import static org.thingsboard.server.controller.ControllerConstants.ALARM_ID_PAR
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID;
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.DASHBOARD_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.DEFAULT_DASHBOARD;
import static org.thingsboard.server.controller.ControllerConstants.HOME_DASHBOARD;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
@ -146,15 +143,7 @@ public class UserController extends BaseController {
checkParameter(USER_ID, strUserId);
UserId userId = new UserId(toUUID(strUserId));
User user = checkUserId(userId, Operation.READ);
if (user.getAdditionalInfo().isObject()) {
ObjectNode additionalInfo = (ObjectNode) user.getAdditionalInfo();
processDashboardIdFromAdditionalInfo(additionalInfo, DEFAULT_DASHBOARD);
processDashboardIdFromAdditionalInfo(additionalInfo, HOME_DASHBOARD);
UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getTenantId(), user.getId());
if (userCredentials.isEnabled() && !additionalInfo.has("userCredentialsEnabled")) {
additionalInfo.put("userCredentialsEnabled", true);
}
}
checkUserInfo(user);
return user;
}

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

@ -142,6 +142,7 @@ public class ThingsboardInstallService {
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");
//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());

260
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,137 +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;
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.0" -> updateSchema("3.8.0", 3008000, "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();
@ -183,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:

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();
}
}

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;

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

@ -137,7 +137,6 @@ public class LwM2MTestClient {
private Map<LwM2MClientState, Integer> clientDtlsCid;
private LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandlerTest;
private LwM2mClientContext clientContext;
public void init(Security security, Security securityBs, int port, boolean isRpc,
LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandler,
LwM2mClientContext clientContext, boolean isWriteAttribute, Integer cIdLength, boolean queueMode,
@ -159,11 +158,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 +176,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));
@ -239,11 +238,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 +261,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 +287,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());

62
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,7 +50,9 @@ public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destr
private double maxMeasuredValue = currentTemp;
private LeshanClient leshanClient;
private List<Double> containingValues;
private int cntRead_5700;
private int cntIdentitySystem;
protected static final Random RANDOM = new Random();
private static final List<Integer> supportedResources = Arrays.asList(5601, 5602, 5700, 5701);
@ -57,7 +63,7 @@ public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destr
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();
@ -73,15 +79,18 @@ public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destr
case 5602:
return ReadResponse.success(resourceId, getTwoDigitValue(maxMeasuredValue));
case 5700:
if (identity == LwM2mServer.SYSTEM) {
setTemperature();
setData();
if (identity == LwM2mServer.SYSTEM) { // return value for ForCollectedValue
cntIdentitySystem++;
return ReadResponse.success(resourceId, cntIdentitySystem == 1 ?
RESOURCE_ID_3303_12_5700_VALUE_0 : RESOURCE_ID_3303_12_5700_VALUE_1);
}
cntRead_5700++;
if (cntRead_5700 == 1) { // read value after start
return ReadResponse.success(resourceId, getTwoDigitValue(currentTemp));
} else if (this.getId() == 12 && this.leshanClient != null) {
containingValues = new ArrayList<>();
sendCollected(5700);
return ReadResponse.success(resourceId, getData());
} else {
if (this.getId() == 12 && this.leshanClient != null) {
sendCollected();
}
return ReadResponse.success(resourceId, getTwoDigitValue(currentTemp));
}
case 5701:
@ -117,10 +126,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 +153,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 +161,26 @@ public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destr
public void destroy() {
}
private void sendCollected(int resourceId) {
private void sendCollected() {
try {
int resourceId = 5700;
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);
Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_0 = Instant.now().toEpochMilli();
Thread.sleep(RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS);
sender.collectData(Arrays.asList(getPathForCollectedValue(resourceId)));
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));
}
}

78
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.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 org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler;
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,19 +50,23 @@ 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 {
@ -84,6 +98,12 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg
protected String idVer_19_0_0;
@SpyBean
protected DefaultLwM2mUplinkMsgHandler defaultUplinkMsgHandlerTest;
@SpyBean
protected LwM2mTransportServerHelper lwM2mTransportServerHelperTest;
public AbstractRpcLwM2MIntegrationTest() {
setResources(resources);
}
@ -144,7 +164,8 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg
" \"" + 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" +
" \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2 + "\": \"" + RESOURCE_ID_NAME_19_0_2 + "\",\n" +
" \"" + objectIdVer_3303 + "/" + OBJECT_INSTANCE_ID_12 + "/" + RESOURCE_ID_5700 + "\": \"" + RESOURCE_ID_NAME_3303_12_5700 + "\"\n" +
" },\n" +
" \"observe\": [\n" +
" \"" + idVer_3_0_9 + "\",\n" +
@ -159,7 +180,8 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg
" \"telemetry\": [\n" +
" \"" + idVer_3_0_9 + "\",\n" +
" \"" + idVer_19_0_0 + "\",\n" +
" \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "\"\n" +
" \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "\",\n" +
" \"" + objectIdVer_3303 + "/" + OBJECT_INSTANCE_ID_12 + "/" + RESOURCE_ID_5700 + "\"\n" +
" ],\n" +
" \"attributeLwm2m\": {}\n" +
" }";
@ -183,4 +205,56 @@ 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 countUpdateReg() {
return Mockito.mockingDetails(defaultUplinkMsgHandlerTest)
.getInvocations().stream()
.filter(invocation -> invocation.getMethod().getName().equals("updatedReg"))
.count();
}
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();
}
}

47
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,10 +52,6 @@ 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
@ -517,13 +510,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 +522,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 +533,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());
}
}

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

@ -24,9 +24,7 @@ import org.eclipse.leshan.core.response.ReadResponse;
import org.eclipse.leshan.server.registration.Registration;
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;
@ -41,14 +39,12 @@ 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;
@Test
public void testObserveReadAll_Count_4_CancelAll_Count_0_Ok() throws Exception {
String actualValuesReadAll = sendRpcObserveOkWithResultValue("ObserveReadAll", null);
@ -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 {

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

@ -15,30 +15,26 @@
*/
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.apache.commons.collections4.map.HashedMap;
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.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 java.util.concurrent.atomic.AtomicReference;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.awaitility.Awaitility.await;
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;
@ -49,20 +45,22 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_11;
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_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_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_3;
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.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 RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest {
@SpyBean
DefaultLwM2mUplinkMsgHandler defaultUplinkMsgHandlerTest;
/**
* Read {"id":"/3"}
* Read {"id":"/6"}...
@ -88,7 +86,7 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest
e.printStackTrace();
}
});
} catch (Exception e2){
} catch (Exception e2) {
e2.printStackTrace();
}
}
@ -99,10 +97,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 +120,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 +133,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 +159,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 +219,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));
@ -232,56 +230,55 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest
/**
* /3303/0/5700
* Read {"id":"/3303/0/5700"}
* 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(), timestamps2 = Instance.now()
*
* 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 {
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)
);
// init test
long startTs = Instant.now().toEpochMilli();
int cntValues = 4;
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++;
sendRPCById(expectedIdVer);
// verify result read: verify count value: 1-2: send CollectedValue; 3 - response for read;
long endTs = Instant.now().toEpochMilli() + RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS * 4;
String expectedVal_1 = String.valueOf(RESOURCE_ID_3303_12_5700_VALUE_0);
String expectedVal_2 = String.valueOf(RESOURCE_ID_3303_12_5700_VALUE_1);
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=" + startTs
+ "&endTs=" + endTs
+ "&interval=0&limit=100&useStrictDataTypes=false",
ObjectNode.class));
// verify cntValues
return actualValues.get() != null && actualValues.get().get(RESOURCE_ID_NAME_3303_12_5700).size() == cntValues;
});
// verify ts
ArrayNode actual = (ArrayNode) actualValues.get().get(RESOURCE_ID_NAME_3303_12_5700);
Map<String, Long> keyTsMaps = new HashedMap();
for (JsonNode tsNode: actual) {
if (tsNode.get("value").asText().equals(expectedVal_1) || tsNode.get("value").asText().equals(expectedVal_2)) {
keyTsMaps.put(tsNode.get("value").asText(), tsNode.get("ts").asLong());
}
}
assertTrue(keyTsMaps.size() == 2);
long actualTS0 = keyTsMaps.get(expectedVal_1).longValue();
long actualTS1 = keyTsMaps.get(expectedVal_2).longValue();
assertTrue(actualTS0 > 0);
assertTrue(actualTS1 > 0);
assertTrue(actualTS1 > actualTS0);
assertTrue((actualTS1 - actualTS0) >= RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS);
assertTrue(actualTS0 <= RESOURCE_ID_3303_12_5700_TS_0);
assertTrue(actualTS1 <= RESOURCE_ID_3303_12_5700_TS_1);
}
/**
@ -301,7 +298,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());

2
common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java

@ -76,4 +76,6 @@ public interface DashboardService extends EntityDaoService {
List<Dashboard> findTenantDashboardsByTitle(TenantId tenantId, String title);
boolean existsById(TenantId tenantId, DashboardId dashboardId);
}

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()

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

@ -46,12 +46,15 @@ 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;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@ -62,6 +65,19 @@ 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;
@ -84,6 +100,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 +115,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 +131,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 +198,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);
}
});
@ -280,6 +300,12 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
}
}
valuesMap.values().forEach(jsonObject -> {
for (PulseCounterType pulseCounterType : PulseCounterType.values()) {
calculatePulseCounterTotalValue(jsonObject, pulseCounterType);
}
});
if (CollectionUtils.isEmpty(valuesMap)) {
throw new IllegalStateException("[" + sessionId + "]: Failed to collect Efento measurements, reason, values map is empty!");
}
@ -312,7 +338,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));
addPulseCounterProperties(values, "iaq_", channelNumber, startPoint + sampleOffset, IAQ_METADATA_FACTOR);
break;
case MEASUREMENT_TYPE_ELECTRICITY_METER:
values.addProperty("watt_hour_" + channelNumber, (double) (startPoint + sampleOffset));
@ -330,22 +356,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));
addPulseCounterProperties(values, "water_cnt_acc_minor_", 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));
addPulseCounterProperties(values, "water_cnt_acc_major_", 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));
addPulseCounterProperties(values, "static_iaq_", channelNumber, startPoint + sampleOffset, STATIC_IAQ_METADATA_FACTOR);
break;
case MEASUREMENT_TYPE_CO2_GAS:
addPulseCounterProperties(values, "co2_gas_", channelNumber, startPoint + sampleOffset, CO2_GAS_METADATA_FACTOR);
break;
case MEASUREMENT_TYPE_CO2_EQUIVALENT:
values.addProperty("co2_ppm_" + channelNumber, (double) (startPoint + sampleOffset));
addPulseCounterProperties(values, "co2_", channelNumber, startPoint + sampleOffset, CO2_EQUIVALENT_METADATA_FACTOR);
break;
case MEASUREMENT_TYPE_BREATH_VOC:
values.addProperty("breath_voc_ppm_" + channelNumber, (double) (startPoint + sampleOffset));
addPulseCounterProperties(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 +386,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));
addPulseCounterProperties(values, "pulse_cnt_acc_minor_", 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));
addPulseCounterProperties(values, "pulse_cnt_acc_major_", 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));
addPulseCounterProperties(values, "elec_meter_acc_minor_", 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));
addPulseCounterProperties(values, "elec_meter_acc_major_", 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));
addPulseCounterProperties(values, "pulse_cnt_acc_wide_minor_", 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));
addPulseCounterProperties(values, "pulse_cnt_acc_wide_major_", 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 +416,20 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
}
}
private void addPulseCounterProperties(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 calculatePulseCounterTotalValue(JsonObject value, PulseCounterType pulseCounterType) {
Set<String> keys = value.keySet();
Optional<String> major = keys.stream().filter(s -> s.startsWith(pulseCounterType.getPrefix() + "major_")).findAny();
Optional<String> minor = keys.stream().filter(s -> s.startsWith(pulseCounterType.getPrefix() + "minor_")).findAny();
if (major.isPresent() && minor.isPresent()) {
value.addProperty(pulseCounterType.getPrefix() + "total_value", value.get(major.get()).getAsInt() * pulseCounterType.getMajorResolution() + value.get(minor.get()).getAsInt());
}
}
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;
}
}

68
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

@ -70,7 +70,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 +152,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 propertyPrefix, double expectedValue) {
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(List.of(MeasurementsProtos.ProtoChannel.newBuilder()
.setType(minorType)
.setTimestamp(Math.toIntExact(tsInSec))
.addAllSampleOffsets(minorSampleOffsets)
.build(),
MeasurementsProtos.ProtoChannel.newBuilder()
.setType(majorType)
.setTimestamp(Math.toIntExact(tsInSec))
.addAllSampleOffsets(majorSampleOffsets)
.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(propertyPrefix + "_total_value").getAsDouble()).isEqualTo(expectedValue);
checkDefaultMeasurements(measurements, efentoMeasurements, 180, false);
}
private static Stream<Arguments> checkPulseCounterSensors() {
return Stream.of(
Arguments.of(MEASUREMENT_TYPE_WATER_METER_ACC_MINOR, List.of(125), MEASUREMENT_TYPE_WATER_METER_ACC_MAJOR,
List.of(2500), "water_cnt_acc", 62520.0),
Arguments.of(MEASUREMENT_TYPE_PULSE_CNT_ACC_MINOR, List.of(180), MEASUREMENT_TYPE_PULSE_CNT_ACC_MAJOR,
List.of(1200), "pulse_cnt_acc", 300030.0),
Arguments.of(MEASUREMENT_TYPE_ELEC_METER_ACC_MINOR, List.of(550), MEASUREMENT_TYPE_ELEC_METER_ACC_MAJOR,
List.of(5500), "elec_meter_acc", 1375091.0),
Arguments.of(MEASUREMENT_TYPE_PULSE_CNT_ACC_WIDE_MINOR, List.of(230), MEASUREMENT_TYPE_PULSE_CNT_ACC_WIDE_MAJOR,
List.of(1700), "pulse_cnt_acc_wide", 425000038.0));
}
@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;
}
}

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();
}

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());
}
}
}

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

@ -205,7 +205,7 @@ public class DefaultGitRepositoryService implements GitRepositoryService {
if (!Files.exists(Path.of(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,13 +280,16 @@ 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)) {
repository = GitRepository.open(repositoryDirectory.toFile(), settings);
if (fetch) {
repository.fetch();
}
} else {
Files.createDirectories(repositoryDirectory);
if (settings.isLocalOnly()) {

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;
}

5
dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java

@ -385,6 +385,11 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
return dashboardDao.findByTenantIdAndTitle(tenantId.getId(), title);
}
@Override
public boolean existsById(TenantId tenantId, DashboardId dashboardId) {
return dashboardDao.existsById(tenantId, dashboardId.getId());
}
private final PaginatedRemover<TenantId, DashboardId> tenantDashboardsRemover = new PaginatedRemover<>() {
@Override

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());

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() {

50
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());

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();

1
ui-ngx/package.json

@ -7,6 +7,7 @@
"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",
"build:icon-metadata": "node generate-icon-metadata.js",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",

16
ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts

@ -64,9 +64,13 @@ export interface AnalogueGaugeSettings {
animationRule: AnimationRule;
}
interface BaseGaugeModel extends BaseGauge {
_value?: number;
}
export abstract class TbBaseGauge<S, O extends GenericOptions> {
private gauge: BaseGauge;
private gauge: BaseGaugeModel;
protected constructor(protected ctx: WidgetContext, canvasId: string) {
const gaugeElement = $('#' + canvasId, ctx.$container)[0];
@ -77,16 +81,20 @@ export abstract class TbBaseGauge<S, O extends GenericOptions> {
protected abstract createGaugeOptions(gaugeElement: HTMLElement, settings: S): O;
protected abstract createGauge(gaugeData: O): BaseGauge;
protected abstract createGauge(gaugeData: O): BaseGaugeModel;
update() {
if (this.ctx.data.length > 0) {
const cellData = this.ctx.data[0];
if (cellData.data.length > 0) {
const tvPair = cellData.data[cellData.data.length -
1];
const tvPair = cellData.data[cellData.data.length - 1];
const value = parseFloat(tvPair[1]);
if (value !== this.gauge.value) {
if (!this.gauge.options.animation) {
this.gauge._value = value;
} else {
delete this.gauge._value;
}
this.gauge.value = value;
}
}

3
ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts

@ -26,7 +26,6 @@ import { prepareFontSettings } from '@home/components/widget/lib/settings.models
import { CanvasDigitalGauge, CanvasDigitalGaugeOptions } from '@home/components/widget/lib/canvas-digital-gauge';
import { DatePipe } from '@angular/common';
import { IWidgetSubscription } from '@core/api/widget-api.models';
import { Subscription } from 'rxjs';
import { ColorProcessor, createValueSubscription, ValueSourceType } from '@shared/models/widget-settings.models';
import GenericOptions = CanvasGauges.GenericOptions;
@ -260,6 +259,8 @@ export class TbCanvasDigitalGauge {
if (value !== this.gauge.value) {
if (!this.gauge.options.animation) {
this.gauge._value = value;
} else {
delete this.gauge._value;
}
this.gauge.value = value;
} else if (this.localSettings.showTimestamp && this.gauge.timestamp !== timestamp) {

22
ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/gateway-connector-version-processor.abstract.ts

@ -15,15 +15,17 @@
///
import { GatewayConnector, GatewayVersion } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { isNumber, isString } from '@core/utils';
import {
GatewayConnectorVersionMappingUtil
} from '@home/components/widget/lib/gateway/utils/gateway-connector-version-mapping.util';
export abstract class GatewayConnectorVersionProcessor<BasicConfig> {
gatewayVersion: number;
configVersion: number;
protected constructor(protected gatewayVersionIn: string | number, protected connector: GatewayConnector<BasicConfig>) {
this.gatewayVersion = this.parseVersion(this.gatewayVersionIn);
this.configVersion = this.parseVersion(this.connector.configVersion);
this.gatewayVersion = GatewayConnectorVersionMappingUtil.parseVersion(this.gatewayVersionIn);
this.configVersion = GatewayConnectorVersionMappingUtil.parseVersion(this.connector.configVersion);
}
getProcessedByVersion(): GatewayConnector<BasicConfig> {
@ -53,19 +55,13 @@ export abstract class GatewayConnectorVersionProcessor<BasicConfig> {
}
private isVersionUpgradeNeeded(): boolean {
return this.gatewayVersionIn === GatewayVersion.Current && (!this.configVersion || this.configVersion < this.gatewayVersion);
return this.gatewayVersion >= GatewayConnectorVersionMappingUtil.parseVersion(GatewayVersion.Current)
&& (!this.configVersion || this.configVersion < this.gatewayVersion);
}
private isVersionDowngradeNeeded(): boolean {
return this.configVersion && this.connector.configVersion === GatewayVersion.Current && (this.configVersion > this.gatewayVersion);
}
private parseVersion(version: string | number): number {
if (isNumber(version)) {
return version as number;
}
return isString(version) ? parseFloat((version as string).replace(/\./g, '').slice(0, 3)) / 100 : 0;
return this.configVersion && this.configVersion >= GatewayConnectorVersionMappingUtil.parseVersion(GatewayVersion.Current)
&& (this.configVersion > this.gatewayVersion);
}
protected abstract getDowngradedVersion(): GatewayConnector<BasicConfig>;

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.html

@ -18,7 +18,7 @@
<div [formGroup]="gatewayConfigGroup" class="gateway-config-container">
<div class="content-wrapper">
<mat-toolbar color="primary" [class.page-header]="!dialogRef">
<div class="tb-flex space-between">
<div class="tb-flex space-between align-center">
<h2 translate>gateway.gateway-configuration</h2>
<div class="toolbar-actions">
<tb-toggle-select [class.dialog-toggle]="!!dialogRef" formControlName="mode" appearance="{{dialogRef ? 'stroked' : 'fill'}}">

1
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html

@ -223,6 +223,7 @@
</div>
<tb-report-strategy
*ngIf="withReportStrategy"
[defaultValue]="ReportStrategyDefaultValue.Key"
formControlName="reportStrategy"
[isExpansionMode]="true"
/>

5
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts

@ -28,7 +28,8 @@ import {
import { TbPopoverComponent } from '@shared/components/popover.component';
import {
ModbusDataType,
ModbusEditableDataTypes, ModbusFormValue,
ModbusEditableDataTypes,
ModbusFormValue,
ModbusFunctionCodeTranslationsMap,
ModbusObjectCountByDataType,
ModbusValue,
@ -37,6 +38,7 @@ import {
ModifierTypesMap,
noLeadTrailSpacesRegex,
nonZeroFloat,
ReportStrategyDefaultValue,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
@ -91,6 +93,7 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
readonly ModbusEditableDataTypes = ModbusEditableDataTypes;
readonly ModbusFunctionCodeTranslationsMap = ModbusFunctionCodeTranslationsMap;
readonly ModifierTypesMap = ModifierTypesMap;
readonly ReportStrategyDefaultValue = ReportStrategyDefaultValue;
private destroy$ = new Subject<void>();

14
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html

@ -62,6 +62,14 @@
</mat-toolbar>
<div class="table-container">
<table mat-table [dataSource]="dataSource">
<ng-container [matColumnDef]="'deviceName'">
<mat-header-cell *matHeaderCellDef class="table-value-column">
<div tbTruncateWithTooltip>{{ 'gateway.device-name' | translate }}</div>
</mat-header-cell>
<mat-cell *matCellDef="let slave" class="table-value-column">
<div tbTruncateWithTooltip>{{ slave['deviceName'] }}</div>
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="'info'">
<mat-header-cell *matHeaderCellDef class="table-value-column">
{{ 'gateway.info' | translate }}
@ -80,7 +88,7 @@
</ng-container>
<ng-container [matColumnDef]="'type'">
<mat-header-cell *matHeaderCellDef class="table-value-column">
<div tbTruncateWithTooltip>{{ 'gateway.client-communication-type' | translate }}</div>
<div>{{ 'gateway.type' | translate }}</div>
</mat-header-cell>
<mat-cell *matCellDef="let slave" class="table-value-column">
{{ ModbusProtocolLabelsMap.get(slave['type']) }}
@ -121,8 +129,8 @@
</div>
</mat-cell>
</ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="['info', 'unitId', 'type', 'actions']; sticky: true"></mat-header-row>
<mat-row *matRowDef="let slave; columns: ['info', 'unitId', 'type', 'actions']"></mat-row>
<mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="['deviceName', 'info', 'unitId', 'type', 'actions']; sticky: true"></mat-header-row>
<mat-row *matRowDef="let slave; columns: ['deviceName', 'info', 'unitId', 'type', 'actions']"></mat-row>
</table>
<section [fxShow]="!textSearchMode && (dataSource.isEmpty() | async)" fxLayoutAlign="center center"
class="mat-headline-5 tb-absolute-fill tb-add-new">

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html

@ -175,7 +175,7 @@
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="sendDataToThingsBoard">
<mat-label>
{{ 'gateway.send-data-TB' | translate }}
{{ 'gateway.send-data-to-platform' | translate }}
</mat-label>
</mat-slide-toggle>
</div>

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.abstract.ts

@ -35,6 +35,7 @@ import {
ModbusSlaveInfo,
noLeadTrailSpacesRegex,
PortLimits,
ReportStrategyDefaultValue,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { Subject } from 'rxjs';
import { DialogComponent } from '@shared/components/dialog.component';
@ -64,6 +65,7 @@ export abstract class ModbusSlaveDialogAbstract<Component, Config> extends Dialo
readonly ModbusParityLabelsMap = ModbusParityLabelsMap;
readonly ModbusProtocolLabelsMap = ModbusProtocolLabelsMap;
readonly ModbusMethodLabelsMap = ModbusMethodLabelsMap;
readonly ReportStrategyDefaultValue = ReportStrategyDefaultValue;
readonly modbusHelpLink =
helpBaseUrl + '/docs/iot-gateway/config/modbus/#section-master-description-and-configuration-parameters';

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html

@ -218,7 +218,7 @@
</mat-slide-toggle>
</div>
<ng-template #reportStrategy>
<tb-report-strategy formControlName="reportStrategy" [isExpansionMode]="true"/>
<tb-report-strategy [defaultValue]="ReportStrategyDefaultValue.Device" formControlName="reportStrategy" [isExpansionMode]="true"/>
</ng-template>
<div class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings">

10
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/report-strategy/report-strategy.component.ts

@ -34,6 +34,7 @@ import {
} from '@angular/forms';
import {
ReportStrategyConfig,
ReportStrategyDefaultValue,
ReportStrategyType,
ReportStrategyTypeTranslationsMap
} from '@home/components/widget/lib/gateway/gateway-widget.models';
@ -43,7 +44,7 @@ import { CommonModule } from '@angular/common';
import {
ModbusSecurityConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component';
import { coerceBoolean } from '@shared/decorators/coercion';
import { coerceBoolean, coerceNumber } from '@shared/decorators/coercion';
@Component({
selector: 'tb-report-strategy',
@ -73,6 +74,9 @@ export class ReportStrategyComponent implements ControlValueAccessor, OnDestroy
@coerceBoolean()
@Input() isExpansionMode = false;
@coerceNumber()
@Input() defaultValue = ReportStrategyDefaultValue.Key;
reportStrategyFormGroup: UntypedFormGroup;
showStrategyControl: FormControl<boolean>;
@ -90,7 +94,7 @@ export class ReportStrategyComponent implements ControlValueAccessor, OnDestroy
this.reportStrategyFormGroup = this.fb.group({
type: [{ value: ReportStrategyType.OnReportPeriod, disabled: true }, []],
reportPeriod: [{ value: 5000, disabled: true }, [Validators.required]],
reportPeriod: [{ value: this.defaultValue, disabled: true }, [Validators.required]],
});
this.observeStrategyFormChange();
@ -109,7 +113,7 @@ export class ReportStrategyComponent implements ControlValueAccessor, OnDestroy
if (reportStrategyConfig) {
this.reportStrategyFormGroup.enable({emitEvent: false});
}
const { type = ReportStrategyType.OnReportPeriod, reportPeriod = 5000 } = reportStrategyConfig ?? {};
const { type = ReportStrategyType.OnReportPeriod, reportPeriod = this.defaultValue } = reportStrategyConfig ?? {};
this.reportStrategyFormGroup.setValue({ type, reportPeriod }, {emitEvent: false});
this.onTypeChange(type);
}

1
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts

@ -126,6 +126,7 @@ export class SecurityConfigComponent implements ControlValueAccessor, OnInit, On
if (!securityInfo.type) {
securityInfo.type = SecurityType.ANONYMOUS;
}
this.updateValidators(securityInfo.type);
this.securityFormGroup.reset(securityInfo, {emitEvent: false});
}
this.cdr.markForCheck();

8
ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts

@ -37,6 +37,7 @@ import { Observable, Subject } from 'rxjs';
import { ResourcesService } from '@core/services/resources.service';
import { takeUntil, tap } from 'rxjs/operators';
import { helpBaseUrl } from '@shared/models/constants';
import { LatestVersionConfigPipe } from '@home/components/widget/lib/gateway/pipes/latest-version-config.pipe';
@Component({
selector: 'tb-add-connector-dialog',
@ -63,6 +64,7 @@ export class AddConnectorDialogComponent
@Inject(MAT_DIALOG_DATA) public data: AddConnectorConfigData,
public dialogRef: MatDialogRef<AddConnectorDialogComponent, CreatedConnectorConfigData>,
private fb: FormBuilder,
private isLatestVersionConfig: LatestVersionConfigPipe,
private resourcesService: ResourcesService) {
super(store, router, dialogRef);
this.connectorForm = this.fb.group({
@ -103,9 +105,9 @@ export class AddConnectorDialogComponent
if (gatewayVersion) {
value.configVersion = gatewayVersion;
}
value.configurationJson = (gatewayVersion === GatewayVersion.Current
? defaultConfig[this.data.gatewayVersion]
: defaultConfig.legacy)
value.configurationJson = (this.isLatestVersionConfig.transform(gatewayVersion)
? defaultConfig[GatewayVersion.Current]
: defaultConfig[GatewayVersion.Legacy])
?? defaultConfig;
if (this.connectorForm.valid) {
this.dialogRef.close(value);

15
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html

@ -23,7 +23,7 @@
<button *ngIf="dataSource?.data?.length"
mat-icon-button
[disabled]="isLoading$ | async"
(click)="addConnector($event)"
(click)="onAddConnector($event)"
matTooltip="{{ 'action.add' | translate }}"
matTooltipPosition="above">
<mat-icon>add</mat-icon>
@ -33,7 +33,7 @@
<section *ngIf="!dataSource?.data?.length" fxLayoutAlign="center center"
class="mat-headline-5 tb-absolute-fill tb-add-new">
<button mat-button class="connector"
(click)="addConnector($event)">
(click)="onAddConnector($event)">
<mat-icon class="tb-mat-96">add</mat-icon>
<span>{{ 'gateway.add-connector' | translate }}</span>
</button>
@ -178,7 +178,7 @@
<ng-container [ngSwitch]="initialConnector.type">
<ng-container *ngSwitchCase="ConnectorType.MQTT">
<tb-mqtt-basic-config
*ngIf="connectorForm.get('configVersion').value === GatewayVersion.Current else legacy"
*ngIf="connectorForm.get('configVersion').value | isLatestVersionConfig else legacy"
formControlName="basicConfig"
[generalTabContent]="generalTabContent"
(initialized)="basicConfigInitSubject.next()"
@ -193,7 +193,7 @@
</ng-container>
<ng-container *ngSwitchCase="ConnectorType.OPCUA">
<tb-opc-ua-basic-config
*ngIf="connectorForm.get('configVersion').value === GatewayVersion.Current else legacy"
*ngIf="connectorForm.get('configVersion').value | isLatestVersionConfig else legacy"
formControlName="basicConfig"
[generalTabContent]="generalTabContent"
(initialized)="basicConfigInitSubject.next()"
@ -208,7 +208,7 @@
</ng-container>
<ng-container *ngSwitchCase="ConnectorType.MODBUS">
<tb-modbus-basic-config
*ngIf="connectorForm.get('configVersion').value === GatewayVersion.Current else legacy"
*ngIf="connectorForm.get('configVersion').value | isLatestVersionConfig else legacy"
formControlName="basicConfig"
[generalTabContent]="generalTabContent"
(initialized)="basicConfigInitSubject.next()"
@ -244,7 +244,7 @@
<button mat-raised-button color="primary"
type="button"
[disabled]="!connectorForm.dirty || connectorForm.invalid"
(click)="saveConnector(false)">
(click)="onSaveConnector()">
{{ 'action.save' | translate }}
</button>
</div>
@ -315,7 +315,8 @@
</mat-slide-toggle>
</div>
<tb-report-strategy
*ngIf="connectorForm.get('type').value === ConnectorType.MODBUS && connectorForm.get('configVersion').value === GatewayVersion.Current"
[defaultValue]="ReportStrategyDefaultValue.Connector"
*ngIf="connectorForm.get('type').value === ConnectorType.MODBUS && (connectorForm.get('configVersion').value | isLatestVersionConfig)"
formControlName="reportStrategy"
/>
</section>

150
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts

@ -42,7 +42,7 @@ import { MatTableDataSource } from '@angular/material/table';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { DialogService } from '@core/services/dialog.service';
import { WidgetContext } from '@home/models/widget-component.models';
import { camelCase, deepClone, generateSecret, isEqual, isString } from '@core/utils';
import { camelCase, deepClone, isEqual, isString } from '@core/utils';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { DatasourceType, widgetType } from '@shared/models/widget.models';
@ -59,7 +59,8 @@ import {
GatewayConnectorDefaultTypesTranslatesMap,
GatewayLogLevel,
noLeadTrailSpacesRegex,
GatewayVersion,
ReportStrategyDefaultValue,
ReportStrategyType,
} from './gateway-widget.models';
import { MatDialog } from '@angular/material/dialog';
import { AddConnectorDialogComponent } from '@home/components/widget/lib/gateway/dialog/add-connector-dialog.component';
@ -69,6 +70,7 @@ import { PageData } from '@shared/models/page/page-data';
import {
GatewayConnectorVersionMappingUtil
} from '@home/components/widget/lib/gateway/utils/gateway-connector-version-mapping.util';
import { LatestVersionConfigPipe } from '@home/components/widget/lib/gateway/pipes/latest-version-config.pipe';
export class ForceErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null): boolean {
@ -102,7 +104,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
readonly displayedColumns = ['enabled', 'key', 'type', 'syncStatus', 'errors', 'actions'];
readonly GatewayConnectorTypesTranslatesMap = GatewayConnectorDefaultTypesTranslatesMap;
readonly ConnectorConfigurationModes = ConfigurationModes;
readonly GatewayVersion = GatewayVersion;
readonly ReportStrategyDefaultValue = ReportStrategyDefaultValue;
pageLink: PageLink;
dataSource: MatTableDataSource<GatewayAttributeData>;
@ -146,6 +148,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
private telemetryWsService: TelemetryWebsocketService,
private zone: NgZone,
private utils: UtilsService,
private isLatestVersionConfig: LatestVersionConfigPipe,
private cd: ChangeDetectorRef) {
super(store);
@ -170,18 +173,21 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
super.ngOnDestroy();
}
saveConnector(isNew = true): void {
const value = this.getConnectorData();
onSaveConnector(): void {
this.saveConnector(this.getUpdatedConnectorData(this.connectorForm.value), false);
}
private saveConnector(connector: GatewayConnector, isNew = true): void {
const scope = (isNew || this.activeConnectors.includes(this.initialConnector.name))
? AttributeScope.SHARED_SCOPE
: AttributeScope.SERVER_SCOPE;
forkJoin(this.getEntityAttributeTasks(value, scope)).pipe(take(1)).subscribe(_ => {
this.showToast(!this.initialConnector
? this.translate.instant('gateway.connector-created')
: this.translate.instant('gateway.connector-updated')
forkJoin(this.getEntityAttributeTasks(connector, scope)).pipe(take(1)).subscribe(_ => {
this.showToast(isNew
? this.translate.instant('gateway.connector-created')
: this.translate.instant('gateway.connector-updated')
);
this.initialConnector = value;
this.initialConnector = connector;
this.updateData(true);
this.connectorForm.markAsPristine();
});
@ -237,8 +243,8 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}
}
private getConnectorData(): GatewayConnector {
const value = { ...this.connectorForm.value };
private getUpdatedConnectorData(connector: GatewayConnector): GatewayConnector {
const value = {...connector };
value.configuration = `${camelCase(value.name)}.json`;
delete value.basicConfig;
@ -249,6 +255,16 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
delete value.class;
}
if (value.type === ConnectorType.MODBUS && this.isLatestVersionConfig.transform(value.configVersion)) {
if (!value.reportStrategy) {
value.reportStrategy = {
type: ReportStrategyType.OnReportPeriod,
reportPeriod: ReportStrategyDefaultValue.Connector
};
delete value.sendDataOnlyOnChange;
}
}
if (this.gatewayVersion && !value.configVersion) {
value.configVersion = this.gatewayVersion;
}
@ -472,7 +488,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
return (connector && this.activeConnectors.includes(connectorName)) ? (connector.data[0][1] || 0) : 'Inactive';
}
addConnector(event?: Event): void {
onAddConnector(event?: Event): void {
event?.stopPropagation();
this.confirmConnectorChange()
@ -482,40 +498,58 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
switchMap(() => this.openAddConnectorDialog()),
filter(Boolean),
)
.subscribe(value => {
if (this.connectorForm.disabled) {
this.connectorForm.enable();
}
if (!value.configurationJson) {
value.configurationJson = {} as ConnectorBaseConfig;
}
value.basicConfig = value.configurationJson;
this.initialConnector = value;
this.connectorForm.patchValue(value, {emitEvent: false});
this.generate('basicConfig.broker.clientId');
if (this.connectorForm.get('type').value === value.type || !this.allowBasicConfig.has(value.type)) {
this.saveConnector();
} else {
this.basicConfigInitSubject.pipe(take(1)).subscribe(() => {
this.saveConnector();
});
}
});
.subscribe(connector => this.addConnector(connector));
}
private openAddConnectorDialog(): Observable<GatewayConnector> {
return this.dialog.open<AddConnectorDialogComponent, AddConnectorConfigData>(AddConnectorDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
dataSourceData: this.dataSource.data,
gatewayVersion: this.gatewayVersion,
}
}).afterClosed();
private addConnector(connector: GatewayConnector): void {
if (this.connectorForm.disabled) {
this.connectorForm.enable();
}
if (!connector.configurationJson) {
connector.configurationJson = {} as ConnectorBaseConfig;
}
if (this.gatewayVersion && !connector.configVersion) {
connector.configVersion = this.gatewayVersion;
}
connector.basicConfig = connector.configurationJson;
this.initialConnector = connector;
const previousType = this.connectorForm.get('type').value;
this.setInitialConnectorValues(connector);
this.saveConnector(this.getUpdatedConnectorData(connector));
if (previousType === connector.type || !this.allowBasicConfig.has(connector.type)) {
this.patchBasicConfigConnector(connector);
} else {
this.basicConfigInitSubject.pipe(take(1)).subscribe(() => {
this.patchBasicConfigConnector(connector);
});
}
}
private setInitialConnectorValues(connector: GatewayConnector): void {
const {basicConfig, mode, ...initialConnector} = connector;
this.toggleReportStrategy(connector.type);
this.connectorForm.get('mode').setValue(this.allowBasicConfig.has(connector.type)
? connector.mode ?? ConfigurationModes.BASIC
: null, {emitEvent: false}
);
this.connectorForm.patchValue(initialConnector, {emitEvent: false});
}
generate(formControlName: string): void {
this.connectorForm.get(formControlName)?.patchValue('tb_gw_' + generateSecret(5));
private openAddConnectorDialog(): Observable<GatewayConnector> {
return this.ctx.ngZone.run(() =>
this.dialog.open<AddConnectorDialogComponent, AddConnectorConfigData>(AddConnectorDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
dataSourceData: this.dataSource.data,
gatewayVersion: this.gatewayVersion,
}
}).afterClosed()
);
}
uniqNameRequired(): ValidatorFn {
@ -607,7 +641,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
const active = attributes.find(data => data.key === 'active').value;
const lastDisconnectedTime = attributes.find(data => data.key === 'lastDisconnectTime')?.value;
const lastConnectedTime = attributes.find(data => data.key === 'lastConnectTime').value;
const lastConnectedTime = attributes.find(data => data.key === 'lastConnectTime')?.value;
this.isGatewayActive = this.getGatewayStatus(active, lastConnectedTime, lastDisconnectedTime);
});
@ -621,13 +655,8 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
private observeModeChange(): void {
this.connectorForm.get('mode').valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe((mode) => {
.subscribe(() => {
this.connectorForm.get('mode').markAsPristine();
if (mode === ConfigurationModes.BASIC) {
this.basicConfigInitSubject.pipe(take(1)).subscribe(() => {
this.patchBasicConfigConnector({...this.initialConnector, mode: ConfigurationModes.BASIC});
});
}
});
}
@ -798,13 +827,17 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
...connector,
}, this.gatewayVersion);
if (this.gatewayVersion && !connectorState.configVersion) {
connectorState.configVersion = this.gatewayVersion;
}
connectorState.basicConfig = connectorState.configurationJson;
this.initialConnector = connectorState;
this.updateConnector(connectorState);
}
private updateConnector(connector: GatewayConnector): void {
this.toggleReportStrategy(connector.type);
this.jsonConfigSub?.unsubscribe();
switch (connector.type) {
case ConnectorType.MQTT:
case ConnectorType.OPCUA:
@ -814,19 +847,21 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
default:
this.connectorForm.patchValue({...connector, mode: null});
this.connectorForm.markAsPristine();
this.createJsonConfigWatcher();
}
this.createJsonConfigWatcher();
}
private updateBasicConfigConnector(connector: GatewayConnector): void {
this.connectorForm.get('mode').setValue(connector.mode || ConfigurationModes.BASIC, {emitEvent: false});
this.connectorForm.get('configVersion').setValue(connector.configVersion, {emitEvent: false});
if ((!connector.mode || connector.mode === ConfigurationModes.BASIC) && this.connectorForm.get('type').value !== connector.type) {
this.basicConfigSub?.unsubscribe();
const previousType = this.connectorForm.get('type').value;
this.setInitialConnectorValues(connector);
if (previousType === connector.type || !this.allowBasicConfig.has(connector.type)) {
this.patchBasicConfigConnector(connector);
} else {
this.basicConfigInitSubject.asObservable().pipe(take(1)).subscribe(() => {
this.patchBasicConfigConnector(connector);
});
} else {
this.patchBasicConfigConnector(connector);
}
}
@ -834,6 +869,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.connectorForm.patchValue(connector, {emitEvent: false});
this.connectorForm.markAsPristine();
this.createBasicConfigWatcher();
this.createJsonConfigWatcher();
}
private toggleReportStrategy(type: ConnectorType): void {

9
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts

@ -123,6 +123,9 @@ export interface GatewayConnectorBase {
class?: string;
mode?: ConfigurationModes;
configVersion?: string;
reportStrategy?: ReportStrategyConfig;
sendDataOnlyOnChange?: boolean;
ts?: number;
}
export interface GatewayConnector<BaseConfig = ConnectorBaseConfig> extends GatewayConnectorBase {
@ -645,6 +648,12 @@ export enum ReportStrategyType {
OnChangeOrReportPeriod = 'ON_CHANGE_OR_REPORT_PERIOD'
}
export enum ReportStrategyDefaultValue {
Connector = 60000,
Device = 30000,
Key = 15000
}
export const ReportStrategyTypeTranslationsMap = new Map<ReportStrategyType, string>(
[
[ReportStrategyType.OnChange, 'gateway.report-strategy.on-change'],

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/gateway-help-link.pipe.ts

@ -33,6 +33,8 @@ export class GatewayHelpLinkPipe implements PipeTransform {
} else {
return;
}
} else if (field === 'attributes' || field === 'timeseries') {
return 'widget/lib/gateway/attributes_timeseries_expressions_fn';
}
return 'widget/lib/gateway/expressions_fn';
}

32
ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/latest-version-config.pipe.ts

@ -0,0 +1,32 @@
///
/// 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 { Pipe, PipeTransform } from '@angular/core';
import { GatewayVersion } from '@home/components/widget/lib/gateway/gateway-widget.models';
import {
GatewayConnectorVersionMappingUtil
} from '@home/components/widget/lib/gateway/utils/gateway-connector-version-mapping.util';
@Pipe({
name: 'isLatestVersionConfig',
standalone: true,
})
export class LatestVersionConfigPipe implements PipeTransform {
transform(configVersion: number | string): boolean {
return GatewayConnectorVersionMappingUtil.parseVersion(configVersion)
>= GatewayConnectorVersionMappingUtil.parseVersion(GatewayVersion.Current);
}
}

9
ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/gateway-connector-version-mapping.util.ts

@ -24,6 +24,7 @@ import {
import { MqttVersionProcessor } from '@home/components/widget/lib/gateway/abstract/mqtt-version-processor.abstract';
import { OpcVersionProcessor } from '@home/components/widget/lib/gateway/abstract/opc-version-processor.abstract';
import { ModbusVersionProcessor } from '@home/components/widget/lib/gateway/abstract/modbus-version-processor.abstract';
import { isNumber, isString } from '@core/utils';
export abstract class GatewayConnectorVersionMappingUtil {
@ -39,4 +40,12 @@ export abstract class GatewayConnectorVersionMappingUtil {
return connector;
}
}
static parseVersion(version: string | number): number {
if (isNumber(version)) {
return version as number;
}
return isString(version) ? parseFloat((version as string).replace(/\./g, '').slice(0, 3)) / 100 : 0;
}
}

10
ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/opc-version-mapping.util.ts

@ -56,7 +56,7 @@ export class OpcVersionMappingUtil {
static mapMappingToUpgradedVersion(mapping: LegacyDeviceConnectorMapping[]): DeviceConnectorMapping[] {
return mapping.map((legacyMapping: LegacyDeviceConnectorMapping) => ({
...legacyMapping,
deviceNodeSource: this.getTypeSourceByValue(legacyMapping.deviceNodePattern),
deviceNodeSource: this.getDeviceNodeSourceByValue(legacyMapping.deviceNodePattern),
deviceInfo: {
deviceNameExpression: legacyMapping.deviceNamePattern,
deviceNameExpressionSource: this.getTypeSourceByValue(legacyMapping.deviceNamePattern),
@ -122,6 +122,14 @@ export class OpcVersionMappingUtil {
return OPCUaSourceType.CONST;
}
private static getDeviceNodeSourceByValue(value: string): OPCUaSourceType {
if (value.includes('${')) {
return OPCUaSourceType.IDENTIFIER;
} else {
return OPCUaSourceType.PATH;
}
}
private static getArgumentType(arg: unknown): string {
switch (typeof arg) {
case 'boolean':

16
ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts

@ -1168,6 +1168,22 @@ class CssScadaSymbolAnimation implements ScadaSymbolAnimation {
private element: Element,
duration = 1000) {
this._duration = duration;
this.fixPatternAnimationForChromeBelow128();
}
private fixPatternAnimationForChromeBelow128(): void {
try {
const userAgent = window.navigator.userAgent;
if (+(/Chrome\/(\d+)/i.exec(userAgent)[1]) <= 127) {
if (this.svgShape.defs().findOne('pattern') && !this.svgShape.defs().findOne('pattern.empty-animation')) {
this.svgShape.defs().add(SVG('<pattern class="empty-animation"></pattern>'));
this.svgShape.style()
.rule('.' + 'empty-animation',
{'animation-name': 'empty-animation', 'animation-duration': '1000ms', 'animation-iteration-count': 'infinite'})
.addText('@keyframes empty-animation {0% {<!--opacity:1;-->}100% {<!--opacity:1;-->}}');
}
}
} catch (e) {}
}
public running(): boolean {

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

@ -167,9 +167,10 @@ import {
ModbusRpcParametersComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/rpc-parameters/modbus-rpc-parameters/modbus-rpc-parameters.component';
import { RpcTemplateArrayViewPipe } from '@home/components/widget/lib/gateway/pipes/rpc-template-array-view.pipe';
import {
import {
ReportStrategyComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/report-strategy/report-strategy.component';
import { LatestVersionConfigPipe } from '@home/components/widget/lib/gateway/pipes/latest-version-config.pipe';
@NgModule({
declarations: [
@ -268,6 +269,7 @@ import {
ModbusRpcParametersComponent,
RpcTemplateArrayViewPipe,
ReportStrategyComponent,
LatestVersionConfigPipe,
],
exports: [
EntitiesTableWidgetComponent,
@ -337,7 +339,8 @@ import {
ScadaSymbolWidgetComponent
],
providers: [
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule}
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule},
{provide: LatestVersionConfigPipe}
]
})
export class WidgetComponentsModule {

28
ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.html

@ -162,28 +162,12 @@
</mat-slide-toggle>
</div>
</section>
<div class="tb-form-row tb-standard-fields no-border no-padding column-xs">
<mat-form-field class="flex">
<mat-label translate>admin.oauth2.scope</mat-label>
<mat-chip-grid #scopeList required [disabled]="!this.createNewDialog && !(this.isEdit || this.isAdd)">
<mat-chip-row *ngFor="let scope of entityForm.get('scope').value; let k = index; trackBy: trackByParams"
removable (removed)="removeScope(k, entityForm)">
{{scope}}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip-row>
<input [matChipInputFor]="scopeList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matChipInputAddOnBlur
(matChipInputTokenEnd)="addScope($event, entityForm)">
</mat-chip-grid>
<mat-error *ngIf="entityForm.get('scope').hasError('required')">
{{ 'admin.oauth2.scope-required' | translate }}
</mat-error>
</mat-form-field>
</div>
<tb-error style="display: block; margin-top: -24px;"
[error]="entityForm.get('scope').hasError('required') ? ('admin.oauth2.scope-required' | translate) : ''">
</tb-error>
<tb-string-items-list
formControlName="scope"
label="{{ 'admin.oauth2.scope' | translate }}"
requiredText="{{ 'admin.oauth2.scope-required' | translate }}"
required>
</tb-string-items-list>
</section>
<section class="tb-form-panel no-border no-padding no-gap" *ngIf="!generalSettingsMode">
<div class="tb-form-row tb-standard-fields no-border no-padding">

70
ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.ts

@ -33,18 +33,10 @@ import { AppState } from '@core/core.state';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { TranslateService } from '@ngx-translate/core';
import { Store } from '@ngrx/store';
import {
AbstractControl,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormGroup,
ValidationErrors,
Validators
} from '@angular/forms';
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { isDefinedAndNotNull } from '@core/utils';
import { OAuth2Service } from '@core/http/oauth2.service';
import { Subscription } from 'rxjs';
import { MatChipInputEvent } from '@angular/material/chips';
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { PageLink } from '@shared/models/page/page-link';
import { coerceBoolean } from '@app/shared/decorators/coercion';
@ -84,16 +76,6 @@ export class ClientComponent extends EntityComponent<OAuth2Client, PageLink, OAu
private subscriptions: Array<Subscription> = [];
public static validateScope(control: AbstractControl): ValidationErrors | null {
const scope: string[] = control.value;
if (!scope || !scope.length) {
return {
required: true
};
}
return null;
}
readonly separatorKeysCodes: number[] = [ENTER, COMMA];
clientAuthenticationMethods = Object.keys(ClientAuthenticationMethod);
@ -146,7 +128,7 @@ export class ClientComponent extends EntityComponent<OAuth2Client, PageLink, OAu
loginButtonLabel: [entity?.loginButtonLabel ? entity.loginButtonLabel : null, Validators.required],
loginButtonIcon: [entity?.loginButtonIcon ? entity.loginButtonIcon : null],
userNameAttributeName: [entity?.userNameAttributeName ? entity.userNameAttributeName : 'email', Validators.required],
scope: this.fb.array(entity?.scope ? entity.scope : [], ClientComponent.validateScope),
scope: [entity?.scope ? entity.scope : [], Validators.required],
mapperConfig: this.fb.group({
allowUserCreation: [isDefinedAndNotNull(entity?.mapperConfig?.allowUserCreation) ?
entity.mapperConfig.allowUserCreation : true],
@ -166,6 +148,7 @@ export class ClientComponent extends EntityComponent<OAuth2Client, PageLink, OAu
clientId: entity.clientId,
clientSecret: entity.clientSecret,
accessTokenUri: entity.accessTokenUri,
scope: entity.scope,
authorizationUri: entity.authorizationUri,
jwkSetUri: entity.jwkSetUri,
userInfoUri: entity.userInfoUri,
@ -181,19 +164,6 @@ export class ClientComponent extends EntityComponent<OAuth2Client, PageLink, OAu
}, {emitEvent: false});
this.changeMapperConfigType(this.entityForm, this.entityValue.mapperConfig.type, this.entityValue.mapperConfig);
const scopeControls = this.entityForm.get('scope') as UntypedFormArray;
if (entity.scope.length === scopeControls.length) {
scopeControls.patchValue(entity.scope, {emitEvent: false});
} else {
const newScopeControls: Array<AbstractControl> = [];
if (entity.scope) {
for (const scope of entity.scope) {
newScopeControls.push(this.fb.control(scope, [Validators.required]));
}
}
this.entityForm.setControl('scope', this.fb.array(newScopeControls));
}
}
getProviderName(): string {
@ -204,35 +174,6 @@ export class ClientComponent extends EntityComponent<OAuth2Client, PageLink, OAu
return this.getProviderName() === 'Custom';
}
trackByParams(index: number): number {
return index;
}
trackByItem(i, item) {
return item;
}
addScope(event: MatChipInputEvent, control: AbstractControl): void {
const input = event.chipInput.inputElement;
const value = event.value;
const controller = control.get('scope') as UntypedFormArray;
if ((value.trim() !== '')) {
controller.push(this.fb.control(value.trim()));
controller.markAsDirty();
}
if (input) {
input.value = '';
}
}
removeScope(i: number, control: AbstractControl): void {
const controller = control.get('scope') as UntypedFormArray;
controller.removeAt(i);
controller.markAsTouched();
controller.markAsDirty();
}
private initTemplates(templates: OAuth2ClientRegistrationTemplate[]): void {
templates.map(provider => {
delete provider.additionalInfo;
@ -264,7 +205,7 @@ export class ClientComponent extends EntityComponent<OAuth2Client, PageLink, OAu
}));
this.subscriptions.push(this.entityForm.get('additionalInfo.providerName').valueChanges.subscribe((provider) => {
(this.entityForm.get('scope') as UntypedFormArray).clear();
this.entityForm.get('scope').setValue([]);
this.setProviderDefaultValue(provider, this.entityForm);
}));
}
@ -355,9 +296,6 @@ export class ClientComponent extends EntityComponent<OAuth2Client, PageLink, OAu
const template = this.templates.get(provider);
template.clientId = '';
template.clientSecret = '';
template.scope.forEach(() => {
(clientRegistration.get('scope') as UntypedFormArray).push(this.fb.control(''));
});
clientRegistration.patchValue(template);
}
}

2
ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts

@ -92,7 +92,7 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
override updateFormState(): void {
super.updateFormState();
if (this.isEdit && this.entityForm) {
if (this.isEdit && this.entityForm && !this.isAdd) {
this.entityForm.get('resourceType').disable({ emitEvent: false });
if (this.entityForm.get('resourceType').value !== ResourceType.JS_MODULE) {
this.entityForm.get('fileName').disable({ emitEvent: false });

15
ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts

@ -25,7 +25,9 @@ import { DeviceCredentials, DeviceProfileInfo, DeviceTransportType } from '@shar
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import { DeviceProfileService } from '@core/http/device-profile.service';
import { forkJoin } from 'rxjs';
import { forkJoin, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { HttpStatusCode } from '@angular/common/http';
export interface DeviceCredentialsDialogData {
isReadOnly: boolean;
@ -102,7 +104,16 @@ export class DeviceCredentialsDialogComponent extends
this.submitted = true;
const deviceCredentialsValue = this.deviceCredentialsFormGroup.value.credential;
this.deviceCredentials = {...this.deviceCredentials, ...deviceCredentialsValue};
this.deviceService.saveDeviceCredentials(this.deviceCredentials).subscribe(
this.deviceService.saveDeviceCredentials(this.deviceCredentials)
.pipe(
catchError((err) => {
if (err.status === HttpStatusCode.Conflict) {
return this.deviceService.getDeviceCredentials(this.deviceCredentials.deviceId.id);
}
return throwError(() => err);
})
)
.subscribe(
(deviceCredentials) => {
this.dialogRef.close(deviceCredentials);
}

5
ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html

@ -135,10 +135,11 @@
</a>
</section>
<mat-form-field class="mat-block">
<mat-label translate>notification.webhook-url</mat-label>
<mat-label>{{ (targetNotificationForm.get('configuration.useOldApi').value ?
'notification.webhook-url' : 'notification.workflow-url') | translate }}</mat-label>
<input matInput formControlName="webhookUrl">
<mat-error *ngIf="targetNotificationForm.get('configuration.webhookUrl').hasError('required')">
{{ 'notification.webhook-url-required' | translate }}
{{ (targetNotificationForm.get('configuration.useOldApi').value ? 'notification.webhook-url-required' : 'notification.workflow-url-required') | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">

21
ui-ngx/src/app/shared/components/time/datapoints-limit.component.scss

@ -25,6 +25,27 @@
mat-form-field input[type=number] {
text-align: center;
}
.mdc-slider {
height: 40px;
::ng-deep {
.mdc-slider__thumb {
left: -20px;
height: 40px;
width: 40px;
.mat-ripple-element.mat-mdc-slider-focus-ripple,
.mat-ripple-element.mat-mdc-slider-active-ripple,
.mat-ripple-element.mat-mdc-slider-hover-ripple {
left: 0 !important;
top: 0 !important;
height: 40px !important;
width: 40px !important;
}
}
}
}
}
@media #{$mat-gt-sm} {

36
ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.html

@ -54,7 +54,9 @@
<div class="tb-form-row">
<mat-slide-toggle *ngIf="!quickIntervalOnly"
class="mat-slide" formControlName="hideLastInterval">
{{ 'timewindow.hide' | translate }}
<div tb-hint-tooltip-icon="{{'timewindow.hide-last-interval' | translate}}">
{{ 'timewindow.hide' | translate }}
</div>
</mat-slide-toggle>
<tb-timeinterval
formControlName="timewindowMs"
@ -71,7 +73,9 @@
class="tb-form-row">
<mat-slide-toggle *ngIf="!quickIntervalOnly"
class="mat-slide" formControlName="hideQuickInterval">
{{ 'timewindow.hide' | translate }}
<div tb-hint-tooltip-icon="{{'timewindow.hide-relative-interval' | translate}}">
{{ 'timewindow.hide' | translate }}
</div>
</mat-slide-toggle>
<tb-quick-time-interval
displayLabel="false"
@ -105,7 +109,9 @@
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="hideLastInterval">
{{ 'timewindow.hide' | translate }}
<div tb-hint-tooltip-icon="{{'timewindow.hide-last-interval' | translate}}">
{{ 'timewindow.hide' | translate }}
</div>
</mat-slide-toggle>
<tb-timeinterval
formControlName="timewindowMs"
@ -120,7 +126,9 @@
<div *ngIf="timewindowForm.get('history.historyType').value === historyTypes.FIXED" class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="hideFixedInterval">
{{ 'timewindow.hide' | translate }}
<div tb-hint-tooltip-icon="{{'timewindow.hide-fixed-interval' | translate}}">
{{ 'timewindow.hide' | translate }}
</div>
</mat-slide-toggle>
<tb-datetime-period
formControlName="fixedTimewindow"
@ -134,7 +142,9 @@
<div *ngIf="timewindowForm.get('history.historyType').value === historyTypes.INTERVAL" class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="hideQuickInterval">
{{ 'timewindow.hide' | translate }}
<div tb-hint-tooltip-icon="{{'timewindow.hide-relative-interval' | translate}}">
{{ 'timewindow.hide' | translate }}
</div>
</mat-slide-toggle>
<tb-quick-time-interval
displayLabel="false"
@ -152,7 +162,9 @@
<div class="tb-form-panel-title">{{ 'aggregation.aggregation' | translate }}</div>
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide" formControlName="hideAggregation">
{{ 'timewindow.hide' | translate }}
<div tb-hint-tooltip-icon="{{'timewindow.hide-aggregation' | translate}}">
{{ 'timewindow.hide' | translate }}
</div>
</mat-slide-toggle>
<ng-container formGroupName="aggregation">
<mat-form-field class="flex" subscriptSizing="dynamic" appearance="outline">
@ -171,7 +183,9 @@
<div class="tb-form-panel-title">{{ 'aggregation.limit' | translate }}</div>
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide" formControlName="hideAggInterval">
{{ 'timewindow.hide' | translate }}
<div tb-hint-tooltip-icon="{{'timewindow.hide-max-values' | translate}}">
{{ 'timewindow.hide' | translate }}
</div>
</mat-slide-toggle>
<ng-container formGroupName="aggregation">
<tb-datapoints-limit formControlName="limit"
@ -229,7 +243,9 @@
<ng-template #hideAggInterval>
<mat-slide-toggle class="mat-slide" formControlName="hideAggInterval">
{{ 'timewindow.hide' | translate }}
<div tb-hint-tooltip-icon="{{'timewindow.hide-group-interval' | translate}}">
{{ 'timewindow.hide' | translate }}
</div>
</mat-slide-toggle>
</ng-template>
</section>
@ -239,7 +255,9 @@
<div class="tb-form-panel-title">{{ 'timezone.timezone' | translate }}</div>
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide" formControlName="hideTimezone">
{{ 'timewindow.hide' | translate }}
<div tb-hint-tooltip-icon="{{'timewindow.hide-timezone' | translate}}">
{{ 'timewindow.hide' | translate }}
</div>
</mat-slide-toggle>
<tb-timezone-select [localBrowserTimezonePlaceholderOnEmpty]="true"
formControlName="timezone"

2
ui-ngx/src/app/shared/components/time/timewindow-panel.component.html

@ -16,7 +16,7 @@
-->
<form [formGroup]="timewindowForm" class="tb-timewindow-form">
<div class="tb-timewindow-form-header tb-form-panel no-border no-padding-bottom no-gap" *ngIf="!historyOnly">
<div class="tb-timewindow-form-header tb-form-panel no-border no-padding-bottom" *ngIf="!historyOnly">
<tb-toggle-select class="tb-timewindow-form-type-options" appearance="fill"
[options]="timewindowTypeOptions" formControlName="selectedTab">
</tb-toggle-select>

11
ui-ngx/src/app/shared/models/icon.models.ts

@ -53,7 +53,16 @@ export const svgIcons: {[key: string]: string} = {
'<path fill="#fff" d="M9 4V2H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h5v-2H4V4h5z"/>' +
'<path fill="#fff" d="M7 18V6h2v12H7zM11 6v12h2V6h-2zM15 20v2h5a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2h-5v2h5v16h-5z"/>' +
'<path fill="#fff" d="M15 18V6h2v12h-2z"/>' +
'</svg>'
'</svg>',
trendz: '<svg viewBox="0 0 24 24"><path fill-rule="evenodd" clip-rule="evenodd" d="m 17.329936,0 1.999613,2.003952 -2.674056,' +
'2.679916 2.661746,2.6765649 2.678508,-2.684351 1.999613,2.0039449 -2.682055,2.6878227 2.661607,2.6763665 -2.641298,2.656033 ' +
'2.661746,2.66751 -1.999613,2.004017 -2.658338,-2.664181 -2.661607,2.676508 2.653887,2.659575 -1.999614,2.003804 L 14.679666,' +
'21.39152 11.997681,24.088432 9.3156944,21.39152 6.6653477,24.047482 4.6657413,22.043678 7.3194891,19.384103 4.6578333,16.707382 ' +
'1.9996133,19.371421 0,17.367405 2.6616488,14.700107 0.02053398,12.044216 2.6820275,9.3678495 1.4263538e-5,6.6800126 1.9996273,' +
'4.6760606 4.678212,7.3604329 7.3397982,4.6839955 4.6657413,2.0041717 6.6653477,2.2309572e-4 9.3360035,2.6766286 11.997681,' +
'0 14.659287,2.6765011 Z m -5.332255,4.0079963 1.999613,2.003945 -7.99844,8.0158157 -1.9996133,-2.004017 z m 1.676684,4.3522483 ' +
'1.999613,2.0039454 -6.6654242,6.679793 -1.9996133,-2.003874 z m 2.988987,7.0033574 -1.999544,-2.003945 -4.6658108,4.675848 ' +
'1.9996128,2.004015 z"/></svg>'
};
export const svgIconsUrl: { [key: string]: string } = {

8
ui-ngx/src/app/shared/models/widget-settings.models.ts

@ -329,11 +329,9 @@ export const resolveCssSize = (strSize?: string): [number, cssUnit] => {
}
let resolvedUnit: cssUnit;
let resolvedSize = strSize;
for (const unit of cssUnits) {
if (strSize.endsWith(unit)) {
resolvedUnit = unit;
break;
}
const unitMatch = strSize.match(new RegExp(`(${cssUnits.join('|')})$`));
if (unitMatch) {
resolvedUnit = unitMatch[0] as cssUnit;
}
if (resolvedUnit) {
resolvedSize = strSize.substring(0, strSize.length - resolvedUnit.length);

74
ui-ngx/src/assets/help/en_US/scada/symbol_state_render_fn.md

@ -20,12 +20,15 @@ A JavaScript function used to render SCADA symbol state.
##### Examples
**Handle volume buttons state and appearance**
This JavaScript snippet manages the enablement and appearance of SVG buttons used to control volume settings based on device connectivity and value thresholds.
The 'Up' button is disabled when the volume reaches its maximum allowable value (`maxValue`), and the 'Down' button is disabled when the volume is at its minimum allowable value (`minValue`).
The 'Up' button is disabled when the volume reaches its maximum allowable value `maxValue`, and the 'Down' button is disabled when the volume is at its minimum allowable value `minValue`.
Both buttons are disabled when the device is offline. The visual cues for enabled/disabled states are represented by color changes.
```javascript
<br>
```javascript
var levelUpButton = ctx.tags.levelUpButton; // Button for increasing volume
var levelDownButton = ctx.tags.levelDownButton; // Button for decreasing volume
var levelArrowUp = ctx.tags.levelArrowUp; // Visual arrow for 'Up' button
@ -61,3 +64,70 @@ if (levelDownEnabled) {
{:copy-code}
```
<br>
**Manage icon and label visibility and styling**
This JavaScript code snippet dynamically handles the visibility and styling of an icon and label within a SCADA symbol.
It checks the `showIcon` and `showLabel` properties from the context to determine whether to display the icon and/or label.
If the icon or label is shown, the script sets their respective properties like size, color, and position.
The script utilizes the <a href="${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/scada/scada-symbols-dev-guide/#scadasymbolapi" target="_blank">ScadaSymbolApi</a> to update the visual appearance of these elements based on the context properties.
<br>
This approach ensures that both the icon and label elements are dynamically shown or hidden based on context properties, with appropriate styling applied.
<br>
```javascript
// Retrieve the first icon and label elements from the context tags
var iconTag = ctx.tags.icon[0];
var labelTag = ctx.tags.label[0];
// Check whether the icon should be displayed, based on the context property
var showIcon = ctx.properties.showIcon;
var showLabel = ctx.properties.label;
if (showIcon) {
// Show the icon if the 'showIcon' property is true
iconTag.show();
// Set icon attributes such as icon type, size, and color
var icon = ctx.properties.icon;
var iconSize = ctx.properties.iconSize;
var iconColor = ctx.properties.iconColor;
// Use the ScadaSymbolApi to apply the icon, size, and color to the iconTag
ctx.api.icon(iconTag, icon, iconSize, iconColor, true);
// If the label is not shown, adjust the icon's position
if (!showLabel) {
iconTag.transform({translateX: 83, translateY: 137});
}
} else {
// Hide the icon if 'showIcon' is false
iconTag.hide();
}
if (showLabel) {
// Show the label if the 'showLabel' property is true
var labelTextFont = ctx.properties.labelTextFont;
var labelTextColor = ctx.properties.labelTextColor;
// Apply font and color to the label using the ScadaSymbolApi
ctx.api.font(labelTag, labelTextFont, labelTextColor);
// Set the text content of the label
ctx.api.text(labelTag, ctx.properties.labelText);
// If the icon is not shown, adjust the label's position
if (!showIcon) {
labelTag.transform({translateX: 10});
}
// Show the label
labelTag.show();
} else {
// Hide the label if 'showLabel' is false
labelTag.hide();
}
{:copy-code}
```

144
ui-ngx/src/assets/help/en_US/scada/tag_click_action_fn.md

@ -21,7 +21,34 @@ A JavaScript function invoked when user clicks on SVG element with specific tag.
<div class="divider"></div>
##### Examples
##### Examples without setting values for callAction function
**Invoke widget click action**
<br>
This JavaScript snippet demonstrates triggering a <a href="${siteBaseUrl}/docs${docPlatformPrefix}/user-guide//ui/widget-actions/#action-types" target="_blank">widget action</a> using the <a href="${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/scada/scada-symbols-dev-guide/#scadasymbolcontext" target="_blank">ScadaSymbolContext API</a> when the click event occurs. The widget action will be linked to the behaviorId 'click', which defines the action that will be executed upon the event.
The behavior of this action depends on the type of widget action configured in the ThingsBoard platform (e.g., navigating to a dashboard state, updating the current state, opening a URL, etc.).
<br>
```javascript
ctx.api.callAction(event, 'click'); // Trigger widget action 'click' on event
{:copy-code}
```
<br>
This action is executed automatically upon the 'click' event, making it useful for scenarios where the user interacts with a widget by clicking on it.
*Example Use Case*
- **Navigate to Dashboard State:** If the 'click' action is configured to navigate to a new dashboard state, the user will be redirected to that state when clicking on the widget.
- **Open URL:** If configured to open a URL, clicking on the widget will take the user to a specified web resource.
<br>
**Handle device activation toggle**
The example demonstrates how to dynamically call the 'turnOn' or 'turnOff' actions based on the 'active' status from the context. The actions are implemented using the following methods from the <a href="${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/scada/scada-symbols-dev-guide/#scadasymbolapi" target="_blank">Scada Symbol API</a>:
@ -34,10 +61,9 @@ For more detailed guidelines on device interaction, consider reviewing the <a hr
```javascript
var active = ctx.values.active; // Current active status from context
var action = active ? 'turnOn' : 'turnOff'; // Determine action based on active status
var parameter = "any object or primitive"; // Parameter to pass with the action
// Call the action with observer callbacks for next and error handling
ctx.api.callAction(event, action, parameter, {
ctx.api.callAction(event, action, undefined, {
next: () => {
// Action succeeded; toggle the 'activate' status for debugging
ctx.api.setValue('activate', !active);
@ -47,4 +73,116 @@ ctx.api.callAction(event, action, parameter, {
ctx.api.setValue('activate', active);
}
});
{:copy-code}
```
<br>
This example utilizes two specific action behaviors, `turnOn` and `turnOff`, which interact with the target device based on the `active` status from the context:
1. **turnOn Behavior**
* **Type**: Action
* **Default Value**: `true`
* **Description**: This behavior is triggered when the device is activated. It sends a command to the target device to turn it on, indicating that the device should be in an operational state. The default value of `true` signifies that the action is intended to activate or enable the device.
2. **turnOf Behavior**
* **Type**: Action
* **Default Value**: `false`
* **Description**: This behavior is triggered when the device is deactivated. It sends a command to the target device to turn it off, indicating that the device should be in a non-operational state. The default value of `false` signifies that the action is intended to deactivate or disable the device.
##### Example with setting values for callAction function
**Temperature Adjustment Handler**
<br>
This JavaScript code demonstrates click actions for buttons that either increase or decrease the temperature value within a predefined range.
The behavior triggered by the click event updates the time series data for the temperature and ensures that the value stays within the defined limits.
The widget uses the <a href="${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/scada/scada-symbols-dev-guide/#scadasymbolcontext" target="_blank">ScadaSymbolContext API</a> to manage both temperature changes and the corresponding actions.
1. **Decrease Temperature (levelDown button)**:
* The user clicks the levelDown button to decrease the temperature. If the system is running, the temperature decreases by a step defined by `temperatureStep`, but it will never go below the `minTemperature`.
* The updateTemperatureState action is then triggered to update the time series value for temperature.
* In case of an error during the action, the temperature reverts to its previous value.
<br>
```javascript
if (ctx.values.running) {
// Retrieve the current temperature from the context values
var temperature = ctx.values.temperature;
// Retrieve the minimum allowable temperature from the context properties
var minTemperature = ctx.properties.minTemperature;
// Retrieve the step size by which the temperature should change
var step = ctx.properties.temperatureStep;
// Calculate the new temperature, ensuring it does not go below the minimum temperature
var newTemperature = Math.max(minTemperature, temperature - step);
// Set the new temperature value in the context
ctx.api.setValue('temperature', newTemperature);
// Trigger the action to update the temperature state, passing the new temperature value
ctx.api.callAction(event, 'updateTemperatureState', newTemperature, {
// In case of an error, revert to the original temperature
error: () => {
ctx.api.setValue('temperature', temperature);
}
});
}
{:copy-code}
```
<br>
2. **Increase Temperature (levelUp button)**:
* The user clicks the levelUp button to increase the temperature. If the system is running, the temperature increases by the step, but will not exceed the `maxTemperature`.
* The action `updateTemperatureState` is called to update the time series value for temperature.
* If an error occurs, the previous temperature is restored to ensure consistency.
<br>
```javascript
if (ctx.values.running) {
// Retrieve the current temperature from the context values
var temperature = ctx.values.temperature;
// Retrieve the maximum and minimum allowable temperature from the context properties
var maxTemperature = ctx.properties.maxTemperature;
var minTemperature = ctx.properties.minTemperature;
// Retrieve the step size by which the temperature should change
var step = ctx.properties.temperatureStep;
// Calculate the new temperature:
// - Add the step to the current temperature
// - Ensure it doesn't exceed the maxTemperature
// - If temperature is null/undefined, use minTemperature as the initial value
var newTemperature = temperature || minTemperature === 0 ? Math.min(maxTemperature, temperature + step) : minTemperature;
// Set the new temperature value in the context
ctx.api.setValue('temperature', newTemperature);
// Trigger the action to update the temperature state, passing the new temperature value
ctx.api.callAction(event, 'updateTemperatureState', newTemperature, {
// In case of an error, revert to the original temperature
error: () => {
ctx.api.setValue('temperature', temperature);
}
});
}
{:copy-code}
```
<br>
The `updateTemperatureState` action is triggered to update the temperature value in the system. The action is configured with the following behavior:
*Action Behavior*: 'updateTemperatureState'
* **Type**: action
* **Value Type**: double
* **Default Settings**: Add time series
* **Time Series Key**: 'temperature'
This behavior updates the time series data for the temperature key, ensuring that the new temperature value is stored and displayed correctly.

59
ui-ngx/src/assets/help/en_US/scada/tag_state_render_fn.md

@ -21,43 +21,64 @@ A JavaScript function used to render SCADA symbol element with specific tag.
##### Examples
* Change the background of the element based on the value of the “active”
**Update element state based on device activity**
```javascript
if(ctx.values.active){
element.attr({fill: ctx.properties.activeColor});
} else {
element.attr({fill: ctx.properties.inactiveColor});
}
{:copy-code}
```
This JavaScript snippet demonstrates how to dynamically change the background color of an SVG element based on the `active` status from the context.
Additionally, it enables or disables the element’s click functionality depending on the `active` status.
If the device is active, the element is given the activeColor and click actions are allowed. Otherwise, it is assigned the inactiveColor and the click action is disabled.
* Enable and disable the “On” button based on the state of the "active" (avoid or prevent click action)
<br>
```javascript
if (ctx.values.active) {
ctx.api.disable(element);
} else {
// Context values
var active = ctx.values.active; // Device connectivity status
// Colors from context properties
var activeColor = ctx.properties.activeColor || '#4CAF50'; // Color for enabled state
var inactiveColor = ctx.properties.inactiveColor || '#A1ADB1'; // Color for disabled state
// Check if the device is active
if (active) {
// Set the background color to activeColor if active
element.attr({fill: activeColor});
// Enable the element to allow click actions
ctx.api.enable(element);
} else {
// Set the background color to inactiveColor if not active
element.attr({fill: inactiveColor});
// Disable the element to forbid click actions
ctx.api.disable(element);
}
{:copy-code}
```
* Smooth infinite rotation animation based on the value of the “active” with speed based on the value of the “speed”
<br>
**Smooth rotation based on activity and speed**
This JavaScript snippet creates a smooth, infinite rotation animation for an element based on the `active` status and adjusts the animation speed dynamically according to the `speed` value.
If the element is active, the animation starts or continues rotating with a speed proportional to the speed value. If inactive, the animation pauses.
<br>
```javascript
// Get the 'active' status and the current speed
var on = ctx.values.active;
var speed = ctx.values.speed ? ctx.values.speed / 60 : 1;
var animation = ctx.api.cssAnimation(element);
var animation = ctx.api.cssAnimation(element); // Retrieve any existing animation on the element
var speed = ctx.values.speed ? ctx.values.speed / 60 : 1; // Calculate speed, default to 1 if not provided
var animationDuration = 2000; // Duration for one full rotation (optional, can be adjusted)
var rotationAngle = 360; // Full rotation in degrees
if (on) {
// If active, either create a new rotation animation or adjust the existing one
if (!animation) {
animation = ctx.api.cssAnimate(element, 2000)
.rotate(360).loop().speed(speed);
animation = ctx.api.cssAnimate(element) // Create new animation if not already active
.rotate(rotationAngle) // Set rotation angle to 360 degrees
.loop() // Loop the animation infinitely
.speed(speed); // Adjust speed based on the 'speed' value from the context
} else {
animation.speed(speed).play();
animation.speed(speed).play(); // If animation exists, adjust its speed and continue playing
}
} else {
// If inactive, pause the animation
if (animation) {
animation.pause();
}

65
ui-ngx/src/assets/help/en_US/widget/lib/gateway/attributes_timeseries_expressions_fn.md

@ -0,0 +1,65 @@
### Expressions
#### JSON Path:
The expression field is used to extract data from the MQTT message. There are various available options for different parts of the messages:
- The JSONPath format can be used to extract data from the message body.
- The regular expression format can be used to extract data from the topic where the message will arrive.
- Slices can only be used in the expression fields of bytes converters.
JSONPath expressions specify the items within a JSON structure (which could be an object, array, or nested combination of both) that you want to access. These expressions can select elements from JSON data on specific criteria. Here's a basic overview of how JSONPath expressions are structured:
- `$`: The root element of the JSON document;
- `.`: Child operator used to select child elements. For example, $.store.book ;
- `[]`: Child operator used to select child elements. $['store']['book'] accesses the book array within a store object;
##### Examples:
For example, if we want to extract the device name from the following message, we can use the expression below:
MQTT message:
```
{
"sensorModelInfo": {
"sensorName": "AM-123",
"sensorType": "myDeviceType"
},
"data": {
"temp": 12.2,
"hum": 56,
"status": "ok"
}
}
{:copy-code}
```
Expression:
`${sensorModelInfo.sensorName}`
Converted data:
`AM-123`
If we want to extract all data from the message above, we can use the following expression:
`${data}`
Converted data:
`{"temp": 12.2, "hum": 56, "status": "ok"}`
Or if we want to extract specific data (for example “temperature”), you can use the following expression:
`${data.temp}`
And as a converted data we will get:
`12.2`
<br/>

42
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1211,7 +1211,7 @@
"layout-type-default": "Default",
"layout-type-scada": "SCADA",
"layout-type-divider": "Divider",
"layout-settings-type": "Layout settings: {{ type }}",
"layout-settings-type": "Layout settings: {{ type }} breakpoint",
"columns-count": "Columns count",
"columns-count-required": "Columns count is required.",
"min-columns-count-message": "Only 10 minimum column count is allowed.",
@ -1251,7 +1251,7 @@
"display-dashboards-selection": "Display dashboards selection",
"display-entities-selection": "Display entities selection",
"display-filters": "Display filters",
"display-dashboard-timewindow": "Display timewindow",
"display-dashboard-timewindow": "Display time window",
"display-dashboard-export": "Display export",
"display-update-dashboard-image": "Display update dashboard image",
"dashboard-logo-settings": "Dashboard logo settings",
@ -2955,7 +2955,6 @@
"configuration-delete-dialog-confirm": "Turn Off",
"connector-duplicate-name": "Connector with such name already exists.",
"connector-side": "Connector side",
"client-communication-type": "Client communication type",
"payload-type": "Payload type",
"platform-side": "Platform side",
"JSON": "JSON",
@ -3278,7 +3277,7 @@
},
"select-connector": "Select connector to display config",
"send-change-data": "Send data only on change",
"send-data-TB": "Send data to ThingsBoard",
"send-data-to-platform": "Send data to platform",
"send-data-on-change": "Send data only on change",
"send-change-data-hint": "The values will be saved to the database only if they are different from the corresponding values in the previous converted message. This functionality applies to both attributes and time series in the converter output.",
"server": "Server",
@ -4327,6 +4326,8 @@
"warning": "Warning",
"webhook-url": "Webhook URL",
"webhook-url-required": "Webhook URL is required",
"workflow-url": "Workflow URL",
"workflow-url-required": "Workflow URL is required",
"channel-name": "Channel name",
"channel-name-required": "Channel name is required",
"settings": {
@ -4739,10 +4740,10 @@
"queue-singleton-hint": "Select a queue for message forwarding in multi-instance environments. 'Main' queue is used by default."
},
"timezone": {
"timezone": "Timezone",
"select-timezone": "Select timezone",
"no-timezones-matching": "No timezones matching '{{timezone}}' were found.",
"timezone-required": "Timezone is required.",
"timezone": "Time zone",
"select-timezone": "Select time zone",
"no-timezones-matching": "No time zones matching '{{timezone}}' were found.",
"timezone-required": "Time zone is required.",
"browser-time": "Browser Time"
},
"queue": {
@ -5157,8 +5158,8 @@
"days": "Days"
},
"timewindow": {
"timewindow": "Timewindow",
"timewindow-settings": "Timewindow settings",
"timewindow": "Time window",
"timewindow-settings": "Time window settings",
"years": "{ years, plural, =1 { year } other {# years } }",
"years-short": "{{ years }}y",
"months": "{ months, plural, =1 { month } other {# months } }",
@ -5186,7 +5187,7 @@
"history": "History",
"last-prefix": "last",
"period": "from {{ startTime }} to {{ endTime }}",
"edit": "Edit timewindow",
"edit": "Edit time window",
"date-range": "Date range",
"for-all-time": "For all time",
"last": "Last",
@ -5196,7 +5197,7 @@
"just-now": "Just now",
"just-now-lower": "just now",
"ago": "ago",
"style": "Timewindow style",
"style": "Time window style",
"icon": "Icon",
"icon-position": "Icon position",
"icon-position-left": "Left",
@ -5207,7 +5208,14 @@
"preview": "Preview",
"relative": "Relative",
"range": "Range",
"hide-timewindow-section": "Hide timewindow section from end-users",
"hide-timewindow-section": "Hide time window section from end-users",
"hide-last-interval": "Hide last interval from end-users",
"hide-relative-interval": "Hide relative interval from end-users",
"hide-fixed-interval": "Hide fixed interval from end-users",
"hide-aggregation": "Hide aggregation from end-users",
"hide-group-interval": "Hide grouping interval from end-users",
"hide-max-values": "Hide max values from end-users",
"hide-timezone": "Hide time zone from end-users",
"disable-custom-interval": "Disable custom interval selection"
},
"tooltip": {
@ -6033,10 +6041,10 @@
"units-short": "Units",
"decimals-short": "Decimals",
"decimals-suffix": "decimals",
"timewindow": "Timewindow",
"use-dashboard-timewindow": "Use dashboard timewindow",
"use-widget-timewindow": "Use widget timewindow",
"display-timewindow": "Display timewindow",
"timewindow": "Time window",
"use-dashboard-timewindow": "Use dashboard time window",
"use-widget-timewindow": "Use widget time window",
"display-timewindow": "Display time window",
"legend": "Legend",
"display-legend": "Display legend",
"datasources": "Datasources",

16
ui-ngx/src/assets/metadata/connector-default-configs/modbus.json

@ -20,13 +20,21 @@
"connectAttemptTimeMs": 5000,
"connectAttemptCount": 5,
"waitAfterFailedAttemptsMs": 300000,
"reportStrategy": {
"type": "ON_REPORT_PERIOD",
"reportPeriod": 30000
},
"attributes": [
{
"tag": "string_read",
"type": "string",
"functionCode": 4,
"objectsCount": 4,
"address": 1
"address": 1,
"reportStrategy": {
"type": "ON_REPORT_PERIOD",
"reportPeriod": 15000
}
},
{
"tag": "bits_read",
@ -86,7 +94,11 @@
"type": "8uint",
"functionCode": 4,
"objectsCount": 1,
"address": 17
"address": 17,
"reportStrategy": {
"type": "ON_REPORT_PERIOD",
"reportPeriod": 15000
}
},
{
"tag": "16uint_read",

2
ui-ngx/src/assets/metadata/material-icons.json

File diff suppressed because one or more lines are too long

2
ui-ngx/yarn.lock

@ -5434,7 +5434,7 @@ ecc-jsbn@~0.1.1:
"echarts@https://github.com/thingsboard/echarts/archive/5.5.0-TB.tar.gz":
version "5.5.0-TB"
resolved "https://github.com/thingsboard/echarts/archive/5.5.0-TB.tar.gz#d1017728576b3fd65532eb17e503d1349f7feaed"
resolved "https://github.com/thingsboard/echarts/archive/5.5.0-TB.tar.gz#0b707b5cd2ae4699e9ced8b07ca49cb70189ae2a"
dependencies:
tslib "2.3.0"
zrender "5.5.0"

Loading…
Cancel
Save